Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1833f1a021 | ||
|
|
cc6d46a838 | ||
|
|
8cb026b1be | ||
|
|
cec7417582 | ||
|
|
62bb47a881 | ||
|
|
e38f523a45 | ||
|
|
30550dd189 | ||
|
|
154040f9fb | ||
|
|
365d51f52f | ||
|
|
305ae2f699 | ||
|
|
d6e9b3b7cf | ||
|
|
2b94633212 | ||
|
|
846f8c02b4 | ||
|
|
6e1b5b7a0c | ||
|
|
40cb705494 | ||
|
|
e0b750dbcd | ||
|
|
0a63ffba63 | ||
|
|
5a76fab4ae | ||
|
|
85f05c326b | ||
|
|
b8cabdde97 | ||
|
|
83ce9ed960 | ||
|
|
c2fbf81f1d | ||
|
|
c5bd30e677 | ||
|
|
5d187fcb02 | ||
|
|
39d934ee71 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
- OS: [e.g. Ubuntu 22.04]
|
- OS: [e.g. Ubuntu 22.04]
|
||||||
- Strix Version or Commit: [e.g. 0.1.18]
|
- Strix Version or Commit: [e.g. 0.1.18]
|
||||||
- Python Version: [e.g. 3.12]
|
- Python Version: [e.g. 3.12]
|
||||||
- LLM Used: [e.g. GPT-5, Claude Sonnet 4]
|
- LLM Used: [e.g. GPT-5, Claude Sonnet 4.6]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Thank you for your interest in contributing to Strix! This guide will help you g
|
|||||||
|
|
||||||
3. **Configure your LLM provider**
|
3. **Configure your LLM provider**
|
||||||
```bash
|
```bash
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a>
|
<a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a>
|
||||||
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-3b82f6?style=for-the-badge&logoColor=white" alt="Website"></a>
|
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-f0f0f0?style=for-the-badge&logoColor=000000" alt="Website"></a>
|
||||||
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/badge/PyPI-strix--agent-f59e0b?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
|
[](https://discord.gg/strix-ai)
|
||||||
|
|
||||||
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
||||||
@@ -61,7 +61,7 @@ Strix are autonomous AI agents that act just like real hackers - they run your c
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## 🎯 Use Cases
|
## Use Cases
|
||||||
|
|
||||||
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
|
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
|
||||||
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
|
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
|
||||||
@@ -72,7 +72,9 @@ Strix are autonomous AI agents that act just like real hackers - they run your c
|
|||||||
|
|
||||||
**Prerequisites:**
|
**Prerequisites:**
|
||||||
- Docker (running)
|
- Docker (running)
|
||||||
- An LLM provider key (e.g. [get OpenAI API key](https://platform.openai.com/api-keys) or use a local LLM)
|
- An LLM API key:
|
||||||
|
- Any [supported provider](https://docs.strix.ai/llm-providers/overview) (OpenAI, Anthropic, Google, etc.)
|
||||||
|
- Or [Strix Router](https://models.strix.ai) — single API key for multiple providers with $10 free credit on signup
|
||||||
|
|
||||||
### Installation & First Scan
|
### Installation & First Scan
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ curl -sSL https://strix.ai/install | bash
|
|||||||
pipx install strix-agent
|
pipx install strix-agent
|
||||||
|
|
||||||
# Configure your AI provider
|
# Configure your AI provider
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6" # or "strix/claude-sonnet-4.6" via Strix Router (https://models.strix.ai)
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
|
|
||||||
# Run your first security assessment
|
# Run your first security assessment
|
||||||
@@ -94,19 +96,6 @@ strix --target ./app-directory
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
|
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
|
||||||
|
|
||||||
## Run Strix in Cloud
|
|
||||||
|
|
||||||
Want to skip the local setup, API keys, and unpredictable LLM costs? Run the hosted cloud version of Strix at **[app.strix.ai](https://strix.ai)**.
|
|
||||||
|
|
||||||
Launch a scan in just a few minutes—no setup or configuration required—and you’ll get:
|
|
||||||
|
|
||||||
- **A full pentest report** with validated findings and clear remediation steps
|
|
||||||
- **Shareable dashboards** your team can use to track fixes over time
|
|
||||||
- **CI/CD and GitHub integrations** to block risky changes before production
|
|
||||||
- **Continuous monitoring** so new vulnerabilities are caught quickly
|
|
||||||
|
|
||||||
[**Run your first pentest now →**](https://strix.ai)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
@@ -214,7 +203,7 @@ jobs:
|
|||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
|
|
||||||
# Optional
|
# Optional
|
||||||
@@ -228,8 +217,8 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
|
|||||||
|
|
||||||
**Recommended models for best results:**
|
**Recommended models for best results:**
|
||||||
|
|
||||||
|
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
|
||||||
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
|
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
|
||||||
- [Anthropic Claude Sonnet 4.5](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-5`
|
|
||||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
||||||
|
|
||||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Configure Strix using environment variables or a config file.
|
|||||||
## LLM Configuration
|
## LLM Configuration
|
||||||
|
|
||||||
<ParamField path="STRIX_LLM" type="string" required>
|
<ParamField path="STRIX_LLM" type="string" required>
|
||||||
Model name in LiteLLM format (e.g., `openai/gpt-5`, `anthropic/claude-sonnet-4-5`).
|
Model name in LiteLLM format (e.g., `anthropic/claude-sonnet-4-6`, `openai/gpt-5`).
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="LLM_API_KEY" type="string">
|
<ParamField path="LLM_API_KEY" type="string">
|
||||||
@@ -86,7 +86,7 @@ strix --target ./app --config /path/to/config.json
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"STRIX_LLM": "openai/gpt-5",
|
"STRIX_LLM": "anthropic/claude-sonnet-4-6",
|
||||||
"LLM_API_KEY": "sk-...",
|
"LLM_API_KEY": "sk-...",
|
||||||
"STRIX_REASONING_EFFORT": "high"
|
"STRIX_REASONING_EFFORT": "high"
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ strix --target ./app --config /path/to/config.json
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Required
|
# Required
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="sk-..."
|
export LLM_API_KEY="sk-..."
|
||||||
|
|
||||||
# Optional: Enable web search
|
# Optional: Enable web search
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: "Introduction"
|
|||||||
description: "Managed security testing without local setup"
|
description: "Managed security testing without local setup"
|
||||||
---
|
---
|
||||||
|
|
||||||
Skip the setup. Run Strix in the cloud at [app.usestrix.com](https://app.usestrix.com).
|
Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ Skip the setup. Run Strix in the cloud at [app.usestrix.com](https://app.usestri
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Sign up at [app.usestrix.com](https://app.usestrix.com)
|
1. Sign up at [app.strix.ai](https://app.strix.ai)
|
||||||
2. Connect your repository or enter a target URL
|
2. Connect your repository or enter a target URL
|
||||||
3. Launch your first scan
|
3. Launch your first scan
|
||||||
|
|
||||||
<Card title="Try Strix Cloud" icon="rocket" href="https://app.usestrix.com">
|
<Card title="Try Strix Cloud" icon="rocket" href="https://app.strix.ai">
|
||||||
Run your first pentest in minutes.
|
Run your first pentest in minutes.
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ description: "Contribute to Strix development"
|
|||||||
</Step>
|
</Step>
|
||||||
<Step title="Configure LLM">
|
<Step title="Configure LLM">
|
||||||
```bash
|
```bash
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"group": "LLM Providers",
|
"group": "LLM Providers",
|
||||||
"pages": [
|
"pages": [
|
||||||
"llm-providers/overview",
|
"llm-providers/overview",
|
||||||
|
"llm-providers/models",
|
||||||
"llm-providers/openai",
|
"llm-providers/openai",
|
||||||
"llm-providers/anthropic",
|
"llm-providers/anthropic",
|
||||||
"llm-providers/openrouter",
|
"llm-providers/openrouter",
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
"primary": {
|
"primary": {
|
||||||
"type": "button",
|
"type": "button",
|
||||||
"label": "Try Strix Cloud",
|
"label": "Try Strix Cloud",
|
||||||
"href": "https://app.usestrix.com"
|
"href": "https://app.strix.ai"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ Strix uses a graph of specialized agents for comprehensive security testing:
|
|||||||
curl -sSL https://strix.ai/install | bash
|
curl -sSL https://strix.ai/install | bash
|
||||||
|
|
||||||
# Configure
|
# Configure
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
|
|
||||||
# Scan
|
# Scan
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Add these secrets to your repository:
|
|||||||
|
|
||||||
| Secret | Description |
|
| Secret | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5`) |
|
| `STRIX_LLM` | Model name (e.g., `anthropic/claude-sonnet-4-6`) |
|
||||||
| `LLM_API_KEY` | API key for your LLM provider |
|
| `LLM_API_KEY` | API key for your LLM provider |
|
||||||
|
|
||||||
## Exit Codes
|
## Exit Codes
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: "Configure Strix with Claude models"
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export STRIX_LLM="anthropic/claude-sonnet-4-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="sk-ant-..."
|
export LLM_API_KEY="sk-ant-..."
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ export LLM_API_KEY="sk-ant-..."
|
|||||||
|
|
||||||
| Model | Description |
|
| Model | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `anthropic/claude-sonnet-4-5` | Best balance of intelligence and speed (recommended) |
|
| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed (recommended) |
|
||||||
| `anthropic/claude-opus-4-5` | Maximum capability for deep analysis |
|
| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
|
||||||
|
|
||||||
## Get API Key
|
## Get API Key
|
||||||
|
|
||||||
|
|||||||
80
docs/llm-providers/models.mdx
Normal file
80
docs/llm-providers/models.mdx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: "Strix Router"
|
||||||
|
description: "Access top LLMs through a single API with high rate limits and zero data retention"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix Router gives you access to the best LLMs through a single API key.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Strix Router is currently in **beta**. It's completely optional — Strix works with any [LiteLLM-compatible provider](/llm-providers/overview) using your own API keys, or with [local models](/llm-providers/local). Strix Router is just the setup we test and optimize for.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Why Use Strix Router?
|
||||||
|
|
||||||
|
- **High rate limits** — No throttling during long-running scans
|
||||||
|
- **Zero data retention** — Routes to providers with zero data retention policies enabled
|
||||||
|
- **Failover & load balancing** — Automatic fallback across providers for reliability
|
||||||
|
- **Simple setup** — One API key, one environment variable, no provider accounts needed
|
||||||
|
- **No markup** — Same token pricing as the underlying providers, no extra fees
|
||||||
|
- **$10 free credit** — Try it free on signup, no credit card required
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Get your API key at [models.strix.ai](https://models.strix.ai)
|
||||||
|
2. Set your environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_API_KEY='your-strix-api-key'
|
||||||
|
export STRIX_LLM='strix/claude-sonnet-4.6'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run a scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./your-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
### Anthropic
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| Claude Sonnet 4.6 | `strix/claude-sonnet-4.6` |
|
||||||
|
| Claude Opus 4.6 | `strix/claude-opus-4.6` |
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| GPT-5.2 | `strix/gpt-5.2` |
|
||||||
|
| GPT-5.1 | `strix/gpt-5.1` |
|
||||||
|
| GPT-5 | `strix/gpt-5` |
|
||||||
|
| GPT-5.2 Codex | `strix/gpt-5.2-codex` |
|
||||||
|
| GPT-5.1 Codex Max | `strix/gpt-5.1-codex-max` |
|
||||||
|
| GPT-5.1 Codex | `strix/gpt-5.1-codex` |
|
||||||
|
| GPT-5 Codex | `strix/gpt-5-codex` |
|
||||||
|
|
||||||
|
### Google
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| Gemini 3 Pro | `strix/gemini-3-pro-preview` |
|
||||||
|
| Gemini 3 Flash | `strix/gemini-3-flash-preview` |
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| GLM-5 | `strix/glm-5` |
|
||||||
|
| GLM-4.7 | `strix/glm-4.7` |
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
<ParamField path="LLM_API_KEY" type="string" required>
|
||||||
|
Your Strix API key from [models.strix.ai](https://models.strix.ai).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_LLM" type="string" required>
|
||||||
|
Model ID from the tables above. Must be prefixed with `strix/`.
|
||||||
|
</ParamField>
|
||||||
@@ -19,7 +19,7 @@ Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
|
|||||||
| Model | Configuration |
|
| Model | Configuration |
|
||||||
|-------|---------------|
|
|-------|---------------|
|
||||||
| GPT-5 | `openrouter/openai/gpt-5` |
|
| GPT-5 | `openrouter/openai/gpt-5` |
|
||||||
| Claude 4.5 Sonnet | `openrouter/anthropic/claude-sonnet-4.5` |
|
| Claude Sonnet 4.6 | `openrouter/anthropic/claude-sonnet-4.6` |
|
||||||
| Gemini 3 Pro | `openrouter/google/gemini-3-pro-preview` |
|
| Gemini 3 Pro | `openrouter/google/gemini-3-pro-preview` |
|
||||||
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
|
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,54 @@ description: "Configure your AI model for Strix"
|
|||||||
|
|
||||||
Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers.
|
Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers.
|
||||||
|
|
||||||
## Recommended Models
|
## Strix Router (Recommended)
|
||||||
|
|
||||||
For best results, use one of these models:
|
The fastest way to get started. [Strix Router](/llm-providers/models) gives you access to tested models with the highest rate limits and zero data retention.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="strix/claude-sonnet-4.6"
|
||||||
|
export LLM_API_KEY="your-strix-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Get your API key at [models.strix.ai](https://models.strix.ai).
|
||||||
|
|
||||||
|
## Bring Your Own Key
|
||||||
|
|
||||||
|
You can also use any LiteLLM-compatible provider with your own API keys:
|
||||||
|
|
||||||
| Model | Provider | Configuration |
|
| Model | Provider | Configuration |
|
||||||
| ----------------- | ------------- | -------------------------------- |
|
| ----------------- | ------------- | -------------------------------- |
|
||||||
|
| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
|
||||||
| GPT-5 | OpenAI | `openai/gpt-5` |
|
| GPT-5 | OpenAI | `openai/gpt-5` |
|
||||||
| Claude 4.5 Sonnet | Anthropic | `anthropic/claude-sonnet-4-5` |
|
|
||||||
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
|
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Local Models
|
||||||
|
|
||||||
|
Run models locally with [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai), or any OpenAI-compatible server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="ollama/llama4"
|
||||||
|
export LLM_API_BASE="http://localhost:11434"
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Local Models guide](/llm-providers/local) for setup instructions and recommended models.
|
||||||
|
|
||||||
## Provider Guides
|
## Provider Guides
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Strix Router" href="/llm-providers/models">
|
||||||
|
Recommended models router with high rate limits.
|
||||||
|
</Card>
|
||||||
<Card title="OpenAI" href="/llm-providers/openai">
|
<Card title="OpenAI" href="/llm-providers/openai">
|
||||||
GPT-5 and Codex models.
|
GPT-5 and Codex models.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Anthropic" href="/llm-providers/anthropic">
|
<Card title="Anthropic" href="/llm-providers/anthropic">
|
||||||
Claude 4.5 Sonnet, Opus, and Haiku.
|
Claude Sonnet 4.6, Opus, and Haiku.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="OpenRouter" href="/llm-providers/openrouter">
|
<Card title="OpenRouter" href="/llm-providers/openrouter">
|
||||||
Access 100+ models through a single API.
|
Access 100+ models through a single API.
|
||||||
@@ -38,7 +61,7 @@ export LLM_API_KEY="your-api-key"
|
|||||||
Gemini 3 models via Google Cloud.
|
Gemini 3 models via Google Cloud.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
|
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
|
||||||
Claude 4.5 and Titan models via AWS.
|
Claude and Titan models via AWS.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Azure OpenAI" href="/llm-providers/azure">
|
<Card title="Azure OpenAI" href="/llm-providers/azure">
|
||||||
GPT-5 via Azure.
|
GPT-5 via Azure.
|
||||||
@@ -53,8 +76,8 @@ export LLM_API_KEY="your-api-key"
|
|||||||
Use LiteLLM's `provider/model-name` format:
|
Use LiteLLM's `provider/model-name` format:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
anthropic/claude-sonnet-4-6
|
||||||
openai/gpt-5
|
openai/gpt-5
|
||||||
anthropic/claude-sonnet-4-5
|
|
||||||
vertex_ai/gemini-3-pro-preview
|
vertex_ai/gemini-3-pro-preview
|
||||||
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
|
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
|
||||||
ollama/llama4
|
ollama/llama4
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: "Install Strix and run your first security scan"
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Docker (running)
|
- Docker (running)
|
||||||
- An LLM provider API key (OpenAI, Anthropic, or local model)
|
- An LLM API key — use [Strix Router](/llm-providers/models) for the easiest setup, or bring your own key from any [supported provider](/llm-providers/overview)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -27,13 +27,23 @@ description: "Install Strix and run your first security scan"
|
|||||||
|
|
||||||
Set your LLM provider:
|
Set your LLM provider:
|
||||||
|
|
||||||
```bash
|
<Tabs>
|
||||||
export STRIX_LLM="openai/gpt-5"
|
<Tab title="Strix Router">
|
||||||
export LLM_API_KEY="your-api-key"
|
```bash
|
||||||
```
|
export STRIX_LLM="strix/claude-sonnet-4.6"
|
||||||
|
export LLM_API_KEY="your-strix-api-key"
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Bring Your Own Key">
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||||
|
export LLM_API_KEY="your-api-key"
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
For best results, use `openai/gpt-5`, `anthropic/claude-sonnet-4-5`, or `vertex_ai/gemini-3-pro-preview`.
|
For best results, use `strix/claude-sonnet-4.6`, `strix/claude-opus-4.6`, or `strix/gpt-5.2`.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
## Run Your First Scan
|
## Run Your First Scan
|
||||||
|
|||||||
676
poetry.lock
generated
676
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
description = "Open-source AI Hackers for your apps"
|
description = "Open-source AI Hackers for your apps"
|
||||||
authors = ["Strix <hi@usestrix.com>"]
|
authors = ["Strix <hi@usestrix.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -47,7 +47,7 @@ strix = "strix.interface.main:main"
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.12"
|
||||||
# Core CLI dependencies
|
# Core CLI dependencies
|
||||||
litellm = { version = "~1.80.7", extras = ["proxy"] }
|
litellm = { version = "~1.81.1", extras = ["proxy"] }
|
||||||
tenacity = "^9.0.0"
|
tenacity = "^9.0.0"
|
||||||
pydantic = {extras = ["email"], version = "^2.11.3"}
|
pydantic = {extras = ["email"], version = "^2.11.3"}
|
||||||
rich = "*"
|
rich = "*"
|
||||||
|
|||||||
@@ -335,14 +335,18 @@ echo -e "${MUTED} AI Penetration Testing Agent${NC}"
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${MUTED}To get started:${NC}"
|
echo -e "${MUTED}To get started:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1.${NC} Set your LLM provider:"
|
echo -e " ${CYAN}1.${NC} Get your Strix API key:"
|
||||||
echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5'${NC}"
|
echo -e " ${MUTED}https://models.strix.ai${NC}"
|
||||||
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}2.${NC} Run a penetration test:"
|
echo -e " ${CYAN}2.${NC} Set your environment:"
|
||||||
|
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
|
||||||
|
echo -e " ${MUTED}export STRIX_LLM='strix/claude-sonnet-4.6'${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}3.${NC} Run a penetration test:"
|
||||||
echo -e " ${MUTED}strix --target https://example.com${NC}"
|
echo -e " ${MUTED}strix --target https://example.com${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
||||||
|
echo -e "${MUTED}Supported models ${NC}https://docs.strix.ai/llm-providers/overview"
|
||||||
echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai"
|
echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ class AgentState(BaseModel):
|
|||||||
self.iteration += 1
|
self.iteration += 1
|
||||||
self.last_updated = datetime.now(UTC).isoformat()
|
self.last_updated = datetime.now(UTC).isoformat()
|
||||||
|
|
||||||
def add_message(self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None) -> None:
|
def add_message(
|
||||||
|
self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None
|
||||||
|
) -> None:
|
||||||
message = {"role": role, "content": content}
|
message = {"role": role, "content": content}
|
||||||
if thinking_blocks:
|
if thinking_blocks:
|
||||||
message["thinking_blocks"] = thinking_blocks
|
message["thinking_blocks"] = thinking_blocks
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
STRIX_API_BASE = "https://models.strix.ai/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration Manager for Strix."""
|
"""Configuration Manager for Strix."""
|
||||||
|
|
||||||
@@ -177,3 +180,30 @@ def apply_saved_config(force: bool = False) -> dict[str, str]:
|
|||||||
|
|
||||||
def save_current_config() -> bool:
|
def save_current_config() -> bool:
|
||||||
return Config.save_current()
|
return Config.save_current()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_llm_config() -> tuple[str | None, str | None, str | None]:
|
||||||
|
"""Resolve LLM model, api_key, and api_base based on STRIX_LLM prefix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (model_name, api_key, api_base)
|
||||||
|
"""
|
||||||
|
model = Config.get("strix_llm")
|
||||||
|
if not model:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
api_key = Config.get("llm_api_key")
|
||||||
|
|
||||||
|
if model.startswith("strix/"):
|
||||||
|
model_name = "openai/" + model[6:]
|
||||||
|
api_base: str | None = STRIX_API_BASE
|
||||||
|
else:
|
||||||
|
model_name = model
|
||||||
|
api_base = (
|
||||||
|
Config.get("llm_api_base")
|
||||||
|
or Config.get("openai_api_base")
|
||||||
|
or Config.get("litellm_base_url")
|
||||||
|
or Config.get("ollama_api_base")
|
||||||
|
)
|
||||||
|
|
||||||
|
return model_name, api_key, api_base
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ Screen {
|
|||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screen--selection {
|
||||||
|
background: #2d3d2f;
|
||||||
|
color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToastRack {
|
||||||
|
dock: top;
|
||||||
|
align: right top;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast {
|
||||||
|
width: 25;
|
||||||
|
background: #000000;
|
||||||
|
border-left: outer #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.-information .toast--title {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
#splash_screen {
|
#splash_screen {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -51,10 +51,13 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
missing_required_vars = []
|
missing_required_vars = []
|
||||||
missing_optional_vars = []
|
missing_optional_vars = []
|
||||||
|
|
||||||
if not Config.get("strix_llm"):
|
strix_llm = Config.get("strix_llm")
|
||||||
|
uses_strix_models = strix_llm and strix_llm.startswith("strix/")
|
||||||
|
|
||||||
|
if not strix_llm:
|
||||||
missing_required_vars.append("STRIX_LLM")
|
missing_required_vars.append("STRIX_LLM")
|
||||||
|
|
||||||
has_base_url = any(
|
has_base_url = uses_strix_models or any(
|
||||||
[
|
[
|
||||||
Config.get("llm_api_base"),
|
Config.get("llm_api_base"),
|
||||||
Config.get("openai_api_base"),
|
Config.get("openai_api_base"),
|
||||||
@@ -96,7 +99,7 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
error_text.append("• ", style="white")
|
error_text.append("• ", style="white")
|
||||||
error_text.append("STRIX_LLM", style="bold cyan")
|
error_text.append("STRIX_LLM", style="bold cyan")
|
||||||
error_text.append(
|
error_text.append(
|
||||||
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
" - Model name to use with litellm (e.g., 'anthropic/claude-sonnet-4-6')\n",
|
||||||
style="white",
|
style="white",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,7 +138,10 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
)
|
)
|
||||||
|
|
||||||
error_text.append("\nExample setup:\n", style="white")
|
error_text.append("\nExample setup:\n", style="white")
|
||||||
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
if uses_strix_models:
|
||||||
|
error_text.append("export STRIX_LLM='strix/claude-sonnet-4.6'\n", style="dim white")
|
||||||
|
else:
|
||||||
|
error_text.append("export STRIX_LLM='anthropic/claude-sonnet-4-6'\n", style="dim white")
|
||||||
|
|
||||||
if missing_optional_vars:
|
if missing_optional_vars:
|
||||||
for var in missing_optional_vars:
|
for var in missing_optional_vars:
|
||||||
@@ -198,17 +204,12 @@ def check_docker_installed() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def warm_up_llm() -> None:
|
async def warm_up_llm() -> None:
|
||||||
|
from strix.config.config import resolve_llm_config
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_name = Config.get("strix_llm")
|
model_name, api_key, api_base = resolve_llm_config()
|
||||||
api_key = Config.get("llm_api_key")
|
|
||||||
api_base = (
|
|
||||||
Config.get("llm_api_base")
|
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
)
|
|
||||||
|
|
||||||
test_messages = [
|
test_messages = [
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from rich.padding import Padding
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ from .registry import register_tool_renderer
|
|||||||
|
|
||||||
|
|
||||||
FIELD_STYLE = "bold #4ade80"
|
FIELD_STYLE = "bold #4ade80"
|
||||||
BG_COLOR = "#141414"
|
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -58,7 +56,10 @@ class FinishScanRenderer(BaseToolRenderer):
|
|||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
text.append("Generating final report...", style="dim")
|
text.append("Generating final report...", style="dim")
|
||||||
|
|
||||||
padded = Padding(text, 2, style=f"on {BG_COLOR}")
|
padded = Text()
|
||||||
|
padded.append("\n\n")
|
||||||
|
padded.append_text(text)
|
||||||
|
padded.append("\n\n")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(padded, classes=css_classes)
|
return Static(padded, classes=css_classes)
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ from typing import Any, ClassVar
|
|||||||
|
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
from rich.padding import Padding
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
from strix.tools.reporting.reporting_actions import (
|
||||||
|
parse_code_locations_xml,
|
||||||
|
parse_cvss_xml,
|
||||||
|
)
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
@@ -18,7 +22,13 @@ def _get_style_colors() -> dict[Any, str]:
|
|||||||
|
|
||||||
|
|
||||||
FIELD_STYLE = "bold #4ade80"
|
FIELD_STYLE = "bold #4ade80"
|
||||||
BG_COLOR = "#141414"
|
DIM_STYLE = "dim"
|
||||||
|
FILE_STYLE = "bold #60a5fa"
|
||||||
|
LINE_STYLE = "#facc15"
|
||||||
|
LABEL_STYLE = "italic #a1a1aa"
|
||||||
|
CODE_STYLE = "#e2e8f0"
|
||||||
|
BEFORE_STYLE = "#ef4444"
|
||||||
|
AFTER_STYLE = "#22c55e"
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -82,18 +92,13 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
poc_script_code = args.get("poc_script_code", "")
|
poc_script_code = args.get("poc_script_code", "")
|
||||||
remediation_steps = args.get("remediation_steps", "")
|
remediation_steps = args.get("remediation_steps", "")
|
||||||
|
|
||||||
attack_vector = args.get("attack_vector", "")
|
cvss_breakdown_xml = args.get("cvss_breakdown", "")
|
||||||
attack_complexity = args.get("attack_complexity", "")
|
code_locations_xml = args.get("code_locations", "")
|
||||||
privileges_required = args.get("privileges_required", "")
|
|
||||||
user_interaction = args.get("user_interaction", "")
|
|
||||||
scope = args.get("scope", "")
|
|
||||||
confidentiality = args.get("confidentiality", "")
|
|
||||||
integrity = args.get("integrity", "")
|
|
||||||
availability = args.get("availability", "")
|
|
||||||
|
|
||||||
endpoint = args.get("endpoint", "")
|
endpoint = args.get("endpoint", "")
|
||||||
method = args.get("method", "")
|
method = args.get("method", "")
|
||||||
cve = args.get("cve", "")
|
cve = args.get("cve", "")
|
||||||
|
cwe = args.get("cwe", "")
|
||||||
|
|
||||||
severity = ""
|
severity = ""
|
||||||
cvss_score = None
|
cvss_score = None
|
||||||
@@ -142,38 +147,30 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("CVE: ", style=FIELD_STYLE)
|
text.append("CVE: ", style=FIELD_STYLE)
|
||||||
text.append(cve)
|
text.append(cve)
|
||||||
|
|
||||||
if any(
|
if cwe:
|
||||||
[
|
text.append("\n\n")
|
||||||
attack_vector,
|
text.append("CWE: ", style=FIELD_STYLE)
|
||||||
attack_complexity,
|
text.append(cwe)
|
||||||
privileges_required,
|
|
||||||
user_interaction,
|
parsed_cvss = parse_cvss_xml(cvss_breakdown_xml) if cvss_breakdown_xml else None
|
||||||
scope,
|
if parsed_cvss:
|
||||||
confidentiality,
|
|
||||||
integrity,
|
|
||||||
availability,
|
|
||||||
]
|
|
||||||
):
|
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
cvss_parts = []
|
cvss_parts = []
|
||||||
if attack_vector:
|
for key, prefix in [
|
||||||
cvss_parts.append(f"AV:{attack_vector}")
|
("attack_vector", "AV"),
|
||||||
if attack_complexity:
|
("attack_complexity", "AC"),
|
||||||
cvss_parts.append(f"AC:{attack_complexity}")
|
("privileges_required", "PR"),
|
||||||
if privileges_required:
|
("user_interaction", "UI"),
|
||||||
cvss_parts.append(f"PR:{privileges_required}")
|
("scope", "S"),
|
||||||
if user_interaction:
|
("confidentiality", "C"),
|
||||||
cvss_parts.append(f"UI:{user_interaction}")
|
("integrity", "I"),
|
||||||
if scope:
|
("availability", "A"),
|
||||||
cvss_parts.append(f"S:{scope}")
|
]:
|
||||||
if confidentiality:
|
val = parsed_cvss.get(key)
|
||||||
cvss_parts.append(f"C:{confidentiality}")
|
if val:
|
||||||
if integrity:
|
cvss_parts.append(f"{prefix}:{val}")
|
||||||
cvss_parts.append(f"I:{integrity}")
|
|
||||||
if availability:
|
|
||||||
cvss_parts.append(f"A:{availability}")
|
|
||||||
text.append("CVSS Vector: ", style=FIELD_STYLE)
|
text.append("CVSS Vector: ", style=FIELD_STYLE)
|
||||||
text.append("/".join(cvss_parts), style="dim")
|
text.append("/".join(cvss_parts), style=DIM_STYLE)
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
@@ -193,6 +190,40 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("\n")
|
text.append("\n")
|
||||||
text.append(technical_analysis)
|
text.append(technical_analysis)
|
||||||
|
|
||||||
|
parsed_locations = (
|
||||||
|
parse_code_locations_xml(code_locations_xml) if code_locations_xml else None
|
||||||
|
)
|
||||||
|
if parsed_locations:
|
||||||
|
text.append("\n\n")
|
||||||
|
text.append("Code Locations", style=FIELD_STYLE)
|
||||||
|
for i, loc in enumerate(parsed_locations):
|
||||||
|
text.append("\n\n")
|
||||||
|
text.append(f" Location {i + 1}: ", style=DIM_STYLE)
|
||||||
|
text.append(loc.get("file", "unknown"), style=FILE_STYLE)
|
||||||
|
start = loc.get("start_line")
|
||||||
|
end = loc.get("end_line")
|
||||||
|
if start is not None:
|
||||||
|
if end and end != start:
|
||||||
|
text.append(f":{start}-{end}", style=LINE_STYLE)
|
||||||
|
else:
|
||||||
|
text.append(f":{start}", style=LINE_STYLE)
|
||||||
|
if loc.get("label"):
|
||||||
|
text.append(f"\n {loc['label']}", style=LABEL_STYLE)
|
||||||
|
if loc.get("snippet"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append(loc["snippet"], style=CODE_STYLE)
|
||||||
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("Fix:", style=DIM_STYLE)
|
||||||
|
if loc.get("fix_before"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("- ", style=BEFORE_STYLE)
|
||||||
|
text.append(loc["fix_before"], style=BEFORE_STYLE)
|
||||||
|
if loc.get("fix_after"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("+ ", style=AFTER_STYLE)
|
||||||
|
text.append(loc["fix_after"], style=AFTER_STYLE)
|
||||||
|
|
||||||
if poc_description:
|
if poc_description:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
text.append("PoC Description", style=FIELD_STYLE)
|
text.append("PoC Description", style=FIELD_STYLE)
|
||||||
@@ -215,7 +246,10 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
text.append("Creating report...", style="dim")
|
text.append("Creating report...", style="dim")
|
||||||
|
|
||||||
padded = Padding(text, 2, style=f"on {BG_COLOR}")
|
padded = Text()
|
||||||
|
padded.append("\n\n")
|
||||||
|
padded.append_text(text)
|
||||||
|
padded.append("\n\n")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(padded, classes=css_classes)
|
return Static(padded, classes=css_classes)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ from strix.llm.config import LLMConfig
|
|||||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_package_version() -> str:
|
def get_package_version() -> str:
|
||||||
try:
|
try:
|
||||||
return pkg_version("strix-agent")
|
return pkg_version("strix-agent")
|
||||||
@@ -91,6 +94,7 @@ class ChatTextArea(TextArea): # type: ignore[misc]
|
|||||||
|
|
||||||
|
|
||||||
class SplashScreen(Static): # type: ignore[misc]
|
class SplashScreen(Static): # type: ignore[misc]
|
||||||
|
ALLOW_SELECT = False
|
||||||
PRIMARY_GREEN = "#22c55e"
|
PRIMARY_GREEN = "#22c55e"
|
||||||
BANNER = (
|
BANNER = (
|
||||||
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
|
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
|
||||||
@@ -527,16 +531,30 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
|||||||
lines.append("```")
|
lines.append("```")
|
||||||
|
|
||||||
# Code Analysis
|
# Code Analysis
|
||||||
if vuln.get("code_file") or vuln.get("code_diff"):
|
if vuln.get("code_locations"):
|
||||||
lines.extend(["", "## Code Analysis", ""])
|
lines.extend(["", "## Code Analysis", ""])
|
||||||
if vuln.get("code_file"):
|
for i, loc in enumerate(vuln["code_locations"]):
|
||||||
lines.append(f"**File:** {vuln['code_file']}")
|
file_ref = loc.get("file", "unknown")
|
||||||
|
line_ref = ""
|
||||||
|
if loc.get("start_line") is not None:
|
||||||
|
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
|
||||||
|
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
|
||||||
|
else:
|
||||||
|
line_ref = f" (line {loc['start_line']})"
|
||||||
|
lines.append(f"**Location {i + 1}:** `{file_ref}`{line_ref}")
|
||||||
|
if loc.get("label"):
|
||||||
|
lines.append(f" {loc['label']}")
|
||||||
|
if loc.get("snippet"):
|
||||||
|
lines.append(f"```\n{loc['snippet']}\n```")
|
||||||
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
|
lines.append("**Suggested Fix:**")
|
||||||
|
lines.append("```diff")
|
||||||
|
if loc.get("fix_before"):
|
||||||
|
lines.extend(f"- {line}" for line in loc["fix_before"].splitlines())
|
||||||
|
if loc.get("fix_after"):
|
||||||
|
lines.extend(f"+ {line}" for line in loc["fix_after"].splitlines())
|
||||||
|
lines.append("```")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if vuln.get("code_diff"):
|
|
||||||
lines.append("**Changes:**")
|
|
||||||
lines.append("```diff")
|
|
||||||
lines.append(vuln["code_diff"])
|
|
||||||
lines.append("```")
|
|
||||||
|
|
||||||
# Remediation
|
# Remediation
|
||||||
if vuln.get("remediation_steps"):
|
if vuln.get("remediation_steps"):
|
||||||
@@ -667,6 +685,7 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
|
|||||||
|
|
||||||
class StrixTUIApp(App): # type: ignore[misc]
|
class StrixTUIApp(App): # type: ignore[misc]
|
||||||
CSS_PATH = "assets/tui_styles.tcss"
|
CSS_PATH = "assets/tui_styles.tcss"
|
||||||
|
ALLOW_SELECT = True
|
||||||
|
|
||||||
SIDEBAR_MIN_WIDTH = 140
|
SIDEBAR_MIN_WIDTH = 140
|
||||||
|
|
||||||
@@ -783,13 +802,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
chat_history.can_focus = True
|
chat_history.can_focus = True
|
||||||
|
|
||||||
status_text = Static("", id="status_text")
|
status_text = Static("", id="status_text")
|
||||||
|
status_text.ALLOW_SELECT = False
|
||||||
keymap_indicator = Static("", id="keymap_indicator")
|
keymap_indicator = Static("", id="keymap_indicator")
|
||||||
|
keymap_indicator.ALLOW_SELECT = False
|
||||||
|
|
||||||
agent_status_display = Horizontal(
|
agent_status_display = Horizontal(
|
||||||
status_text, keymap_indicator, id="agent_status_display", classes="hidden"
|
status_text, keymap_indicator, id="agent_status_display", classes="hidden"
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_prompt = Static("> ", id="chat_prompt")
|
chat_prompt = Static("> ", id="chat_prompt")
|
||||||
|
chat_prompt.ALLOW_SELECT = False
|
||||||
chat_input = ChatTextArea(
|
chat_input = ChatTextArea(
|
||||||
"",
|
"",
|
||||||
id="chat_input",
|
id="chat_input",
|
||||||
@@ -807,6 +829,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
agents_tree.guide_style = "dashed"
|
agents_tree.guide_style = "dashed"
|
||||||
|
|
||||||
stats_display = Static("", id="stats_display")
|
stats_display = Static("", id="stats_display")
|
||||||
|
stats_display.ALLOW_SELECT = False
|
||||||
|
|
||||||
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
||||||
|
|
||||||
@@ -1005,6 +1028,33 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
text.append(message)
|
text.append(message)
|
||||||
return text, f"chat-placeholder {placeholder_class}"
|
return text, f"chat-placeholder {placeholder_class}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _merge_renderables(renderables: list[Any]) -> Text:
|
||||||
|
"""Merge renderables into a single Text for mouse text selection support."""
|
||||||
|
combined = Text()
|
||||||
|
for i, item in enumerate(renderables):
|
||||||
|
if i > 0:
|
||||||
|
combined.append("\n")
|
||||||
|
StrixTUIApp._append_renderable(combined, item)
|
||||||
|
return combined
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _append_renderable(combined: Text, item: Any) -> None:
|
||||||
|
"""Recursively append a renderable's text content to a combined Text."""
|
||||||
|
if isinstance(item, Text):
|
||||||
|
combined.append_text(item)
|
||||||
|
elif isinstance(item, Group):
|
||||||
|
for j, sub in enumerate(item.renderables):
|
||||||
|
if j > 0:
|
||||||
|
combined.append("\n")
|
||||||
|
StrixTUIApp._append_renderable(combined, sub)
|
||||||
|
else:
|
||||||
|
inner = getattr(item, "renderable", None)
|
||||||
|
if inner is not None:
|
||||||
|
StrixTUIApp._append_renderable(combined, inner)
|
||||||
|
else:
|
||||||
|
combined.append(str(item))
|
||||||
|
|
||||||
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
|
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
|
||||||
renderables: list[Any] = []
|
renderables: list[Any] = []
|
||||||
|
|
||||||
@@ -1036,10 +1086,10 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
if not renderables:
|
if not renderables:
|
||||||
return Text()
|
return Text()
|
||||||
|
|
||||||
if len(renderables) == 1:
|
if len(renderables) == 1 and isinstance(renderables[0], Text):
|
||||||
return renderables[0]
|
return renderables[0]
|
||||||
|
|
||||||
return Group(*renderables)
|
return self._merge_renderables(renderables)
|
||||||
|
|
||||||
def _render_streaming_content(self, content: str, agent_id: str | None = None) -> Any:
|
def _render_streaming_content(self, content: str, agent_id: str | None = None) -> Any:
|
||||||
cache_key = agent_id or self.selected_agent_id or ""
|
cache_key = agent_id or self.selected_agent_id or ""
|
||||||
@@ -1072,10 +1122,10 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
if not renderables:
|
if not renderables:
|
||||||
result = Text()
|
result = Text()
|
||||||
elif len(renderables) == 1:
|
elif len(renderables) == 1 and isinstance(renderables[0], Text):
|
||||||
result = renderables[0]
|
result = renderables[0]
|
||||||
else:
|
else:
|
||||||
result = Group(*renderables)
|
result = self._merge_renderables(renderables)
|
||||||
|
|
||||||
self._streaming_render_cache[cache_key] = (content_len, result)
|
self._streaming_render_cache[cache_key] = (content_len, result)
|
||||||
return result
|
return result
|
||||||
@@ -1622,7 +1672,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
interrupted_text.append("\n")
|
interrupted_text.append("\n")
|
||||||
interrupted_text.append("⚠ ", style="yellow")
|
interrupted_text.append("⚠ ", style="yellow")
|
||||||
interrupted_text.append("Interrupted by user", style="yellow dim")
|
interrupted_text.append("Interrupted by user", style="yellow dim")
|
||||||
return Group(streaming_result, interrupted_text)
|
return self._merge_renderables([streaming_result, interrupted_text])
|
||||||
|
|
||||||
return AgentMessageRenderer.render_simple(content)
|
return AgentMessageRenderer.render_simple(content)
|
||||||
|
|
||||||
@@ -1931,6 +1981,92 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
sidebar.remove_class("-hidden")
|
sidebar.remove_class("-hidden")
|
||||||
chat_area.remove_class("-full-width")
|
chat_area.remove_class("-full-width")
|
||||||
|
|
||||||
|
def on_mouse_up(self, _event: events.MouseUp) -> None:
|
||||||
|
self.set_timer(0.05, self._auto_copy_selection)
|
||||||
|
|
||||||
|
_ICON_PREFIXES: ClassVar[tuple[str, ...]] = (
|
||||||
|
"🐞 ",
|
||||||
|
"🌐 ",
|
||||||
|
"📋 ",
|
||||||
|
"🧠 ",
|
||||||
|
"◆ ",
|
||||||
|
"◇ ",
|
||||||
|
"◈ ",
|
||||||
|
"→ ",
|
||||||
|
"○ ",
|
||||||
|
"● ",
|
||||||
|
"✓ ",
|
||||||
|
"✗ ",
|
||||||
|
"⚠ ",
|
||||||
|
"▍ ",
|
||||||
|
"▍",
|
||||||
|
"┃ ",
|
||||||
|
"• ",
|
||||||
|
">_ ",
|
||||||
|
"</> ",
|
||||||
|
"<~> ",
|
||||||
|
"[ ] ",
|
||||||
|
"[~] ",
|
||||||
|
"[•] ",
|
||||||
|
)
|
||||||
|
|
||||||
|
_DECORATIVE_LINES: ClassVar[frozenset[str]] = frozenset(
|
||||||
|
{
|
||||||
|
"● In progress...",
|
||||||
|
"✓ Done",
|
||||||
|
"✗ Failed",
|
||||||
|
"✗ Error",
|
||||||
|
"○ Unknown",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clean_copied_text(text: str) -> str:
|
||||||
|
lines = text.split("\n")
|
||||||
|
cleaned: list[str] = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if stripped in StrixTUIApp._DECORATIVE_LINES:
|
||||||
|
continue
|
||||||
|
if stripped and all(c == "─" for c in stripped):
|
||||||
|
continue
|
||||||
|
out = line
|
||||||
|
for prefix in StrixTUIApp._ICON_PREFIXES:
|
||||||
|
if stripped.startswith(prefix):
|
||||||
|
leading = line[: len(line) - len(line.lstrip())]
|
||||||
|
out = leading + stripped[len(prefix) :]
|
||||||
|
break
|
||||||
|
cleaned.append(out)
|
||||||
|
return "\n".join(cleaned)
|
||||||
|
|
||||||
|
def _auto_copy_selection(self) -> None:
|
||||||
|
copied = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.screen.selections:
|
||||||
|
selected = self.screen.get_selected_text()
|
||||||
|
self.screen.clear_selection()
|
||||||
|
if selected and selected.strip():
|
||||||
|
cleaned = self._clean_copied_text(selected)
|
||||||
|
self.copy_to_clipboard(cleaned if cleaned.strip() else selected)
|
||||||
|
copied = True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Failed to copy screen selection", exc_info=True)
|
||||||
|
|
||||||
|
if not copied:
|
||||||
|
try:
|
||||||
|
chat_input = self.query_one("#chat_input", ChatTextArea)
|
||||||
|
selected = chat_input.selected_text
|
||||||
|
if selected and selected.strip():
|
||||||
|
self.copy_to_clipboard(selected)
|
||||||
|
chat_input.move_cursor(chat_input.cursor_location)
|
||||||
|
copied = True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Failed to copy chat input selection", exc_info=True)
|
||||||
|
|
||||||
|
if copied:
|
||||||
|
self.notify("Copied to clipboard", timeout=2)
|
||||||
|
|
||||||
|
|
||||||
async def run_tui(args: argparse.Namespace) -> None:
|
async def run_tui(args: argparse.Namespace) -> None:
|
||||||
"""Run strix in interactive TUI mode with textual."""
|
"""Run strix in interactive TUI mode with textual."""
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
from docker.errors import DockerException, ImageNotFound
|
from docker.errors import DockerException, ImageNotFound
|
||||||
@@ -161,32 +163,34 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
|
|||||||
text.append("\n")
|
text.append("\n")
|
||||||
text.append(poc_script_code, style="dim")
|
text.append(poc_script_code, style="dim")
|
||||||
|
|
||||||
code_file = report.get("code_file")
|
code_locations = report.get("code_locations")
|
||||||
if code_file:
|
if code_locations:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
text.append("Code File: ", style=field_style)
|
text.append("Code Locations", style=field_style)
|
||||||
text.append(code_file)
|
for i, loc in enumerate(code_locations):
|
||||||
|
text.append("\n\n")
|
||||||
code_before = report.get("code_before")
|
text.append(f" Location {i + 1}: ", style="dim")
|
||||||
if code_before:
|
text.append(loc.get("file", "unknown"), style="bold")
|
||||||
text.append("\n\n")
|
start = loc.get("start_line")
|
||||||
text.append("Code Before", style=field_style)
|
end = loc.get("end_line")
|
||||||
text.append("\n")
|
if start is not None:
|
||||||
text.append(code_before, style="dim")
|
if end and end != start:
|
||||||
|
text.append(f":{start}-{end}")
|
||||||
code_after = report.get("code_after")
|
else:
|
||||||
if code_after:
|
text.append(f":{start}")
|
||||||
text.append("\n\n")
|
if loc.get("label"):
|
||||||
text.append("Code After", style=field_style)
|
text.append(f"\n {loc['label']}", style="italic dim")
|
||||||
text.append("\n")
|
if loc.get("snippet"):
|
||||||
text.append(code_after, style="dim")
|
text.append("\n ")
|
||||||
|
text.append(loc["snippet"], style="dim")
|
||||||
code_diff = report.get("code_diff")
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
if code_diff:
|
text.append("\n Fix:")
|
||||||
text.append("\n\n")
|
if loc.get("fix_before"):
|
||||||
text.append("Code Diff", style=field_style)
|
text.append("\n - ", style="dim")
|
||||||
text.append("\n")
|
text.append(loc["fix_before"], style="dim")
|
||||||
text.append(code_diff, style="dim")
|
if loc.get("fix_after"):
|
||||||
|
text.append("\n + ", style="dim")
|
||||||
|
text.append(loc["fix_after"], style="dim")
|
||||||
|
|
||||||
remediation_steps = report.get("remediation_steps")
|
remediation_steps = report.get("remediation_steps")
|
||||||
if remediation_steps:
|
if remediation_steps:
|
||||||
@@ -450,29 +454,42 @@ def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Target processing utilities
|
# Target processing utilities
|
||||||
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
|
|
||||||
|
|
||||||
|
def _is_http_git_repo(url: str) -> bool:
|
||||||
|
check_url = f"{url.rstrip('/')}/info/refs?service=git-upload-pack"
|
||||||
|
try:
|
||||||
|
req = Request(check_url, headers={"User-Agent": "git/strix"}) # noqa: S310
|
||||||
|
with urlopen(req, timeout=10) as resp: # noqa: S310 # nosec B310
|
||||||
|
return "x-git-upload-pack-advertisement" in resp.headers.get("Content-Type", "")
|
||||||
|
except HTTPError as e:
|
||||||
|
return e.code == 401
|
||||||
|
except (URLError, OSError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911, PLR0912
|
||||||
if not target or not isinstance(target, str):
|
if not target or not isinstance(target, str):
|
||||||
raise ValueError("Target must be a non-empty string")
|
raise ValueError("Target must be a non-empty string")
|
||||||
|
|
||||||
target = target.strip()
|
target = target.strip()
|
||||||
|
|
||||||
lower_target = target.lower()
|
if target.startswith("git@"):
|
||||||
bare_repo_prefixes = (
|
return "repository", {"target_repo": target}
|
||||||
"github.com/",
|
|
||||||
"www.github.com/",
|
if target.startswith("git://"):
|
||||||
"gitlab.com/",
|
return "repository", {"target_repo": target}
|
||||||
"www.gitlab.com/",
|
|
||||||
"bitbucket.org/",
|
|
||||||
"www.bitbucket.org/",
|
|
||||||
)
|
|
||||||
if any(lower_target.startswith(p) for p in bare_repo_prefixes):
|
|
||||||
return "repository", {"target_repo": f"https://{target}"}
|
|
||||||
|
|
||||||
parsed = urlparse(target)
|
parsed = urlparse(target)
|
||||||
if parsed.scheme in ("http", "https"):
|
if parsed.scheme in ("http", "https"):
|
||||||
if any(
|
if parsed.username or parsed.password:
|
||||||
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
|
return "repository", {"target_repo": target}
|
||||||
):
|
if parsed.path.rstrip("/").endswith(".git"):
|
||||||
|
return "repository", {"target_repo": target}
|
||||||
|
if parsed.query or parsed.fragment:
|
||||||
|
return "web_application", {"target_url": target}
|
||||||
|
path_segments = [s for s in parsed.path.split("/") if s]
|
||||||
|
if len(path_segments) >= 2 and _is_http_git_repo(target):
|
||||||
return "repository", {"target_repo": target}
|
return "repository", {"target_repo": target}
|
||||||
return "web_application", {"target_url": target}
|
return "web_application", {"target_url": target}
|
||||||
|
|
||||||
@@ -487,15 +504,22 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
|
|||||||
try:
|
try:
|
||||||
if path.exists():
|
if path.exists():
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
resolved = path.resolve()
|
return "local_code", {"target_path": str(path.resolve())}
|
||||||
return "local_code", {"target_path": str(resolved)}
|
|
||||||
raise ValueError(f"Path exists but is not a directory: {target}")
|
raise ValueError(f"Path exists but is not a directory: {target}")
|
||||||
except (OSError, RuntimeError) as e:
|
except (OSError, RuntimeError) as e:
|
||||||
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
||||||
|
|
||||||
if target.startswith("git@") or target.endswith(".git"):
|
if target.endswith(".git"):
|
||||||
return "repository", {"target_repo": target}
|
return "repository", {"target_repo": target}
|
||||||
|
|
||||||
|
if "/" in target:
|
||||||
|
host_part, _, path_part = target.partition("/")
|
||||||
|
if "." in host_part and not host_part.startswith(".") and path_part:
|
||||||
|
full_url = f"https://{target}"
|
||||||
|
if _is_http_git_repo(full_url):
|
||||||
|
return "repository", {"target_repo": full_url}
|
||||||
|
return "web_application", {"target_url": full_url}
|
||||||
|
|
||||||
if "." in target and "/" not in target and not target.startswith("."):
|
if "." in target and "/" not in target and not target.startswith("."):
|
||||||
parts = target.split(".")
|
parts = target.split(".")
|
||||||
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
||||||
@@ -505,7 +529,7 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
|
|||||||
f"Invalid target: {target}\n"
|
f"Invalid target: {target}\n"
|
||||||
"Target must be one of:\n"
|
"Target must be one of:\n"
|
||||||
"- A valid URL (http:// or https://)\n"
|
"- A valid URL (http:// or https://)\n"
|
||||||
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
|
"- A Git repository URL (https://host/org/repo or git@host:org/repo.git)\n"
|
||||||
"- A local directory path\n"
|
"- A local directory path\n"
|
||||||
"- A domain name (e.g., example.com)\n"
|
"- A domain name (e.g., example.com)\n"
|
||||||
"- An IP address (e.g., 192.168.1.10)"
|
"- An IP address (e.g., 192.168.1.10)"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from strix.config import Config
|
from strix.config import Config
|
||||||
|
from strix.config.config import resolve_llm_config
|
||||||
|
|
||||||
|
|
||||||
class LLMConfig:
|
class LLMConfig:
|
||||||
@@ -10,7 +11,8 @@ class LLMConfig:
|
|||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
scan_mode: str = "deep",
|
scan_mode: str = "deep",
|
||||||
):
|
):
|
||||||
self.model_name = model_name or Config.get("strix_llm")
|
resolved_model, self.api_key, self.api_base = resolve_llm_config()
|
||||||
|
self.model_name = model_name or resolved_model
|
||||||
|
|
||||||
if not self.model_name:
|
if not self.model_name:
|
||||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
from strix.config import Config
|
from strix.config.config import resolve_llm_config
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -155,14 +155,7 @@ def check_duplicate(
|
|||||||
|
|
||||||
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
||||||
|
|
||||||
model_name = Config.get("strix_llm")
|
model_name, api_key, api_base = resolve_llm_config()
|
||||||
api_key = Config.get("llm_api_key")
|
|
||||||
api_base = (
|
|
||||||
Config.get("llm_api_base")
|
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
)
|
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
||||||
|
|||||||
@@ -200,15 +200,10 @@ class LLM:
|
|||||||
"stream_options": {"include_usage": True},
|
"stream_options": {"include_usage": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
if api_key := Config.get("llm_api_key"):
|
if self.config.api_key:
|
||||||
args["api_key"] = api_key
|
args["api_key"] = self.config.api_key
|
||||||
if api_base := (
|
if self.config.api_base:
|
||||||
Config.get("llm_api_base")
|
args["api_base"] = self.config.api_base
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
):
|
|
||||||
args["api_base"] = api_base
|
|
||||||
if self._supports_reasoning():
|
if self._supports_reasoning():
|
||||||
args["reasoning_effort"] = self._reasoning_effort
|
args["reasoning_effort"] = self._reasoning_effort
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Any
|
|||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
from strix.config import Config
|
from strix.config.config import Config, resolve_llm_config
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -104,12 +104,18 @@ def _summarize_messages(
|
|||||||
conversation = "\n".join(formatted)
|
conversation = "\n".join(formatted)
|
||||||
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
||||||
|
|
||||||
|
_, api_key, api_base = resolve_llm_config()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
completion_args = {
|
completion_args: dict[str, Any] = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
}
|
}
|
||||||
|
if api_key:
|
||||||
|
completion_args["api_key"] = api_key
|
||||||
|
if api_base:
|
||||||
|
completion_args["api_base"] = api_base
|
||||||
|
|
||||||
response = litellm.completion(**completion_args)
|
response = litellm.completion(**completion_args)
|
||||||
summary = response.choices[0].message.content or ""
|
summary = response.choices[0].message.content or ""
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ def load_skills(skill_names: list[str]) -> dict[str, str]:
|
|||||||
if skill_path and (skills_dir / skill_path).exists():
|
if skill_path and (skills_dir / skill_path).exists():
|
||||||
full_path = skills_dir / skill_path
|
full_path = skills_dir / skill_path
|
||||||
var_name = skill_name.split("/")[-1]
|
var_name = skill_name.split("/")[-1]
|
||||||
content = full_path.read_text()
|
content = full_path.read_text(encoding="utf-8")
|
||||||
content = _FRONTMATTER_PATTERN.sub("", content).lstrip()
|
content = _FRONTMATTER_PATTERN.sub("", content).lstrip()
|
||||||
skill_content[var_name] = content
|
skill_content[var_name] = content
|
||||||
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
|
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
|
||||||
|
|||||||
@@ -89,10 +89,8 @@ class Tracer:
|
|||||||
endpoint: str | None = None,
|
endpoint: str | None = None,
|
||||||
method: str | None = None,
|
method: str | None = None,
|
||||||
cve: str | None = None,
|
cve: str | None = None,
|
||||||
code_file: str | None = None,
|
cwe: str | None = None,
|
||||||
code_before: str | None = None,
|
code_locations: list[dict[str, Any]] | None = None,
|
||||||
code_after: str | None = None,
|
|
||||||
code_diff: str | None = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
|
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
|
||||||
|
|
||||||
@@ -127,14 +125,10 @@ class Tracer:
|
|||||||
report["method"] = method.strip()
|
report["method"] = method.strip()
|
||||||
if cve:
|
if cve:
|
||||||
report["cve"] = cve.strip()
|
report["cve"] = cve.strip()
|
||||||
if code_file:
|
if cwe:
|
||||||
report["code_file"] = code_file.strip()
|
report["cwe"] = cwe.strip()
|
||||||
if code_before:
|
if code_locations:
|
||||||
report["code_before"] = code_before.strip()
|
report["code_locations"] = code_locations
|
||||||
if code_after:
|
|
||||||
report["code_after"] = code_after.strip()
|
|
||||||
if code_diff:
|
|
||||||
report["code_diff"] = code_diff.strip()
|
|
||||||
|
|
||||||
self.vulnerability_reports.append(report)
|
self.vulnerability_reports.append(report)
|
||||||
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
||||||
@@ -323,6 +317,7 @@ class Tracer:
|
|||||||
("Endpoint", report.get("endpoint")),
|
("Endpoint", report.get("endpoint")),
|
||||||
("Method", report.get("method")),
|
("Method", report.get("method")),
|
||||||
("CVE", report.get("cve")),
|
("CVE", report.get("cve")),
|
||||||
|
("CWE", report.get("cwe")),
|
||||||
]
|
]
|
||||||
cvss_score = report.get("cvss")
|
cvss_score = report.get("cvss")
|
||||||
if cvss_score is not None:
|
if cvss_score is not None:
|
||||||
@@ -353,15 +348,33 @@ class Tracer:
|
|||||||
f.write(f"{report['poc_script_code']}\n")
|
f.write(f"{report['poc_script_code']}\n")
|
||||||
f.write("```\n\n")
|
f.write("```\n\n")
|
||||||
|
|
||||||
if report.get("code_file") or report.get("code_diff"):
|
if report.get("code_locations"):
|
||||||
f.write("## Code Analysis\n\n")
|
f.write("## Code Analysis\n\n")
|
||||||
if report.get("code_file"):
|
for i, loc in enumerate(report["code_locations"]):
|
||||||
f.write(f"**File:** {report['code_file']}\n\n")
|
prefix = f"**Location {i + 1}:**"
|
||||||
if report.get("code_diff"):
|
file_ref = loc.get("file", "unknown")
|
||||||
f.write("**Changes:**\n")
|
line_ref = ""
|
||||||
f.write("```diff\n")
|
if loc.get("start_line") is not None:
|
||||||
f.write(f"{report['code_diff']}\n")
|
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
|
||||||
f.write("```\n\n")
|
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
|
||||||
|
else:
|
||||||
|
line_ref = f" (line {loc['start_line']})"
|
||||||
|
f.write(f"{prefix} `{file_ref}`{line_ref}\n")
|
||||||
|
if loc.get("label"):
|
||||||
|
f.write(f" {loc['label']}\n")
|
||||||
|
if loc.get("snippet"):
|
||||||
|
f.write(f" ```\n {loc['snippet']}\n ```\n")
|
||||||
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
|
f.write("\n **Suggested Fix:**\n")
|
||||||
|
f.write("```diff\n")
|
||||||
|
if loc.get("fix_before"):
|
||||||
|
for line in loc["fix_before"].splitlines():
|
||||||
|
f.write(f"- {line}\n")
|
||||||
|
if loc.get("fix_after"):
|
||||||
|
for line in loc["fix_after"].splitlines():
|
||||||
|
f.write(f"+ {line}\n")
|
||||||
|
f.write("```\n")
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
if report.get("remediation_steps"):
|
if report.get("remediation_steps"):
|
||||||
f.write("## Remediation\n\n")
|
f.write("## Remediation\n\n")
|
||||||
|
|||||||
@@ -51,16 +51,16 @@ Professional, customer-facing penetration test report rules (PDF-ready):
|
|||||||
</description>
|
</description>
|
||||||
<parameters>
|
<parameters>
|
||||||
<parameter name="executive_summary" type="string" required="true">
|
<parameter name="executive_summary" type="string" required="true">
|
||||||
<description>High-level summary for executives: key findings, overall security posture, critical risks, business impact</description>
|
<description>High-level summary for non-technical stakeholders. Include: risk posture assessment, key findings in business context, potential business impact (data exposure, compliance, reputation), and an overarching remediation theme. Write in clear, accessible language for executive leadership.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="methodology" type="string" required="true">
|
<parameter name="methodology" type="string" required="true">
|
||||||
<description>Testing methodology: approach, tools used, scope, techniques employed</description>
|
<description>Testing methodology and scope. Include: frameworks and standards followed (e.g., OWASP WSTG, PTES), engagement type and approach (black-box, gray-box), in-scope assets and target environment, categories of testing activities performed, and evidence validation standards applied.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="technical_analysis" type="string" required="true">
|
<parameter name="technical_analysis" type="string" required="true">
|
||||||
<description>Detailed technical findings and security assessment results over the scan</description>
|
<description>Consolidated overview of confirmed findings and risk patterns. Include: severity model used, a high-level summary of each finding with its severity rating, and systemic root causes or recurring themes observed across findings. Reference individual vulnerability reports for reproduction details — do not duplicate full evidence here.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="recommendations" type="string" required="true">
|
<parameter name="recommendations" type="string" required="true">
|
||||||
<description>Actionable security recommendations and remediation priorities</description>
|
<description>Prioritized, actionable remediation guidance organized by urgency (Immediate, Short-term, Medium-term). Each recommendation should provide specific technical remediation steps. Conclude with retest and validation guidance.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
</parameters>
|
</parameters>
|
||||||
<returns type="Dict[str, Any]">
|
<returns type="Dict[str, Any]">
|
||||||
@@ -69,12 +69,11 @@ Professional, customer-facing penetration test report rules (PDF-ready):
|
|||||||
<examples>
|
<examples>
|
||||||
|
|
||||||
<function=finish_scan>
|
<function=finish_scan>
|
||||||
<parameter=executive_summary>Executive summary
|
<parameter=executive_summary>An external penetration test of the Acme Customer Portal and associated API identified multiple security weaknesses that, if exploited, could result in unauthorized access to customer data, cross-tenant exposure, and access to internal network resources.
|
||||||
An external penetration test of the Acme Customer Portal and associated API identified multiple security weaknesses that, if exploited, could result in unauthorized access to customer data, cross-tenant exposure, and access to internal network resources.
|
|
||||||
|
|
||||||
Overall risk posture: Elevated.
|
Overall risk posture: Elevated.
|
||||||
|
|
||||||
Key outcomes
|
Key findings
|
||||||
- Confirmed server-side request forgery (SSRF) in a URL preview capability that enables the application to initiate outbound requests to attacker-controlled destinations and internal network ranges.
|
- Confirmed server-side request forgery (SSRF) in a URL preview capability that enables the application to initiate outbound requests to attacker-controlled destinations and internal network ranges.
|
||||||
- Identified broken access control patterns in business-critical workflows that can enable cross-tenant data access (tenant isolation failures).
|
- Identified broken access control patterns in business-critical workflows that can enable cross-tenant data access (tenant isolation failures).
|
||||||
- Observed session and authorization hardening gaps that materially increase risk when combined with other weaknesses.
|
- Observed session and authorization hardening gaps that materially increase risk when combined with other weaknesses.
|
||||||
@@ -86,8 +85,7 @@ Business impact
|
|||||||
|
|
||||||
Remediation theme
|
Remediation theme
|
||||||
Prioritize eliminating SSRF pathways and centralizing authorization enforcement (deny-by-default). Follow with session hardening and monitoring improvements, then validate with a focused retest.</parameter>
|
Prioritize eliminating SSRF pathways and centralizing authorization enforcement (deny-by-default). Follow with session hardening and monitoring improvements, then validate with a focused retest.</parameter>
|
||||||
<parameter=methodology>Methodology
|
<parameter=methodology>The assessment was conducted in accordance with the OWASP Web Security Testing Guide (WSTG) and aligned to industry-standard penetration testing methodology.
|
||||||
The assessment followed industry-standard penetration testing practices aligned to OWASP Web Security Testing Guide (WSTG) concepts and common web/API security testing methodology.
|
|
||||||
|
|
||||||
Engagement details
|
Engagement details
|
||||||
- Assessment type: External penetration test (black-box with limited gray-box context)
|
- Assessment type: External penetration test (black-box with limited gray-box context)
|
||||||
@@ -107,13 +105,12 @@ High-level testing activities
|
|||||||
|
|
||||||
Evidence handling and validation standard
|
Evidence handling and validation standard
|
||||||
Only validated issues with reproducible impact were treated as findings. Each finding was documented with clear reproduction steps and sufficient evidence to support remediation and verification testing.</parameter>
|
Only validated issues with reproducible impact were treated as findings. Each finding was documented with clear reproduction steps and sufficient evidence to support remediation and verification testing.</parameter>
|
||||||
<parameter=technical_analysis>Technical analysis
|
<parameter=technical_analysis>This section provides a consolidated view of the confirmed findings and observed risk patterns. Detailed reproduction steps and evidence are documented in the individual vulnerability reports.
|
||||||
This section provides a consolidated view of the confirmed findings and observed risk patterns. Detailed reproduction steps and evidence are documented in the individual vulnerability reports.
|
|
||||||
|
|
||||||
Severity model
|
Severity model
|
||||||
Severity reflects a combination of exploitability and potential impact to confidentiality, integrity, and availability, considering realistic attacker capabilities.
|
Severity reflects a combination of exploitability and potential impact to confidentiality, integrity, and availability, considering realistic attacker capabilities.
|
||||||
|
|
||||||
Confirmed findings (high level)
|
Confirmed findings
|
||||||
1) Server-side request forgery (SSRF) in URL preview (Critical)
|
1) Server-side request forgery (SSRF) in URL preview (Critical)
|
||||||
The application fetches user-supplied URLs server-side to generate previews. Validation controls were insufficient to prevent access to internal and link-local destinations. This creates a pathway to internal network enumeration and potential access to sensitive internal services. Redirect and DNS/normalization bypass risk must be assumed unless controls are comprehensive and applied on every request hop.
|
The application fetches user-supplied URLs server-side to generate previews. Validation controls were insufficient to prevent access to internal and link-local destinations. This creates a pathway to internal network enumeration and potential access to sensitive internal services. Redirect and DNS/normalization bypass risk must be assumed unless controls are comprehensive and applied on every request hop.
|
||||||
|
|
||||||
@@ -130,22 +127,40 @@ Systemic themes and root causes
|
|||||||
- Authorization enforcement appears distributed and inconsistent across endpoints instead of centralized and testable.
|
- Authorization enforcement appears distributed and inconsistent across endpoints instead of centralized and testable.
|
||||||
- Outbound request functionality lacks a robust, deny-by-default policy for destination validation.
|
- Outbound request functionality lacks a robust, deny-by-default policy for destination validation.
|
||||||
- Hardening controls (session lifetime, sensitive-action controls, logging) are applied unevenly, increasing the likelihood of successful attack chains.</parameter>
|
- Hardening controls (session lifetime, sensitive-action controls, logging) are applied unevenly, increasing the likelihood of successful attack chains.</parameter>
|
||||||
<parameter=recommendations>Recommendations
|
<parameter=recommendations>The following recommendations are prioritized by urgency and potential risk reduction.
|
||||||
Priority 0
|
|
||||||
- Eliminate SSRF by implementing a strict destination allowlist and deny-by-default policy for outbound requests. Block private, loopback, and link-local ranges (IPv4 and IPv6) after DNS resolution. Re-validate on every redirect hop. Apply URL parsing/normalization safeguards against ambiguous encodings and unusual IP notations.
|
|
||||||
- Apply network egress controls so the application runtime cannot reach sensitive internal ranges or link-local services. Route necessary outbound requests through a policy-enforcing egress proxy with logging.
|
|
||||||
|
|
||||||
Priority 1
|
Immediate priority
|
||||||
- Centralize authorization enforcement for all object access and administrative actions. Implement consistent tenant-ownership checks for every read/write path involving orders, invoices, and account resources. Adopt deny-by-default authorization middleware/policies.
|
These items address the most severe confirmed risks and should be prioritized for immediate remediation.
|
||||||
- Add regression tests for authorization decisions, including cross-tenant negative cases and privilege-boundary testing for administrative endpoints.
|
|
||||||
- Harden session management: secure cookie attributes, session rotation after authentication and privilege change events, reduced session lifetime for privileged contexts, and consistent CSRF protections for state-changing actions.
|
|
||||||
|
|
||||||
Priority 2
|
1. Remediate server-side request forgery
|
||||||
- Harden file handling and preview behaviors: strict content-type allowlists, forced download for active formats, safe rendering pipelines, and scanning/sanitization where applicable.
|
Implement a strict destination allowlist with a deny-by-default policy for all server-initiated outbound requests. Block private, loopback, and link-local address ranges (IPv4 and IPv6) at the application layer after DNS resolution. Re-validate destination addresses on every redirect hop. Apply URL normalization to prevent bypass via ambiguous encodings, alternate IP notations, or DNS rebinding.
|
||||||
- Improve monitoring and detection: alert on high-risk events such as repeated authorization failures, anomalous outbound fetch attempts, sensitive administrative actions, and unusual access patterns to business-critical resources.
|
|
||||||
|
|
||||||
Follow-up validation
|
2. Enforce network-level egress controls
|
||||||
- Conduct a targeted retest after remediation to confirm SSRF controls, tenant isolation enforcement, and session hardening, and to ensure no bypasses exist via redirects, DNS rebinding, or encoding edge cases.</parameter>
|
Restrict application runtime network egress to prevent outbound connections to internal and link-local address spaces at the network layer. Route legitimate outbound requests through a policy-enforcing egress proxy with request logging and alerting.
|
||||||
|
|
||||||
|
3. Enforce tenant-scoped authorization
|
||||||
|
Implement consistent tenant-ownership validation on every read and write path for business-critical resources, including orders, invoices, and account data. Adopt centralized, deny-by-default authorization middleware that enforces object- and function-level access controls uniformly across all endpoints.
|
||||||
|
|
||||||
|
Short-term priority
|
||||||
|
These items reduce residual risk and harden defensive controls.
|
||||||
|
|
||||||
|
4. Centralize and harden authorization logic
|
||||||
|
Consolidate authorization enforcement into a centralized policy layer. Require re-authentication for high-risk administrative actions. Add regression tests covering cross-tenant access, privilege escalation, and negative authorization cases.
|
||||||
|
|
||||||
|
5. Harden session management
|
||||||
|
Enforce secure cookie attributes (Secure, HttpOnly, SameSite). Implement session rotation after authentication events and privilege changes. Reduce session lifetime for privileged contexts. Apply consistent CSRF protections to all state-changing operations.
|
||||||
|
|
||||||
|
Medium-term priority
|
||||||
|
These items strengthen defense-in-depth and improve operational visibility.
|
||||||
|
|
||||||
|
6. Harden file handling and content rendering
|
||||||
|
Enforce strict content-type allowlists for file uploads and previews. Force download disposition for active content types. Implement content sanitization and scanning where applicable. Prevent inline rendering of potentially executable formats.
|
||||||
|
|
||||||
|
7. Improve security monitoring and detection
|
||||||
|
Implement alerting for high-risk events: repeated authorization failures, anomalous outbound request patterns, sensitive administrative actions, and unusual access to business-critical resources. Ensure sufficient logging granularity to support incident investigation.
|
||||||
|
|
||||||
|
Retest and validation
|
||||||
|
Conduct a focused retest after remediation of immediate-priority items to verify the effectiveness of SSRF controls, tenant isolation enforcement, and session hardening. Validate that no bypasses exist through redirect chains, DNS rebinding, or encoding edge cases. Repeat authorization regression tests to confirm consistent enforcement across all endpoints.</parameter>
|
||||||
</function>
|
</function>
|
||||||
</examples>
|
</examples>
|
||||||
</tool>
|
</tool>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def _load_xml_schema(path: Path) -> Any:
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
content = path.read_text()
|
content = path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
content = _process_dynamic_content(content)
|
content = _process_dynamic_content(content)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,120 @@
|
|||||||
|
import contextlib
|
||||||
|
import re
|
||||||
|
from pathlib import PurePosixPath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from strix.tools.registry import register_tool
|
from strix.tools.registry import register_tool
|
||||||
|
|
||||||
|
|
||||||
|
_CVSS_FIELDS = (
|
||||||
|
"attack_vector",
|
||||||
|
"attack_complexity",
|
||||||
|
"privileges_required",
|
||||||
|
"user_interaction",
|
||||||
|
"scope",
|
||||||
|
"confidentiality",
|
||||||
|
"integrity",
|
||||||
|
"availability",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cvss_xml(xml_str: str) -> dict[str, str] | None:
|
||||||
|
if not xml_str or not xml_str.strip():
|
||||||
|
return None
|
||||||
|
result = {}
|
||||||
|
for field in _CVSS_FIELDS:
|
||||||
|
match = re.search(rf"<{field}>(.*?)</{field}>", xml_str, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
result[field] = match.group(1).strip()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_code_locations_xml(xml_str: str) -> list[dict[str, Any]] | None:
|
||||||
|
if not xml_str or not xml_str.strip():
|
||||||
|
return None
|
||||||
|
locations = []
|
||||||
|
for loc_match in re.finditer(r"<location>(.*?)</location>", xml_str, re.DOTALL):
|
||||||
|
loc: dict[str, Any] = {}
|
||||||
|
loc_content = loc_match.group(1)
|
||||||
|
for field in (
|
||||||
|
"file",
|
||||||
|
"start_line",
|
||||||
|
"end_line",
|
||||||
|
"snippet",
|
||||||
|
"label",
|
||||||
|
"fix_before",
|
||||||
|
"fix_after",
|
||||||
|
):
|
||||||
|
field_match = re.search(rf"<{field}>(.*?)</{field}>", loc_content, re.DOTALL)
|
||||||
|
if field_match:
|
||||||
|
raw = field_match.group(1)
|
||||||
|
value = (
|
||||||
|
raw.strip("\n")
|
||||||
|
if field in ("snippet", "fix_before", "fix_after")
|
||||||
|
else raw.strip()
|
||||||
|
)
|
||||||
|
if field in ("start_line", "end_line"):
|
||||||
|
with contextlib.suppress(ValueError, TypeError):
|
||||||
|
loc[field] = int(value)
|
||||||
|
elif value:
|
||||||
|
loc[field] = value
|
||||||
|
if loc.get("file") and loc.get("start_line") is not None:
|
||||||
|
locations.append(loc)
|
||||||
|
return locations if locations else None
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_file_path(path: str) -> str | None:
|
||||||
|
if not path or not path.strip():
|
||||||
|
return "file path cannot be empty"
|
||||||
|
p = PurePosixPath(path)
|
||||||
|
if p.is_absolute():
|
||||||
|
return f"file path must be relative, got absolute: '{path}'"
|
||||||
|
if ".." in p.parts:
|
||||||
|
return f"file path must not contain '..': '{path}'"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_code_locations(locations: list[dict[str, Any]]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
for i, loc in enumerate(locations):
|
||||||
|
path_err = _validate_file_path(loc.get("file", ""))
|
||||||
|
if path_err:
|
||||||
|
errors.append(f"code_locations[{i}]: {path_err}")
|
||||||
|
start = loc.get("start_line")
|
||||||
|
if not isinstance(start, int) or start < 1:
|
||||||
|
errors.append(f"code_locations[{i}]: start_line must be a positive integer")
|
||||||
|
end = loc.get("end_line")
|
||||||
|
if end is None:
|
||||||
|
errors.append(f"code_locations[{i}]: end_line is required")
|
||||||
|
elif not isinstance(end, int) or end < 1:
|
||||||
|
errors.append(f"code_locations[{i}]: end_line must be a positive integer")
|
||||||
|
elif isinstance(start, int) and end < start:
|
||||||
|
errors.append(f"code_locations[{i}]: end_line ({end}) must be >= start_line ({start})")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_cve(cve: str) -> str:
|
||||||
|
match = re.search(r"CVE-\d{4}-\d{4,}", cve)
|
||||||
|
return match.group(0) if match else cve.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_cve(cve: str) -> str | None:
|
||||||
|
if not re.match(r"^CVE-\d{4}-\d{4,}$", cve):
|
||||||
|
return f"invalid CVE format: '{cve}' (expected 'CVE-YYYY-NNNNN')"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_cwe(cwe: str) -> str:
|
||||||
|
match = re.search(r"CWE-\d+", cwe)
|
||||||
|
return match.group(0) if match else cwe.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_cwe(cwe: str) -> str | None:
|
||||||
|
if not re.match(r"^CWE-\d+$", cwe):
|
||||||
|
return f"invalid CWE format: '{cwe}' (expected 'CWE-NNN')"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def calculate_cvss_and_severity(
|
def calculate_cvss_and_severity(
|
||||||
attack_vector: str,
|
attack_vector: str,
|
||||||
attack_complexity: str,
|
attack_complexity: str,
|
||||||
@@ -87,7 +199,7 @@ def _validate_cvss_parameters(**kwargs: str) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
@register_tool(sandbox_execution=False)
|
@register_tool(sandbox_execution=False)
|
||||||
def create_vulnerability_report(
|
def create_vulnerability_report( # noqa: PLR0912
|
||||||
title: str,
|
title: str,
|
||||||
description: str,
|
description: str,
|
||||||
impact: str,
|
impact: str,
|
||||||
@@ -96,23 +208,12 @@ def create_vulnerability_report(
|
|||||||
poc_description: str,
|
poc_description: str,
|
||||||
poc_script_code: str,
|
poc_script_code: str,
|
||||||
remediation_steps: str,
|
remediation_steps: str,
|
||||||
# CVSS Breakdown Components
|
cvss_breakdown: str,
|
||||||
attack_vector: str,
|
|
||||||
attack_complexity: str,
|
|
||||||
privileges_required: str,
|
|
||||||
user_interaction: str,
|
|
||||||
scope: str,
|
|
||||||
confidentiality: str,
|
|
||||||
integrity: str,
|
|
||||||
availability: str,
|
|
||||||
# Optional fields
|
|
||||||
endpoint: str | None = None,
|
endpoint: str | None = None,
|
||||||
method: str | None = None,
|
method: str | None = None,
|
||||||
cve: str | None = None,
|
cve: str | None = None,
|
||||||
code_file: str | None = None,
|
cwe: str | None = None,
|
||||||
code_before: str | None = None,
|
code_locations: str | None = None,
|
||||||
code_after: str | None = None,
|
|
||||||
code_diff: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
validation_errors = _validate_required_fields(
|
validation_errors = _validate_required_fields(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -125,32 +226,32 @@ def create_vulnerability_report(
|
|||||||
remediation_steps=remediation_steps,
|
remediation_steps=remediation_steps,
|
||||||
)
|
)
|
||||||
|
|
||||||
validation_errors.extend(
|
parsed_cvss = parse_cvss_xml(cvss_breakdown)
|
||||||
_validate_cvss_parameters(
|
if not parsed_cvss:
|
||||||
attack_vector=attack_vector,
|
validation_errors.append("cvss: could not parse CVSS breakdown XML")
|
||||||
attack_complexity=attack_complexity,
|
else:
|
||||||
privileges_required=privileges_required,
|
validation_errors.extend(_validate_cvss_parameters(**parsed_cvss))
|
||||||
user_interaction=user_interaction,
|
|
||||||
scope=scope,
|
parsed_locations = parse_code_locations_xml(code_locations) if code_locations else None
|
||||||
confidentiality=confidentiality,
|
|
||||||
integrity=integrity,
|
if parsed_locations:
|
||||||
availability=availability,
|
validation_errors.extend(_validate_code_locations(parsed_locations))
|
||||||
)
|
if cve:
|
||||||
)
|
cve = _extract_cve(cve)
|
||||||
|
cve_err = _validate_cve(cve)
|
||||||
|
if cve_err:
|
||||||
|
validation_errors.append(cve_err)
|
||||||
|
if cwe:
|
||||||
|
cwe = _extract_cwe(cwe)
|
||||||
|
cwe_err = _validate_cwe(cwe)
|
||||||
|
if cwe_err:
|
||||||
|
validation_errors.append(cwe_err)
|
||||||
|
|
||||||
if validation_errors:
|
if validation_errors:
|
||||||
return {"success": False, "message": "Validation failed", "errors": validation_errors}
|
return {"success": False, "message": "Validation failed", "errors": validation_errors}
|
||||||
|
|
||||||
cvss_score, severity, cvss_vector = calculate_cvss_and_severity(
|
assert parsed_cvss is not None
|
||||||
attack_vector,
|
cvss_score, severity, cvss_vector = calculate_cvss_and_severity(**parsed_cvss)
|
||||||
attack_complexity,
|
|
||||||
privileges_required,
|
|
||||||
user_interaction,
|
|
||||||
scope,
|
|
||||||
confidentiality,
|
|
||||||
integrity,
|
|
||||||
availability,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from strix.telemetry.tracer import get_global_tracer
|
from strix.telemetry.tracer import get_global_tracer
|
||||||
@@ -196,17 +297,6 @@ def create_vulnerability_report(
|
|||||||
"reason": dedupe_result.get("reason", ""),
|
"reason": dedupe_result.get("reason", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
cvss_breakdown = {
|
|
||||||
"attack_vector": attack_vector,
|
|
||||||
"attack_complexity": attack_complexity,
|
|
||||||
"privileges_required": privileges_required,
|
|
||||||
"user_interaction": user_interaction,
|
|
||||||
"scope": scope,
|
|
||||||
"confidentiality": confidentiality,
|
|
||||||
"integrity": integrity,
|
|
||||||
"availability": availability,
|
|
||||||
}
|
|
||||||
|
|
||||||
report_id = tracer.add_vulnerability_report(
|
report_id = tracer.add_vulnerability_report(
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
@@ -218,14 +308,12 @@ def create_vulnerability_report(
|
|||||||
poc_script_code=poc_script_code,
|
poc_script_code=poc_script_code,
|
||||||
remediation_steps=remediation_steps,
|
remediation_steps=remediation_steps,
|
||||||
cvss=cvss_score,
|
cvss=cvss_score,
|
||||||
cvss_breakdown=cvss_breakdown,
|
cvss_breakdown=parsed_cvss,
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
method=method,
|
method=method,
|
||||||
cve=cve,
|
cve=cve,
|
||||||
code_file=code_file,
|
cwe=cwe,
|
||||||
code_before=code_before,
|
code_locations=parsed_locations,
|
||||||
code_after=code_after,
|
|
||||||
code_diff=code_diff,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ DO NOT USE:
|
|||||||
- For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
|
- For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
|
||||||
- To re-report a vulnerability that was already reported (even with different details)
|
- To re-report a vulnerability that was already reported (even with different details)
|
||||||
|
|
||||||
White-box requirement (when you have access to the code): You MUST include code_file, code_before, code_after, and code_diff. These must contain the actual code (before/after) and a complete, apply-able unified diff.
|
White-box requirement (when you have access to the code): You MUST include code_locations with nested XML, including fix_before/fix_after on locations where a fix is proposed.
|
||||||
|
|
||||||
DEDUPLICATION: If this tool returns with success=false and mentions a duplicate, DO NOT attempt to re-submit. The vulnerability has already been reported. Move on to testing other areas.
|
DEDUPLICATION: If this tool returns with success=false and mentions a duplicate, DO NOT attempt to re-submit. The vulnerability has already been reported. Move on to testing other areas.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Professional, customer-facing report rules (PDF-ready):
|
|||||||
6) Impact
|
6) Impact
|
||||||
7) Remediation
|
7) Remediation
|
||||||
8) Evidence (optional request/response excerpts, etc.) in the technical analysis field.
|
8) Evidence (optional request/response excerpts, etc.) in the technical analysis field.
|
||||||
- Numbered steps are allowed ONLY within the proof of concept. Elsewhere, use clear, concise paragraphs suitable for customer-facing reports.
|
- Numbered steps are allowed ONLY within the proof of concept and remediation sections. Elsewhere, use clear, concise paragraphs suitable for customer-facing reports.
|
||||||
- Language must be precise and non-vague; avoid hedging.
|
- Language must be precise and non-vague; avoid hedging.
|
||||||
</description>
|
</description>
|
||||||
<parameters>
|
<parameters>
|
||||||
@@ -58,51 +58,28 @@ Professional, customer-facing report rules (PDF-ready):
|
|||||||
<parameter name="remediation_steps" type="string" required="true">
|
<parameter name="remediation_steps" type="string" required="true">
|
||||||
<description>Specific, actionable steps to fix the vulnerability</description>
|
<description>Specific, actionable steps to fix the vulnerability</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="attack_vector" type="string" required="true">
|
<parameter name="cvss_breakdown" type="string" required="true">
|
||||||
<description>CVSS Attack Vector - How the vulnerability is exploited:
|
<description>CVSS 3.1 base score breakdown as nested XML. All 8 metrics are required.
|
||||||
N = Network (remotely exploitable)
|
|
||||||
A = Adjacent (same network segment)
|
Each metric element contains a single uppercase letter value:
|
||||||
L = Local (local access required)
|
- attack_vector: N (Network), A (Adjacent), L (Local), P (Physical)
|
||||||
P = Physical (physical access required)</description>
|
- attack_complexity: L (Low), H (High)
|
||||||
</parameter>
|
- privileges_required: N (None), L (Low), H (High)
|
||||||
<parameter name="attack_complexity" type="string" required="true">
|
- user_interaction: N (None), R (Required)
|
||||||
<description>CVSS Attack Complexity - Conditions beyond attacker's control:
|
- scope: U (Unchanged), C (Changed)
|
||||||
L = Low (no special conditions)
|
- confidentiality: N (None), L (Low), H (High)
|
||||||
H = High (special conditions must exist)</description>
|
- integrity: N (None), L (Low), H (High)
|
||||||
</parameter>
|
- availability: N (None), L (Low), H (High)</description>
|
||||||
<parameter name="privileges_required" type="string" required="true">
|
<format>
|
||||||
<description>CVSS Privileges Required - Level of privileges needed:
|
<attack_vector>N</attack_vector>
|
||||||
N = None (no privileges needed)
|
<attack_complexity>L</attack_complexity>
|
||||||
L = Low (basic user privileges)
|
<privileges_required>N</privileges_required>
|
||||||
H = High (admin privileges)</description>
|
<user_interaction>N</user_interaction>
|
||||||
</parameter>
|
<scope>U</scope>
|
||||||
<parameter name="user_interaction" type="string" required="true">
|
<confidentiality>H</confidentiality>
|
||||||
<description>CVSS User Interaction - Does exploit require user action:
|
<integrity>H</integrity>
|
||||||
N = None (no user interaction needed)
|
<availability>N</availability>
|
||||||
R = Required (user must perform some action)</description>
|
</format>
|
||||||
</parameter>
|
|
||||||
<parameter name="scope" type="string" required="true">
|
|
||||||
<description>CVSS Scope - Can the vulnerability affect resources beyond its security scope:
|
|
||||||
U = Unchanged (only affects the vulnerable component)
|
|
||||||
C = Changed (affects resources beyond vulnerable component)</description>
|
|
||||||
</parameter>
|
|
||||||
<parameter name="confidentiality" type="string" required="true">
|
|
||||||
<description>CVSS Confidentiality Impact - Impact to confidentiality:
|
|
||||||
N = None (no impact)
|
|
||||||
L = Low (some information disclosure)
|
|
||||||
H = High (all information disclosed)</description>
|
|
||||||
</parameter>
|
|
||||||
<parameter name="integrity" type="string" required="true">
|
|
||||||
<description>CVSS Integrity Impact - Impact to integrity:
|
|
||||||
N = None (no impact)
|
|
||||||
L = Low (data can be modified but scope is limited)
|
|
||||||
H = High (total loss of integrity)</description>
|
|
||||||
</parameter>
|
|
||||||
<parameter name="availability" type="string" required="true">
|
|
||||||
<description>CVSS Availability Impact - Impact to availability:
|
|
||||||
N = None (no impact)
|
|
||||||
L = Low (reduced performance or interruptions)
|
|
||||||
H = High (total loss of availability)</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="endpoint" type="string" required="false">
|
<parameter name="endpoint" type="string" required="false">
|
||||||
<description>API endpoint(s) or URL path(s) (e.g., "/api/login") - for web vulnerabilities, or Git repository path(s) - for code vulnerabilities</description>
|
<description>API endpoint(s) or URL path(s) (e.g., "/api/login") - for web vulnerabilities, or Git repository path(s) - for code vulnerabilities</description>
|
||||||
@@ -111,19 +88,93 @@ H = High (total loss of availability)</description>
|
|||||||
<description>HTTP method(s) (GET, POST, etc.) - for web vulnerabilities.</description>
|
<description>HTTP method(s) (GET, POST, etc.) - for web vulnerabilities.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="cve" type="string" required="false">
|
<parameter name="cve" type="string" required="false">
|
||||||
<description>CVE identifier (e.g., "CVE-2024-1234"). Make sure it's a valid CVE. Use web search or vulnerability databases to make sure it's a valid CVE number.</description>
|
<description>CVE identifier. ONLY the ID, e.g. "CVE-2024-1234" — do NOT include the name or description.
|
||||||
|
You must be 100% certain of the exact CVE number. Do NOT guess, approximate, or hallucinate CVE IDs.
|
||||||
|
If web_search is available, use it to verify the CVE exists and matches this vulnerability. If you cannot verify it, omit this field entirely.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="code_file" type="string" required="false">
|
<parameter name="cwe" type="string" required="false">
|
||||||
<description>MANDATORY for white-box testing: exact affected source file path(s).</description>
|
<description>CWE identifier. ONLY the ID, e.g. "CWE-89" — do NOT include the name or parenthetical (wrong: "CWE-89 (SQL Injection)").
|
||||||
|
|
||||||
|
You must be 100% certain of the exact CWE number. Do NOT guess or approximate.
|
||||||
|
If web_search is available and you are unsure, use it to look up the correct CWE. If you cannot be certain, omit this field entirely.
|
||||||
|
Always prefer the most specific child CWE over a broad parent.
|
||||||
|
For example, use CWE-89 instead of CWE-74, or CWE-78 instead of CWE-77.
|
||||||
|
|
||||||
|
Reference (ID only — names here are just for your reference, do NOT include them in the value):
|
||||||
|
- Injection: CWE-79 XSS, CWE-89 SQLi, CWE-78 OS Command Injection, CWE-94 Code Injection, CWE-77 Command Injection
|
||||||
|
- Auth/Access: CWE-287 Improper Authentication, CWE-862 Missing Authorization, CWE-863 Incorrect Authorization, CWE-306 Missing Authentication for Critical Function, CWE-639 Authorization Bypass Through User-Controlled Key
|
||||||
|
- Web: CWE-352 CSRF, CWE-918 SSRF, CWE-601 Open Redirect, CWE-434 Unrestricted Upload of File with Dangerous Type
|
||||||
|
- Memory: CWE-787 Out-of-bounds Write, CWE-125 Out-of-bounds Read, CWE-416 Use After Free, CWE-120 Classic Buffer Overflow
|
||||||
|
- Data: CWE-502 Deserialization of Untrusted Data, CWE-22 Path Traversal, CWE-611 XXE
|
||||||
|
- Crypto/Config: CWE-798 Use of Hard-coded Credentials, CWE-327 Use of Broken or Risky Cryptographic Algorithm, CWE-311 Missing Encryption of Sensitive Data, CWE-916 Password Hash With Insufficient Computational Effort
|
||||||
|
|
||||||
|
Do NOT use broad/parent CWEs like CWE-74, CWE-20, CWE-200, CWE-284, or CWE-693.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="code_before" type="string" required="false">
|
<parameter name="code_locations" type="string" required="false">
|
||||||
<description>MANDATORY for white-box testing: actual vulnerable code snippet(s) copied verbatim from the repository.</description>
|
<description>Nested XML list of code locations where the vulnerability exists. MANDATORY for white-box testing.
|
||||||
</parameter>
|
|
||||||
<parameter name="code_after" type="string" required="false">
|
CRITICAL — HOW fix_before/fix_after WORK:
|
||||||
<description>MANDATORY for white-box testing: corrected code snippet(s) exactly as they should appear after the fix.</description>
|
fix_before and fix_after are LITERAL BLOCK-LEVEL REPLACEMENTS used directly for GitHub/GitLab PR suggestion blocks. When a reviewer clicks "Accept suggestion", the platform replaces the EXACT lines from start_line to end_line with the fix_after content. This means:
|
||||||
</parameter>
|
|
||||||
<parameter name="code_diff" type="string" required="false">
|
1. fix_before MUST be an EXACT, VERBATIM copy of the source code at lines start_line through end_line. Same whitespace, same indentation, same line breaks. If fix_before does not match the actual file content character-for-character, the suggestion will be wrong or will corrupt the code when accepted.
|
||||||
<description>MANDATORY for white-box testing: unified diff showing the code changes. Must be a complete, apply-able unified diff (git format) covering all affected files, with proper file headers, line numbers, and sufficient context.</description>
|
|
||||||
|
2. fix_after is the COMPLETE replacement for that entire block. It replaces ALL lines from start_line to end_line. It can be more lines, fewer lines, or the same number of lines as fix_before.
|
||||||
|
|
||||||
|
3. start_line and end_line define the EXACT line range being replaced. They must precisely cover the lines in fix_before — no more, no less. If the vulnerable code spans lines 45-48, then start_line=45 and end_line=48, and fix_before must contain all 4 lines exactly as they appear in the file.
|
||||||
|
|
||||||
|
MULTI-PART FIXES:
|
||||||
|
Many fixes require changes in multiple non-contiguous parts of a file (e.g., adding an import at the top AND changing code lower down), or across multiple files. Since each fix_before/fix_after pair covers ONE contiguous block, you MUST create SEPARATE location entries for each part of the fix:
|
||||||
|
|
||||||
|
- Each location covers one contiguous block of lines to change
|
||||||
|
- Use the label field to describe how each part relates to the overall fix (e.g., "Add import for parameterized query library", "Replace string interpolation with parameterized query")
|
||||||
|
- Order fix locations logically: primary fix first (where the vulnerability manifests), then supporting changes (imports, config, etc.)
|
||||||
|
|
||||||
|
COMMON MISTAKES TO AVOID:
|
||||||
|
- Do NOT guess line numbers. Read the file and verify the exact lines before reporting.
|
||||||
|
- Do NOT paraphrase or reformat code in fix_before. It must be a verbatim copy.
|
||||||
|
- Do NOT set start_line=end_line when the vulnerable code spans multiple lines. Cover the full range.
|
||||||
|
- Do NOT put an import addition and a code change in the same fix_before/fix_after if they are not on adjacent lines. Split them into separate locations.
|
||||||
|
- Do NOT include lines outside the vulnerable/fixed code in fix_before just to "pad" the range.
|
||||||
|
- Do NOT duplicate changes across locations. Each location's fix_after must ONLY contain changes for its own line range. Never repeat a change that is already covered by another location.
|
||||||
|
|
||||||
|
Each location element fields:
|
||||||
|
- file (REQUIRED): Path relative to repository root. No leading slash, no absolute paths, no ".." traversal.
|
||||||
|
Correct: "src/db/queries.ts" or "app/routes/users.py"
|
||||||
|
Wrong: "/workspace/repo/src/db/queries.ts", "./src/db/queries.ts", "../../etc/passwd"
|
||||||
|
- start_line (REQUIRED): Exact 1-based line number where the vulnerable/affected code begins. Must be a positive integer. You must be certain of this number — go back and verify against the actual file content if needed.
|
||||||
|
- end_line (REQUIRED): Exact 1-based line number where the vulnerable/affected code ends. Must be >= start_line. Set equal to start_line ONLY if the code is truly on a single line.
|
||||||
|
- snippet (optional): The actual source code at this location, copied verbatim from the file.
|
||||||
|
- label (optional): Short role description for this location. For multi-part fixes, use this to explain the purpose of each change (e.g., "Add import for escape utility", "Sanitize user input before SQL query").
|
||||||
|
- fix_before (optional): The vulnerable code to be replaced — VERBATIM copy of lines start_line through end_line. Must match the actual source character-for-character including whitespace and indentation.
|
||||||
|
- fix_after (optional): The corrected code that replaces the entire fix_before block. Must be syntactically valid and ready to apply as a direct replacement.
|
||||||
|
|
||||||
|
Locations without fix_before/fix_after are informational context (e.g. showing the source of tainted data).
|
||||||
|
Locations with fix_before/fix_after are actionable fixes (used directly for PR suggestion blocks).</description>
|
||||||
|
<format>
|
||||||
|
<location>
|
||||||
|
<file>src/db/queries.ts</file>
|
||||||
|
<start_line>42</start_line>
|
||||||
|
<end_line>45</end_line>
|
||||||
|
<snippet>const query = (
|
||||||
|
`SELECT * FROM users ` +
|
||||||
|
`WHERE id = ${id}`
|
||||||
|
);</snippet>
|
||||||
|
<label>Unsanitized input used in SQL query (sink)</label>
|
||||||
|
<fix_before>const query = (
|
||||||
|
`SELECT * FROM users ` +
|
||||||
|
`WHERE id = ${id}`
|
||||||
|
);</fix_before>
|
||||||
|
<fix_after>const query = 'SELECT * FROM users WHERE id = $1';
|
||||||
|
const result = await db.query(query, [id]);</fix_after>
|
||||||
|
</location>
|
||||||
|
<location>
|
||||||
|
<file>src/routes/users.ts</file>
|
||||||
|
<start_line>15</start_line>
|
||||||
|
<end_line>15</end_line>
|
||||||
|
<snippet>const id = req.params.id</snippet>
|
||||||
|
<label>User input from request parameter (source)</label>
|
||||||
|
</location>
|
||||||
|
</format>
|
||||||
</parameter>
|
</parameter>
|
||||||
</parameters>
|
</parameters>
|
||||||
<returns type="Dict[str, Any]">
|
<returns type="Dict[str, Any]">
|
||||||
@@ -177,7 +228,6 @@ Impact validation:
|
|||||||
- Use a controlled internal endpoint (or a benign endpoint that returns a distinct marker) to demonstrate that the request is performed by the server, not the client.
|
- Use a controlled internal endpoint (or a benign endpoint that returns a distinct marker) to demonstrate that the request is performed by the server, not the client.
|
||||||
- If the application follows redirects, validate whether an allowlisted URL can redirect to a disallowed destination, and whether the redirected-to destination is still fetched.</parameter>
|
- If the application follows redirects, validate whether an allowlisted URL can redirect to a disallowed destination, and whether the redirected-to destination is still fetched.</parameter>
|
||||||
<parameter=poc_script_code>import json
|
<parameter=poc_script_code>import json
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
@@ -262,16 +312,58 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
6. Monitoring and alerting
|
6. Monitoring and alerting
|
||||||
- Log and alert on preview attempts to unusual destinations, repeated failures, high-frequency requests, or attempts to access blocked ranges.</parameter>
|
- Log and alert on preview attempts to unusual destinations, repeated failures, high-frequency requests, or attempts to access blocked ranges.</parameter>
|
||||||
<parameter=attack_vector>N</parameter>
|
<parameter=cvss_breakdown>
|
||||||
<parameter=attack_complexity>L</parameter>
|
<attack_vector>N</attack_vector>
|
||||||
<parameter=privileges_required>L</parameter>
|
<attack_complexity>L</attack_complexity>
|
||||||
<parameter=user_interaction>N</parameter>
|
<privileges_required>L</privileges_required>
|
||||||
<parameter=scope>C</parameter>
|
<user_interaction>N</user_interaction>
|
||||||
<parameter=confidentiality>H</parameter>
|
<scope>C</scope>
|
||||||
<parameter=integrity>H</parameter>
|
<confidentiality>H</confidentiality>
|
||||||
<parameter=availability>L</parameter>
|
<integrity>H</integrity>
|
||||||
|
<availability>L</availability>
|
||||||
|
</parameter>
|
||||||
<parameter=endpoint>/api/v1/link-preview</parameter>
|
<parameter=endpoint>/api/v1/link-preview</parameter>
|
||||||
<parameter=method>POST</parameter>
|
<parameter=method>POST</parameter>
|
||||||
|
<parameter=cwe>CWE-918</parameter>
|
||||||
|
<parameter=code_locations>
|
||||||
|
<location>
|
||||||
|
<file>src/services/link-preview.ts</file>
|
||||||
|
<start_line>45</start_line>
|
||||||
|
<end_line>48</end_line>
|
||||||
|
<snippet> const options = { timeout: 5000 };
|
||||||
|
const response = await fetch(userUrl, options);
|
||||||
|
const html = await response.text();
|
||||||
|
return extractMetadata(html);</snippet>
|
||||||
|
<label>Unvalidated user URL passed to server-side fetch (sink)</label>
|
||||||
|
<fix_before> const options = { timeout: 5000 };
|
||||||
|
const response = await fetch(userUrl, options);
|
||||||
|
const html = await response.text();
|
||||||
|
return extractMetadata(html);</fix_before>
|
||||||
|
<fix_after> const validated = await validateAndResolveUrl(userUrl);
|
||||||
|
if (!validated) throw new ForbiddenError('URL not allowed');
|
||||||
|
const options = { timeout: 5000 };
|
||||||
|
const response = await fetch(validated, options);
|
||||||
|
const html = await response.text();
|
||||||
|
return extractMetadata(html);</fix_after>
|
||||||
|
</location>
|
||||||
|
<location>
|
||||||
|
<file>src/services/link-preview.ts</file>
|
||||||
|
<start_line>2</start_line>
|
||||||
|
<end_line>2</end_line>
|
||||||
|
<snippet>import { extractMetadata } from '../utils/html';</snippet>
|
||||||
|
<label>Add import for URL validation utility</label>
|
||||||
|
<fix_before>import { extractMetadata } from '../utils/html';</fix_before>
|
||||||
|
<fix_after>import { extractMetadata } from '../utils/html';
|
||||||
|
import { validateAndResolveUrl } from '../utils/url-validator';</fix_after>
|
||||||
|
</location>
|
||||||
|
<location>
|
||||||
|
<file>src/routes/api/v1/links.ts</file>
|
||||||
|
<start_line>12</start_line>
|
||||||
|
<end_line>12</end_line>
|
||||||
|
<snippet>const userUrl = req.body.url</snippet>
|
||||||
|
<label>User-controlled URL from request body (source)</label>
|
||||||
|
</location>
|
||||||
|
</parameter>
|
||||||
</function>
|
</function>
|
||||||
</examples>
|
</examples>
|
||||||
</tool>
|
</tool>
|
||||||
|
|||||||
Reference in New Issue
Block a user