94 Commits

Author SHA1 Message Date
0xallam
643f6ba54a chore: Bump version to 0.8.1 2026-02-20 10:36:48 -08:00
0xallam
7fb4b63b96 fix: Change default model from claude-sonnet-4-6 to gpt-5 across docs and code 2026-02-20 10:35:58 -08:00
0xallam
027cea2f25 fix: Handle stray quotes in tag names and enforce parameter tags in prompt 2026-02-20 08:29:01 -08:00
0xallam
b9dcf7f63d fix: Address code review feedback on tool format normalization 2026-02-20 08:29:01 -08:00
0xallam
e09b5b42c1 fix: Prevent assistant-message prefill rejected by Claude 4.6 2026-02-20 08:29:01 -08:00
0xallam
e7970de6d2 fix: Handle single-quoted and whitespace-padded tool call tags 2026-02-20 08:29:01 -08:00
0xallam
7614fcc512 fix: Strip quotes from parameter/function names in tool calls 2026-02-20 08:29:01 -08:00
0xallam
f4d522164d feat: Normalize alternative tool call formats (invoke/function_calls) 2026-02-20 08:29:01 -08:00
Ahmed Allam
6166be841b Resolve LLM API Base and Models (#317) 2026-02-20 07:14:10 -08:00
0xallam
bf8020fafb fix: Strip custom_llm_provider before cost lookup for proxied models 2026-02-20 06:52:27 -08:00
0xallam
3b3576b024 refactor: Centralize strix model resolution with separate API and capability names
- Replace fragile prefix matching with explicit STRIX_MODEL_MAP
- Add resolve_strix_model() returning (api_model, canonical_model)
- api_model (openai/ prefix) for API calls to OpenAI-compatible Strix API
- canonical_model (actual provider name) for litellm capability lookups
- Centralize resolution in LLMConfig instead of scattered call sites
2026-02-20 04:40:04 -08:00
octovimmer
d2c99ea4df resolve: merge conflict resolution, llm api base resolution 2026-02-19 17:37:00 -08:00
octovimmer
06ae3d3860 fix: linting errors 2026-02-19 17:25:10 -08:00
0xallam
1833f1a021 chore: Bump version to 0.8.0 2026-02-19 14:12:59 -08:00
dependabot[bot]
cc6d46a838 chore(deps): bump pypdf from 6.6.2 to 6.7.1
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.2 to 6.7.1.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.6.2...6.7.1)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-19 14:09:55 -08:00
0xallam
8cb026b1be docs: Revert discord badge cache bust 2026-02-19 13:53:27 -08:00
0xallam
cec7417582 docs: Cache bust discord badge 2026-02-19 13:52:13 -08:00
0xallam
62bb47a881 docs: Add Strix Router page to navigation sidebar 2026-02-19 13:46:44 -08:00
octovimmer
e38f523a45 Strix LLM Documentation and Config Changes (#315)
* feat: add to readme new keys

* feat: shoutout strix models, docs

* fix: mypy error

* fix: base api

* docs: update quickstart and models

* fixes: changes to docs

uniform api_key variable naming

* test: git commit hook

* nevermind it was nothing

* docs: Update default model to claude-sonnet-4.6 and improve Strix Router docs

- Replace gpt-5 and opus-4.6 defaults with claude-sonnet-4.6 across all docs and code
- Rewrite Strix Router (models.mdx) page with clearer structure and messaging
- Add Strix Router as recommended option in overview.mdx and quickstart prerequisites
- Update stale Claude 4.5 references to 4.6 in anthropic.mdx, openrouter.mdx, bug_report.md
- Fix install.sh links to point to models.strix.ai and correct docs URLs
- Update error message examples in main.py to use claude-sonnet-4-6

---------

Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-02-20 01:43:18 +04:00
0xallam
30550dd189 fix: Add rule against duplicating changes across code_locations 2026-02-17 14:59:13 -08:00
0xallam
154040f9fb fix: Improve code_locations schema for accurate block-level fixes and multi-part suggestions
Rewrote the code_locations parameter description to make fix_before/fix_after
semantics explicit: they are literal block-level replacements mapped directly
to GitHub/GitLab PR suggestion blocks. Added guidance for multi-part fixes
(separate locations for non-contiguous changes like imports + code), common
mistakes to avoid, and updated all examples to demonstrate multi-line ranges.
2026-02-17 14:17:33 -08:00
TaeBbong
365d51f52f fix: Add explicit UTF-8 encoding to read_text() calls
- Specify encoding="utf-8" in registry.py _load_xml_schema()
- Specify encoding="utf-8" in skills/__init__.py load_skills()
- Prevents cp949/shift_jis/cp1252 decoding errors on non-English Windows
2026-02-15 17:41:10 -08:00
0xallam
305ae2f699 fix: Remove indentation prefix from diff code block markers for syntax highlighting 2026-02-15 17:25:59 -08:00
0xallam
d6e9b3b7cf feat: Redesign vulnerability reporting with nested XML code locations and CVSS
Replace 12 flat parameters (code_file, code_before, code_after, code_diff,
and 8 CVSS fields) with structured nested XML fields: code_locations with
co-located fix_before/fix_after per location, cvss_breakdown, and cwe.

This enables multi-file vulnerability locations, per-location fixes with
precise line numbers, data flow representation (source/sink), CWE
classification, and compatibility with GitHub/GitLab PR review APIs.
2026-02-15 17:25:59 -08:00
dependabot[bot]
2b94633212 chore(deps): bump protobuf from 6.33.4 to 6.33.5
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.4 to 6.33.5.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 6.33.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:44:26 -08:00
dependabot[bot]
846f8c02b4 chore(deps): bump cryptography from 44.0.1 to 46.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.1 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.1...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:44:06 -08:00
dependabot[bot]
6e1b5b7a0c chore(deps): bump pillow from 11.3.0 to 12.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.3.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/11.3.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:43:54 -08:00
0xallam
40cb705494 fix: Skip clipboard copy for whitespace-only selections 2026-02-07 11:04:31 -08:00
0xallam
e0b750dbcd feat: Add mouse text selection auto-copy to clipboard in TUI
Enable native text selection across tool components and agent messages
with automatic clipboard copy, toast notification, and decorative icon
stripping. Replace Padding wrappers with Text to support selection
across multiple renderables.
2026-02-07 11:04:31 -08:00
0xallam
0a63ffba63 fix: Polish finish_scan report schema descriptions and examples
Improve the finish_scan tool schema to produce more professional
pentest reports: expand parameter descriptions with structural
guidance, rewrite recommendations example with proper urgency tiers
instead of Priority 0/1/2, fix duplicated section titles, and clean
up informal language.
2026-02-04 13:30:24 -08:00
0xallam
5a76fab4ae fix: Replace hardcoded git host detection with HTTP protocol probe
Remove hardcoded github.com/gitlab.com/bitbucket.org host lists from
infer_target_type. Instead, detect git repositories on any host by
querying the standard /info/refs?service=git-upload-pack endpoint.

Works for any self-hosted git instance.
2026-01-31 23:24:59 -08:00
dependabot[bot]
85f05c326b chore(deps): bump pypdf from 6.6.0 to 6.6.2
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.0 to 6.6.2.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.2)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-31 23:17:33 -08:00
Ahmed Allam
b8cabdde97 Update README 2026-02-01 05:13:59 +04:00
Ahmed Allam
83ce9ed960 Update README.md 2026-02-01 05:11:44 +04:00
0xallam
c2fbf81f1d fix(llm): Pass API key and base URL to memory compressor litellm calls
The memory compressor was calling litellm.completion() without passing
the api_key and api_base parameters, causing authentication errors when
LLM_API_KEY is set but provider-specific env vars (OPENAI_API_KEY, etc.)
are not. This matches the pattern used in dedupe.py.
2026-01-28 01:29:33 -08:00
0xallam
c5bd30e677 chore: update cloud URLs 2026-01-25 23:06:47 -08:00
0xallam
5d187fcb02 chore: update poetry lock 2026-01-23 12:16:06 -08:00
LegendEvent
39d934ee71 chore: upgrade litellm to 1.81.1 for zai provider support
Updates LiteLLM from ~1.80.7 to ~1.81.1 which includes
full support for z.ai (Zhipu AI) provider using the 'zai/model-name'
format. This enables Strix to work with z.ai subscription
credentials by setting STRIX_LLM="zai/glm-4.7" with appropriate
LLM_API_KEY and LLM_API_BASE environment variables.

Changes:
- Updated litellm version constraint in pyproject.toml
- No breaking changes to Strix API or configuration

Closes #ISSUE_ID (to be linked if applicable)

Signed-off-by: legendevent <legendevent@users.noreply.github.com>
2026-01-23 12:16:06 -08:00
0xallam
386e64fa29 chore: bump version to 0.7.0 2026-01-23 11:06:29 -08:00
Ahmed Allam
655ddb4d7f Update README with full details section 2026-01-23 23:05:26 +04:00
0xallam
2bc1e5e1cb docs: add benchmarks directory with XBEN results 2026-01-23 11:04:22 -08:00
Ahmed Allam
6bacc796e2 Update README 2026-01-23 06:56:10 +04:00
Ahmed Allam
c50c79084b Update README 2026-01-23 06:55:35 +04:00
0xallam
83914f454f docs: update screenshot and add to intro page 2026-01-22 13:09:45 -08:00
0xallam
6da639ce58 chore: unify token stats color scheme 2026-01-22 11:37:21 -08:00
0xallam
a97836c335 chore: improve stats panel layout 2026-01-22 11:17:32 -08:00
0xallam
5f77dd7052 docs: update Discord links 2026-01-21 20:27:28 -08:00
0xallam
33b94a7034 docs: improve introduction page with use cases, tools, and architecture 2026-01-21 20:27:28 -08:00
0xallam
456705e5e9 docs: remove custom Docker image example from config 2026-01-21 15:35:26 -08:00
0xallam
82d1c0cec4 docs: update configuration documentation
- Add missing config options: STRIX_LLM_MAX_RETRIES, STRIX_MEMORY_COMPRESSOR_TIMEOUT, STRIX_TELEMETRY
- Remove non-existent options: LLM_RATE_LIMIT_DELAY, LLM_RATE_LIMIT_CONCURRENT
- Fix defaults: STRIX_SANDBOX_EXECUTION_TIMEOUT (500 -> 120), STRIX_IMAGE (0.1.10 -> 0.1.11)
- Add config file documentation section
- Add --config CLI option to cli.mdx
2026-01-21 15:13:15 -08:00
0xallam
1b394b808b docs: update skills documentation for markdown format
Reflect PR #275 changes - skills now use Markdown files with YAML
frontmatter instead of Jinja templates with XML-style tags.
2026-01-21 14:54:09 -08:00
0xallam
25ac2f1e08 docs: add documentation to main repository 2026-01-20 21:13:32 -08:00
0xallam
b456a4ed8c fix(llm): collect usage stats from final stream chunk
The early break on </function> prevented receiving the final chunk
that contains token usage data (input_tokens, output_tokens).
2026-01-20 20:36:00 -08:00
0xallam
165887798d refactor: simplify --config implementation to reuse existing config system
- Reuse apply_saved() instead of custom override logic
- Add force parameter to override existing env vars
- Move validation to utils.py
- Prevent saving when using custom config (one-time override)
- Fix: don't modify ~/.strix/cli-config.json when --config is used

Co-Authored-By: FeedClogger <feedclogger@users.noreply.github.com>
2026-01-20 17:02:29 -08:00
FeedClogger
4ab9af6e47 Added .env variable override through --config param 2026-01-20 17:02:29 -08:00
0xallam
4337991d05 chore: update Discord invite link 2026-01-20 12:58:14 -08:00
0xallam
9cff247d89 docs: update skills README for markdown format 2026-01-20 12:50:59 -08:00
0xallam
af2c830f70 refactor: standardize vulnerability skills format 2026-01-20 12:50:59 -08:00
0xallam
91feb3e01c fix: remove icon from ListFilesRenderer 2026-01-20 12:50:59 -08:00
0xallam
762c25d6ed fix: exclude scan_modes and coordination from available skills 2026-01-20 12:50:59 -08:00
0xallam
6cb1c20978 refactor: migrate skills from Jinja to Markdown 2026-01-20 12:50:59 -08:00
0xallam
4b62169f74 fix: remove unintended margin from stats panel 2026-01-19 21:48:56 -08:00
0xallam
e948f06d64 refactor: improve stats panel styling and add version display 2026-01-19 21:46:13 -08:00
0xallam
3d4b1bfb08 refactor: update agent tree status indicators 2026-01-19 21:23:29 -08:00
0xallam
8413987fcd feat: remove docker container on shutdown
Add automatic cleanup of Docker containers when the application exits.
Uses a singleton runtime pattern and spawns a detached subprocess for
cleanup to ensure fast exit without blocking the UI.
2026-01-19 18:26:41 -08:00
0xallam
a67fe4c45c refactor: redesign finished dialogs and UI elements 2026-01-19 16:52:02 -08:00
0xallam
9f7b532056 refactor: revamp proxy tool renderers for better UX
- Show actual request/response data with visual flow (>> / <<)
- Display all relevant params: filters, sort, scope, modifications
- Add type-safe handling for streaming edge cases
- Use color-coded status codes (2xx green, 3xx yellow, 4xx/5xx red)
- Show search context (before/after) not just matched text
- Show full request details in send/repeat request renderers
- Show modifications on separate lines with full content
- Increase truncation limits for better visibility (200 char lines)
- Use present tense lowercase titles (listing, viewing, searching)
2026-01-19 15:33:53 -08:00
0xallam
43572242f1 fix: remove 'unknown' fallback display in browser tool renderer 2026-01-19 13:46:20 -08:00
0xallam
a7bd635c11 fix: strip ANSI codes from Python tool output and optimize highlighting
- Add comprehensive ECMA-48 ANSI pattern to strip escape sequences from output
- Fix _truncate_line to strip ANSI before length calculation
- Cache PythonLexer instance (was creating new one per call)
- Memoize token color lookups to avoid repeated parent chain traversal
2026-01-19 12:21:08 -08:00
0xallam
e30ef9aec8 perf: optimize TUI streaming rendering performance
- Pre-compile regex patterns in streaming_parser.py
- Move hot-path imports to module level in tui.py
- Add streaming content caching to avoid re-rendering unchanged content
- Track streaming length to skip unnecessary re-renders
- Reduce UI update interval from 250ms to 350ms
2026-01-19 11:46:38 -08:00
0xallam
03fb1e940f fix: always show shell restart warning after install 2026-01-18 19:22:44 -08:00
0xallam
7417e6f8d0 fix: improve install script PATH handling for more shells
- Add ZDOTDIR support for zsh users who relocate their config
- Add XDG_CONFIG_HOME paths for zsh and bash
- Add ash and sh shell support (Alpine/BusyBox)
- Warn user instead of silently creating .bashrc when no config found
- Add user feedback on what file was modified
- Handle non-writable config files gracefully
2026-01-18 19:11:44 -08:00
0xallam
86f8835ccb chore: bump version to 0.6.2 and sandbox to 0.1.11 2026-01-18 18:29:44 -08:00
0xallam
2bfb80ff4a refactor: share single browser instance across all agents
- Use singleton browser with isolated BrowserContext per agent instead of
  separate Chromium processes per agent
- Add cleanup logic for stale browser/playwright on reconnect
- Add resource management instructions to browser schema (close tabs/browser when done)
- Suppress Kali login message in Dockerfile
2026-01-18 17:51:23 -08:00
0xallam
7ff0e68466 fix: create fresh gql client per request to avoid transport state issues 2026-01-17 22:19:21 -08:00
0xallam
2ebfd20db5 fix: add telemetry module to Dockerfile for posthog error tracking 2026-01-17 22:19:21 -08:00
0xallam
918a151892 refactor: simplify tool server to asyncio tasks with per-agent isolation
- Replace multiprocessing/threading with single asyncio task per agent
- Add task cancellation: new request cancels previous for same agent
- Add per-agent state isolation via ContextVar for Terminal, Browser, Python managers
- Add posthog telemetry for tool execution errors (timeout, http, sandbox)
- Fix proxy manager singleton pattern
- Increase client timeout buffer over server timeout
- Add context.py to Dockerfile
2026-01-17 22:19:21 -08:00
0xallam
a80ecac7bd fix: run tool server as module to ensure correct sys.path for workers 2026-01-17 22:19:21 -08:00
0xallam
19246d8a5a style: remove redundant sudo -E flag 2026-01-17 22:19:21 -08:00
0xallam
4cb2cebd1e fix: add initial delay and increase retries for tool server health check 2026-01-17 22:19:21 -08:00
0xallam
26b0786a4e fix: replace pgrep with health check for tool server validation 2026-01-17 22:19:21 -08:00
0xallam
61dea7010a refactor: simplify container initialization and fix startup reliability
- Move tool server startup from Python to entrypoint script
- Hardcode Caido port (48080) in entrypoint, remove from Python
- Use /app/venv/bin/python directly instead of poetry run
- Fix env var passing through sudo with sudo -E and explicit vars
- Add Caido process monitoring and logging during startup
- Add retry logic with exponential backoff for token fetch
- Add tool server process validation before declaring ready
- Simplify docker_runtime.py (489 -> 310 lines)
- DRY up container state recovery into _recover_container_state()
- Add container creation retry logic (3 attempts)
- Fix GraphQL health check URL (/graphql/ with trailing slash)
2026-01-17 22:19:21 -08:00
dependabot[bot]
c433d4ffb2 chore(deps): bump pyasn1 from 0.6.1 to 0.6.2
Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/pyasn1/pyasn1/releases)
- [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst)
- [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.1...v0.6.2)

---
updated-dependencies:
- dependency-name: pyasn1
  dependency-version: 0.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-16 15:26:13 -08:00
0xallam
ed6861db64 fix(tool_server): include request_id in worker errors and use get_running_loop
- Add request_id to worker error responses to prevent client hangs
- Replace deprecated get_event_loop() with get_running_loop() in execute_tool
2026-01-16 01:11:02 -08:00
0xallam
a74ed69471 fix(tool_server): use get_running_loop() instead of deprecated get_event_loop() 2026-01-16 01:11:02 -08:00
0xallam
9102b22381 fix(python): prevent stdout/stderr race on timeout
Add cancelled flag to prevent timed-out thread's finally block from
overwriting stdout/stderr when a subsequent execution has already
started capturing output.
2026-01-16 01:11:02 -08:00
0xallam
693ef16060 fix(runtime): parallel tool execution and remove signal handlers
- Add ThreadPoolExecutor in agent_worker for parallel request execution
- Add request_id correlation to prevent response mismatch between concurrent requests
- Add background listener thread per agent to dispatch responses to correct futures
- Add --timeout argument for hard request timeout (default: 120s from config)
- Remove signal handlers from terminal_manager, python_manager, tab_manager (use atexit only)
- Replace SIGALRM timeout in python_instance with threading-based timeout

This fixes requests getting queued behind slow operations and timeouts.
2026-01-16 01:11:02 -08:00
0xallam
8dc6f1dc8f fix(llm): remove hardcoded temperature from dedupe check
Allow the model's default temperature setting to be used instead of
forcing temperature=0 for duplicate detection.
2026-01-15 18:56:48 -08:00
0xallam
4d9154a7f8 fix(config): keep non-LLM saved env values
When LLM env differs, drop only LLM-related saved entries instead of
clearing all saved env vars, preserving other config like API keys.
2026-01-15 18:37:38 -08:00
0xallam
2898db318e fix(config): canonicalize LLM env and respect cleared vars
Drop saved LLM config if any current LLM env var differs, and treat
explicit empty env vars as cleared so saved values are removed and
not re-applied.
2026-01-15 18:37:38 -08:00
0xallam
960bb91790 fix(tui): suppress stderr output in python renderer 2026-01-15 17:44:49 -08:00
0xallam
4de4be683f fix(executor): include error type in httpx RequestError messages
The str() of httpx.RequestError was often empty, making error messages
unhelpful. Now includes the exception type (e.g., ConnectError) for
better debugging.
2026-01-15 17:40:21 -08:00
0xallam
d351b14ae7 docs(tools): add comprehensive multiline examples and remove XML terminology
- Add professional, realistic multiline examples to all tool schemas
- finish_scan: Complete pentest report with SSRF/access control findings
- create_vulnerability_report: Full SSRF writeup with cloud metadata PoC
- file_edit, notes, thinking: Realistic security testing examples
- Remove XML terminology from system prompt and tool descriptions
- All examples use real newlines (not literal \n) to demonstrate correct usage
2026-01-15 17:25:28 -08:00
Ahmed Allam
ceeec8faa8 Update README 2026-01-16 02:34:30 +04:00
138 changed files with 9476 additions and 5647 deletions

View File

@@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. Ubuntu 22.04] - OS: [e.g. Ubuntu 22.04]
- Strix Version or Commit: [e.g. 0.1.18] - Strix Version or Commit: [e.g. 0.1.18]
- Python Version: [e.g. 3.12] - Python Version: [e.g. 3.12]
- LLM Used: [e.g. GPT-5, Claude Sonnet 4] - LLM Used: [e.g. GPT-5, Claude Sonnet 4.6]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

BIN
.github/screenshot.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -46,7 +46,7 @@ Skills are specialized knowledge packages that enhance agent capabilities. See [
### Quick Guide ### Quick Guide
1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.) 1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.)
2. **Create a** `.jinja` file with your skill content 2. **Create a** `.md` file with your skill content
3. **Include practical examples** - Working payloads, commands, or test cases 3. **Include practical examples** - Working payloads, commands, or test cases
4. **Provide validation methods** - How to confirm findings and avoid false positives 4. **Provide validation methods** - How to confirm findings and avoid false positives
5. **Submit via PR** with clear description 5. **Submit via PR** with clear description
@@ -101,7 +101,7 @@ We welcome feature ideas! Please:
## 🤝 Community ## 🤝 Community
- **Discord**: [Join our community](https://discord.gg/YjKFvEZSdZ) - **Discord**: [Join our community](https://discord.gg/strix-ai)
- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues) - **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues)
## ✨ Recognition ## ✨ Recognition
@@ -113,4 +113,4 @@ We value all contributions! Contributors will be:
--- ---
**Questions?** Reach out on [Discord](https://discord.gg/YjKFvEZSdZ) or create an issue. We're here to help! **Questions?** Reach out on [Discord](https://discord.gg/strix-ai) or create an issue. We're here to help!

View File

@@ -14,8 +14,8 @@
<a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a> <a href="https://docs.strix.ai"><img src="https://img.shields.io/badge/Docs-docs.strix.ai-2b9246?style=for-the-badge&logo=gitbook&logoColor=white" alt="Docs"></a>
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-3b82f6?style=for-the-badge&logoColor=white" alt="Website"></a> <a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-f0f0f0?style=for-the-badge&logoColor=000000" alt="Website"></a>
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/badge/PyPI-strix--agent-f59e0b?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a> [![](https://dcbadge.limes.pink/api/server/8Suzzd9z)](https://discord.gg/strix-ai)
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a> <a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
@@ -23,7 +23,7 @@
<a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/pypi/v/strix-agent?style=flat-square" alt="PyPI Version"></a> <a href="https://pypi.org/project/strix-agent/"><img src="https://img.shields.io/pypi/v/strix-agent?style=flat-square" alt="PyPI Version"></a>
<a href="https://discord.gg/YjKFvEZSdZ"><img src="https://github.com/usestrix/.github/raw/main/imgs/Discord.png" height="40" alt="Join Discord"></a> <a href="https://discord.gg/strix-ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/Discord.png" height="40" alt="Join Discord"></a>
<a href="https://x.com/strix_ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/X.png" height="40" alt="Follow on X"></a> <a href="https://x.com/strix_ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/X.png" height="40" alt="Follow on X"></a>
@@ -31,13 +31,6 @@
</div> </div>
<br/>
<div align="center">
<img src=".github/screenshot.png" alt="Strix Demo" width="900" style="border-radius: 16px;">
</div>
<br>
> [!TIP] > [!TIP]
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production! > **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production!
@@ -58,20 +51,30 @@ Strix are autonomous AI agents that act just like real hackers - they run your c
- **Autofix & reporting** to accelerate remediation - **Autofix & reporting** to accelerate remediation
## 🎯 Use Cases <br>
<div align="center">
<a href="https://strix.ai">
<img src=".github/screenshot.png" alt="Strix Demo" width="1000" style="border-radius: 16px;">
</a>
</div>
## Use Cases
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications - **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports - **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
- **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting - **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting
- **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production - **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production
---
## 🚀 Quick Start ## 🚀 Quick Start
**Prerequisites:** **Prerequisites:**
- Docker (running) - Docker (running)
- An LLM provider key (e.g. [get OpenAI API key](https://platform.openai.com/api-keys) or use a local LLM) - An LLM API key:
- Any [supported provider](https://docs.strix.ai/llm-providers/overview) (OpenAI, Anthropic, Google, etc.)
- Or [Strix Router](https://models.strix.ai) — single API key for multiple providers with $10 free credit on signup
### Installation & First Scan ### Installation & First Scan
@@ -83,7 +86,7 @@ curl -sSL https://strix.ai/install | bash
pipx install strix-agent pipx install strix-agent
# Configure your AI provider # Configure your AI provider
export STRIX_LLM="openai/gpt-5" export STRIX_LLM="openai/gpt-5" # or "strix/gpt-5" via Strix Router (https://models.strix.ai)
export LLM_API_KEY="your-api-key" export LLM_API_KEY="your-api-key"
# Run your first security assessment # Run your first security assessment
@@ -93,19 +96,6 @@ strix --target ./app-directory
> [!NOTE] > [!NOTE]
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>` > First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
## Run Strix in Cloud
Want to skip the local setup, API keys, and unpredictable LLM costs? Run the hosted cloud version of Strix at **[app.strix.ai](https://strix.ai)**.
Launch a scan in just a few minutes—no setup or configuration required—and youll get:
- **A full pentest report** with validated findings and clear remediation steps
- **Shareable dashboards** your team can use to track fixes over time
- **CI/CD and GitHub integrations** to block risky changes before production
- **Continuous monitoring** so new vulnerabilities are caught quickly
[**Run your first pentest now →**](https://strix.ai)
--- ---
## ✨ Features ## ✨ Features
@@ -228,7 +218,7 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
**Recommended models for best results:** **Recommended models for best results:**
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5` - [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
- [Anthropic Claude Sonnet 4.5](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-5` - [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview` - [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models. See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
@@ -243,7 +233,7 @@ We welcome contributions of code, docs, and new skills - check out our [Contribu
## Join Our Community ## Join Our Community
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/YjKFvEZSdZ)** Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/strix-ai)**
## Support the Project ## Support the Project
@@ -251,7 +241,7 @@ Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://d
## Acknowledgements ## Acknowledgements
Strix builds on the incredible work of open-source projects like [LiteLLM](https://github.com/BerriAI/litellm), [Caido](https://github.com/caido/caido), [ProjectDiscovery](https://github.com/projectdiscovery), [Playwright](https://github.com/microsoft/playwright), and [Textual](https://github.com/Textualize/textual). Huge thanks to their maintainers! Strix builds on the incredible work of open-source projects like [LiteLLM](https://github.com/BerriAI/litellm), [Caido](https://github.com/caido/caido), [Nuclei](https://github.com/projectdiscovery/nuclei), [Playwright](https://github.com/microsoft/playwright), and [Textual](https://github.com/Textualize/textual). Huge thanks to their maintainers!
> [!WARNING] > [!WARNING]

43
benchmarks/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Benchmarks
We use security benchmarks to track Strix's capabilities and improvements over time. We plan to add more benchmarks, both existing ones and our own, to help the community evaluate and compare security agents.
## Full Details
For the complete benchmark results, evaluation scripts, and run data, see the [usestrix/benchmarks](https://github.com/usestrix/benchmarks) repository.
> [!NOTE]
> We are actively adding more benchmarks to our evaluation suite.
## Results
| Benchmark | Challenges | Success Rate |
|-----------|------------|--------------|
| [XBEN](https://github.com/usestrix/benchmarks/tree/main/XBEN) | 104 | **96%** |
### XBEN
The [XBOW benchmark](https://github.com/usestrix/benchmarks/tree/main/XBEN) is a set of 104 web security challenges designed to evaluate autonomous penetration testing agents. Each challenge follows a CTF format where the agent must discover and exploit vulnerabilities to extract a hidden flag.
Strix `v0.4.0` achieved a **96% success rate** (100/104 challenges) in black-box mode.
```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'pie1': '#3b82f6', 'pie2': '#1e3a5f', 'pieTitleTextColor': '#ffffff', 'pieSectionTextColor': '#ffffff', 'pieLegendTextColor': '#ffffff'}}}%%
pie title Challenge Outcomes (104 Total)
"Solved" : 100
"Unsolved" : 4
```
**Performance by Difficulty:**
| Difficulty | Solved | Success Rate |
|------------|--------|--------------|
| Level 1 (Easy) | 45/45 | 100% |
| Level 2 (Medium) | 49/51 | 96% |
| Level 3 (Hard) | 6/8 | 75% |
**Resource Usage:**
- Average solve time: ~19 minutes
- Total cost: ~$337 for 100 challenges

View File

@@ -9,7 +9,8 @@ RUN apt-get update && \
RUN useradd -m -s /bin/bash pentester && \ RUN useradd -m -s /bin/bash pentester && \
usermod -aG sudo pentester && \ usermod -aG sudo pentester && \
echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
touch /home/pentester/.hushlogin
RUN mkdir -p /home/pentester/configs \ RUN mkdir -p /home/pentester/configs \
/home/pentester/wordlists \ /home/pentester/wordlists \
@@ -168,9 +169,12 @@ RUN /app/venv/bin/pip install -r /home/pentester/tools/jwt_tool/requirements.txt
RUN echo "# Sandbox Environment" > README.md RUN echo "# Sandbox Environment" > README.md
COPY strix/__init__.py strix/ COPY strix/__init__.py strix/
COPY strix/config/ /app/strix/config/
COPY strix/utils/ /app/strix/utils/
COPY strix/telemetry/ /app/strix/telemetry/
COPY strix/runtime/tool_server.py strix/runtime/__init__.py strix/runtime/runtime.py /app/strix/runtime/ COPY strix/runtime/tool_server.py strix/runtime/__init__.py strix/runtime/runtime.py /app/strix/runtime/
COPY strix/tools/__init__.py strix/tools/registry.py strix/tools/executor.py strix/tools/argument_parser.py /app/strix/tools/ COPY strix/tools/__init__.py strix/tools/registry.py strix/tools/executor.py strix/tools/argument_parser.py strix/tools/context.py /app/strix/tools/
COPY strix/tools/browser/ /app/strix/tools/browser/ COPY strix/tools/browser/ /app/strix/tools/browser/
COPY strix/tools/file_edit/ /app/strix/tools/file_edit/ COPY strix/tools/file_edit/ /app/strix/tools/file_edit/

View File

@@ -1,9 +1,12 @@
#!/bin/bash #!/bin/bash
set -e set -e
if [ -z "$CAIDO_PORT" ]; then CAIDO_PORT=48080
echo "Error: CAIDO_PORT must be set." CAIDO_LOG="/tmp/caido_startup.log"
exit 1
if [ ! -f /app/certs/ca.p12 ]; then
echo "ERROR: CA certificate file /app/certs/ca.p12 not found."
exit 1
fi fi
caido-cli --listen 127.0.0.1:${CAIDO_PORT} \ caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
@@ -11,28 +14,62 @@ caido-cli --listen 127.0.0.1:${CAIDO_PORT} \
--no-logging \ --no-logging \
--no-open \ --no-open \
--import-ca-cert /app/certs/ca.p12 \ --import-ca-cert /app/certs/ca.p12 \
--import-ca-cert-pass "" > /dev/null 2>&1 & --import-ca-cert-pass "" > "$CAIDO_LOG" 2>&1 &
CAIDO_PID=$!
echo "Started Caido with PID $CAIDO_PID on port $CAIDO_PORT"
echo "Waiting for Caido API to be ready..." echo "Waiting for Caido API to be ready..."
CAIDO_READY=false
for i in {1..30}; do for i in {1..30}; do
if curl -s -o /dev/null http://localhost:${CAIDO_PORT}/graphql; then if ! kill -0 $CAIDO_PID 2>/dev/null; then
echo "Caido API is ready." echo "ERROR: Caido process died while waiting for API (iteration $i)."
echo "=== Caido log ==="
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
exit 1
fi
if curl -s -o /dev/null -w "%{http_code}" http://localhost:${CAIDO_PORT}/graphql/ | grep -qE "^(200|400)$"; then
echo "Caido API is ready (attempt $i)."
CAIDO_READY=true
break break
fi fi
sleep 1 sleep 1
done done
if [ "$CAIDO_READY" = false ]; then
echo "ERROR: Caido API did not become ready within 30 seconds."
echo "Caido process status: $(kill -0 $CAIDO_PID 2>&1 && echo 'running' || echo 'dead')"
echo "=== Caido log ==="
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
exit 1
fi
sleep 2 sleep 2
echo "Fetching API token..." echo "Fetching API token..."
TOKEN=$(curl -s -X POST \ TOKEN=""
-H "Content-Type: application/json" \ for attempt in 1 2 3 4 5; do
-d '{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}' \ RESPONSE=$(curl -sL -X POST \
http://localhost:${CAIDO_PORT}/graphql | jq -r '.data.loginAsGuest.token.accessToken') -H "Content-Type: application/json" \
-d '{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}' \
http://localhost:${CAIDO_PORT}/graphql)
TOKEN=$(echo "$RESPONSE" | jq -r '.data.loginAsGuest.token.accessToken // empty')
if [ -n "$TOKEN" ] && [ "$TOKEN" != "null" ]; then
echo "Successfully obtained API token (attempt $attempt)."
break
fi
echo "Token fetch attempt $attempt failed: $RESPONSE"
sleep $((attempt * 2))
done
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
echo "Failed to get API token from Caido." echo "ERROR: Failed to get API token from Caido after 5 attempts."
curl -s -X POST -H "Content-Type: application/json" -d '{"query":"mutation { loginAsGuest { token { accessToken } } }"}' http://localhost:${CAIDO_PORT}/graphql echo "=== Caido log ==="
cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)"
exit 1 exit 1
fi fi
@@ -40,7 +77,7 @@ export CAIDO_API_TOKEN=$TOKEN
echo "Caido API token has been set." echo "Caido API token has been set."
echo "Creating a new Caido project..." echo "Creating a new Caido project..."
CREATE_PROJECT_RESPONSE=$(curl -s -X POST \ CREATE_PROJECT_RESPONSE=$(curl -sL -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation CreateProject { createProject(input: {name: \"sandbox\", temporary: true}) { project { id } } }"}' \ -d '{"query":"mutation CreateProject { createProject(input: {name: \"sandbox\", temporary: true}) { project { id } } }"}' \
@@ -57,7 +94,7 @@ fi
echo "Caido project created with ID: $PROJECT_ID" echo "Caido project created with ID: $PROJECT_ID"
echo "Selecting Caido project..." echo "Selecting Caido project..."
SELECT_RESPONSE=$(curl -s -X POST \ SELECT_RESPONSE=$(curl -sL -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation SelectProject { selectProject(id: \"'$PROJECT_ID'\") { currentProject { project { id } } } }"}' \ -d '{"query":"mutation SelectProject { selectProject(id: \"'$PROJECT_ID'\") { currentProject { project { id } } } }"}' \
@@ -114,9 +151,36 @@ sudo -u pentester certutil -N -d sql:/home/pentester/.pki/nssdb --empty-password
sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb
echo "✅ CA added to browser trust store" echo "✅ CA added to browser trust store"
echo "Container initialization complete - agents will start their own tool servers as needed" echo "Starting tool server..."
echo "✅ Shared container ready for multi-agent use" cd /app
export PYTHONPATH=/app
export STRIX_SANDBOX_MODE=true
export POETRY_VIRTUALENVS_CREATE=false
export TOOL_SERVER_TIMEOUT="${STRIX_SANDBOX_EXECUTION_TIMEOUT:-120}"
TOOL_SERVER_LOG="/tmp/tool_server.log"
sudo -E -u pentester \
poetry run python -m strix.runtime.tool_server \
--token="$TOOL_SERVER_TOKEN" \
--host=0.0.0.0 \
--port="$TOOL_SERVER_PORT" \
--timeout="$TOOL_SERVER_TIMEOUT" > "$TOOL_SERVER_LOG" 2>&1 &
for i in {1..10}; do
if curl -s "http://127.0.0.1:$TOOL_SERVER_PORT/health" | grep -q '"status":"healthy"'; then
echo "✅ Tool server healthy on port $TOOL_SERVER_PORT"
break
fi
if [ $i -eq 10 ]; then
echo "ERROR: Tool server failed to become healthy"
echo "=== Tool server log ==="
cat "$TOOL_SERVER_LOG" 2>/dev/null || echo "(no log)"
exit 1
fi
sleep 1
done
echo "✅ Container ready"
cd /workspace cd /workspace
exec "$@" exec "$@"

10
docs/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Strix Documentation
Documentation source files for Strix, powered by [Mintlify](https://mintlify.com).
## Local Preview
```bash
npm i -g mintlify
cd docs && mintlify dev
```

View File

@@ -0,0 +1,109 @@
---
title: "Configuration"
description: "Environment variables for Strix"
---
Configure Strix using environment variables or a config file.
## LLM Configuration
<ParamField path="STRIX_LLM" type="string" required>
Model name in LiteLLM format (e.g., `openai/gpt-5`, `anthropic/claude-sonnet-4-6`).
</ParamField>
<ParamField path="LLM_API_KEY" type="string">
API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock).
</ParamField>
<ParamField path="LLM_API_BASE" type="string">
Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`.
</ParamField>
<ParamField path="LLM_TIMEOUT" default="300" type="integer">
Request timeout in seconds for LLM calls.
</ParamField>
<ParamField path="STRIX_LLM_MAX_RETRIES" default="5" type="integer">
Maximum number of retries for LLM API calls on transient failures.
</ParamField>
<ParamField path="STRIX_REASONING_EFFORT" default="high" type="string">
Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Defaults to `medium` for quick scan mode.
</ParamField>
<ParamField path="STRIX_MEMORY_COMPRESSOR_TIMEOUT" default="30" type="integer">
Timeout in seconds for memory compression operations (context summarization).
</ParamField>
## Optional Features
<ParamField path="PERPLEXITY_API_KEY" type="string">
API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research.
</ParamField>
<ParamField path="STRIX_DISABLE_BROWSER" default="false" type="boolean">
Disable browser automation tools.
</ParamField>
<ParamField path="STRIX_TELEMETRY" default="1" type="string">
Enable/disable anonymous telemetry. Set to `0`, `false`, `no`, or `off` to disable.
</ParamField>
## Docker Configuration
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:0.1.11" type="string">
Docker image to use for the sandbox container.
</ParamField>
<ParamField path="DOCKER_HOST" type="string">
Docker daemon socket path. Use for remote Docker hosts or custom configurations.
</ParamField>
<ParamField path="STRIX_RUNTIME_BACKEND" default="docker" type="string">
Runtime backend for the sandbox environment.
</ParamField>
## Sandbox Configuration
<ParamField path="STRIX_SANDBOX_EXECUTION_TIMEOUT" default="120" type="integer">
Maximum execution time in seconds for sandbox operations.
</ParamField>
<ParamField path="STRIX_SANDBOX_CONNECT_TIMEOUT" default="10" type="integer">
Timeout in seconds for connecting to the sandbox container.
</ParamField>
## Config File
Strix stores configuration in `~/.strix/cli-config.json`. You can also specify a custom config file:
```bash
strix --target ./app --config /path/to/config.json
```
**Config file format:**
```json
{
"env": {
"STRIX_LLM": "openai/gpt-5",
"LLM_API_KEY": "sk-...",
"STRIX_REASONING_EFFORT": "high"
}
}
```
## Example Setup
```bash
# Required
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="sk-..."
# Optional: Enable web search
export PERPLEXITY_API_KEY="pplx-..."
# Optional: Custom timeouts
export LLM_TIMEOUT="600"
export STRIX_SANDBOX_EXECUTION_TIMEOUT="300"
```

121
docs/advanced/skills.mdx Normal file
View File

@@ -0,0 +1,121 @@
---
title: "Skills"
description: "Specialized knowledge packages that enhance agent capabilities"
---
Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methodologies.
## The Idea
LLMs have broad but shallow security knowledge. They know _about_ SQL injection, but lack the nuanced techniques that experienced pentesters use—parser quirks, bypass methods, validation tricks, and chain attacks.
Skills inject this deep, specialized knowledge directly into the agent's context, transforming it from a generalist into a specialist for the task at hand.
## How They Work
When Strix spawns an agent for a specific task, it selects up to 5 relevant skills based on the context:
```python
# Agent created for JWT testing automatically loads relevant skills
create_agent(
task="Test authentication mechanisms",
skills=["authentication_jwt", "business_logic"]
)
```
The skills are injected into the agent's system prompt, giving it access to:
- **Advanced techniques** — Non-obvious methods beyond standard testing
- **Working payloads** — Practical examples with variations
- **Validation methods** — How to confirm findings and avoid false positives
## Skill Categories
### Vulnerabilities
Core vulnerability classes with deep exploitation techniques.
| Skill | Coverage |
| ------------------------------------- | ------------------------------------------------------ |
| `authentication_jwt` | JWT attacks, algorithm confusion, claim tampering |
| `idor` | Object reference attacks, horizontal/vertical access |
| `sql_injection` | SQL injection variants, WAF bypasses, blind techniques |
| `xss` | XSS types, filter bypasses, DOM exploitation |
| `ssrf` | Server-side request forgery, protocol handlers |
| `csrf` | Cross-site request forgery, token bypasses |
| `xxe` | XML external entities, OOB exfiltration |
| `rce` | Remote code execution vectors |
| `business_logic` | Logic flaws, state manipulation, race conditions |
| `race_conditions` | TOCTOU, parallel request attacks |
| `path_traversal_lfi_rfi` | File inclusion, path traversal |
| `open_redirect` | Redirect bypasses, URL parsing tricks |
| `mass_assignment` | Attribute injection, hidden parameter pollution |
| `insecure_file_uploads` | Upload bypasses, extension tricks |
| `information_disclosure` | Data leakage, error-based enumeration |
| `subdomain_takeover` | Dangling DNS, cloud resource claims |
| `broken_function_level_authorization` | Privilege escalation, role bypasses |
### Frameworks
Framework-specific testing patterns.
| Skill | Coverage |
| --------- | -------------------------------------------- |
| `fastapi` | FastAPI security patterns, Pydantic bypasses |
| `nextjs` | Next.js SSR/SSG issues, API route security |
### Technologies
Third-party service and platform security.
| Skill | Coverage |
| -------------------- | ---------------------------------- |
| `supabase` | Supabase RLS bypasses, auth issues |
| `firebase_firestore` | Firestore rules, Firebase auth |
### Protocols
Protocol-specific testing techniques.
| Skill | Coverage |
| --------- | ------------------------------------------------ |
| `graphql` | GraphQL introspection, batching, resolver issues |
## Skill Structure
Each skill is a Markdown file with YAML frontmatter for metadata:
```markdown
---
name: skill_name
description: Brief description of the skill's coverage
---
# Skill Title
Key insight about this vulnerability or technique.
## Attack Surface
What this skill covers and where to look.
## Methodology
Step-by-step testing approach.
## Techniques
How to discover and exploit the vulnerability.
## Bypass Methods
How to bypass common protections.
## Validation
How to confirm findings and avoid false positives.
```
## Contributing Skills
Community contributions are welcome. Create a `.md` file in the appropriate category with YAML frontmatter (`name` and `description` fields). Good skills include:
1. **Real-world techniques** — Methods that work in practice
2. **Practical payloads** — Working examples with variations
3. **Validation steps** — How to confirm without false positives
4. **Context awareness** — Version/environment-specific behavior

40
docs/cloud/overview.mdx Normal file
View File

@@ -0,0 +1,40 @@
---
title: "Introduction"
description: "Managed security testing without local setup"
---
Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
## Features
<CardGroup cols={2}>
<Card title="No Setup Required" icon="cloud">
No Docker, API keys, or local installation needed.
</Card>
<Card title="Full Reports" icon="file-lines">
Detailed findings with remediation guidance.
</Card>
<Card title="Team Dashboards" icon="users">
Track vulnerabilities and fixes over time.
</Card>
<Card title="GitHub Integration" icon="github">
Automatic scans on pull requests.
</Card>
</CardGroup>
## What You Get
- **Penetration test reports** — Validated findings with PoCs
- **Shareable dashboards** — Collaborate with your team
- **CI/CD integration** — Block risky changes automatically
- **Continuous monitoring** — Catch new vulnerabilities quickly
## Getting Started
1. Sign up at [app.strix.ai](https://app.strix.ai)
2. Connect your repository or enter a target URL
3. Launch your first scan
<Card title="Try Strix Cloud" icon="rocket" href="https://app.strix.ai">
Run your first pentest in minutes.
</Card>

96
docs/contributing.mdx Normal file
View File

@@ -0,0 +1,96 @@
---
title: "Contributing"
description: "Contribute to Strix development"
---
## Development Setup
### Prerequisites
- Python 3.12+
- Docker (running)
- Poetry
- Git
### Local Development
<Steps>
<Step title="Clone the repository">
```bash
git clone https://github.com/usestrix/strix.git
cd strix
```
</Step>
<Step title="Install dependencies">
```bash
make setup-dev
# or manually:
poetry install --with=dev
poetry run pre-commit install
```
</Step>
<Step title="Configure LLM">
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key"
```
</Step>
<Step title="Run Strix">
```bash
poetry run strix --target https://example.com
```
</Step>
</Steps>
## Contributing Skills
Skills are specialized knowledge packages that enhance agent capabilities. They live in `strix/skills/`
### Creating a Skill
1. Choose the right category
2. Create a `.md` file with YAML frontmatter (`name` and `description` fields)
3. Include practical examples—working payloads, commands, test cases
4. Provide validation methods to confirm findings
5. Submit via PR
## Contributing Code
### Pull Request Process
1. **Create an issue first** — Describe the problem or feature
2. **Fork and branch** — Work from `main`
3. **Make changes** — Follow existing code style
4. **Write tests** — Ensure coverage for new features
5. **Run checks** — `make check-all` should pass
6. **Submit PR** — Link to issue and provide context
### Code Style
- PEP 8 with 100-character line limit
- Type hints for all functions
- Docstrings for public methods
- Small, focused functions
- Meaningful variable names
## Reporting Issues
Include:
- Python version and OS
- Strix version (`strix --version`)
- LLM being used
- Full error traceback
- Steps to reproduce
## Community
<CardGroup cols={2}>
<Card title="Discord" icon="discord" href="https://discord.gg/strix-ai">
Join the community for help and discussion.
</Card>
<Card title="GitHub Issues" icon="github" href="https://github.com/usestrix/strix/issues">
Report bugs and request features.
</Card>
</CardGroup>

130
docs/docs.json Normal file
View File

@@ -0,0 +1,130 @@
{
"$schema": "https://mintlify.com/docs.json",
"theme": "maple",
"name": "Strix",
"colors": {
"primary": "#000000",
"light": "#ffffff",
"dark": "#000000"
},
"favicon": "/images/favicon-48.ico",
"navigation": {
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Getting Started",
"pages": [
"index",
"quickstart"
]
},
{
"group": "Usage",
"pages": [
"usage/cli",
"usage/scan-modes",
"usage/instructions"
]
},
{
"group": "LLM Providers",
"pages": [
"llm-providers/overview",
"llm-providers/models",
"llm-providers/openai",
"llm-providers/anthropic",
"llm-providers/openrouter",
"llm-providers/vertex",
"llm-providers/bedrock",
"llm-providers/azure",
"llm-providers/local"
]
},
{
"group": "Integrations",
"pages": [
"integrations/github-actions",
"integrations/ci-cd"
]
},
{
"group": "Tools",
"pages": [
"tools/overview",
"tools/browser",
"tools/proxy",
"tools/terminal",
"tools/sandbox"
]
},
{
"group": "Advanced",
"pages": [
"advanced/configuration",
"advanced/skills",
"contributing"
]
}
]
},
{
"tab": "Cloud",
"groups": [
{
"group": "Strix Cloud",
"pages": [
"cloud/overview"
]
}
]
}
],
"global": {
"anchors": [
{
"anchor": "GitHub",
"href": "https://github.com/usestrix/strix",
"icon": "github"
},
{
"anchor": "Discord",
"href": "https://discord.gg/strix-ai",
"icon": "discord"
}
]
}
},
"navbar": {
"links": [],
"primary": {
"type": "button",
"label": "Try Strix Cloud",
"href": "https://app.strix.ai"
}
},
"footer": {
"socials": {
"x": "https://x.com/strix_ai",
"github": "https://github.com/usestrix",
"discord": "https://discord.gg/strix-ai"
}
},
"fonts": {
"family": "Geist",
"heading": {
"family": "Geist"
},
"body": {
"family": "Geist"
}
},
"appearance": {
"default": "dark"
},
"description": "Open-source AI Hackers to secure your Apps",
"background": {
"decoration": "grid"
}
}

BIN
docs/images/favicon-48.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
docs/images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

101
docs/index.mdx Normal file
View File

@@ -0,0 +1,101 @@
---
title: "Introduction"
description: "Open-source AI hackers to secure your apps"
---
Strix are autonomous AI agents that act like real hackers—they run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
<Frame>
<img src="/images/screenshot.png" alt="Strix Demo" />
</Frame>
<CardGroup cols={2}>
<Card title="Quick Start" icon="rocket" href="/quickstart">
Install and run your first scan in minutes.
</Card>
<Card title="CLI Reference" icon="terminal" href="/usage/cli">
Learn all command-line options.
</Card>
<Card title="Tools" icon="wrench" href="/tools/overview">
Explore the security testing toolkit.
</Card>
<Card title="GitHub Actions" icon="github" href="/integrations/github-actions">
Integrate into your CI/CD pipeline.
</Card>
</CardGroup>
## Use Cases
- **Application Security Testing** — Detect and validate critical vulnerabilities in your applications
- **Rapid Penetration Testing** — Get penetration tests done in hours, not weeks
- **Bug Bounty Automation** — Automate research and generate PoCs for faster reporting
- **CI/CD Integration** — Block vulnerabilities before they reach production
## Key Capabilities
- **Full hacker toolkit** — Browser automation, HTTP proxy, terminal, Python runtime
- **Real validation** — PoCs, not false positives
- **Multi-agent orchestration** — Specialized agents collaborate on complex targets
- **Developer-first CLI** — Interactive TUI or headless mode for automation
## Security Tools
Strix agents come equipped with a comprehensive toolkit:
| Tool | Purpose |
|------|---------|
| HTTP Proxy | Full request/response manipulation and analysis |
| Browser Automation | Multi-tab browser for XSS, CSRF, auth flow testing |
| Terminal | Interactive shells for command execution |
| Python Runtime | Custom exploit development and validation |
| Reconnaissance | Automated OSINT and attack surface mapping |
| Code Analysis | Static and dynamic analysis capabilities |
## Vulnerability Coverage
| Category | Examples |
|----------|----------|
| Access Control | IDOR, privilege escalation, auth bypass |
| Injection | SQL, NoSQL, command injection |
| Server-Side | SSRF, XXE, deserialization |
| Client-Side | XSS, prototype pollution, DOM vulnerabilities |
| Business Logic | Race conditions, workflow manipulation |
| Authentication | JWT vulnerabilities, session management |
| Infrastructure | Misconfigurations, exposed services |
## Multi-Agent Architecture
Strix uses a graph of specialized agents for comprehensive security testing:
- **Distributed Workflows** — Specialized agents for different attacks and assets
- **Scalable Testing** — Parallel execution for fast comprehensive coverage
- **Dynamic Coordination** — Agents collaborate and share discoveries
## Quick Example
```bash
# Install
curl -sSL https://strix.ai/install | bash
# Configure
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key"
# Scan
strix --target ./your-app
```
## Community
<CardGroup cols={2}>
<Card title="Discord" icon="discord" href="https://discord.gg/strix-ai">
Join the community for help and discussion.
</Card>
<Card title="GitHub" icon="github" href="https://github.com/usestrix/strix">
Star the repo and contribute.
</Card>
</CardGroup>
<Warning>
Only test applications you own or have explicit permission to test.
</Warning>

View File

@@ -0,0 +1,80 @@
---
title: "CI/CD Integration"
description: "Run Strix in any CI/CD pipeline"
---
Strix runs in headless mode for automated pipelines.
## Headless Mode
Use the `-n` or `--non-interactive` flag:
```bash
strix -n --target ./app --scan-mode quick
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | No vulnerabilities found |
| 1 | Execution error |
| 2 | Vulnerabilities found |
## GitLab CI
```yaml .gitlab-ci.yml
security-scan:
image: docker:latest
services:
- docker:dind
variables:
STRIX_LLM: $STRIX_LLM
LLM_API_KEY: $LLM_API_KEY
script:
- curl -sSL https://strix.ai/install | bash
- strix -n -t ./ --scan-mode quick
```
## Jenkins
```groovy Jenkinsfile
pipeline {
agent any
environment {
STRIX_LLM = credentials('strix-llm')
LLM_API_KEY = credentials('llm-api-key')
}
stages {
stage('Security Scan') {
steps {
sh 'curl -sSL https://strix.ai/install | bash'
sh 'strix -n -t ./ --scan-mode quick'
}
}
}
}
```
## CircleCI
```yaml .circleci/config.yml
version: 2.1
jobs:
security-scan:
docker:
- image: cimg/base:current
steps:
- checkout
- setup_remote_docker
- run:
name: Install Strix
command: curl -sSL https://strix.ai/install | bash
- run:
name: Run Scan
command: strix -n -t ./ --scan-mode quick
```
<Note>
All CI platforms require Docker access. Ensure your runner has Docker available.
</Note>

View File

@@ -0,0 +1,60 @@
---
title: "GitHub Actions"
description: "Run Strix security scans on every pull request"
---
Integrate Strix into your GitHub workflow to catch vulnerabilities before they reach production.
## Basic Workflow
```yaml .github/workflows/security.yml
name: Security Scan
on:
pull_request:
jobs:
strix-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Strix
run: curl -sSL https://strix.ai/install | bash
- name: Run Security Scan
env:
STRIX_LLM: ${{ secrets.STRIX_LLM }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
run: strix -n -t ./ --scan-mode quick
```
## Required Secrets
Add these secrets to your repository:
| Secret | Description |
|--------|-------------|
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5`) |
| `LLM_API_KEY` | API key for your LLM provider |
## Exit Codes
The workflow fails when vulnerabilities are found:
| Code | Result |
|------|--------|
| 0 | Pass — No vulnerabilities |
| 2 | Fail — Vulnerabilities found |
## Scan Modes for CI
| Mode | Duration | Use Case |
|------|----------|----------|
| `quick` | Minutes | Every PR |
| `standard` | ~30 min | Nightly builds |
| `deep` | 1-4 hours | Release candidates |
<Tip>
Use `quick` mode for PRs to keep feedback fast. Schedule `deep` scans nightly.
</Tip>

View File

@@ -0,0 +1,24 @@
---
title: "Anthropic"
description: "Configure Strix with Claude models"
---
## Setup
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="sk-ant-..."
```
## Available Models
| Model | Description |
|-------|-------------|
| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed |
| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
## Get API Key
1. Go to [console.anthropic.com](https://console.anthropic.com)
2. Navigate to API Keys
3. Create a new key

View File

@@ -0,0 +1,37 @@
---
title: "Azure OpenAI"
description: "Configure Strix with OpenAI models via Azure"
---
## Setup
```bash
export STRIX_LLM="azure/your-gpt5-deployment"
export AZURE_API_KEY="your-azure-api-key"
export AZURE_API_BASE="https://your-resource.openai.azure.com"
export AZURE_API_VERSION="2025-11-01-preview"
```
## Configuration
| Variable | Description |
|----------|-------------|
| `STRIX_LLM` | `azure/<your-deployment-name>` |
| `AZURE_API_KEY` | Your Azure OpenAI API key |
| `AZURE_API_BASE` | Your Azure OpenAI endpoint URL |
| `AZURE_API_VERSION` | API version (e.g., `2025-11-01-preview`) |
## Example
```bash
export STRIX_LLM="azure/gpt-5-deployment"
export AZURE_API_KEY="abc123..."
export AZURE_API_BASE="https://mycompany.openai.azure.com"
export AZURE_API_VERSION="2025-11-01-preview"
```
## Prerequisites
1. Create an Azure OpenAI resource
2. Deploy a model (e.g., GPT-5)
3. Get the endpoint URL and API key from the Azure portal

View File

@@ -0,0 +1,47 @@
---
title: "AWS Bedrock"
description: "Configure Strix with models via AWS Bedrock"
---
## Setup
```bash
export STRIX_LLM="bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0"
```
No API key required—uses AWS credentials from environment.
## Authentication
### Option 1: AWS CLI Profile
```bash
export AWS_PROFILE="your-profile"
export AWS_REGION="us-east-1"
```
### Option 2: Access Keys
```bash
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
```
### Option 3: IAM Role (EC2/ECS)
Automatically uses instance role credentials.
## Available Models
| Model | Description |
|-------|-------------|
| `bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0` | Claude 4.5 Sonnet |
| `bedrock/anthropic.claude-4-5-opus-20251022-v1:0` | Claude 4.5 Opus |
| `bedrock/anthropic.claude-4-5-haiku-20251022-v1:0` | Claude 4.5 Haiku |
| `bedrock/amazon.titan-text-premier-v2:0` | Amazon Titan Premier v2 |
## Prerequisites
1. Enable model access in the AWS Bedrock console
2. Ensure your IAM role/user has `bedrock:InvokeModel` permission

View File

@@ -0,0 +1,56 @@
---
title: "Local Models"
description: "Run Strix with self-hosted LLMs for privacy and air-gapped testing"
---
Running Strix with local models allows for completely offline, privacy-first security assessments. Data never leaves your machine, making this ideal for sensitive internal networks or air-gapped environments.
## Privacy vs Performance
| Feature | Local Models | Cloud Models (GPT-5/Claude 4.5) |
|---------|--------------|--------------------------------|
| **Privacy** | 🔒 Data stays local | Data sent to provider |
| **Cost** | Free (hardware only) | Pay-per-token |
| **Reasoning** | Lower (struggles with agents) | State-of-the-art |
| **Setup** | Complex (GPU required) | Instant |
<Warning>
**Compatibility Note**: Strix relies on advanced agentic capabilities (tool use, multi-step planning, self-correction). Most local models, especially those under 70B parameters, struggle with these complex tasks.
For critical assessments, we strongly recommend using state-of-the-art cloud models like **Claude 4.5 Sonnet** or **GPT-5**. Use local models only when privacy is the absolute priority.
</Warning>
## Ollama
[Ollama](https://ollama.ai) is the easiest way to run local models on macOS, Linux, and Windows.
### Setup
1. Install Ollama from [ollama.ai](https://ollama.ai)
2. Pull a high-performance model:
```bash
ollama pull qwen3-vl
```
3. Configure Strix:
```bash
export STRIX_LLM="ollama/qwen3-vl"
export LLM_API_BASE="http://localhost:11434"
```
### Recommended Models
We recommend these models for the best balance of reasoning and tool use:
**Recommended models:**
- **Qwen3 VL** (`ollama pull qwen3-vl`)
- **DeepSeek V3.1** (`ollama pull deepseek-v3.1`)
- **Devstral 2** (`ollama pull devstral-2`)
## LM Studio / OpenAI Compatible
If you use LM Studio, vLLM, or other runners:
```bash
export STRIX_LLM="openai/local-model"
export LLM_API_BASE="http://localhost:1234/v1" # Adjust port as needed
```

View File

@@ -0,0 +1,80 @@
---
title: "Strix Router"
description: "Access top LLMs through a single API with high rate limits and zero data retention"
---
Strix Router gives you access to the best LLMs through a single API key.
<Note>
Strix Router is currently in **beta**. It's completely optional — Strix works with any [LiteLLM-compatible provider](/llm-providers/overview) using your own API keys, or with [local models](/llm-providers/local). Strix Router is just the setup we test and optimize for.
</Note>
## Why Use Strix Router?
- **High rate limits** — No throttling during long-running scans
- **Zero data retention** — Routes to providers with zero data retention policies enabled
- **Failover & load balancing** — Automatic fallback across providers for reliability
- **Simple setup** — One API key, one environment variable, no provider accounts needed
- **No markup** — Same token pricing as the underlying providers, no extra fees
- **$10 free credit** — Try it free on signup, no credit card required
## Quick Start
1. Get your API key at [models.strix.ai](https://models.strix.ai)
2. Set your environment:
```bash
export LLM_API_KEY='your-strix-api-key'
export STRIX_LLM='strix/gpt-5'
```
3. Run a scan:
```bash
strix --target ./your-app
```
## Available Models
### Anthropic
| Model | ID |
|-------|-----|
| Claude Sonnet 4.6 | `strix/claude-sonnet-4.6` |
| Claude Opus 4.6 | `strix/claude-opus-4.6` |
### OpenAI
| Model | ID |
|-------|-----|
| GPT-5.2 | `strix/gpt-5.2` |
| GPT-5.1 | `strix/gpt-5.1` |
| GPT-5 | `strix/gpt-5` |
| GPT-5.2 Codex | `strix/gpt-5.2-codex` |
| GPT-5.1 Codex Max | `strix/gpt-5.1-codex-max` |
| GPT-5.1 Codex | `strix/gpt-5.1-codex` |
| GPT-5 Codex | `strix/gpt-5-codex` |
### Google
| Model | ID |
|-------|-----|
| Gemini 3 Pro | `strix/gemini-3-pro-preview` |
| Gemini 3 Flash | `strix/gemini-3-flash-preview` |
### Other
| Model | ID |
|-------|-----|
| GLM-5 | `strix/glm-5` |
| GLM-4.7 | `strix/glm-4.7` |
## Configuration Reference
<ParamField path="LLM_API_KEY" type="string" required>
Your Strix API key from [models.strix.ai](https://models.strix.ai).
</ParamField>
<ParamField path="STRIX_LLM" type="string" required>
Model ID from the tables above. Must be prefixed with `strix/`.
</ParamField>

View File

@@ -0,0 +1,31 @@
---
title: "OpenAI"
description: "Configure Strix with OpenAI models"
---
## Setup
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="sk-..."
```
## Available Models
See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list of available models.
## Get API Key
1. Go to [platform.openai.com](https://platform.openai.com)
2. Navigate to API Keys
3. Create a new secret key
## Custom Base URL
For OpenAI-compatible APIs:
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-key"
export LLM_API_BASE="https://your-proxy.com/v1"
```

View File

@@ -0,0 +1,37 @@
---
title: "OpenRouter"
description: "Configure Strix with models via OpenRouter"
---
[OpenRouter](https://openrouter.ai) provides access to 100+ models from multiple providers through a single API.
## Setup
```bash
export STRIX_LLM="openrouter/openai/gpt-5"
export LLM_API_KEY="sk-or-..."
```
## Available Models
Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
| Model | Configuration |
|-------|---------------|
| GPT-5 | `openrouter/openai/gpt-5` |
| Claude Sonnet 4.6 | `openrouter/anthropic/claude-sonnet-4.6` |
| Gemini 3 Pro | `openrouter/google/gemini-3-pro-preview` |
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
## Get API Key
1. Go to [openrouter.ai](https://openrouter.ai)
2. Sign in and navigate to Keys
3. Create a new API key
## Benefits
- **Single API** — Access models from OpenAI, Anthropic, Google, Meta, and more
- **Fallback routing** — Automatic failover between providers
- **Cost tracking** — Monitor usage across all models
- **Higher rate limits** — OpenRouter handles provider limits for you

View File

@@ -0,0 +1,84 @@
---
title: "Overview"
description: "Configure your AI model for Strix"
---
Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers.
## Strix Router (Recommended)
The fastest way to get started. [Strix Router](/llm-providers/models) gives you access to tested models with the highest rate limits and zero data retention.
```bash
export STRIX_LLM="strix/gpt-5"
export LLM_API_KEY="your-strix-api-key"
```
Get your API key at [models.strix.ai](https://models.strix.ai).
## Bring Your Own Key
You can also use any LiteLLM-compatible provider with your own API keys:
| Model | Provider | Configuration |
| ----------------- | ------------- | -------------------------------- |
| GPT-5 | OpenAI | `openai/gpt-5` |
| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key"
```
## Local Models
Run models locally with [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai), or any OpenAI-compatible server:
```bash
export STRIX_LLM="ollama/llama4"
export LLM_API_BASE="http://localhost:11434"
```
See the [Local Models guide](/llm-providers/local) for setup instructions and recommended models.
## Provider Guides
<CardGroup cols={2}>
<Card title="Strix Router" href="/llm-providers/models">
Recommended models router with high rate limits.
</Card>
<Card title="OpenAI" href="/llm-providers/openai">
GPT-5 and Codex models.
</Card>
<Card title="Anthropic" href="/llm-providers/anthropic">
Claude Opus, Sonnet, and Haiku.
</Card>
<Card title="OpenRouter" href="/llm-providers/openrouter">
Access 100+ models through a single API.
</Card>
<Card title="Google Vertex AI" href="/llm-providers/vertex">
Gemini 3 models via Google Cloud.
</Card>
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
Claude and Titan models via AWS.
</Card>
<Card title="Azure OpenAI" href="/llm-providers/azure">
GPT-5 via Azure.
</Card>
<Card title="Local Models" href="/llm-providers/local">
Llama 4, Mistral, and self-hosted models.
</Card>
</CardGroup>
## Model Format
Use LiteLLM's `provider/model-name` format:
```
openai/gpt-5
anthropic/claude-sonnet-4-6
vertex_ai/gemini-3-pro-preview
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
ollama/llama4
```

View File

@@ -0,0 +1,53 @@
---
title: "Google Vertex AI"
description: "Configure Strix with Gemini models via Google Cloud"
---
## Installation
Vertex AI requires the Google Cloud dependency. Install Strix with the vertex extra:
```bash
pipx install "strix-agent[vertex]"
```
## Setup
```bash
export STRIX_LLM="vertex_ai/gemini-3-pro-preview"
```
No API key required—uses Google Cloud Application Default Credentials.
## Authentication
### Option 1: gcloud CLI
```bash
gcloud auth application-default login
```
### Option 2: Service Account
```bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
```
## Available Models
| Model | Description |
|-------|-------------|
| `vertex_ai/gemini-3-pro-preview` | Best overall performance for security testing |
| `vertex_ai/gemini-3-flash-preview` | Faster and cheaper |
## Project Configuration
```bash
export VERTEXAI_PROJECT="your-project-id"
export VERTEXAI_LOCATION="us-central1"
```
## Prerequisites
1. Enable the Vertex AI API in your Google Cloud project
2. Ensure your account has the `Vertex AI User` role

BIN
docs/logo/strix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

86
docs/quickstart.mdx Normal file
View File

@@ -0,0 +1,86 @@
---
title: "Quick Start"
description: "Install Strix and run your first security scan"
---
## Prerequisites
- Docker (running)
- An LLM API key — use [Strix Router](/llm-providers/models) for the easiest setup, or bring your own key from any [supported provider](/llm-providers/overview)
## Installation
<Tabs>
<Tab title="curl">
```bash
curl -sSL https://strix.ai/install | bash
```
</Tab>
<Tab title="pipx">
```bash
pipx install strix-agent
```
</Tab>
</Tabs>
## Configuration
Set your LLM provider:
<Tabs>
<Tab title="Strix Router">
```bash
export STRIX_LLM="strix/gpt-5"
export LLM_API_KEY="your-strix-api-key"
```
</Tab>
<Tab title="Bring Your Own Key">
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key"
```
</Tab>
</Tabs>
<Tip>
For best results, use `strix/gpt-5`, `strix/claude-opus-4.6`, or `strix/gpt-5.2`.
</Tip>
## Run Your First Scan
```bash
strix --target ./your-app
```
<Note>
First run pulls the Docker sandbox image automatically. Results are saved to `strix_runs/<run-name>`.
</Note>
## Target Types
Strix accepts multiple target types:
```bash
# Local codebase
strix --target ./app-directory
# GitHub repository
strix --target https://github.com/org/repo
# Live web application
strix --target https://your-app.com
# Multiple targets (white-box testing)
strix -t https://github.com/org/repo -t https://your-app.com
```
## Next Steps
<CardGroup cols={2}>
<Card title="CLI Options" icon="terminal" href="/usage/cli">
Explore all command-line options.
</Card>
<Card title="Scan Modes" icon="gauge" href="/usage/scan-modes">
Choose the right scan depth.
</Card>
</CardGroup>

34
docs/tools/browser.mdx Normal file
View File

@@ -0,0 +1,34 @@
---
title: "Browser"
description: "Playwright-powered Chrome for web application testing"
---
Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would.
## How It Works
All browser traffic is automatically routed through the Caido proxy, giving Strix full visibility into every request and response. This enables:
- Testing client-side vulnerabilities (XSS, DOM manipulation)
- Navigating authenticated flows (login, OAuth, MFA)
- Triggering JavaScript-heavy functionality
- Capturing dynamically generated requests
## Capabilities
| Action | Description |
| ---------- | ------------------------------------------- |
| Navigate | Go to URLs, follow links, handle redirects |
| Click | Interact with buttons, links, form elements |
| Type | Fill in forms, search boxes, input fields |
| Execute JS | Run custom JavaScript in the page context |
| Screenshot | Capture visual state for reports |
| Multi-tab | Test across multiple browser tabs |
## Example Flow
1. Agent launches browser and navigates to login page
2. Fills in credentials and submits form
3. Proxy captures the authentication request
4. Agent navigates to protected areas
5. Tests for IDOR by replaying requests with modified IDs

33
docs/tools/overview.mdx Normal file
View File

@@ -0,0 +1,33 @@
---
title: "Agent Tools"
description: "How Strix agents interact with targets"
---
Strix agents use specialized tools to test your applications like a real penetration tester would.
## Core Tools
<CardGroup cols={2}>
<Card title="Browser" icon="globe" href="/tools/browser">
Playwright-powered Chrome for interacting with web UIs.
</Card>
<Card title="HTTP Proxy" icon="network-wired" href="/tools/proxy">
Caido-powered proxy for intercepting and replaying requests.
</Card>
<Card title="Terminal" icon="terminal" href="/tools/terminal">
Bash shell for running commands and security tools.
</Card>
<Card title="Sandbox Tools" icon="toolbox" href="/tools/sandbox">
Pre-installed security tools: Nuclei, ffuf, and more.
</Card>
</CardGroup>
## Additional Tools
| Tool | Purpose |
| -------------- | ---------------------------------------- |
| Python Runtime | Write and execute custom exploit scripts |
| File Editor | Read and modify source code |
| Web Search | Real-time OSINT via Perplexity |
| Notes | Document findings during the scan |
| Reporting | Generate vulnerability reports with PoCs |

90
docs/tools/proxy.mdx Normal file
View File

@@ -0,0 +1,90 @@
---
title: "HTTP Proxy"
description: "Caido-powered proxy for request interception and replay"
---
Strix includes [Caido](https://caido.io), a modern HTTP proxy built for security testing. All browser traffic flows through Caido, giving the agent full control over requests and responses.
## Capabilities
| Feature | Description |
| ---------------- | -------------------------------------------- |
| Request Capture | Log all HTTP/HTTPS traffic automatically |
| Request Replay | Repeat any request with modifications |
| HTTPQL | Query captured traffic with powerful filters |
| Scope Management | Focus on specific domains or paths |
| Sitemap | Visualize the discovered attack surface |
## HTTPQL Filtering
Query captured requests using Caido's HTTPQL syntax
## Request Replay
The agent can take any captured request and replay it with modifications:
- Change path parameters (test for IDOR)
- Modify request body (test for injection)
- Add/remove headers (test for auth bypass)
- Alter cookies (test for session issues)
## Python Integration
All proxy functions are automatically available in Python sessions. This enables powerful scripted security testing:
```python
# List recent POST requests
post_requests = list_requests(
httpql_filter='req.method.eq:"POST"',
page_size=20
)
# View a specific request
request_details = view_request("req_123", part="request")
# Replay with modified payload
response = repeat_request("req_123", {
"body": '{"user_id": "admin"}'
})
print(f"Status: {response['status_code']}")
```
### Available Functions
| Function | Description |
| ---------------------- | ------------------------------------------ |
| `list_requests()` | Query captured traffic with HTTPQL filters |
| `view_request()` | Get full request/response details |
| `repeat_request()` | Replay a request with modifications |
| `send_request()` | Send a new HTTP request |
| `scope_rules()` | Manage proxy scope (allowlist/denylist) |
| `list_sitemap()` | View discovered endpoints |
| `view_sitemap_entry()` | Get details for a sitemap entry |
### Example: Automated IDOR Testing
```python
# Get all requests to user endpoints
user_requests = list_requests(
httpql_filter='req.path.cont:"/users/"'
)
for req in user_requests.get('requests', []):
# Try accessing with different user IDs
for test_id in ['1', '2', 'admin', '../admin']:
response = repeat_request(req['id'], {
'url': req['path'].replace('/users/1', f'/users/{test_id}')
})
if response['status_code'] == 200:
print(f"Potential IDOR: {test_id} returned 200")
```
## Scope
Create scopes to filter traffic to relevant domains:
```
Allowlist: ["api.example.com", "*.example.com"]
Denylist: ["*.gif", "*.jpg", "*.png", "*.css", "*.js"]
```

83
docs/tools/sandbox.mdx Normal file
View File

@@ -0,0 +1,83 @@
---
title: "Sandbox Tools"
description: "Pre-installed security tools in the Strix container"
---
Strix runs inside a Kali Linux-based Docker container with a comprehensive set of security tools pre-installed. The agent can use any of these tools through the [terminal](/tools/terminal).
## Reconnaissance
| Tool | Description |
| ---------------------------------------------------------- | -------------------------------------- |
| [Subfinder](https://github.com/projectdiscovery/subfinder) | Subdomain discovery |
| [Naabu](https://github.com/projectdiscovery/naabu) | Fast port scanner |
| [httpx](https://github.com/projectdiscovery/httpx) | HTTP probing and analysis |
| [Katana](https://github.com/projectdiscovery/katana) | Web crawling and spidering |
| [ffuf](https://github.com/ffuf/ffuf) | Fast web fuzzer |
| [Nmap](https://nmap.org) | Network scanning and service detection |
## Web Testing
| Tool | Description |
| ------------------------------------------------------ | -------------------------------- |
| [Arjun](https://github.com/s0md3v/Arjun) | HTTP parameter discovery |
| [Dirsearch](https://github.com/maurosoria/dirsearch) | Directory and file brute-forcing |
| [wafw00f](https://github.com/EnableSecurity/wafw00f) | WAF fingerprinting |
| [GoSpider](https://github.com/jaeles-project/gospider) | Web spider for link extraction |
## Automated Scanners
| Tool | Description |
| ---------------------------------------------------- | -------------------------------------------------- |
| [Nuclei](https://github.com/projectdiscovery/nuclei) | Template-based vulnerability scanner |
| [SQLMap](https://sqlmap.org) | Automatic SQL injection detection and exploitation |
| [Wapiti](https://wapiti-scanner.github.io) | Web application vulnerability scanner |
| [ZAP](https://zaproxy.org) | OWASP Zed Attack Proxy |
## JavaScript Analysis
| Tool | Description |
| -------------------------------------------------------- | ------------------------------ |
| [JS-Snooper](https://github.com/aravind0x7/JS-Snooper) | JavaScript reconnaissance |
| [jsniper](https://github.com/xchopath/jsniper.sh) | JavaScript file analysis |
| [Retire.js](https://retirejs.github.io/retire.js) | Detect vulnerable JS libraries |
| [ESLint](https://eslint.org) | JavaScript static analysis |
| [js-beautify](https://github.com/beautifier/js-beautify) | JavaScript deobfuscation |
| [JSHint](https://jshint.com) | JavaScript code quality tool |
## Secret Detection
| Tool | Description |
| ----------------------------------------------------------- | ------------------------------------- |
| [TruffleHog](https://github.com/trufflesecurity/trufflehog) | Find secrets in code and history |
| [Semgrep](https://github.com/semgrep/semgrep) | Static analysis for security patterns |
| [Bandit](https://bandit.readthedocs.io) | Python security linter |
## Authentication Testing
| Tool | Description |
| ------------------------------------------------------------ | ---------------------------------- |
| [jwt_tool](https://github.com/ticarpi/jwt_tool) | JWT token testing and exploitation |
| [Interactsh](https://github.com/projectdiscovery/interactsh) | Out-of-band interaction detection |
## Container & Supply Chain
| Tool | Description |
| -------------------------- | ---------------------------------------------- |
| [Trivy](https://trivy.dev) | Container and dependency vulnerability scanner |
## HTTP Proxy
| Tool | Description |
| ------------------------- | --------------------------------------------- |
| [Caido](https://caido.io) | Modern HTTP proxy for interception and replay |
## Browser
| Tool | Description |
| ------------------------------------ | --------------------------- |
| [Playwright](https://playwright.dev) | Headless browser automation |
<Note>
All tools are pre-configured and ready to use. The agent selects the appropriate tool based on the vulnerability being tested.
</Note>

61
docs/tools/terminal.mdx Normal file
View File

@@ -0,0 +1,61 @@
---
title: "Terminal"
description: "Bash shell for running commands and security tools"
---
Strix has access to a persistent bash terminal running inside the Docker sandbox. This gives the agent access to all [pre-installed security tools](/tools/sandbox).
## Capabilities
| Feature | Description |
| ----------------- | ---------------------------------------------------------- |
| Persistent state | Working directory and environment persist between commands |
| Multiple sessions | Run parallel terminals for concurrent operations |
| Background jobs | Start long-running processes without blocking |
| Interactive | Respond to prompts and control running processes |
## Common Uses
### Running Security Tools
```bash
# Subdomain enumeration
subfinder -d example.com
# Vulnerability scanning
nuclei -u https://example.com
# SQL injection testing
sqlmap -u "https://example.com/page?id=1"
```
### Code Analysis
```bash
# Search for secrets
trufflehog filesystem ./
# Static analysis
semgrep --config auto ./src
# Grep for patterns
grep -r "password" ./
```
### Custom Scripts
```bash
# Run Python exploits
python3 exploit.py
# Execute shell scripts
./test_auth_bypass.sh
```
## Session Management
The agent can run multiple terminal sessions concurrently, for example:
- Main session for primary testing
- Secondary session for monitoring
- Background processes for servers or watchers

62
docs/usage/cli.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: "CLI Reference"
description: "Command-line options for Strix"
---
## Basic Usage
```bash
strix --target <target> [options]
```
## Options
<ParamField path="--target, -t" type="string" required>
Target to test. Accepts URLs, repositories, local directories, domains, or IP addresses. Can be specified multiple times.
</ParamField>
<ParamField path="--instruction" type="string">
Custom instructions for the scan. Use for credentials, focus areas, or specific testing approaches.
</ParamField>
<ParamField path="--instruction-file" type="string">
Path to a file containing detailed instructions.
</ParamField>
<ParamField path="--scan-mode, -m" type="string" default="deep">
Scan depth: `quick`, `standard`, or `deep`.
</ParamField>
<ParamField path="--non-interactive, -n" type="boolean">
Run in headless mode without TUI. Ideal for CI/CD.
</ParamField>
<ParamField path="--config" type="string">
Path to a custom config file (JSON) to use instead of `~/.strix/cli-config.json`.
</ParamField>
## Examples
```bash
# Basic scan
strix --target https://example.com
# Authenticated testing
strix --target https://app.com --instruction "Use credentials: user:pass"
# Focused testing
strix --target api.example.com --instruction "Focus on IDOR and auth bypass"
# CI/CD mode
strix -n --target ./ --scan-mode quick
# Multi-target white-box testing
strix -t https://github.com/org/app -t https://staging.example.com
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Scan completed, no vulnerabilities found |
| 2 | Vulnerabilities found (headless mode only) |

View File

@@ -0,0 +1,73 @@
---
title: "Custom Instructions"
description: "Guide Strix with custom testing instructions"
---
Use instructions to provide context, credentials, or focus areas for your scan.
## Inline Instructions
```bash
strix --target https://app.com --instruction "Focus on authentication vulnerabilities"
```
## File-Based Instructions
For complex instructions, use a file:
```bash
strix --target https://app.com --instruction-file ./pentest-instructions.md
```
## Common Use Cases
### Authenticated Testing
```bash
strix --target https://app.com \
--instruction "Login with email: test@example.com, password: TestPass123"
```
### Focused Scope
```bash
strix --target https://api.example.com \
--instruction "Focus on IDOR vulnerabilities in the /api/users endpoints"
```
### Exclusions
```bash
strix --target https://app.com \
--instruction "Do not test /admin or /internal endpoints"
```
### API Testing
```bash
strix --target https://api.example.com \
--instruction "Use API key header: X-API-Key: abc123. Focus on rate limiting bypass."
```
## Instruction File Example
```markdown instructions.md
# Penetration Test Instructions
## Credentials
- Admin: admin@example.com / AdminPass123
- User: user@example.com / UserPass123
## Focus Areas
1. IDOR in user profile endpoints
2. Privilege escalation between roles
3. JWT token manipulation
## Out of Scope
- /health endpoints
- Third-party integrations
```
<Tip>
Be specific. Good instructions help Strix prioritize the most valuable attack paths.
</Tip>

58
docs/usage/scan-modes.mdx Normal file
View File

@@ -0,0 +1,58 @@
---
title: "Scan Modes"
description: "Choose the right scan depth for your use case"
---
Strix offers three scan modes to balance speed and thoroughness.
## Quick
```bash
strix --target ./app --scan-mode quick
```
Fast checks for obvious vulnerabilities. Best for:
- CI/CD pipelines
- Pull request validation
- Rapid smoke tests
**Duration**: Minutes
## Standard
```bash
strix --target ./app --scan-mode standard
```
Balanced testing for routine security reviews. Best for:
- Regular security assessments
- Pre-release validation
- Development milestones
**Duration**: 30 minutes to 1 hour
## Deep
```bash
strix --target ./app --scan-mode deep
```
Thorough penetration testing. Best for:
- Comprehensive security audits
- Pre-production reviews
- Critical application assessments
**Duration**: 1-4 hours depending on target complexity
<Note>
Deep mode is the default. It explores edge cases, chained vulnerabilities, and complex attack paths.
</Note>
## Choosing a Mode
| Scenario | Recommended Mode |
|----------|------------------|
| Every PR | Quick |
| Weekly scans | Standard |
| Before major release | Deep |
| Bug bounty hunting | Deep |

679
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "strix-agent" name = "strix-agent"
version = "0.6.1" version = "0.8.1"
description = "Open-source AI Hackers for your apps" description = "Open-source AI Hackers for your apps"
authors = ["Strix <hi@usestrix.com>"] authors = ["Strix <hi@usestrix.com>"]
readme = "README.md" readme = "README.md"
@@ -35,7 +35,8 @@ packages = [
include = [ include = [
"LICENSE", "LICENSE",
"README.md", "README.md",
"strix/**/*.jinja", "strix/agents/**/*.jinja",
"strix/skills/**/*.md",
"strix/**/*.xml", "strix/**/*.xml",
"strix/**/*.tcss" "strix/**/*.tcss"
] ]
@@ -46,7 +47,7 @@ strix = "strix.interface.main:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
# Core CLI dependencies # Core CLI dependencies
litellm = { version = "~1.80.7", extras = ["proxy"] } litellm = { version = "~1.81.1", extras = ["proxy"] }
tenacity = "^9.0.0" tenacity = "^9.0.0"
pydantic = {extras = ["email"], version = "^2.11.3"} pydantic = {extras = ["email"], version = "^2.11.3"}
rich = "*" rich = "*"

View File

@@ -4,7 +4,7 @@ set -euo pipefail
APP=strix APP=strix
REPO="usestrix/strix" REPO="usestrix/strix"
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.10" STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.11"
MUTED='\033[0;2m' MUTED='\033[0;2m'
RED='\033[0;31m' RED='\033[0;31m'
@@ -209,11 +209,16 @@ check_docker() {
add_to_path() { add_to_path() {
local config_file=$1 local config_file=$1
local command=$2 local command=$2
if grep -Fxq "$command" "$config_file" 2>/dev/null; then if grep -Fxq "$command" "$config_file" 2>/dev/null; then
return 0 print_message info "${MUTED}PATH already configured in ${NC}$config_file"
elif [[ -w $config_file ]]; then elif [[ -w $config_file ]]; then
echo -e "\n# strix" >> "$config_file" echo -e "\n# strix" >> "$config_file"
echo "$command" >> "$config_file" echo "$command" >> "$config_file"
print_message info "${MUTED}Successfully added ${NC}strix ${MUTED}to \$PATH in ${NC}$config_file"
else
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " $command"
fi fi
} }
@@ -226,13 +231,19 @@ setup_path() {
config_files="$HOME/.config/fish/config.fish" config_files="$HOME/.config/fish/config.fish"
;; ;;
zsh) zsh)
config_files="$HOME/.zshrc $HOME/.zshenv" config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
;; ;;
bash) bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;;
ash)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;;
sh)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;; ;;
*) *)
config_files="$HOME/.bashrc $HOME/.profile" config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;; ;;
esac esac
@@ -245,23 +256,36 @@ setup_path() {
done done
if [[ -z $config_file ]]; then if [[ -z $config_file ]]; then
config_file="$HOME/.bashrc" print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
touch "$config_file" print_message info " export PATH=$INSTALL_DIR:\$PATH"
fi elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in case $current_shell in
fish) fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR" add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;; ;;
zsh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
bash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
ash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
sh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*) *)
add_to_path "$config_file" "export PATH=\"$INSTALL_DIR:\$PATH\"" export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;; ;;
esac esac
fi fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> "$GITHUB_PATH" echo "$INSTALL_DIR" >> "$GITHUB_PATH"
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi fi
} }
@@ -311,18 +335,20 @@ echo -e "${MUTED} AI Penetration Testing Agent${NC}"
echo "" echo ""
echo -e "${MUTED}To get started:${NC}" echo -e "${MUTED}To get started:${NC}"
echo "" echo ""
echo -e " ${CYAN}1.${NC} Set your LLM provider:" echo -e " ${CYAN}1.${NC} Get your Strix API key:"
echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5'${NC}" echo -e " ${MUTED}https://models.strix.ai${NC}"
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
echo "" echo ""
echo -e " ${CYAN}2.${NC} Run a penetration test:" echo -e " ${CYAN}2.${NC} Set your environment:"
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
echo -e " ${MUTED}export STRIX_LLM='strix/gpt-5'${NC}"
echo ""
echo -e " ${CYAN}3.${NC} Run a penetration test:"
echo -e " ${MUTED}strix --target https://example.com${NC}" echo -e " ${MUTED}strix --target https://example.com${NC}"
echo "" echo ""
echo -e "${MUTED}For more information visit ${NC}https://strix.ai" echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
echo -e "${MUTED}Join our community ${NC}https://discord.gg/YjKFvEZSdZ" echo -e "${MUTED}Supported models ${NC}https://docs.strix.ai/llm-providers/overview"
echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai"
echo "" echo ""
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then echo -e "${YELLOW}${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
echo -e "${YELLOW}${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal" echo ""
echo ""
fi

View File

@@ -9,7 +9,11 @@ strix_root = project_root / 'strix'
datas = [] datas = []
for jinja_file in strix_root.rglob('*.jinja'): for md_file in strix_root.rglob('skills/**/*.md'):
rel_path = md_file.relative_to(project_root)
datas.append((str(md_file), str(rel_path.parent)))
for jinja_file in strix_root.rglob('agents/**/*.jinja'):
rel_path = jinja_file.relative_to(project_root) rel_path = jinja_file.relative_to(project_root)
datas.append((str(jinja_file), str(rel_path.parent))) datas.append((str(jinja_file), str(rel_path.parent)))
@@ -86,6 +90,14 @@ hiddenimports = [
# XML parsing # XML parsing
'xmltodict', 'xmltodict',
'defusedxml',
'defusedxml.ElementTree',
# Syntax highlighting
'pygments',
'pygments.lexers',
'pygments.styles',
'pygments.util',
# Tiktoken (for token counting) # Tiktoken (for token counting)
'tiktoken', 'tiktoken',
@@ -95,6 +107,9 @@ hiddenimports = [
# Tenacity retry # Tenacity retry
'tenacity', 'tenacity',
# CVSS scoring
'cvss',
# Strix modules # Strix modules
'strix', 'strix',
'strix.interface', 'strix.interface',
@@ -128,6 +143,7 @@ hiddenimports += collect_submodules('litellm')
hiddenimports += collect_submodules('textual') hiddenimports += collect_submodules('textual')
hiddenimports += collect_submodules('rich') hiddenimports += collect_submodules('rich')
hiddenimports += collect_submodules('pydantic') hiddenimports += collect_submodules('pydantic')
hiddenimports += collect_submodules('pygments')
excludes = [ excludes = [
# Sandbox-only packages # Sandbox-only packages

View File

@@ -16,9 +16,9 @@ CLI OUTPUT:
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs - NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
INTER-AGENT MESSAGES: INTER-AGENT MESSAGES:
- NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output. - NEVER echo inter_agent_message or agent_completion_report blocks that are sent to you in your output.
- Process these internally without displaying the XML - Process these internally without displaying them
- NEVER echo agent_identity XML blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls. - NEVER echo agent_identity blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
- Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging - Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging
AUTONOMOUS BEHAVIOR: AUTONOMOUS BEHAVIOR:
@@ -301,7 +301,7 @@ PERSISTENCE IS MANDATORY:
</multi_agent_system> </multi_agent_system>
<tool_usage> <tool_usage>
Tool calls use XML format: Tool call format:
<function=tool_name> <function=tool_name>
<parameter=param_name>value</parameter> <parameter=param_name>value</parameter>
</function> </function>
@@ -311,16 +311,40 @@ CRITICAL RULES:
1. Exactly one tool call per message — never include more than one <function>...</function> block in a single LLM message. 1. Exactly one tool call per message — never include more than one <function>...</function> block in a single LLM message.
2. Tool call must be last in message 2. Tool call must be last in message
3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>. 3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>.
4. Use ONLY the exact XML format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters. 4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. If you send "\n" instead of real line breaks inside the XML parameter value, tools may fail or behave incorrectly. 5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants). 6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
- Correct: <function=think> ... </function>
- Incorrect: <thinking_tools.think> ... </function>
- Incorrect: <think> ... </think>
- Incorrect: {"think": {...}}
7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values. 7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block. 8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
CORRECT format — use this EXACTLY:
<function=tool_name>
<parameter=param_name>value</parameter>
</function>
WRONG formats — NEVER use these:
- <invoke name="tool_name"><parameter name="param_name">value</parameter></invoke>
- <function_calls><invoke name="tool_name">...</invoke></function_calls>
- <tool_call><tool_name>...</tool_name></tool_call>
- {"tool_name": {"param_name": "value"}}
- ```<function=tool_name>...</function>```
- <function=tool_name>value_without_parameter_tags</function>
EVERY argument MUST be wrapped in <parameter=name>...</parameter> tags. NEVER put values directly in the function body without parameter tags. This WILL cause the tool call to fail.
Do NOT emit any extra XML tags in your output. In particular:
- NO <thinking>...</thinking> or <thought>...</thought> blocks
- NO <scratchpad>...</scratchpad> or <reasoning>...</reasoning> blocks
- NO <answer>...</answer> or <response>...</response> wrappers
If you need to reason, use the think tool. Your raw output must contain ONLY the tool call — no surrounding XML tags.
Notice: use <function=X> NOT <invoke name="X">, use <parameter=X> NOT <parameter name="X">, use </function> NOT </invoke>.
Example (terminal tool):
<function=terminal_execute>
<parameter=command>nmap -sV -p 1-1000 target.com</parameter>
</function>
Example (agent creation tool): Example (agent creation tool):
<function=create_agent> <function=create_agent>
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter> <parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
@@ -398,11 +422,10 @@ Default user: pentester (sudo available)
{% if loaded_skill_names %} {% if loaded_skill_names %}
<specialized_knowledge> <specialized_knowledge>
{# Dynamic skills loaded based on agent specialization #}
{% for skill_name in loaded_skill_names %} {% for skill_name in loaded_skill_names %}
<{{ skill_name }}>
{{ get_skill(skill_name) }} {{ get_skill(skill_name) }}
</{{ skill_name }}>
{% endfor %} {% endfor %}
</specialized_knowledge> </specialized_knowledge>
{% endif %} {% endif %}

View File

@@ -43,7 +43,9 @@ class AgentState(BaseModel):
self.iteration += 1 self.iteration += 1
self.last_updated = datetime.now(UTC).isoformat() self.last_updated = datetime.now(UTC).isoformat()
def add_message(self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None) -> None: def add_message(
self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None
) -> None:
message = {"role": role, "content": content} message = {"role": role, "content": content}
if thinking_blocks: if thinking_blocks:
message["thinking_blocks"] = thinking_blocks message["thinking_blocks"] = thinking_blocks

View File

@@ -5,6 +5,9 @@ from pathlib import Path
from typing import Any from typing import Any
STRIX_API_BASE = "https://models.strix.ai/api/v1"
class Config: class Config:
"""Configuration Manager for Strix.""" """Configuration Manager for Strix."""
@@ -19,13 +22,25 @@ class Config:
strix_llm_max_retries = "5" strix_llm_max_retries = "5"
strix_memory_compressor_timeout = "30" strix_memory_compressor_timeout = "30"
llm_timeout = "300" llm_timeout = "300"
_LLM_CANONICAL_NAMES = (
"strix_llm",
"llm_api_key",
"llm_api_base",
"openai_api_base",
"litellm_base_url",
"ollama_api_base",
"strix_reasoning_effort",
"strix_llm_max_retries",
"strix_memory_compressor_timeout",
"llm_timeout",
)
# Tool & Feature Configuration # Tool & Feature Configuration
perplexity_api_key = None perplexity_api_key = None
strix_disable_browser = "false" strix_disable_browser = "false"
# Runtime Configuration # Runtime Configuration
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.10" strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.11"
strix_runtime_backend = "docker" strix_runtime_backend = "docker"
strix_sandbox_execution_timeout = "120" strix_sandbox_execution_timeout = "120"
strix_sandbox_connect_timeout = "10" strix_sandbox_connect_timeout = "10"
@@ -33,6 +48,9 @@ class Config:
# Telemetry # Telemetry
strix_telemetry = "1" strix_telemetry = "1"
# Config file override (set via --config CLI arg)
_config_file_override: Path | None = None
@classmethod @classmethod
def _tracked_names(cls) -> list[str]: def _tracked_names(cls) -> list[str]:
return [ return [
@@ -45,6 +63,20 @@ class Config:
def tracked_vars(cls) -> list[str]: def tracked_vars(cls) -> list[str]:
return [name.upper() for name in cls._tracked_names()] return [name.upper() for name in cls._tracked_names()]
@classmethod
def _llm_env_vars(cls) -> set[str]:
return {name.upper() for name in cls._LLM_CANONICAL_NAMES}
@classmethod
def _llm_env_changed(cls, saved_env: dict[str, Any]) -> bool:
for var_name in cls._llm_env_vars():
current = os.getenv(var_name)
if current is None:
continue
if saved_env.get(var_name) != current:
return True
return False
@classmethod @classmethod
def get(cls, name: str) -> str | None: def get(cls, name: str) -> str | None:
env_name = name.upper() env_name = name.upper()
@@ -57,6 +89,8 @@ class Config:
@classmethod @classmethod
def config_file(cls) -> Path: def config_file(cls) -> Path:
if cls._config_file_override is not None:
return cls._config_file_override
return cls.config_dir() / "cli-config.json" return cls.config_dir() / "cli-config.json"
@classmethod @classmethod
@@ -75,7 +109,7 @@ class Config:
def save(cls, config: dict[str, Any]) -> bool: def save(cls, config: dict[str, Any]) -> bool:
try: try:
cls.config_dir().mkdir(parents=True, exist_ok=True) cls.config_dir().mkdir(parents=True, exist_ok=True)
config_path = cls.config_file() config_path = cls.config_dir() / "cli-config.json"
with config_path.open("w", encoding="utf-8") as f: with config_path.open("w", encoding="utf-8") as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
except OSError: except OSError:
@@ -85,13 +119,30 @@ class Config:
return True return True
@classmethod @classmethod
def apply_saved(cls) -> dict[str, str]: def apply_saved(cls, force: bool = False) -> dict[str, str]:
saved = cls.load() saved = cls.load()
env_vars = saved.get("env", {}) env_vars = saved.get("env", {})
if not isinstance(env_vars, dict):
env_vars = {}
cleared_vars = {
var_name
for var_name in cls.tracked_vars()
if var_name in os.environ and os.environ.get(var_name) == ""
}
if cleared_vars:
for var_name in cleared_vars:
env_vars.pop(var_name, None)
if cls._config_file_override is None:
cls.save({"env": env_vars})
if cls._llm_env_changed(env_vars):
for var_name in cls._llm_env_vars():
env_vars.pop(var_name, None)
if cls._config_file_override is None:
cls.save({"env": env_vars})
applied = {} applied = {}
for var_name, var_value in env_vars.items(): for var_name, var_value in env_vars.items():
if var_name in cls.tracked_vars() and not os.getenv(var_name): if var_name in cls.tracked_vars() and (force or var_name not in os.environ):
os.environ[var_name] = var_value os.environ[var_name] = var_value
applied[var_name] = var_value applied[var_name] = var_value
@@ -123,9 +174,37 @@ class Config:
return cls.save({"env": merged}) return cls.save({"env": merged})
def apply_saved_config() -> dict[str, str]: def apply_saved_config(force: bool = False) -> dict[str, str]:
return Config.apply_saved() return Config.apply_saved(force=force)
def save_current_config() -> bool: def save_current_config() -> bool:
return Config.save_current() return Config.save_current()
def resolve_llm_config() -> tuple[str | None, str | None, str | None]:
"""Resolve LLM model, api_key, and api_base based on STRIX_LLM prefix.
Returns:
tuple: (model_name, api_key, api_base)
- model_name: Original model name (strix/ prefix preserved for display)
- api_key: LLM API key
- api_base: API base URL (auto-set to STRIX_API_BASE for strix/ models)
"""
model = Config.get("strix_llm")
if not model:
return None, None, None
api_key = Config.get("llm_api_key")
if model.startswith("strix/"):
api_base: str | None = STRIX_API_BASE
else:
api_base = (
Config.get("llm_api_base")
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
)
return model, api_key, api_base

View File

@@ -3,6 +3,28 @@ Screen {
color: #d4d4d4; color: #d4d4d4;
} }
.screen--selection {
background: #2d3d2f;
color: #e5e5e5;
}
ToastRack {
dock: top;
align: right top;
margin-bottom: 0;
margin-top: 1;
}
Toast {
width: 25;
background: #000000;
border-left: outer #22c55e;
}
Toast.-information .toast--title {
color: #22c55e;
}
#splash_screen { #splash_screen {
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -36,7 +58,7 @@ Screen {
} }
#sidebar { #sidebar {
width: 25%; width: 20%;
background: transparent; background: transparent;
margin-left: 1; margin-left: 1;
} }
@@ -174,7 +196,7 @@ VulnerabilityDetailScreen {
} }
#chat_area_container { #chat_area_container {
width: 75%; width: 80%;
background: transparent; background: transparent;
} }

View File

@@ -24,30 +24,26 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
console = Console() console = Console()
start_text = Text() start_text = Text()
start_text.append("🦉 ", style="bold white") start_text.append("Penetration test initiated", style="bold #22c55e")
start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
target_text = Text() target_text = Text()
target_text.append("Target", style="dim")
target_text.append(" ")
if len(args.targets_info) == 1: if len(args.targets_info) == 1:
target_text.append("🎯 Target: ", style="bold cyan")
target_text.append(args.targets_info[0]["original"], style="bold white") target_text.append(args.targets_info[0]["original"], style="bold white")
else: else:
target_text.append("🎯 Targets: ", style="bold cyan") target_text.append(f"{len(args.targets_info)} targets", style="bold white")
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white") for target_info in args.targets_info:
for i, target_info in enumerate(args.targets_info): target_text.append("\n ")
target_text.append("", style="dim white")
target_text.append(target_info["original"], style="white") target_text.append(target_info["original"], style="white")
if i < len(args.targets_info) - 1:
target_text.append("\n")
results_text = Text() results_text = Text()
results_text.append("📊 Results will be saved to: ", style="bold cyan") results_text.append("Output", style="dim")
results_text.append(f"strix_runs/{args.run_name}", style="bold white") results_text.append(" ")
results_text.append(f"strix_runs/{args.run_name}", style="#60a5fa")
note_text = Text() note_text = Text()
note_text.append("\n\n", style="dim") note_text.append("\n\n", style="dim")
note_text.append("⏱️ ", style="dim")
note_text.append("This may take a while depending on target complexity. ", style="dim")
note_text.append("Vulnerabilities will be displayed in real-time.", style="dim") note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
startup_panel = Panel( startup_panel = Panel(
@@ -59,9 +55,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
results_text, results_text,
note_text, note_text,
), ),
title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="green", border_style="#22c55e",
padding=(1, 2), padding=(1, 2),
) )
@@ -110,7 +106,10 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
tracer.vulnerability_found_callback = display_vulnerability tracer.vulnerability_found_callback = display_vulnerability
def cleanup_on_exit() -> None: def cleanup_on_exit() -> None:
from strix.runtime import cleanup_runtime
tracer.cleanup() tracer.cleanup()
cleanup_runtime()
def signal_handler(_signum: int, _frame: Any) -> None: def signal_handler(_signum: int, _frame: Any) -> None:
tracer.cleanup() tracer.cleanup()
@@ -126,8 +125,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
def create_live_status() -> Panel: def create_live_status() -> Panel:
status_text = Text() status_text = Text()
status_text.append("🦉 ", style="bold white") status_text.append("Penetration test in progress", style="bold #22c55e")
status_text.append("Running penetration test...", style="bold #22c55e")
status_text.append("\n\n") status_text.append("\n\n")
stats_text = build_live_stats_text(tracer, agent_config) stats_text = build_live_stats_text(tracer, agent_config)
@@ -136,8 +134,8 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
return Panel( return Panel(
status_text, status_text,
title="[bold #22c55e]🔍 Live Penetration Test Status", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="#22c55e", border_style="#22c55e",
padding=(1, 2), padding=(1, 2),
) )
@@ -169,7 +167,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
error_msg = result.get("error", "Unknown error") error_msg = result.get("error", "Unknown error")
error_details = result.get("details") error_details = result.get("details")
console.print() console.print()
console.print(f"[bold red]Penetration test failed:[/] {error_msg}") console.print(f"[bold red]Penetration test failed:[/] {error_msg}")
if error_details: if error_details:
console.print(f"[dim]{error_details}[/]") console.print(f"[dim]{error_details}[/]")
console.print() console.print()
@@ -186,8 +184,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
console.print() console.print()
final_report_text = Text() final_report_text = Text()
final_report_text.append("📄 ", style="bold cyan") final_report_text.append("Penetration test summary", style="bold #60a5fa")
final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
final_report_panel = Panel( final_report_panel = Panel(
Text.assemble( Text.assemble(
@@ -195,9 +192,9 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
"\n\n", "\n\n",
tracer.final_scan_result, tracer.final_scan_result,
), ),
title="[bold cyan]📊 PENETRATION TEST SUMMARY", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="cyan", border_style="#60a5fa",
padding=(1, 2), padding=(1, 2),
) )

View File

@@ -18,6 +18,8 @@ from rich.panel import Panel
from rich.text import Text from rich.text import Text
from strix.config import Config, apply_saved_config, save_current_config from strix.config import Config, apply_saved_config, save_current_config
from strix.config.config import resolve_llm_config
from strix.llm.utils import resolve_strix_model
apply_saved_config() apply_saved_config()
@@ -35,6 +37,7 @@ from strix.interface.utils import ( # noqa: E402
infer_target_type, infer_target_type,
process_pull_line, process_pull_line,
rewrite_localhost_targets, rewrite_localhost_targets,
validate_config_file,
validate_llm_response, validate_llm_response,
) )
from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402 from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402
@@ -50,10 +53,13 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
missing_required_vars = [] missing_required_vars = []
missing_optional_vars = [] missing_optional_vars = []
if not Config.get("strix_llm"): strix_llm = Config.get("strix_llm")
uses_strix_models = strix_llm and strix_llm.startswith("strix/")
if not strix_llm:
missing_required_vars.append("STRIX_LLM") missing_required_vars.append("STRIX_LLM")
has_base_url = any( has_base_url = uses_strix_models or any(
[ [
Config.get("llm_api_base"), Config.get("llm_api_base"),
Config.get("openai_api_base"), Config.get("openai_api_base"),
@@ -76,7 +82,6 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
if missing_required_vars: if missing_required_vars:
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red") error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
@@ -135,7 +140,10 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
) )
error_text.append("\nExample setup:\n", style="white") error_text.append("\nExample setup:\n", style="white")
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white") if uses_strix_models:
error_text.append("export STRIX_LLM='strix/gpt-5'\n", style="dim white")
else:
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
if missing_optional_vars: if missing_optional_vars:
for var in missing_optional_vars: for var in missing_optional_vars:
@@ -163,8 +171,8 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX CONFIGURATION ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -179,7 +187,6 @@ def check_docker_installed() -> None:
if shutil.which("docker") is None: if shutil.which("docker") is None:
console = Console() console = Console()
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT INSTALLED", style="bold red") error_text.append("DOCKER NOT INSTALLED", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white") error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
@@ -189,8 +196,8 @@ def check_docker_installed() -> None:
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -202,14 +209,9 @@ async def warm_up_llm() -> None:
console = Console() console = Console()
try: try:
model_name = Config.get("strix_llm") model_name, api_key, api_base = resolve_llm_config()
api_key = Config.get("llm_api_key") litellm_model, _ = resolve_strix_model(model_name)
api_base = ( litellm_model = litellm_model or model_name
Config.get("llm_api_base")
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
)
test_messages = [ test_messages = [
{"role": "system", "content": "You are a helpful assistant."}, {"role": "system", "content": "You are a helpful assistant."},
@@ -219,7 +221,7 @@ async def warm_up_llm() -> None:
llm_timeout = int(Config.get("llm_timeout") or "300") llm_timeout = int(Config.get("llm_timeout") or "300")
completion_kwargs: dict[str, Any] = { completion_kwargs: dict[str, Any] = {
"model": model_name, "model": litellm_model,
"messages": test_messages, "messages": test_messages,
"timeout": llm_timeout, "timeout": llm_timeout,
} }
@@ -234,7 +236,6 @@ async def warm_up_llm() -> None:
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("LLM CONNECTION FAILED", style="bold red") error_text.append("LLM CONNECTION FAILED", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append("Could not establish connection to the language model.\n", style="white") error_text.append("Could not establish connection to the language model.\n", style="white")
@@ -243,8 +244,8 @@ async def warm_up_llm() -> None:
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -359,6 +360,12 @@ Examples:
), ),
) )
parser.add_argument(
"--config",
type=str,
help="Path to a custom config file (JSON) to use instead of ~/.strix/cli-config.json",
)
args = parser.parse_args() args = parser.parse_args()
if args.instruction and args.instruction_file: if args.instruction and args.instruction_file:
@@ -410,30 +417,22 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
completion_text = Text() completion_text = Text()
if scan_completed: if scan_completed:
completion_text.append("🦉 ", style="bold white") completion_text.append("Penetration test completed", style="bold #22c55e")
completion_text.append("AGENT FINISHED", style="bold green")
completion_text.append("", style="dim white")
completion_text.append("Penetration test completed", style="white")
else: else:
completion_text.append("🦉 ", style="bold white") completion_text.append("SESSION ENDED", style="bold #eab308")
completion_text.append("SESSION ENDED", style="bold yellow")
completion_text.append("", style="dim white")
completion_text.append("Penetration test interrupted by user", style="white")
stats_text = build_final_stats_text(tracer)
target_text = Text() target_text = Text()
target_text.append("Target", style="dim")
target_text.append(" ")
if len(args.targets_info) == 1: if len(args.targets_info) == 1:
target_text.append("🎯 Target: ", style="bold cyan")
target_text.append(args.targets_info[0]["original"], style="bold white") target_text.append(args.targets_info[0]["original"], style="bold white")
else: else:
target_text.append("🎯 Targets: ", style="bold cyan") target_text.append(f"{len(args.targets_info)} targets", style="bold white")
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white") for target_info in args.targets_info:
for i, target_info in enumerate(args.targets_info): target_text.append("\n ")
target_text.append("", style="dim white")
target_text.append(target_info["original"], style="white") target_text.append(target_info["original"], style="white")
if i < len(args.targets_info) - 1:
target_text.append("\n") stats_text = build_final_stats_text(tracer)
panel_parts = [completion_text, "\n\n", target_text] panel_parts = [completion_text, "\n\n", target_text]
@@ -442,18 +441,20 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
if scan_completed or has_vulnerabilities: if scan_completed or has_vulnerabilities:
results_text = Text() results_text = Text()
results_text.append("📊 Results Saved To: ", style="bold cyan") results_text.append("\n")
results_text.append(str(results_path), style="bold yellow") results_text.append("Output", style="dim")
panel_parts.extend(["\n\n", results_text]) results_text.append(" ")
results_text.append(str(results_path), style="#60a5fa")
panel_parts.extend(["\n", results_text])
panel_content = Text.assemble(*panel_parts) panel_content = Text.assemble(*panel_parts)
border_style = "green" if scan_completed else "yellow" border_style = "#22c55e" if scan_completed else "#eab308"
panel = Panel( panel = Panel(
panel_content, panel_content,
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style=border_style, border_style=border_style,
padding=(1, 2), padding=(1, 2),
) )
@@ -461,8 +462,7 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
console.print("\n") console.print("\n")
console.print(panel) console.print(panel)
console.print() console.print()
console.print("[dim]🌐 Website:[/] [cyan]https://strix.ai[/]") console.print("[#60a5fa]strix.ai[/] [dim]·[/] [#60a5fa]discord.gg/strix-ai[/]")
console.print("[dim]💬 Discord:[/] [cyan]https://discord.gg/YjKFvEZSdZ[/]")
console.print() console.print()
@@ -474,7 +474,7 @@ def pull_docker_image() -> None:
return return
console.print() console.print()
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {Config.get('strix_image')}") console.print(f"[dim]Pulling image[/] {Config.get('strix_image')}")
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]") console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
console.print() console.print()
@@ -489,7 +489,6 @@ def pull_docker_image() -> None:
except DockerException as e: except DockerException as e:
console.print() console.print()
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("FAILED TO PULL IMAGE", style="bold red") error_text.append("FAILED TO PULL IMAGE", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white") error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white")
@@ -497,8 +496,8 @@ def pull_docker_image() -> None:
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ DOCKER PULL ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -506,25 +505,37 @@ def pull_docker_image() -> None:
sys.exit(1) sys.exit(1)
success_text = Text() success_text = Text()
success_text.append("", style="bold green") success_text.append("Docker image ready", style="#22c55e")
success_text.append("Successfully pulled Docker image", style="green")
console.print(success_text) console.print(success_text)
console.print() console.print()
def apply_config_override(config_path: str) -> None:
Config._config_file_override = validate_config_file(config_path)
apply_saved_config(force=True)
def persist_config() -> None:
if Config._config_file_override is None:
save_current_config()
def main() -> None: def main() -> None:
if sys.platform == "win32": if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
args = parse_arguments() args = parse_arguments()
if args.config:
apply_config_override(args.config)
check_docker_installed() check_docker_installed()
pull_docker_image() pull_docker_image()
validate_environment() validate_environment()
asyncio.run(warm_up_llm()) asyncio.run(warm_up_llm())
save_current_config() persist_config()
args.run_name = generate_run_name(args.targets_info) args.run_name = generate_run_name(args.targets_info)

View File

@@ -3,8 +3,16 @@ import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal from typing import Literal
from strix.llm.utils import normalize_tool_format
_FUNCTION_TAG_PREFIX = "<function=" _FUNCTION_TAG_PREFIX = "<function="
_INVOKE_TAG_PREFIX = "<invoke "
_FUNC_PATTERN = re.compile(r"<function=([^>]+)>")
_FUNC_END_PATTERN = re.compile(r"</function>")
_COMPLETE_PARAM_PATTERN = re.compile(r"<parameter=([^>]+)>(.*?)</parameter>", re.DOTALL)
_INCOMPLETE_PARAM_PATTERN = re.compile(r"<parameter=([^>]+)>(.*)$", re.DOTALL)
def _get_safe_content(content: str) -> tuple[str, str]: def _get_safe_content(content: str) -> tuple[str, str]:
@@ -16,9 +24,8 @@ def _get_safe_content(content: str) -> tuple[str, str]:
return content, "" return content, ""
suffix = content[last_lt:] suffix = content[last_lt:]
target = _FUNCTION_TAG_PREFIX # "<function="
if target.startswith(suffix): if _FUNCTION_TAG_PREFIX.startswith(suffix) or _INVOKE_TAG_PREFIX.startswith(suffix):
return content[:last_lt], suffix return content[:last_lt], suffix
return content, "" return content, ""
@@ -37,10 +44,11 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
if not content: if not content:
return [] return []
content = normalize_tool_format(content)
segments: list[StreamSegment] = [] segments: list[StreamSegment] = []
func_pattern = r"<function=([^>]+)>" func_matches = list(_FUNC_PATTERN.finditer(content))
func_matches = list(re.finditer(func_pattern, content))
if not func_matches: if not func_matches:
safe_content, _ = _get_safe_content(content) safe_content, _ = _get_safe_content(content)
@@ -59,12 +67,12 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
tool_name = match.group(1) tool_name = match.group(1)
func_start = match.end() func_start = match.end()
func_end_match = re.search(r"</function>", content[func_start:]) func_end_match = _FUNC_END_PATTERN.search(content, func_start)
if func_end_match: if func_end_match:
func_body = content[func_start : func_start + func_end_match.start()] func_body = content[func_start : func_end_match.start()]
is_complete = True is_complete = True
end_pos = func_start + func_end_match.end() end_pos = func_end_match.end()
else: else:
if i + 1 < len(func_matches): if i + 1 < len(func_matches):
next_func_start = func_matches[i + 1].start() next_func_start = func_matches[i + 1].start()
@@ -98,8 +106,7 @@ def parse_streaming_content(content: str) -> list[StreamSegment]:
def _parse_streaming_params(func_body: str) -> dict[str, str]: def _parse_streaming_params(func_body: str) -> dict[str, str]:
args: dict[str, str] = {} args: dict[str, str] = {}
complete_pattern = r"<parameter=([^>]+)>(.*?)</parameter>" complete_matches = list(_COMPLETE_PARAM_PATTERN.finditer(func_body))
complete_matches = list(re.finditer(complete_pattern, func_body, re.DOTALL))
complete_end_pos = 0 complete_end_pos = 0
for match in complete_matches: for match in complete_matches:
@@ -109,8 +116,7 @@ def _parse_streaming_params(func_body: str) -> dict[str, str]:
complete_end_pos = max(complete_end_pos, match.end()) complete_end_pos = max(complete_end_pos, match.end())
remaining = func_body[complete_end_pos:] remaining = func_body[complete_end_pos:]
incomplete_pattern = r"<parameter=([^>]+)>(.*)$" incomplete_match = _INCOMPLETE_PARAM_PATTERN.search(remaining)
incomplete_match = re.search(incomplete_pattern, remaining, re.DOTALL)
if incomplete_match: if incomplete_match:
param_name = incomplete_match.group(1) param_name = incomplete_match.group(1)
param_value = html.unescape(incomplete_match.group(2).strip()) param_value = html.unescape(incomplete_match.group(2).strip())

View File

@@ -92,12 +92,13 @@ class AgentFinishRenderer(BaseToolRenderer):
success = args.get("success", True) success = args.get("success", True)
text = Text() text = Text()
text.append("🏁 ")
if success: if success:
text.append("Agent completed", style="bold #fbbf24") text.append("", style="#22c55e")
text.append("Agent completed", style="bold #22c55e")
else: else:
text.append("Agent failed", style="bold #fbbf24") text.append("", style="#ef4444")
text.append("Agent failed", style="bold #ef4444")
if result_summary: if result_summary:
text.append("\n ") text.append("\n ")

View File

@@ -64,7 +64,7 @@ class BrowserRenderer(BaseToolRenderer):
args = tool_data.get("args", {}) args = tool_data.get("args", {})
status = tool_data.get("status", "unknown") status = tool_data.get("status", "unknown")
action = args.get("action", "unknown") action = args.get("action", "")
content = cls._build_content(action, args) content = cls._build_content(action, args)
css_classes = cls.get_css_classes(status) css_classes = cls.get_css_classes(status)
@@ -131,5 +131,6 @@ class BrowserRenderer(BaseToolRenderer):
text.append_text(cls._highlight_js(js_code)) text.append_text(cls._highlight_js(js_code))
return text return text
text.append(action, style="#06b6d4") if action:
text.append(action, style="#06b6d4")
return text return text

View File

@@ -65,16 +65,16 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
text = Text() text = Text()
icons_and_labels = { icons_and_labels = {
"view": ("📖 ", "Reading file", "#10b981"), "view": (" ", "read", "#10b981"),
"str_replace": ("✏️ ", "Editing file", "#10b981"), "str_replace": (" ", "edit", "#10b981"),
"create": ("📝 ", "Creating file", "#10b981"), "create": (" ", "create", "#10b981"),
"insert": ("✏️ ", "Inserting text", "#10b981"), "insert": (" ", "insert", "#10b981"),
"undo_edit": ("↩️ ", "Undoing edit", "#10b981"), "undo_edit": (" ", "undo", "#10b981"),
} }
icon, label, color = icons_and_labels.get(command, ("📄 ", "File operation", "#10b981")) icon, label, color = icons_and_labels.get(command, (" ", "file", "#10b981"))
text.append(icon) text.append(icon, style=color)
text.append(label, style=f"bold {color}") text.append(label, style="dim")
if path: if path:
path_display = path[-60:] if len(path) > 60 else path path_display = path[-60:] if len(path) > 60 else path
@@ -132,8 +132,8 @@ class ListFilesRenderer(BaseToolRenderer):
path = args.get("path", "") path = args.get("path", "")
text = Text() text = Text()
text.append("📂 ") text.append("", style="#10b981")
text.append("Listing files", style="bold #10b981") text.append("list", style="dim")
text.append(" ") text.append(" ")
if path: if path:
@@ -158,23 +158,20 @@ class SearchFilesRenderer(BaseToolRenderer):
regex = args.get("regex", "") regex = args.get("regex", "")
text = Text() text = Text()
text.append("🔍 ") text.append("", style="#a855f7")
text.append("Searching files", style="bold purple") text.append("search", style="dim")
text.append(" ") text.append(" ")
if path and regex: if path and regex:
text.append(path, style="dim") text.append(path, style="dim")
text.append(" for '", style="dim") text.append(" ", style="dim")
text.append(regex, style="dim") text.append(regex, style="#a855f7")
text.append("'", style="dim")
elif path: elif path:
text.append(path, style="dim") text.append(path, style="dim")
elif regex: elif regex:
text.append("'", style="dim") text.append(regex, style="#a855f7")
text.append(regex, style="dim")
text.append("'", style="dim")
else: else:
text.append("Searching...", style="dim") text.append("...", style="dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes) return Static(text, classes=css_classes)

View File

@@ -1,6 +1,5 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.padding import Padding
from rich.text import Text from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
@@ -9,7 +8,6 @@ from .registry import register_tool_renderer
FIELD_STYLE = "bold #4ade80" FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414"
@register_tool_renderer @register_tool_renderer
@@ -27,8 +25,8 @@ class FinishScanRenderer(BaseToolRenderer):
recommendations = args.get("recommendations", "") recommendations = args.get("recommendations", "")
text = Text() text = Text()
text.append("🏁 ") text.append("", style="#22c55e")
text.append("Finishing Scan", style="bold #dc2626") text.append("Penetration test completed", style="bold #22c55e")
if executive_summary: if executive_summary:
text.append("\n\n") text.append("\n\n")
@@ -58,7 +56,10 @@ class FinishScanRenderer(BaseToolRenderer):
text.append("\n ") text.append("\n ")
text.append("Generating final report...", style="dim") text.append("Generating final report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}") padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes) return Static(padded, classes=css_classes)

View File

@@ -21,8 +21,8 @@ class CreateNoteRenderer(BaseToolRenderer):
category = args.get("category", "general") category = args.get("category", "general")
text = Text() text = Text()
text.append("📝 ") text.append("", style="#fbbf24")
text.append("Note", style="bold #fbbf24") text.append("note", style="dim")
text.append(" ") text.append(" ")
text.append(f"({category})", style="dim") text.append(f"({category})", style="dim")
@@ -50,8 +50,8 @@ class DeleteNoteRenderer(BaseToolRenderer):
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
text = Text() text = Text()
text.append("📝 ") text.append("", style="#fbbf24")
text.append("Note Removed", style="bold #94a3b8") text.append("note removed", style="dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -70,8 +70,8 @@ class UpdateNoteRenderer(BaseToolRenderer):
content = args.get("content") content = args.get("content")
text = Text() text = Text()
text.append("📝 ") text.append("", style="#fbbf24")
text.append("Note Updated", style="bold #fbbf24") text.append("note updated", style="dim")
if title: if title:
text.append("\n ") text.append("\n ")
@@ -99,8 +99,8 @@ class ListNotesRenderer(BaseToolRenderer):
result = tool_data.get("result") result = tool_data.get("result")
text = Text() text = Text()
text.append("📝 ") text.append("", style="#fbbf24")
text.append("Notes", style="bold #fbbf24") text.append("notes", style="dim")
if isinstance(result, str) and result.strip(): if isinstance(result, str) and result.strip():
text.append("\n ") text.append("\n ")

View File

@@ -7,53 +7,105 @@ from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer from .registry import register_tool_renderer
PROXY_ICON = "<~>"
MAX_REQUESTS_DISPLAY = 20
MAX_LINE_LENGTH = 200
def _truncate(text: str, max_len: int = 80) -> str:
return text[: max_len - 3] + "..." if len(text) > max_len else text
def _sanitize(text: str, max_len: int = 150) -> str:
"""Remove newlines and truncate text."""
clean = text.replace("\n", " ").replace("\r", "").replace("\t", " ")
return _truncate(clean, max_len)
def _status_style(code: int | None) -> str:
if code is None:
return "dim"
if 200 <= code < 300:
return "#22c55e" # green
if 300 <= code < 400:
return "#eab308" # yellow
if 400 <= code < 500:
return "#f97316" # orange
if code >= 500:
return "#ef4444" # red
return "dim"
@register_tool_renderer @register_tool_renderer
class ListRequestsRenderer(BaseToolRenderer): class ListRequestsRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_requests" tool_name: ClassVar[str] = "list_requests"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912 # noqa: PLR0912
args = tool_data.get("args", {}) args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
httpql_filter = args.get("httpql_filter") httpql_filter = args.get("httpql_filter")
sort_by = args.get("sort_by")
sort_order = args.get("sort_order")
scope_id = args.get("scope_id")
text = Text() text = Text()
text.append("📋 ") text.append(PROXY_ICON, style="dim")
text.append("Listing requests", style="bold #06b6d4") text.append(" listing requests", style="#06b6d4")
if isinstance(result, str) and result.strip(): if httpql_filter:
text.append("\n ") text.append(f" where {_truncate(httpql_filter, 150)}", style="dim italic")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "requests" in result: meta_parts = []
requests = result["requests"] if sort_by and sort_by != "timestamp":
if isinstance(requests, list) and requests: meta_parts.append(f"by:{sort_by}")
for req in requests[:25]: if sort_order and sort_order != "desc":
if isinstance(req, dict): meta_parts.append(sort_order)
method = req.get("method", "?") if scope_id and isinstance(scope_id, str):
path = req.get("path", "?") meta_parts.append(f"scope:{scope_id[:8]}")
response = req.get("response") or {} if meta_parts:
status = response.get("statusCode", "?") text.append(f" ({', '.join(meta_parts)})", style="dim")
text.append("\n ")
text.append(f"{method} {path}{status}", style="dim") if status == "completed" and isinstance(result, dict):
if len(requests) > 25: if "error" in result:
text.append("\n ") text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
text.append(f"... +{len(requests) - 25} more", style="dim")
else: else:
text.append("\n ") total = result.get("total_count", 0)
text.append("No requests found", style="dim") requests = result.get("requests", [])
elif httpql_filter:
filter_display = (
httpql_filter[:500] + "..." if len(httpql_filter) > 500 else httpql_filter
)
text.append("\n ")
text.append(filter_display, style="dim")
else:
text.append("\n ")
text.append("All requests", style="dim")
css_classes = cls.get_css_classes("completed") text.append(f" [{total} found]", style="dim")
if requests and isinstance(requests, list):
text.append("\n")
for i, req in enumerate(requests[:MAX_REQUESTS_DISPLAY]):
if not isinstance(req, dict):
continue
method = req.get("method", "?")
host = req.get("host", "")
path = req.get("path", "/")
resp = req.get("response") or {}
code = resp.get("statusCode") if isinstance(resp, dict) else None
text.append(" ")
text.append(f"{method:6}", style="#a78bfa")
text.append(f" {_truncate(host + path, 180)}", style="dim")
if code:
text.append(f" {code}", style=_status_style(code))
if i < min(len(requests), MAX_REQUESTS_DISPLAY) - 1:
text.append("\n")
if len(requests) > MAX_REQUESTS_DISPLAY:
text.append("\n")
text.append(
f" ... +{len(requests) - MAX_REQUESTS_DISPLAY} more",
style="dim italic",
)
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -63,46 +115,83 @@ class ViewRequestRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {}) args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
request_id = args.get("request_id", "")
part = args.get("part", "request") part = args.get("part", "request")
search_pattern = args.get("search_pattern")
text = Text() text = Text()
text.append("👀 ") text.append(PROXY_ICON, style="dim")
text.append(f"Viewing {part}", style="bold #06b6d4")
if isinstance(result, str) and result.strip(): action = "searching" if search_pattern else "viewing"
text.append("\n ") text.append(f" {action} {part}", style="#06b6d4")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict): if request_id:
if "content" in result: text.append(f" #{request_id}", style="dim")
content = result["content"]
content_preview = content[:2000] + "..." if len(content) > 2000 else content if search_pattern:
text.append("\n ") text.append(f" /{_truncate(search_pattern, 100)}/", style="dim italic")
text.append(content_preview, style="dim")
if status == "completed" and isinstance(result, dict):
if "error" in result:
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
elif "matches" in result: elif "matches" in result:
matches = result["matches"] matches = result.get("matches", [])
if isinstance(matches, list) and matches: total = result.get("total_matches", len(matches))
for match in matches[:25]: text.append(f" [{total} matches]", style="dim")
if isinstance(match, dict) and "match" in match:
text.append("\n ")
text.append(match["match"], style="dim")
if len(matches) > 25:
text.append("\n ")
text.append(f"... +{len(matches) - 25} more matches", style="dim")
else:
text.append("\n ")
text.append("No matches found", style="dim")
else:
text.append("\n ")
text.append("Viewing content...", style="dim")
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed") if matches and isinstance(matches, list):
text.append("\n")
for i, m in enumerate(matches[:5]):
if not isinstance(m, dict):
continue
before = m.get("before", "") or ""
match_text = m.get("match", "") or ""
after = m.get("after", "") or ""
before = before.replace("\n", " ").replace("\r", "")[-100:]
after = after.replace("\n", " ").replace("\r", "")[:100]
text.append(" ")
if before:
text.append(f"...{before}", style="dim")
text.append(match_text, style="#22c55e bold")
if after:
text.append(f"{after}...", style="dim")
if i < min(len(matches), 5) - 1:
text.append("\n")
if len(matches) > 5:
text.append("\n")
text.append(f" ... +{len(matches) - 5} more matches", style="dim italic")
elif "content" in result:
showing = result.get("showing_lines", "")
has_more = result.get("has_more", False)
content = result.get("content", "")
text.append(f" [{showing}]", style="dim")
if content and isinstance(content, str):
lines = content.split("\n")[:15]
text.append("\n")
for i, line in enumerate(lines):
text.append(" ")
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
if i < len(lines) - 1:
text.append("\n")
if has_more or len(lines) > 15:
text.append("\n")
text.append(" ... more content available", style="dim italic")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -112,45 +201,71 @@ class SendRequestRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {}) args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
method = args.get("method", "GET") method = args.get("method", "GET")
url = args.get("url", "") url = args.get("url", "")
req_headers = args.get("headers")
req_body = args.get("body", "")
text = Text() text = Text()
text.append("📤 ") text.append(PROXY_ICON, style="dim")
text.append(f"Sending {method}", style="bold #06b6d4") text.append(" sending request", style="#06b6d4")
if isinstance(result, str) and result.strip(): text.append("\n")
text.append("\n ") text.append(" >> ", style="#3b82f6")
text.append(result.strip(), style="dim") text.append(method, style="#a78bfa")
elif result and isinstance(result, dict): text.append(f" {_truncate(url, 180)}", style="dim")
status_code = result.get("status_code")
response_body = result.get("body", "")
if status_code: if req_headers and isinstance(req_headers, dict):
text.append("\n ") for k, v in list(req_headers.items())[:5]:
text.append(f"Status: {status_code}", style="dim") text.append("\n")
if response_body: text.append(" >> ", style="#3b82f6")
body_preview = ( text.append(f"{k}: ", style="dim")
response_body[:2000] + "..." if len(response_body) > 2000 else response_body text.append(_sanitize(str(v), 150), style="dim")
)
text.append("\n ") if req_body and isinstance(req_body, str):
text.append(body_preview, style="dim") text.append("\n")
text.append(" >> ", style="#3b82f6")
body_lines = req_body.split("\n")[:4]
for i, line in enumerate(body_lines):
if i > 0:
text.append("\n")
text.append(" ", style="dim")
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
if len(req_body.split("\n")) > 4:
text.append(" ...", style="dim italic")
if status == "completed" and isinstance(result, dict):
if "error" in result:
text.append(f"\n error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
else: else:
text.append("\n ") code = result.get("status_code")
text.append("Response received", style="dim") time_ms = result.get("response_time_ms")
elif url:
url_display = url[:500] + "..." if len(url) > 500 else url
text.append("\n ")
text.append(url_display, style="dim")
else:
text.append("\n ")
text.append("Sending...", style="dim")
css_classes = cls.get_css_classes("completed") text.append("\n")
text.append(" << ", style="#22c55e")
if code:
text.append(f"{code}", style=_status_style(code))
if time_ms:
text.append(f" ({time_ms}ms)", style="dim")
body = result.get("body", "")
if body and isinstance(body, str):
lines = body.split("\n")[:6]
for line in lines:
text.append("\n")
text.append(" << ", style="#22c55e")
text.append(_truncate(line, MAX_LINE_LENGTH - 5), style="dim")
if len(body.split("\n")) > 6:
text.append("\n")
text.append(" ...", style="dim italic")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -160,45 +275,99 @@ class RepeatRequestRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {}) args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
modifications = args.get("modifications", {}) request_id = args.get("request_id", "")
modifications = args.get("modifications")
text = Text() text = Text()
text.append("🔄 ") text.append(PROXY_ICON, style="dim")
text.append("Repeating request", style="bold #06b6d4") text.append(" repeating request", style="#06b6d4")
if isinstance(result, str) and result.strip(): if request_id:
text.append("\n ") text.append(f" #{request_id}", style="dim")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
status_code = result.get("status_code")
response_body = result.get("body", "")
if status_code: if modifications and isinstance(modifications, dict):
text.append("\n ") text.append("\n modifications:", style="dim italic")
text.append(f"Status: {status_code}", style="dim")
if response_body: if "url" in modifications:
body_preview = ( text.append("\n")
response_body[:2000] + "..." if len(response_body) > 2000 else response_body text.append(" >> ", style="#3b82f6")
) text.append(f"url: {_truncate(str(modifications['url']), 180)}", style="dim")
text.append("\n ")
text.append(body_preview, style="dim") if "headers" in modifications and isinstance(modifications["headers"], dict):
for k, v in list(modifications["headers"].items())[:5]:
text.append("\n")
text.append(" >> ", style="#3b82f6")
text.append(f"{k}: {_sanitize(str(v), 150)}", style="dim")
if "cookies" in modifications and isinstance(modifications["cookies"], dict):
for k, v in list(modifications["cookies"].items())[:5]:
text.append("\n")
text.append(" >> ", style="#3b82f6")
text.append(f"cookie {k}={_sanitize(str(v), 100)}", style="dim")
if "params" in modifications and isinstance(modifications["params"], dict):
for k, v in list(modifications["params"].items())[:5]:
text.append("\n")
text.append(" >> ", style="#3b82f6")
text.append(f"param {k}={_sanitize(str(v), 100)}", style="dim")
if "body" in modifications and isinstance(modifications["body"], str):
text.append("\n")
text.append(" >> ", style="#3b82f6")
body_lines = modifications["body"].split("\n")[:4]
for i, line in enumerate(body_lines):
if i > 0:
text.append("\n")
text.append(" ", style="dim")
text.append(_truncate(line, MAX_LINE_LENGTH), style="dim")
if len(modifications["body"].split("\n")) > 4:
text.append(" ...", style="dim italic")
elif modifications and isinstance(modifications, str):
text.append(f"\n {_truncate(modifications, 200)}", style="dim italic")
if status == "completed" and isinstance(result, dict):
if "error" in result:
text.append(f"\n error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
else: else:
text.append("\n ") req = result.get("request", {})
text.append("Response received", style="dim") method = req.get("method", "")
elif modifications: url = req.get("url", "")
mod_str = str(modifications) code = result.get("status_code")
mod_display = mod_str[:500] + "..." if len(mod_str) > 500 else mod_str time_ms = result.get("response_time_ms")
text.append("\n ")
text.append(mod_display, style="dim")
else:
text.append("\n ")
text.append("No modifications", style="dim")
css_classes = cls.get_css_classes("completed") text.append("\n")
text.append(" >> ", style="#3b82f6")
if method:
text.append(f"{method} ", style="#a78bfa")
if url:
text.append(_truncate(url, 180), style="dim")
text.append("\n")
text.append(" << ", style="#22c55e")
if code:
text.append(f"{code}", style=_status_style(code))
if time_ms:
text.append(f" ({time_ms}ms)", style="dim")
body = result.get("body", "")
if body and isinstance(body, str):
lines = body.split("\n")[:5]
for line in lines:
text.append("\n")
text.append(" << ", style="#22c55e")
text.append(_truncate(line, MAX_LINE_LENGTH - 5), style="dim")
if len(body.split("\n")) > 5:
text.append("\n")
text.append(" ...", style="dim italic")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -208,14 +377,87 @@ class ScopeRulesRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
text = Text() args = tool_data.get("args", {})
text.append("⚙️ ") result = tool_data.get("result")
text.append("Updating proxy scope", style="bold #06b6d4") status = tool_data.get("status", "running")
text.append("\n ")
text.append("Configuring...", style="dim")
css_classes = cls.get_css_classes("completed") action = args.get("action", "")
scope_name = args.get("scope_name", "")
scope_id = args.get("scope_id", "")
allowlist = args.get("allowlist")
denylist = args.get("denylist")
text = Text()
text.append(PROXY_ICON, style="dim")
action_map = {
"get": "getting",
"list": "listing",
"create": "creating",
"update": "updating",
"delete": "deleting",
}
action_text = action_map.get(action, action + "ing" if action else "managing")
text.append(f" {action_text} proxy scope", style="#06b6d4")
if scope_name:
text.append(f" '{_truncate(scope_name, 50)}'", style="dim italic")
if scope_id and isinstance(scope_id, str):
text.append(f" #{scope_id[:8]}", style="dim")
if allowlist and isinstance(allowlist, list):
allow_str = ", ".join(_truncate(str(a), 40) for a in allowlist[:4])
text.append(f"\n allow: {allow_str}", style="dim")
if len(allowlist) > 4:
text.append(f" +{len(allowlist) - 4}", style="dim italic")
if denylist and isinstance(denylist, list):
deny_str = ", ".join(_truncate(str(d), 40) for d in denylist[:4])
text.append(f"\n deny: {deny_str}", style="dim")
if len(denylist) > 4:
text.append(f" +{len(denylist) - 4}", style="dim italic")
if status == "completed" and isinstance(result, dict):
if "error" in result:
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
elif "scopes" in result:
scopes = result.get("scopes", [])
text.append(f" [{len(scopes)} scopes]", style="dim")
if scopes and isinstance(scopes, list):
text.append("\n")
for i, scope in enumerate(scopes[:5]):
if not isinstance(scope, dict):
continue
name = scope.get("name", "?")
allow = scope.get("allowlist") or []
text.append(" ")
text.append(_truncate(str(name), 40), style="#22c55e")
if allow and isinstance(allow, list):
allow_str = ", ".join(_truncate(str(a), 30) for a in allow[:3])
text.append(f" {allow_str}", style="dim")
if len(allow) > 3:
text.append(f" +{len(allow) - 3}", style="dim italic")
if i < min(len(scopes), 5) - 1:
text.append("\n")
elif "scope" in result:
scope = result.get("scope") or {}
if isinstance(scope, dict):
allow = scope.get("allowlist") or []
deny = scope.get("denylist") or []
if allow and isinstance(allow, list):
allow_str = ", ".join(_truncate(str(a), 40) for a in allow[:5])
text.append(f"\n allow: {allow_str}", style="dim")
if deny and isinstance(deny, list):
deny_str = ", ".join(_truncate(str(d), 40) for d in deny[:5])
text.append(f"\n deny: {deny_str}", style="dim")
elif "message" in result:
text.append(f" {result['message']}", style="#22c55e")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -225,36 +467,81 @@ class ListSitemapRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
parent_id = args.get("parent_id")
scope_id = args.get("scope_id")
depth = args.get("depth")
text = Text() text = Text()
text.append("🗺️ ") text.append(PROXY_ICON, style="dim")
text.append("Listing sitemap", style="bold #06b6d4") text.append(" listing sitemap", style="#06b6d4")
if isinstance(result, str) and result.strip(): if parent_id:
text.append("\n ") text.append(f" under #{_truncate(str(parent_id), 20)}", style="dim")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "entries" in result: meta_parts = []
entries = result["entries"] if scope_id and isinstance(scope_id, str):
if isinstance(entries, list) and entries: meta_parts.append(f"scope:{scope_id[:8]}")
for entry in entries[:30]: if depth and depth != "DIRECT":
if isinstance(entry, dict): meta_parts.append(depth.lower())
label = entry.get("label", "?") if meta_parts:
kind = entry.get("kind", "?") text.append(f" ({', '.join(meta_parts)})", style="dim")
text.append("\n ")
text.append(f"{kind}: {label}", style="dim") if status == "completed" and isinstance(result, dict):
if len(entries) > 30: if "error" in result:
text.append("\n ") text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
text.append(f"... +{len(entries) - 30} more entries", style="dim")
else: else:
text.append("\n ") total = result.get("total_count", 0)
text.append("No entries found", style="dim") entries = result.get("entries", [])
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed") text.append(f" [{total} entries]", style="dim")
if entries and isinstance(entries, list):
text.append("\n")
for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]):
if not isinstance(entry, dict):
continue
kind = entry.get("kind") or "?"
label = entry.get("label") or "?"
has_children = entry.get("hasDescendants", False)
req = entry.get("request") or {}
kind_style = {
"DOMAIN": "#f59e0b",
"DIRECTORY": "#3b82f6",
"REQUEST": "#22c55e",
}.get(kind, "dim")
text.append(" ")
kind_abbr = kind[:3] if isinstance(kind, str) else "?"
text.append(f"{kind_abbr:3}", style=kind_style)
text.append(f" {_truncate(label, 150)}", style="dim")
if req:
method = req.get("method", "")
code = req.get("status")
if method:
text.append(f" {method}", style="#a78bfa")
if code:
text.append(f" {code}", style=_status_style(code))
if has_children:
text.append(" +", style="dim italic")
if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1:
text.append("\n")
if len(entries) > MAX_REQUESTS_DISPLAY:
text.append("\n")
text.append(
f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", style="dim italic"
)
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)
@@ -264,33 +551,60 @@ class ViewSitemapEntryRenderer(BaseToolRenderer):
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912
args = tool_data.get("args", {})
result = tool_data.get("result") result = tool_data.get("result")
status = tool_data.get("status", "running")
entry_id = args.get("entry_id", "")
text = Text() text = Text()
text.append("📍 ") text.append(PROXY_ICON, style="dim")
text.append("Viewing sitemap entry", style="bold #06b6d4") text.append(" viewing sitemap", style="#06b6d4")
if isinstance(result, str) and result.strip(): if entry_id:
text.append("\n ") text.append(f" #{_truncate(str(entry_id), 20)}", style="dim")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "entry" in result: if status == "completed" and isinstance(result, dict):
entry = result["entry"] if "error" in result:
if isinstance(entry, dict): text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
label = entry.get("label", "") elif "entry" in result:
entry = result.get("entry") or {}
if not isinstance(entry, dict):
entry = {}
kind = entry.get("kind", "") kind = entry.get("kind", "")
if label and kind: label = entry.get("label", "")
text.append("\n ") related = entry.get("related_requests") or {}
text.append(f"{kind}: {label}", style="dim") related_reqs = related.get("requests", []) if isinstance(related, dict) else []
else: total_related = related.get("total_count", 0) if isinstance(related, dict) else 0
text.append("\n ")
text.append("Entry details loaded", style="dim")
else:
text.append("\n ")
text.append("Entry details loaded", style="dim")
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed") if kind and label:
text.append(f" {kind}: {_truncate(label, 120)}", style="dim")
if total_related:
text.append(f" [{total_related} requests]", style="dim")
if related_reqs and isinstance(related_reqs, list):
text.append("\n")
for i, req in enumerate(related_reqs[:10]):
if not isinstance(req, dict):
continue
method = req.get("method", "?")
path = req.get("path", "/")
code = req.get("status")
text.append(" ")
text.append(f"{method:6}", style="#a78bfa")
text.append(f" {_truncate(path, 180)}", style="dim")
if code:
text.append(f" {code}", style=_status_style(code))
if i < min(len(related_reqs), 10) - 1:
text.append("\n")
if len(related_reqs) > 10:
text.append("\n")
text.append(f" ... +{len(related_reqs) - 10} more", style="dim italic")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes) return Static(text, classes=css_classes)

View File

@@ -14,6 +14,8 @@ from .registry import register_tool_renderer
MAX_OUTPUT_LINES = 50 MAX_OUTPUT_LINES = 50
MAX_LINE_LENGTH = 200 MAX_LINE_LENGTH = 200
ANSI_PATTERN = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)")
STRIP_PATTERNS = [ STRIP_PATTERNS = [
r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]", r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]",
] ]
@@ -25,31 +27,32 @@ def _get_style_colors() -> dict[Any, str]:
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]} return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
@cache
def _get_lexer() -> PythonLexer:
return PythonLexer()
@cache
def _get_token_color(token_type: Any) -> str | None:
colors = _get_style_colors()
while token_type:
if token_type in colors:
return colors[token_type]
token_type = token_type.parent
return None
@register_tool_renderer @register_tool_renderer
class PythonRenderer(BaseToolRenderer): class PythonRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "python_action" tool_name: ClassVar[str] = "python_action"
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"] css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
@classmethod
def _get_token_color(cls, token_type: Any) -> str | None:
colors = _get_style_colors()
while token_type:
if token_type in colors:
return colors[token_type]
token_type = token_type.parent
return None
@classmethod @classmethod
def _highlight_python(cls, code: str) -> Text: def _highlight_python(cls, code: str) -> Text:
lexer = PythonLexer()
text = Text() text = Text()
for token_type, token_value in _get_lexer().get_tokens(code):
for token_type, token_value in lexer.get_tokens(code): if token_value:
if not token_value: text.append(token_value, style=_get_token_color(token_type))
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text return text
@classmethod @classmethod
@@ -59,11 +62,16 @@ class PythonRenderer(BaseToolRenderer):
cleaned = re.sub(pattern, "", cleaned) cleaned = re.sub(pattern, "", cleaned)
return cleaned.strip() return cleaned.strip()
@classmethod
def _strip_ansi(cls, text: str) -> str:
return ANSI_PATTERN.sub("", text)
@classmethod @classmethod
def _truncate_line(cls, line: str) -> str: def _truncate_line(cls, line: str) -> str:
if len(line) > MAX_LINE_LENGTH: clean_line = cls._strip_ansi(line)
return line[: MAX_LINE_LENGTH - 3] + "..." if len(clean_line) > MAX_LINE_LENGTH:
return line return clean_line[: MAX_LINE_LENGTH - 3] + "..."
return clean_line
@classmethod @classmethod
def _format_output(cls, output: str) -> Text: def _format_output(cls, output: str) -> Text:
@@ -112,22 +120,13 @@ class PythonRenderer(BaseToolRenderer):
return return
stdout = result.get("stdout", "") stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
stdout = cls._clean_output(stdout) if stdout else "" stdout = cls._clean_output(stdout) if stdout else ""
stderr = cls._clean_output(stderr) if stderr else ""
if stdout: if stdout:
text.append("\n") text.append("\n")
formatted_output = cls._format_output(stdout) formatted_output = cls._format_output(stdout)
text.append_text(formatted_output) text.append_text(formatted_output)
if stderr:
text.append("\n")
text.append(" stderr: ", style="bold #ef4444")
formatted_stderr = cls._format_output(stderr)
text.append_text(formatted_stderr)
@classmethod @classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {}) args = tool_data.get("args", {})

View File

@@ -3,10 +3,14 @@ from typing import Any, ClassVar
from pygments.lexers import PythonLexer from pygments.lexers import PythonLexer
from pygments.styles import get_style_by_name from pygments.styles import get_style_by_name
from rich.padding import Padding
from rich.text import Text from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
from strix.tools.reporting.reporting_actions import (
parse_code_locations_xml,
parse_cvss_xml,
)
from .base_renderer import BaseToolRenderer from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer from .registry import register_tool_renderer
@@ -18,7 +22,13 @@ def _get_style_colors() -> dict[Any, str]:
FIELD_STYLE = "bold #4ade80" FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414" DIM_STYLE = "dim"
FILE_STYLE = "bold #60a5fa"
LINE_STYLE = "#facc15"
LABEL_STYLE = "italic #a1a1aa"
CODE_STYLE = "#e2e8f0"
BEFORE_STYLE = "#ef4444"
AFTER_STYLE = "#22c55e"
@register_tool_renderer @register_tool_renderer
@@ -82,18 +92,13 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
poc_script_code = args.get("poc_script_code", "") poc_script_code = args.get("poc_script_code", "")
remediation_steps = args.get("remediation_steps", "") remediation_steps = args.get("remediation_steps", "")
attack_vector = args.get("attack_vector", "") cvss_breakdown_xml = args.get("cvss_breakdown", "")
attack_complexity = args.get("attack_complexity", "") code_locations_xml = args.get("code_locations", "")
privileges_required = args.get("privileges_required", "")
user_interaction = args.get("user_interaction", "")
scope = args.get("scope", "")
confidentiality = args.get("confidentiality", "")
integrity = args.get("integrity", "")
availability = args.get("availability", "")
endpoint = args.get("endpoint", "") endpoint = args.get("endpoint", "")
method = args.get("method", "") method = args.get("method", "")
cve = args.get("cve", "") cve = args.get("cve", "")
cwe = args.get("cwe", "")
severity = "" severity = ""
cvss_score = None cvss_score = None
@@ -142,38 +147,30 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("CVE: ", style=FIELD_STYLE) text.append("CVE: ", style=FIELD_STYLE)
text.append(cve) text.append(cve)
if any( if cwe:
[ text.append("\n\n")
attack_vector, text.append("CWE: ", style=FIELD_STYLE)
attack_complexity, text.append(cwe)
privileges_required,
user_interaction, parsed_cvss = parse_cvss_xml(cvss_breakdown_xml) if cvss_breakdown_xml else None
scope, if parsed_cvss:
confidentiality,
integrity,
availability,
]
):
text.append("\n\n") text.append("\n\n")
cvss_parts = [] cvss_parts = []
if attack_vector: for key, prefix in [
cvss_parts.append(f"AV:{attack_vector}") ("attack_vector", "AV"),
if attack_complexity: ("attack_complexity", "AC"),
cvss_parts.append(f"AC:{attack_complexity}") ("privileges_required", "PR"),
if privileges_required: ("user_interaction", "UI"),
cvss_parts.append(f"PR:{privileges_required}") ("scope", "S"),
if user_interaction: ("confidentiality", "C"),
cvss_parts.append(f"UI:{user_interaction}") ("integrity", "I"),
if scope: ("availability", "A"),
cvss_parts.append(f"S:{scope}") ]:
if confidentiality: val = parsed_cvss.get(key)
cvss_parts.append(f"C:{confidentiality}") if val:
if integrity: cvss_parts.append(f"{prefix}:{val}")
cvss_parts.append(f"I:{integrity}")
if availability:
cvss_parts.append(f"A:{availability}")
text.append("CVSS Vector: ", style=FIELD_STYLE) text.append("CVSS Vector: ", style=FIELD_STYLE)
text.append("/".join(cvss_parts), style="dim") text.append("/".join(cvss_parts), style=DIM_STYLE)
if description: if description:
text.append("\n\n") text.append("\n\n")
@@ -193,6 +190,40 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("\n") text.append("\n")
text.append(technical_analysis) text.append(technical_analysis)
parsed_locations = (
parse_code_locations_xml(code_locations_xml) if code_locations_xml else None
)
if parsed_locations:
text.append("\n\n")
text.append("Code Locations", style=FIELD_STYLE)
for i, loc in enumerate(parsed_locations):
text.append("\n\n")
text.append(f" Location {i + 1}: ", style=DIM_STYLE)
text.append(loc.get("file", "unknown"), style=FILE_STYLE)
start = loc.get("start_line")
end = loc.get("end_line")
if start is not None:
if end and end != start:
text.append(f":{start}-{end}", style=LINE_STYLE)
else:
text.append(f":{start}", style=LINE_STYLE)
if loc.get("label"):
text.append(f"\n {loc['label']}", style=LABEL_STYLE)
if loc.get("snippet"):
text.append("\n ")
text.append(loc["snippet"], style=CODE_STYLE)
if loc.get("fix_before") or loc.get("fix_after"):
text.append("\n ")
text.append("Fix:", style=DIM_STYLE)
if loc.get("fix_before"):
text.append("\n ")
text.append("- ", style=BEFORE_STYLE)
text.append(loc["fix_before"], style=BEFORE_STYLE)
if loc.get("fix_after"):
text.append("\n ")
text.append("+ ", style=AFTER_STYLE)
text.append(loc["fix_after"], style=AFTER_STYLE)
if poc_description: if poc_description:
text.append("\n\n") text.append("\n\n")
text.append("PoC Description", style=FIELD_STYLE) text.append("PoC Description", style=FIELD_STYLE)
@@ -215,7 +246,10 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
text.append("\n ") text.append("\n ")
text.append("Creating report...", style="dim") text.append("Creating report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}") padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes) return Static(padded, classes=css_classes)

View File

@@ -19,7 +19,8 @@ class ScanStartInfoRenderer(BaseToolRenderer):
targets = args.get("targets", []) targets = args.get("targets", [])
text = Text() text = Text()
text.append("🚀 Starting penetration test") text.append("", style="#22c55e")
text.append("Starting penetration test")
if len(targets) == 1: if len(targets) == 1:
text.append(" on ") text.append(" on ")

View File

@@ -29,11 +29,18 @@ from textual.widgets import Button, Label, Static, TextArea, Tree
from textual.widgets.tree import TreeNode from textual.widgets.tree import TreeNode
from strix.agents.StrixAgent import StrixAgent from strix.agents.StrixAgent import StrixAgent
from strix.interface.streaming_parser import parse_streaming_content
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
from strix.interface.tool_components.registry import get_tool_renderer
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
from strix.interface.utils import build_tui_stats_text from strix.interface.utils import build_tui_stats_text
from strix.llm.config import LLMConfig from strix.llm.config import LLMConfig
from strix.telemetry.tracer import Tracer, set_global_tracer from strix.telemetry.tracer import Tracer, set_global_tracer
logger = logging.getLogger(__name__)
def get_package_version() -> str: def get_package_version() -> str:
try: try:
return pkg_version("strix-agent") return pkg_version("strix-agent")
@@ -87,6 +94,7 @@ class ChatTextArea(TextArea): # type: ignore[misc]
class SplashScreen(Static): # type: ignore[misc] class SplashScreen(Static): # type: ignore[misc]
ALLOW_SELECT = False
PRIMARY_GREEN = "#22c55e" PRIMARY_GREEN = "#22c55e"
BANNER = ( BANNER = (
" ███████╗████████╗██████╗ ██╗██╗ ██╗\n" " ███████╗████████╗██████╗ ██╗██╗ ██╗\n"
@@ -188,7 +196,7 @@ class SplashScreen(Static): # type: ignore[misc]
class HelpScreen(ModalScreen): # type: ignore[misc] class HelpScreen(ModalScreen): # type: ignore[misc]
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Grid( yield Grid(
Label("🦉 Strix Help", id="help_title"), Label("Strix Help", id="help_title"),
Label( Label(
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n" "F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n"
"Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree", "Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree",
@@ -523,16 +531,30 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
lines.append("```") lines.append("```")
# Code Analysis # Code Analysis
if vuln.get("code_file") or vuln.get("code_diff"): if vuln.get("code_locations"):
lines.extend(["", "## Code Analysis", ""]) lines.extend(["", "## Code Analysis", ""])
if vuln.get("code_file"): for i, loc in enumerate(vuln["code_locations"]):
lines.append(f"**File:** {vuln['code_file']}") file_ref = loc.get("file", "unknown")
line_ref = ""
if loc.get("start_line") is not None:
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
else:
line_ref = f" (line {loc['start_line']})"
lines.append(f"**Location {i + 1}:** `{file_ref}`{line_ref}")
if loc.get("label"):
lines.append(f" {loc['label']}")
if loc.get("snippet"):
lines.append(f"```\n{loc['snippet']}\n```")
if loc.get("fix_before") or loc.get("fix_after"):
lines.append("**Suggested Fix:**")
lines.append("```diff")
if loc.get("fix_before"):
lines.extend(f"- {line}" for line in loc["fix_before"].splitlines())
if loc.get("fix_after"):
lines.extend(f"+ {line}" for line in loc["fix_after"].splitlines())
lines.append("```")
lines.append("") lines.append("")
if vuln.get("code_diff"):
lines.append("**Changes:**")
lines.append("```diff")
lines.append(vuln["code_diff"])
lines.append("```")
# Remediation # Remediation
if vuln.get("remediation_steps"): if vuln.get("remediation_steps"):
@@ -663,8 +685,9 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
class StrixTUIApp(App): # type: ignore[misc] class StrixTUIApp(App): # type: ignore[misc]
CSS_PATH = "assets/tui_styles.tcss" CSS_PATH = "assets/tui_styles.tcss"
ALLOW_SELECT = True
SIDEBAR_MIN_WIDTH = 100 SIDEBAR_MIN_WIDTH = 140
selected_agent_id: reactive[str | None] = reactive(default=None) selected_agent_id: reactive[str | None] = reactive(default=None)
show_splash: reactive[bool] = reactive(default=True) show_splash: reactive[bool] = reactive(default=True)
@@ -691,6 +714,9 @@ class StrixTUIApp(App): # type: ignore[misc]
self._displayed_agents: set[str] = set() self._displayed_agents: set[str] = set()
self._displayed_events: list[str] = [] self._displayed_events: list[str] = []
self._streaming_render_cache: dict[str, tuple[int, Any]] = {}
self._last_streaming_len: dict[str, int] = {}
self._scan_thread: threading.Thread | None = None self._scan_thread: threading.Thread | None = None
self._scan_stop_event = threading.Event() self._scan_stop_event = threading.Event()
self._scan_completed = threading.Event() self._scan_completed = threading.Event()
@@ -735,7 +761,10 @@ class StrixTUIApp(App): # type: ignore[misc]
def _setup_cleanup_handlers(self) -> None: def _setup_cleanup_handlers(self) -> None:
def cleanup_on_exit() -> None: def cleanup_on_exit() -> None:
from strix.runtime import cleanup_runtime
self.tracer.cleanup() self.tracer.cleanup()
cleanup_runtime()
def signal_handler(_signum: int, _frame: Any) -> None: def signal_handler(_signum: int, _frame: Any) -> None:
self.tracer.cleanup() self.tracer.cleanup()
@@ -773,13 +802,16 @@ class StrixTUIApp(App): # type: ignore[misc]
chat_history.can_focus = True chat_history.can_focus = True
status_text = Static("", id="status_text") status_text = Static("", id="status_text")
status_text.ALLOW_SELECT = False
keymap_indicator = Static("", id="keymap_indicator") keymap_indicator = Static("", id="keymap_indicator")
keymap_indicator.ALLOW_SELECT = False
agent_status_display = Horizontal( agent_status_display = Horizontal(
status_text, keymap_indicator, id="agent_status_display", classes="hidden" status_text, keymap_indicator, id="agent_status_display", classes="hidden"
) )
chat_prompt = Static("> ", id="chat_prompt") chat_prompt = Static("> ", id="chat_prompt")
chat_prompt.ALLOW_SELECT = False
chat_input = ChatTextArea( chat_input = ChatTextArea(
"", "",
id="chat_input", id="chat_input",
@@ -788,7 +820,7 @@ class StrixTUIApp(App): # type: ignore[misc]
chat_input.set_app_reference(self) chat_input.set_app_reference(self)
chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container") chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container")
agents_tree = Tree("🤖 Active Agents", id="agents_tree") agents_tree = Tree("Agents", id="agents_tree")
agents_tree.root.expand() agents_tree.root.expand()
agents_tree.show_root = False agents_tree.show_root = False
@@ -797,6 +829,7 @@ class StrixTUIApp(App): # type: ignore[misc]
agents_tree.guide_style = "dashed" agents_tree.guide_style = "dashed"
stats_display = Static("", id="stats_display") stats_display = Static("", id="stats_display")
stats_display.ALLOW_SELECT = False
vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel") vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel")
@@ -853,7 +886,7 @@ class StrixTUIApp(App): # type: ignore[misc]
self._start_scan_thread() self._start_scan_thread()
self.set_interval(0.25, self._update_ui_from_tracer) self.set_interval(0.35, self._update_ui_from_tracer)
def _update_ui_from_tracer(self) -> None: def _update_ui_from_tracer(self) -> None:
if self.show_splash: if self.show_splash:
@@ -904,16 +937,16 @@ class StrixTUIApp(App): # type: ignore[misc]
status = agent_data.get("status", "running") status = agent_data.get("status", "running")
status_indicators = { status_indicators = {
"running": "🟢", "running": "",
"waiting": "", "waiting": "",
"completed": "", "completed": "🟢",
"failed": "", "failed": "🔴",
"stopped": "⏹️", "stopped": "",
"stopping": "⏸️", "stopping": "",
"llm_failed": "🔴", "llm_failed": "🔴",
} }
status_icon = status_indicators.get(status, "🔵") status_icon = status_indicators.get(status, "")
vuln_count = self._agent_vulnerability_count(agent_id) vuln_count = self._agent_vulnerability_count(agent_id)
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
@@ -946,11 +979,17 @@ class StrixTUIApp(App): # type: ignore[misc]
) )
current_event_ids = [e["id"] for e in events] current_event_ids = [e["id"] for e in events]
current_streaming_len = len(streaming) if streaming else 0
last_streaming_len = self._last_streaming_len.get(self.selected_agent_id, 0)
if not streaming and current_event_ids == self._displayed_events: if (
current_event_ids == self._displayed_events
and current_streaming_len == last_streaming_len
):
return None, None return None, None
self._displayed_events = current_event_ids self._displayed_events = current_event_ids
self._last_streaming_len[self.selected_agent_id] = current_streaming_len
return self._get_rendered_events_content(events), "chat-content" return self._get_rendered_events_content(events), "chat-content"
def _update_chat_view(self) -> None: def _update_chat_view(self) -> None:
@@ -989,6 +1028,33 @@ class StrixTUIApp(App): # type: ignore[misc]
text.append(message) text.append(message)
return text, f"chat-placeholder {placeholder_class}" return text, f"chat-placeholder {placeholder_class}"
@staticmethod
def _merge_renderables(renderables: list[Any]) -> Text:
"""Merge renderables into a single Text for mouse text selection support."""
combined = Text()
for i, item in enumerate(renderables):
if i > 0:
combined.append("\n")
StrixTUIApp._append_renderable(combined, item)
return combined
@staticmethod
def _append_renderable(combined: Text, item: Any) -> None:
"""Recursively append a renderable's text content to a combined Text."""
if isinstance(item, Text):
combined.append_text(item)
elif isinstance(item, Group):
for j, sub in enumerate(item.renderables):
if j > 0:
combined.append("\n")
StrixTUIApp._append_renderable(combined, sub)
else:
inner = getattr(item, "renderable", None)
if inner is not None:
StrixTUIApp._append_renderable(combined, inner)
else:
combined.append(str(item))
def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any: def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
renderables: list[Any] = [] renderables: list[Any] = []
@@ -1020,23 +1086,25 @@ class StrixTUIApp(App): # type: ignore[misc]
if not renderables: if not renderables:
return Text() return Text()
if len(renderables) == 1: if len(renderables) == 1 and isinstance(renderables[0], Text):
return renderables[0] return renderables[0]
return Group(*renderables) return self._merge_renderables(renderables)
def _render_streaming_content(self, content: str) -> Any: def _render_streaming_content(self, content: str, agent_id: str | None = None) -> Any:
from strix.interface.streaming_parser import parse_streaming_content cache_key = agent_id or self.selected_agent_id or ""
content_len = len(content)
if cache_key in self._streaming_render_cache:
cached_len, cached_output = self._streaming_render_cache[cache_key]
if cached_len == content_len:
return cached_output
renderables: list[Any] = [] renderables: list[Any] = []
segments = parse_streaming_content(content) segments = parse_streaming_content(content)
for segment in segments: for segment in segments:
if segment.type == "text": if segment.type == "text":
from strix.interface.tool_components.agent_message_renderer import (
AgentMessageRenderer,
)
text_content = AgentMessageRenderer.render_simple(segment.content) text_content = AgentMessageRenderer.render_simple(segment.content)
if renderables: if renderables:
renderables.append(Text("")) renderables.append(Text(""))
@@ -1053,18 +1121,18 @@ class StrixTUIApp(App): # type: ignore[misc]
renderables.append(tool_renderable) renderables.append(tool_renderable)
if not renderables: if not renderables:
return Text() result = Text()
elif len(renderables) == 1 and isinstance(renderables[0], Text):
result = renderables[0]
else:
result = self._merge_renderables(renderables)
if len(renderables) == 1: self._streaming_render_cache[cache_key] = (content_len, result)
return renderables[0] return result
return Group(*renderables)
def _render_streaming_tool( def _render_streaming_tool(
self, tool_name: str, args: dict[str, str], is_complete: bool self, tool_name: str, args: dict[str, str], is_complete: bool
) -> Any: ) -> Any:
from strix.interface.tool_components.registry import get_tool_renderer
tool_data = { tool_data = {
"tool_name": tool_name, "tool_name": tool_name,
"args": args, "args": args,
@@ -1210,6 +1278,9 @@ class StrixTUIApp(App): # type: ignore[misc]
if stats_text: if stats_text:
stats_content.append(stats_text) stats_content.append(stats_text)
version = get_package_version()
stats_content.append(f"\nv{version}", style="white")
from rich.panel import Panel from rich.panel import Panel
stats_panel = Panel( stats_panel = Panel(
@@ -1395,6 +1466,8 @@ class StrixTUIApp(App): # type: ignore[misc]
return return
self._displayed_events.clear() self._displayed_events.clear()
self._streaming_render_cache.clear()
self._last_streaming_len.clear()
self.call_later(self._update_chat_view) self.call_later(self._update_chat_view)
self._update_agent_status_display() self._update_agent_status_display()
@@ -1449,15 +1522,16 @@ class StrixTUIApp(App): # type: ignore[misc]
agent_name_raw = agent_data.get("name", "Agent") agent_name_raw = agent_data.get("name", "Agent")
status_indicators = { status_indicators = {
"running": "🟢", "running": "",
"waiting": "🟡", "waiting": "",
"completed": "", "completed": "🟢",
"failed": "", "failed": "🔴",
"stopped": "⏹️", "stopped": "",
"stopping": "⏸️", "stopping": "",
"llm_failed": "🔴",
} }
status_icon = status_indicators.get(status, "🔵") status_icon = status_indicators.get(status, "")
vuln_count = self._agent_vulnerability_count(agent_id) vuln_count = self._agent_vulnerability_count(agent_id)
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
@@ -1523,15 +1597,16 @@ class StrixTUIApp(App): # type: ignore[misc]
status = agent_data.get("status", "running") status = agent_data.get("status", "running")
status_indicators = { status_indicators = {
"running": "🟢", "running": "",
"waiting": "🟡", "waiting": "",
"completed": "", "completed": "🟢",
"failed": "", "failed": "🔴",
"stopped": "⏹️", "stopped": "",
"stopping": "⏸️", "stopping": "",
"llm_failed": "🔴",
} }
status_icon = status_indicators.get(status, "🔵") status_icon = status_indicators.get(status, "")
vuln_count = self._agent_vulnerability_count(agent_id) vuln_count = self._agent_vulnerability_count(agent_id)
vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else ""
agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}"
@@ -1589,8 +1664,6 @@ class StrixTUIApp(App): # type: ignore[misc]
return None return None
if role == "user": if role == "user":
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
return UserMessageRenderer.render_simple(content) return UserMessageRenderer.render_simple(content)
if metadata.get("interrupted"): if metadata.get("interrupted"):
@@ -1599,9 +1672,7 @@ class StrixTUIApp(App): # type: ignore[misc]
interrupted_text.append("\n") interrupted_text.append("\n")
interrupted_text.append("", style="yellow") interrupted_text.append("", style="yellow")
interrupted_text.append("Interrupted by user", style="yellow dim") interrupted_text.append("Interrupted by user", style="yellow dim")
return Group(streaming_result, interrupted_text) return self._merge_renderables([streaming_result, interrupted_text])
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
return AgentMessageRenderer.render_simple(content) return AgentMessageRenderer.render_simple(content)
@@ -1611,8 +1682,6 @@ class StrixTUIApp(App): # type: ignore[misc]
status = tool_data.get("status", "unknown") status = tool_data.get("status", "unknown")
result = tool_data.get("result") result = tool_data.get("result")
from strix.interface.tool_components.registry import get_tool_renderer
renderer = get_tool_renderer(tool_name) renderer = get_tool_renderer(tool_name)
if renderer: if renderer:
@@ -1912,6 +1981,92 @@ class StrixTUIApp(App): # type: ignore[misc]
sidebar.remove_class("-hidden") sidebar.remove_class("-hidden")
chat_area.remove_class("-full-width") chat_area.remove_class("-full-width")
def on_mouse_up(self, _event: events.MouseUp) -> None:
self.set_timer(0.05, self._auto_copy_selection)
_ICON_PREFIXES: ClassVar[tuple[str, ...]] = (
"🐞 ",
"🌐 ",
"📋 ",
"🧠 ",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
">_ ",
"</> ",
"<~> ",
"[ ] ",
"[~] ",
"[•] ",
)
_DECORATIVE_LINES: ClassVar[frozenset[str]] = frozenset(
{
"● In progress...",
"✓ Done",
"✗ Failed",
"✗ Error",
"○ Unknown",
}
)
@staticmethod
def _clean_copied_text(text: str) -> str:
lines = text.split("\n")
cleaned: list[str] = []
for line in lines:
stripped = line.lstrip()
if stripped in StrixTUIApp._DECORATIVE_LINES:
continue
if stripped and all(c == "" for c in stripped):
continue
out = line
for prefix in StrixTUIApp._ICON_PREFIXES:
if stripped.startswith(prefix):
leading = line[: len(line) - len(line.lstrip())]
out = leading + stripped[len(prefix) :]
break
cleaned.append(out)
return "\n".join(cleaned)
def _auto_copy_selection(self) -> None:
copied = False
try:
if self.screen.selections:
selected = self.screen.get_selected_text()
self.screen.clear_selection()
if selected and selected.strip():
cleaned = self._clean_copied_text(selected)
self.copy_to_clipboard(cleaned if cleaned.strip() else selected)
copied = True
except Exception: # noqa: BLE001
logger.debug("Failed to copy screen selection", exc_info=True)
if not copied:
try:
chat_input = self.query_one("#chat_input", ChatTextArea)
selected = chat_input.selected_text
if selected and selected.strip():
self.copy_to_clipboard(selected)
chat_input.move_cursor(chat_input.cursor_location)
copied = True
except Exception: # noqa: BLE001
logger.debug("Failed to copy chat input selection", exc_info=True)
if copied:
self.notify("Copied to clipboard", timeout=2)
async def run_tui(args: argparse.Namespace) -> None: async def run_tui(args: argparse.Namespace) -> None:
"""Run strix in interactive TUI mode with textual.""" """Run strix in interactive TUI mode with textual."""

View File

@@ -1,4 +1,5 @@
import ipaddress import ipaddress
import json
import re import re
import secrets import secrets
import shutil import shutil
@@ -7,7 +8,9 @@ import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.request import Request, urlopen
import docker import docker
from docker.errors import DockerException, ImageNotFound from docker.errors import DockerException, ImageNotFound
@@ -160,32 +163,34 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
text.append("\n") text.append("\n")
text.append(poc_script_code, style="dim") text.append(poc_script_code, style="dim")
code_file = report.get("code_file") code_locations = report.get("code_locations")
if code_file: if code_locations:
text.append("\n\n") text.append("\n\n")
text.append("Code File: ", style=field_style) text.append("Code Locations", style=field_style)
text.append(code_file) for i, loc in enumerate(code_locations):
text.append("\n\n")
code_before = report.get("code_before") text.append(f" Location {i + 1}: ", style="dim")
if code_before: text.append(loc.get("file", "unknown"), style="bold")
text.append("\n\n") start = loc.get("start_line")
text.append("Code Before", style=field_style) end = loc.get("end_line")
text.append("\n") if start is not None:
text.append(code_before, style="dim") if end and end != start:
text.append(f":{start}-{end}")
code_after = report.get("code_after") else:
if code_after: text.append(f":{start}")
text.append("\n\n") if loc.get("label"):
text.append("Code After", style=field_style) text.append(f"\n {loc['label']}", style="italic dim")
text.append("\n") if loc.get("snippet"):
text.append(code_after, style="dim") text.append("\n ")
text.append(loc["snippet"], style="dim")
code_diff = report.get("code_diff") if loc.get("fix_before") or loc.get("fix_after"):
if code_diff: text.append("\n Fix:")
text.append("\n\n") if loc.get("fix_before"):
text.append("Code Diff", style=field_style) text.append("\n - ", style="dim")
text.append("\n") text.append(loc["fix_before"], style="dim")
text.append(code_diff, style="dim") if loc.get("fix_after"):
text.append("\n + ", style="dim")
text.append(loc["fix_after"], style="dim")
remediation_steps = report.get("remediation_steps") remediation_steps = report.get("remediation_steps")
if remediation_steps: if remediation_steps:
@@ -208,7 +213,7 @@ def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
if severity in severity_counts: if severity in severity_counts:
severity_counts[severity] += 1 severity_counts[severity] += 1
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red") stats_text.append("Vulnerabilities ", style="bold red")
severity_parts = [] severity_parts = []
for severity in ["critical", "high", "medium", "low", "info"]: for severity in ["critical", "high", "medium", "low", "info"]:
@@ -230,7 +235,7 @@ def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
stats_text.append(")", style="dim white") stats_text.append(")", style="dim white")
stats_text.append("\n") stats_text.append("\n")
else: else:
stats_text.append("🔍 Vulnerabilities Found: ", style="bold green") stats_text.append("Vulnerabilities ", style="bold #22c55e")
stats_text.append("0", style="bold white") stats_text.append("0", style="bold white")
stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green") stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
stats_text.append("\n") stats_text.append("\n")
@@ -240,29 +245,29 @@ def _build_llm_stats(stats_text: Text, total_stats: dict[str, Any]) -> None:
"""Build LLM usage section of stats text.""" """Build LLM usage section of stats text."""
if total_stats["requests"] > 0: if total_stats["requests"] > 0:
stats_text.append("\n") stats_text.append("\n")
stats_text.append("📥 Input Tokens: ", style="bold cyan") stats_text.append("Input Tokens ", style="dim")
stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white") stats_text.append(format_token_count(total_stats["input_tokens"]), style="white")
if total_stats["cached_tokens"] > 0: if total_stats["cached_tokens"] > 0:
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("Cached Tokens: ", style="bold green") stats_text.append("Cached Tokens ", style="dim")
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="bold white") stats_text.append(format_token_count(total_stats["cached_tokens"]), style="white")
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("📤 Output Tokens: ", style="bold cyan") stats_text.append("Output Tokens ", style="dim")
stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white") stats_text.append(format_token_count(total_stats["output_tokens"]), style="white")
if total_stats["cost"] > 0: if total_stats["cost"] > 0:
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("💰 Total Cost: ", style="bold cyan") stats_text.append("Cost ", style="dim")
stats_text.append(f"${total_stats['cost']:.4f}", style="bold yellow") stats_text.append(f"${total_stats['cost']:.4f}", style="bold #fbbf24")
else: else:
stats_text.append("\n") stats_text.append("\n")
stats_text.append("💰 Total Cost: ", style="bold cyan") stats_text.append("Cost ", style="dim")
stats_text.append("$0.0000 ", style="bold yellow") stats_text.append("$0.0000 ", style="#fbbf24")
stats_text.append(" ", style="bold white") stats_text.append("· ", style="dim white")
stats_text.append("📊 Tokens: ", style="bold cyan") stats_text.append("Tokens ", style="dim")
stats_text.append("0", style="bold white") stats_text.append("0", style="white")
def build_final_stats_text(tracer: Any) -> Text: def build_final_stats_text(tracer: Any) -> Text:
@@ -276,10 +281,12 @@ def build_final_stats_text(tracer: Any) -> Text:
tool_count = tracer.get_real_tool_count() tool_count = tracer.get_real_tool_count()
agent_count = len(tracer.agents) agent_count = len(tracer.agents)
stats_text.append("🤖 Agents Used: ", style="bold cyan") stats_text.append("Agents", style="dim")
stats_text.append(" ")
stats_text.append(str(agent_count), style="bold white") stats_text.append(str(agent_count), style="bold white")
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("🛠️ Tools Called: ", style="bold cyan") stats_text.append("Tools", style="dim")
stats_text.append(" ")
stats_text.append(str(tool_count), style="bold white") stats_text.append(str(tool_count), style="bold white")
llm_stats = tracer.get_total_llm_stats() llm_stats = tracer.get_total_llm_stats()
@@ -296,15 +303,16 @@ def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = Non
if agent_config: if agent_config:
llm_config = agent_config["llm_config"] llm_config = agent_config["llm_config"]
model = getattr(llm_config, "model_name", "Unknown") model = getattr(llm_config, "model_name", "Unknown")
stats_text.append(f"🧠 Model: {model}") stats_text.append("Model ", style="dim")
stats_text.append(model, style="white")
stats_text.append("\n") stats_text.append("\n")
vuln_count = len(tracer.vulnerability_reports) vuln_count = len(tracer.vulnerability_reports)
tool_count = tracer.get_real_tool_count() tool_count = tracer.get_real_tool_count()
agent_count = len(tracer.agents) agent_count = len(tracer.agents)
stats_text.append("🔍 Vulnerabilities: ", style="bold white") stats_text.append("Vulnerabilities ", style="dim")
stats_text.append(f"{vuln_count}", style="dim white") stats_text.append(f"{vuln_count}", style="white")
stats_text.append("\n") stats_text.append("\n")
if vuln_count > 0: if vuln_count > 0:
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
@@ -330,33 +338,32 @@ def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = Non
stats_text.append("\n") stats_text.append("\n")
stats_text.append("🤖 Agents: ", style="bold white") stats_text.append("Agents ", style="dim")
stats_text.append(str(agent_count), style="dim white") stats_text.append(str(agent_count), style="white")
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("🛠️ Tools: ", style="bold white") stats_text.append("Tools ", style="dim")
stats_text.append(str(tool_count), style="dim white") stats_text.append(str(tool_count), style="white")
llm_stats = tracer.get_total_llm_stats() llm_stats = tracer.get_total_llm_stats()
total_stats = llm_stats["total"] total_stats = llm_stats["total"]
stats_text.append("\n") stats_text.append("\n")
stats_text.append("📥 Input: ", style="bold white") stats_text.append("Input Tokens ", style="dim")
stats_text.append(format_token_count(total_stats["input_tokens"]), style="dim white") stats_text.append(format_token_count(total_stats["input_tokens"]), style="white")
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append(" ", style="bold white") stats_text.append("Cached Tokens ", style="dim")
stats_text.append("Cached: ", style="bold white") stats_text.append(format_token_count(total_stats["cached_tokens"]), style="white")
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="dim white")
stats_text.append("\n") stats_text.append("\n")
stats_text.append("📤 Output: ", style="bold white") stats_text.append("Output Tokens ", style="dim")
stats_text.append(format_token_count(total_stats["output_tokens"]), style="dim white") stats_text.append(format_token_count(total_stats["output_tokens"]), style="white")
stats_text.append(" ", style="dim white") stats_text.append(" · ", style="dim white")
stats_text.append("💰 Cost: ", style="bold white") stats_text.append("Cost ", style="dim")
stats_text.append(f"${total_stats['cost']:.4f}", style="dim white") stats_text.append(f"${total_stats['cost']:.4f}", style="#fbbf24")
return stats_text return stats_text
@@ -369,7 +376,7 @@ def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None
if agent_config: if agent_config:
llm_config = agent_config["llm_config"] llm_config = agent_config["llm_config"]
model = getattr(llm_config, "model_name", "Unknown") model = getattr(llm_config, "model_name", "Unknown")
stats_text.append(model, style="dim") stats_text.append(model, style="white")
llm_stats = tracer.get_total_llm_stats() llm_stats = tracer.get_total_llm_stats()
total_stats = llm_stats["total"] total_stats = llm_stats["total"]
@@ -377,11 +384,11 @@ def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None
total_tokens = total_stats["input_tokens"] + total_stats["output_tokens"] total_tokens = total_stats["input_tokens"] + total_stats["output_tokens"]
if total_tokens > 0: if total_tokens > 0:
stats_text.append("\n") stats_text.append("\n")
stats_text.append(f"{format_token_count(total_tokens)} tokens", style="dim") stats_text.append(f"{format_token_count(total_tokens)} tokens", style="white")
if total_stats["cost"] > 0: if total_stats["cost"] > 0:
stats_text.append("\n") stats_text.append(" · ", style="white")
stats_text.append(f"${total_stats['cost']:.2f} spent", style="dim") stats_text.append(f"${total_stats['cost']:.2f}", style="white")
return stats_text return stats_text
@@ -447,29 +454,42 @@ def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
# Target processing utilities # Target processing utilities
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
def _is_http_git_repo(url: str) -> bool:
check_url = f"{url.rstrip('/')}/info/refs?service=git-upload-pack"
try:
req = Request(check_url, headers={"User-Agent": "git/strix"}) # noqa: S310
with urlopen(req, timeout=10) as resp: # noqa: S310 # nosec B310
return "x-git-upload-pack-advertisement" in resp.headers.get("Content-Type", "")
except HTTPError as e:
return e.code == 401
except (URLError, OSError, ValueError):
return False
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911, PLR0912
if not target or not isinstance(target, str): if not target or not isinstance(target, str):
raise ValueError("Target must be a non-empty string") raise ValueError("Target must be a non-empty string")
target = target.strip() target = target.strip()
lower_target = target.lower() if target.startswith("git@"):
bare_repo_prefixes = ( return "repository", {"target_repo": target}
"github.com/",
"www.github.com/", if target.startswith("git://"):
"gitlab.com/", return "repository", {"target_repo": target}
"www.gitlab.com/",
"bitbucket.org/",
"www.bitbucket.org/",
)
if any(lower_target.startswith(p) for p in bare_repo_prefixes):
return "repository", {"target_repo": f"https://{target}"}
parsed = urlparse(target) parsed = urlparse(target)
if parsed.scheme in ("http", "https"): if parsed.scheme in ("http", "https"):
if any( if parsed.username or parsed.password:
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"] return "repository", {"target_repo": target}
): if parsed.path.rstrip("/").endswith(".git"):
return "repository", {"target_repo": target}
if parsed.query or parsed.fragment:
return "web_application", {"target_url": target}
path_segments = [s for s in parsed.path.split("/") if s]
if len(path_segments) >= 2 and _is_http_git_repo(target):
return "repository", {"target_repo": target} return "repository", {"target_repo": target}
return "web_application", {"target_url": target} return "web_application", {"target_url": target}
@@ -484,15 +504,22 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
try: try:
if path.exists(): if path.exists():
if path.is_dir(): if path.is_dir():
resolved = path.resolve() return "local_code", {"target_path": str(path.resolve())}
return "local_code", {"target_path": str(resolved)}
raise ValueError(f"Path exists but is not a directory: {target}") raise ValueError(f"Path exists but is not a directory: {target}")
except (OSError, RuntimeError) as e: except (OSError, RuntimeError) as e:
raise ValueError(f"Invalid path: {target} - {e!s}") from e raise ValueError(f"Invalid path: {target} - {e!s}") from e
if target.startswith("git@") or target.endswith(".git"): if target.endswith(".git"):
return "repository", {"target_repo": target} return "repository", {"target_repo": target}
if "/" in target:
host_part, _, path_part = target.partition("/")
if "." in host_part and not host_part.startswith(".") and path_part:
full_url = f"https://{target}"
if _is_http_git_repo(full_url):
return "repository", {"target_repo": full_url}
return "web_application", {"target_url": full_url}
if "." in target and "/" not in target and not target.startswith("."): if "." in target and "/" not in target and not target.startswith("."):
parts = target.split(".") parts = target.split(".")
if len(parts) >= 2 and all(p and p.strip() for p in parts): if len(parts) >= 2 and all(p and p.strip() for p in parts):
@@ -502,7 +529,7 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
f"Invalid target: {target}\n" f"Invalid target: {target}\n"
"Target must be one of:\n" "Target must be one of:\n"
"- A valid URL (http:// or https://)\n" "- A valid URL (http:// or https://)\n"
"- A Git repository URL (https://github.com/... or git@github.com:...)\n" "- A Git repository URL (https://host/org/repo or git@host:org/repo.git)\n"
"- A local directory path\n" "- A local directory path\n"
"- A domain name (e.g., example.com)\n" "- A domain name (e.g., example.com)\n"
"- An IP address (e.g., 192.168.1.10)" "- An IP address (e.g., 192.168.1.10)"
@@ -668,7 +695,6 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("REPOSITORY CLONE FAILED", style="bold red") error_text.append("REPOSITORY CLONE FAILED", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append(f"Could not clone repository: {repo_url}\n", style="white") error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
@@ -678,8 +704,8 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX CLONE ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -689,7 +715,6 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
sys.exit(1) sys.exit(1)
except FileNotFoundError: except FileNotFoundError:
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("GIT NOT FOUND", style="bold red") error_text.append("GIT NOT FOUND", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append("Git is not installed or not available in PATH.\n", style="white") error_text.append("Git is not installed or not available in PATH.\n", style="white")
@@ -697,8 +722,8 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX CLONE ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -715,7 +740,6 @@ def check_docker_connection() -> Any:
except DockerException: except DockerException:
console = Console() console = Console()
error_text = Text() error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT AVAILABLE", style="bold red") error_text.append("DOCKER NOT AVAILABLE", style="bold red")
error_text.append("\n\n", style="white") error_text.append("\n\n", style="white")
error_text.append("Cannot connect to Docker daemon.\n", style="white") error_text.append("Cannot connect to Docker daemon.\n", style="white")
@@ -726,8 +750,8 @@ def check_docker_connection() -> Any:
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR", title="[bold white]STRIX",
title_align="center", title_align="left",
border_style="red", border_style="red",
padding=(1, 2), padding=(1, 2),
) )
@@ -789,3 +813,33 @@ def process_pull_line(
def validate_llm_response(response: Any) -> None: def validate_llm_response(response: Any) -> None:
if not response or not response.choices or not response.choices[0].message.content: if not response or not response.choices or not response.choices[0].message.content:
raise RuntimeError("Invalid response from LLM") raise RuntimeError("Invalid response from LLM")
def validate_config_file(config_path: str) -> Path:
console = Console()
path = Path(config_path)
if not path.exists():
console.print(f"[bold red]Error:[/] Config file not found: {config_path}")
sys.exit(1)
if path.suffix != ".json":
console.print("[bold red]Error:[/] Config file must be a .json file")
sys.exit(1)
try:
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as e:
console.print(f"[bold red]Error:[/] Invalid JSON in config file: {e}")
sys.exit(1)
if not isinstance(data, dict):
console.print("[bold red]Error:[/] Config file must contain a JSON object")
sys.exit(1)
if "env" not in data or not isinstance(data.get("env"), dict):
console.print("[bold red]Error:[/] Config file must have an 'env' object")
sys.exit(1)
return path

View File

@@ -1,4 +1,6 @@
from strix.config import Config from strix.config import Config
from strix.config.config import resolve_llm_config
from strix.llm.utils import resolve_strix_model
class LLMConfig: class LLMConfig:
@@ -10,11 +12,16 @@ class LLMConfig:
timeout: int | None = None, timeout: int | None = None,
scan_mode: str = "deep", scan_mode: str = "deep",
): ):
self.model_name = model_name or Config.get("strix_llm") resolved_model, self.api_key, self.api_base = resolve_llm_config()
self.model_name = model_name or resolved_model
if not self.model_name: if not self.model_name:
raise ValueError("STRIX_LLM environment variable must be set and not empty") raise ValueError("STRIX_LLM environment variable must be set and not empty")
api_model, canonical = resolve_strix_model(self.model_name)
self.litellm_model: str = api_model or self.model_name
self.canonical_model: str = canonical or self.model_name
self.enable_prompt_caching = enable_prompt_caching self.enable_prompt_caching = enable_prompt_caching
self.skills = skills or [] self.skills = skills or []

View File

@@ -5,7 +5,8 @@ from typing import Any
import litellm import litellm
from strix.config import Config from strix.config.config import resolve_llm_config
from strix.llm.utils import resolve_strix_model
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -155,14 +156,9 @@ def check_duplicate(
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned} comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
model_name = Config.get("strix_llm") model_name, api_key, api_base = resolve_llm_config()
api_key = Config.get("llm_api_key") litellm_model, _ = resolve_strix_model(model_name)
api_base = ( litellm_model = litellm_model or model_name
Config.get("llm_api_base")
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
)
messages = [ messages = [
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT}, {"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
@@ -177,10 +173,9 @@ def check_duplicate(
] ]
completion_kwargs: dict[str, Any] = { completion_kwargs: dict[str, Any] = {
"model": model_name, "model": litellm_model,
"messages": messages, "messages": messages,
"timeout": 120, "timeout": 120,
"temperature": 0,
} }
if api_key: if api_key:
completion_kwargs["api_key"] = api_key completion_kwargs["api_key"] = api_key

View File

@@ -14,6 +14,7 @@ from strix.llm.memory_compressor import MemoryCompressor
from strix.llm.utils import ( from strix.llm.utils import (
_truncate_to_first_function, _truncate_to_first_function,
fix_incomplete_tool_call, fix_incomplete_tool_call,
normalize_tool_format,
parse_tool_invocations, parse_tool_invocations,
) )
from strix.skills import load_skills from strix.skills import load_skills
@@ -63,7 +64,7 @@ class LLM:
self.agent_name = agent_name self.agent_name = agent_name
self.agent_id: str | None = None self.agent_id: str | None = None
self._total_stats = RequestStats() self._total_stats = RequestStats()
self.memory_compressor = MemoryCompressor(model_name=config.model_name) self.memory_compressor = MemoryCompressor(model_name=config.litellm_model)
self.system_prompt = self._load_system_prompt(agent_name) self.system_prompt = self._load_system_prompt(agent_name)
reasoning = Config.get("strix_reasoning_effort") reasoning = Config.get("strix_reasoning_effort")
@@ -90,7 +91,7 @@ class LLM:
*list(self.config.skills or []), *list(self.config.skills or []),
f"scan_modes/{self.config.scan_mode}", f"scan_modes/{self.config.scan_mode}",
] ]
skill_content = load_skills(skills_to_load, env) skill_content = load_skills(skills_to_load)
env.globals["get_skill"] = lambda name: skill_content.get(name, "") env.globals["get_skill"] = lambda name: skill_content.get(name, "")
result = env.get_template("system_prompt.jinja").render( result = env.get_template("system_prompt.jinja").render(
@@ -128,26 +129,34 @@ class LLM:
async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]: async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]:
accumulated = "" accumulated = ""
chunks: list[Any] = [] chunks: list[Any] = []
done_streaming = 0
self._total_stats.requests += 1 self._total_stats.requests += 1
response = await acompletion(**self._build_completion_args(messages), stream=True) response = await acompletion(**self._build_completion_args(messages), stream=True)
async for chunk in response: async for chunk in response:
chunks.append(chunk) chunks.append(chunk)
if done_streaming:
done_streaming += 1
if getattr(chunk, "usage", None) or done_streaming > 5:
break
continue
delta = self._get_chunk_content(chunk) delta = self._get_chunk_content(chunk)
if delta: if delta:
accumulated += delta accumulated += delta
if "</function>" in accumulated: if "</function>" in accumulated or "</invoke>" in accumulated:
accumulated = accumulated[ end_tag = "</function>" if "</function>" in accumulated else "</invoke>"
: accumulated.find("</function>") + len("</function>") pos = accumulated.find(end_tag)
] accumulated = accumulated[: pos + len(end_tag)]
yield LLMResponse(content=accumulated) yield LLMResponse(content=accumulated)
break done_streaming = 1
continue
yield LLMResponse(content=accumulated) yield LLMResponse(content=accumulated)
if chunks: if chunks:
self._update_usage_stats(stream_chunk_builder(chunks)) self._update_usage_stats(stream_chunk_builder(chunks))
accumulated = normalize_tool_format(accumulated)
accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated)) accumulated = fix_incomplete_tool_call(_truncate_to_first_function(accumulated))
yield LLMResponse( yield LLMResponse(
content=accumulated, content=accumulated,
@@ -177,6 +186,9 @@ class LLM:
conversation_history.extend(compressed) conversation_history.extend(compressed)
messages.extend(compressed) messages.extend(compressed)
if messages[-1].get("role") == "assistant":
messages.append({"role": "user", "content": "<meta>Continue the task.</meta>"})
if self._is_anthropic() and self.config.enable_prompt_caching: if self._is_anthropic() and self.config.enable_prompt_caching:
messages = self._add_cache_control(messages) messages = self._add_cache_control(messages)
@@ -187,21 +199,16 @@ class LLM:
messages = self._strip_images(messages) messages = self._strip_images(messages)
args: dict[str, Any] = { args: dict[str, Any] = {
"model": self.config.model_name, "model": self.config.litellm_model,
"messages": messages, "messages": messages,
"timeout": self.config.timeout, "timeout": self.config.timeout,
"stream_options": {"include_usage": True}, "stream_options": {"include_usage": True},
} }
if api_key := Config.get("llm_api_key"): if self.config.api_key:
args["api_key"] = api_key args["api_key"] = self.config.api_key
if api_base := ( if self.config.api_base:
Config.get("llm_api_base") args["api_base"] = self.config.api_base
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
):
args["api_base"] = api_base
if self._supports_reasoning(): if self._supports_reasoning():
args["reasoning_effort"] = self._reasoning_effort args["reasoning_effort"] = self._reasoning_effort
@@ -227,8 +234,8 @@ class LLM:
def _update_usage_stats(self, response: Any) -> None: def _update_usage_stats(self, response: Any) -> None:
try: try:
if hasattr(response, "usage") and response.usage: if hasattr(response, "usage") and response.usage:
input_tokens = getattr(response.usage, "prompt_tokens", 0) input_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
output_tokens = getattr(response.usage, "completion_tokens", 0) output_tokens = getattr(response.usage, "completion_tokens", 0) or 0
cached_tokens = 0 cached_tokens = 0
if hasattr(response.usage, "prompt_tokens_details"): if hasattr(response.usage, "prompt_tokens_details"):
@@ -236,14 +243,11 @@ class LLM:
if hasattr(prompt_details, "cached_tokens"): if hasattr(prompt_details, "cached_tokens"):
cached_tokens = prompt_details.cached_tokens or 0 cached_tokens = prompt_details.cached_tokens or 0
cost = self._extract_cost(response)
else: else:
input_tokens = 0 input_tokens = 0
output_tokens = 0 output_tokens = 0
cached_tokens = 0 cached_tokens = 0
try:
cost = completion_cost(response) or 0.0
except Exception: # noqa: BLE001
cost = 0.0 cost = 0.0
self._total_stats.input_tokens += input_tokens self._total_stats.input_tokens += input_tokens
@@ -254,6 +258,18 @@ class LLM:
except Exception: # noqa: BLE001, S110 # nosec B110 except Exception: # noqa: BLE001, S110 # nosec B110
pass pass
def _extract_cost(self, response: Any) -> float:
if hasattr(response, "usage") and response.usage:
direct_cost = getattr(response.usage, "cost", None)
if direct_cost is not None:
return float(direct_cost)
try:
if hasattr(response, "_hidden_params"):
response._hidden_params.pop("custom_llm_provider", None)
return completion_cost(response, model=self.config.canonical_model) or 0.0
except Exception: # noqa: BLE001
return 0.0
def _should_retry(self, e: Exception) -> bool: def _should_retry(self, e: Exception) -> bool:
code = getattr(e, "status_code", None) or getattr( code = getattr(e, "status_code", None) or getattr(
getattr(e, "response", None), "status_code", None getattr(e, "response", None), "status_code", None
@@ -273,13 +289,13 @@ class LLM:
def _supports_vision(self) -> bool: def _supports_vision(self) -> bool:
try: try:
return bool(supports_vision(model=self.config.model_name)) return bool(supports_vision(model=self.config.canonical_model))
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
return False return False
def _supports_reasoning(self) -> bool: def _supports_reasoning(self) -> bool:
try: try:
return bool(supports_reasoning(model=self.config.model_name)) return bool(supports_reasoning(model=self.config.canonical_model))
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
return False return False
@@ -300,7 +316,7 @@ class LLM:
return result return result
def _add_cache_control(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: def _add_cache_control(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
if not messages or not supports_prompt_caching(self.config.model_name): if not messages or not supports_prompt_caching(self.config.canonical_model):
return messages return messages
result = list(messages) result = list(messages)

View File

@@ -3,7 +3,7 @@ from typing import Any
import litellm import litellm
from strix.config import Config from strix.config.config import Config, resolve_llm_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -91,7 +91,7 @@ def _summarize_messages(
if not messages: if not messages:
empty_summary = "<context_summary message_count='0'>{text}</context_summary>" empty_summary = "<context_summary message_count='0'>{text}</context_summary>"
return { return {
"role": "assistant", "role": "user",
"content": empty_summary.format(text="No messages to summarize"), "content": empty_summary.format(text="No messages to summarize"),
} }
@@ -104,12 +104,18 @@ def _summarize_messages(
conversation = "\n".join(formatted) conversation = "\n".join(formatted)
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation) prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
_, api_key, api_base = resolve_llm_config()
try: try:
completion_args = { completion_args: dict[str, Any] = {
"model": model, "model": model,
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"timeout": timeout, "timeout": timeout,
} }
if api_key:
completion_args["api_key"] = api_key
if api_base:
completion_args["api_base"] = api_base
response = litellm.completion(**completion_args) response = litellm.completion(**completion_args)
summary = response.choices[0].message.content or "" summary = response.choices[0].message.content or ""
@@ -117,7 +123,7 @@ def _summarize_messages(
return messages[0] return messages[0]
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>" summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
return { return {
"role": "assistant", "role": "user",
"content": summary_msg.format(count=len(messages), text=summary), "content": summary_msg.format(count=len(messages), text=summary),
} }
except Exception: except Exception:
@@ -152,7 +158,7 @@ class MemoryCompressor:
): ):
self.max_images = max_images self.max_images = max_images
self.model_name = model_name or Config.get("strix_llm") self.model_name = model_name or Config.get("strix_llm")
self.timeout = timeout or int(Config.get("strix_memory_compressor_timeout") or "30") self.timeout = timeout or int(Config.get("strix_memory_compressor_timeout") or "120")
if not self.model_name: if not self.model_name:
raise ValueError("STRIX_LLM environment variable must be set and not empty") raise ValueError("STRIX_LLM environment variable must be set and not empty")

View File

@@ -3,11 +3,75 @@ import re
from typing import Any from typing import Any
_INVOKE_OPEN = re.compile(r'<invoke\s+name=["\']([^"\']+)["\']>')
_PARAM_NAME_ATTR = re.compile(r'<parameter\s+name=["\']([^"\']+)["\']>')
_FUNCTION_CALLS_TAG = re.compile(r"</?function_calls>")
_STRIP_TAG_QUOTES = re.compile(r"<(function|parameter)\s*=\s*([^>]*?)>")
def normalize_tool_format(content: str) -> str:
"""Convert alternative tool-call XML formats to the expected one.
Handles:
<function_calls>...</function_calls> → stripped
<invoke name="X"> → <function=X>
<parameter name="X"> → <parameter=X>
</invoke> → </function>
<function="X"> → <function=X>
<parameter="X"> → <parameter=X>
"""
if "<invoke" in content or "<function_calls" in content:
content = _FUNCTION_CALLS_TAG.sub("", content)
content = _INVOKE_OPEN.sub(r"<function=\1>", content)
content = _PARAM_NAME_ATTR.sub(r"<parameter=\1>", content)
content = content.replace("</invoke>", "</function>")
return _STRIP_TAG_QUOTES.sub(
lambda m: f"<{m.group(1)}={m.group(2).strip().strip(chr(34) + chr(39))}>", content
)
STRIX_MODEL_MAP: dict[str, str] = {
"claude-sonnet-4.6": "anthropic/claude-sonnet-4-6",
"claude-opus-4.6": "anthropic/claude-opus-4-6",
"gpt-5.2": "openai/gpt-5.2",
"gpt-5.1": "openai/gpt-5.1",
"gpt-5": "openai/gpt-5",
"gpt-5.2-codex": "openai/gpt-5.2-codex",
"gpt-5.1-codex-max": "openai/gpt-5.1-codex-max",
"gpt-5.1-codex": "openai/gpt-5.1-codex",
"gpt-5-codex": "openai/gpt-5-codex",
"gemini-3-pro-preview": "gemini/gemini-3-pro-preview",
"gemini-3-flash-preview": "gemini/gemini-3-flash-preview",
"glm-5": "openrouter/z-ai/glm-5",
"glm-4.7": "openrouter/z-ai/glm-4.7",
}
def resolve_strix_model(model_name: str | None) -> tuple[str | None, str | None]:
"""Resolve a strix/ model into names for API calls and capability lookups.
Returns (api_model, canonical_model):
- api_model: openai/<base> for API calls (Strix API is OpenAI-compatible)
- canonical_model: actual provider model name for litellm capability lookups
Non-strix models return the same name for both.
"""
if not model_name or not model_name.startswith("strix/"):
return model_name, model_name
base_model = model_name[6:]
api_model = f"openai/{base_model}"
canonical_model = STRIX_MODEL_MAP.get(base_model, api_model)
return api_model, canonical_model
def _truncate_to_first_function(content: str) -> str: def _truncate_to_first_function(content: str) -> str:
if not content: if not content:
return content return content
function_starts = [match.start() for match in re.finditer(r"<function=", content)] function_starts = [
match.start() for match in re.finditer(r"<function=|<invoke\s+name=", content)
]
if len(function_starts) >= 2: if len(function_starts) >= 2:
second_function_start = function_starts[1] second_function_start = function_starts[1]
@@ -18,6 +82,7 @@ def _truncate_to_first_function(content: str) -> str:
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None: def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
content = normalize_tool_format(content)
content = fix_incomplete_tool_call(content) content = fix_incomplete_tool_call(content)
tool_invocations: list[dict[str, Any]] = [] tool_invocations: list[dict[str, Any]] = []
@@ -47,12 +112,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
def fix_incomplete_tool_call(content: str) -> str: def fix_incomplete_tool_call(content: str) -> str:
"""Fix incomplete tool calls by adding missing </function> tag.""" """Fix incomplete tool calls by adding missing closing tag.
if (
"<function=" in content Handles both ``<function=…>`` and ``<invoke name="">`` formats.
and content.count("<function=") == 1 """
and "</function>" not in content has_open = "<function=" in content or "<invoke " in content
): count_open = content.count("<function=") + content.count("<invoke ")
has_close = "</function>" in content or "</invoke>" in content
if has_open and count_open == 1 and not has_close:
content = content.rstrip() content = content.rstrip()
content = content + "function>" if content.endswith("</") else content + "\n</function>" content = content + "function>" if content.endswith("</") else content + "\n</function>"
return content return content
@@ -73,6 +140,7 @@ def clean_content(content: str) -> str:
if not content: if not content:
return "" return ""
content = normalize_tool_format(content)
content = fix_incomplete_tool_call(content) content = fix_incomplete_tool_call(content)
tool_pattern = r"<function=[^>]+>.*?</function>" tool_pattern = r"<function=[^>]+>.*?</function>"

View File

@@ -12,17 +12,32 @@ class SandboxInitializationError(Exception):
self.details = details self.details = details
_global_runtime: AbstractRuntime | None = None
def get_runtime() -> AbstractRuntime: def get_runtime() -> AbstractRuntime:
global _global_runtime # noqa: PLW0603
runtime_backend = Config.get("strix_runtime_backend") runtime_backend = Config.get("strix_runtime_backend")
if runtime_backend == "docker": if runtime_backend == "docker":
from .docker_runtime import DockerRuntime from .docker_runtime import DockerRuntime
return DockerRuntime() if _global_runtime is None:
_global_runtime = DockerRuntime()
return _global_runtime
raise ValueError( raise ValueError(
f"Unsupported runtime backend: {runtime_backend}. Only 'docker' is supported for now." f"Unsupported runtime backend: {runtime_backend}. Only 'docker' is supported for now."
) )
__all__ = ["AbstractRuntime", "SandboxInitializationError", "get_runtime"] def cleanup_runtime() -> None:
global _global_runtime # noqa: PLW0603
if _global_runtime is not None:
_global_runtime.cleanup()
_global_runtime = None
__all__ = ["AbstractRuntime", "SandboxInitializationError", "cleanup_runtime", "get_runtime"]

View File

@@ -1,15 +1,13 @@
import contextlib import contextlib
import logging
import os import os
import secrets import secrets
import socket import socket
import time import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError as FuturesTimeoutError
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import cast
import docker import docker
import httpx
from docker.errors import DockerException, ImageNotFound, NotFound from docker.errors import DockerException, ImageNotFound, NotFound
from docker.models.containers import Container from docker.models.containers import Container
from requests.exceptions import ConnectionError as RequestsConnectionError from requests.exceptions import ConnectionError as RequestsConnectionError
@@ -22,10 +20,8 @@ from .runtime import AbstractRuntime, SandboxInfo
HOST_GATEWAY_HOSTNAME = "host.docker.internal" HOST_GATEWAY_HOSTNAME = "host.docker.internal"
DOCKER_TIMEOUT = 60 # seconds DOCKER_TIMEOUT = 60
TOOL_SERVER_HEALTH_REQUEST_TIMEOUT = 5 # seconds per health check request CONTAINER_TOOL_SERVER_PORT = 48081
TOOL_SERVER_HEALTH_RETRIES = 10 # number of retries for health check
logger = logging.getLogger(__name__)
class DockerRuntime(AbstractRuntime): class DockerRuntime(AbstractRuntime):
@@ -33,50 +29,20 @@ class DockerRuntime(AbstractRuntime):
try: try:
self.client = docker.from_env(timeout=DOCKER_TIMEOUT) self.client = docker.from_env(timeout=DOCKER_TIMEOUT)
except (DockerException, RequestsConnectionError, RequestsTimeout) as e: except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
logger.exception("Failed to connect to Docker daemon")
if isinstance(e, RequestsConnectionError | RequestsTimeout):
raise SandboxInitializationError(
"Docker daemon unresponsive",
f"Connection timed out after {DOCKER_TIMEOUT} seconds. "
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from e
raise SandboxInitializationError( raise SandboxInitializationError(
"Docker is not available", "Docker is not available",
"Docker is not available or not configured correctly. " "Please ensure Docker Desktop is installed and running.",
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from e ) from e
self._scan_container: Container | None = None self._scan_container: Container | None = None
self._tool_server_port: int | None = None self._tool_server_port: int | None = None
self._tool_server_token: str | None = None self._tool_server_token: str | None = None
def _generate_sandbox_token(self) -> str:
return secrets.token_urlsafe(32)
def _find_available_port(self) -> int: def _find_available_port(self) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0)) s.bind(("", 0))
return cast("int", s.getsockname()[1]) return cast("int", s.getsockname()[1])
def _exec_run_with_timeout(
self, container: Container, cmd: str, timeout: int = DOCKER_TIMEOUT, **kwargs: Any
) -> Any:
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(container.exec_run, cmd, **kwargs)
try:
return future.result(timeout=timeout)
except FuturesTimeoutError:
logger.exception(f"exec_run timed out after {timeout}s: {cmd[:100]}...")
raise SandboxInitializationError(
"Container command timed out",
f"Command timed out after {timeout} seconds. "
"Docker may be overloaded or unresponsive. "
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from None
def _get_scan_id(self, agent_id: str) -> str: def _get_scan_id(self, agent_id: str) -> str:
try: try:
from strix.telemetry.tracer import get_global_tracer from strix.telemetry.tracer import get_global_tracer
@@ -84,129 +50,118 @@ class DockerRuntime(AbstractRuntime):
tracer = get_global_tracer() tracer = get_global_tracer()
if tracer and tracer.scan_config: if tracer and tracer.scan_config:
return str(tracer.scan_config.get("scan_id", "default-scan")) return str(tracer.scan_config.get("scan_id", "default-scan"))
except ImportError: except (ImportError, AttributeError):
logger.debug("Failed to import tracer, using fallback scan ID") pass
except AttributeError:
logger.debug("Tracer missing scan_config, using fallback scan ID")
return f"scan-{agent_id.split('-')[0]}" return f"scan-{agent_id.split('-')[0]}"
def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None: def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None:
def _validate_image(image: docker.models.images.Image) -> None:
if not image.id or not image.attrs:
raise ImageNotFound(f"Image {image_name} metadata incomplete")
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
image = self.client.images.get(image_name) image = self.client.images.get(image_name)
_validate_image(image) if not image.id or not image.attrs:
except ImageNotFound: raise ImageNotFound(f"Image {image_name} metadata incomplete") # noqa: TRY301
except (ImageNotFound, DockerException):
if attempt == max_retries - 1: if attempt == max_retries - 1:
logger.exception(f"Image {image_name} not found after {max_retries} attempts")
raise raise
logger.warning(f"Image {image_name} not ready, attempt {attempt + 1}/{max_retries}")
time.sleep(2**attempt)
except DockerException:
if attempt == max_retries - 1:
logger.exception(f"Failed to verify image {image_name}")
raise
logger.warning(f"Docker error verifying image, attempt {attempt + 1}/{max_retries}")
time.sleep(2**attempt) time.sleep(2**attempt)
else: else:
logger.debug(f"Image {image_name} verified as available")
return return
def _create_container_with_retry(self, scan_id: str, max_retries: int = 3) -> Container: def _recover_container_state(self, container: Container) -> None:
last_exception = None for env_var in container.attrs["Config"]["Env"]:
if env_var.startswith("TOOL_SERVER_TOKEN="):
self._tool_server_token = env_var.split("=", 1)[1]
break
port_bindings = container.attrs.get("NetworkSettings", {}).get("Ports", {})
port_key = f"{CONTAINER_TOOL_SERVER_PORT}/tcp"
if port_bindings.get(port_key):
self._tool_server_port = int(port_bindings[port_key][0]["HostPort"])
def _wait_for_tool_server(self, max_retries: int = 30, timeout: int = 5) -> None:
host = self._resolve_docker_host()
health_url = f"http://{host}:{self._tool_server_port}/health"
time.sleep(5)
for attempt in range(max_retries):
try:
with httpx.Client(trust_env=False, timeout=timeout) as client:
response = client.get(health_url)
if response.status_code == 200:
data = response.json()
if data.get("status") == "healthy":
return
except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError):
pass
time.sleep(min(2**attempt * 0.5, 5))
raise SandboxInitializationError(
"Tool server failed to start",
"Container initialization timed out. Please try again.",
)
def _create_container(self, scan_id: str, max_retries: int = 2) -> Container:
container_name = f"strix-scan-{scan_id}" container_name = f"strix-scan-{scan_id}"
image_name = Config.get("strix_image") image_name = Config.get("strix_image")
if not image_name: if not image_name:
raise ValueError("STRIX_IMAGE must be configured") raise ValueError("STRIX_IMAGE must be configured")
for attempt in range(max_retries): self._verify_image_available(image_name)
last_error: Exception | None = None
for attempt in range(max_retries + 1):
try: try:
self._verify_image_available(image_name) with contextlib.suppress(NotFound):
existing = self.client.containers.get(container_name)
try:
existing_container = self.client.containers.get(container_name)
logger.warning(f"Container {container_name} already exists, removing it")
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
existing_container.stop(timeout=5) existing.stop(timeout=5)
existing_container.remove(force=True) existing.remove(force=True)
time.sleep(1) time.sleep(1)
except NotFound:
pass
except DockerException as e:
logger.warning(f"Error checking/removing existing container: {e}")
caido_port = self._find_available_port() self._tool_server_port = self._find_available_port()
tool_server_port = self._find_available_port() self._tool_server_token = secrets.token_urlsafe(32)
tool_server_token = self._generate_sandbox_token() execution_timeout = Config.get("strix_sandbox_execution_timeout") or "120"
self._tool_server_port = tool_server_port
self._tool_server_token = tool_server_token
container = self.client.containers.run( container = self.client.containers.run(
image_name, image_name,
command="sleep infinity", command="sleep infinity",
detach=True, detach=True,
name=container_name, name=container_name,
hostname=f"strix-scan-{scan_id}", hostname=container_name,
ports={ ports={f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port},
f"{caido_port}/tcp": caido_port,
f"{tool_server_port}/tcp": tool_server_port,
},
cap_add=["NET_ADMIN", "NET_RAW"], cap_add=["NET_ADMIN", "NET_RAW"],
labels={"strix-scan-id": scan_id}, labels={"strix-scan-id": scan_id},
environment={ environment={
"PYTHONUNBUFFERED": "1", "PYTHONUNBUFFERED": "1",
"CAIDO_PORT": str(caido_port), "TOOL_SERVER_PORT": str(CONTAINER_TOOL_SERVER_PORT),
"TOOL_SERVER_PORT": str(tool_server_port), "TOOL_SERVER_TOKEN": self._tool_server_token,
"TOOL_SERVER_TOKEN": tool_server_token, "STRIX_SANDBOX_EXECUTION_TIMEOUT": str(execution_timeout),
"HOST_GATEWAY": HOST_GATEWAY_HOSTNAME, "HOST_GATEWAY": HOST_GATEWAY_HOSTNAME,
}, },
extra_hosts=self._get_extra_hosts(), extra_hosts={HOST_GATEWAY_HOSTNAME: "host-gateway"},
tty=True, tty=True,
) )
self._scan_container = container self._scan_container = container
logger.info("Created container %s for scan %s", container.id, scan_id) self._wait_for_tool_server()
self._initialize_container(
container, caido_port, tool_server_port, tool_server_token
)
except (DockerException, RequestsConnectionError, RequestsTimeout) as e: except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
last_exception = e last_error = e
if attempt == max_retries - 1: if attempt < max_retries:
logger.exception(f"Failed to create container after {max_retries} attempts") self._tool_server_port = None
break self._tool_server_token = None
time.sleep(2**attempt)
logger.warning(f"Container creation attempt {attempt + 1}/{max_retries} failed")
self._tool_server_port = None
self._tool_server_token = None
sleep_time = (2**attempt) + (0.1 * attempt)
time.sleep(sleep_time)
else: else:
return container return container
if isinstance(last_exception, RequestsConnectionError | RequestsTimeout):
raise SandboxInitializationError(
"Failed to create sandbox container",
f"Docker daemon unresponsive after {max_retries} attempts "
f"(timed out after {DOCKER_TIMEOUT}s). "
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from last_exception
raise SandboxInitializationError( raise SandboxInitializationError(
"Failed to create sandbox container", "Failed to create container",
f"Container creation failed after {max_retries} attempts: {last_exception}. " f"Container creation failed after {max_retries + 1} attempts: {last_error}",
"Please ensure Docker Desktop is installed and running, " ) from last_error
"and try running strix again.",
) from last_exception
def _get_or_create_scan_container(self, scan_id: str) -> Container: # noqa: PLR0912 def _get_or_create_container(self, scan_id: str) -> Container:
container_name = f"strix-scan-{scan_id}" container_name = f"strix-scan-{scan_id}"
if self._scan_container: if self._scan_container:
@@ -223,33 +178,14 @@ class DockerRuntime(AbstractRuntime):
container = self.client.containers.get(container_name) container = self.client.containers.get(container_name)
container.reload() container.reload()
if (
"strix-scan-id" not in container.labels
or container.labels["strix-scan-id"] != scan_id
):
logger.warning(
f"Container {container_name} exists but missing/wrong label, updating"
)
if container.status != "running": if container.status != "running":
logger.info(f"Starting existing container {container_name}")
container.start() container.start()
time.sleep(2) time.sleep(2)
self._scan_container = container self._scan_container = container
self._recover_container_state(container)
for env_var in container.attrs["Config"]["Env"]:
if env_var.startswith("TOOL_SERVER_PORT="):
self._tool_server_port = int(env_var.split("=")[1])
elif env_var.startswith("TOOL_SERVER_TOKEN="):
self._tool_server_token = env_var.split("=")[1]
logger.info(f"Reusing existing container {container_name}")
except NotFound: except NotFound:
pass pass
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
logger.warning(f"Failed to get container by name {container_name}: {e}")
else: else:
return container return container
@@ -262,101 +198,14 @@ class DockerRuntime(AbstractRuntime):
if container.status != "running": if container.status != "running":
container.start() container.start()
time.sleep(2) time.sleep(2)
self._scan_container = container self._scan_container = container
self._recover_container_state(container)
for env_var in container.attrs["Config"]["Env"]:
if env_var.startswith("TOOL_SERVER_PORT="):
self._tool_server_port = int(env_var.split("=")[1])
elif env_var.startswith("TOOL_SERVER_TOKEN="):
self._tool_server_token = env_var.split("=")[1]
logger.info(f"Found existing container by label for scan {scan_id}")
return container return container
except (DockerException, RequestsConnectionError, RequestsTimeout) as e: except DockerException:
logger.warning("Failed to find existing container by label for scan %s: %s", scan_id, e) pass
logger.info("Creating new Docker container for scan %s", scan_id) return self._create_container(scan_id)
return self._create_container_with_retry(scan_id)
def _initialize_container(
self, container: Container, caido_port: int, tool_server_port: int, tool_server_token: str
) -> None:
logger.info("Initializing Caido proxy on port %s", caido_port)
self._exec_run_with_timeout(
container,
f"bash -c 'export CAIDO_PORT={caido_port} && /usr/local/bin/docker-entrypoint.sh true'",
detach=False,
)
time.sleep(5)
result = self._exec_run_with_timeout(
container,
"bash -c 'source /etc/profile.d/proxy.sh && echo $CAIDO_API_TOKEN'",
user="pentester",
)
caido_token = result.output.decode().strip() if result.exit_code == 0 else ""
container.exec_run(
f"bash -c 'source /etc/profile.d/proxy.sh && cd /app && "
f"STRIX_SANDBOX_MODE=true CAIDO_API_TOKEN={caido_token} CAIDO_PORT={caido_port} "
f"poetry run python strix/runtime/tool_server.py --token {tool_server_token} "
f"--host 0.0.0.0 --port {tool_server_port} &'",
detach=True,
user="pentester",
)
time.sleep(2)
host = self._resolve_docker_host()
health_url = f"http://{host}:{tool_server_port}/health"
self._wait_for_tool_server_health(health_url)
def _wait_for_tool_server_health(
self,
health_url: str,
max_retries: int = TOOL_SERVER_HEALTH_RETRIES,
request_timeout: int = TOOL_SERVER_HEALTH_REQUEST_TIMEOUT,
) -> None:
import httpx
logger.info(f"Waiting for tool server health at {health_url}")
for attempt in range(max_retries):
try:
with httpx.Client(trust_env=False, timeout=request_timeout) as client:
response = client.get(health_url)
response.raise_for_status()
health_data = response.json()
if health_data.get("status") == "healthy":
logger.info(
f"Tool server is healthy after {attempt + 1} attempt(s): {health_data}"
)
return
logger.warning(f"Tool server returned unexpected status: {health_data}")
except httpx.ConnectError:
logger.debug(
f"Tool server not ready (attempt {attempt + 1}/{max_retries}): "
f"Connection refused"
)
except httpx.TimeoutException:
logger.debug(
f"Tool server not ready (attempt {attempt + 1}/{max_retries}): "
f"Request timed out"
)
except (httpx.RequestError, httpx.HTTPStatusError) as e:
logger.debug(f"Tool server not ready (attempt {attempt + 1}/{max_retries}): {e}")
sleep_time = min(2**attempt * 0.5, 5)
time.sleep(sleep_time)
raise SandboxInitializationError(
"Tool server failed to start",
"Please ensure Docker Desktop is installed and running, and try running strix again.",
)
def _copy_local_directory_to_container( def _copy_local_directory_to_container(
self, container: Container, local_path: str, target_name: str | None = None self, container: Container, local_path: str, target_name: str | None = None
@@ -367,17 +216,8 @@ class DockerRuntime(AbstractRuntime):
try: try:
local_path_obj = Path(local_path).resolve() local_path_obj = Path(local_path).resolve()
if not local_path_obj.exists() or not local_path_obj.is_dir(): if not local_path_obj.exists() or not local_path_obj.is_dir():
logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
return return
if target_name:
logger.info(
f"Copying local directory {local_path_obj} to container at "
f"/workspace/{target_name}"
)
else:
logger.info(f"Copying local directory {local_path_obj} to container")
tar_buffer = BytesIO() tar_buffer = BytesIO()
with tarfile.open(fileobj=tar_buffer, mode="w") as tar: with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
for item in local_path_obj.rglob("*"): for item in local_path_obj.rglob("*"):
@@ -388,16 +228,12 @@ class DockerRuntime(AbstractRuntime):
tar_buffer.seek(0) tar_buffer.seek(0)
container.put_archive("/workspace", tar_buffer.getvalue()) container.put_archive("/workspace", tar_buffer.getvalue())
container.exec_run( container.exec_run(
"chown -R pentester:pentester /workspace && chmod -R 755 /workspace", "chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
user="root", user="root",
) )
logger.info("Successfully copied local directory to /workspace")
except (OSError, DockerException): except (OSError, DockerException):
logger.exception("Failed to copy local directory to container") pass
async def create_sandbox( async def create_sandbox(
self, self,
@@ -406,7 +242,7 @@ class DockerRuntime(AbstractRuntime):
local_sources: list[dict[str, str]] | None = None, local_sources: list[dict[str, str]] | None = None,
) -> SandboxInfo: ) -> SandboxInfo:
scan_id = self._get_scan_id(agent_id) scan_id = self._get_scan_id(agent_id)
container = self._get_or_create_scan_container(scan_id) container = self._get_or_create_container(scan_id)
source_copied_key = f"_source_copied_{scan_id}" source_copied_key = f"_source_copied_{scan_id}"
if local_sources and not hasattr(self, source_copied_key): if local_sources and not hasattr(self, source_copied_key):
@@ -414,40 +250,33 @@ class DockerRuntime(AbstractRuntime):
source_path = source.get("source_path") source_path = source.get("source_path")
if not source_path: if not source_path:
continue continue
target_name = (
target_name = source.get("workspace_subdir") source.get("workspace_subdir") or Path(source_path).name or f"target_{index}"
if not target_name: )
target_name = Path(source_path).name or f"target_{index}"
self._copy_local_directory_to_container(container, source_path, target_name) self._copy_local_directory_to_container(container, source_path, target_name)
setattr(self, source_copied_key, True) setattr(self, source_copied_key, True)
container_id = container.id if container.id is None:
if container_id is None:
raise RuntimeError("Docker container ID is unexpectedly None") raise RuntimeError("Docker container ID is unexpectedly None")
token = existing_token if existing_token is not None else self._tool_server_token token = existing_token or self._tool_server_token
if self._tool_server_port is None or token is None: if self._tool_server_port is None or token is None:
raise RuntimeError("Tool server not initialized or no token available") raise RuntimeError("Tool server not initialized")
api_url = await self.get_sandbox_url(container_id, self._tool_server_port) host = self._resolve_docker_host()
api_url = f"http://{host}:{self._tool_server_port}"
await self._register_agent_with_tool_server(api_url, agent_id, token) await self._register_agent(api_url, agent_id, token)
return { return {
"workspace_id": container_id, "workspace_id": container.id,
"api_url": api_url, "api_url": api_url,
"auth_token": token, "auth_token": token,
"tool_server_port": self._tool_server_port, "tool_server_port": self._tool_server_port,
"agent_id": agent_id, "agent_id": agent_id,
} }
async def _register_agent_with_tool_server( async def _register_agent(self, api_url: str, agent_id: str, token: str) -> None:
self, api_url: str, agent_id: str, token: str
) -> None:
import httpx
try: try:
async with httpx.AsyncClient(trust_env=False) as client: async with httpx.AsyncClient(trust_env=False) as client:
response = await client.post( response = await client.post(
@@ -457,54 +286,52 @@ class DockerRuntime(AbstractRuntime):
timeout=30, timeout=30,
) )
response.raise_for_status() response.raise_for_status()
logger.info(f"Registered agent {agent_id} with tool server") except httpx.RequestError:
except (httpx.RequestError, httpx.HTTPStatusError) as e: pass
logger.warning(f"Failed to register agent {agent_id}: {e}")
async def get_sandbox_url(self, container_id: str, port: int) -> str: async def get_sandbox_url(self, container_id: str, port: int) -> str:
try: try:
container = self.client.containers.get(container_id) self.client.containers.get(container_id)
container.reload() return f"http://{self._resolve_docker_host()}:{port}"
host = self._resolve_docker_host()
except NotFound: except NotFound:
raise ValueError(f"Container {container_id} not found.") from None raise ValueError(f"Container {container_id} not found.") from None
except DockerException as e:
raise RuntimeError(f"Failed to get container URL for {container_id}: {e}") from e
else:
return f"http://{host}:{port}"
def _resolve_docker_host(self) -> str: def _resolve_docker_host(self) -> str:
docker_host = os.getenv("DOCKER_HOST", "") docker_host = os.getenv("DOCKER_HOST", "")
if not docker_host: if docker_host:
return "127.0.0.1" from urllib.parse import urlparse
from urllib.parse import urlparse
parsed = urlparse(docker_host)
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
return parsed.hostname
parsed = urlparse(docker_host)
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
return parsed.hostname
return "127.0.0.1" return "127.0.0.1"
def _get_extra_hosts(self) -> dict[str, str]:
return {HOST_GATEWAY_HOSTNAME: "host-gateway"}
async def destroy_sandbox(self, container_id: str) -> None: async def destroy_sandbox(self, container_id: str) -> None:
logger.info("Destroying scan container %s", container_id)
try: try:
container = self.client.containers.get(container_id) container = self.client.containers.get(container_id)
container.stop() container.stop()
container.remove() container.remove()
logger.info("Successfully destroyed container %s", container_id) self._scan_container = None
self._tool_server_port = None
self._tool_server_token = None
except (NotFound, DockerException):
pass
def cleanup(self) -> None:
if self._scan_container is not None:
container_name = self._scan_container.name
self._scan_container = None self._scan_container = None
self._tool_server_port = None self._tool_server_port = None
self._tool_server_token = None self._tool_server_token = None
except NotFound: if container_name is None:
logger.warning("Container %s not found for destruction.", container_id) return
except DockerException as e:
logger.warning("Failed to destroy container %s: %s", container_id, e) import subprocess
subprocess.Popen( # noqa: S603
["docker", "rm", "-f", container_name], # noqa: S607
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)

View File

@@ -27,3 +27,6 @@ class AbstractRuntime(ABC):
@abstractmethod @abstractmethod
async def destroy_sandbox(self, container_id: str) -> None: async def destroy_sandbox(self, container_id: str) -> None:
raise NotImplementedError raise NotImplementedError
def cleanup(self) -> None:
raise NotImplementedError

View File

@@ -2,11 +2,9 @@ from __future__ import annotations
import argparse import argparse
import asyncio import asyncio
import logging
import os import os
import signal import signal
import sys import sys
from multiprocessing import Process, Queue
from typing import Any from typing import Any
import uvicorn import uvicorn
@@ -23,17 +21,22 @@ parser = argparse.ArgumentParser(description="Start Strix tool server")
parser.add_argument("--token", required=True, help="Authentication token") parser.add_argument("--token", required=True, help="Authentication token")
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec
parser.add_argument("--port", type=int, required=True, help="Port to bind to") parser.add_argument("--port", type=int, required=True, help="Port to bind to")
parser.add_argument(
"--timeout",
type=int,
default=120,
help="Hard timeout in seconds for each request execution (default: 120)",
)
args = parser.parse_args() args = parser.parse_args()
EXPECTED_TOKEN = args.token EXPECTED_TOKEN = args.token
REQUEST_TIMEOUT = args.timeout
app = FastAPI() app = FastAPI()
security = HTTPBearer() security = HTTPBearer()
security_dependency = Depends(security) security_dependency = Depends(security)
agent_processes: dict[str, dict[str, Any]] = {} agent_tasks: dict[str, asyncio.Task[Any]] = {}
agent_queues: dict[str, dict[str, Queue[Any]]] = {}
def verify_token(credentials: HTTPAuthorizationCredentials) -> str: def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
@@ -65,60 +68,19 @@ class ToolExecutionResponse(BaseModel):
error: str | None = None error: str | None = None
def agent_worker(_agent_id: str, request_queue: Queue[Any], response_queue: Queue[Any]) -> None: async def _run_tool(agent_id: str, tool_name: str, kwargs: dict[str, Any]) -> Any:
null_handler = logging.NullHandler() from strix.tools.argument_parser import convert_arguments
from strix.tools.context import set_current_agent_id
root_logger = logging.getLogger()
root_logger.handlers = [null_handler]
root_logger.setLevel(logging.CRITICAL)
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments
from strix.tools.registry import get_tool_by_name from strix.tools.registry import get_tool_by_name
while True: set_current_agent_id(agent_id)
try:
request = request_queue.get()
if request is None: tool_func = get_tool_by_name(tool_name)
break if not tool_func:
raise ValueError(f"Tool '{tool_name}' not found")
tool_name = request["tool_name"] converted_kwargs = convert_arguments(tool_func, kwargs)
kwargs = request["kwargs"] return await asyncio.to_thread(tool_func, **converted_kwargs)
try:
tool_func = get_tool_by_name(tool_name)
if not tool_func:
response_queue.put({"error": f"Tool '{tool_name}' not found"})
continue
converted_kwargs = convert_arguments(tool_func, kwargs)
result = tool_func(**converted_kwargs)
response_queue.put({"result": result})
except (ArgumentConversionError, ValidationError) as e:
response_queue.put({"error": f"Invalid arguments: {e}"})
except (RuntimeError, ValueError, ImportError) as e:
response_queue.put({"error": f"Tool execution error: {e}"})
except (RuntimeError, ValueError, ImportError) as e:
response_queue.put({"error": f"Worker error: {e}"})
def ensure_agent_process(agent_id: str) -> tuple[Queue[Any], Queue[Any]]:
if agent_id not in agent_processes:
request_queue: Queue[Any] = Queue()
response_queue: Queue[Any] = Queue()
process = Process(
target=agent_worker, args=(agent_id, request_queue, response_queue), daemon=True
)
process.start()
agent_processes[agent_id] = {"process": process, "pid": process.pid}
agent_queues[agent_id] = {"request": request_queue, "response": response_queue}
return agent_queues[agent_id]["request"], agent_queues[agent_id]["response"]
@app.post("/execute", response_model=ToolExecutionResponse) @app.post("/execute", response_model=ToolExecutionResponse)
@@ -127,20 +89,42 @@ async def execute_tool(
) -> ToolExecutionResponse: ) -> ToolExecutionResponse:
verify_token(credentials) verify_token(credentials)
request_queue, response_queue = ensure_agent_process(request.agent_id) agent_id = request.agent_id
request_queue.put({"tool_name": request.tool_name, "kwargs": request.kwargs}) if agent_id in agent_tasks:
old_task = agent_tasks[agent_id]
if not old_task.done():
old_task.cancel()
task = asyncio.create_task(
asyncio.wait_for(
_run_tool(agent_id, request.tool_name, request.kwargs), timeout=REQUEST_TIMEOUT
)
)
agent_tasks[agent_id] = task
try: try:
loop = asyncio.get_event_loop() result = await task
response = await loop.run_in_executor(None, response_queue.get) return ToolExecutionResponse(result=result)
if "error" in response: except asyncio.CancelledError:
return ToolExecutionResponse(error=response["error"]) return ToolExecutionResponse(error="Cancelled by newer request")
return ToolExecutionResponse(result=response.get("result"))
except (RuntimeError, ValueError, OSError) as e: except TimeoutError:
return ToolExecutionResponse(error=f"Worker error: {e}") return ToolExecutionResponse(error=f"Tool timed out after {REQUEST_TIMEOUT}s")
except ValidationError as e:
return ToolExecutionResponse(error=f"Invalid arguments: {e}")
except (ValueError, RuntimeError, ImportError) as e:
return ToolExecutionResponse(error=f"Tool execution error: {e}")
except Exception as e: # noqa: BLE001
return ToolExecutionResponse(error=f"Unexpected error: {e}")
finally:
if agent_tasks.get(agent_id) is task:
del agent_tasks[agent_id]
@app.post("/register_agent") @app.post("/register_agent")
@@ -148,8 +132,6 @@ async def register_agent(
agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
) -> dict[str, str]: ) -> dict[str, str]:
verify_token(credentials) verify_token(credentials)
ensure_agent_process(agent_id)
return {"status": "registered", "agent_id": agent_id} return {"status": "registered", "agent_id": agent_id}
@@ -160,35 +142,16 @@ async def health_check() -> dict[str, Any]:
"sandbox_mode": str(SANDBOX_MODE), "sandbox_mode": str(SANDBOX_MODE),
"environment": "sandbox" if SANDBOX_MODE else "main", "environment": "sandbox" if SANDBOX_MODE else "main",
"auth_configured": "true" if EXPECTED_TOKEN else "false", "auth_configured": "true" if EXPECTED_TOKEN else "false",
"active_agents": len(agent_processes), "active_agents": len(agent_tasks),
"agents": list(agent_processes.keys()), "agents": list(agent_tasks.keys()),
} }
def cleanup_all_agents() -> None:
for agent_id in list(agent_processes.keys()):
try:
agent_queues[agent_id]["request"].put(None)
process = agent_processes[agent_id]["process"]
process.join(timeout=1)
if process.is_alive():
process.terminate()
process.join(timeout=1)
if process.is_alive():
process.kill()
except (BrokenPipeError, EOFError, OSError):
pass
except (RuntimeError, ValueError) as e:
logging.getLogger(__name__).debug(f"Error during agent cleanup: {e}")
def signal_handler(_signum: int, _frame: Any) -> None: def signal_handler(_signum: int, _frame: Any) -> None:
signal.signal(signal.SIGPIPE, signal.SIG_IGN) if hasattr(signal, "SIGPIPE") else None if hasattr(signal, "SIGPIPE"):
cleanup_all_agents() signal.signal(signal.SIGPIPE, signal.SIG_IGN)
for task in agent_tasks.values():
task.cancel()
sys.exit(0) sys.exit(0)
@@ -199,7 +162,4 @@ signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
if __name__ == "__main__": if __name__ == "__main__":
try: uvicorn.run(app, host=args.host, port=args.port, log_level="info")
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
finally:
cleanup_all_agents()

View File

@@ -49,8 +49,9 @@ A good skill is a structured knowledge package that typically includes:
- **Practical examples** - Working payloads, commands, or test cases with variations - **Practical examples** - Working payloads, commands, or test cases with variations
- **Validation methods** - How to confirm findings and avoid false positives - **Validation methods** - How to confirm findings and avoid false positives
- **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases - **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases
- **YAML frontmatter** - `name` and `description` fields for skill metadata
Skills use XML-style tags for structure and focus on deep, specialized knowledge that significantly enhances agent capabilities for that specific context. Skills focus on deep, specialized knowledge to significantly enhance agent capabilities. They are dynamically injected into agent context when needed.
--- ---

View File

@@ -1,8 +1,12 @@
from jinja2 import Environment import re
from strix.utils.resource_paths import get_strix_resource_path from strix.utils.resource_paths import get_strix_resource_path
_EXCLUDED_CATEGORIES = {"scan_modes", "coordination"}
_FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
def get_available_skills() -> dict[str, list[str]]: def get_available_skills() -> dict[str, list[str]]:
skills_dir = get_strix_resource_path("skills") skills_dir = get_strix_resource_path("skills")
available_skills: dict[str, list[str]] = {} available_skills: dict[str, list[str]] = {}
@@ -13,9 +17,13 @@ def get_available_skills() -> dict[str, list[str]]:
for category_dir in skills_dir.iterdir(): for category_dir in skills_dir.iterdir():
if category_dir.is_dir() and not category_dir.name.startswith("__"): if category_dir.is_dir() and not category_dir.name.startswith("__"):
category_name = category_dir.name category_name = category_dir.name
if category_name in _EXCLUDED_CATEGORIES:
continue
skills = [] skills = []
for file_path in category_dir.glob("*.jinja"): for file_path in category_dir.glob("*.md"):
skill_name = file_path.stem skill_name = file_path.stem
skills.append(skill_name) skills.append(skill_name)
@@ -70,36 +78,61 @@ def generate_skills_description() -> str:
return description return description
def load_skills(skill_names: list[str], jinja_env: Environment) -> dict[str, str]: def _get_all_categories() -> dict[str, list[str]]:
"""Get all skill categories including internal ones (scan_modes, coordination)."""
skills_dir = get_strix_resource_path("skills")
all_categories: dict[str, list[str]] = {}
if not skills_dir.exists():
return all_categories
for category_dir in skills_dir.iterdir():
if category_dir.is_dir() and not category_dir.name.startswith("__"):
category_name = category_dir.name
skills = []
for file_path in category_dir.glob("*.md"):
skill_name = file_path.stem
skills.append(skill_name)
if skills:
all_categories[category_name] = sorted(skills)
return all_categories
def load_skills(skill_names: list[str]) -> dict[str, str]:
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
skill_content = {} skill_content = {}
skills_dir = get_strix_resource_path("skills") skills_dir = get_strix_resource_path("skills")
available_skills = get_available_skills() all_categories = _get_all_categories()
for skill_name in skill_names: for skill_name in skill_names:
try: try:
skill_path = None skill_path = None
if "/" in skill_name: if "/" in skill_name:
skill_path = f"{skill_name}.jinja" skill_path = f"{skill_name}.md"
else: else:
for category, skills in available_skills.items(): for category, skills in all_categories.items():
if skill_name in skills: if skill_name in skills:
skill_path = f"{category}/{skill_name}.jinja" skill_path = f"{category}/{skill_name}.md"
break break
if not skill_path: if not skill_path:
root_candidate = f"{skill_name}.jinja" root_candidate = f"{skill_name}.md"
if (skills_dir / root_candidate).exists(): if (skills_dir / root_candidate).exists():
skill_path = root_candidate skill_path = root_candidate
if skill_path and (skills_dir / skill_path).exists(): if skill_path and (skills_dir / skill_path).exists():
template = jinja_env.get_template(skill_path) full_path = skills_dir / skill_path
var_name = skill_name.split("/")[-1] var_name = skill_name.split("/")[-1]
skill_content[var_name] = template.render() content = full_path.read_text(encoding="utf-8")
content = _FRONTMATTER_PATTERN.sub("", content).lstrip()
skill_content[var_name] = content
logger.info(f"Loaded skill: {skill_name} -> {var_name}") logger.info(f"Loaded skill: {skill_name} -> {var_name}")
else: else:
logger.warning(f"Skill not found: {skill_name}") logger.warning(f"Skill not found: {skill_name}")

View File

@@ -1,41 +0,0 @@
<coordination_role>
You are a COORDINATION AGENT ONLY. You do NOT perform any security testing, vulnerability assessment, or technical work yourself.
Your ONLY responsibilities:
1. Create specialized agents for specific security tasks
2. Monitor agent progress and coordinate between them
3. Compile final scan reports from agent findings
4. Manage agent communication and dependencies
CRITICAL RESTRICTIONS:
- NEVER perform vulnerability testing or security assessments
- NEVER write detailed vulnerability reports (only compile final summaries)
- ONLY use agent_graph and finish tools for coordination
- You can create agents throughout the scan process, depending on the task and findings, not just at the beginning!
</coordination_role>
<agent_management>
BEFORE CREATING AGENTS:
1. Analyze the target scope and break into independent tasks
2. Check existing agents to avoid duplication
3. Create agents with clear, specific objectives to avoid duplication
AGENT TYPES YOU CAN CREATE:
- Reconnaissance: subdomain enum, port scanning, tech identification, etc.
- Vulnerability Testing: SQL injection, XSS, auth bypass, IDOR, RCE, SSRF, etc. Can be black-box or white-box.
- Direct vulnerability testing agents to implement hierarchical workflow (per finding: discover, verify, report, fix): each one should create validation agents for findings verification, which spawn reporting agents for documentation, which create fix agents for remediation
COORDINATION GUIDELINES:
- Ensure clear task boundaries and success criteria
- Terminate redundant agents when objectives overlap
- Use message passing only when essential (requests/answers or critical handoffs); avoid routine status messages and prefer batched updates
</agent_management>
<final_responsibilities>
When all agents complete:
1. Collect findings from all agents
2. Compile a final scan summary report
3. Use finish tool to complete the assessment
Your value is in orchestration, not execution.
</final_responsibilities>

View File

@@ -0,0 +1,92 @@
---
name: root-agent
description: Orchestration layer that coordinates specialized subagents for security assessments
---
# Root Agent
Orchestration layer for security assessments. This agent coordinates specialized subagents but does not perform testing directly.
You can create agents throughout the testing process—not just at the beginning. Spawn agents dynamically based on findings and evolving scope.
## Role
- Decompose targets into discrete, parallelizable tasks
- Spawn and monitor specialized subagents
- Aggregate findings into a cohesive final report
- Manage dependencies and handoffs between agents
## Scope Decomposition
Before spawning agents, analyze the target:
1. **Identify attack surfaces** - web apps, APIs, infrastructure, etc.
2. **Define boundaries** - in-scope domains, IP ranges, excluded assets
3. **Determine approach** - blackbox, greybox, or whitebox assessment
4. **Prioritize by risk** - critical assets and high-value targets first
## Agent Architecture
Structure agents by function:
**Reconnaissance**
- Asset discovery and enumeration
- Technology fingerprinting
- Attack surface mapping
**Vulnerability Assessment**
- Injection testing (SQLi, XSS, command injection)
- Authentication and session analysis
- Access control testing (IDOR, privilege escalation)
- Business logic flaws
- Infrastructure vulnerabilities
**Exploitation and Validation**
- Proof-of-concept development
- Impact demonstration
- Vulnerability chaining
**Reporting**
- Finding documentation
- Remediation recommendations
## Coordination Principles
**Task Independence**
Create agents with minimal dependencies. Parallel execution is faster than sequential.
**Clear Objectives**
Each agent should have a specific, measurable goal. Vague objectives lead to scope creep and redundant work.
**Avoid Duplication**
Before creating agents:
1. Analyze the target scope and break into independent tasks
2. Check existing agents to avoid overlap
3. Create agents with clear, specific objectives
**Hierarchical Delegation**
Complex findings warrant specialized subagents:
- Discovery agent finds potential vulnerability
- Validation agent confirms exploitability
- Reporting agent documents with reproduction steps
- Fix agent provides remediation (if needed)
**Resource Efficiency**
- Avoid duplicate coverage across agents
- Terminate agents when objectives are met or no longer relevant
- Use message passing only when essential (requests/answers, critical handoffs)
- Prefer batched updates over routine status messages
## Completion
When all agents report completion:
1. Collect and deduplicate findings across agents
2. Assess overall security posture
3. Compile executive summary with prioritized recommendations
4. Invoke finish tool with final report

View File

@@ -1,142 +0,0 @@
<fastapi_security_testing_guide>
<title>FASTAPI — ADVERSARIAL TESTING PLAYBOOK</title>
<critical>FastAPI (on Starlette) spans HTTP, WebSocket, and background tasks with powerful dependency injection and automatic OpenAPI. Security breaks where identity, authorization, and validation drift across routers, middlewares, proxies, and channels. Treat every dependency, header, and object reference as untrusted until bound to the caller and tenant.</critical>
<surface_map>
- ASGI stack: Starlette middlewares (CORS, TrustedHost, ProxyHeaders, Session), exception handlers, lifespan events
- Routers/sub-apps: APIRouter with prefixes/tags, mounted apps (StaticFiles, admin subapps), `include_router`, versioned paths
- Security and DI: `Depends`, `Security`, `OAuth2PasswordBearer`, `HTTPBearer`, scopes, per-router vs per-route dependencies
- Models and validation: Pydantic v1/v2 models, unions/Annotated, custom validators, extra fields policy, coercion
- Docs and schema: `/openapi.json`, `/docs`, `/redoc`, alternative docs_url/redoc_url, schema extensions
- Files and static: `UploadFile`, `File`, `FileResponse`, `StaticFiles` mounts, template engines (`Jinja2Templates`)
- Channels: HTTP (sync/async), WebSocket, StreamingResponse/SSE, BackgroundTasks/Task queues
- Deployment: Uvicorn/Gunicorn, reverse proxies/CDN, TLS termination, header trust
</surface_map>
<methodology>
1. Enumerate routes from OpenAPI and via crawling; diff with 404-fuzzing for hidden endpoints (`include_in_schema=False`).
2. Build a Principal × Channel × Content-Type matrix (unauth, user, staff/admin; HTTP vs WebSocket; JSON/form/multipart) and capture baselines.
3. For each route, identify dependencies (router-level and route-level). Attempt to satisfy security dependencies minimally, then mutate context (tokens, scopes, tenant headers) and object IDs.
4. Compare behavior across deployments: dev/stage/prod often differ in middlewares (CORS, TrustedHost, ProxyHeaders) and docs exposure.
</methodology>
<high_value_targets>
- `/openapi.json`, `/docs`, `/redoc` in production (full attack surface map; securitySchemes and server URLs)
- Auth flows: token endpoints, session/cookie bridges, OAuth device/PKCE, scope checks
- Admin/staff routers, feature-flagged routes, `include_in_schema=False` endpoints
- File upload/download, import/export/report endpoints, signed URL generators
- WebSocket endpoints carrying notifications, admin channels, or commands
- Background job creation/fetch (`/jobs/{id}`, `/tasks/{id}/result`)
- Mounted subapps (admin UI, storage browsers, metrics/health endpoints)
</high_value_targets>
<advanced_techniques>
<openapi_and_docs>
- Try default and alternate locations: `/openapi.json`, `/docs`, `/redoc`, `/api/openapi.json`, `/internal/openapi.json`.
- If OpenAPI is exposed, mine: paths, parameter names, securitySchemes, scopes, servers; find endpoints hidden in UI but present in schema.
- Schema drift: endpoints with `include_in_schema=False` wont 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; its 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>

View File

@@ -0,0 +1,191 @@
---
name: fastapi
description: Security testing playbook for FastAPI applications covering ASGI, dependency injection, and API vulnerabilities
---
# FastAPI
Security testing for FastAPI/Starlette applications. Focus on dependency injection flaws, middleware gaps, and authorization drift across routers and channels.
## Attack Surface
**Core Components**
- ASGI middlewares: CORS, TrustedHost, ProxyHeaders, Session, exception handlers, lifespan events
- Routers and sub-apps: APIRouter prefixes/tags, mounted apps (StaticFiles, admin), `include_router`, versioned paths
- Dependency injection: `Depends`, `Security`, `OAuth2PasswordBearer`, `HTTPBearer`, scopes
**Data Handling**
- Pydantic models: v1/v2, unions/Annotated, custom validators, extra fields policy, coercion
- File operations: UploadFile, File, FileResponse, StaticFiles mounts
- Templates: Jinja2Templates rendering
**Channels**
- HTTP (sync/async), WebSocket, SSE/StreamingResponse
- BackgroundTasks and task queues
**Deployment**
- Uvicorn/Gunicorn, reverse proxies/CDN, TLS termination, header trust
## High-Value Targets
- `/openapi.json`, `/docs`, `/redoc` in production (full attack surface map, securitySchemes, server URLs)
- Auth flows: token endpoints, session/cookie bridges, OAuth device/PKCE
- Admin/staff routers, feature-flagged routes, `include_in_schema=False` endpoints
- File upload/download, import/export/report endpoints, signed URL generators
- WebSocket endpoints (notifications, admin channels, commands)
- Background job endpoints (`/jobs/{id}`, `/tasks/{id}/result`)
- Mounted subapps (admin UI, storage browsers, metrics/health)
## Reconnaissance
**OpenAPI Mining**
```
GET /openapi.json
GET /docs
GET /redoc
GET /api/openapi.json
GET /internal/openapi.json
```
Extract: paths, parameters, securitySchemes, scopes, servers. Endpoints with `include_in_schema=False` won't appear—fuzz based on discovered prefixes and common admin/debug names.
**Dependency Mapping**
For each route, identify:
- Router-level dependencies (applied to all routes)
- Route-level dependencies (per endpoint)
- Which dependencies enforce auth vs just parse input
## Key Vulnerabilities
### Authentication & Authorization
**Dependency Injection Gaps**
- Routes missing security dependencies present on other routes
- `Depends` used instead of `Security` (ignores scope enforcement)
- Token presence treated as authentication without signature verification
- `OAuth2PasswordBearer` only yields a token string—verify routes don't treat presence as auth
**JWT Misuse**
- Decode without verify: test unsigned tokens, attacker-signed tokens
- Algorithm confusion: HS256/RS256 cross-use if not pinned
- `kid` header injection for custom key lookup paths
- Missing issuer/audience validation, cross-service token reuse
**Session Weaknesses**
- SessionMiddleware with weak `secret_key`
- Session fixation via predictable signing
- Cookie-based auth without CSRF protection
**OAuth/OIDC**
- Device/PKCE flows: verify strict PKCE S256 and state/nonce enforcement
### Access Control
**IDOR via Dependencies**
- Object IDs in path/query not validated against caller
- Tenant headers trusted without binding to authenticated user
- BackgroundTasks acting on IDs without re-validating ownership at execution time
- Export/import pipelines with IDOR and cross-tenant leaks
**Scope Bypass**
- Minimal scope satisfaction (any valid token accepted)
- Router vs route scope enforcement inconsistency
### Input Handling
**Pydantic Exploitation**
- Type coercion: strings to ints/bools, empty strings to None, truthiness edge cases
- Extra fields: `extra = "allow"` permits injecting control fields (role, ownerId, scope)
- Union types and `Annotated`: craft shapes hitting unintended validation branches
**Content-Type Switching**
```
application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data
```
Different content types hit different validators or code paths (parser differentials).
**Parameter Manipulation**
- Case variations in header/cookie names
- Duplicate parameters exploiting DI precedence
- Method override via `X-HTTP-Method-Override` (upstream respects, app doesn't)
### CORS & CSRF
**CORS Misconfiguration**
- Overly broad `allow_origin_regex`
- Origin reflection without validation
- Credentialed requests with permissive origins
- Verify preflight vs actual request deltas
**CSRF Exposure**
- No built-in CSRF in FastAPI/Starlette
- Cookie-based auth without origin validation
- Missing SameSite attribute
### Proxy & Host Trust
**Header Spoofing**
- ProxyHeadersMiddleware without network boundary: spoof `X-Forwarded-For/Proto` to influence auth/IP gating
- Absent TrustedHostMiddleware: Host header poisoning in password reset links, absolute URL generation
- Cache key confusion: missing Vary on Authorization/Cookie/Tenant
### Server-Side Vulnerabilities
**Template Injection (Jinja2)**
```python
{{7*7}} # Arithmetic confirmation
{{cycler.__init__.__globals__['os'].popen('id').read()}} # RCE
```
Check autoescape settings and custom filters/globals.
**SSRF**
- User-supplied URLs in imports, previews, webhooks validation
- Test: loopback, RFC1918, IPv6, redirects, DNS rebinding, header control
- Library behavior (httpx/requests): redirect policy, header forwarding, protocol support
- Protocol smuggling: `file://`, `ftp://`, gopher-like shims if custom clients
**File Upload**
- Path traversal in `UploadFile.filename` with control characters
- Missing storage root enforcement, symlink following
- Vary filename encodings, dot segments, NUL-like bytes
- Verify storage paths and served URLs
### WebSocket Security
- Missing per-connection authentication
- Cross-origin WebSocket without origin validation
- Topic/channel IDOR (subscribing to other users' channels)
- Authorization only at handshake, not per-message
### Mounted Apps
Sub-apps at `/admin`, `/static`, `/metrics` may bypass global middlewares. Verify auth enforcement parity across all mounts.
### Alternative Stacks
- If GraphQL (Strawberry/Graphene) is mounted: validate resolver-level authorization, IDOR on node/global IDs
- If SQLModel/SQLAlchemy present: probe for raw query usage and row-level authorization gaps
## Bypass Techniques
- Content-type switching to traverse alternate validators
- Parameter duplication and case variants exploiting DI precedence
- Method confusion via proxies (`X-HTTP-Method-Override`)
- Race windows around dependency-validated state transitions (issue token then mutate with parallel requests)
## Testing Methodology
1. **Enumerate** - Fetch OpenAPI, diff with 404-fuzzing for hidden endpoints
2. **Matrix testing** - Test each route across: unauth/user/admin × HTTP/WebSocket × JSON/form/multipart
3. **Dependency analysis** - Map which dependencies enforce auth vs parse input
4. **Cross-environment** - Compare dev/stage/prod for middleware and docs exposure differences
5. **Channel consistency** - Verify same authorization on HTTP and WebSocket for equivalent operations
## Validation Requirements
- Side-by-side requests showing unauthorized access (owner vs non-owner, cross-tenant)
- Cross-channel proof (HTTP and WebSocket for same rule)
- Header/proxy manipulation showing altered outcomes (Host/XFF/CORS)
- Minimal payloads for template injection, SSRF, token misuse with safe/OAST oracles
- Document exact dependency paths (router-level, route-level) that missed enforcement

View File

@@ -1,152 +0,0 @@
<nextjs_security_testing_guide>
<title>NEXT.JS — ADVERSARIAL TESTING PLAYBOOK</title>
<critical>Modern Next.js combines multiple execution contexts (Edge, Node, RSC, client) with smart caching (ISR/RSC fetch cache), middleware, and server actions. Authorization and cache boundaries must be enforced consistently across all paths or attackers will cross tenants, leak data, or invoke privileged actions.</critical>
<surface_map>
- Routers: App Router (`app/`) and Pages Router (`pages/`) coexist; test both
- Runtimes: Node.js vs Edge (V8 isolates with restricted APIs)
- Data paths: RSC (server components), Client components, Route Handlers (`app/api/**`), API routes (`pages/api/**`)
- Middleware: `middleware.ts`/`_middleware.ts`
- Rendering modes: SSR, SSG, ISR, on-demand revalidation, draft/preview mode
- Images: `next/image` optimization and remote loader
- Auth: NextAuth.js (callbacks, CSRF/state, callbackUrl), custom JWT/session bridges
- Server Actions: streamed POST with `Next-Action` header and action IDs
</surface_map>
<methodology>
1. Inventory routes (pages + app), static vs dynamic segments, and params. Map middleware coverage and runtime per path.
2. Capture baseline for each role (unauth, user, admin) across SSR, API routes, Route Handlers, Server Actions, and streaming data.
3. Diff responses while toggling runtime (Edge/Node), content-type, fetch cache directives, and preview/draft mode.
4. Probe caching and revalidation boundaries (ISR, RSC fetch, CDN) for cross-user/tenant leaks.
</methodology>
<high_value_targets>
- Middleware-protected routes (auth, geo, A/B)
- Admin/staff paths, draft/preview content, on-demand revalidate endpoints
- RSC payloads and flight data, streamed responses (server actions)
- Image optimizer and custom loaders, remotePatterns/domains
- NextAuth callbacks (`/api/auth/callback/*`), sign-in providers, CSRF/state handling
- Edge-only features (bot protection, IP gates) and their Node equivalents
</high_value_targets>
<advanced_techniques>
<route_enumeration>
- __BUILD_MANIFEST.sortedPages: Execute `console.log(__BUILD_MANIFEST.sortedPages.join('\n'))` in browser console to instantly reveal all registered routes (Pages Router and static App Router paths compiled at build time)
- __NEXT_DATA__: Inspect `<script id="__NEXT_DATA__">` for serverside props, pageProps, buildId, and dynamic route params on current page; reveals data flow and prop structure
- Source maps exposure: Check `/_next/static/` for exposed .map files revealing full route structure, server action IDs, API endpoints, and internal function names
- Client bundle mining: Search main-*.js and page chunks for route definitions; grep for 'pathname:', 'href:', '__next_route__', 'serverActions', and API endpoint strings
- Static chunk enumeration: Probe `/_next/static/chunks/pages/` and `/_next/static/chunks/app/` for build artifacts; filenames map directly to routes (e.g., `admin.js` → `/admin`)
- Build manifest fetch: GET `/_next/static/<buildId>/_buildManifest.js` and `/_next/static/<buildId>/_ssgManifest.js` for complete route and static generation metadata
- Sitemap/robots leakage: Check `/sitemap.xml`, `/robots.txt`, and `/sitemap-*.xml` for unintended exposure of admin/internal/preview paths
- Server action discovery: Inspect Network tab for POST requests with `Next-Action` header; extract action IDs from response streams and client hydration data
- Environment variable leakage: Execute `Object.keys(process.env).filter(k => k.startsWith('NEXT_PUBLIC_'))` in console to list public env vars; grep bundles for 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD' to find accidentally leaked credentials
</route_enumeration>
<middleware_bypass>
- Test for CVE-class middleware bypass via `x-middleware-subrequest` crafting and `x-nextjs-data` probing. Look for 307 + `x-middleware-rewrite`/`x-nextjs-redirect` headers and attempt bypass on protected routes.
- Attempt direct route access on Node vs Edge runtimes; confirm protection parity.
</middleware_bypass>
<server_actions>
- Capture streamed POSTs containing `Next-Action` headers. Map hashed action IDs via source maps or specialized tooling to discover hidden actions.
- Invoke actions out of UI flow and with alternate content-types; verify server-side authorization is enforced per action and not assumed from client state.
- Try cross-tenant/object references within action payloads to expose BOLA/IDOR via server actions.
</server_actions>
<rsc_and_cache>
- RSC fetch cache: probe `fetch` cache modes (force-cache, default, no-store) and revalidate tags/paths. Look for user-bound data cached without identity keys (ETag/Set-Cookie unaware).
- Confirm that personalized data is rendered via `no-store` or properly keyed; attempt cross-user content via shared caches/CDN.
- Inspect Flight data streams for serialized sensitive fields leaking through props.
</rsc_and_cache>
<isr_and_revalidation>
- Identify ISR pages (stale-while-revalidate). Check if responses may include user-bound fragments or tenant-dependent content.
- On-demand revalidation endpoints: look for weak secrets in URLs, referer-disclosed tokens, or unvalidated hosts triggering `revalidatePath`/`revalidateTag`.
- Attempt header-smuggling or method variations to trigger revalidation flows.
</isr_and_revalidation>
<draft_preview_mode>
- Draft/preview mode toggles via secret URLs/cookies; search for preview enable endpoints and secrets in client bundles/env leaks.
- Try setting preview cookies from subdomains, alternate paths, or through open redirects; observe content differences and persistence.
</draft_preview_mode>
<next_image_ssrf>
- Review `images.domains`/`remotePatterns` in `next.config.js`; test SSRF to internal hosts (IPv4/IPv6 variants, DNS rebinding) if patterns are broad.
- Custom loader functions may fetch with arbitrary URLs; test protocol smuggling and redirection chains.
- Attempt cache poisoning: craft same URL with different normalization to affect other users.
</next_image_ssrf>
<nextauth_pitfalls>
- State/nonce/PKCE: validate per-provider correctness; attempt missing/relaxed checks leading to login CSRF or token mix-up.
- Callback URL restrictions: open redirect in `callbackUrl` or mis-scoped allowed hosts; hijack sessions by forcing callbacks.
- JWT/session bridges: audience/issuer not enforced across API routes/Route Handlers; attempt cross-service token reuse.
</nextauth_pitfalls>
<edge_runtime_diffs>
- Edge runtime lacks certain Node APIs; defenses relying on Node-only modules may be skipped. Compare behavior of the same route in Edge vs Node.
- Header trust and IP determination can differ at the edge; test auth decisions tied to `x-forwarded-*` variance.
</edge_runtime_diffs>
<client_and_dom>
- Identify `dangerouslySetInnerHTML`, Markdown renderers, and user-controlled href/src attributes. Validate CSP/Trusted Types coverage for SSR/CSR/hydration.
- Attack hydration boundaries: server vs client render mismatches can enable gadget-based XSS.
</client_and_dom>
<data_fetching_over_exposure>
- getServerSideProps/getStaticProps leakage: Execute `JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps` in console to inspect all server-fetched data; look for sensitive fields (emails, tokens, internal IDs, full user objects) passed to client but not rendered in UI
- Over-fetched database queries: Check if pageProps include entire user records, relations, or admin-only fields when only username is displayed; common when using ORM select-all patterns
- API response pass-through: Verify if API responses are sanitized before passing to props; developers often forward entire responses including metadata, cursors, or debug info
- Environment-dependent data: Test if staging/dev accidentally exposes more fields in props than production due to inconsistent serialization logic
- Nested object inspection: Drill into nested props objects; look for `_metadata`, `_internal`, `__typename` (GraphQL), or framework-added fields containing sensitive context
</data_fetching_over_exposure>
</advanced_techniques>
<bypass_techniques>
- Content-type switching: `application/json` ↔ `multipart/form-data` ↔ `application/x-www-form-urlencoded` to traverse alternate code paths.
- Method override/tunneling: `_method`, `X-HTTP-Method-Override`, GET on endpoints unexpectedly accepting writes.
- Case/param aliasing and query duplication affecting middleware vs handler parsing.
- Cache key confusion at CDN/proxy (lack of Vary on auth cookies/headers) to leak personalized SSR/ISR content.
- API route path normalization: Test `/api/users` vs `/api/users/` vs `/api//users` vs `/api/./users`; middleware may normalize differently than route handlers, allowing protection bypass. Try double slashes, trailing slashes, and dot segments.
- Parameter pollution: Send duplicate query params (`?id=1&id=2`) or array notation (`?filter[]=a&filter[]=b`) to exploit parsing differences between middleware (which may check first value) and handler (which may use last or array).
</bypass_techniques>
<special_contexts>
<uploads_and_files>
- API routes and Route Handlers handling file uploads: check MIME sniffing, Content-Disposition, stored path traversal, and public serving of user files.
- Validate signing/scoping of any generated file URLs (short TTL, audience-bound).
</uploads_and_files>
<integrations_and_webhooks>
- Webhooks that trigger revalidation/imports: require HMAC verification; test with replay and cross-tenant object IDs.
- Analytics/AB testing flags controlled via cookies/headers; ensure they do not unlock privileged server paths.
</integrations_and_webhooks>
</special_contexts>
<validation>
1. Provide side-by-side requests for different principals showing cross-user/tenant content or actions.
2. Prove cache boundary failure (RSC/ISR/CDN) with response diffs or ETag collisions.
3. Demonstrate server action invocation outside UI with insufficient authorization checks.
4. Show middleware bypass (where applicable) with explicit headers and resulting protected content.
5. Include runtime parity checks (Edge vs Node) proving inconsistent enforcement.
6. For route enumeration: verify discovered routes return 200/403 (deployed) not 404 (build artifacts); test with authenticated vs unauthenticated requests.
7. For leaked credentials: test API keys with minimal read-only calls; filter placeholders (YOUR_API_KEY, demo-token); confirm keys match provider patterns (sk_live_*, pk_prod_*).
8. For __NEXT_DATA__ over-exposure: test cross-user (User A's props should not contain User B's PII); verify exposed fields are not in DOM; validate token validity with API calls.
9. For path normalization bypasses: show differential responses (403 vs 200 for path variants); redirects (307/308) don't count—only direct access bypasses matter.
</validation>
<pro_tips>
1. Enumerate with both App and Pages routers: many apps ship a hybrid surface.
2. Treat caching as an identity boundary—test with cookies stripped, altered, and with Vary/ETag diffs.
3. Decode client bundles for preview/revalidate secrets, action IDs, and hidden routes.
4. Use streaming-aware tooling to capture server actions and RSC payloads; diff flight data.
5. For NextAuth, fuzz provider params (state, nonce, scope, callbackUrl) and verify strictness.
6. Always retest under Edge and Node; misconfigurations often exist in only one runtime.
7. Probe `next/image` aggressively but safely—test IPv6/obscure encodings and redirect behavior.
8. Validate negative paths: other-user IDs, other-tenant headers/subdomains, lower roles.
9. Focus on export/report/download endpoints; they often bypass resolver-level checks.
10. Document minimal, reproducible PoCs; avoid noisy payloads—prefer precise diffs.
</pro_tips>
<remember>Next.js security breaks where identity, authorization, and caching diverge across routers, runtimes, and data paths. Bind subject, action, and object on every path, and key caches to identity and tenant explicitly.</remember>
</nextjs_security_testing_guide>

View File

@@ -0,0 +1,228 @@
---
name: nextjs
description: Security testing playbook for Next.js covering App Router, Server Actions, RSC, and Edge runtime vulnerabilities
---
# Next.js
Security testing for Next.js applications. Focus on authorization drift across runtimes (Edge/Node), caching boundaries, server actions, and middleware bypass.
## Attack Surface
**Routers**
- App Router (`app/`) and Pages Router (`pages/`) often coexist
- Route Handlers (`app/api/**`) and API routes (`pages/api/**`)
- Middleware: `middleware.ts` at project root
**Runtimes**
- Node.js (full API access)
- Edge (V8 isolates, restricted APIs)
**Rendering & Caching**
- SSR, SSG, ISR, on-demand revalidation
- RSC (React Server Components) with fetch cache
- Draft/preview mode
**Data Paths**
- Server Components, Client Components
- Server Actions (streamed POST with `Next-Action` header)
- `getServerSideProps`, `getStaticProps`
**Integrations**
- NextAuth.js (callbacks, CSRF, callbackUrl)
- `next/image` optimization and remote loaders
## High-Value Targets
- Middleware-protected routes (auth, geo, A/B)
- Admin/staff paths, draft/preview content, on-demand revalidate endpoints
- RSC payloads and flight data, streamed responses
- Image optimizer and custom loaders, remotePatterns/domains
- NextAuth callbacks (`/api/auth/callback/*`), sign-in providers
- Edge-only features (bot protection, IP gates) and their Node equivalents
## Reconnaissance
**Route Discovery**
```javascript
// Browser console - list all routes
console.log(__BUILD_MANIFEST.sortedPages.join('\n'))
// Inspect server-fetched data
JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps
// List public environment variables
Object.keys(process.env).filter(k => k.startsWith('NEXT_PUBLIC_'))
```
**Build Artifacts**
```
GET /_next/static/<buildId>/_buildManifest.js
GET /_next/static/<buildId>/_ssgManifest.js
GET /_next/static/chunks/pages/
GET /_next/static/chunks/app/
```
Chunk filenames map to routes (e.g., `admin.js``/admin`).
**Source Maps**
Check `/_next/static/` for exposed `.map` files revealing route structure, server action IDs, and internal functions.
**Client Bundle Mining**
Search main-*.js for: `pathname:`, `href:`, `__next_route__`, `serverActions`, API endpoints. Grep for `API_KEY`, `SECRET`, `TOKEN`, `PASSWORD` to find accidentally leaked credentials.
**Server Action Discovery**
Inspect Network tab for POST requests with `Next-Action` header. Extract action IDs from response streams and hydration data.
**Additional Leakage**
- `/sitemap.xml`, `/robots.txt`, `/sitemap-*.xml` for unintended admin/internal/preview paths
- Client bundles/env for secret paths and preview/admin flags (many teams hide routes via UI only)
## Key Vulnerabilities
### Middleware Bypass
**Known Techniques**
- `x-middleware-subrequest` header crafting (CVE-class bypass)
- `x-nextjs-data` probing
- Look for 307 + `x-middleware-rewrite`/`x-nextjs-redirect` headers
**Path Normalization**
```
/api/users
/api/users/
/api//users
/api/./users
```
Middleware may normalize differently than route handlers. Test double slashes, trailing slashes, dot segments.
**Parameter Pollution**
```
?id=1&id=2
?filter[]=a&filter[]=b
```
Middleware checks first value, handler uses last or array.
### Server Actions
- Invoke actions outside UI flow with alternate content-types
- Authorization assumed from client state rather than enforced server-side
- IDOR via object references in action payloads
- Map action IDs from source maps to discover hidden actions
### RSC & Caching
**Cache Boundary Failures**
- User-bound data cached without identity keys (ETag/Set-Cookie unaware)
- Personalized content served from shared cache/CDN
- Missing `no-store` on sensitive fetches
**Flight Data Leakage**
Inspect streamed RSC payloads for serialized sensitive fields in props.
**ISR Issues**
- Stale-while-revalidate responses containing user-specific or tenant-dependent data
- Weak secrets in on-demand revalidation endpoint URLs
- Referer-disclosed tokens or unvalidated hosts triggering `revalidatePath`/`revalidateTag`
- Header-smuggling or method variations to trigger revalidation
### Authentication
**NextAuth Pitfalls**
- Missing/relaxed state/nonce/PKCE per provider (login CSRF, token mix-up)
- Open redirect in `callbackUrl` or mis-scoped allowed hosts
- JWT audience/issuer not enforced across routes
- Cross-service token reuse
- Session hijacking by forcing callbacks
**Session Boundaries**
- Different auth enforcement between App Router and Pages Router
- API routes vs Route Handlers authorization inconsistency
### Data Exposure
**__NEXT_DATA__ Over-fetching**
Server-fetched data passed to client but not rendered:
- Full user objects when only username needed
- Internal IDs, tokens, admin-only fields
- ORM select-all patterns exposing entire records
- API responses forwarded without sanitization (metadata, cursors, debug info)
**Environment-Dependent Exposure**
- Staging/dev accidentally exposes more fields than production
- Inconsistent serialization logic across environments
**Props Inspection**
```javascript
// Check for sensitive data in page props
JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props
```
Look for `_metadata`, `_internal`, `__typename` (GraphQL), nested sensitive objects.
### Image Optimizer SSRF
**Remote Patterns**
- Broad `images.domains`/`remotePatterns` in `next.config.js`
- Test: internal hosts, IPv4/IPv6 variants, DNS rebinding
**Custom Loaders**
- Protocol smuggling via redirect chains
- Cache poisoning via URL normalization differences affecting other users
### Runtime Divergence
**Edge vs Node**
- Defenses relying on Node-only modules skipped on Edge
- Header trust differs (`x-forwarded-*` handling)
- Same route may behave differently across runtimes
### Client-Side
**XSS Vectors**
- `dangerouslySetInnerHTML`
- Markdown renderers
- User-controlled href/src attributes
- Validate CSP/Trusted Types coverage for SSR/CSR/hydration
**Hydration Mismatches**
Server vs client render differences can enable gadget-based XSS.
### Draft/Preview Mode
- Secret URLs/cookies enabling preview
- Preview secrets leaked in client bundles/env
- Setting preview cookies from subdomains or via open redirects
## Bypass Techniques
- Content-type switching: `application/json``multipart/form-data``application/x-www-form-urlencoded`
- Method override: `_method`, `X-HTTP-Method-Override`, GET on endpoints accepting writes
- Case/param aliasing and query duplication affecting middleware vs handler parsing
- Cache key confusion at CDN/proxy (lack of Vary on auth cookies/headers)
## Testing Methodology
1. **Enumerate** - Use `__BUILD_MANIFEST`, source maps, build artifacts, sitemap/robots to map all routes
2. **Runtime matrix** - Test each route under Edge and Node runtimes
3. **Role matrix** - Test as unauth/user/admin across SSR, API routes, Route Handlers, Server Actions
4. **Cache probing** - Verify caching respects identity (strip cookies, alter Vary headers, check ETags)
5. **Middleware validation** - Test path variants and header manipulation for bypass
6. **Cross-router** - Compare authorization between App Router and Pages Router paths
## Validation Requirements
- Side-by-side requests showing cross-user/tenant access
- Cache boundary failure proof (response diffs, ETag collisions)
- Server action invocation outside UI with insufficient auth
- Middleware bypass with explicit headers showing protected content access
- Runtime parity checks (Edge vs Node inconsistent enforcement)
- Discovered routes verified as deployed (200/403) not just build artifacts (404)
- Leaked credentials tested with minimal read-only calls; filter placeholders
- `__NEXT_DATA__` exposure: verify cross-user (User A's props shouldn't contain User B's PII), confirm exposed fields not in DOM
- Path normalization bypasses: show differential responses (403 vs 200), redirects don't count

View File

@@ -1,215 +0,0 @@
<graphql_protocol_guide>
<title>GRAPHQL — ADVANCED TESTING AND EXPLOITATION</title>
<critical>GraphQLs 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; dont 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>

View File

@@ -0,0 +1,276 @@
---
name: graphql
description: GraphQL security testing covering introspection, resolver injection, batching attacks, and authorization bypass
---
# GraphQL
Security testing for GraphQL APIs. Focus on resolver-level authorization, field/edge access control, batching abuse, and federation trust boundaries.
## Attack Surface
**Operations**
- Queries, mutations, subscriptions
- Persisted queries / Automatic Persisted Queries (APQ)
**Transports**
- HTTP POST/GET with `application/json` or `application/graphql`
- WebSocket: graphql-ws, graphql-transport-ws protocols
- Multipart for file uploads
**Schema Features**
- Introspection (`__schema`, `__type`)
- Directives: `@defer`, `@stream`, custom auth directives (@auth, @private)
- Custom scalars: Upload, JSON, DateTime
- Relay: global node IDs, connections/cursors, interfaces/unions
**Architecture**
- Federation (Apollo, GraphQL Mesh): `_service`, `_entities`
- Gateway vs subgraph authorization boundaries
## Reconnaissance
**Endpoint Discovery**
```
POST /graphql {"query":"{__typename}"}
POST /api/graphql {"query":"{__typename}"}
POST /v1/graphql {"query":"{__typename}"}
POST /gql {"query":"{__typename}"}
GET /graphql?query={__typename}
```
Check for GraphiQL/Playground exposure with credentials enabled (cross-origin with cookies can leak data via postMessage bridges).
**Schema Acquisition**
If introspection enabled:
```graphql
{__schema{types{name fields{name args{name}}}}}
```
If disabled, infer schema via:
- `__typename` probes on candidate fields
- Field suggestion errors (submit near-miss names to harvest suggestions)
- "Expected one of" errors revealing enum values
- Type coercion errors exposing field structure
- Error taxonomy: different codes for "unknown field" vs "unauthorized field" reveal existence
**Schema Mapping**
Map: root operations, object types, interfaces/unions, directives, custom scalars. Identify sensitive fields: email, tokens, roles, billing, API keys, admin flags, file URLs. Note cascade paths where child resolvers may skip auth under parent assumptions.
## Key Vulnerabilities
### Authorization Bypass
**Field-Level IDOR**
Test with aliases comparing owned vs foreign objects in single request:
```graphql
query {
own: order(id:"OWNED_ID") { id total owner { email } }
foreign: order(id:"FOREIGN_ID") { id total owner { email } }
}
```
**Edge/Child Resolver Gaps**
Parent resolver checks auth, child resolver assumes it's already validated:
```graphql
query {
user(id:"FOREIGN") {
id
privateData { secrets } # Child may skip auth check
}
}
```
**Relay Node Resolution**
Decode base64 global IDs, swap type/id pairs:
```graphql
query {
node(id:"VXNlcjoxMjM=") { ... on User { email } }
}
```
Ensure per-type authorization is enforced inside resolvers. Verify connection filters (owner/tenant) apply before pagination; cursor tampering should not cross ownership boundaries.
**Mutation Bypass**
- Probe mutations for partial updates bypassing validation (JSON Merge Patch semantics)
- Test mutations that accept extra fields passed to downstream logic
### Batching & Alias Abuse
**Enumeration via Aliases**
```graphql
query {
u1:user(id:"1"){email}
u2:user(id:"2"){email}
u3:user(id:"3"){email}
}
```
Bypasses per-request rate limits; exposes per-field vs per-request auth inconsistencies.
**Array Batching**
If supported (non-standard), submit multiple operations to achieve partial failures and bypass limits.
### Input Manipulation
**Type Confusion**
```
{id: 123} vs {id: "123"}
{id: [123]} vs {id: null}
{id: 0} vs {id: -1}
```
**Duplicate Keys**
```json
{"id": 1, "id": 2}
```
Parser precedence varies; may bypass validation. Also test default argument values.
**Extra Fields**
Send unexpected keys in input objects; backends may pass them to resolvers or downstream logic.
### Cursor Manipulation
Decode cursors (usually base64) to:
- Manipulate offsets/IDs
- Skip filters
- Cross ownership boundaries
### Directive Abuse
**@defer/@stream**
```graphql
query {
me { id }
... @defer { adminPanel { secrets } }
}
```
May return gated data in incremental delivery. Confirm server supports incremental delivery.
**Custom Directives**
@auth, @private and similar directives often annotate intent but do not enforce—verify actual checks in each resolver path.
### Complexity Attacks
**Fragment Bombs**
```graphql
fragment x on User { friends { ...x } }
query { me { ...x } }
```
Test depth/complexity limits, query cost analyzers, timeouts.
**Wide Selection Sets**
Abuse selection sets and fragments to force overfetching of sensitive subfields.
### Federation Exploitation
**SDL Exposure**
```graphql
query { _service { sdl } }
```
**Entity Materialization**
```graphql
query {
_entities(representations:[
{__typename:"User", id:"TARGET_ID"}
]) { ... on User { email roles } }
}
```
Gateway may enforce auth; subgraph resolvers may not. Look for cross-subgraph IDOR via inconsistent ownership checks.
### Subscription Security
- Authorization at handshake only, not per-message
- Subscribe to other users' channels via filter args
- Cross-tenant event leakage
- Abuse filter args in subscription resolvers to reference foreign IDs
### Persisted Query Abuse
- APQ hashes leaked from client bundles
- Replay privileged operations with attacker variables
- Hash bruteforce for common operations
- Validate hash→operation mapping enforces principal and operation allowlists
### CORS & CSRF
- Cookie-auth with GET queries enables CSRF on mutations via query parameters
- GraphiQL/Playground cross-origin with credentials leaks data
- Missing SameSite and origin validation
### File Uploads
GraphQL multipart spec:
- Multiple Upload scalars
- Filename/path traversal tricks
- Unexpected content-types, oversize chunks
- Server-side ownership/scoping for returned URLs
## WAF Evasion
**Query Reshaping**
- Comments and block strings (`"""..."""`)
- Unicode escapes
- Alias/fragment indirection
- JSON variables vs inline args
- GET vs POST vs `application/graphql`
**Fragment Splitting**
Split fields across fragments and inline spreads to avoid naive signatures:
```graphql
fragment a on User { email }
fragment b on User { password }
query { me { ...a ...b } }
```
## Bypass Techniques
**Transport Switching**
```
Content-Type: application/json
Content-Type: application/graphql
Content-Type: multipart/form-data
GET with query params
```
**Timing & Rate Limits**
- HTTP/2 multiplexing and connection reuse to widen timing windows
- Batching to bypass rate limits
**Naming Tricks**
- Case/underscore variations
- Unicode homoglyphs (server-dependent)
- Aliases masking sensitive field names
**Cache Confusion**
- CDN caching without Vary on Authorization
- Variable manipulation affecting cache keys
- Redirects and 304/206 behaviors leaking partial responses
## Testing Methodology
1. **Fingerprint** - Identify endpoints, transports, stack (Apollo, Hasura, etc.), GraphiQL exposure
2. **Schema mapping** - Introspection or inference to build complete type graph
3. **Principal matrix** - Collect tokens for unauth, user, premium, admin roles with at least one valid object ID per subject
4. **Field sweep** - Test each resolver with owned vs foreign IDs via aliases in same request
5. **Transport parity** - Verify same auth on HTTP, WebSocket, persisted queries
6. **Federation probe** - Test `_service` and `_entities` for subgraph auth gaps
7. **Edge cases** - Cursors, @defer/@stream, subscriptions, file uploads
## Validation Requirements
- Paired requests (owner vs non-owner) showing unauthorized access
- Resolver-level bypass: parent checks present, child field exposes data
- Transport parity proof: HTTP and WebSocket for same operation
- Federation bypass: `_entities` accessing data without subgraph auth
- Minimal payloads with exact selection sets and variable shapes
- Document exact resolver paths that missed enforcement

View File

@@ -1,145 +0,0 @@
<scan_mode>
DEEP SCAN MODE - Exhaustive Security Assessment
This mode is for thorough security reviews where finding vulnerabilities is critical.
PHASE 1: EXHAUSTIVE RECONNAISSANCE AND MAPPING
Spend significant effort understanding the target before exploitation.
For whitebox (source code available):
- Map EVERY file, module, and code path in the repository
- Trace all entry points from HTTP handlers to database queries
- Identify all authentication mechanisms and their implementations
- Map all authorization checks and understand the access control model
- Identify all external service integrations and API calls
- Analyze all configuration files for secrets and misconfigurations
- Review all database schemas and understand data relationships
- Map all background jobs, cron tasks, and async processing
- Identify all serialization/deserialization points
- Review all file handling operations (upload, download, processing)
- Understand the deployment model and infrastructure assumptions
- Check all dependency versions against known CVE databases
For blackbox (no source code):
- Exhaustive subdomain enumeration using multiple sources and tools
- Full port scanning to identify all services
- Complete content discovery with multiple wordlists
- Technology fingerprinting on all discovered assets
- API endpoint discovery through documentation, JavaScript analysis, and fuzzing
- Identify all parameters including hidden and rarely-used ones
- Map all user roles by testing with different account types
- Understand rate limiting, WAF rules, and security controls in place
- Document the complete application architecture as understood from outside
EXECUTION STRATEGY - HIERARCHICAL AGENT SWARM:
After Phase 1 (Recon & Mapping) is complete:
1. Divide the application into major components/parts (e.g., Auth System, Payment Gateway, User Profile, Admin Panel)
2. Spawn a specialized subagent for EACH major component
3. Each component agent must then:
- Further subdivide its scope into subparts (e.g., Login Form, Registration API, Password Reset)
- Spawn sub-subagents for each distinct subpart
4. At the lowest level (specific functionality), spawn specialized agents for EACH potential vulnerability type:
- "Auth System" → "Login Form" → "SQLi Agent", "XSS Agent", "Auth Bypass Agent"
- This creates a massive parallel swarm covering every angle
- Do NOT overload a single agent with multiple vulnerability types
- Scale horizontally to maximum capacity
PHASE 2: DEEP BUSINESS LOGIC ANALYSIS
Understand the application deeply enough to find logic flaws:
- CREATE A FULL STORYBOARD of all user flows and state transitions
- Document every step of the business logic in a structured flow diagram
- Use the application extensively as every type of user to map the full lifecycle of data
- Document all state machines and workflows (e.g. Order Created -> Paid -> Shipped)
- Identify trust boundaries between components
- Map all integrations with third-party services
- Understand what invariants the application tries to maintain
- Identify all points where roles, privileges, or sensitive data changes hands
- Look for implicit assumptions in the business logic
- Consider multi-step attacks that abuse normal functionality
PHASE 3: COMPREHENSIVE ATTACK SURFACE TESTING
Test EVERY input vector with EVERY applicable technique.
Input Handling - Test all parameters, headers, cookies with:
- Multiple injection payloads (SQL, NoSQL, LDAP, XPath, Command, Template)
- Various encodings and bypass techniques (double encoding, unicode, null bytes)
- Boundary conditions and type confusion
- Large payloads and buffer-related issues
Authentication and Session:
- Exhaustive brute force protection testing
- Session fixation, hijacking, and prediction attacks
- JWT/token manipulation if applicable
- OAuth flow abuse scenarios
- Password reset flow vulnerabilities (token leakage, reuse, timing)
- Multi-factor authentication bypass techniques
- Account enumeration through all possible channels
Access Control:
- Test EVERY endpoint for horizontal and vertical access control
- Parameter tampering on all object references
- Forced browsing to all discovered resources
- HTTP method tampering
- Test access control after session changes (logout, role change)
File Operations:
- Exhaustive file upload bypass testing (extension, content-type, magic bytes)
- Path traversal on all file parameters
- Server-side request forgery through file inclusion
- XXE through all XML parsing points
Business Logic:
- Race conditions on all state-changing operations
- Workflow bypass attempts on every multi-step process
- Price/quantity manipulation in all transactions
- Parallel execution attacks
- Time-of-check to time-of-use vulnerabilities
Advanced Attacks:
- HTTP request smuggling if multiple proxies/servers
- Cache poisoning and cache deception
- Subdomain takeover on all subdomains
- Prototype pollution in JavaScript applications
- CORS misconfiguration exploitation
- WebSocket security testing
- GraphQL specific attacks if applicable
PHASE 4: VULNERABILITY CHAINING
Don't just find individual bugs - chain them:
- Combine information disclosure with access control bypass
- Chain SSRF to access internal services
- Use low-severity findings to enable high-impact attacks
- Look for multi-step attack paths that automated tools miss
- Consider attacks that span multiple application components
CHAINING PRINCIPLES (MAX IMPACT):
- Treat every finding as a pivot: ask "What does this unlock next?" until you reach maximum privilege / maximum data exposure / maximum control
- Prefer end-to-end exploit paths over isolated bugs: initial foothold → pivot → privilege gain → sensitive action/data
- Cross boundaries deliberately: user → admin, external → internal, unauthenticated → authenticated, read → write, single-tenant → cross-tenant
- Validate chains by executing the full sequence using the available tools (proxy + browser for workflows, python for automation, terminal for supporting commands)
- When a component agent finds a potential pivot, it must message/spawn the next focused agent to continue the chain in the next component/subpart
PHASE 5: PERSISTENT TESTING
If initial attempts fail, don't give up:
- Research specific technologies for known bypasses
- Try alternative exploitation techniques
- Look for edge cases and unusual functionality
- Test with different client contexts
- Revisit previously tested areas with new information
- Consider timing-based and blind exploitation techniques
PHASE 6: THOROUGH REPORTING
- Document EVERY confirmed vulnerability with full details
- Include all severity levels - even low findings may enable chains
- Provide complete reproduction steps and PoC
- Document remediation recommendations
- Note areas requiring additional review beyond current scope
MINDSET:
- Relentless - this is about finding what others miss
- Creative - think of unconventional attack vectors
- Patient - real vulnerabilities often require deep investigation
- Thorough - test every parameter, every endpoint, every edge case
- Persistent - if one approach fails, try ten more
- Holistic - understand how components interact to find systemic issues
</scan_mode>

View File

@@ -0,0 +1,157 @@
---
name: deep
description: Exhaustive security assessment with maximum coverage, depth, and vulnerability chaining
---
# Deep Testing Mode
Exhaustive security assessment. Maximum coverage, maximum depth. Finding what others miss is the goal.
## Approach
Thorough understanding before exploitation. Test every parameter, every endpoint, every edge case. Chain findings for maximum impact.
## Phase 1: Exhaustive Reconnaissance
**Whitebox (source available)**
- Map every file, module, and code path in the repository
- Trace all entry points from HTTP handlers to database queries
- Document all authentication mechanisms and implementations
- Map authorization checks and access control model
- Identify all external service integrations and API calls
- Analyze configuration for secrets and misconfigurations
- Review database schemas and data relationships
- Map background jobs, cron tasks, async processing
- Identify all serialization/deserialization points
- Review file handling: upload, download, processing
- Understand the deployment model and infrastructure assumptions
- Check all dependency versions against CVE databases
**Blackbox (no source)**
- Exhaustive subdomain enumeration with multiple sources and tools
- Full port scanning across all services
- Complete content discovery with multiple wordlists
- Technology fingerprinting on all assets
- API discovery via docs, JavaScript analysis, fuzzing
- Identify all parameters including hidden and rarely-used ones
- Map all user roles with different account types
- Document rate limiting, WAF rules, security controls
- Document complete application architecture as understood from outside
## Phase 2: Business Logic Deep Dive
Create a complete storyboard of the application:
- **User flows** - document every step of every workflow
- **State machines** - map all transitions (Created → Paid → Shipped → Delivered)
- **Trust boundaries** - identify where privilege changes hands
- **Invariants** - what rules should the application always enforce
- **Implicit assumptions** - what does the code assume that might be violated
- **Multi-step attack surfaces** - where can normal functionality be abused
- **Third-party integrations** - map all external service dependencies
Use the application extensively as every user type to understand the full data lifecycle.
## Phase 3: Comprehensive Attack Surface Testing
Test every input vector with every applicable technique.
**Input Handling**
- Multiple injection types: SQL, NoSQL, LDAP, XPath, command, template
- Encoding bypasses: double encoding, unicode, null bytes
- Boundary conditions and type confusion
- Large payloads and buffer-related issues
**Authentication & Session**
- Exhaustive brute force protection testing
- Session fixation, hijacking, prediction
- JWT/token manipulation
- OAuth flow abuse scenarios
- Password reset vulnerabilities: token leakage, reuse, timing
- MFA bypass techniques
- Account enumeration through all channels
**Access Control**
- Test every endpoint for horizontal and vertical access control
- Parameter tampering on all object references
- Forced browsing to all discovered resources
- HTTP method tampering (GET vs POST vs PUT vs DELETE)
- Access control after session state changes (logout, role change)
**File Operations**
- Exhaustive file upload bypass: extension, content-type, magic bytes
- Path traversal on all file parameters
- SSRF through file inclusion
- XXE through all XML parsing points
**Business Logic**
- Race conditions on all state-changing operations
- Workflow bypass on every multi-step process
- Price/quantity manipulation in transactions
- Parallel execution attacks
- TOCTOU (time-of-check to time-of-use) vulnerabilities
**Advanced Techniques**
- HTTP request smuggling (multiple proxies/servers)
- Cache poisoning and cache deception
- Subdomain takeover
- Prototype pollution (JavaScript applications)
- CORS misconfiguration exploitation
- WebSocket security testing
- GraphQL-specific attacks (introspection, batching, nested queries)
## Phase 4: Vulnerability Chaining
Individual bugs are starting points. Chain them for maximum impact:
- Combine information disclosure with access control bypass
- Chain SSRF to reach internal services
- Use low-severity findings to enable high-impact attacks
- Build multi-step attack paths that automated tools miss
- Cross component boundaries: user → admin, external → internal, read → write, single-tenant → cross-tenant
**Chaining Principles**
- Treat every finding as a pivot point: ask "what does this unlock next?"
- Continue until reaching maximum privilege / maximum data exposure / maximum control
- Prefer end-to-end exploit paths over isolated bugs: initial foothold → pivot → privilege gain → sensitive action/data
- Validate chains by executing the full sequence (proxy + browser for workflows, python for automation)
- When a pivot is found, spawn focused agents to continue the chain in the next component
## Phase 5: Persistent Testing
When initial attempts fail:
- Research technology-specific bypasses
- Try alternative exploitation techniques
- Test edge cases and unusual functionality
- Test with different client contexts
- Revisit areas with new information from other findings
- Consider timing-based and blind exploitation
- Look for logic flaws that require deep application understanding
## Phase 6: Comprehensive Reporting
- Document every confirmed vulnerability with full details
- Include all severity levels—low findings may enable chains
- Complete reproduction steps and working PoC
- Remediation recommendations with specific guidance
- Note areas requiring additional review beyond current scope
## Agent Strategy
After reconnaissance, decompose the application hierarchically:
1. **Component level** - Auth System, Payment Gateway, User Profile, Admin Panel
2. **Feature level** - Login Form, Registration API, Password Reset
3. **Vulnerability level** - SQLi Agent, XSS Agent, Auth Bypass Agent
Spawn specialized agents at each level. Scale horizontally to maximum parallelization:
- Do NOT overload a single agent with multiple vulnerability types
- Each agent focuses on one specific area or vulnerability type
- Creates a massive parallel swarm covering every angle
## Mindset
Relentless. Creative. Patient. Thorough. Persistent.
This is about finding what others miss. Test every parameter, every endpoint, every edge case. If one approach fails, try ten more. Understand how components interact to find systemic issues.

View File

@@ -1,63 +0,0 @@
<scan_mode>
QUICK SCAN MODE - Rapid Security Assessment
This mode is optimized for fast feedback. Focus on HIGH-IMPACT vulnerabilities with minimal overhead.
PHASE 1: RAPID ORIENTATION
- If source code is available: Focus primarily on RECENT CHANGES (git diff, new commits, modified files)
- Identify the most critical entry points: authentication endpoints, payment flows, admin interfaces, API endpoints handling sensitive data
- Quickly understand the tech stack and frameworks in use
- Skip exhaustive reconnaissance - use what's immediately visible
PHASE 2: TARGETED ATTACK SURFACE
For whitebox (source code available):
- Prioritize files changed in recent commits/PRs - these are most likely to contain fresh bugs
- Look for security-sensitive patterns in diffs: auth checks, input handling, database queries, file operations
- Trace user-controllable input in changed code paths
- Check if security controls were modified or bypassed
For blackbox (no source code):
- Focus on authentication and session management
- Test the most critical user flows only
- Check for obvious misconfigurations and exposed endpoints
- Skip deep content discovery - test what's immediately accessible
PHASE 3: HIGH-IMPACT VULNERABILITY FOCUS
Prioritize in this order:
1. Authentication bypass and broken access control
2. Remote code execution vectors
3. SQL injection in critical endpoints
4. Insecure direct object references (IDOR) in sensitive resources
5. Server-side request forgery (SSRF)
6. Hardcoded credentials or secrets in code
Skip lower-priority items:
- Extensive subdomain enumeration
- Full directory bruteforcing
- Information disclosure that doesn't lead to exploitation
- Theoretical vulnerabilities without PoC
PHASE 4: VALIDATION AND REPORTING
- Validate only critical/high severity findings with minimal PoC
- Report findings as you discover them - don't wait for completion
- Focus on exploitability and business impact
QUICK CHAINING RULE:
- If you find ANY strong primitive (auth weakness, access control gap, injection point, internal reachability), immediately attempt a single high-impact pivot to demonstrate real impact
- Do not stop at a low-context “maybe”; turn it into a concrete exploit sequence (even if short) that reaches privileged action or sensitive data
OPERATIONAL GUIDELINES:
- Use the browser tool for quick manual testing of critical flows
- Use terminal for targeted scans with fast presets (e.g., nuclei with critical/high templates only)
- Use proxy to inspect traffic on key endpoints
- Skip extensive fuzzing - use targeted payloads only
- Create subagents only for parallel high-priority tasks
- If whitebox: file_edit tool to review specific suspicious code sections
- Use notes tool to track critical findings only
MINDSET:
- Think like a time-boxed bug bounty hunter going for quick wins
- Prioritize breadth over depth on critical areas
- If something looks exploitable, validate quickly and move on
- Don't get stuck - if an attack vector isn't yielding results quickly, pivot
</scan_mode>

View File

@@ -0,0 +1,64 @@
---
name: quick
description: Time-boxed rapid assessment targeting high-impact vulnerabilities
---
# Quick Testing Mode
Time-boxed assessment focused on high-impact vulnerabilities. Prioritize breadth over depth.
## Approach
Optimize for fast feedback on critical security issues. Skip exhaustive enumeration in favor of targeted testing on high-value attack surfaces.
## Phase 1: Rapid Orientation
**Whitebox (source available)**
- Focus on recent changes: git diffs, new commits, modified files—these are most likely to contain fresh bugs
- Identify security-sensitive patterns in changed code: auth checks, input handling, database queries, file operations
- Trace user input through modified code paths
- Check if security controls were modified or bypassed
**Blackbox (no source)**
- Map authentication and critical user flows
- Identify exposed endpoints and entry points
- Skip deep content discovery—test what's immediately accessible
## Phase 2: High-Impact Targets
Test in priority order:
1. **Authentication bypass** - login flaws, session issues, token weaknesses
2. **Broken access control** - IDOR, privilege escalation, missing authorization
3. **Remote code execution** - command injection, deserialization, SSTI
4. **SQL injection** - authentication endpoints, search, filters
5. **SSRF** - URL parameters, webhooks, integrations
6. **Exposed secrets** - hardcoded credentials, API keys, config files
Skip for quick scans:
- Exhaustive subdomain enumeration
- Full directory bruteforcing
- Low-severity information disclosure
- Theoretical issues without working PoC
## Phase 3: Validation
- Confirm exploitability with minimal proof-of-concept
- Demonstrate real impact, not theoretical risk
- Report findings immediately as discovered
## Chaining
When a strong primitive is found (auth weakness, injection point, internal access), immediately attempt one high-impact pivot to demonstrate maximum severity. Don't stop at a low-context "maybe"—turn it into a concrete exploit sequence that reaches privileged action or sensitive data.
## Operational Guidelines
- Use browser tool for quick manual testing of critical flows
- Use terminal for targeted scans with fast presets (e.g., nuclei with critical/high templates only)
- Use proxy to inspect traffic on key endpoints
- Skip extensive fuzzing—use targeted payloads only
- Create subagents only for parallel high-priority tasks
## Mindset
Think like a time-boxed bug bounty hunter going for quick wins. Prioritize breadth over depth on critical areas. If something looks exploitable, validate quickly and move on. Don't get stuck—if an attack vector isn't yielding results quickly, pivot.

View File

@@ -1,91 +0,0 @@
<scan_mode>
STANDARD SCAN MODE - Balanced Security Assessment
This mode provides thorough coverage with a structured methodology. Balance depth with efficiency.
PHASE 1: RECONNAISSANCE AND MAPPING
Understanding the target is critical before exploitation. Never skip this phase.
For whitebox (source code available):
- Map the entire codebase structure: directories, modules, entry points
- Identify the application architecture (MVC, microservices, monolith)
- Understand the routing: how URLs map to handlers/controllers
- Identify all user input vectors: forms, APIs, file uploads, headers, cookies
- Map authentication and authorization flows
- Identify database interactions and ORM usage
- Review dependency manifests for known vulnerable packages
- Understand the data model and sensitive data locations
For blackbox (no source code):
- Crawl the application thoroughly using browser tool - interact with every feature
- Enumerate all endpoints, parameters, and functionality
- Identify the technology stack through fingerprinting
- Map user roles and access levels
- Understand the business logic by using the application as intended
- Document all forms, APIs, and data entry points
- Use proxy tool to capture and analyze all traffic during exploration
PHASE 2: BUSINESS LOGIC UNDERSTANDING
Before testing for vulnerabilities, understand what the application DOES:
- What are the critical business flows? (payments, user registration, data access)
- What actions should be restricted to specific roles?
- What data should users NOT be able to access?
- What state transitions exist? (order pending → paid → shipped)
- Where does money, sensitive data, or privilege flow?
PHASE 3: SYSTEMATIC VULNERABILITY ASSESSMENT
Test each attack surface methodically. Create focused subagents for different areas.
Entry Point Analysis:
- Test all input fields for injection vulnerabilities
- Check all API endpoints for authentication and authorization
- Verify all file upload functionality for bypass
- Test all search and filter functionality
- Check redirect parameters and URL handling
Authentication and Session:
- Test login for brute force protection
- Check session token entropy and handling
- Test password reset flows for weaknesses
- Verify logout invalidates sessions
- Test for authentication bypass techniques
Access Control:
- For every privileged action, test as unprivileged user
- Test horizontal access control (user A accessing user B's data)
- Test vertical access control (user escalating to admin)
- Check API endpoints mirror UI access controls
- Test direct object references with different user contexts
Business Logic:
- Attempt to skip steps in multi-step processes
- Test for race conditions in critical operations
- Try negative values, zero values, boundary conditions
- Attempt to replay transactions
- Test for price manipulation in e-commerce flows
PHASE 4: EXPLOITATION AND VALIDATION
- Every finding must have a working proof-of-concept
- Demonstrate actual impact, not theoretical risk
- Chain vulnerabilities when possible to show maximum impact
- Document the full attack path from initial access to impact
- Use python tool for complex exploit development
CHAINING & MAX IMPACT MINDSET:
- Always ask: "If I can do X, what does that enable me to do next?" Keep pivoting until you reach maximum privilege or maximum sensitive data access
- Prefer complete end-to-end paths (entry point → pivot → privileged action/data) over isolated bug reports
- Use the application as a real user would: exploit must survive the actual workflow and state transitions
- When you discover a useful pivot (info leak, weak boundary, partial access), immediately pursue the next step rather than stopping at the first win
PHASE 5: COMPREHENSIVE REPORTING
- Report all confirmed vulnerabilities with clear reproduction steps
- Include severity based on actual exploitability and business impact
- Provide remediation recommendations
- Document any areas that need further investigation
MINDSET:
- Methodical and systematic - cover the full attack surface
- Document as you go - findings and areas tested
- Validate everything - no assumptions about exploitability
- Think about business impact, not just technical severity
</scan_mode>

View File

@@ -0,0 +1,96 @@
---
name: standard
description: Balanced security assessment with systematic methodology and full attack surface coverage
---
# Standard Testing Mode
Balanced security assessment with structured methodology. Thorough coverage without exhaustive depth.
## Approach
Systematic testing across the full attack surface. Understand the application before exploiting it.
## Phase 1: Reconnaissance
**Whitebox (source available)**
- Map codebase structure: modules, entry points, routing
- Identify architecture pattern (MVC, microservices, monolith)
- Trace input vectors: forms, APIs, file uploads, headers, cookies
- Review authentication and authorization flows
- Analyze database interactions and ORM usage
- Check dependencies for known CVEs
- Understand the data model and sensitive data locations
**Blackbox (no source)**
- Crawl application thoroughly, interact with every feature
- Enumerate endpoints, parameters, and functionality
- Fingerprint technology stack
- Map user roles and access levels
- Capture traffic with proxy to understand request/response patterns
## Phase 2: Business Logic Analysis
Before testing for vulnerabilities, understand the application:
- **Critical flows** - payments, registration, data access, admin functions
- **Role boundaries** - what actions are restricted to which users
- **Data access rules** - what data should be isolated between users
- **State transitions** - order lifecycle, account status changes
- **Trust boundaries** - where does privilege or sensitive data flow
## Phase 3: Systematic Testing
Test each attack surface methodically. Spawn focused subagents for different areas.
**Input Validation**
- Injection testing on all input fields (SQL, XSS, command, template)
- File upload bypass attempts
- Search and filter parameter manipulation
- Redirect and URL parameter handling
**Authentication & Session**
- Brute force protection
- Session token entropy and handling
- Password reset flow analysis
- Logout session invalidation
- Authentication bypass techniques
**Access Control**
- Horizontal: user A accessing user B's resources
- Vertical: unprivileged user accessing admin functions
- API endpoints vs UI access control consistency
- Direct object reference manipulation
**Business Logic**
- Multi-step process bypass (skip steps, reorder)
- Race conditions on state-changing operations
- Boundary conditions: negative values, zero, extremes
- Transaction replay and manipulation
## Phase 4: Exploitation
- Every finding requires a working proof-of-concept
- Demonstrate actual impact, not theoretical risk
- Chain vulnerabilities to show maximum severity
- Document full attack path from entry to impact
- Use python tool for complex exploit development
## Phase 5: Reporting
- Document all confirmed vulnerabilities with reproduction steps
- Severity based on exploitability and business impact
- Remediation recommendations
- Note areas requiring further investigation
## Chaining
Always ask: "If I can do X, what does that enable next?" Keep pivoting until reaching maximum privilege or data exposure.
Prefer complete end-to-end paths (entry point → pivot → privileged action/data) over isolated findings. Use the application as a real user would—exploit must survive actual workflow and state transitions.
When you discover a useful pivot (info leak, weak boundary, partial access), immediately pursue the next step rather than stopping at the first win.
## Mindset
Methodical and systematic. Document as you go. Validate everything—no assumptions about exploitability. Think about business impact, not just technical severity.

View File

@@ -1,177 +0,0 @@
<firebase_firestore_security_guide>
<title>FIREBASE / FIRESTORE — ADVERSARIAL TESTING AND EXPLOITATION</title>
<critical>Most impactful findings in Firebase apps arise from weak Firestore/Realtime Database rules, Cloud Storage exposure, callable/onRequest Functions trusting client input, incorrect ID token validation, and over-trusted App Check. Treat every client-supplied field and token as untrusted. Bind subject/tenant on the server, not in the client.</critical>
<scope>
- Firestore (documents/collections, rules, REST/SDK)
- Realtime Database (JSON tree, rules)
- Cloud Storage (rules, signed URLs)
- Auth (ID tokens, custom claims, anonymous/sign-in providers)
- Cloud Functions (onCall/onRequest, triggers)
- Hosting rewrites, CDN/caching, CORS
- App Check (attestation) and its limits
</scope>
<methodology>
1. Extract project config from client (apiKey, authDomain, projectId, appId, storageBucket, messagingSenderId). Identify all used Firebase products.
2. Obtain multiple principals: unauth, anonymous (if enabled), basic user A, user B, and any staff/admin if available. Capture their ID tokens.
3. Build Resource × Action × Principal matrix across Firestore/Realtime/Storage/Functions. Exercise every action via SDK and raw REST (googleapis) to detect parity gaps.
4. Start from list/query paths (where allowed) to seed IDs; then swap document paths, tenants, and user IDs across principals and transports.
</methodology>
<architecture>
- Firestore REST: https://firestore.googleapis.com/v1/projects/<project>/databases/(default)/documents/<path>
- Storage REST: https://storage.googleapis.com/storage/v1/b/<bucket>
- Auth: Google-signed ID tokens (iss accounts.google.com/securetoken.google.com/<project>), aud <project/app-id>; identity is in sub/uid.
- Rules engines: separate for Firestore, Realtime DB, and Storage; Functions bypass rules when using Admin SDK.
</architecture>
<auth_and_tokens>
- ID token verification must enforce issuer, audience (project), signature (Google JWKS), expiration, and optionally App Check binding when used.
- Custom claims are appended by Admin SDK; client-supplied claims are ignored by Auth but may be trusted by app code if copied into docs.
- Pitfalls:
- Accepting any JWT with valid signature but wrong audience/project.
- Trusting uid/account IDs from request body instead of context.auth.uid in Functions.
- Mixing session cookies and ID tokens without verifying both paths equivalently.
- Tests:
- Replay tokens across environments/projects; expect strict aud/iss rejection server-side.
- Call Functions with and without Authorization; verify identical checks on both onCall and onRequest variants.
</auth_and_tokens>
<firestore_rules>
- Rules are not filters: a query must include constraints that make the rule true for all returned documents; otherwise reads fail. Do not rely on client to include where clauses correctly.
- Prefer ownership derived from request.auth.uid and server data, not from client payload fields.
- Common gaps:
- allow read: if request.auth != null (any user reads all data)
- allow write: if request.auth != null (mass write)
- Missing per-field validation (adds isAdmin/role/tenantId fields).
- Using client-supplied ownerId/orgId instead of enforcing doc.ownerId == request.auth.uid or membership in org.
- Over-broad list rules on root collections; per-doc checks exist but list still leaks via queries.
- Validation patterns:
- Restrict writes: request.resource.data.keys().hasOnly([...]) and forbid privilege fields.
- Enforce ownership: resource.data.ownerId == request.auth.uid && request.resource.data.ownerId == request.auth.uid
- Org membership: exists(/databases/(default)/documents/orgs/$(org)/members/$(request.auth.uid))
- Tests:
- Compare results for users A/B on identical queries; diff counts and IDs.
- Attempt cross-tenant reads: where orgId == otherOrg; try queries without org filter to confirm denial.
- Write-path: set/patch with foreign ownerId/orgId; attempt to flip privilege flags.
</firestore_rules>
<firestore_queries>
- Enumerate via REST to avoid SDK client-side constraints; try structured and REST filters.
- Probe composite index requirements: UI-driven queries may hide missing rule coverage when indexes are enabled but rules are broad.
- Explore collection group queries (collectionGroup) that may bypass per-collection rules if not mirrored.
- Use startAt/endAt/in/array-contains to probe rule edges and pagination cursors for cross-tenant bleed.
</firestore_queries>
<realtime_database>
- Misconfigured rules frequently expose entire JSON trees. Probe https://<project>.firebaseio.com/.json with and without auth.
- Confirm rules for read/write use auth.uid and granular path checks; avoid .read/.write: true or auth != null at high-level nodes.
- Attempt to write privilege-bearing nodes (roles, org membership) and observe downstream effects (e.g., Cloud Functions triggers).
</realtime_database>
<cloud_storage>
- Rules parallel Firestore but apply to object paths. Common issues:
- Public reads on sensitive buckets/paths.
- Signed URLs with long TTL, no content-disposition controls; replayable across tenants.
- List operations exposed: /o?prefix= enumerates object keys.
- Tests:
- GET gs:// paths via https endpoints without auth; verify content-type and Content-Disposition: attachment.
- Generate and reuse signed URLs across accounts and paths; try case/URL-encoding variants.
- Upload HTML/SVG and verify X-Content-Type-Options: nosniff; check for script execution.
</cloud_storage>
<cloud_functions>
- onCall provides context.auth automatically; onRequest must verify ID tokens explicitly. Admin SDK bypasses rules; all ownership/tenant checks must be enforced in code.
- Common gaps:
- Trusting client uid/orgId from request body instead of context.auth.
- Missing aud/iss verification when manually parsing tokens.
- Over-broad CORS allowing credentialed cross-origin requests; echoing Authorization in responses.
- Triggers (onCreate/onWrite) granting roles or issuing signed URLs solely based on document content controlled by the client.
- Tests:
- Call both onCall and equivalent onRequest endpoints with varied tokens and bodies; expect identical decisions.
- Create crafted docs to trigger privilege-granting functions; verify that server re-derives subject/tenant before acting.
- Attempt internal fetches (SSRF) via Functions to project/metadata endpoints.
</cloud_functions>
<app_check>
- App Check is not a substitute for authorization. Many apps enable App Check enforcement on client SDKs but do not verify on custom backends.
- Bypasses:
- Unenforced paths: REST calls directly to googleapis endpoints with ID token succeed regardless of App Check.
- Mobile reverse engineering: hook client and reuse ID token flows without attestation.
- Tests:
- Compare SDK vs REST behavior with/without App Check headers; confirm no elevated authorization via App Check alone.
</app_check>
<tenant_isolation>
- Apps often implement multi-tenant data models (orgs/<orgId>/...). Bind tenant from server context (membership doc or custom claim), not from client payload.
- Tests:
- Vary org header/subdomain/query while keeping token fixed; verify server denies cross-tenant access.
- Export/report Functions: ensure queries execute under caller scope; signed outputs must encode tenant and short TTL.
</tenant_isolation>
<bypass_techniques>
- Content-type switching: JSON vs form vs multipart to hit alternate code paths in onRequest Functions.
- Parameter/field pollution: duplicate JSON keys; last-one-wins in many parsers; attempt to sneak privilege fields.
- Caching/CDN: Hosting rewrites or proxies that key responses without Authorization or tenant headers.
- Race windows: write then read before background enforcements (e.g., post-write claim synchronizations) complete.
</bypass_techniques>
<blind_channels>
- Firestore: use error shape, document count, and ETag/length to infer existence under partial denial.
- Storage: length/timing differences on signed URL attempts leak validity.
- Functions: constant-time comparisons vs variable messages reveal authorization branches.
</blind_channels>
<tooling_and_automation>
- SDK + REST: httpie/curl + jq for REST; Firebase emulator and Rules Playground for rapid iteration.
- Mobile: apktool/objection/frida to extract config and hook SDK calls; inspect network logs for endpoints and tokens.
- Rules analysis: script rule probes for common patterns (auth != null, missing field validation, list vs get parity).
- Functions: fuzz onRequest endpoints with varied content-types and missing/forged Authorization; verify CORS and token handling.
- Storage: enumerate prefixes; test signed URL generation and reuse patterns.
</tooling_and_automation>
<reviewer_checklist>
- Do Firestore/Realtime/Storage rules derive subject and tenant from auth, not client fields?
- Are list/query rules aligned with per-doc checks (no broad list leaks)?
- Are privilege-bearing fields immutable or server-only (forbidden in writes)?
- Do Functions verify ID tokens (iss/aud/exp/signature) and re-derive identity before acting?
- Are Admin SDK operations scoped by server-side checks (ownership/tenant)?
- Is App Check treated as advisory, not authorization, across all paths?
- Are Hosting/CDN cache keys bound to Authorization/tenant to prevent leaks?
</reviewer_checklist>
<validation>
1. Provide owner vs non-owner Firestore queries showing unauthorized access or metadata leak.
2. Demonstrate Cloud Storage read/write beyond intended scope (public object, signed URL reuse, or list exposure).
3. Show a Function accepting forged/foreign identity (wrong aud/iss) or trusting client uid/orgId.
4. Document minimal reproducible requests with roles/tokens used and observed deltas.
</validation>
<false_positives>
- Public collections/objects documented and intended.
- Rules that correctly enforce per-doc checks with matching query constraints.
- Functions verifying tokens and ignoring client-supplied identifiers.
- App Check enforced but not relied upon for authorization.
</false_positives>
<impact>
- Cross-account and cross-tenant data exposure.
- Unauthorized state changes via Functions or direct writes.
- Exfiltration of PII/PHI and private files from Storage.
- Durable privilege escalation via misused custom claims or triggers.
</impact>
<pro_tips>
1. Treat apiKey as project identifier only; identity must come from verified ID tokens.
2. Start from rules: read them, then prove gaps with diffed owner/non-owner requests.
3. Prefer REST for parity checks; SDKs can mask errors via client-side filters.
4. Hunt privilege fields in docs and forbid them via rules; verify immutability.
5. Probe collectionGroup queries and list rules; many leaks live there.
6. Functions are the authority boundary—enforce subject/tenant there even if rules exist.
7. Keep concise PoCs: one owner vs non-owner request per surface that clearly demonstrates the unauthorized delta.
</pro_tips>
<remember>Authorization must hold at every layer: rules, Functions, and Storage. Bind subject and tenant from verified tokens and server data, never from client payload or UI assumptions. Any gap becomes a cross-account or cross-tenant vulnerability.</remember>
</firebase_firestore_security_guide>

View File

@@ -0,0 +1,211 @@
---
name: firebase-firestore
description: Firebase/Firestore security testing covering security rules, Cloud Functions, and client-side trust issues
---
# Firebase / Firestore
Security testing for Firebase applications. Focus on Firestore/Realtime Database rules, Cloud Storage exposure, callable/onRequest Functions trusting client input, and incorrect ID token validation.
## Attack Surface
**Data Stores**
- Firestore (documents/collections, rules, REST/SDK)
- Realtime Database (JSON tree, rules)
- Cloud Storage (rules, signed URLs)
**Authentication**
- Auth ID tokens, custom claims, anonymous/sign-in providers
- App Check attestation (and its limits)
**Server-Side**
- Cloud Functions (onCall/onRequest, triggers)
- Admin SDK (bypasses rules)
**Infrastructure**
- Hosting rewrites, CDN/caching, CORS
## Architecture
**Endpoints**
- Firestore REST: `https://firestore.googleapis.com/v1/projects/<project>/databases/(default)/documents/<path>`
- Realtime DB: `https://<project>.firebaseio.com/.json`
- Storage REST: `https://storage.googleapis.com/storage/v1/b/<bucket>`
**Auth**
- Google-signed ID tokens (iss: `accounts.google.com` or `securetoken.google.com/<project>`)
- Audience: `<project>` or `<app-id>`, identity in `sub`/`uid`
- Rules engines: separate for Firestore, Realtime DB, and Storage
- Functions bypass rules when using Admin SDK
## High-Value Targets
- Firestore collections with sensitive data (users, orders, payments)
- Realtime Database root and high-level nodes
- Cloud Storage buckets with private files
- Cloud Functions (especially triggers that grant roles or issue signed URLs)
- Admin/staff routes and privilege-granting endpoints
- Export/report functions that generate signed outputs
## Reconnaissance
**Extract Project Config**
From client bundle:
```javascript
// apiKey, authDomain, projectId, appId, storageBucket, messagingSenderId
firebase.apps[0].options
```
**Obtain Principals**
- Unauthenticated
- Anonymous (if enabled)
- Basic user A, user B
- Staff/admin (if available)
Capture ID tokens for each.
## Key Vulnerabilities
### Firestore Rules
Rules are not filters—a query must include constraints that make the rule true for all returned documents.
**Common Gaps**
- `allow read: if request.auth != null` — any authenticated user reads all data
- `allow write: if request.auth != null` — mass write access
- Missing per-field validation (allows adding `isAdmin`/`role`/`tenantId` fields)
- Using client-supplied `ownerId`/`orgId` instead of `resource.data.ownerId == request.auth.uid`
- Over-broad list rules on root collections (per-doc checks exist but list still leaks)
**Secure Patterns**
```javascript
// Restrict write fields
request.resource.data.keys().hasOnly(['field1', 'field2', 'field3'])
// Enforce ownership
resource.data.ownerId == request.auth.uid &&
request.resource.data.ownerId == request.auth.uid
// Org membership check
exists(/databases/(default)/documents/orgs/$(org)/members/$(request.auth.uid))
```
**Tests**
- Compare results for users A/B on identical queries; diff counts and IDs
- Cross-tenant reads: `where orgId == otherOrg`; try queries without org filter
- Write-path: set/patch with foreign `ownerId`/`orgId`; attempt to flip privilege flags
### Firestore Queries
- Use REST to avoid SDK client-side constraints
- Probe composite index requirements (UI-driven queries may hide missing rule coverage)
- Explore `collectionGroup` queries that may bypass per-collection rules
- Use `startAt`/`endAt`/`in`/`array-contains` to probe rule edges and pagination cursors
### Realtime Database
- Misconfigured rules frequently expose entire JSON trees
- Probe `https://<project>.firebaseio.com/.json` with and without auth
- Confirm rules use `auth.uid` and granular path checks
- Avoid `.read/.write: true` or `auth != null` at high-level nodes
- Attempt to write privilege-bearing nodes (roles, org membership)
### Cloud Storage
**Common Issues**
- Public reads on sensitive buckets/paths
- Signed URLs with long TTL, no content-disposition controls, replayable across tenants
- List operations exposed: `/o?prefix=` enumerates object keys
**Tests**
- GET gs:// paths via HTTPS without auth; verify Content-Type and `Content-Disposition: attachment`
- Generate and reuse signed URLs across accounts and paths; try case/URL-encoding variants
- Upload HTML/SVG and verify `X-Content-Type-Options: nosniff`; check for script execution
### Cloud Functions
`onCall` provides `context.auth` automatically; `onRequest` must verify ID tokens explicitly. Admin SDK bypasses rules—all ownership/tenant checks must be in code.
**Common Gaps**
- Trusting client `uid`/`orgId` from request body instead of `context.auth`
- Missing `aud`/`iss` verification when manually parsing tokens
- Over-broad CORS allowing credentialed cross-origin requests
- Triggers (onCreate/onWrite) granting roles based on document content controlled by client
**Tests**
- Call both onCall and onRequest endpoints with varied tokens; expect identical decisions
- Create crafted docs to trigger privilege-granting functions
- Attempt SSRF via Functions to project/metadata endpoints
### Auth & Token Issues
**Verification Requirements**
- Issuer, audience (project), signature (Google JWKS), expiration
- Optionally App Check binding when used
**Pitfalls**
- Accepting any JWT with valid signature but wrong audience/project
- Trusting `uid`/account IDs from request body instead of `context.auth.uid`
- Mixing session cookies and ID tokens without verifying both paths equivalently
- Custom claims copied into docs then trusted by app code
**Tests**
- Replay tokens across environments/projects; expect strict `aud`/`iss` rejection
- Call Functions with and without Authorization; verify identical checks
### App Check
App Check is not a substitute for authorization.
**Bypasses**
- REST calls directly to googleapis endpoints with ID token succeed regardless of App Check
- Mobile reverse engineering: hook client and reuse ID token flows without attestation
**Tests**
- Compare SDK vs REST behavior with/without App Check headers
- Confirm no elevated authorization via App Check alone
### Tenant Isolation
Apps often implement multi-tenant data models (`orgs/<orgId>/...`). Bind tenant from server context (membership doc or custom claim), not client payload.
**Tests**
- Vary org header/subdomain/query while keeping token fixed; verify server denies cross-tenant access
- Export/report Functions: ensure queries execute under caller scope
## Bypass Techniques
- Content-type switching: JSON vs form vs multipart to hit alternate code paths in onRequest
- Parameter/field pollution: duplicate JSON keys (last-one-wins in many parsers); sneak privilege fields
- Caching/CDN: Hosting rewrites keying responses without Authorization or tenant headers
- Race windows: write then read before background enforcements complete
## Blind Enumeration
- Firestore: use error shape, document count, ETag/length to infer existence
- Storage: length/timing differences on signed URL attempts leak validity
- Functions: constant-time comparisons vs variable messages reveal authorization branches
## Testing Methodology
1. **Extract config** - Get project config from client bundle
2. **Obtain principals** - Collect tokens for unauth, anonymous, user A/B, admin
3. **Build matrix** - Resource × Action × Principal across Firestore/Realtime/Storage/Functions
4. **SDK vs REST** - Exercise every action via both to detect parity gaps
5. **Seed IDs** - Start from list/query paths to gather document IDs
6. **Cross-principal** - Swap document paths, tenants, and user IDs across principals
## Tooling
- SDK + REST: httpie/curl + jq for REST; Firebase emulator and Rules Playground for rapid iteration
- Rules analysis: script probes for common patterns (`auth != null`, missing field validation)
- Functions: fuzz onRequest with varied content-types and missing/forged Authorization
- Storage: enumerate prefixes; test signed URL generation and reuse patterns
## Validation Requirements
- Owner vs non-owner Firestore queries showing unauthorized access or metadata leak
- Cloud Storage read/write beyond intended scope (public object, signed URL reuse, list exposure)
- Function accepting forged/foreign identity (wrong `aud`/`iss`) or trusting client `uid`/`orgId`
- Minimal reproducible requests with roles/tokens used and observed deltas

View File

@@ -1,189 +0,0 @@
<supabase_security_guide>
<title>SUPABASE — ADVERSARIAL TESTING AND EXPLOITATION</title>
<critical>Supabase exposes Postgres through PostgREST, Realtime, GraphQL, Storage, Auth (GoTrue), and Edge Functions. Most impactful findings come from mis-scoped Row Level Security (RLS), unsafe RPCs, leaked service_role keys, lax Storage policies, GraphQL overfetching, and Edge Functions trusting headers or tokens without binding to issuer/audience/tenant.</critical>
<scope>
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
- RLS: row ownership/tenant isolation via policies and auth.uid()
- Storage: buckets, objects, signed URLs, public/private policies
- Realtime: replication subscriptions, broadcast/presence channels
- GraphQL: pg_graphql over Postgres schema with RLS interaction
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
- Edge Functions (Deno): server-side code calling Supabase with secrets
</scope>
<methodology>
1. Inventory surfaces: REST /rest/v1, Storage /storage/v1, GraphQL /graphql/v1, Realtime wss, Auth /auth/v1, Functions https://<project>.functions.supabase.co/.
2. Obtain tokens for: unauth (anon), basic user, other user, and (if disclosed) admin/staff; enumerate anon key exposure and verify if service_role leaked anywhere.
3. Build a Resource × Action × Principal matrix and test each via REST and GraphQL. Confirm parity across channels and content-types (json/form/multipart).
4. Start with list/search/export endpoints to gather IDs, then attempt direct reads/writes across principals, tenants, and transports. Validate RLS and function guards.
</methodology>
<architecture>
- Project endpoints: https://<ref>.supabase.co; REST at /rest/v1/<table>, RPC at /rest/v1/rpc/<fn>.
- Headers: apikey: <anon-or-service>, Authorization: Bearer <JWT>. Anon key only identifies the project; JWT binds user context.
- Roles: anon, authenticated; service_role bypasses RLS and must never be client-exposed.
- auth.uid(): current user UUID claim; policies must never trust client-supplied IDs over server context.
</architecture>
<rls>
- Enable RLS on every non-public table; absence or “permit-all” policies → bulk exposure.
- Common gaps:
- Policies check auth.uid() for read but forget UPDATE/DELETE/INSERT.
- Missing tenant constraints (org_id/tenant_id) allow cross-tenant reads/writes.
- Policies rely on client-provided columns (user_id in payload) instead of deriving from JWT.
- Complex joins where the effective policy is applied after filters, enabling inference via counts or projections.
- Tests:
- Compare results for two users: GET /rest/v1/<table>?select=*&Prefer=count=exact; diff row counts and IDs.
- Try cross-tenant: add &org_id=eq.<other_org> or use or=(org_id.eq.other,org_id.is.null).
- Write-path: PATCH/DELETE single row with foreign id; INSERT with foreign owner_id then read.
</rls>
<postgrest_and_rest>
- Filters: eq, neq, lt, gt, ilike, or, is, in; embed relations with select=*,profile(*); exploit embeddings to overfetch linked rows if resolvers skip per-row checks.
- Headers to know: Prefer: return=representation (echo writes), Prefer: count=exact (exposure via counts), Accept-Profile/Content-Profile to select schema.
- IDOR patterns: /rest/v1/<table>?select=*&id=eq.<other_id>; query alternative keys (slug, email) and composite keys.
- Search leaks: generous LIKE/ILIKE filters + lack of RLS → mass disclosure.
- Mass assignment: if RPC not used, PATCH can update unintended columns; verify restricted columns via database permissions/policies.
</postgrest_and_rest>
<rpc_functions>
- RPC endpoints map to SQL functions. SECURITY DEFINER bypasses RLS unless carefully coded; SECURITY INVOKER respects caller.
- Anti-patterns:
- SECURITY DEFINER + missing owner checks → vertical/horizontal bypass.
- set search_path left to public; function resolves unsafe objects.
- Trusting client-supplied user_id/tenant_id rather than auth.uid().
- Tests:
- Call /rest/v1/rpc/<fn> as different users with foreign ids in body.
- Remove or alter JWT entirely (Authorization: Bearer <anon>) to see if function still executes.
- Validate that functions perform explicit ownership/tenant checks inside SQL, not only in docs.
</rpc_functions>
<storage>
- Buckets: public vs private; objects live in storage.objects with RLS-like policies.
- Find misconfigs:
- Public buckets holding sensitive data: GET https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>
- Signed URLs with long TTL and no audience binding; reuse/guess tokens across tenants/paths.
- Listing prefixes without auth: /storage/v1/object/list/<bucket>?prefix=
- Path confusion: mixed case, URL-encoding, “..” segments rejected at UI but accepted by API.
- Abuse vectors:
- Content-type/XSS: upload HTML/SVG served as text/html or image/svg+xml; confirm X-Content-Type-Options: nosniff and Content-Disposition: attachment.
- Signed URL replay across accounts/buckets if validation is lax.
</storage>
<realtime>
- Endpoint: wss://<ref>.supabase.co/realtime/v1. Join channels with apikey + Authorization.
- Risks:
- Channel names derived from table/schema/filters leaking other users updates when RLS or channel guards are weak.
- Broadcast/presence channels allowing cross-room join/publish without auth checks.
- Tests:
- Subscribe to public:realtime changes on protected tables; confirm row data visibility aligns with RLS.
- Attempt joining other users presence/broadcast channels (e.g., room:<user_id>, org:<id>).
</realtime>
<graphql>
- Endpoint: /graphql/v1 using pg_graphql with RLS. Risks:
- Introspection reveals schema relations; ensure its 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>

View File

@@ -0,0 +1,268 @@
---
name: supabase
description: Supabase security testing covering Row Level Security, PostgREST, Edge Functions, and service key exposure
---
# Supabase
Security testing for Supabase applications. Focus on mis-scoped Row Level Security (RLS), unsafe RPCs, leaked `service_role` keys, lax Storage policies, and Edge Functions trusting headers without binding to issuer/audience/tenant.
## Attack Surface
**Data Access**
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
- GraphQL: pg_graphql over Postgres schema with RLS interaction
- Realtime: replication subscriptions, broadcast/presence channels
**Storage**
- Buckets, objects, signed URLs, public/private policies
**Authentication**
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
**Server-Side**
- Edge Functions (Deno): server-side code calling Supabase with secrets
## Architecture
**Endpoints**
- REST: `https://<ref>.supabase.co/rest/v1/<table>`
- RPC: `https://<ref>.supabase.co/rest/v1/rpc/<fn>`
- Storage: `https://<ref>.supabase.co/storage/v1`
- GraphQL: `https://<ref>.supabase.co/graphql/v1`
- Realtime: `wss://<ref>.supabase.co/realtime/v1`
- Auth: `https://<ref>.supabase.co/auth/v1`
- Functions: `https://<ref>.functions.supabase.co/`
**Headers**
- `apikey: <anon-or-service>` — identifies project
- `Authorization: Bearer <JWT>` — binds user context
**Roles**
- `anon`, `authenticated` — standard roles
- `service_role` — bypasses RLS, must never be client-exposed
**Key Principle**
`auth.uid()` returns current user UUID from JWT. Policies must never trust client-supplied IDs over server context.
## High-Value Targets
- Tables with sensitive data (users, orders, payments, PII)
- RPC functions (especially `SECURITY DEFINER`)
- Storage buckets with private files
- Edge Functions with `service_role` access
- Export/report endpoints generating signed outputs
- Admin/staff routes and privilege-granting endpoints
## Reconnaissance
**Enumerate Surfaces**
```
/rest/v1/<table>
/rest/v1/rpc/<fn>
/storage/v1/object/public/<bucket>/
/storage/v1/object/list/<bucket>?prefix=
/graphql/v1
/auth/v1
```
**Obtain Principals**
- Unauthenticated (anon key only)
- Basic user A, user B
- Admin/staff (if available)
- Check if `service_role` key leaked in client bundle or Edge Function responses
## Key Vulnerabilities
### Row Level Security (RLS)
Enable RLS on every non-public table; absence or "permit-all" policies → bulk exposure.
**Common Gaps**
- Policies check `auth.uid()` for SELECT but forget UPDATE/DELETE/INSERT
- Missing tenant constraints (`org_id`/`tenant_id`) allow cross-tenant access
- Policies rely on client-provided columns (`user_id` in payload) instead of JWT
- Complex joins where policy is applied after filters, enabling inference via counts
**Tests**
```bash
# Compare row counts for two users
GET /rest/v1/<table>?select=*&Prefer=count=exact
# Cross-tenant probe
GET /rest/v1/<table>?org_id=eq.<other_org>
GET /rest/v1/<table>?or=(org_id.eq.other,org_id.is.null)
# Write-path
PATCH /rest/v1/<table>?id=eq.<foreign_id>
DELETE /rest/v1/<table>?id=eq.<foreign_id>
POST /rest/v1/<table> with foreign owner_id
```
### PostgREST & REST
**Filters**
- `eq`, `neq`, `lt`, `gt`, `ilike`, `or`, `is`, `in`
- Embed relations: `select=*,profile(*)`—exploits overfetch if resolvers skip per-row checks
- Search leaks: generous `LIKE`/`ILIKE` filters combined with missing RLS → mass disclosure via wildcard queries
**Headers**
- `Prefer: return=representation` — echo writes
- `Prefer: count=exact` — exposure via counts
- `Accept-Profile`/`Content-Profile` — select schema
**IDOR Patterns**
```
/rest/v1/<table>?select=*&id=eq.<other_id>
/rest/v1/<table>?select=*&slug=eq.<other_slug>
/rest/v1/<table>?select=*&email=eq.<other_email>
```
**Mass Assignment**
- If RPC not used, PATCH can update unintended columns
- Verify restricted columns via database permissions/policies
### RPC Functions
RPC endpoints map to SQL functions. `SECURITY DEFINER` bypasses RLS unless carefully coded; `SECURITY INVOKER` respects caller.
**Anti-Patterns**
- `SECURITY DEFINER` + missing owner checks → vertical/horizontal bypass
- `set search_path` left to public; function resolves unsafe objects
- Trusting client-supplied `user_id`/`tenant_id` rather than `auth.uid()`
**Tests**
```bash
# Call as different users with foreign IDs
POST /rest/v1/rpc/<fn> {"user_id": "<foreign_id>"}
# Remove JWT entirely
Authorization: Bearer <anon_token>
```
Verify functions perform explicit ownership/tenant checks inside SQL.
### Storage
**Buckets**
- Public vs private; objects in `storage.objects` with RLS-like policies
**Misconfigurations**
```bash
# Public bucket with sensitive data
GET /storage/v1/object/public/<bucket>/<path>
# List prefixes without auth
GET /storage/v1/object/list/<bucket>?prefix=
# Signed URL reuse across tenants/paths
```
**Content-Type Abuse**
- Upload HTML/SVG served as `text/html` or `image/svg+xml`
- Verify `X-Content-Type-Options: nosniff` and `Content-Disposition: attachment`
**Path Confusion**
- Mixed case, URL-encoding, `..` segments may be rejected at UI but accepted by API
- Test path normalization differences between client validation and server handling
### Realtime
**Endpoint**: `wss://<ref>.supabase.co/realtime/v1`
**Risks**
- Channel names derived from table/schema/filters leaking other users' updates when RLS or channel guards are weak
- Broadcast/presence channels allowing cross-room join/publish without auth
**Tests**
- Subscribe to `public:realtime` changes on protected tables; confirm visibility aligns with RLS
- Attempt joining other users' channels: `room:<user_id>`, `org:<org_id>`
### GraphQL
**Endpoint**: `/graphql/v1` using pg_graphql with RLS
**Risks**
- Introspection reveals schema relations
- Overfetch via nested relations where resolvers skip per-row ownership checks
- Global node IDs leaked and reusable via different viewers
**Tests**
- Compare REST vs GraphQL responses for same principal and query shape
- Query deep nested fields; verify RLS holds at each edge
### Auth & Tokens
GoTrue issues JWTs with claims (`sub=uid`, `role`, `aud=authenticated`).
**Verification Requirements**
- Issuer, audience, expiration, signature, tenant context
**Pitfalls**
- Storing tokens in localStorage → XSS exfiltration
- Treating `apikey` as identity (it's project-scoped, not user identity)
- Exposing `service_role` key in client bundle or Edge Function responses
- Refresh token mismanagement leading to long-lived sessions beyond intended TTL
**Tests**
- Replay tokens across services; check audience/issuer pinning
- Try downgraded tokens (expired/other audience) against custom endpoints
### Edge Functions
Deno-based functions often initialize Supabase client with `service_role`.
**Risks**
- Trusting Authorization/apikey headers without verifying JWT against issuer/audience
- CORS: wildcard origins with credentials; reflected Authorization in responses
- SSRF via fetch; secrets exposed via error traces or logs
**Tests**
- Call functions with and without Authorization; compare behavior
- Try foreign resource IDs in payloads; verify server re-derives user/tenant from JWT
- Attempt to reach internal endpoints (metadata services) via function fetch
### Tenant Isolation
Ensure every query joins or filters by `tenant_id`/`org_id` derived from JWT context, not client input.
**Tests**
- Change subdomain/header/path tenant selectors while keeping JWT tenant constant
- Export/report endpoints: confirm queries execute under caller scope
## Bypass Techniques
- Content-type switching: `application/json``application/x-www-form-urlencoded``multipart/form-data`
- Parameter pollution: duplicate keys in JSON/query (PostgREST chooses last/first depending on parser)
- GraphQL+REST parity probing: protections often drift; fetch via the weaker path
- Race windows: parallel writes to bypass post-insert ownership updates
## Blind Enumeration
- Use `Prefer: count=exact` and ETag/length diffs to infer unauthorized rows
- Conditional requests (`If-None-Match`) to detect object existence
- Storage signed URLs: timing/length deltas to map valid vs invalid tokens
## Testing Methodology
1. **Inventory surfaces** - Map REST, Storage, GraphQL, Realtime, Auth, Functions endpoints
2. **Obtain principals** - Collect tokens for anon, user A/B, admin; check for `service_role` leaks
3. **Build matrix** - Resource × Action × Principal
4. **REST vs GraphQL** - Test both to find parity gaps
5. **Seed IDs** - Start with list/search endpoints to gather IDs
6. **Cross-principal** - Swap IDs, tenants, and transports across principals
## Tooling
- PostgREST: httpie/curl + jq; enumerate tables; fuzz filters (`or=`, `ilike`, `neq`, `is.null`)
- GraphQL: graphql-inspector, voyager; deep queries for field-level enforcement
- Realtime: custom ws client; subscribe to suspicious channels; diff payloads per principal
- Storage: enumerate bucket listing APIs; script signed URL patterns
- Auth/JWT: jwt-cli/jose to validate audience/issuer; replay against Edge Functions
- Policy diffing: maintain request sets per role; compare results across releases
## Validation Requirements
- Owner vs non-owner requests for REST/GraphQL showing unauthorized access (content or metadata)
- Mis-scoped RPC or Storage signed URL usable by another user/tenant
- Realtime or GraphQL exposure matching missing policy checks
- Minimal reproducible requests with role contexts documented

View File

@@ -1,147 +1,156 @@
<authentication_jwt_guide> ---
<title>AUTHENTICATION AND JWT/OIDC</title> name: authentication-jwt
description: JWT and OIDC security testing covering token forgery, algorithm confusion, and claim manipulation
---
<critical>JWT/OIDC failures often enable token forgery, token confusion, cross-service acceptance, and durable account takeover. Do not trust headers, claims, or token opacity without strict validation bound to issuer, audience, key, and context.</critical> # Authentication / JWT / OIDC
JWT/OIDC failures often enable token forgery, token confusion, cross-service acceptance, and durable account takeover. Do not trust headers, claims, or token opacity without strict validation bound to issuer, audience, key, and context.
## Attack Surface
<scope>
- Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2 - Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2
- Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows - Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows
- First-party and microservices verification, gateways, and JWKS distribution - First-party and microservices verification, gateways, and JWKS distribution
</scope>
<methodology> ## Reconnaissance
1. Inventory issuers and consumers: identity providers, API gateways, services, mobile/web clients.
2. Capture real tokens (access and ID) for multiple roles. Note header, claims, signature, and verification endpoints (/.well-known, /jwks.json).
3. Build a matrix: Token Type × Audience × Service; attempt cross-use (wrong audience/issuer/service) and observe acceptance.
4. Mutate headers (alg, kid, jku/x5u/jwk, typ/cty/crit), claims (iss/aud/azp/sub/nbf/iat/exp/scope/nonce), and signatures; verify what is actually enforced.
</methodology>
<discovery_techniques> ### Endpoints
<endpoints>
- Well-known: /.well-known/openid-configuration, /oauth2/.well-known/openid-configuration
- Keys: /jwks.json, rotating key endpoints, tenant-specific JWKS
- Auth: /authorize, /token, /introspect, /revoke, /logout, device code endpoints
- App: /login, /callback, /refresh, /me, /session, /impersonate
</endpoints>
<token_features> - Well-known: `/.well-known/openid-configuration`, `/oauth2/.well-known/openid-configuration`
- Headers: {% raw %}{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}{% endraw %} - Keys: `/jwks.json`, rotating key endpoints, tenant-specific JWKS
- Claims: {% raw %}{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}{% endraw %} - Auth: `/authorize`, `/token`, `/introspect`, `/revoke`, `/logout`, device code endpoints
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option ("b64":false) and critical headers ("crit"). - App: `/login`, `/callback`, `/refresh`, `/me`, `/session`, `/impersonate`
</token_features>
</discovery_techniques> ### Token Features
- Headers: `{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}`
- Claims: `{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}`
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option (`"b64":false`) and critical headers (`"crit"`)
## Key Vulnerabilities
### Signature Verification
<exploitation_techniques>
<signature_verification>
- RS256→HS256 confusion: change alg to HS256 and use the RSA public key as HMAC secret if algorithm is not pinned - RS256→HS256 confusion: change alg to HS256 and use the RSA public key as HMAC secret if algorithm is not pinned
- "none" algorithm acceptance: set {% raw %}"alg":"none"{% endraw %} and drop the signature if libraries accept it - "none" algorithm acceptance: set `"alg":"none"` and drop the signature if libraries accept it
- ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures - ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures
</signature_verification>
<header_manipulation> ### Header Manipulation
- kid injection: path traversal {% raw %}../../../../keys/prod.key{% endraw %}, SQL/command/template injection in key lookup, or pointing to world-readable files
- jku/x5u abuse: host attacker-controlled JWKS/X509 chain; if not pinned/whitelisted, server fetches and trusts attacker keys - **kid injection**: path traversal `../../../../keys/prod.key`, SQL/command/template injection in key lookup, or pointing to world-readable files
- jwk header injection: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys - **jku/x5u abuse**: host attacker-controlled JWKS/X509 chain; if not pinned/whitelisted, server fetches and trusts attacker keys
- SSRF via remote key fetch: exploit JWKS URL fetching to reach internal hosts - **jwk header injection**: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys
</header_manipulation> - **SSRF via remote key fetch**: exploit JWKS URL fetching to reach internal hosts
### Key and Cache Issues
<key_and_cache_issues>
- JWKS caching TTL and key rollover: accept obsolete keys; race rotation windows; missing kid pinning → accept any matching kty/alg - JWKS caching TTL and key rollover: accept obsolete keys; race rotation windows; missing kid pinning → accept any matching kty/alg
- Mixed environments: same secrets across dev/stage/prod; key reuse across tenants or services - Mixed environments: same secrets across dev/stage/prod; key reuse across tenants or services
- Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs) - Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs)
</key_and_cache_issues>
<claims_validation_gaps> ### Claims Validation Gaps
- iss/aud/azp not enforced: cross-service token reuse; accept tokens from any issuer or wrong audience - iss/aud/azp not enforced: cross-service token reuse; accept tokens from any issuer or wrong audience
- scope/roles fully trusted from token: server does not re-derive authorization; privilege inflation via claim edits when signature checks are weak - scope/roles fully trusted from token: server does not re-derive authorization; privilege inflation via claim edits when signature checks are weak
- exp/nbf/iat not enforced or large clock skew tolerance; accept long-expired or not-yet-valid tokens - exp/nbf/iat not enforced or large clock skew tolerance; accept long-expired or not-yet-valid tokens
- typ/cty not enforced: accept ID token where access token required (token confusion) - typ/cty not enforced: accept ID token where access token required (token confusion)
</claims_validation_gaps>
<token_confusion_and_oidc> ### Token Confusion and OIDC
- Access vs ID token swap: use ID token against APIs when they only verify signature but not audience/typ - Access vs ID token swap: use ID token against APIs when they only verify signature but not audience/typ
- OIDC mix-up: redirect_uri and client mix-ups causing tokens for Client A to be redeemed at Client B - OIDC mix-up: redirect_uri and client mix-ups causing tokens for Client A to be redeemed at Client B
- PKCE downgrades: missing S256 requirement; accept plain or absent code_verifier - PKCE downgrades: missing S256 requirement; accept plain or absent code_verifier
- State/nonce weaknesses: predictable or missing → CSRF/logical interception of login\n- Device/Backchannel flows: codes and tokens accepted by unintended clients or services - State/nonce weaknesses: predictable or missing → CSRF/logical interception of login
</token_confusion_and_oidc> - Device/Backchannel flows: codes and tokens accepted by unintended clients or services
### Refresh and Session
<refresh_and_session>
- Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection - Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection
- Long-lived JWTs with no revocation: persistent access post-logout - Long-lived JWTs with no revocation: persistent access post-logout
- Session fixation: bind new tokens to attacker-controlled session identifiers or cookies - Session fixation: bind new tokens to attacker-controlled session identifiers or cookies
</refresh_and_session>
<transport_and_storage> ### Transport and Storage
- Token in localStorage/sessionStorage: susceptible to XSS exfiltration; cookie vs header trade-offs with SameSite/CSRF - Token in localStorage/sessionStorage: susceptible to XSS exfiltration; cookie vs header trade-offs with SameSite/CSRF
- Insecure CORS: wildcard origins with credentialed requests expose tokens and protected responses - Insecure CORS: wildcard origins with credentialed requests expose tokens and protected responses
- TLS and cookie flags: missing Secure/HttpOnly; lack of mTLS or DPoP/"cnf" binding permits replay from another device - TLS and cookie flags: missing Secure/HttpOnly; lack of mTLS or DPoP/"cnf" binding permits replay from another device
</transport_and_storage>
</exploitation_techniques>
<advanced_techniques> ## Advanced Techniques
<microservices_and_gateways>
### Microservices and Gateways
- Audience mismatch: internal services verify signature but ignore aud → accept tokens for other services - Audience mismatch: internal services verify signature but ignore aud → accept tokens for other services
- Header trust: edge or gateway injects X-User-Id; backend trusts it over token claims - Header trust: edge or gateway injects X-User-Id; backend trusts it over token claims
- Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay - Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay
</microservices_and_gateways>
<jws_edge_cases> ### JWS Edge Cases
- Unencoded payload (b64=false) with crit header: libraries mishandle verification paths - Unencoded payload (b64=false) with crit header: libraries mishandle verification paths
- Nested JWT (JWT-in-JWT) verification order errors; outer token accepted while inner claims ignored - Nested JWT (JWT-in-JWT) verification order errors; outer token accepted while inner claims ignored
</jws_edge_cases>
<special_contexts> ## Special Contexts
<mobile>
### Mobile
- Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens - Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens
- Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible - Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible
</mobile>
<sso_federation> ### SSO Federation
- Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens - Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens
</sso_federation>
</special_contexts>
<chaining_attacks> ## Chaining Attacks
- XSS → token theft → replay across services with weak audience checks - XSS → token theft → replay across services with weak audience checks
- SSRF → fetch private JWKS → sign tokens accepted by internal services - SSRF → fetch private JWKS → sign tokens accepted by internal services
- Host header poisoning → OIDC redirect_uri poisoning → code capture - Host header poisoning → OIDC redirect_uri poisoning → code capture
- IDOR in sessions/impersonation endpoints → mint tokens for other users - IDOR in sessions/impersonation endpoints → mint tokens for other users
</chaining_attacks>
<validation> ## Testing Methodology
1. Show forged or cross-context token acceptance (wrong alg, wrong audience/issuer, or attacker-signed JWKS).
2. Demonstrate access token vs ID token confusion at an API. 1. **Inventory issuers/consumers** - Identity providers, API gateways, services, mobile/web clients
3. Prove refresh token reuse without rotation detection or revocation. 2. **Capture tokens** - Access and ID tokens for multiple roles; note header, claims, signature
4. Confirm header abuse (kid/jku/x5u/jwk) leading to key selection under attacker control. 3. **Map verification endpoints** - `/.well-known`, `/jwks.json`
5. Provide owner vs non-owner evidence with identical requests differing only in token context. 4. **Build matrix** - Token Type × Audience × Service; attempt cross-use
</validation> 5. **Mutate components** - Headers (alg, kid, jku/x5u/jwk), claims (iss/aud/azp/sub/exp), signatures
6. **Verify enforcement** - What is actually checked vs assumed
## Validation
1. Show forged or cross-context token acceptance (wrong alg, wrong audience/issuer, or attacker-signed JWKS)
2. Demonstrate access token vs ID token confusion at an API
3. Prove refresh token reuse without rotation detection or revocation
4. Confirm header abuse (kid/jku/x5u/jwk) leading to key selection under attacker control
5. Provide owner vs non-owner evidence with identical requests differing only in token context
## False Positives
<false_positives>
- Token rejected due to strict audience/issuer enforcement - Token rejected due to strict audience/issuer enforcement
- Key pinning with JWKS whitelist and TLS validation - Key pinning with JWKS whitelist and TLS validation
- Short-lived tokens with rotation and revocation on logout - Short-lived tokens with rotation and revocation on logout
- ID token not accepted by APIs that require access tokens - ID token not accepted by APIs that require access tokens
</false_positives>
<impact> ## Impact
- Account takeover and durable session persistence - Account takeover and durable session persistence
- Privilege escalation via claim manipulation or cross-service acceptance - Privilege escalation via claim manipulation or cross-service acceptance
- Cross-tenant or cross-application data access - Cross-tenant or cross-application data access
- Token minting by attacker-controlled keys or endpoints - Token minting by attacker-controlled keys or endpoints
</impact>
<pro_tips> ## Pro Tips
1. Pin verification to issuer and audience; log and diff claim sets across services.
2. Attempt RS256→HS256 and "none" first only if algorithm pinning is unclear; otherwise focus on header key control (kid/jku/x5u/jwk).
3. Test token reuse across all services; many backends only check signature, not audience/typ.
4. Exploit JWKS caching and rotation races; try retired keys and missing kid fallbacks.
5. Exercise OIDC flows with PKCE/state/nonce variants and mixed clients; look for mix-up.
6. Try DPoP/mTLS absence to replay tokens from different devices.
7. Treat refresh as its own surface: rotation, reuse detection, and audience scoping.
8. Validate every acceptance path: gateway, service, worker, WebSocket, and gRPC.
9. Favor minimal PoCs that clearly show cross-context acceptance and durable access.
10. When in doubt, assume verification differs per stack (mobile vs web vs gateway) and test each.
</pro_tips>
<remember>Verification must bind the token to the correct issuer, audience, key, and client context on every acceptance path. Any missing binding enables forgery or confusion.</remember> 1. Pin verification to issuer and audience; log and diff claim sets across services
</authentication_jwt_guide> 2. Attempt RS256→HS256 and "none" first only if algorithm pinning is unclear; otherwise focus on header key control (kid/jku/x5u/jwk)
3. Test token reuse across all services; many backends only check signature, not audience/typ
4. Exploit JWKS caching and rotation races; try retired keys and missing kid fallbacks
5. Exercise OIDC flows with PKCE/state/nonce variants and mixed clients; look for mix-up
6. Try DPoP/mTLS absence to replay tokens from different devices
7. Treat refresh as its own surface: rotation, reuse detection, and audience scoping
8. Validate every acceptance path: gateway, service, worker, WebSocket, and gRPC
9. Favor minimal PoCs that clearly show cross-context acceptance and durable access
10. When in doubt, assume verification differs per stack (mobile vs web vs gateway) and test each
## Summary
Verification must bind the token to the correct issuer, audience, key, and client context on every acceptance path. Any missing binding enables forgery or confusion.

View File

@@ -1,146 +1,154 @@
<broken_function_level_authorization_guide> ---
<title>BROKEN FUNCTION LEVEL AUTHORIZATION (BFLA)</title> name: broken-function-level-authorization
description: BFLA testing for action-level authorization failures across endpoints, admin functions, and API operations
---
<critical>BFLA is action-level authorization failure: callers invoke functions (endpoints, mutations, admin tools) they are not entitled to. It appears when enforcement differs across transports, gateways, roles, or when services trust client hints. Bind subject × action at the service that performs the action.</critical> # Broken Function Level Authorization (BFLA)
BFLA is action-level authorization failure: callers invoke functions (endpoints, mutations, admin tools) they are not entitled to. It appears when enforcement differs across transports, gateways, roles, or when services trust client hints. Bind subject × action at the service that performs the action.
## Attack Surface
<scope>
- Vertical authz: privileged/admin/staff-only actions reachable by basic users - Vertical authz: privileged/admin/staff-only actions reachable by basic users
- Feature gates: toggles enforced at edge/UI, not at core services - Feature gates: toggles enforced at edge/UI, not at core services
- Transport drift: REST vs GraphQL vs gRPC vs WebSocket with inconsistent checks - Transport drift: REST vs GraphQL vs gRPC vs WebSocket with inconsistent checks
- Gateway trust: backends trust X-User-Id/X-Role injected by proxies/edges - Gateway trust: backends trust X-User-Id/X-Role injected by proxies/edges
- Background workers/jobs performing actions without re-checking authz - Background workers/jobs performing actions without re-checking authz
</scope>
<methodology> ## High-Value Actions
1. Build an Actor × Action matrix with at least: unauth, basic, premium, staff/admin. Enumerate actions (create/update/delete, approve/cancel, impersonate, export, invite, role-change, credit/refund).
2. Obtain tokens/sessions for each role. Exercise every action across all transports and encodings (JSON, form, multipart), including method overrides.
3. Vary headers and contextual selectors (org/tenant/project) and test behavior behind gateway vs direct-to-service.
4. Include background flows: job creation/finalization, webhooks, queues. Confirm re-validation of authz in consumers.
</methodology>
<discovery_techniques>
<surface_enumeration>
- Admin/staff consoles and APIs, support tools, internal-only endpoints exposed via gateway
- Hidden buttons and disabled UI paths (feature-flagged) mapped to still-live endpoints
- GraphQL schemas: mutations and admin-only fields/types; gRPC service descriptors (reflection)
- Mobile clients often reveal extra endpoints/roles in app bundles or network logs
</surface_enumeration>
<signals>
- 401/403 on UI but 200 via direct API call; differing status codes across transports
- Actions succeed via background jobs when direct call is denied
- Changing only headers (role/org) alters access without token change
</signals>
<high_value_actions>
- Role/permission changes, impersonation/sudo, invite/accept into orgs - Role/permission changes, impersonation/sudo, invite/accept into orgs
- Approve/void/refund/credit issuance, price/plan overrides - Approve/void/refund/credit issuance, price/plan overrides
- Export/report generation, data deletion, account suspension/reactivation - Export/report generation, data deletion, account suspension/reactivation
- Feature flag toggles, quota/grant adjustments, license/seat changes - Feature flag toggles, quota/grant adjustments, license/seat changes
- Security settings: 2FA reset, email/phone verification overrides - Security settings: 2FA reset, email/phone verification overrides
</high_value_actions>
<exploitation_techniques> ## Reconnaissance
<verb_drift_and_aliases>
### Surface Enumeration
- Admin/staff consoles and APIs, support tools, internal-only endpoints exposed via gateway
- Hidden buttons and disabled UI paths (feature-flagged) mapped to still-live endpoints
- GraphQL schemas: mutations and admin-only fields/types; gRPC service descriptors (reflection)
- Mobile clients often reveal extra endpoints/roles in app bundles or network logs
### Signals
- 401/403 on UI but 200 via direct API call; differing status codes across transports
- Actions succeed via background jobs when direct call is denied
- Changing only headers (role/org) alters access without token change
## Key Vulnerabilities
### Verb Drift and Aliases
- Alternate methods: GET performing state change; POST vs PUT vs PATCH differences; X-HTTP-Method-Override/_method - Alternate methods: GET performing state change; POST vs PUT vs PATCH differences; X-HTTP-Method-Override/_method
- Alternate endpoints performing the same action with weaker checks (legacy vs v2, mobile vs web) - Alternate endpoints performing the same action with weaker checks (legacy vs v2, mobile vs web)
</verb_drift_and_aliases>
<edge_vs_core_mismatch> ### Edge vs Core Mismatch
- Edge blocks an action but core service RPC accepts it directly; call internal service via exposed API route or SSRF - Edge blocks an action but core service RPC accepts it directly; call internal service via exposed API route or SSRF
- Gateway-injected identity headers override token claims; supply conflicting headers to test precedence - Gateway-injected identity headers override token claims; supply conflicting headers to test precedence
</edge_vs_core_mismatch>
<feature_flag_bypass> ### Feature Flag Bypass
- Client-checked feature gates; call backend endpoints directly - Client-checked feature gates; call backend endpoints directly
- Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools - Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools
</feature_flag_bypass>
<batch_job_paths> ### Batch Job Paths
- Create export/import jobs where creation is allowed but finalize/approve lacks authz; finalize others' jobs - Create export/import jobs where creation is allowed but finalize/approve lacks authz; finalize others' jobs
- Replay webhooks/background tasks endpoints that perform privileged actions without verifying caller - Replay webhooks/background tasks endpoints that perform privileged actions without verifying caller
</batch_job_paths>
<content_type_paths> ### Content-Type Paths
- JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser - JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser
</content_type_paths>
</exploitation_techniques>
<advanced_techniques> ## Advanced Techniques
<graphql>
### GraphQL
- Resolver-level checks per mutation/field; do not assume top-level auth covers nested mutations or admin fields - Resolver-level checks per mutation/field; do not assume top-level auth covers nested mutations or admin fields
- Abuse aliases/batching to sneak privileged fields; persisted queries sometimes bypass auth transforms - Abuse aliases/batching to sneak privileged fields; persisted queries sometimes bypass auth transforms
- Example:
{% raw %} ```graphql
mutation Promote($id:ID!){ mutation Promote($id:ID!){
a: updateUser(id:$id, role: ADMIN){ id role } a: updateUser(id:$id, role: ADMIN){ id role }
} }
{% endraw %} ```
</graphql>
### gRPC
<grpc>
- Method-level auth via interceptors must enforce audience/roles; probe direct gRPC with tokens of lower role - Method-level auth via interceptors must enforce audience/roles; probe direct gRPC with tokens of lower role
- Reflection lists services/methods; call admin methods that the gateway hid - Reflection lists services/methods; call admin methods that the gateway hid
</grpc>
<websocket> ### WebSocket
- Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate) - Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate)
- Try emitting privileged actions after joining standard channels - Try emitting privileged actions after joining standard channels
</websocket>
<multi_tenant> ### Multi-Tenant
- Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token - Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token
</multi_tenant>
<microservices> ### Microservices
- Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz - Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz
</microservices>
<bypass_techniques> ## Bypass Techniques
<header_trust>
### Header Trust
- Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins - Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins
</header_trust>
<route_shadowing> ### Route Shadowing
- Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains - Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains
</route_shadowing>
<idempotency_and_retries> ### Idempotency and Retries
- Retry or replay finalize/approve endpoints that apply state without checking actor on each call - Retry or replay finalize/approve endpoints that apply state without checking actor on each call
</idempotency_and_retries>
<cache_key_confusion> ### Cache Key Confusion
- Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps - Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps
</cache_key_confusion>
</bypass_techniques>
<validation> ## Testing Methodology
1. Show a lower-privileged principal successfully invokes a restricted action (same inputs) while the proper role succeeds and another lower role fails.
2. Provide evidence across at least two transports or encodings demonstrating inconsistent enforcement. 1. **Build Actor × Action matrix** - Unauth, basic, premium, staff/admin; enumerate actions per role
3. Demonstrate that removing/altering client-side gates (buttons/flags) does not affect backend success. 2. **Obtain tokens/sessions** - For each role
4. Include durable state change proof: before/after snapshots, audit logs, and authoritative sources. 3. **Exercise every action** - Across all transports and encodings (JSON, form, multipart), including method overrides
</validation> 4. **Vary headers and selectors** - Org/tenant/project; test behind gateway vs direct-to-service
5. **Include background flows** - Job creation/finalization, webhooks, queues; confirm re-validation
## Validation
1. Show a lower-privileged principal successfully invokes a restricted action (same inputs) while the proper role succeeds and another lower role fails
2. Provide evidence across at least two transports or encodings demonstrating inconsistent enforcement
3. Demonstrate that removing/altering client-side gates (buttons/flags) does not affect backend success
4. Include durable state change proof: before/after snapshots, audit logs, and authoritative sources
## False Positives
<false_positives>
- Read-only endpoints mislabeled as admin but publicly documented - Read-only endpoints mislabeled as admin but publicly documented
- Feature toggles intentionally open to all roles for preview/beta with clear policy - Feature toggles intentionally open to all roles for preview/beta with clear policy
- Simulated environments where admin endpoints are stubbed with no side effects - Simulated environments where admin endpoints are stubbed with no side effects
</false_positives>
<impact> ## Impact
- Privilege escalation to admin/staff actions - Privilege escalation to admin/staff actions
- Monetary/state impact: refunds/credits/approvals without authorization - Monetary/state impact: refunds/credits/approvals without authorization
- Tenant-wide configuration changes, impersonation, or data deletion - Tenant-wide configuration changes, impersonation, or data deletion
- Compliance and audit violations due to bypassed approval workflows - Compliance and audit violations due to bypassed approval workflows
</impact>
<pro_tips> ## Pro Tips
1. Start from the role matrix; test every action with basic vs admin tokens across REST/GraphQL/gRPC.
2. Diff middleware stacks between routes; weak chains often exist on legacy or alternate encodings.
3. Inspect gateways for identity header injection; never trust client-provided identity.
4. Treat jobs/webhooks as first-class: finalize/approve must re-check the actor.
5. Prefer minimal PoCs: one request that flips a privileged field or invokes an admin method with a basic token.
</pro_tips>
<remember>Authorization must bind the actor to the specific action at the service boundary on every request and message. UI gates, gateways, or prior steps do not substitute for function-level checks.</remember> 1. Start from the role matrix; test every action with basic vs admin tokens across REST/GraphQL/gRPC
</broken_function_level_authorization_guide> 2. Diff middleware stacks between routes; weak chains often exist on legacy or alternate encodings
3. Inspect gateways for identity header injection; never trust client-provided identity
4. Treat jobs/webhooks as first-class: finalize/approve must re-check the actor
5. Prefer minimal PoCs: one request that flips a privileged field or invokes an admin method with a basic token
## Summary
Authorization must bind the actor to the specific action at the service boundary on every request and message. UI gates, gateways, or prior steps do not substitute for function-level checks.

View File

@@ -1,46 +1,23 @@
<business_logic_flaws_guide> ---
<title>BUSINESS LOGIC FLAWS</title> name: business-logic
description: Business logic testing for workflow bypass, state manipulation, and domain invariant violations
---
<critical>Business logic flaws exploit intended functionality to violate domain invariants: move money without paying, exceed limits, retain privileges, or bypass reviews. They require a model of the business, not just payloads.</critical> # Business Logic Flaws
Business logic flaws exploit intended functionality to violate domain invariants: move money without paying, exceed limits, retain privileges, or bypass reviews. They require a model of the business, not just payloads.
## Attack Surface
<scope>
- Financial logic: pricing, discounts, payments, refunds, credits, chargebacks - Financial logic: pricing, discounts, payments, refunds, credits, chargebacks
- Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion - Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion
- Authorization-by-logic: feature gates, role transitions, approval workflows - Authorization-by-logic: feature gates, role transitions, approval workflows
- Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing - Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing
- Multi-tenant isolation: cross-organization data or action bleed - Multi-tenant isolation: cross-organization data or action bleed
- Event-driven flows: jobs, webhooks, sagas, compensations, idempotency - Event-driven flows: jobs, webhooks, sagas, compensations, idempotency
</scope>
<methodology> ## High-Value Targets
1. Enumerate a state machine per critical workflow (states, transitions, pre/post-conditions). Note invariants (e.g., "refund ≤ captured amount").
2. Build an Actor × Action × Resource matrix with at least: unauth, basic user, premium, staff/admin; identify actions per role.
3. For each transition, test step skipping, repetition, reordering, and late mutation (modify inputs after validation but before commit).
4. Introduce time, concurrency, and channel variance: repeat with parallel requests, different content-types, mobile/web/API/GraphQL.
5. Validate persistence boundaries: verify that all services, queues, and jobs re-enforce invariants (no trust in upstream validation).
</methodology>
<discovery_techniques>
<workflow_mapping>
- Derive endpoints from the UI and proxy/network logs; map hidden/undocumented API calls, especially finalize/confirm endpoints
- Identify tokens/flags: stepToken, paymentIntentId, orderStatus, reviewState, approvalId; test reuse across users/sessions
- Document invariants: conservation of value (ledger balance), uniqueness (idempotency), monotonicity (non-decreasing counters), exclusivity (one active subscription)
</workflow_mapping>
<input_surface>
- Hidden fields and client-computed totals; server must recompute on trusted sources
- Alternate encodings and shapes: arrays instead of scalars, objects with unexpected keys, null/empty/0/negative, scientific notation
- Business selectors: currency, locale, timezone, tax region; vary to trigger rounding and ruleset changes
</input_surface>
<state_time_axes>
- Replays: resubmit stale finalize/confirm requests
- Out-of-order: call finalize before verify; refund before capture; cancel after ship
- Time windows: end-of-day/month cutovers, daylight saving, grace periods, trial expiry edges
</state_time_axes>
</discovery_techniques>
<high_value_targets>
- Pricing/cart: price locks, quote to order, tax/shipping computation - Pricing/cart: price locks, quote to order, tax/shipping computation
- Discount engines: stacking, mutual exclusivity, scope (cart vs item), once-per-user enforcement - Discount engines: stacking, mutual exclusivity, scope (cart vs item), once-per-user enforcement
- Payments: auth/capture/void/refund sequences, partials, split tenders, chargebacks, idempotency keys - Payments: auth/capture/void/refund sequences, partials, split tenders, chargebacks, idempotency keys
@@ -49,123 +26,153 @@
- Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges - Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges
- Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags - Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags
- Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters - Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters
</high_value_targets>
<exploitation_techniques> ## Reconnaissance
<state_machine_abuse>
### Workflow Mapping
- Derive endpoints from the UI and proxy/network logs; map hidden/undocumented API calls, especially finalize/confirm endpoints
- Identify tokens/flags: stepToken, paymentIntentId, orderStatus, reviewState, approvalId; test reuse across users/sessions
- Document invariants: conservation of value (ledger balance), uniqueness (idempotency), monotonicity (non-decreasing counters), exclusivity (one active subscription)
### Input Surface
- Hidden fields and client-computed totals; server must recompute on trusted sources
- Alternate encodings and shapes: arrays instead of scalars, objects with unexpected keys, null/empty/0/negative, scientific notation
- Business selectors: currency, locale, timezone, tax region; vary to trigger rounding and ruleset changes
### State and Time Axes
- Replays: resubmit stale finalize/confirm requests
- Out-of-order: call finalize before verify; refund before capture; cancel after ship
- Time windows: end-of-day/month cutovers, daylight saving, grace periods, trial expiry edges
## Key Vulnerabilities
### State Machine Abuse
- Skip or reorder steps via direct API calls; verify server enforces preconditions on each transition - Skip or reorder steps via direct API calls; verify server enforces preconditions on each transition
- Replay prior steps with altered parameters (e.g., swap price after approval but before capture) - Replay prior steps with altered parameters (e.g., swap price after approval but before capture)
- Split a single constrained action into many sub-actions under the threshold (limit slicing) - Split a single constrained action into many sub-actions under the threshold (limit slicing)
</state_machine_abuse>
<concurrency_and_idempotency> ### Concurrency and Idempotency
- Parallelize identical operations to bypass atomic checks (create, apply, redeem, transfer) - Parallelize identical operations to bypass atomic checks (create, apply, redeem, transfer)
- Abuse idempotency: key scoped to path but not principal → reuse other users' keys; or idempotency stored only in cache - Abuse idempotency: key scoped to path but not principal → reuse other users' keys; or idempotency stored only in cache
- Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund - Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund
</concurrency_and_idempotency>
<numeric_and_currency> ### Numeric and Currency
- Floating point vs decimal rounding; rounding/truncation favoring attacker at boundaries - Floating point vs decimal rounding; rounding/truncation favoring attacker at boundaries
- Cross-currency arbitrage: buy in currency A, refund in B at stale rates; tax rounding per-item vs per-order - Cross-currency arbitrage: buy in currency A, refund in B at stale rates; tax rounding per-item vs per-order
- Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails - Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails
</numeric_and_currency>
<quotas_limits_inventory> ### Quotas, Limits, and Inventory
- Off-by-one and time-bound resets (UTC vs local); pre-warm at T-1s and post-fire at T+1s - Off-by-one and time-bound resets (UTC vs local); pre-warm at T-1s and post-fire at T+1s
- Reservation/hold leaks: reserve multiple, complete one, release not enforced; backorder logic inconsistencies - Reservation/hold leaks: reserve multiple, complete one, release not enforced; backorder logic inconsistencies
- Distributed counters without strong consistency enabling double-consumption - Distributed counters without strong consistency enabling double-consumption
</quotas_limits_inventory>
<refunds_chargebacks> ### Refunds and Chargebacks
- Double-refund: refund via UI and support tool; refund partials summing above captured amount - Double-refund: refund via UI and support tool; refund partials summing above captured amount
- Refund after benefits consumed (downloaded digital goods, shipped items) due to missing post-consumption checks - Refund after benefits consumed (downloaded digital goods, shipped items) due to missing post-consumption checks
</refunds_chargebacks>
<feature_gates_and_roles> ### Feature Gates and Roles
- Feature flags enforced client-side or at edge but not in core services; toggle names guessed or fallback to default-enabled - Feature flags enforced client-side or at edge but not in core services; toggle names guessed or fallback to default-enabled
- Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion) - Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion)
</feature_gates_and_roles>
<advanced_techniques> ## Advanced Techniques
<event_driven_sagas>
### Event-Driven Sagas
- Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation - Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation
- Outbox/Inbox patterns missing idempotency → duplicate downstream side effects - Outbox/Inbox patterns missing idempotency → duplicate downstream side effects
- Cron/backfill jobs operating outside request-time authorization; mutate state broadly - Cron/backfill jobs operating outside request-time authorization; mutate state broadly
</event_driven_sagas>
<microservices_boundaries> ### Microservices Boundaries
- Cross-service assumption mismatch: one service validates total, another trusts line items; alter between calls - Cross-service assumption mismatch: one service validates total, another trusts line items; alter between calls
- Header trust: internal services trusting X-Role or X-User-Id from untrusted edges - Header trust: internal services trusting X-Role or X-User-Id from untrusted edges
- Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state - Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state
</microservices_boundaries>
<multi_tenant_isolation> ### Multi-Tenant Isolation
- Tenant-scoped counters and credits updated without tenant key in the where-clause; leak across orgs - Tenant-scoped counters and credits updated without tenant key in the where-clause; leak across orgs
- Admin aggregate views allowing actions that impact other tenants due to missing per-tenant enforcement - Admin aggregate views allowing actions that impact other tenants due to missing per-tenant enforcement
</multi_tenant_isolation>
<bypass_techniques> ## Bypass Techniques
- Content-type switching (json/form/multipart) to hit different code paths
- Content-type switching (JSON/form/multipart) to hit different code paths
- Method alternation (GET performing state change; overrides via X-HTTP-Method-Override) - Method alternation (GET performing state change; overrides via X-HTTP-Method-Override)
- Client recomputation: totals, taxes, discounts computed on client and accepted by server - Client recomputation: totals, taxes, discounts computed on client and accepted by server
- Cache/gateway differentials: stale decisions from CDN/APIM that are not identity-aware - Cache/gateway differentials: stale decisions from CDN/APIM that are not identity-aware
</bypass_techniques>
<special_contexts> ## Special Contexts
<ecommerce>
### E-commerce
- Stack incompatible discounts via parallel apply; remove qualifying item after discount applied; retain free shipping after cart changes - Stack incompatible discounts via parallel apply; remove qualifying item after discount applied; retain free shipping after cart changes
- Modify shipping tier post-quote; abuse returns to keep product and refund - Modify shipping tier post-quote; abuse returns to keep product and refund
</ecommerce>
<banking_fintech> ### Banking/Fintech
- Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies - Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies
- Exploit grace periods on holds/authorizations to withdraw again before settlement - Exploit grace periods on holds/authorizations to withdraw again before settlement
</banking_fintech>
<saas_b2b> ### SaaS/B2B
- Seat licensing: race seat assignment to exceed purchased seats; stale license checks in background tasks - Seat licensing: race seat assignment to exceed purchased seats; stale license checks in background tasks
- Usage metering: report late or duplicate usage to avoid billing or to over-consume - Usage metering: report late or duplicate usage to avoid billing or to over-consume
</saas_b2b>
</special_contexts>
<chaining_attacks> ## Chaining Attacks
- Business logic + race: duplicate benefits before state updates - Business logic + race: duplicate benefits before state updates
- Business logic + IDOR: operate on others' resources once a workflow leak reveals IDs - Business logic + IDOR: operate on others' resources once a workflow leak reveals IDs
- Business logic + CSRF: force a victim to complete a sensitive step sequence - Business logic + CSRF: force a victim to complete a sensitive step sequence
</chaining_attacks>
<validation> ## Testing Methodology
1. Show an invariant violation (e.g., two refunds for one charge, negative inventory, exceeding quotas).
2. Provide side-by-side evidence for intended vs abused flows with the same principal. 1. **Enumerate state machine** - Per critical workflow (states, transitions, pre/post-conditions); note invariants
3. Demonstrate durability: the undesired state persists and is observable in authoritative sources (ledger, emails, admin views). 2. **Build Actor × Action × Resource matrix** - Unauth, basic user, premium, staff/admin; identify actions per role
4. Quantify impact per action and at scale (unit loss × feasible repetitions). 3. **Test transitions** - Step skipping, repetition, reordering, late mutation
</validation> 4. **Introduce variance** - Time, concurrency, channel (mobile/web/API/GraphQL), content-types
5. **Validate persistence boundaries** - All services, queues, and jobs re-enforce invariants
## Validation
1. Show an invariant violation (e.g., two refunds for one charge, negative inventory, exceeding quotas)
2. Provide side-by-side evidence for intended vs abused flows with the same principal
3. Demonstrate durability: the undesired state persists and is observable in authoritative sources (ledger, emails, admin views)
4. Quantify impact per action and at scale (unit loss × feasible repetitions)
## False Positives
<false_positives>
- Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits) - Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits)
- Visual-only inconsistencies with no durable or exploitable state change - Visual-only inconsistencies with no durable or exploitable state change
- Admin-only operations with proper audit and approvals - Admin-only operations with proper audit and approvals
</false_positives>
<impact> ## Impact
- Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption) - Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption)
- Regulatory/contractual violations (billing accuracy, consumer protection) - Regulatory/contractual violations (billing accuracy, consumer protection)
- Denial of inventory/services to legitimate users through resource exhaustion - Denial of inventory/services to legitimate users through resource exhaustion
- Privilege retention or unauthorized access to premium features - Privilege retention or unauthorized access to premium features
</impact>
<pro_tips> ## Pro Tips
1. Start from invariants and ledgers, not UI—prove conservation of value breaks.
2. Test with time and concurrency; many bugs only appear under pressure.
3. Recompute totals server-side; never accept client math—flag when you observe otherwise.
4. Treat idempotency and retries as first-class: verify key scope and persistence.
5. Probe background workers and webhooks separately; they often skip auth and rule checks.
6. Validate role/feature gates at the service that mutates state, not only at the edge.
7. Explore end-of-period edges (month-end, trial end, DST) for rounding and window issues.
8. Use minimal, auditable PoCs that demonstrate durable state change and exact loss.
9. Chain with authorization tests (IDOR/Function-level access) to magnify impact.
10. When in doubt, map the state machine; gaps appear where transitions lack server-side guards.
</pro_tips>
<remember>Business logic security is the enforcement of domain invariants under adversarial sequencing, timing, and inputs. If any step trusts the client or prior steps, expect abuse.</remember> 1. Start from invariants and ledgers, not UI—prove conservation of value breaks
</business_logic_flaws_guide> 2. Test with time and concurrency; many bugs only appear under pressure
3. Recompute totals server-side; never accept client math—flag when you observe otherwise
4. Treat idempotency and retries as first-class: verify key scope and persistence
5. Probe background workers and webhooks separately; they often skip auth and rule checks
6. Validate role/feature gates at the service that mutates state, not only at the edge
7. Explore end-of-period edges (month-end, trial end, DST) for rounding and window issues
8. Use minimal, auditable PoCs that demonstrate durable state change and exact loss
9. Chain with authorization tests (IDOR/Function-level access) to magnify impact
10. When in doubt, map the state machine; gaps appear where transitions lack server-side guards
## Summary
Business logic security is the enforcement of domain invariants under adversarial sequencing, timing, and inputs. If any step trusts the client or prior steps, expect abuse.

View File

@@ -1,174 +0,0 @@
<csrf_vulnerability_guide>
<title>CROSS-SITE REQUEST FORGERY (CSRF)</title>
<critical>CSRF abuses ambient authority (cookies, HTTP auth) across origins. Do not rely on CORS alone; enforce non-replayable tokens and strict origin checks for every state change.</critical>
<scope>
- Web apps with cookie-based sessions and HTTP auth
- JSON/REST, GraphQL (GET/persisted queries), file upload endpoints
- Authentication flows: login/logout, password/email change, MFA toggles
- OAuth/OIDC: authorize, token, logout, disconnect/connect
</scope>
<methodology>
1. Inventory all state-changing endpoints (including admin/staff) and note method, content-type, and whether they are reachable via top-level navigation or simple requests (no preflight).
2. For each, determine session model (cookies with SameSite attrs, custom headers, tokens) and whether server enforces anti-CSRF tokens and Origin/Referer.
3. Attempt preflightless delivery (form POST, text/plain, multipart/form-data) and top-level GET navigation.
4. Validate across browsers; behavior differs by SameSite and navigation context.
</methodology>
<high_value_targets>
- Credentials and profile changes (email/password/phone)
- Payment and money movement, subscription/plan changes
- API key/secret generation, PAT rotation, SSH keys
- 2FA/TOTP enable/disable; backup codes; device trust
- OAuth connect/disconnect; logout; account deletion
- Admin/staff actions and impersonation flows
- File uploads/deletes; access control changes
</high_value_targets>
<discovery_techniques>
<session_and_cookies>
- Inspect cookies: HttpOnly, Secure, SameSite (Strict/Lax/None). Note that Lax allows cookies on top-level cross-site GET; None requires Secure.
- Determine if Authorization headers or bearer tokens are used (generally not CSRF-prone) versus cookies (CSRF-prone).
</session_and_cookies>
<token_and_header_checks>
- Locate anti-CSRF tokens (hidden inputs, meta tags, custom headers). Test removal, reuse across requests, reuse across sessions, and binding to method/path.
- Verify server checks Origin and/or Referer on state changes; test null/missing and cross-origin values.
</token_and_header_checks>
<method_and_content_types>
- Confirm whether GET, HEAD, or OPTIONS perform state changes.
- Try simple content-types to avoid preflight: application/x-www-form-urlencoded, multipart/form-data, text/plain.
- Probe parsers that auto-coerce text/plain or form-encoded bodies into JSON.
</method_and_content_types>
<cors_profile>
- Identify Access-Control-Allow-Origin and -Credentials. Overly permissive CORS is not a CSRF fix and can turn CSRF into data exfiltration.
- Test per-endpoint CORS differences; preflight vs simple request behavior can diverge.
</cors_profile>
</discovery_techniques>
<exploitation_techniques>
<navigation_csrf>
- Auto-submitting form to target origin; works when cookies are sent and no token/origin checks are enforced.
- Top-level GET navigation can trigger state if server misuses GET or links actions to GET callbacks.
</navigation_csrf>
<simple_ct_csrf>
- application/x-www-form-urlencoded and multipart/form-data POSTs do not require preflight; prefer these encodings.
- text/plain form bodies can slip through validators and be parsed server-side.
</simple_ct_csrf>
<json_csrf>
- If server parses JSON from text/plain or form-encoded bodies, craft parameters to reconstruct JSON server-side.
- Some frameworks accept JSON keys via form fields (e.g., {% raw %}data[foo]=bar{% endraw %}) or treat duplicate keys leniently.
</json_csrf>
<login_logout_csrf>
- Force logout to clear CSRF tokens, then chain login CSRF to bind victim to attackers account.
- Login CSRF: submit attacker credentials to victims browser; later actions occur under attackers account.
</login_logout_csrf>
<oauth_oidc_flows>
- Abuse authorize/logout endpoints reachable via GET or form POST without origin checks; exploit relaxed SameSite on top-level navigations.
- Open redirects or loose redirect_uri validation can chain with CSRF to force unintended authorizations.
</oauth_oidc_flows>
<file_and_action_endpoints>
- File upload/delete often lack token checks; forge multipart requests to modify storage.
- Admin actions exposed as simple POST links are frequently CSRFable.
</file_and_action_endpoints>
</exploitation_techniques>
<advanced_techniques>
<samesite_nuance>
- Lax-by-default cookies are sent on top-level cross-site GET but not POST; exploit GET state changes and GET-based confirmation steps.
- Legacy or nonstandard clients may ignore SameSite; validate across browsers/devices.
</samesite_nuance>
<origin_referer_obfuscation>
- Sandbox/iframes can produce null Origin; some frameworks incorrectly accept null.
- about:blank/data: URLs alter Referer; ensure server requires explicit Origin/Referer match.
</origin_referer_obfuscation>
<method_override>
- Backends honoring _method or X-HTTP-Method-Override may allow destructive actions through a simple POST.
</method_override>
<graphql_csrf>
- If queries/mutations are allowed via GET or persisted queries, exploit top-level navigation with encoded payloads.
- Batched operations may hide mutations within a nominally safe request.
</graphql_csrf>
<websocket_csrf>
- Browsers send cookies on WebSocket handshake; enforce Origin checks server-side. Without them, cross-site pages can open authenticated sockets and issue actions.
</websocket_csrf>
</advanced_techniques>
<bypass_techniques>
<token_weaknesses>
- Accepting missing/empty tokens; tokens not tied to session, user, or path; tokens reused indefinitely; tokens in GET.
- Double-submit cookie without Secure/HttpOnly, or with predictable token sources.
</token_weaknesses>
<content_type_switching>
- Switch between form, multipart, and text/plain to reach different code paths and validators.
- Use duplicate keys and array shapes to confuse parsers.
</content_type_switching>
<header_manipulation>
- Strip Referer via meta refresh or navigate from about:blank; test null Origin acceptance.
- Leverage misconfigured CORS to add custom headers that servers mistakenly treat as CSRF tokens.
</header_manipulation>
</bypass_techniques>
<special_contexts>
<mobile_spa>
- Deep links and embedded WebViews may auto-send cookies; trigger actions via crafted intents/links.
- SPAs that rely solely on bearer tokens are less CSRF-prone, but hybrid apps mixing cookies and APIs can still be vulnerable.
</mobile_spa>
<integrations>
- Webhooks and back-office tools sometimes expose state-changing GETs intended for staff; confirm CSRF defenses there too.
</integrations>
</special_contexts>
<chaining_attacks>
- CSRF + IDOR: force actions on other users' resources once references are known.
- CSRF + Clickjacking: guide user interactions to bypass UI confirmations.
- CSRF + OAuth mix-up: bind victim sessions to unintended clients.
</chaining_attacks>
<validation>
1. Demonstrate a cross-origin page that triggers a state change without user interaction beyond visiting.
2. Show that removing the anti-CSRF control (token/header) is accepted, or that Origin/Referer are not verified.
3. Prove behavior across at least two browsers or contexts (top-level nav vs XHR/fetch).
4. Provide before/after state evidence for the same account.
5. If defenses exist, show the exact condition under which they are bypassed (content-type, method override, null Origin).
</validation>
<false_positives>
- Token verification present and required; Origin/Referer enforced consistently.
- No cookies sent on cross-site requests (SameSite=Strict, no HTTP auth) and no state change via simple requests.
- Only idempotent, non-sensitive operations affected.
</false_positives>
<impact>
- Account state changes (email/password/MFA), session hijacking via login CSRF, financial operations, administrative actions.
- Durable authorization changes (role/permission flips, key rotations) and data loss.
</impact>
<pro_tips>
1. Prefer preflightless vectors (form-encoded, multipart, text/plain) and top-level GET if available.
2. Test login/logout, OAuth connect/disconnect, and account linking first.
3. Validate Origin/Referer behavior explicitly; do not assume frameworks enforce them.
4. Toggle SameSite and observe differences across navigation vs XHR.
5. For GraphQL, attempt GET queries or persisted queries that carry mutations.
6. Always try method overrides and parser differentials.
7. Combine with clickjacking when visual confirmations block CSRF.
</pro_tips>
<remember>CSRF is eliminated only when state changes require a secret the attacker cannot supply and the server verifies the callers origin. Tokens and Origin checks must hold across methods, content-types, and transports.</remember>
</csrf_vulnerability_guide>

View File

@@ -0,0 +1,198 @@
---
name: csrf
description: CSRF testing covering token bypass, SameSite cookies, CORS misconfigurations, and state-changing request abuse
---
# CSRF
Cross-site request forgery abuses ambient authority (cookies, HTTP auth) across origins. Do not rely on CORS alone; enforce non-replayable tokens and strict origin checks for every state change.
## Attack Surface
**Session Types**
- Web apps with cookie-based sessions and HTTP auth
- JSON/REST, GraphQL (GET/persisted queries), file upload endpoints
**Authentication Flows**
- Login/logout, password/email change, MFA toggles
**OAuth/OIDC**
- Authorize, token, logout, disconnect/connect endpoints
## High-Value Targets
- Credentials and profile changes (email/password/phone)
- Payment and money movement, subscription/plan changes
- API key/secret generation, PAT rotation, SSH keys
- 2FA/TOTP enable/disable; backup codes; device trust
- OAuth connect/disconnect; logout; account deletion
- Admin/staff actions and impersonation flows
- File uploads/deletes; access control changes
## Reconnaissance
### Session and Cookies
- Inspect cookies: HttpOnly, Secure, SameSite (Strict/Lax/None)
- Lax allows cookies on top-level cross-site GET; None requires Secure
- Determine if Authorization headers or bearer tokens are used (generally not CSRF-prone) versus cookies (CSRF-prone)
### Token and Header Checks
- Locate anti-CSRF tokens (hidden inputs, meta tags, custom headers)
- Test removal, reuse across requests, reuse across sessions, binding to method/path
- Verify server checks Origin and/or Referer on state changes
- Test null/missing and cross-origin values
### Method and Content-Types
- Confirm whether GET, HEAD, or OPTIONS perform state changes
- Try simple content-types to avoid preflight: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`
- Probe parsers that auto-coerce `text/plain` or form-encoded bodies into JSON
### CORS Profile
- Identify `Access-Control-Allow-Origin` and `-Credentials`
- Overly permissive CORS is not a CSRF fix and can turn CSRF into data exfiltration
- Test per-endpoint CORS differences; preflight vs simple request behavior can diverge
## Key Vulnerabilities
### Navigation CSRF
- Auto-submitting form to target origin; works when cookies are sent and no token/origin checks are enforced
- Top-level GET navigation can trigger state if server misuses GET or links actions to GET callbacks
### Simple Content-Type CSRF
- `application/x-www-form-urlencoded` and `multipart/form-data` POSTs do not require preflight
- `text/plain` form bodies can slip through validators and be parsed server-side
### JSON CSRF
- If server parses JSON from `text/plain` or form-encoded bodies, craft parameters to reconstruct JSON
- Some frameworks accept JSON keys via form fields (e.g., `data[foo]=bar`) or treat duplicate keys leniently
### Login/Logout CSRF
- Force logout to clear CSRF tokens, then chain login CSRF to bind victim to attacker's account
- Login CSRF: submit attacker credentials to victim's browser; later actions occur under attacker's account
### OAuth/OIDC Flows
- Abuse authorize/logout endpoints reachable via GET or form POST without origin checks
- Exploit relaxed SameSite on top-level navigations
- Open redirects or loose redirect_uri validation can chain with CSRF to force unintended authorizations
### File and Action Endpoints
- File upload/delete often lack token checks; forge multipart requests to modify storage
- Admin actions exposed as simple POST links are frequently CSRFable
### GraphQL CSRF
- If queries/mutations are allowed via GET or persisted queries, exploit top-level navigation with encoded payloads
- Batched operations may hide mutations within a nominally safe request
### WebSocket CSRF
- Browsers send cookies on WebSocket handshake
- Enforce Origin checks server-side; without them, cross-site pages can open authenticated sockets and issue actions
## Bypass Techniques
### SameSite Nuance
- Lax-by-default cookies are sent on top-level cross-site GET but not POST
- Exploit GET state changes and GET-based confirmation steps
- Legacy or nonstandard clients may ignore SameSite; validate across browsers/devices
### Origin/Referer Obfuscation
- Sandbox/iframes can produce null Origin; some frameworks incorrectly accept null
- `about:blank`/`data:` URLs alter Referer
- Ensure server requires explicit Origin/Referer match
### Method Override
- Backends honoring `_method` or `X-HTTP-Method-Override` may allow destructive actions through a simple POST
### Token Weaknesses
- Accepting missing/empty tokens
- Tokens not tied to session, user, or path
- Tokens reused indefinitely; tokens in GET
- Double-submit cookie without Secure/HttpOnly, or with predictable token sources
### Content-Type Switching
- Switch between form, multipart, and `text/plain` to reach different code paths
- Use duplicate keys and array shapes to confuse parsers
### Header Manipulation
- Strip Referer via meta refresh or navigate from `about:blank`
- Test null Origin acceptance
- Leverage misconfigured CORS to add custom headers that servers mistakenly treat as CSRF tokens
## Special Contexts
### Mobile/SPA
- Deep links and embedded WebViews may auto-send cookies; trigger actions via crafted intents/links
- SPAs that rely solely on bearer tokens are less CSRF-prone, but hybrid apps mixing cookies and APIs can still be vulnerable
### Integrations
- Webhooks and back-office tools sometimes expose state-changing GETs intended for staff
- Confirm CSRF defenses there too
## Chaining Attacks
- CSRF + IDOR: force actions on other users' resources once references are known
- CSRF + Clickjacking: guide user interactions to bypass UI confirmations
- CSRF + OAuth mix-up: bind victim sessions to unintended clients
## Testing Methodology
1. **Inventory endpoints** - All state-changing endpoints including admin/staff
2. **Note request details** - Method, content-type, whether reachable via simple requests
3. **Assess session model** - Cookies with SameSite attrs, custom headers, tokens
4. **Check defenses** - Anti-CSRF tokens and Origin/Referer enforcement
5. **Attempt preflightless delivery** - Form POST, text/plain, multipart/form-data
6. **Test navigation** - Top-level GET navigation
7. **Cross-browser validation** - Behavior differs by SameSite and navigation context
## Validation
1. Demonstrate a cross-origin page that triggers a state change without user interaction beyond visiting
2. Show that removing the anti-CSRF control (token/header) is accepted, or that Origin/Referer are not verified
3. Prove behavior across at least two browsers or contexts (top-level nav vs XHR/fetch)
4. Provide before/after state evidence for the same account
5. If defenses exist, show the exact condition under which they are bypassed (content-type, method override, null Origin)
## False Positives
- Token verification present and required; Origin/Referer enforced consistently
- No cookies sent on cross-site requests (SameSite=Strict, no HTTP auth) and no state change via simple requests
- Only idempotent, non-sensitive operations affected
## Impact
- Account state changes (email/password/MFA), session hijacking via login CSRF
- Financial operations, administrative actions
- Durable authorization changes (role/permission flips, key rotations) and data loss
## Pro Tips
1. Prefer preflightless vectors (form-encoded, multipart, text/plain) and top-level GET if available
2. Test login/logout, OAuth connect/disconnect, and account linking first
3. Validate Origin/Referer behavior explicitly; do not assume frameworks enforce them
4. Toggle SameSite and observe differences across navigation vs XHR
5. For GraphQL, attempt GET queries or persisted queries that carry mutations
6. Always try method overrides and parser differentials
7. Combine with clickjacking when visual confirmations block CSRF
## Summary
CSRF is eliminated only when state changes require a secret the attacker cannot supply and the server verifies the caller's origin. Tokens and Origin checks must hold across methods, content-types, and transports.

View File

@@ -1,195 +0,0 @@
<idor_vulnerability_guide>
<title>INSECURE DIRECT OBJECT REFERENCE (IDOR)</title>
<critical>Object- and function-level authorization failures (BOLA/IDOR) routinely lead to cross-account data exposure and unauthorized state changes across APIs, web, mobile, and microservices. Treat every object reference as untrusted until proven bound to the caller.</critical>
<scope>
- Horizontal access: access another subject's objects of the same type
- Vertical access: access privileged objects/actions (admin-only, staff-only)
- Cross-tenant access: break isolation boundaries in multi-tenant systems
- Cross-service access: token or context accepted by the wrong service
</scope>
<methodology>
1. Build a Subject × Object × Action matrix (who can do what to which resource).
2. For each resource type, obtain at least two principals: owner and non-owner (plus admin/staff if applicable). Capture at least one valid object ID per principal.
3. Exercise every action (R/W/D/Export) while swapping IDs, tokens, tenants, and channels (web, mobile, API, GraphQL, WebSocket, gRPC).
4. Track consistency: the same rule must hold regardless of transport, content-type, serialization, or gateway.
</methodology>
<discovery_techniques>
<parameter_analysis>
- Object references appear in: paths, query params, JSON bodies, form-data, headers, cookies, JWT claims, GraphQL arguments, WebSocket messages, gRPC messages
- Identifier forms: integers, UUID/ULID/CUID, Snowflake, slugs, composite keys (e.g., {orgId}:{userId}), opaque tokens, base64/hex-encoded blobs
- Relationship references: parentId, ownerId, accountId, tenantId, organization, teamId, projectId, subscriptionId
- Expansion/projection knobs: fields, include, expand, projection, with, select, populate (often bypass authorization in resolvers or serializers)
- Pagination/cursors: page[offset], page[limit], cursor, nextPageToken (often reveal or accept cross-tenant/state)
</parameter_analysis>
<advanced_enumeration>
- Alternate types: {% raw %}{"id":123}{% endraw} vs {% raw %}{"id":"123"}{% endraw}, arrays vs scalars, objects vs scalars, null/empty/0/-1/MAX_INT, scientific notation, overflows, unknown attributes retained by backend
- Duplicate keys/parameter pollution: id=1&id=2, JSON duplicate keys {% raw %}{"id":1,"id":2}{% endraw} (parser precedence differences)
- Case/aliasing: userId vs userid vs USER_ID; alt names like resourceId, targetId, account
- Path traversal-like in virtual file systems: /files/user_123/../../user_456/report.csv
- Directory/list endpoints as seeders: search/list/suggest/export often leak object IDs for secondary exploitation
</advanced_enumeration>
</discovery_techniques>
<high_value_targets>
- Exports/backups/reporting endpoints (CSV/PDF/ZIP)
- Messaging/mailbox/notifications, audit logs, activity feeds
- Billing: invoices, payment methods, transactions, credits
- Healthcare/education records, HR documents, PII/PHI/PCI
- Admin/staff tools, impersonation/session management
- File/object storage keys (S3/GCS signed URLs, share links)
- Background jobs: import/export job IDs, task results
- Multi-tenant resources: organizations, workspaces, projects
</high_value_targets>
<exploitation_techniques>
<horizontal_vertical>
- Swap object IDs between principals using the same token to probe horizontal access; then repeat with lower-privilege tokens to probe vertical access
- Target partial updates (PATCH, JSON Patch/JSON Merge Patch) for silent unauthorized modifications
</horizontal_vertical>
<bulk_and_batch>
- Batch endpoints (bulk update/delete) often validate only the first element; include cross-tenant IDs mid-array
- CSV/JSON imports referencing foreign object IDs (ownerId, orgId) may bypass create-time checks
</bulk_and_batch>
<secondary_idor>
- Use list/search endpoints, notifications, emails, webhooks, and client logs to collect valid IDs, then fetch or mutate those objects directly
- Pagination/cursor manipulation to skip filters and pull other users' pages
</secondary_idor>
<job_task_objects>
- Access job/task IDs from one user to retrieve results for another (export/{jobId}/download, reports/{taskId})
- Cancel/approve someone else's jobs by referencing their task IDs
</job_task_objects>
<file_object_storage>
- Direct object paths or weakly scoped signed URLs; attempt key prefix changes, content-disposition tricks, or stale signatures reused across tenants
- Replace share tokens with tokens from other tenants; try case/URL-encoding variations
</file_object_storage>
</exploitation_techniques>
<advanced_techniques>
<graphql>
- Enforce resolver-level checks: do not rely on a top-level gate. Verify field and edge resolvers bind the resource to the caller on every hop
- Abuse batching/aliases to retrieve multiple users' nodes in one request and compare responses
- Global node patterns (Relay): decode base64 IDs and swap raw IDs; test {% raw %}node(id: "...base64..."){...}{% endraw %}
- Overfetching via fragments on privileged types; verify hidden fields cannot be queried by unprivileged callers
- Example:
{% raw %}
query IDOR {
me { id }
u1: user(id: "VXNlcjo0NTY=") { email billing { last4 } }
u2: node(id: "VXNlcjo0NTc=") { ... on User { email } }
}
{% endraw %}
</graphql>
<microservices_gateways>
- Token confusion: a token scoped for Service A accepted by Service B due to shared JWT verification but missing audience/claims checks
- Trust on headers: reverse proxies or API gateways injecting/trusting headers like X-User-Id, X-Organization-Id; try overriding or removing them
- Context loss: async consumers (queues, workers) re-process requests without re-checking authorization
</microservices_gateways>
<multi_tenant>
- Probe tenant scoping through headers, subdomains, and path params (e.g., X-Tenant-ID, org slug). Try mixing org of token with resource from another org
- Test cross-tenant reports/analytics rollups and admin views which aggregate multiple tenants
</multi_tenant>
<uuid_and_opaque_ids>
- UUID/ULID are not authorization: acquire valid IDs from logs, exports, JS bundles, analytics endpoints, emails, or public activity, then test ownership binding
- Time-based IDs (UUIDv1, ULID) may be guessable within a window; combine with leakage sources for targeted access
</uuid_and_opaque_ids>
<blind_channels>
- Use differential responses (status, size, ETag, timing) to detect existence; error shape often differs for owned vs foreign objects
- HEAD/OPTIONS, conditional requests (If-None-Match/If-Modified-Since) can confirm existence without full content
</blind_channels>
</advanced_techniques>
<bypass_techniques>
<parser_and_transport>
- Content-type switching: application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data; some paths enforce checks per parser
- Method tunneling: X-HTTP-Method-Override, _method=PATCH; or using GET on endpoints incorrectly accepting state changes
- JSON duplicate keys/array injection to bypass naive validators
</parser_and_transport>
<parameter_pollution>
- Duplicate parameters in query/body to influence server-side precedence (id=123&id=456); try both orderings
- Mix case/alias param names so gateway and backend disagree (userId vs userid)
</parameter_pollution>
<cache_and_gateway>
- CDN/proxy key confusion: responses keyed without Authorization or tenant headers expose cached objects to other users; manipulate Vary and Accept
- Redirect chains and 304/206 behaviors can leak content across tenants
</cache_and_gateway>
<race_windows>
- Time-of-check vs time-of-use: change the referenced ID between validation and execution using parallel requests
</race_windows>
</bypass_techniques>
<special_contexts>
<websocket>
- Authorization per-subscription: ensure channel/topic names cannot be guessed (user_{id}, org_{id}); subscribe/publish checks must run server-side, not only at handshake
- Try sending messages with target user IDs after subscribing to own channels
</websocket>
<grpc>
- Direct protobuf fields (owner_id, tenant_id) often bypass HTTP-layer middleware; validate references via grpcurl with tokens from different principals
</grpc>
<integrations>
- Webhooks/callbacks referencing foreign objects (e.g., invoice_id) processed without verifying ownership
- Third-party importers syncing data into wrong tenant due to missing tenant binding
</integrations>
</special_contexts>
<chaining_attacks>
- IDOR + CSRF: force victims to trigger unauthorized changes on objects you discovered
- IDOR + Stored XSS: pivot into other users' sessions through data you gained access to
- IDOR + SSRF: exfiltrate internal IDs, then access their corresponding resources
- IDOR + Race: bypass spot checks with simultaneous requests
</chaining_attacks>
<validation>
1. Demonstrate access to an object not owned by the caller (content or metadata).
2. Show the same request fails with appropriately enforced authorization when corrected.
3. Prove cross-channel consistency: same unauthorized access via at least two transports (e.g., REST and GraphQL).
4. Document tenant boundary violations (if applicable).
5. Provide reproducible steps and evidence (requests/responses for owner vs non-owner).
</validation>
<false_positives>
- Public/anonymous resources by design
- Soft-privatized data where content is already public
- Idempotent metadata lookups that do not reveal sensitive content
- Correct row-level checks enforced across all channels
</false_positives>
<impact>
- Cross-account data exposure (PII/PHI/PCI)
- Unauthorized state changes (transfers, role changes, cancellations)
- Cross-tenant data leaks violating contractual and regulatory boundaries
- Regulatory risk (GDPR/HIPAA/PCI), fraud, reputational damage
</impact>
<pro_tips>
1. Always test list/search/export endpoints first; they are rich ID seeders.
2. Build a reusable ID corpus from logs, notifications, emails, and client bundles.
3. Toggle content-types and transports; authorization middleware often differs per stack.
4. In GraphQL, validate at resolver boundaries; never trust parent auth to cover children.
5. In multi-tenant apps, vary org headers, subdomains, and path params independently.
6. Check batch/bulk operations and background job endpoints; they frequently skip per-item checks.
7. Inspect gateways for header trust and cache key configuration.
8. Treat UUIDs as untrusted; obtain them via OSINT/leaks and test binding.
9. Use timing/size/ETag differentials for blind confirmation when content is masked.
10. Prove impact with precise before/after diffs and role-separated evidence.
</pro_tips>
<remember>Authorization must bind subject, action, and specific object on every request, regardless of identifier opacity or transport. If the binding is missing anywhere, the system is vulnerable.</remember>
</idor_vulnerability_guide>

View File

@@ -0,0 +1,213 @@
---
name: idor
description: IDOR/BOLA testing for object-level authorization failures and cross-account data access
---
# IDOR
Object-level authorization failures (BOLA/IDOR) lead to cross-account data exposure and unauthorized state changes across APIs, web, mobile, and microservices. Treat every object reference as untrusted until proven bound to the caller.
## Attack Surface
**Scope**
- Horizontal access: access another subject's objects of the same type
- Vertical access: access privileged objects/actions (admin-only, staff-only)
- Cross-tenant access: break isolation boundaries in multi-tenant systems
- Cross-service access: token or context accepted by the wrong service
**Reference Locations**
- Paths, query params, JSON bodies, form-data, headers, cookies
- JWT claims, GraphQL arguments, WebSocket messages, gRPC messages
**Identifier Forms**
- Integers, UUID/ULID/CUID, Snowflake, slugs
- Composite keys (e.g., `{orgId}:{userId}`)
- Opaque tokens, base64/hex-encoded blobs
**Relationship References**
- parentId, ownerId, accountId, tenantId, organization, teamId, projectId, subscriptionId
**Expansion/Projection Knobs**
- `fields`, `include`, `expand`, `projection`, `with`, `select`, `populate`
- Often bypass authorization in resolvers or serializers
## High-Value Targets
- Exports/backups/reporting endpoints (CSV/PDF/ZIP)
- Messaging/mailbox/notifications, audit logs, activity feeds
- Billing: invoices, payment methods, transactions, credits
- Healthcare/education records, HR documents, PII/PHI/PCI
- Admin/staff tools, impersonation/session management
- File/object storage keys (S3/GCS signed URLs, share links)
- Background jobs: import/export job IDs, task results
- Multi-tenant resources: organizations, workspaces, projects
## Reconnaissance
**Parameter Analysis**
- Pagination/cursors: `page[offset]`, `page[limit]`, `cursor`, `nextPageToken` (often reveal or accept cross-tenant/state)
- Directory/list endpoints as seeders: search/list/suggest/export often leak object IDs for secondary exploitation
**Enumeration Techniques**
- Alternate types: `{"id":123}` vs `{"id":"123"}`, arrays vs scalars, objects vs scalars
- Edge values: null/empty/0/-1/MAX_INT, scientific notation, overflows
- Duplicate keys/parameter pollution: `id=1&id=2`, JSON duplicate keys `{"id":1,"id":2}` (parser precedence)
- Case/aliasing: userId vs userid vs USER_ID; alt names like resourceId, targetId, account
- Path traversal-like in virtual file systems: `/files/user_123/../../user_456/report.csv`
**UUID/Opaque ID Sources**
- Logs, exports, JS bundles, analytics endpoints, emails, public activity
- Time-based IDs (UUIDv1, ULID) may be guessable within a window
## Key Vulnerabilities
### Horizontal & Vertical Access
- Swap object IDs between principals using the same token to probe horizontal access
- Repeat with lower-privilege tokens to probe vertical access
- Target partial updates (PATCH, JSON Patch/JSON Merge Patch) for silent unauthorized modifications
### Bulk & Batch Operations
- Batch endpoints (bulk update/delete) often validate only the first element; include cross-tenant IDs mid-array
- CSV/JSON imports referencing foreign object IDs (ownerId, orgId) may bypass create-time checks
### Secondary IDOR
- Use list/search endpoints, notifications, emails, webhooks, and client logs to collect valid IDs
- Fetch or mutate those objects directly
- Pagination/cursor manipulation to skip filters and pull other users' pages
### Job/Task Objects
- Access job/task IDs from one user to retrieve results for another (`export/{jobId}/download`, `reports/{taskId}`)
- Cancel/approve someone else's jobs by referencing their task IDs
### File/Object Storage
- Direct object paths or weakly scoped signed URLs
- Attempt key prefix changes, content-disposition tricks, or stale signatures reused across tenants
- Replace share tokens with tokens from other tenants; try case/URL-encoding variations
### GraphQL
- Enforce resolver-level checks: do not rely on a top-level gate
- Verify field and edge resolvers bind the resource to the caller on every hop
- Abuse batching/aliases to retrieve multiple users' nodes in one request
- Global node patterns (Relay): decode base64 IDs and swap raw IDs
- Overfetching via fragments on privileged types
```graphql
query IDOR {
me { id }
u1: user(id: "VXNlcjo0NTY=") { email billing { last4 } }
u2: node(id: "VXNlcjo0NTc=") { ... on User { email } }
}
```
### Microservices & Gateways
- Token confusion: token scoped for Service A accepted by Service B due to shared JWT verification but missing audience/claims checks
- Trust on headers: reverse proxies or API gateways injecting/trusting headers like `X-User-Id`, `X-Organization-Id`; try overriding or removing them
- Context loss: async consumers (queues, workers) re-process requests without re-checking authorization
### Multi-Tenant
- Probe tenant scoping through headers, subdomains, and path params (`X-Tenant-ID`, org slug)
- Try mixing org of token with resource from another org
- Test cross-tenant reports/analytics rollups and admin views which aggregate multiple tenants
### WebSocket
- Authorization per-subscription: ensure channel/topic names cannot be guessed (`user_{id}`, `org_{id}`)
- Subscribe/publish checks must run server-side, not only at handshake
- Try sending messages with target user IDs after subscribing to own channels
### gRPC
- Direct protobuf fields (`owner_id`, `tenant_id`) often bypass HTTP-layer middleware
- Validate references via grpcurl with tokens from different principals
### Integrations
- Webhooks/callbacks referencing foreign objects (e.g., `invoice_id`) processed without verifying ownership
- Third-party importers syncing data into wrong tenant due to missing tenant binding
## Bypass Techniques
**Parser & Transport**
- Content-type switching: `application/json``application/x-www-form-urlencoded``multipart/form-data`
- Method tunneling: `X-HTTP-Method-Override`, `_method=PATCH`; or using GET on endpoints incorrectly accepting state changes
- JSON duplicate keys/array injection to bypass naive validators
**Parameter Pollution**
- Duplicate parameters in query/body to influence server-side precedence (`id=123&id=456`); try both orderings
- Mix case/alias param names so gateway and backend disagree (userId vs userid)
**Cache & Gateway**
- CDN/proxy key confusion: responses keyed without Authorization or tenant headers expose cached objects to other users
- Manipulate Vary and Accept headers
- Redirect chains and 304/206 behaviors can leak content across tenants
**Race Windows**
- Time-of-check vs time-of-use: change the referenced ID between validation and execution using parallel requests
**Blind Channels**
- Use differential responses (status, size, ETag, timing) to detect existence
- Error shape often differs for owned vs foreign objects
- HEAD/OPTIONS, conditional requests (`If-None-Match`/`If-Modified-Since`) can confirm existence without full content
## Chaining Attacks
- IDOR + CSRF: force victims to trigger unauthorized changes on objects you discovered
- IDOR + Stored XSS: pivot into other users' sessions through data you gained access to
- IDOR + SSRF: exfiltrate internal IDs, then access their corresponding resources
- IDOR + Race: bypass spot checks with simultaneous requests
## Testing Methodology
1. **Build matrix** - Subject × Object × Action matrix (who can do what to which resource)
2. **Obtain principals** - At least two: owner and non-owner (plus admin/staff if applicable)
3. **Collect IDs** - Capture at least one valid object ID per principal from list/search/export endpoints
4. **Cross-channel testing** - Exercise every action (R/W/D/Export) while swapping IDs, tokens, tenants
5. **Transport variation** - Test across web, mobile, API, GraphQL, WebSocket, gRPC
6. **Consistency check** - Same rule must hold regardless of transport, content-type, serialization, or gateway
## Validation
1. Demonstrate access to an object not owned by the caller (content or metadata)
2. Show the same request fails with appropriately enforced authorization when corrected
3. Prove cross-channel consistency: same unauthorized access via at least two transports (e.g., REST and GraphQL)
4. Document tenant boundary violations (if applicable)
5. Provide reproducible steps and evidence (requests/responses for owner vs non-owner)
## False Positives
- Public/anonymous resources by design
- Soft-privatized data where content is already public
- Idempotent metadata lookups that do not reveal sensitive content
- Correct row-level checks enforced across all channels
## Impact
- Cross-account data exposure (PII/PHI/PCI)
- Unauthorized state changes (transfers, role changes, cancellations)
- Cross-tenant data leaks violating contractual and regulatory boundaries
- Regulatory risk (GDPR/HIPAA/PCI), fraud, reputational damage
## Pro Tips
1. Always test list/search/export endpoints first; they are rich ID seeders
2. Build a reusable ID corpus from logs, notifications, emails, and client bundles
3. Toggle content-types and transports; authorization middleware often differs per stack
4. In GraphQL, validate at resolver boundaries; never trust parent auth to cover children
5. In multi-tenant apps, vary org headers, subdomains, and path params independently
6. Check batch/bulk operations and background job endpoints; they frequently skip per-item checks
7. Inspect gateways for header trust and cache key configuration
8. Treat UUIDs as untrusted; obtain them via OSINT/leaks and test binding
9. Use timing/size/ETag differentials for blind confirmation when content is masked
10. Prove impact with precise before/after diffs and role-separated evidence
## Summary
Authorization must bind subject, action, and specific object on every request, regardless of identifier opacity or transport. If the binding is missing anywhere, the system is vulnerable.

View File

@@ -1,222 +0,0 @@
<information_disclosure_vulnerability_guide>
<title>INFORMATION DISCLOSURE</title>
<critical>Information leaks accelerate exploitation by revealing code, configuration, identifiers, and trust boundaries. Treat every response byte, artifact, and header as potential intelligence. Minimize, normalize, and scope disclosure across all channels.</critical>
<scope>
- Errors and exception pages: stack traces, file paths, SQL, framework versions
- Debug/dev tooling reachable in prod: debuggers, profilers, feature flags
- DVCS/build artifacts and temp/backup files: .git, .svn, .hg, .bak, .swp, archives
- Configuration and secrets: .env, phpinfo, appsettings.json, Docker/K8s manifests
- API schemas and introspection: OpenAPI/Swagger, GraphQL introspection, gRPC reflection
- Client bundles and source maps: webpack/Vite maps, embedded env, __NEXT_DATA__, static JSON
- Headers and response metadata: Server/X-Powered-By, tracing, ETag, Accept-Ranges, Server-Timing
- Storage/export surfaces: public buckets, signed URLs, export/download endpoints
- Observability/admin: /metrics, /actuator, /health, tracing UIs (Jaeger, Zipkin), Kibana, Admin UIs
- Directory listings and indexing: autoindex, sitemap/robots revealing hidden routes
- Cross-origin signals: CORS misconfig, Referrer-Policy leakage, Expose-Headers
- File/document metadata: EXIF, PDF/Office properties
</scope>
<methodology>
1. Build a channel map: Web, API, GraphQL, WebSocket, gRPC, mobile, background jobs, exports, CDN.
2. Establish a diff harness: compare owner vs non-owner vs anonymous across transports; normalize on status/body length/ETag/headers.
3. Trigger controlled failures: send malformed types, boundary values, missing params, and alternate content-types to elicit error detail and stack traces.
4. Enumerate artifacts: DVCS folders, backups, config endpoints, source maps, client bundles, API docs, observability routes.
5. Correlate disclosures to impact: versions→CVE, paths→LFI/RCE, keys→cloud access, schemas→auth bypass, IDs→IDOR.
</methodology>
<surfaces>
<errors_and_exceptions>
- SQL/ORM errors: reveal table/column names, DBMS, query fragments
- Stack traces: absolute paths, class/method names, framework versions, developer emails
- Template engine probes: {% raw %}{{7*7}}, ${7*7}{% endraw %} identify templating stack and code paths
- JSON/XML parsers: type mismatches and coercion logs leak internal model names
</errors_and_exceptions>
<debug_and_env_modes>
- Debug pages and flags: Django DEBUG, Laravel Telescope, Rails error pages, Flask/Werkzeug debugger, ASP.NET customErrors Off
- Profiler endpoints: /debug/pprof, /actuator, /_profiler, custom /debug APIs
- Feature/config toggles exposed in JS or headers; admin/staff banners in HTML
</debug_and_env_modes>
<dvcs_and_backups>
- DVCS: /.git/ (HEAD, config, index, objects), .svn/entries, .hg/store → reconstruct source and secrets
- Backups/temp: .bak/.old/~/.swp/.swo/.tmp/.orig, db dumps, zipped deployments under /backup/, /old/, /archive/
- Build artifacts: dist artifacts containing .map, env prints, internal URLs
</dvcs_and_backups>
<configs_and_secrets>
- Classic: web.config, appsettings.json, settings.py, config.php, phpinfo.php
- Containers/cloud: Dockerfile, docker-compose.yml, Kubernetes manifests, service account tokens, cloud credentials files
- Credentials and connection strings; internal hosts and ports; JWT secrets
</configs_and_secrets>
<api_schemas_and_introspection>
- OpenAPI/Swagger: /swagger, /api-docs, /openapi.json — enumerate hidden/privileged operations
- GraphQL: introspection enabled; field suggestions; error disclosure via invalid fields; persisted queries catalogs
- gRPC: server reflection exposing services/messages; proto download via reflection
</api_schemas_and_introspection>
<client_bundles_and_maps>
- Source maps (.map) reveal original sources, comments, and internal logic
- Client env leakage: NEXT_PUBLIC_/VITE_/REACT_APP_ variables; runtime config; embedded secrets accidentally shipped
- Next.js data: __NEXT_DATA__ and pre-fetched JSON under /_next/data can include internal IDs, flags, or PII
- Static JSON/CSV feeds used by the UI that bypass server-side auth filtering
</client_bundles_and_maps>
<headers_and_response_metadata>
- Fingerprinting: Server, X-Powered-By, X-AspNet-Version
- Tracing: X-Request-Id, traceparent, Server-Timing, debug headers
- Caching oracles: ETag/If-None-Match, Last-Modified/If-Modified-Since, Accept-Ranges/Range (partial content reveals)
- Content sniffing and MIME metadata that implies backend components
</headers_and_response_metadata>
<storage_and_exports>
- Public object storage: S3/GCS/Azure blobs with world-readable ACLs or guessable keys
- Signed URLs: long-lived, weakly scoped, re-usable across tenants; metadata leaks in headers
- Export/report endpoints returning foreign data sets or unfiltered fields
</storage_and_exports>
<observability_and_admin>
- Metrics: Prometheus /metrics exposing internal hostnames, process args, SQL, credentials by mistake
- Health/config: /actuator/health, /actuator/env, Spring Boot info endpoints
- Tracing UIs and dashboards: Jaeger/Zipkin/Kibana/Grafana exposed without auth
</observability_and_admin>
<directory_and_indexing>
- Autoindex on /uploads/, /files/, /logs/, /tmp/, /assets/
- Robots/sitemap reveal hidden paths, admin panels, export feeds
</directory_and_indexing>
<cross_origin_signals>
- Referrer leakage: missing/referrer policy leading to path/query/token leaks to third parties
- CORS: overly permissive Access-Control-Allow-Origin/Expose-Headers revealing data cross-origin; preflight error shapes
</cross_origin_signals>
<file_metadata>
- EXIF, PDF/Office properties: authors, paths, software versions, timestamps, embedded objects
</file_metadata>
</surfaces>
<advanced_techniques>
<differential_oracles>
- Compare owner vs non-owner vs anonymous for the same resource and track: status, length, ETag, Last-Modified, Cache-Control
- HEAD vs GET: header-only differences can confirm existence or type without content
- Conditional requests: 304 vs 200 behaviors leak existence/state; binary search content size via Range requests
</differential_oracles>
<cdn_and_cache_keys>
- Identity-agnostic caches: CDN/proxy keys missing Authorization/tenant headers → cross-user cached responses
- Vary misconfiguration: user-agent/language vary without auth vary leaks alternate content
- 206 partial content + stale caches leak object fragments
</cdn_and_cache_keys>
<cross_channel_mirroring>
- Inconsistent hardening between REST, GraphQL, WebSocket, and gRPC; one channel leaks schema or fields hidden in others
- SSR vs CSR: server-rendered pages omit fields while JSON API includes them; compare responses
</cross_channel_mirroring>
<introspection_and_reflection>
- GraphQL: disabled introspection still leaks via errors, fragment suggestions, and client bundles containing schema
- gRPC reflection: list services/messages and infer internal resource names and flows
</introspection_and_reflection>
<cloud_specific>
- S3/GCS/Azure: anonymous listing disabled but object reads allowed; metadata headers leak owner/project identifiers
- Pre-signed URLs: audience not bound; observe key scope and lifetime in URL params
</cloud_specific>
</advanced_techniques>
<usefulness_assessment>
- Actionable signals:
- Secrets/keys/tokens that grant new access (DB creds, cloud keys, JWT signing/refresh, signed URL secrets)
- Versions with a reachable, unpatched CVE on an exposed path
- Cross-tenant identifiers/data or per-user fields that differ by principal
- File paths, service hosts, or internal URLs that enable LFI/SSRF/RCE pivots
- Cache/CDN differentials (Vary/ETag/Range) that expose other users' content
- Schema/introspection revealing hidden operations or fields that return sensitive data
- Likely benign or intended:
- Public docs or non-sensitive metadata explicitly documented as public
- Generic server names without precise versions or exploit path
- Redacted/sanitized fields with stable length/ETag across principals
- Per-user data visible only to the owner and consistent with privacy policy
</usefulness_assessment>
<triage_rubric>
- Critical: Credentials/keys; signed URL secrets; config dumps; unrestricted admin/observability panels
- High: Versions with reachable CVEs; cross-tenant data; caches serving cross-user content; schema enabling auth bypass
- Medium: Internal paths/hosts enabling LFI/SSRF pivots; source maps revealing hidden endpoints/IDs
- Low: Generic headers, marketing versions, intended documentation without exploit path
- Guidance: Always attempt a minimal, reversible proof for Critical/High; if no safe chain exists, document precise blocker and downgrade
</triage_rubric>
<escalation_playbook>
- If DVCS/backups/configs → extract secrets; test least-privileged read; rotate after coordinated disclosure
- If versions → map to CVE; verify exposure; execute minimal PoC under strict scope
- If schema/introspection → call hidden/privileged fields with non-owner tokens; confirm auth gaps
- If source maps/client JSON → mine endpoints/IDs/flags; pivot to IDOR/listing; validate filtering
- If cache/CDN keys → demonstrate cross-user cache leak via Vary/ETag/Range; escalate to broken access control
- If paths/hosts → target LFI/SSRF with harmless reads (e.g., /etc/hostname, metadata headers); avoid destructive actions
- If observability/admin → enumerate read-only info first; prove data scope breach; avoid write/exec operations
</escalation_playbook>
<exploitation_chains>
<credential_extraction>
- DVCS/config dumps exposing secrets (DB, SMTP, JWT, cloud)
- Keys → cloud control plane access; rotate and verify scope
</credential_extraction>
<version_to_cve>
1. Derive precise component versions from headers/errors/bundles.
2. Map to known CVEs and confirm reachability.
3. Execute minimal proof targeting disclosed component.
</version_to_cve>
<path_disclosure_to_lfi>
1. Paths from stack traces/templates reveal filesystem layout.
2. Use LFI/traversal to fetch config/keys.
3. Prove controlled access without altering state.
</path_disclosure_to_lfi>
<schema_to_auth_bypass>
1. Schema reveals hidden fields/endpoints.
2. Attempt requests with those fields; confirm missing authorization or field filtering.
</schema_to_auth_bypass>
</exploitation_chains>
<validation>
1. Provide raw evidence (headers/body/artifact) and explain exact data revealed.
2. Determine intent: cross-check docs/UX; classify per triage rubric (Critical/High/Medium/Low).
3. Attempt minimal, reversible exploitation or present a concrete step-by-step chain (what to try next and why).
4. Show reproducibility and minimal request set; include cross-channel confirmation where applicable.
5. Bound scope (user, tenant, environment) and data sensitivity classification.
</validation>
<false_positives>
- Intentional public docs or non-sensitive metadata with no exploit path
- Generic errors with no actionable details
- Redacted fields that do not change differential oracles (length/ETag stable)
- Version banners with no exposed vulnerable surface and no chain
- Owner-visible-only details that do not cross identity/tenant boundaries
</false_positives>
<impact>
- Accelerated exploitation of RCE/LFI/SSRF via precise versions and paths
- Credential/secret exposure leading to persistent external compromise
- Cross-tenant data disclosure through exports, caches, or mis-scoped signed URLs
- Privacy/regulatory violations and business intelligence leakage
</impact>
<pro_tips>
1. Start with artifacts (DVCS, backups, maps) before payloads; artifacts yield the fastest wins.
2. Normalize responses and diff by digest to reduce noise when comparing roles.
3. Hunt source maps and client data JSON; they often carry internal IDs and flags.
4. Probe caches/CDNs for identity-unaware keys; verify Vary includes Authorization/tenant.
5. Treat introspection and reflection as configuration findings across GraphQL/gRPC; validate per environment.
6. Mine observability endpoints last; they are noisy but high-yield in misconfigured setups.
7. Chain quickly to a concrete risk and stop—proof should be minimal and reversible.
</pro_tips>
<remember>Information disclosure is an amplifier. Convert leaks into precise, minimal exploits or clear architectural risks.</remember>
</information_disclosure_vulnerability_guide>

View File

@@ -0,0 +1,183 @@
---
name: information-disclosure
description: Information disclosure testing covering error messages, debug endpoints, metadata leakage, and source exposure
---
# Information Disclosure
Information leaks accelerate exploitation by revealing code, configuration, identifiers, and trust boundaries. Treat every response byte, artifact, and header as potential intelligence. Minimize, normalize, and scope disclosure across all channels.
## Attack Surface
- Errors and exception pages: stack traces, file paths, SQL, framework versions
- Debug/dev tooling reachable in prod: debuggers, profilers, feature flags
- DVCS/build artifacts and temp/backup files: .git, .svn, .hg, .bak, .swp, archives
- Configuration and secrets: .env, phpinfo, appsettings.json, Docker/K8s manifests
- API schemas and introspection: OpenAPI/Swagger, GraphQL introspection, gRPC reflection
- Client bundles and source maps: webpack/Vite maps, embedded env, `__NEXT_DATA__`, static JSON
- Headers and response metadata: Server/X-Powered-By, tracing, ETag, Accept-Ranges, Server-Timing
- Storage/export surfaces: public buckets, signed URLs, export/download endpoints
- Observability/admin: /metrics, /actuator, /health, tracing UIs (Jaeger, Zipkin), Kibana, Admin UIs
- Directory listings and indexing: autoindex, sitemap/robots revealing hidden routes
## High-Value Surfaces
### Errors and Exceptions
- SQL/ORM errors: reveal table/column names, DBMS, query fragments
- Stack traces: absolute paths, class/method names, framework versions, developer emails
- Template engine probes: `{{7*7}}`, `${7*7}` identify templating stack
- JSON/XML parsers: type mismatches leak internal model names
### Debug and Env Modes
- Debug pages: Django DEBUG, Laravel Telescope, Rails error pages, Flask/Werkzeug debugger, ASP.NET customErrors Off
- Profiler endpoints: `/debug/pprof`, `/actuator`, `/_profiler`, custom `/debug` APIs
- Feature/config toggles exposed in JS or headers
### DVCS and Backups
- DVCS: `/.git/` (HEAD, config, index, objects), `.svn/entries`, `.hg/store` → reconstruct source and secrets
- Backups/temp: `.bak`/`.old`/`~`/`.swp`/`.swo`/`.tmp`/`.orig`, db dumps, zipped deployments
- Build artifacts: dist artifacts containing `.map`, env prints, internal URLs
### Configs and Secrets
- Classic: web.config, appsettings.json, settings.py, config.php, phpinfo.php
- Containers/cloud: Dockerfile, docker-compose.yml, Kubernetes manifests, service account tokens
- Credentials and connection strings; internal hosts and ports; JWT secrets
### API Schemas and Introspection
- OpenAPI/Swagger: `/swagger`, `/api-docs`, `/openapi.json` — enumerate hidden/privileged operations
- GraphQL: introspection enabled; field suggestions; error disclosure via invalid fields
- gRPC: server reflection exposing services/messages
### Client Bundles and Maps
- Source maps (`.map`) reveal original sources, comments, and internal logic
- Client env leakage: `NEXT_PUBLIC_`/`VITE_`/`REACT_APP_` variables; embedded secrets
- `__NEXT_DATA__` and pre-fetched JSON can include internal IDs, flags, or PII
### Headers and Response Metadata
- Fingerprinting: Server, X-Powered-By, X-AspNet-Version
- Tracing: X-Request-Id, traceparent, Server-Timing, debug headers
- Caching oracles: ETag/If-None-Match, Last-Modified/If-Modified-Since, Accept-Ranges/Range
### Storage and Exports
- Public object storage: S3/GCS/Azure blobs with world-readable ACLs or guessable keys
- Signed URLs: long-lived, weakly scoped, re-usable across tenants
- Export/report endpoints returning foreign data sets or unfiltered fields
### Observability and Admin
- Metrics: Prometheus `/metrics` exposing internal hostnames, process args
- Health/config: `/actuator/health`, `/actuator/env`, Spring Boot info endpoints
- Tracing UIs: Jaeger/Zipkin/Kibana/Grafana exposed without auth
### Cross-Origin Signals
- Referrer leakage: missing/weak referrer policy leading to path/query/token leaks to third parties
- CORS: overly permissive Access-Control-Allow-Origin/Expose-Headers revealing data cross-origin; preflight error shapes
### File Metadata
- EXIF, PDF/Office properties: authors, paths, software versions, timestamps, embedded objects
### Cloud Storage
- S3/GCS/Azure: anonymous listing disabled but object reads allowed; metadata headers leak owner/project identifiers
- Pre-signed URLs: audience not bound; observe key scope and lifetime in URL params
## Key Vulnerabilities
### Differential Oracles
- Compare owner vs non-owner vs anonymous for the same resource
- Track: status, length, ETag, Last-Modified, Cache-Control
- HEAD vs GET: header-only differences can confirm existence
- Conditional requests: 304 vs 200 behaviors leak existence/state
### CDN and Cache Keys
- Identity-agnostic caches: CDN/proxy keys missing Authorization/tenant headers
- Vary misconfiguration: user-agent/language vary without auth vary leaks content
- 206 partial content + stale caches leak object fragments
### Cross-Channel Mirroring
- Inconsistent hardening between REST, GraphQL, WebSocket, and gRPC
- SSR vs CSR: server-rendered pages omit fields while JSON API includes them
## Triage Rubric
- **Critical**: Credentials/keys; signed URL secrets; config dumps; unrestricted admin/observability panels
- **High**: Versions with reachable CVEs; cross-tenant data; caches serving cross-user content
- **Medium**: Internal paths/hosts enabling LFI/SSRF pivots; source maps revealing hidden endpoints
- **Low**: Generic headers, marketing versions, intended documentation without exploit path
## Exploitation Chains
### Credential Extraction
- DVCS/config dumps exposing secrets (DB, SMTP, JWT, cloud)
- Keys → cloud control plane access
### Version to CVE
1. Derive precise component versions from headers/errors/bundles
2. Map to known CVEs and confirm reachability
3. Execute minimal proof targeting disclosed component
### Path Disclosure to LFI
1. Paths from stack traces/templates reveal filesystem layout
2. Use LFI/traversal to fetch config/keys
### Schema to Auth Bypass
1. Schema reveals hidden fields/endpoints
2. Attempt requests with those fields; confirm missing authorization
## Testing Methodology
1. **Build channel map** - Web, API, GraphQL, WebSocket, gRPC, mobile, background jobs, exports, CDN
2. **Establish diff harness** - Compare owner vs non-owner vs anonymous; normalize on status/body length/ETag/headers
3. **Trigger controlled failures** - Malformed types, boundary values, missing params, alternate content-types
4. **Enumerate artifacts** - DVCS folders, backups, config endpoints, source maps, client bundles, API docs
5. **Correlate to impact** - Versions→CVE, paths→LFI/RCE, keys→cloud access, schemas→auth bypass
## Validation
1. Provide raw evidence (headers/body/artifact) and explain exact data revealed
2. Determine intent: cross-check docs/UX; classify per triage rubric
3. Attempt minimal, reversible exploitation or present a concrete step-by-step chain
4. Show reproducibility and minimal request set
5. Bound scope (user, tenant, environment) and data sensitivity classification
## False Positives
- Intentional public docs or non-sensitive metadata with no exploit path
- Generic errors with no actionable details
- Redacted fields that do not change differential oracles
- Version banners with no exposed vulnerable surface and no chain
- Owner-visible-only details that do not cross identity/tenant boundaries
## Impact
- Accelerated exploitation of RCE/LFI/SSRF via precise versions and paths
- Credential/secret exposure leading to persistent external compromise
- Cross-tenant data disclosure through exports, caches, or mis-scoped signed URLs
- Privacy/regulatory violations and business intelligence leakage
## Pro Tips
1. Start with artifacts (DVCS, backups, maps) before payloads; artifacts yield the fastest wins
2. Normalize responses and diff by digest to reduce noise when comparing roles
3. Hunt source maps and client data JSON; they often carry internal IDs and flags
4. Probe caches/CDNs for identity-unaware keys; verify Vary includes Authorization/tenant
5. Treat introspection and reflection as configuration findings across GraphQL/gRPC
6. Mine observability endpoints last; they are noisy but high-yield in misconfigured setups
7. Chain quickly to a concrete risk and stop—proof should be minimal and reversible
## Summary
Information disclosure is an amplifier. Convert leaks into precise, minimal exploits or clear architectural risks.

View File

@@ -1,188 +0,0 @@
<insecure_file_uploads_guide>
<title>INSECURE FILE UPLOADS</title>
<critical>Upload surfaces are high risk: server-side execution (RCE), stored XSS, malware distribution, storage takeover, and DoS. Modern stacks mix direct-to-cloud uploads, background processors, and CDNs—authorization and validation must hold across every step.</critical>
<scope>
- Web/mobile/API uploads, direct-to-cloud (S3/GCS/Azure) presigned flows, resumable/multipart protocols (tus, S3 MPU)
- Image/document/media pipelines (ImageMagick/GraphicsMagick, Ghostscript, ExifTool, PDF engines, office converters)
- Admin/bulk importers, archive uploads (zip/tar), report/template uploads, rich text with attachments
- Serving paths: app directly, object storage, CDN, email attachments, previews/thumbnails
</scope>
<methodology>
1. Map the pipeline: client → ingress (edge/app/gateway) → storage → processors (thumb, OCR, AV, CDR) → serving (app/storage/CDN). Note where validation and auth occur.
2. Identify allowed types, size limits, filename rules, storage keys, and who serves the content. Collect baseline uploads per type and capture resulting URLs and headers.
3. Exercise bypass families systematically: extension games, MIME/content-type, magic bytes, polyglots, metadata payloads, archive structure, chunk/finalize differentials.
4. Validate execution and rendering: can uploaded content execute on server or client? Confirm with minimal PoCs and headers analysis.
</methodology>
<discovery_techniques>
<surface_map>
- Endpoints/fields: upload, file, avatar, image, attachment, import, media, document, template
- Direct-to-cloud params: key, bucket, acl, Content-Type, Content-Disposition, x-amz-meta-*, cache-control
- Resumable APIs: create/init → upload/chunk → complete/finalize; check if metadata/headers can be altered late
- Background processors: thumbnails, PDF→image, virus scan queues; identify timing and status transitions
</surface_map>
<capability_probes>
- Small probe files of each claimed type; diff resulting Content-Type, Content-Disposition, and X-Content-Type-Options on download
- Magic bytes vs extension: JPEG/GIF/PNG headers; mismatches reveal reliance on extension or MIME sniffing
- SVG/HTML probe: do they render inline (text/html or image/svg+xml) or download (attachment)?
- Archive probe: simple zip with nested path traversal entries and symlinks to detect extraction rules
</capability_probes>
</discovery_techniques>
<detection_channels>
<server_execution>
- Web shell execution (language dependent), config/handler uploads (.htaccess, .user.ini, web.config) enabling execution
- Interpreter-side template/script evaluation during conversion (ImageMagick/Ghostscript/ExifTool)
</server_execution>
<client_execution>
- Stored XSS via SVG/HTML/JS if served inline without correct headers; PDF JavaScript; office macros in previewers
</client_execution>
<header_and_render>
- Missing X-Content-Type-Options: nosniff enabling browser sniff to script
- Content-Type reflection from upload vs server-set; Content-Disposition: inline vs attachment
</header_and_render>
<process_side_effects>
- AV/CDR race or absence; background job status allows access before scan completes; password-protected archives bypass scanning
</process_side_effects>
</detection_channels>
<core_payloads>
<web_shells_and_configs>
- PHP: GIF polyglot (starts with GIF89a) followed by <?php echo 1; ?>; place where PHP is executed
- .htaccess to map extensions to code (AddType/AddHandler); .user.ini (auto_prepend/append_file) for PHP-FPM
- ASP/JSP equivalents where supported; IIS web.config to enable script execution
</web_shells_and_configs>
<stored_xss>
- SVG with onload/onerror handlers served as image/svg+xml or text/html
- HTML file with script when served as text/html or sniffed due to missing nosniff
</stored_xss>
<mime_magic_polyglots>
- Double extensions: avatar.jpg.php, report.pdf.html; mixed casing: .pHp, .PhAr
- Magic-byte spoofing: valid JPEG header then embedded script; verify server uses content inspection, not extensions alone
</mime_magic_polyglots>
<archive_attacks>
- Zip Slip: entries with ../../ to escape extraction dir; symlink-in-zip pointing outside target; nested zips
- Zip bomb: extreme compression ratios (e.g., 42.zip) to exhaust resources in processors
</archive_attacks>
<toolchain_exploits>
- ImageMagick/GraphicsMagick legacy vectors (policy.xml may mitigate): crafted SVG/PS/EPS invoking external commands or reading files
- Ghostscript in PDF/PS with file operators (%pipe%)
- ExifTool metadata parsing bugs; overly large or crafted EXIF/IPTC/XMP fields
</toolchain_exploits>
<cloud_storage_vectors>
- S3/GCS presigned uploads: attacker controls Content-Type/Disposition; set text/html or image/svg+xml and inline rendering
- Public-read ACL or permissive bucket policies expose uploads broadly; object key injection via user-controlled path prefixes
- Signed URL reuse and stale URLs; serving directly from bucket without attachment + nosniff headers
</cloud_storage_vectors>
</core_payloads>
<advanced_techniques>
<resumable_multipart>
- Change metadata between init and complete (e.g., swap Content-Type/Disposition at finalize)
- Upload benign chunks, then swap last chunk or complete with different source if server trusts client-side digests only
</resumable_multipart>
<filename_and_path>
- Unicode homoglyphs, trailing dots/spaces, device names, reserved characters to bypass validators and filesystem rules
- Null-byte truncation on legacy stacks; overlong paths; case-insensitive collisions overwriting existing files
</filename_and_path>
<processing_races>
- Request file immediately after upload but before AV/CDR completes; or during derivative creation to get unprocessed content
- Trigger heavy conversions (large images, deep PDFs) to widen race windows
</processing_races>
<metadata_abuse>
- Oversized EXIF/XMP/IPTC blocks to trigger parser flaws; payloads in document properties of Office/PDF rendered by previewers
</metadata_abuse>
<header_manipulation>
- Force inline rendering with Content-Type + inline Content-Disposition; test browsers with and without nosniff
- Cache poisoning via CDN with keys missing Vary on Content-Type/Disposition
</header_manipulation>
</advanced_techniques>
<filter_bypasses>
<validation_gaps>
- Client-side only checks; relying on JS/MIME provided by browser; trusting multipart boundary part headers blindly
- Extension allowlists without server-side content inspection; magic-bytes only without full parsing
</validation_gaps>
<evasion_tricks>
- Double extensions, mixed case, hidden dotfiles, extra dots (file..png), long paths with allowed suffix
- Multipart name vs filename vs path discrepancies; duplicate parameters and late parameter precedence
</evasion_tricks>
</filter_bypasses>
<special_contexts>
<rich_text_editors>
- RTEs allow image/attachment uploads and embed links; verify sanitization and serving headers for embedded content
</rich_text_editors>
<mobile_clients>
- Mobile SDKs may send nonstandard MIME or metadata; servers sometimes trust client-side transformations or EXIF orientation
</mobile_clients>
<serverless_and_cdn>
- Direct-to-bucket uploads with Lambda/Workers post-processing; verify that security decisions are not delegated to frontends
- CDN caching of uploaded content; ensure correct cache keys and headers (attachment, nosniff)
</serverless_and_cdn>
</special_contexts>
<parser_hardening>
- Validate on server: strict allowlist by true type (parse enough to confirm), size caps, and structural checks (dimensions, page count)
- Strip active content: convert SVG→PNG; remove scripts/JS from PDF; disable macros; normalize EXIF; consider CDR for risky types
- Store outside web root; serve via application or signed, time-limited URLs with Content-Disposition: attachment and X-Content-Type-Options: nosniff
- For cloud: private buckets, per-request signed GET, enforce Content-Type/Disposition on GET responses from your app/gateway
- Disable execution in upload paths; ignore .htaccess/.user.ini; sanitize keys to prevent path injections; randomize filenames
- AV + CDR: scan synchronously when possible; quarantine until verdict; block password-protected archives or process in sandbox
</parser_hardening>
<validation>
1. Demonstrate execution or rendering of active content: web shell reachable, or SVG/HTML executing JS when viewed.
2. Show filter bypass: upload accepted despite restrictions (extension/MIME/magic mismatch) with evidence on retrieval.
3. Prove header weaknesses: inline rendering without nosniff or missing attachment; present exact response headers.
4. Show race or pipeline gap: access before AV/CDR; extraction outside intended directory; derivative creation from malicious input.
5. Provide reproducible steps: request/response for upload and subsequent access, with minimal PoCs.
</validation>
<false_positives>
- Upload stored but never served back; or always served as attachment with strict nosniff
- Converters run in locked-down sandboxes with no external IO and no script engines; no path traversal on archive extraction
- AV/CDR blocks the payload and quarantines; access before scan is impossible by design
</false_positives>
<impact>
- Remote code execution on application stack or media toolchain host
- Persistent cross-site scripting and session/token exfiltration via served uploads
- Malware distribution via public storage/CDN; brand/reputation damage
- Data loss or corruption via overwrite/zip slip; service degradation via zip bombs or oversized assets
</impact>
<pro_tips>
1. Keep PoCs minimal: tiny SVG/HTML for XSS, a single-line PHP/ASP where relevant, and benign magic-byte polyglots.
2. Always capture download response headers and final MIME from the server/CDN; that decides browser behavior.
3. Prefer transforming risky formats to safe renderings (SVG→PNG) rather than attempting complex sanitization.
4. In presigned flows, constrain all headers and object keys server-side; ignore client-supplied ACL and metadata.
5. For archives, extract in a chroot/jail with explicit allowlist; drop symlinks and reject traversal.
6. Test finalize/complete steps in resumable flows; many validations only run on init, not at completion.
7. Verify background processors with EICAR and tiny polyglots; ensure quarantine gates access until safe.
8. When you cannot get execution, aim for stored XSS or header-driven script execution; both are impactful.
9. Validate that CDNs honor attachment/nosniff and do not override Content-Type/Disposition.
10. Document full pipeline behavior per asset type; defenses must match actual processors and serving paths.
</pro_tips>
<remember>Secure uploads are a pipeline property. Enforce strict type, size, and header controls; transform or strip active content; never execute or inline-render untrusted uploads; and keep storage private with controlled, signed access.</remember>
</insecure_file_uploads_guide>

View File

@@ -0,0 +1,188 @@
---
name: insecure-file-uploads
description: File upload security testing covering extension bypass, content-type manipulation, and path traversal
---
# Insecure File Uploads
Upload surfaces are high risk: server-side execution (RCE), stored XSS, malware distribution, storage takeover, and DoS. Modern stacks mix direct-to-cloud uploads, background processors, and CDNs—authorization and validation must hold across every step.
## Attack Surface
- Web/mobile/API uploads, direct-to-cloud (S3/GCS/Azure) presigned flows, resumable/multipart protocols (tus, S3 MPU)
- Image/document/media pipelines (ImageMagick/GraphicsMagick, Ghostscript, ExifTool, PDF engines, office converters)
- Admin/bulk importers, archive uploads (zip/tar), report/template uploads, rich text with attachments
- Serving paths: app directly, object storage, CDN, email attachments, previews/thumbnails
## Reconnaissance
### Surface Map
- Endpoints/fields: upload, file, avatar, image, attachment, import, media, document, template
- Direct-to-cloud params: key, bucket, acl, Content-Type, Content-Disposition, x-amz-meta-*, cache-control
- Resumable APIs: create/init → upload/chunk → complete/finalize; check if metadata/headers can be altered late
- Background processors: thumbnails, PDF→image, virus scan queues; identify timing and status transitions
### Capability Probes
- Small probe files of each claimed type; diff resulting Content-Type, Content-Disposition, and X-Content-Type-Options on download
- Magic bytes vs extension: JPEG/GIF/PNG headers; mismatches reveal reliance on extension or MIME sniffing
- SVG/HTML probe: do they render inline (text/html or image/svg+xml) or download (attachment)?
- Archive probe: simple zip with nested path traversal entries and symlinks to detect extraction rules
## Detection Channels
### Server Execution
- Web shell execution (language dependent), config/handler uploads (.htaccess, .user.ini, web.config) enabling execution
- Interpreter-side template/script evaluation during conversion (ImageMagick/Ghostscript/ExifTool)
### Client Execution
- Stored XSS via SVG/HTML/JS if served inline without correct headers; PDF JavaScript; office macros in previewers
### Header and Render
- Missing X-Content-Type-Options: nosniff enabling browser sniff to script
- Content-Type reflection from upload vs server-set; Content-Disposition: inline vs attachment
### Process Side Effects
- AV/CDR race or absence; background job status allows access before scan completes; password-protected archives bypass scanning
## Core Payloads
### Web Shells and Configs
- PHP: GIF polyglot (starts with GIF89a) followed by `<?php echo 1; ?>`; place where PHP is executed
- .htaccess to map extensions to code (AddType/AddHandler); .user.ini (auto_prepend/append_file) for PHP-FPM
- ASP/JSP equivalents where supported; IIS web.config to enable script execution
### Stored XSS
- SVG with onload/onerror handlers served as image/svg+xml or text/html
- HTML file with script when served as text/html or sniffed due to missing nosniff
### MIME Magic Polyglots
- Double extensions: avatar.jpg.php, report.pdf.html; mixed casing: .pHp, .PhAr
- Magic-byte spoofing: valid JPEG header then embedded script; verify server uses content inspection, not extensions alone
### Archive Attacks
- Zip Slip: entries with `../../` to escape extraction dir; symlink-in-zip pointing outside target; nested zips
- Zip bomb: extreme compression ratios to exhaust resources in processors
### Toolchain Exploits
- ImageMagick/GraphicsMagick legacy vectors (policy.xml may mitigate): crafted SVG/PS/EPS invoking external commands or reading files
- Ghostscript in PDF/PS with file operators (%pipe%)
- ExifTool metadata parsing bugs; overly large or crafted EXIF/IPTC/XMP fields
### Cloud Storage Vectors
- S3/GCS presigned uploads: attacker controls Content-Type/Disposition; set text/html or image/svg+xml and inline rendering
- Public-read ACL or permissive bucket policies expose uploads broadly
- Object key injection via user-controlled path prefixes
- Signed URL reuse and stale URLs; serving directly from bucket without attachment + nosniff headers
## Advanced Techniques
### Resumable Multipart
- Change metadata between init and complete (e.g., swap Content-Type/Disposition at finalize)
- Upload benign chunks, then swap last chunk or complete with different source
### Filename and Path
- Unicode homoglyphs, trailing dots/spaces, device names, reserved characters to bypass validators
- Null-byte truncation on legacy stacks; overlong paths; case-insensitive collisions overwriting existing files
### Processing Races
- Request file immediately after upload but before AV/CDR completes
- Trigger heavy conversions (large images, deep PDFs) to widen race windows
### Metadata Abuse
- Oversized EXIF/XMP/IPTC blocks to trigger parser flaws
- Payloads in document properties of Office/PDF rendered by previewers
### Header Manipulation
- Force inline rendering with Content-Type + inline Content-Disposition
- Cache poisoning via CDN with keys missing Vary on Content-Type/Disposition
## Bypass Techniques
### Validation Gaps
- Client-side only checks; relying on JS/MIME provided by browser
- Trusting multipart boundary part headers blindly
- Extension allowlists without server-side content inspection
### Evasion Tricks
- Double extensions, mixed case, hidden dotfiles, extra dots (file..png), long paths with allowed suffix
- Multipart name vs filename vs path discrepancies; duplicate parameters and late parameter precedence
## Special Contexts
### Rich Text Editors
- RTEs allow image/attachment uploads and embed links; verify sanitization and serving headers
### Mobile Clients
- Mobile SDKs may send nonstandard MIME or metadata; servers sometimes trust client-side transformations
### Serverless and CDN
- Direct-to-bucket uploads with Lambda/Workers post-processing; verify security decisions are not delegated to frontends
- CDN caching of uploaded content; ensure correct cache keys and headers
## Testing Methodology
1. **Map the pipeline** - Client → ingress → storage → processors → serving. Note where validation and auth occur
2. **Identify allowed types** - Size limits, filename rules, storage keys, and who serves the content
3. **Collect baselines** - Capture resulting URLs and headers for legitimate uploads
4. **Exercise bypass families** - Extension games, MIME/content-type, magic bytes, polyglots, metadata payloads, archive structure
5. **Validate execution** - Can uploaded content execute on server or client?
## Validation
1. Demonstrate execution or rendering of active content: web shell reachable, or SVG/HTML executing JS when viewed
2. Show filter bypass: upload accepted despite restrictions with evidence on retrieval
3. Prove header weaknesses: inline rendering without nosniff or missing attachment
4. Show race or pipeline gap: access before AV/CDR; extraction outside intended directory
5. Provide reproducible steps: request/response for upload and subsequent access
## False Positives
- Upload stored but never served back; or always served as attachment with strict nosniff
- Converters run in locked-down sandboxes with no external IO and no script engines
- AV/CDR blocks the payload and quarantines; access before scan is impossible by design
## Impact
- Remote code execution on application stack or media toolchain host
- Persistent cross-site scripting and session/token exfiltration via served uploads
- Malware distribution via public storage/CDN; brand/reputation damage
- Data loss or corruption via overwrite/zip slip; service degradation via zip bombs
## Pro Tips
1. Keep PoCs minimal: tiny SVG/HTML for XSS, a single-line PHP/ASP where relevant
2. Always capture download response headers and final MIME; that decides browser behavior
3. Prefer transforming risky formats to safe renderings (SVG→PNG) rather than complex sanitization
4. In presigned flows, constrain all headers and object keys server-side
5. For archives, extract in a chroot/jail with explicit allowlist; drop symlinks and reject traversal
6. Test finalize/complete steps in resumable flows; many validations only run on init
7. Verify background processors with EICAR and tiny polyglots
8. When you cannot get execution, aim for stored XSS or header-driven script execution
9. Validate that CDNs honor attachment/nosniff
10. Document full pipeline behavior per asset type
## Summary
Secure uploads are a pipeline property. Enforce strict type, size, and header controls; transform or strip active content; never execute or inline-render untrusted uploads; and keep storage private with controlled, signed access.

Some files were not shown because too many files have changed in this diff Show More