diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index f9b9106..85919ce 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. Ubuntu 22.04]
- Strix Version or Commit: [e.g. 0.1.18]
- 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**
Add any other context about the problem here.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d6b9a64..e272e0b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,7 +30,7 @@ Thank you for your interest in contributing to Strix! This guide will help you g
3. **Configure your LLM provider**
```bash
- export STRIX_LLM="openai/gpt-5"
+ export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="your-api-key"
```
diff --git a/README.md b/README.md
index 5e2a0be..fd79f86 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,9 @@ Strix are autonomous AI agents that act just like real hackers - they run your c
**Prerequisites:**
- 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
@@ -84,7 +86,7 @@ curl -sSL https://strix.ai/install | bash
pipx install strix-agent
# 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"
# Run your first security assessment
@@ -201,7 +203,7 @@ jobs:
### Configuration
```bash
-export STRIX_LLM="openai/gpt-5"
+export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="your-api-key"
# Optional
@@ -215,8 +217,8 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
**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`
-- [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`
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx
index 9a6d9e4..bd71c68 100644
--- a/docs/advanced/configuration.mdx
+++ b/docs/advanced/configuration.mdx
@@ -8,7 +8,7 @@ Configure Strix using environment variables or a config file.
## LLM Configuration
- 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`).
@@ -86,7 +86,7 @@ strix --target ./app --config /path/to/config.json
```json
{
"env": {
- "STRIX_LLM": "openai/gpt-5",
+ "STRIX_LLM": "anthropic/claude-sonnet-4-6",
"LLM_API_KEY": "sk-...",
"STRIX_REASONING_EFFORT": "high"
}
@@ -97,7 +97,7 @@ strix --target ./app --config /path/to/config.json
```bash
# Required
-export STRIX_LLM="openai/gpt-5"
+export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="sk-..."
# Optional: Enable web search
diff --git a/docs/contributing.mdx b/docs/contributing.mdx
index b2e50a0..ffa3192 100644
--- a/docs/contributing.mdx
+++ b/docs/contributing.mdx
@@ -32,7 +32,7 @@ description: "Contribute to Strix development"
```bash
- export STRIX_LLM="openai/gpt-5"
+ export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="your-api-key"
```
diff --git a/docs/docs.json b/docs/docs.json
index e15b496..27ee5dc 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -32,6 +32,7 @@
"group": "LLM Providers",
"pages": [
"llm-providers/overview",
+ "llm-providers/models",
"llm-providers/openai",
"llm-providers/anthropic",
"llm-providers/openrouter",
diff --git a/docs/index.mdx b/docs/index.mdx
index ef5ab9a..14de192 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -78,7 +78,7 @@ Strix uses a graph of specialized agents for comprehensive security testing:
curl -sSL https://strix.ai/install | bash
# Configure
-export STRIX_LLM="openai/gpt-5"
+export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="your-api-key"
# Scan
diff --git a/docs/integrations/github-actions.mdx b/docs/integrations/github-actions.mdx
index 827dce0..fcc5eb9 100644
--- a/docs/integrations/github-actions.mdx
+++ b/docs/integrations/github-actions.mdx
@@ -35,7 +35,7 @@ Add these secrets to your repository:
| 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 |
## Exit Codes
diff --git a/docs/llm-providers/anthropic.mdx b/docs/llm-providers/anthropic.mdx
index 81680a1..b7b3085 100644
--- a/docs/llm-providers/anthropic.mdx
+++ b/docs/llm-providers/anthropic.mdx
@@ -6,7 +6,7 @@ description: "Configure Strix with Claude models"
## Setup
```bash
-export STRIX_LLM="anthropic/claude-sonnet-4-5"
+export STRIX_LLM="anthropic/claude-sonnet-4-6"
export LLM_API_KEY="sk-ant-..."
```
@@ -14,8 +14,8 @@ export LLM_API_KEY="sk-ant-..."
| Model | Description |
|-------|-------------|
-| `anthropic/claude-sonnet-4-5` | Best balance of intelligence and speed (recommended) |
-| `anthropic/claude-opus-4-5` | Maximum capability for deep analysis |
+| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed (recommended) |
+| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
## Get API Key
diff --git a/docs/llm-providers/models.mdx b/docs/llm-providers/models.mdx
new file mode 100644
index 0000000..54007a9
--- /dev/null
+++ b/docs/llm-providers/models.mdx
@@ -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.
+
+
+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.
+
+
+## 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
+
+
+ Your Strix API key from [models.strix.ai](https://models.strix.ai).
+
+
+
+ Model ID from the tables above. Must be prefixed with `strix/`.
+
diff --git a/docs/llm-providers/openrouter.mdx b/docs/llm-providers/openrouter.mdx
index 31919c1..d4d36bf 100644
--- a/docs/llm-providers/openrouter.mdx
+++ b/docs/llm-providers/openrouter.mdx
@@ -19,7 +19,7 @@ Access any model on OpenRouter using the format `openrouter//`:
| Model | Configuration |
|-------|---------------|
| 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` |
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
diff --git a/docs/llm-providers/overview.mdx b/docs/llm-providers/overview.mdx
index 9027aac..567af50 100644
--- a/docs/llm-providers/overview.mdx
+++ b/docs/llm-providers/overview.mdx
@@ -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.
-## 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 |
| ----------------- | ------------- | -------------------------------- |
+| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
| 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` |
-## Quick Setup
-
```bash
-export STRIX_LLM="openai/gpt-5"
+export STRIX_LLM="anthropic/claude-sonnet-4-6"
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
+
+ Recommended models router with high rate limits.
+
GPT-5 and Codex models.
- Claude 4.5 Sonnet, Opus, and Haiku.
+ Claude Sonnet 4.6, Opus, and Haiku.
Access 100+ models through a single API.
@@ -38,7 +61,7 @@ export LLM_API_KEY="your-api-key"
Gemini 3 models via Google Cloud.
- Claude 4.5 and Titan models via AWS.
+ Claude and Titan models via AWS.
GPT-5 via Azure.
@@ -53,8 +76,8 @@ export LLM_API_KEY="your-api-key"
Use LiteLLM's `provider/model-name` format:
```
+anthropic/claude-sonnet-4-6
openai/gpt-5
-anthropic/claude-sonnet-4-5
vertex_ai/gemini-3-pro-preview
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
ollama/llama4
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index 487caae..32eac3d 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -6,7 +6,7 @@ description: "Install Strix and run your first security scan"
## Prerequisites
- 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
@@ -27,13 +27,23 @@ description: "Install Strix and run your first security scan"
Set your LLM provider:
-```bash
-export STRIX_LLM="openai/gpt-5"
-export LLM_API_KEY="your-api-key"
-```
+
+
+ ```bash
+ export STRIX_LLM="strix/claude-sonnet-4.6"
+ export LLM_API_KEY="your-strix-api-key"
+ ```
+
+
+ ```bash
+ export STRIX_LLM="anthropic/claude-sonnet-4-6"
+ export LLM_API_KEY="your-api-key"
+ ```
+
+
-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`.
## Run Your First Scan
diff --git a/poetry.lock b/poetry.lock
index e91e9dc..9367ef4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1104,56 +1104,74 @@ pytz = ">2021.1"
[[package]]
name = "cryptography"
-version = "44.0.1"
+version = "46.0.5"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+python-versions = "!=3.9.0,!=3.9.1,>=3.8"
groups = ["main"]
files = [
- {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"},
- {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"},
- {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"},
- {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"},
- {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"},
- {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"},
- {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"},
- {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"},
- {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"},
- {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"},
- {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"},
- {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"},
- {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"},
- {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"},
+ {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"},
+ {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"},
+ {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"},
+ {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"},
+ {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"},
+ {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"},
+ {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"},
+ {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"},
+ {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"},
+ {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"},
+ {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"},
+ {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"},
+ {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"},
+ {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"},
+ {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"},
+ {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"},
]
[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
+docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
-pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+nox = ["nox[uv] (>=2024.4.15)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -4353,128 +4371,112 @@ ptyprocess = ">=0.5"
[[package]]
name = "pillow"
-version = "11.3.0"
-description = "Python Imaging Library (Fork)"
+version = "12.1.1"
+description = "Python Imaging Library (fork)"
optional = true
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"sandbox\""
files = [
- {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
- {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
- {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"},
- {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"},
- {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"},
- {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"},
- {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"},
- {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"},
- {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"},
- {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"},
- {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"},
- {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"},
- {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"},
- {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"},
- {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"},
- {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"},
- {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"},
- {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"},
- {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"},
- {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"},
- {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"},
- {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"},
- {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"},
- {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"},
- {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"},
- {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"},
- {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"},
- {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"},
- {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"},
- {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"},
- {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"},
- {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"},
- {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"},
- {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"},
- {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"},
- {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"},
- {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"},
- {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"},
- {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"},
- {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"},
- {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"},
- {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"},
- {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"},
- {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"},
- {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"},
- {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"},
- {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"},
- {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"},
- {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"},
- {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"},
- {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"},
- {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"},
- {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"},
- {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"},
- {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"},
- {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"},
- {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"},
- {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"},
- {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"},
- {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"},
- {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"},
- {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"},
- {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"},
- {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"},
- {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"},
- {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"},
- {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"},
- {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"},
- {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"},
- {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"},
- {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"},
- {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"},
- {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"},
- {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"},
- {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"},
- {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"},
- {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"},
- {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"},
- {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"},
- {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"},
- {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"},
- {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"},
- {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"},
- {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"},
- {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"},
- {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"},
- {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"},
- {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"},
- {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"},
- {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"},
- {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"},
- {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"},
- {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"},
- {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"},
+ {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"},
+ {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"},
+ {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"},
+ {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"},
+ {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"},
+ {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"},
+ {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"},
+ {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"},
+ {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"},
+ {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"},
+ {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"},
+ {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"},
+ {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"},
+ {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"},
+ {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"},
+ {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"},
+ {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"},
+ {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"},
+ {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"},
+ {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"},
+ {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"},
+ {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"},
+ {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"},
+ {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"},
+ {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"},
+ {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"},
+ {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"},
+ {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"},
+ {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"},
+ {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"},
+ {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"},
+ {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"},
+ {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"},
+ {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"},
+ {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"},
+ {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"},
+ {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"},
+ {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"},
+ {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"},
+ {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"},
+ {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"},
+ {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"},
+ {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"},
+ {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"},
+ {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"},
+ {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"},
+ {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"},
+ {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"},
+ {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"},
+ {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"},
+ {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"},
+ {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"},
+ {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"},
+ {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"},
+ {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"},
+ {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"},
+ {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"},
+ {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"},
+ {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"},
+ {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"},
+ {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"},
+ {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"},
+ {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"},
+ {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"},
+ {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"},
+ {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"},
+ {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"},
+ {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"},
+ {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"},
+ {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"},
+ {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"},
+ {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"},
+ {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"},
+ {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"},
+ {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"},
+ {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"},
+ {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"},
+ {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"},
+ {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"},
+ {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"},
+ {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"},
+ {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"},
+ {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"},
+ {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"},
+ {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
-test-arrow = ["pyarrow"]
-tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
-typing = ["typing-extensions ; python_version < \"3.10\""]
+test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
+tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
xmp = ["defusedxml"]
[[package]]
@@ -4741,23 +4743,23 @@ testing = ["google-api-core (>=1.31.5)"]
[[package]]
name = "protobuf"
-version = "6.33.4"
+version = "6.33.5"
description = ""
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"vertex\""
files = [
- {file = "protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d"},
- {file = "protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc"},
- {file = "protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0"},
- {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e"},
- {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6"},
- {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9"},
- {file = "protobuf-6.33.4-cp39-cp39-win32.whl", hash = "sha256:955478a89559fa4568f5a81dce77260eabc5c686f9e8366219ebd30debf06aa6"},
- {file = "protobuf-6.33.4-cp39-cp39-win_amd64.whl", hash = "sha256:0f12ddbf96912690c3582f9dffb55530ef32015ad8e678cd494312bd78314c4f"},
- {file = "protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc"},
- {file = "protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91"},
+ {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"},
+ {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"},
+ {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"},
+ {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"},
+ {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"},
+ {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"},
+ {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"},
+ {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"},
+ {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"},
+ {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"},
]
[[package]]
@@ -5235,20 +5237,20 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pypdf"
-version = "6.6.2"
+version = "6.7.1"
description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "pypdf-6.6.2-py3-none-any.whl", hash = "sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba"},
- {file = "pypdf-6.6.2.tar.gz", hash = "sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016"},
+ {file = "pypdf-6.7.1-py3-none-any.whl", hash = "sha256:a02ccbb06463f7c334ce1612e91b3e68a8e827f3cee100b9941771e6066b094e"},
+ {file = "pypdf-6.7.1.tar.gz", hash = "sha256:6b7a63be5563a0a35d54c6d6b550d75c00b8ccf36384be96365355e296e6b3b0"},
]
[package.extras]
crypto = ["cryptography"]
cryptodome = ["PyCryptodome"]
-dev = ["black", "flit", "pip-tools", "pre-commit", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"]
+dev = ["flit", "pip-tools", "pre-commit", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"]
docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
full = ["Pillow (>=8.0.0)", "cryptography"]
image = ["Pillow (>=8.0.0)"]
diff --git a/pyproject.toml b/pyproject.toml
index 6c32647..ab8ca11 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "strix-agent"
-version = "0.7.0"
+version = "0.8.0"
description = "Open-source AI Hackers for your apps"
authors = ["Strix "]
readme = "README.md"
diff --git a/scripts/install.sh b/scripts/install.sh
index ae5b11d..7fb158b 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -335,14 +335,18 @@ echo -e "${MUTED} AI Penetration Testing Agent${NC}"
echo ""
echo -e "${MUTED}To get started:${NC}"
echo ""
-echo -e " ${CYAN}1.${NC} Set your LLM provider:"
-echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5'${NC}"
-echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
+echo -e " ${CYAN}1.${NC} Get your Strix API key:"
+echo -e " ${MUTED}https://models.strix.ai${NC}"
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 ""
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 ""
diff --git a/strix/config/config.py b/strix/config/config.py
index 387834b..f8836b2 100644
--- a/strix/config/config.py
+++ b/strix/config/config.py
@@ -5,6 +5,9 @@ from pathlib import Path
from typing import Any
+STRIX_API_BASE = "https://models.strix.ai/api/v1"
+
+
class Config:
"""Configuration Manager for Strix."""
@@ -177,3 +180,31 @@ def apply_saved_config(force: bool = False) -> dict[str, str]:
def save_current_config() -> bool:
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_name: Original model name (strix/ prefix preserved for display)
+ - api_key: LLM API key
+ - api_base: API base URL (auto-set to STRIX_API_BASE for strix/ models)
+ """
+ model = Config.get("strix_llm")
+ if not model:
+ return None, None, None
+
+ api_key = Config.get("llm_api_key")
+
+ if model.startswith("strix/"):
+ api_base: str | None = STRIX_API_BASE
+ else:
+ 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, api_key, api_base
diff --git a/strix/interface/main.py b/strix/interface/main.py
index 58d52a0..5df4ac5 100644
--- a/strix/interface/main.py
+++ b/strix/interface/main.py
@@ -18,7 +18,8 @@ from rich.panel import Panel
from rich.text import Text
from strix.config import Config, apply_saved_config, save_current_config
-from strix.llm.utils import get_litellm_model_name, get_strix_api_base
+from strix.config.config import resolve_llm_config
+from strix.llm.utils import get_litellm_model_name
apply_saved_config()
@@ -52,10 +53,13 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
missing_required_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")
- has_base_url = any(
+ has_base_url = uses_strix_models or any(
[
Config.get("llm_api_base"),
Config.get("openai_api_base"),
@@ -97,7 +101,7 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
error_text.append("• ", style="white")
error_text.append("STRIX_LLM", style="bold cyan")
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",
)
@@ -136,7 +140,10 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
)
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:
for var in missing_optional_vars:
@@ -202,15 +209,7 @@ async def warm_up_llm() -> None:
console = Console()
try:
- model_name = Config.get("strix_llm")
- 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")
- or get_strix_api_base(model_name)
- )
+ model_name, api_key, api_base = resolve_llm_config()
test_messages = [
{"role": "system", "content": "You are a helpful assistant."},
diff --git a/strix/interface/tool_components/reporting_renderer.py b/strix/interface/tool_components/reporting_renderer.py
index 98cc85d..898157d 100644
--- a/strix/interface/tool_components/reporting_renderer.py
+++ b/strix/interface/tool_components/reporting_renderer.py
@@ -6,6 +6,11 @@ from pygments.styles import get_style_by_name
from rich.text import Text
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 .registry import register_tool_renderer
@@ -17,6 +22,13 @@ def _get_style_colors() -> dict[Any, str]:
FIELD_STYLE = "bold #4ade80"
+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
@@ -80,18 +92,13 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
poc_script_code = args.get("poc_script_code", "")
remediation_steps = args.get("remediation_steps", "")
- attack_vector = args.get("attack_vector", "")
- attack_complexity = args.get("attack_complexity", "")
- 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", "")
+ cvss_breakdown_xml = args.get("cvss_breakdown", "")
+ code_locations_xml = args.get("code_locations", "")
endpoint = args.get("endpoint", "")
method = args.get("method", "")
cve = args.get("cve", "")
+ cwe = args.get("cwe", "")
severity = ""
cvss_score = None
@@ -140,38 +147,30 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("CVE: ", style=FIELD_STYLE)
text.append(cve)
- if any(
- [
- attack_vector,
- attack_complexity,
- privileges_required,
- user_interaction,
- scope,
- confidentiality,
- integrity,
- availability,
- ]
- ):
+ if cwe:
+ text.append("\n\n")
+ text.append("CWE: ", style=FIELD_STYLE)
+ text.append(cwe)
+
+ parsed_cvss = parse_cvss_xml(cvss_breakdown_xml) if cvss_breakdown_xml else None
+ if parsed_cvss:
text.append("\n\n")
cvss_parts = []
- if attack_vector:
- cvss_parts.append(f"AV:{attack_vector}")
- if attack_complexity:
- cvss_parts.append(f"AC:{attack_complexity}")
- if privileges_required:
- cvss_parts.append(f"PR:{privileges_required}")
- if user_interaction:
- cvss_parts.append(f"UI:{user_interaction}")
- if scope:
- cvss_parts.append(f"S:{scope}")
- if confidentiality:
- cvss_parts.append(f"C:{confidentiality}")
- if integrity:
- cvss_parts.append(f"I:{integrity}")
- if availability:
- cvss_parts.append(f"A:{availability}")
+ for key, prefix in [
+ ("attack_vector", "AV"),
+ ("attack_complexity", "AC"),
+ ("privileges_required", "PR"),
+ ("user_interaction", "UI"),
+ ("scope", "S"),
+ ("confidentiality", "C"),
+ ("integrity", "I"),
+ ("availability", "A"),
+ ]:
+ val = parsed_cvss.get(key)
+ if val:
+ cvss_parts.append(f"{prefix}:{val}")
text.append("CVSS Vector: ", style=FIELD_STYLE)
- text.append("/".join(cvss_parts), style="dim")
+ text.append("/".join(cvss_parts), style=DIM_STYLE)
if description:
text.append("\n\n")
@@ -191,6 +190,40 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("\n")
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:
text.append("\n\n")
text.append("PoC Description", style=FIELD_STYLE)
diff --git a/strix/interface/tui.py b/strix/interface/tui.py
index 49a0489..cb1adff 100644
--- a/strix/interface/tui.py
+++ b/strix/interface/tui.py
@@ -531,16 +531,30 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
lines.append("```")
# Code Analysis
- if vuln.get("code_file") or vuln.get("code_diff"):
+ if vuln.get("code_locations"):
lines.extend(["", "## Code Analysis", ""])
- if vuln.get("code_file"):
- lines.append(f"**File:** {vuln['code_file']}")
+ for i, loc in enumerate(vuln["code_locations"]):
+ 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("")
- if vuln.get("code_diff"):
- lines.append("**Changes:**")
- lines.append("```diff")
- lines.append(vuln["code_diff"])
- lines.append("```")
# Remediation
if vuln.get("remediation_steps"):
diff --git a/strix/interface/utils.py b/strix/interface/utils.py
index 8dbaebb..fe5bdfc 100644
--- a/strix/interface/utils.py
+++ b/strix/interface/utils.py
@@ -163,32 +163,34 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
text.append("\n")
text.append(poc_script_code, style="dim")
- code_file = report.get("code_file")
- if code_file:
+ code_locations = report.get("code_locations")
+ if code_locations:
text.append("\n\n")
- text.append("Code File: ", style=field_style)
- text.append(code_file)
-
- code_before = report.get("code_before")
- if code_before:
- text.append("\n\n")
- text.append("Code Before", style=field_style)
- text.append("\n")
- text.append(code_before, style="dim")
-
- code_after = report.get("code_after")
- if code_after:
- text.append("\n\n")
- text.append("Code After", style=field_style)
- text.append("\n")
- text.append(code_after, style="dim")
-
- code_diff = report.get("code_diff")
- if code_diff:
- text.append("\n\n")
- text.append("Code Diff", style=field_style)
- text.append("\n")
- text.append(code_diff, style="dim")
+ text.append("Code Locations", style=field_style)
+ for i, loc in enumerate(code_locations):
+ text.append("\n\n")
+ text.append(f" Location {i + 1}: ", style="dim")
+ text.append(loc.get("file", "unknown"), style="bold")
+ 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}")
+ else:
+ text.append(f":{start}")
+ if loc.get("label"):
+ text.append(f"\n {loc['label']}", style="italic dim")
+ if loc.get("snippet"):
+ text.append("\n ")
+ text.append(loc["snippet"], style="dim")
+ if loc.get("fix_before") or loc.get("fix_after"):
+ text.append("\n Fix:")
+ if loc.get("fix_before"):
+ text.append("\n - ", style="dim")
+ text.append(loc["fix_before"], 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")
if remediation_steps:
diff --git a/strix/llm/config.py b/strix/llm/config.py
index 3426327..1ee2ddd 100644
--- a/strix/llm/config.py
+++ b/strix/llm/config.py
@@ -1,4 +1,5 @@
from strix.config import Config
+from strix.config.config import resolve_llm_config
class LLMConfig:
@@ -10,7 +11,8 @@ class LLMConfig:
timeout: int | None = None,
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:
raise ValueError("STRIX_LLM environment variable must be set and not empty")
diff --git a/strix/llm/dedupe.py b/strix/llm/dedupe.py
index f8cdb08..33b3bc9 100644
--- a/strix/llm/dedupe.py
+++ b/strix/llm/dedupe.py
@@ -5,8 +5,8 @@ from typing import Any
import litellm
-from strix.config import Config
-from strix.llm.utils import get_litellm_model_name, get_strix_api_base
+from strix.config.config import resolve_llm_config
+from strix.llm.utils import get_litellm_model_name
logger = logging.getLogger(__name__)
@@ -156,15 +156,7 @@ def check_duplicate(
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
- model_name = Config.get("strix_llm")
- 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")
- or get_strix_api_base(model_name)
- )
+ model_name, api_key, api_base = resolve_llm_config()
messages = [
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
diff --git a/strix/llm/llm.py b/strix/llm/llm.py
index d1b6370..d6373ec 100644
--- a/strix/llm/llm.py
+++ b/strix/llm/llm.py
@@ -15,7 +15,6 @@ from strix.llm.utils import (
_truncate_to_first_function,
fix_incomplete_tool_call,
get_litellm_model_name,
- get_strix_api_base,
parse_tool_invocations,
)
from strix.skills import load_skills
@@ -206,18 +205,10 @@ class LLM:
"stream_options": {"include_usage": True},
}
- if api_key := Config.get("llm_api_key"):
- args["api_key"] = 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")
- or get_strix_api_base(self.config.model_name)
- )
- if api_base:
- args["api_base"] = api_base
+ if self.config.api_key:
+ args["api_key"] = self.config.api_key
+ if self.config.api_base:
+ args["api_base"] = self.config.api_base
if self._supports_reasoning():
args["reasoning_effort"] = self._reasoning_effort
diff --git a/strix/llm/memory_compressor.py b/strix/llm/memory_compressor.py
index e46b331..4590972 100644
--- a/strix/llm/memory_compressor.py
+++ b/strix/llm/memory_compressor.py
@@ -3,8 +3,8 @@ from typing import Any
import litellm
-from strix.config import Config
-from strix.llm.utils import get_litellm_model_name, get_strix_api_base
+from strix.config.config import Config, resolve_llm_config
+from strix.llm.utils import get_litellm_model_name
logger = logging.getLogger(__name__)
@@ -106,14 +106,7 @@ def _summarize_messages(
conversation = "\n".join(formatted)
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
- 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")
- or get_strix_api_base(model)
- )
+ _, api_key, api_base = resolve_llm_config()
try:
litellm_model = get_litellm_model_name(model) or model
diff --git a/strix/llm/utils.py b/strix/llm/utils.py
index 8abe6ba..c7d83a9 100644
--- a/strix/llm/utils.py
+++ b/strix/llm/utils.py
@@ -3,8 +3,6 @@ import re
from typing import Any
-STRIX_API_BASE = "https://models.strix.ai/api/v1"
-
STRIX_PROVIDER_PREFIXES: dict[str, str] = {
"claude-": "anthropic",
"gpt-": "openai",
@@ -12,18 +10,6 @@ STRIX_PROVIDER_PREFIXES: dict[str, str] = {
}
-def is_strix_model(model_name: str | None) -> bool:
- """Check if model uses strix/ prefix."""
- return bool(model_name and model_name.startswith("strix/"))
-
-
-def get_strix_api_base(model_name: str | None) -> str | None:
- """Return Strix API base URL if using strix/ model, None otherwise."""
- if is_strix_model(model_name):
- return STRIX_API_BASE
- return None
-
-
def get_litellm_model_name(model_name: str | None) -> str | None:
"""Convert strix/ prefixed model to litellm-compatible provider/model format.
diff --git a/strix/skills/__init__.py b/strix/skills/__init__.py
index c3ac391..c9cdf03 100644
--- a/strix/skills/__init__.py
+++ b/strix/skills/__init__.py
@@ -130,7 +130,7 @@ def load_skills(skill_names: list[str]) -> dict[str, str]:
if skill_path and (skills_dir / skill_path).exists():
full_path = skills_dir / skill_path
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()
skill_content[var_name] = content
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py
index 25af62c..8bbb412 100644
--- a/strix/telemetry/tracer.py
+++ b/strix/telemetry/tracer.py
@@ -89,10 +89,8 @@ class Tracer:
endpoint: str | None = None,
method: str | None = None,
cve: str | None = None,
- code_file: str | None = None,
- code_before: str | None = None,
- code_after: str | None = None,
- code_diff: str | None = None,
+ cwe: str | None = None,
+ code_locations: list[dict[str, Any]] | None = None,
) -> str:
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
@@ -127,14 +125,10 @@ class Tracer:
report["method"] = method.strip()
if cve:
report["cve"] = cve.strip()
- if code_file:
- report["code_file"] = code_file.strip()
- if code_before:
- report["code_before"] = code_before.strip()
- if code_after:
- report["code_after"] = code_after.strip()
- if code_diff:
- report["code_diff"] = code_diff.strip()
+ if cwe:
+ report["cwe"] = cwe.strip()
+ if code_locations:
+ report["code_locations"] = code_locations
self.vulnerability_reports.append(report)
logger.info(f"Added vulnerability report: {report_id} - {title}")
@@ -323,6 +317,7 @@ class Tracer:
("Endpoint", report.get("endpoint")),
("Method", report.get("method")),
("CVE", report.get("cve")),
+ ("CWE", report.get("cwe")),
]
cvss_score = report.get("cvss")
if cvss_score is not None:
@@ -353,15 +348,33 @@ class Tracer:
f.write(f"{report['poc_script_code']}\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")
- if report.get("code_file"):
- f.write(f"**File:** {report['code_file']}\n\n")
- if report.get("code_diff"):
- f.write("**Changes:**\n")
- f.write("```diff\n")
- f.write(f"{report['code_diff']}\n")
- f.write("```\n\n")
+ for i, loc in enumerate(report["code_locations"]):
+ prefix = f"**Location {i + 1}:**"
+ 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']})"
+ 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"):
f.write("## Remediation\n\n")
diff --git a/strix/tools/registry.py b/strix/tools/registry.py
index ac6e15f..7313bc3 100644
--- a/strix/tools/registry.py
+++ b/strix/tools/registry.py
@@ -48,7 +48,7 @@ def _load_xml_schema(path: Path) -> Any:
if not path.exists():
return None
try:
- content = path.read_text()
+ content = path.read_text(encoding="utf-8")
content = _process_dynamic_content(content)
diff --git a/strix/tools/reporting/reporting_actions.py b/strix/tools/reporting/reporting_actions.py
index c307d93..b84797f 100644
--- a/strix/tools/reporting/reporting_actions.py
+++ b/strix/tools/reporting/reporting_actions.py
@@ -1,8 +1,120 @@
+import contextlib
+import re
+from pathlib import PurePosixPath
from typing import Any
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"(.*?)", 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(
attack_vector: str,
attack_complexity: str,
@@ -87,7 +199,7 @@ def _validate_cvss_parameters(**kwargs: str) -> list[str]:
@register_tool(sandbox_execution=False)
-def create_vulnerability_report(
+def create_vulnerability_report( # noqa: PLR0912
title: str,
description: str,
impact: str,
@@ -96,23 +208,12 @@ def create_vulnerability_report(
poc_description: str,
poc_script_code: str,
remediation_steps: str,
- # CVSS Breakdown Components
- attack_vector: str,
- attack_complexity: str,
- privileges_required: str,
- user_interaction: str,
- scope: str,
- confidentiality: str,
- integrity: str,
- availability: str,
- # Optional fields
+ cvss_breakdown: str,
endpoint: str | None = None,
method: str | None = None,
cve: str | None = None,
- code_file: str | None = None,
- code_before: str | None = None,
- code_after: str | None = None,
- code_diff: str | None = None,
+ cwe: str | None = None,
+ code_locations: str | None = None,
) -> dict[str, Any]:
validation_errors = _validate_required_fields(
title=title,
@@ -125,32 +226,32 @@ def create_vulnerability_report(
remediation_steps=remediation_steps,
)
- validation_errors.extend(
- _validate_cvss_parameters(
- attack_vector=attack_vector,
- attack_complexity=attack_complexity,
- privileges_required=privileges_required,
- user_interaction=user_interaction,
- scope=scope,
- confidentiality=confidentiality,
- integrity=integrity,
- availability=availability,
- )
- )
+ parsed_cvss = parse_cvss_xml(cvss_breakdown)
+ if not parsed_cvss:
+ validation_errors.append("cvss: could not parse CVSS breakdown XML")
+ else:
+ validation_errors.extend(_validate_cvss_parameters(**parsed_cvss))
+
+ parsed_locations = parse_code_locations_xml(code_locations) if code_locations else None
+
+ if parsed_locations:
+ 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:
return {"success": False, "message": "Validation failed", "errors": validation_errors}
- cvss_score, severity, cvss_vector = calculate_cvss_and_severity(
- attack_vector,
- attack_complexity,
- privileges_required,
- user_interaction,
- scope,
- confidentiality,
- integrity,
- availability,
- )
+ assert parsed_cvss is not None
+ cvss_score, severity, cvss_vector = calculate_cvss_and_severity(**parsed_cvss)
try:
from strix.telemetry.tracer import get_global_tracer
@@ -196,17 +297,6 @@ def create_vulnerability_report(
"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(
title=title,
description=description,
@@ -218,14 +308,12 @@ def create_vulnerability_report(
poc_script_code=poc_script_code,
remediation_steps=remediation_steps,
cvss=cvss_score,
- cvss_breakdown=cvss_breakdown,
+ cvss_breakdown=parsed_cvss,
endpoint=endpoint,
method=method,
cve=cve,
- code_file=code_file,
- code_before=code_before,
- code_after=code_after,
- code_diff=code_diff,
+ cwe=cwe,
+ code_locations=parsed_locations,
)
return {
diff --git a/strix/tools/reporting/reporting_actions_schema.xml b/strix/tools/reporting/reporting_actions_schema.xml
index 56c6cc0..0f4780a 100644
--- a/strix/tools/reporting/reporting_actions_schema.xml
+++ b/strix/tools/reporting/reporting_actions_schema.xml
@@ -14,7 +14,7 @@ DO NOT USE:
- 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)
-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.
@@ -30,7 +30,7 @@ Professional, customer-facing report rules (PDF-ready):
6) Impact
7) Remediation
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.
@@ -58,51 +58,28 @@ Professional, customer-facing report rules (PDF-ready):
Specific, actionable steps to fix the vulnerability
-
- CVSS Attack Vector - How the vulnerability is exploited:
-N = Network (remotely exploitable)
-A = Adjacent (same network segment)
-L = Local (local access required)
-P = Physical (physical access required)
-
-
- CVSS Attack Complexity - Conditions beyond attacker's control:
-L = Low (no special conditions)
-H = High (special conditions must exist)
-
-
- CVSS Privileges Required - Level of privileges needed:
-N = None (no privileges needed)
-L = Low (basic user privileges)
-H = High (admin privileges)
-
-
- CVSS User Interaction - Does exploit require user action:
-N = None (no user interaction needed)
-R = Required (user must perform some action)
-
-
- 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)
-
-
- CVSS Confidentiality Impact - Impact to confidentiality:
-N = None (no impact)
-L = Low (some information disclosure)
-H = High (all information disclosed)
-
-
- 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)
-
-
- CVSS Availability Impact - Impact to availability:
-N = None (no impact)
-L = Low (reduced performance or interruptions)
-H = High (total loss of availability)
+
+ CVSS 3.1 base score breakdown as nested XML. All 8 metrics are required.
+
+Each metric element contains a single uppercase letter value:
+- attack_vector: N (Network), A (Adjacent), L (Local), P (Physical)
+- attack_complexity: L (Low), H (High)
+- privileges_required: N (None), L (Low), H (High)
+- user_interaction: N (None), R (Required)
+- scope: U (Unchanged), C (Changed)
+- confidentiality: N (None), L (Low), H (High)
+- integrity: N (None), L (Low), H (High)
+- availability: N (None), L (Low), H (High)
+
+ N
+ L
+ N
+ N
+ U
+ H
+ H
+ N
+
API endpoint(s) or URL path(s) (e.g., "/api/login") - for web vulnerabilities, or Git repository path(s) - for code vulnerabilities
@@ -111,19 +88,93 @@ H = High (total loss of availability)
HTTP method(s) (GET, POST, etc.) - for web vulnerabilities.
- 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.
+ 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.
-
- MANDATORY for white-box testing: exact affected source file path(s).
+
+ 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.
-
- MANDATORY for white-box testing: actual vulnerable code snippet(s) copied verbatim from the repository.
-
-
- MANDATORY for white-box testing: corrected code snippet(s) exactly as they should appear after the fix.
-
-
- 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.
+
+ Nested XML list of code locations where the vulnerability exists. MANDATORY for white-box testing.
+
+CRITICAL — HOW fix_before/fix_after WORK:
+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:
+
+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.
+
+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).
+
+
+ src/db/queries.ts
+ 42
+ 45
+ const query = (
+ `SELECT * FROM users ` +
+ `WHERE id = ${id}`
+);
+
+ const query = (
+ `SELECT * FROM users ` +
+ `WHERE id = ${id}`
+);
+ const query = 'SELECT * FROM users WHERE id = $1';
+const result = await db.query(query, [id]);
+
+
+ src/routes/users.ts
+ 15
+ 15
+ const id = req.params.id
+
+
+
@@ -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.
- 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.
import json
-import sys
import time
from urllib.parse import urljoin
@@ -262,16 +312,58 @@ if __name__ == "__main__":
6. Monitoring and alerting
- Log and alert on preview attempts to unusual destinations, repeated failures, high-frequency requests, or attempts to access blocked ranges.
-N
-L
-L
-N
-C
-H
-H
-L
+
+ N
+ L
+ L
+ N
+ C
+ H
+ H
+ L
+
/api/v1/link-preview
POST
+CWE-918
+
+
+ src/services/link-preview.ts
+ 45
+ 48
+ const options = { timeout: 5000 };
+ const response = await fetch(userUrl, options);
+ const html = await response.text();
+ return extractMetadata(html);
+
+ const options = { timeout: 5000 };
+ const response = await fetch(userUrl, options);
+ const html = await response.text();
+ return extractMetadata(html);
+ 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);
+
+
+ src/services/link-preview.ts
+ 2
+ 2
+ import { extractMetadata } from '../utils/html';
+
+ import { extractMetadata } from '../utils/html';
+ import { extractMetadata } from '../utils/html';
+import { validateAndResolveUrl } from '../utils/url-validator';
+
+
+ src/routes/api/v1/links.ts
+ 12
+ 12
+ const userUrl = req.body.url
+
+
+