mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/usestrix/strix.git
synced 2026-09-20 16:13:44 +08:00
Compare commits
2 Commits
fix/api-ty
...
feat/error
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f8f3c306 | ||
|
|
7a1de951ac |
29
README.md
29
README.md
@@ -17,9 +17,6 @@
|
||||
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-f0f0f0?style=for-the-badge&logoColor=000000" alt="Website"></a>
|
||||
[](https://discord.gg/strix-ai)
|
||||
|
||||
<a href="https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=badge_cloud"><img src="https://img.shields.io/badge/Strix%20Cloud-app.strix.ai-2b9246?style=for-the-badge&logoColor=white" alt="Strix Cloud"></a>
|
||||
<a href="https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=badge_demo"><img src="https://img.shields.io/badge/Try%20Strix%20Enterprise-555555?style=for-the-badge&logoColor=white" alt="Try Strix Enterprise"></a>
|
||||
|
||||
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-3b82f6?style=flat-square" alt="License"></a>
|
||||
@@ -37,7 +34,7 @@
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production - [Get started with no setup required](https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=tip_ci).
|
||||
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production - [Get started with no setup required](https://app.strix.ai).
|
||||
|
||||
---
|
||||
|
||||
@@ -97,17 +94,9 @@ strix --target ./app-directory
|
||||
|
||||
---
|
||||
|
||||
## Ways to Run Strix
|
||||
## ☁️ Strix Platform
|
||||
|
||||
- **Open Source** - free, runs locally with Docker and your own LLM key. [Quick Start](https://docs.strix.ai/quickstart)
|
||||
- **Strix Cloud** - no setup, validated findings, one-click autofix, and PR reviews. [Run a pentest →](https://app.strix.ai?intent=pentest&utm_source=github&utm_medium=readme&utm_content=table_cloud)
|
||||
- **Enterprise** - SSO, compliance-ready reports, VPC or self-hosted deployment. [Try Strix Enterprise →](https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=table_demo)
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Strix Cloud
|
||||
|
||||
Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=cloud_heading)** - sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
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.
|
||||
|
||||
- **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
|
||||
@@ -115,13 +104,7 @@ Try the Strix full-stack penetration testing platform at **[app.strix.ai](https:
|
||||
- **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
|
||||
|
||||
[**Run a pentest →**](https://app.strix.ai?intent=pentest&utm_source=github&utm_medium=readme&utm_content=cloud_cta)
|
||||
|
||||
## 🏢 Enterprise
|
||||
|
||||
Get the same Strix experience with enterprise-grade controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support and SLA, custom deployment options (VPC or self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment.
|
||||
|
||||
[**Try Strix Enterprise →**](https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=enterprise_cta)
|
||||
[**Start your first pentest →**](https://app.strix.ai)
|
||||
|
||||
---
|
||||
|
||||
@@ -350,6 +333,10 @@ Each server's tools are namespaced by `name`, for example `github_list_issues`.
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -19,12 +19,6 @@ Configure Strix using environment variables or a config file.
|
||||
Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_API_TYPE" type="string">
|
||||
Select the OpenAI API path for the model: `responses` or `chat_completions`.
|
||||
By default, a custom `LLM_API_BASE` uses chat completions. Set this variable
|
||||
when your gateway requires the other API. Also accepts `STRIX_FORCE_API`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_EXTRA_HEADERS" type="string">
|
||||
Extra HTTP headers sent on every LLM request, as a JSON object (e.g.
|
||||
`{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"llm-providers/openai",
|
||||
"llm-providers/anthropic",
|
||||
"llm-providers/openrouter",
|
||||
"llm-providers/vercel-ai-gateway",
|
||||
"llm-providers/vertex",
|
||||
"llm-providers/bedrock",
|
||||
"llm-providers/azure",
|
||||
|
||||
@@ -46,9 +46,6 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
|
||||
<Card title="OpenRouter" href="/llm-providers/openrouter">
|
||||
Access 100+ models through a single API.
|
||||
</Card>
|
||||
<Card title="Vercel AI Gateway" href="/llm-providers/vercel-ai-gateway">
|
||||
Access models from multiple providers through one endpoint.
|
||||
</Card>
|
||||
<Card title="Google Vertex AI" href="/llm-providers/vertex">
|
||||
Gemini 3 models via Google Cloud.
|
||||
</Card>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
title: "Vercel AI Gateway"
|
||||
description: "Configure Strix with models via Vercel AI Gateway"
|
||||
---
|
||||
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides an OpenAI-compatible API for models from multiple providers.
|
||||
|
||||
## Setup
|
||||
|
||||
Create an [AI Gateway API key](https://vercel.com/docs/ai-gateway/authentication-and-byok), then configure Strix:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/anthropic/claude-opus-5"
|
||||
export LLM_API_KEY="your-ai-gateway-api-key"
|
||||
export LLM_API_BASE="https://ai-gateway.vercel.sh/v1"
|
||||
```
|
||||
|
||||
The first `openai/` segment tells Strix to use its OpenAI-compatible client. The remaining value is the [AI Gateway model ID](https://vercel.com/docs/ai-gateway/models-and-providers).
|
||||
|
||||
## Available Models
|
||||
|
||||
Use any language model returned by the AI Gateway models endpoint:
|
||||
|
||||
```text
|
||||
https://ai-gateway.vercel.sh/v1/models
|
||||
```
|
||||
|
||||
Prefix its model ID with `openai/` when setting `STRIX_LLM`. For example, the Gateway model ID `anthropic/claude-opus-5` becomes `openai/anthropic/claude-opus-5` in Strix.
|
||||
|
||||
## Get API Key
|
||||
|
||||
1. Open the [AI Gateway API key settings](https://vercel.com/docs/ai-gateway/authentication-and-byok)
|
||||
2. Create an API key
|
||||
3. Set the key as `LLM_API_KEY`
|
||||
|
||||
## Benefits
|
||||
|
||||
- Access models from multiple providers through one endpoint
|
||||
- Track Gateway usage and cost in Vercel
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "strix-agent"
|
||||
version = "1.6.2"
|
||||
version = "1.6.1"
|
||||
description = "Open-source AI Hackers for your apps"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -346,9 +346,6 @@ echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
||||
echo -e "${MUTED}Supported models ${NC}https://docs.strix.ai/llm-providers/overview"
|
||||
echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai"
|
||||
echo ""
|
||||
echo -e "${MUTED}Run a pentest in Strix Cloud ${NC}https://app.strix.ai"
|
||||
echo -e "${MUTED}Enterprise ${NC}https://strix.ai/demo"
|
||||
echo ""
|
||||
|
||||
echo -e "${YELLOW}→${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
|
||||
echo ""
|
||||
|
||||
@@ -240,7 +240,6 @@ VALIDATION REQUIREMENTS:
|
||||
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
||||
- Reporting and fixing are ONE step, not two: when source is available, the reporting agent derives the concrete fix and files it INLINE via create_vulnerability_report (`code_locations` with `fix_before`/`fix_after` + `fix_pr_body`) — the report is not complete without it. Do NOT report first and then spawn a separate downstream agent to re-derive and re-apply the same patch; that just re-does the analysis and wastes tokens. (Do not silently patch a finding WITHOUT filing a report — the report, with its embedded fix, is the deliverable.)
|
||||
- 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. If your evidence proves more than the finding it matched (a working exploit where that one had only a static trace, a chain that raises the impact), revise that finding with update_vulnerability_report using the duplicate_of id — never re-file it.
|
||||
- HTTP EVIDENCE: a finding you validated through the proxy is not fully filed until `http_exchange_ids` carries the proxy request ids of the exchanges that prove it — the request that triggers the vulnerability plus the baseline/control request it differs from (an unauthenticated success next to the authenticated one, the payload response next to the benign one). Copy the ids exactly as `list_requests`/`view_request` show them, never invent or guess one, and never omit the field to bypass validation. Leave it out only when there is no captured HTTP exchange at all (static-only code findings, dependency CVEs). If you filed before the proving exchanges existed, attach them afterwards with update_vulnerability_report. Without the ids, the finding ships as prose nobody can replay.
|
||||
- REVISING A FINDING: use update_vulnerability_report (report id + the fields you want to replace + update_reason) when you learn something a finding already on file does not carry — you built the PoC after filing it, a chain raised its impact, further testing weakened it, or its counterevidence/remediation/code locations were wrong. Editing a finding needs no duplicate verdict, and it is always better than filing a second report for the same issue. Read the finding first with get_report, and pass only the fields that change.
|
||||
- REVIEWING FILED FINDINGS (orchestrator/root agent): use list_reports to see every vulnerability filed so far in this scan (by any agent, root or child) — metadata-first with per-severity counts — and get_report to read one finding in full by its id. These are read-only orchestration tools: the root agent uses them to track coverage, avoid dispatching work on already-covered ground, assemble the finish_scan executive summary, and reason about attack-chaining across confirmed findings. Leaf/specialist agents should NOT call them — just do your assigned testing and file findings. Each entry shows which agent filed it (agent_name), and your own entries are flagged by_you. list_notes/get_note do the same for notes.
|
||||
|
||||
@@ -494,8 +493,6 @@ SPECIALIZED TOOLS:
|
||||
PROXY & INTERCEPTION:
|
||||
- Caido CLI - Modern web proxy (already running). Use the proxy tools
|
||||
directly, or import `caido_api` from sandbox Python scripts.
|
||||
- Every proxied exchange has a request id (`list_requests`/`view_request`). Note the ids of the
|
||||
requests that prove a finding as you test — they go into `http_exchange_ids` when you report it.
|
||||
- HTTPQL filters (for `list_requests`): quote string values, leave integers unquoted (`resp.code.eq:200`, not `"200"`); combine terms with `AND`/`OR` (there is no `NOT` — use the negated operator `ne`/`ncont`/`nregex`). Numeric fields (`resp.code`, `req.port`) use `eq`/`ne`/`gt`/`gte`/`lt`/`lte`; text fields (`req.host`, `req.path`, `req.method`, `req.raw`) use `cont`/`ncont`/`eq`/`regex`. Example: `resp.code.gte:200 AND resp.code.lt:300 AND req.host.cont:"api"`.
|
||||
|
||||
CAIDO PROXY ERROR PAGES — NOT RESPONSES FROM THE TARGET:
|
||||
|
||||
@@ -593,27 +593,17 @@ RECOMMENDED_MODEL_NAMES = (
|
||||
|
||||
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
|
||||
|
||||
# Matched against the bare model name only: the route (``openai/``, ``openrouter/``,
|
||||
# a local gateway, ...) says nothing about the model's quality.
|
||||
FRONTIER_MODEL_PREFIXES = (
|
||||
"gpt-5",
|
||||
"claude-fable-5",
|
||||
"claude-opus-5",
|
||||
"claude-opus-4",
|
||||
"claude-sonnet-5",
|
||||
"claude-sonnet-4",
|
||||
"gemini-3",
|
||||
"deepseek-v4",
|
||||
"deepseek-r1",
|
||||
"deepseek-reasoner",
|
||||
"qwen3.8",
|
||||
"qwen3.7",
|
||||
"qwen3-max",
|
||||
"kimi-k3",
|
||||
"kimi-k2.7",
|
||||
"kimi-k2.6",
|
||||
"glm-5.3",
|
||||
"glm-5.2",
|
||||
FRONTIER_MODEL_FAMILIES = (
|
||||
(("azure", "azure_ai", "bedrock_mantle", "chatgpt", "openai"), ("gpt-5",)),
|
||||
(
|
||||
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
|
||||
("claude-fable-5", "claude-opus-5", "claude-opus-4", "claude-sonnet-5", "claude-sonnet-4"),
|
||||
),
|
||||
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
|
||||
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "qwen"), ("qwen3.8", "qwen3.7", "qwen3-max")),
|
||||
(("moonshot", "moonshotai", "kimi"), ("kimi-k3", "kimi-k2.7", "kimi-k2.6")),
|
||||
(("zai", "z-ai", "zai-org", "zhipuai"), ("glm-5.3", "glm-5.2")),
|
||||
)
|
||||
|
||||
|
||||
@@ -632,11 +622,9 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
if llm.api_base:
|
||||
os.environ["OPENAI_BASE_URL"] = llm.api_base
|
||||
_configure_litellm_default("api_base", llm.api_base)
|
||||
api_type = llm.api_type
|
||||
if api_type is None:
|
||||
api_type = "chat_completions" if llm.api_base else "responses"
|
||||
|
||||
set_default_openai_api(api_type)
|
||||
set_default_openai_api("chat_completions")
|
||||
else:
|
||||
set_default_openai_api("responses")
|
||||
_configure_extra_headers(llm)
|
||||
|
||||
|
||||
@@ -811,8 +799,6 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo
|
||||
model = model_name.strip().lower()
|
||||
if "/" in model and not model.startswith("openai/"):
|
||||
return True
|
||||
if settings.llm.api_type is not None:
|
||||
return settings.llm.api_type == "chat_completions"
|
||||
if settings.llm.api_base:
|
||||
return True
|
||||
return not model_supports_reasoning(model_name)
|
||||
@@ -851,8 +837,11 @@ def is_recommended_or_frontier_model(model_name: str) -> bool:
|
||||
return False
|
||||
if name in _RECOMMENDED_MODEL_NAME_SET:
|
||||
return True
|
||||
bare_model_name = name.rsplit("/", 1)[-1]
|
||||
return _matches_model_prefix(bare_model_name, FRONTIER_MODEL_PREFIXES)
|
||||
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:
|
||||
@@ -864,6 +853,28 @@ def _normalized_model_name(model_name: str) -> str:
|
||||
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)
|
||||
@@ -881,6 +892,16 @@ def _model_name_candidates(model_name: str) -> tuple[str, ...]:
|
||||
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
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
ApiType = Literal["responses", "chat_completions"]
|
||||
|
||||
DEFAULT_MAX_TURNS = 500
|
||||
|
||||
@@ -24,11 +23,6 @@ class LlmSettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
model: str | None = Field(default=None, alias="STRIX_LLM")
|
||||
api_type: ApiType | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("STRIX_API_TYPE", "STRIX_FORCE_API"),
|
||||
description="Force 'responses' or 'chat_completions' API path",
|
||||
)
|
||||
api_key: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"),
|
||||
|
||||
@@ -98,14 +98,6 @@ Examples:
|
||||
# Extra files placed in the sandbox workspace
|
||||
strix --target ./my-project --workspace-file ./wordlist.txt
|
||||
strix --target https://app.com --workspace-file ./openapi.yaml:specs/openapi.yaml
|
||||
|
||||
Strix Cloud:
|
||||
strix cloud login
|
||||
strix cloud scans start --source . --yes --wait
|
||||
strix cloud # list every cloud resource
|
||||
|
||||
Run a pentest in Strix Cloud https://app.strix.ai
|
||||
Try Strix Enterprise https://strix.ai/demo
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@@ -333,11 +333,6 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
||||
"[#60a5fa]docs.strix.ai[/] [dim]·[/] "
|
||||
"[#60a5fa]discord.gg/strix-ai[/]"
|
||||
)
|
||||
if not args.non_interactive:
|
||||
console.print(
|
||||
"[dim]Run a pentest in Strix Cloud[/] [#60a5fa]app.strix.ai[/] [dim]·[/] "
|
||||
"[dim]Enterprise[/] [#60a5fa]strix.ai/demo[/]"
|
||||
)
|
||||
console.print()
|
||||
if not args.non_interactive:
|
||||
notify_update(console)
|
||||
|
||||
@@ -192,7 +192,7 @@ class TuiController:
|
||||
model_warning = ""
|
||||
if model and not is_recommended_or_frontier_model(model):
|
||||
model_warning = (
|
||||
f"{model} is not a recommended frontier model. Pentest quality could be degraded."
|
||||
f"{model} is not a recommended frontier model; pentest quality could be degraded"
|
||||
)
|
||||
state = {
|
||||
"setup_mode": self.setup_mode,
|
||||
|
||||
@@ -370,17 +370,6 @@ func TestStartedSnapshotTransitionsToLiveView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplashModelWarningRendersTheBackendSentenceOnce(t *testing.T) {
|
||||
warning := "openai/glm-5.3 is not a recommended frontier model. Pentest quality could be degraded."
|
||||
got := ansi.Strip(splashModelWarning("openai/glm-5.3", warning))
|
||||
if got != "⚠ "+warning {
|
||||
t.Fatalf("splash warning = %q, want %q", got, "⚠ "+warning)
|
||||
}
|
||||
if got := ansi.Strip(splashModelWarning("other/model", warning)); got != "⚠ "+warning {
|
||||
t.Fatalf("splash warning with unrelated model = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupStartScreenFitsNarrowTerminal(t *testing.T) {
|
||||
model := New(nil)
|
||||
model.width, model.height = 40, 18
|
||||
|
||||
@@ -411,7 +411,7 @@ func (m Model) splashView() string {
|
||||
welcome + "\n" + version + "\n" + tagline + "\n\n" +
|
||||
start.String() + "\n\n" + url
|
||||
if warn := m.snapshot.ModelWarning; warn != "" {
|
||||
content += "\n\n" + splashModelWarning(m.snapshot.Model, warn)
|
||||
content += "\n\n" + splashModelWarning(warn)
|
||||
}
|
||||
panel := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(green).Padding(1, 6).Align(lipgloss.Center).Render(content)
|
||||
// #splash_screen background is solid black.
|
||||
@@ -419,16 +419,12 @@ func (m Model) splashView() string {
|
||||
lipgloss.WithWhitespaceBackground(black))
|
||||
}
|
||||
|
||||
// splashModelWarning renders the backend's full warning sentence, with the
|
||||
// model name highlighted when the sentence leads with it.
|
||||
func splashModelWarning(model, warning string) string {
|
||||
// splashModelWarning ports SplashScreen._build_model_warning_text.
|
||||
func splashModelWarning(model string) string {
|
||||
yellow := lipgloss.Color("#eab308")
|
||||
out := lipgloss.NewStyle().Bold(true).Foreground(yellow).Render("⚠ ")
|
||||
if model != "" && strings.HasPrefix(warning, model) {
|
||||
out += lipgloss.NewStyle().Bold(true).Foreground(render.Cyan).Render(model)
|
||||
warning = strings.TrimPrefix(warning, model)
|
||||
}
|
||||
return out + lipgloss.NewStyle().Foreground(yellow).Render(warning)
|
||||
return lipgloss.NewStyle().Bold(true).Foreground(yellow).Render("⚠ ") +
|
||||
lipgloss.NewStyle().Bold(true).Foreground(render.Cyan).Render(model) +
|
||||
lipgloss.NewStyle().Foreground(yellow).Render(" is not a recommended frontier model - pentest quality could be degraded")
|
||||
}
|
||||
|
||||
// chatPaneKey identifies everything the bordered trace depends on.
|
||||
|
||||
@@ -49,11 +49,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _revision_count(report: dict[str, Any]) -> int:
|
||||
history = report.get("update_history")
|
||||
return len(history) if isinstance(history, list) else 0
|
||||
|
||||
|
||||
class GoTuiPreActivationError(RuntimeError):
|
||||
"""A sidecar failure raised before the Go TUI activates."""
|
||||
|
||||
@@ -344,9 +339,7 @@ class GoTuiRuntime:
|
||||
if self.report_state is not None:
|
||||
usage = dict(self.report_state.get_total_llm_usage())
|
||||
vulnerabilities = [
|
||||
(report.get("id", index), _revision_count(report))
|
||||
if isinstance(report, dict)
|
||||
else index
|
||||
report.get("id", index) if isinstance(report, dict) else index
|
||||
for index, report in enumerate(self.report_state.vulnerability_reports)
|
||||
]
|
||||
return json.dumps(
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Radar,
|
||||
Rocket,
|
||||
ArrowUpRight,
|
||||
Building2,
|
||||
History,
|
||||
} from "lucide-react";
|
||||
import type { Vulnerability, VulnerabilitySeverity } from "@/types/issues";
|
||||
@@ -36,7 +35,7 @@ import {
|
||||
type LoadedRun,
|
||||
type RunsPayload,
|
||||
} from "@/data/serverSource";
|
||||
import { SIGNUP_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { runTitle } from "@/lib/target-utils";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import PastRunsView from "@/components/PastRunsView";
|
||||
@@ -707,31 +706,6 @@ function OverviewTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{finished && (
|
||||
<div className="animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
|
||||
<p className="text-sm font-semibold text-white">Strix Cloud</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Run your next pentest in Strix Cloud.</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2.5">
|
||||
<ProInlineCta
|
||||
label="Run a pentest in Strix Cloud"
|
||||
desc="Validated findings, autofix, and PR reviews."
|
||||
slug="overview_cloud"
|
||||
surface="overview"
|
||||
icon={Rocket}
|
||||
primary
|
||||
/>
|
||||
<ProInlineCta
|
||||
label="Try Strix Enterprise"
|
||||
desc="SSO, compliance-ready reports, VPC or self-hosted deployment."
|
||||
slug="book_demo"
|
||||
surface="overview"
|
||||
icon={Building2}
|
||||
href={DEMO_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections.length > 0 ? (
|
||||
<div className="animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5 space-y-8">
|
||||
{sections.map((s) => (
|
||||
@@ -815,23 +789,14 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
|
||||
{/* Re-run always routes to Strix Cloud. */}
|
||||
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
|
||||
<p className="text-sm font-semibold text-white">Run this pentest with more depth</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Run this pentest again in Strix Cloud.</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Re-run this pentest on managed infra in the cloud.</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2.5">
|
||||
<ProInlineCta
|
||||
label="Re-run in Strix Pro with more depth"
|
||||
desc="More depth, validated findings, and autofix."
|
||||
desc="Run this pentest on managed infra with more depth."
|
||||
slug="live_scan"
|
||||
surface="agents"
|
||||
icon={Rocket}
|
||||
primary
|
||||
/>
|
||||
<ProInlineCta
|
||||
label="Try Strix Enterprise"
|
||||
desc="SSO, compliance-ready reports, VPC or self-hosted deployment."
|
||||
slug="book_demo"
|
||||
surface="agents"
|
||||
icon={Building2}
|
||||
href={DEMO_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,40 +48,23 @@ export function ProInlineCta({
|
||||
slug,
|
||||
icon: Icon,
|
||||
surface,
|
||||
href = SIGNUP_URL,
|
||||
primary = false,
|
||||
}: {
|
||||
label: string;
|
||||
desc: string;
|
||||
slug: string;
|
||||
icon: React.ElementType;
|
||||
surface?: string;
|
||||
/** Destination before attribution params. Defaults to cloud sign-up. */
|
||||
href?: string;
|
||||
/** Solid white button instead of the outlined default. */
|
||||
primary?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip text={desc}>
|
||||
<a
|
||||
href={ctaUrl(href, slug)}
|
||||
href={ctaUrl(SIGNUP_URL, slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackCta(slug, surface)}
|
||||
className={
|
||||
primary
|
||||
? "group inline-flex items-center gap-2 rounded-lg border border-white bg-white px-3 py-2 text-sm font-semibold text-black transition-colors hover:bg-[#e5e5e5]"
|
||||
: "group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
|
||||
}
|
||||
className="group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
primary
|
||||
? "h-4 w-4 text-black"
|
||||
: "h-4 w-4 text-[#888] transition-colors group-hover:text-white"
|
||||
}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
WandSparkles,
|
||||
Plug,
|
||||
} from "lucide-react";
|
||||
import { SIGNUP_URL, PRICING_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { SIGNUP_URL, PRICING_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
|
||||
/**
|
||||
* Dialog shown when a platform feature is clicked in the sidebar: a short
|
||||
@@ -141,19 +141,6 @@ export function UpgradeModal({
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-[#666]">
|
||||
SSO, compliance reports, or a private deployment?{" "}
|
||||
<a
|
||||
href={ctaUrl(DEMO_URL, "upgrade_book_demo")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackCta("upgrade_book_demo", source)}
|
||||
className="whitespace-nowrap text-[#aaa] underline underline-offset-2 transition-colors hover:text-white"
|
||||
>
|
||||
Try Strix Enterprise
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Strix Results</title>
|
||||
<script type="module" crossorigin src="./assets/index-B94ANU8d.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DN__rVv3.css">
|
||||
<script type="module" crossorigin src="./assets/index-Bpn8GiSb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-qwPOPAGC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -73,7 +73,6 @@ UPDATABLE_REPORT_FIELDS = frozenset(
|
||||
"cve",
|
||||
"cwe",
|
||||
"code_locations",
|
||||
"http_exchange_ids",
|
||||
"fix_verification",
|
||||
"fix_pr_body",
|
||||
}
|
||||
@@ -334,7 +333,6 @@ class ReportState:
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
finding_class: str | None = None,
|
||||
@@ -393,8 +391,6 @@ class ReportState:
|
||||
report["cwe"] = cwe.strip()
|
||||
if code_locations:
|
||||
report["code_locations"] = code_locations
|
||||
if http_exchange_ids:
|
||||
report["http_exchange_ids"] = http_exchange_ids
|
||||
if fix_verification:
|
||||
report["fix_verification"] = fix_verification.strip()
|
||||
if fix_pr_body:
|
||||
@@ -407,14 +403,14 @@ class ReportState:
|
||||
if agent_name:
|
||||
report["agent_name"] = agent_name
|
||||
|
||||
if self.vulnerability_found_callback:
|
||||
self.vulnerability_found_callback(report)
|
||||
|
||||
self.vulnerability_reports.append(report)
|
||||
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
||||
posthog.finding(severity, cwe=cwe, is_cve=bool(cve))
|
||||
scarf.finding(severity, cwe=cwe, is_cve=bool(cve))
|
||||
|
||||
if self.vulnerability_found_callback:
|
||||
self.vulnerability_found_callback(report)
|
||||
|
||||
self.save_run_data()
|
||||
return report_id
|
||||
|
||||
@@ -490,18 +486,11 @@ class ReportState:
|
||||
)
|
||||
history.append(entry)
|
||||
|
||||
revised = {**report, **changed}
|
||||
report.update(changed)
|
||||
for dependent in superseded:
|
||||
revised.pop(dependent, None)
|
||||
revised["update_history"] = history
|
||||
revised["updated_at"] = entry["timestamp"]
|
||||
|
||||
# Persistence must accept the revision before local state changes. A
|
||||
# failed callback leaves the old evidence intact and the update retryable.
|
||||
if self.vulnerability_updated_callback:
|
||||
self.vulnerability_updated_callback(revised)
|
||||
report.clear()
|
||||
report.update(revised)
|
||||
report.pop(dependent, None)
|
||||
report["update_history"] = history
|
||||
report["updated_at"] = entry["timestamp"]
|
||||
|
||||
# The markdown on disk still shows the superseded evidence, so let the
|
||||
# writer re-render it.
|
||||
@@ -513,6 +502,9 @@ class ReportState:
|
||||
", ".join(entry["fields"]) or "no field replaced",
|
||||
)
|
||||
|
||||
if self.vulnerability_updated_callback:
|
||||
self.vulnerability_updated_callback(report)
|
||||
|
||||
self.save_run_data()
|
||||
return report
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -68,31 +67,6 @@ async def _call[T](client: Client, fn: Callable[[Client], Awaitable[T]]) -> T:
|
||||
return await fn(client)
|
||||
|
||||
|
||||
async def existing_request_ids(
|
||||
ctx: RunContextWrapper,
|
||||
request_ids: list[str],
|
||||
) -> set[str]:
|
||||
"""Return request IDs that exist in the current Caido project."""
|
||||
if not request_ids:
|
||||
return set()
|
||||
|
||||
client = await _ctx_client(ctx)
|
||||
if client is None:
|
||||
raise RuntimeError("Caido client is not available")
|
||||
|
||||
# Request IDs are not an HTTPQL field. Resolve each ID through the same
|
||||
# project-bound lookup as view_request rather than constructing a filter.
|
||||
existing: set[str] = set()
|
||||
for request_id in request_ids:
|
||||
result = await _call(
|
||||
client,
|
||||
functools.partial(caido_api.get_request_with_client, request_id=request_id),
|
||||
)
|
||||
if result is not None:
|
||||
existing.add(str(result.request.id))
|
||||
return existing
|
||||
|
||||
|
||||
def _to_tool_json(value: Any) -> Any:
|
||||
"""Recursively convert SDK dataclasses/Pydantic objects to tool JSON values."""
|
||||
if value is None or isinstance(value, str | int | float | bool):
|
||||
|
||||
@@ -17,7 +17,6 @@ from typing import TYPE_CHECKING, Any
|
||||
from agents import RunContextWrapper, function_tool
|
||||
|
||||
from strix.tools.nullish import clean_optional
|
||||
from strix.tools.proxy.tools import existing_request_ids
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -169,8 +168,6 @@ _REQUIRED_FIELDS = {
|
||||
|
||||
_VALID_FIX_EFFORT = frozenset({"trivial", "low", "medium", "high"})
|
||||
_VALID_CONFIDENCE = frozenset({"high", "medium", "low"})
|
||||
_MAX_HTTP_EXCHANGE_IDS = 10
|
||||
_MAX_HTTP_EXCHANGE_ID_CHARS = 128
|
||||
|
||||
|
||||
def _validate_required_text(fields: dict[str, str]) -> list[str]:
|
||||
@@ -180,97 +177,6 @@ def _validate_required_text(fields: dict[str, str]) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def _normalize_http_exchange_ids(raw: Any) -> tuple[list[str] | None, list[str]]:
|
||||
"""Return distinct proxy exchange ids in their original order."""
|
||||
if raw is None:
|
||||
return None, []
|
||||
if not isinstance(raw, list):
|
||||
return None, ["http_exchange_ids must be a list of proxy request ids"]
|
||||
|
||||
normalized: list[str] = []
|
||||
errors: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for index, value in enumerate(raw):
|
||||
if not isinstance(value, str):
|
||||
errors.append(f"http_exchange_ids[{index}] must be a string")
|
||||
continue
|
||||
request_id = value.strip()
|
||||
if not request_id:
|
||||
errors.append(f"http_exchange_ids[{index}] cannot be empty")
|
||||
continue
|
||||
if len(request_id) > _MAX_HTTP_EXCHANGE_ID_CHARS:
|
||||
errors.append(
|
||||
f"http_exchange_ids[{index}] must be {_MAX_HTTP_EXCHANGE_ID_CHARS} "
|
||||
"characters or fewer"
|
||||
)
|
||||
continue
|
||||
if any(ord(char) < 0x21 or ord(char) > 0x7E for char in request_id):
|
||||
errors.append(f"http_exchange_ids[{index}] must contain only visible ASCII characters")
|
||||
continue
|
||||
if not request_id.isdigit():
|
||||
errors.append(f"http_exchange_ids[{index}] must be a numeric proxy request id")
|
||||
continue
|
||||
if request_id not in seen:
|
||||
seen.add(request_id)
|
||||
normalized.append(request_id)
|
||||
if len(normalized) > _MAX_HTTP_EXCHANGE_IDS:
|
||||
errors.append(
|
||||
f"http_exchange_ids can contain at most "
|
||||
f"{_MAX_HTTP_EXCHANGE_IDS} distinct request ids"
|
||||
)
|
||||
break
|
||||
return normalized, errors
|
||||
|
||||
|
||||
_HTTP_EXCHANGE_DROPPED_WARNING = (
|
||||
"http_exchange_ids were not stored: the proxy project could not be reached to verify "
|
||||
"them. Attach them with update_vulnerability_report when the proxy responds again."
|
||||
)
|
||||
|
||||
|
||||
async def _verify_http_exchange_ids(
|
||||
ctx: RunContextWrapper,
|
||||
raw: Any,
|
||||
) -> tuple[list[str] | None, list[str], str | None]:
|
||||
"""Verify proxy exchange IDs against the current Caido project.
|
||||
|
||||
IDs the project does not know are rejected. When the proxy itself cannot be
|
||||
queried the IDs are dropped and a warning is returned instead, so a proxy
|
||||
outage never blocks a finding and unverified IDs are never recorded as
|
||||
evidence.
|
||||
"""
|
||||
request_ids, errors = _normalize_http_exchange_ids(raw)
|
||||
if request_ids is None or errors or not request_ids:
|
||||
return request_ids, errors, None
|
||||
|
||||
try:
|
||||
existing_ids = await existing_request_ids(ctx, request_ids)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning(
|
||||
"Could not verify HTTP exchange IDs against the current Caido project",
|
||||
exc_info=True,
|
||||
)
|
||||
return None, [], _HTTP_EXCHANGE_DROPPED_WARNING
|
||||
|
||||
missing_ids = [request_id for request_id in request_ids if request_id not in existing_ids]
|
||||
if missing_ids:
|
||||
return (
|
||||
None,
|
||||
[
|
||||
"http_exchange_ids do not exist in the current proxy project: "
|
||||
+ ", ".join(missing_ids)
|
||||
],
|
||||
None,
|
||||
)
|
||||
return request_ids, [], None
|
||||
|
||||
|
||||
def _with_warning(result: dict[str, Any], warning: str | None) -> dict[str, Any]:
|
||||
if warning and result.get("success"):
|
||||
result["warning"] = warning
|
||||
return result
|
||||
|
||||
|
||||
def _validate_cvss_breakdown(breakdown: Any) -> list[str]:
|
||||
"""Check the 8 CVSS metrics are all present with legal values."""
|
||||
if not isinstance(breakdown, dict) or not breakdown:
|
||||
@@ -393,7 +299,7 @@ _UPDATE_TEXT_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def _collect_update_changes( # noqa: PLR0912, PLR0915
|
||||
def _collect_update_changes( # noqa: PLR0912
|
||||
fields: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], list[str]]:
|
||||
"""Validate the fields a revision replaces and return them with any errors."""
|
||||
@@ -462,12 +368,6 @@ def _collect_update_changes( # noqa: PLR0912, PLR0915
|
||||
if cwe:
|
||||
changes["cwe"] = cwe
|
||||
|
||||
raw_http_exchange_ids = fields.get("http_exchange_ids")
|
||||
http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids(raw_http_exchange_ids)
|
||||
errors.extend(http_exchange_errors)
|
||||
if raw_http_exchange_ids is not None and not http_exchange_errors:
|
||||
changes["http_exchange_ids"] = http_exchange_ids or []
|
||||
|
||||
return changes, errors
|
||||
|
||||
|
||||
@@ -478,7 +378,6 @@ _DYNAMIC_ONLY_UPDATE_FIELDS = (
|
||||
"method",
|
||||
"poc_description",
|
||||
"poc_script_code",
|
||||
"http_exchange_ids",
|
||||
)
|
||||
|
||||
# A dependency finding is rated in the context of the codebase that pins it, and
|
||||
@@ -652,24 +551,13 @@ def _do_update(
|
||||
if class_error is not None:
|
||||
return class_error
|
||||
|
||||
try:
|
||||
updated = report_state.update_vulnerability_report(
|
||||
report_id,
|
||||
changes,
|
||||
update_reason=update_reason,
|
||||
updated_by_agent_id=agent_id,
|
||||
updated_by_agent_name=agent_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("update_vulnerability_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to revise report '{report_id}': {e!s}. "
|
||||
"The report still carries its previous content; retry the update."
|
||||
),
|
||||
"report_id": report_id,
|
||||
}
|
||||
updated = report_state.update_vulnerability_report(
|
||||
report_id,
|
||||
changes,
|
||||
update_reason=update_reason,
|
||||
updated_by_agent_id=agent_id,
|
||||
updated_by_agent_name=agent_name,
|
||||
)
|
||||
if updated is None:
|
||||
known = [r.get("id") for r in report_state.get_existing_vulnerabilities()]
|
||||
if report_id not in known:
|
||||
@@ -718,7 +606,6 @@ async def _do_create(
|
||||
cve: str | None,
|
||||
cwe: str | None,
|
||||
code_locations: list[dict[str, Any]] | None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
confidence_rationale: str | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
@@ -764,10 +651,6 @@ async def _do_create(
|
||||
errors.extend(_validate_fix_verification(parsed_locations, fix_verification))
|
||||
cve, cwe, identifier_errors = _validate_identifiers(cve, cwe)
|
||||
errors.extend(identifier_errors)
|
||||
normalized_http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids(
|
||||
http_exchange_ids
|
||||
)
|
||||
errors.extend(http_exchange_errors)
|
||||
|
||||
if errors:
|
||||
return {"success": False, "error": "Validation failed", "errors": errors}
|
||||
@@ -829,7 +712,6 @@ async def _do_create(
|
||||
"code_locations": parsed_locations,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"http_exchange_ids": normalized_http_exchange_ids,
|
||||
}
|
||||
|
||||
dedupe = await check_duplicate(candidate, existing)
|
||||
@@ -856,15 +738,9 @@ async def _do_create(
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
agent_name=agent_name if isinstance(agent_name, str) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.exception("create_vulnerability_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to create vulnerability report: {e!s}. "
|
||||
"The finding was not stored; file it again."
|
||||
),
|
||||
}
|
||||
return {"success": False, "error": f"Failed to create vulnerability report: {e!s}"}
|
||||
else:
|
||||
logger.info(
|
||||
"Vulnerability report created: id=%s severity=%s cvss=%.1f title=%s",
|
||||
@@ -920,7 +796,6 @@ async def create_vulnerability_report(
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
confidence_rationale: str | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
@@ -1165,18 +1040,6 @@ async def create_vulnerability_report(
|
||||
cve: ``CVE-YYYY-NNNNN`` if certain, else omit.
|
||||
cwe: ``CWE-NNN`` (most specific child) if certain, else omit.
|
||||
code_locations: White-box findings — list of location objects.
|
||||
http_exchange_ids: Proxy request IDs that prove this finding.
|
||||
Copy these IDs from ``list_requests`` or ``view_request``.
|
||||
For a finding validated over HTTP, capture and inspect the
|
||||
supporting exchanges and include their IDs here before filing.
|
||||
Include relevant baseline/control requests as well as the exploit.
|
||||
Omit only when the finding has no captured HTTP evidence (for
|
||||
example a static-only code finding). Never invent IDs or drop
|
||||
them to bypass a verification error; retry the capture instead.
|
||||
If the result carries a ``warning`` that the IDs were not
|
||||
stored, the finding is filed without them: attach them with
|
||||
``update_vulnerability_report`` once the proxy responds.
|
||||
Keep IDs out of ``evidence`` and all other report text.
|
||||
|
||||
**How ``fix_before`` / ``fix_after`` work**: they're used as
|
||||
literal GitHub/GitLab PR suggestion blocks. When a reviewer
|
||||
@@ -1324,22 +1187,6 @@ async def create_vulnerability_report(
|
||||
reduce impact and lower the severity.
|
||||
fix_effort: "low"
|
||||
"""
|
||||
(
|
||||
http_exchange_ids,
|
||||
http_exchange_errors,
|
||||
http_exchange_warning,
|
||||
) = await _verify_http_exchange_ids(ctx, http_exchange_ids)
|
||||
if http_exchange_errors:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Validation failed",
|
||||
"errors": http_exchange_errors,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
agent_id, agent_name = _caller_identity(ctx)
|
||||
|
||||
result = await _do_create(
|
||||
@@ -1364,13 +1211,12 @@ async def create_vulnerability_report(
|
||||
cve=cve,
|
||||
cwe=cwe,
|
||||
code_locations=code_locations,
|
||||
http_exchange_ids=http_exchange_ids,
|
||||
fix_verification=fix_verification,
|
||||
fix_pr_body=fix_pr_body,
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
return json.dumps(_with_warning(result, http_exchange_warning), ensure_ascii=False, default=str)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
@function_tool(timeout=60, strict_mode=False)
|
||||
@@ -1399,7 +1245,6 @@ async def update_vulnerability_report(
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
contextual_cvss_reasoning: str | None = None,
|
||||
@@ -1472,74 +1317,47 @@ async def update_vulnerability_report(
|
||||
cve: Replacement CVE id.
|
||||
cwe: Replacement CWE id.
|
||||
code_locations: Replacement code locations.
|
||||
http_exchange_ids: Replacement proxy request ids. Pass an empty
|
||||
list to remove all linked exchanges.
|
||||
fix_verification: Verification statement for an applyable fix.
|
||||
fix_pr_body: Replacement fix PR body.
|
||||
contextual_cvss_reasoning: Dependency findings only. What you
|
||||
observed in this codebase that justifies the contextual
|
||||
``cvss_breakdown``.
|
||||
"""
|
||||
(
|
||||
http_exchange_ids,
|
||||
http_exchange_errors,
|
||||
http_exchange_warning,
|
||||
) = await _verify_http_exchange_ids(ctx, http_exchange_ids)
|
||||
if http_exchange_errors:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Validation failed",
|
||||
"errors": http_exchange_errors,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
fields = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"impact": impact,
|
||||
"target": target,
|
||||
"technical_analysis": technical_analysis,
|
||||
"poc_description": poc_description,
|
||||
"poc_script_code": poc_script_code,
|
||||
"remediation_steps": remediation_steps,
|
||||
"evidence": evidence,
|
||||
"assumptions": assumptions,
|
||||
"counterevidence": counterevidence,
|
||||
"confidence": confidence,
|
||||
"confidence_rationale": confidence_rationale,
|
||||
"severity_change_conditions": severity_change_conditions,
|
||||
"fix_effort": fix_effort,
|
||||
"cvss_breakdown": cvss_breakdown,
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"cve": cve,
|
||||
"cwe": cwe,
|
||||
"code_locations": code_locations,
|
||||
"http_exchange_ids": http_exchange_ids,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"contextual_cvss_reasoning": contextual_cvss_reasoning,
|
||||
}
|
||||
if http_exchange_warning and all(value is None for value in fields.values()):
|
||||
return json.dumps(
|
||||
{"success": False, "error": http_exchange_warning, "report_id": report_id},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
agent_id, agent_name = _caller_identity(ctx)
|
||||
result = await asyncio.to_thread(
|
||||
_do_update,
|
||||
report_id=report_id,
|
||||
update_reason=update_reason,
|
||||
fields=fields,
|
||||
fields={
|
||||
"title": title,
|
||||
"description": description,
|
||||
"impact": impact,
|
||||
"target": target,
|
||||
"technical_analysis": technical_analysis,
|
||||
"poc_description": poc_description,
|
||||
"poc_script_code": poc_script_code,
|
||||
"remediation_steps": remediation_steps,
|
||||
"evidence": evidence,
|
||||
"assumptions": assumptions,
|
||||
"counterevidence": counterevidence,
|
||||
"confidence": confidence,
|
||||
"confidence_rationale": confidence_rationale,
|
||||
"severity_change_conditions": severity_change_conditions,
|
||||
"fix_effort": fix_effort,
|
||||
"cvss_breakdown": cvss_breakdown,
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"cve": cve,
|
||||
"cwe": cwe,
|
||||
"code_locations": code_locations,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"contextual_cvss_reasoning": contextual_cvss_reasoning,
|
||||
},
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
return json.dumps(_with_warning(result, http_exchange_warning), ensure_ascii=False, default=str)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
_DEP_SEVERITY_FROM_CVSS = {
|
||||
@@ -1929,15 +1747,9 @@ async def _do_create_dependency( # noqa: PLR0912
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
agent_name=agent_name if isinstance(agent_name, str) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.exception("create_dependency_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to create dependency report: {e!s}. "
|
||||
"The finding was not stored; file it again."
|
||||
),
|
||||
}
|
||||
return {"success": False, "error": f"Failed to create dependency report: {e!s}"}
|
||||
else:
|
||||
logger.info(
|
||||
"Dependency report created: id=%s cve=%s package=%s severity=%s",
|
||||
|
||||
@@ -121,7 +121,7 @@ def _exa_content(api_key: str, query: str, search_type: str, num_results: int) -
|
||||
api_key,
|
||||
"https://api.exa.ai/search",
|
||||
{
|
||||
"query": query,
|
||||
"query": f"{_SYSTEM_PROMPT}\n\n{query}",
|
||||
"type": search_type,
|
||||
"numResults": num_results,
|
||||
"contents": {"summary": {"query": _EXA_SUMMARY_PROMPT}},
|
||||
|
||||
@@ -19,7 +19,6 @@ from strix.config.settings import DEFAULT_MAX_TURNS
|
||||
from strix.interface.tui import runtime as go_tui
|
||||
from strix.interface.tui import sidecar
|
||||
from strix.interface.tui.runtime import GoTuiRuntime
|
||||
from strix.report.state import ReportState
|
||||
|
||||
|
||||
def args() -> argparse.Namespace:
|
||||
@@ -1028,29 +1027,3 @@ async def test_prepare_and_start_runs_the_scan_after_preparation(
|
||||
|
||||
assert order == ["preflight", "persist", "prepare", "telemetry", "state", "scan"]
|
||||
assert runtime.controller.scan_state == "running"
|
||||
|
||||
|
||||
def test_sync_fingerprint_tracks_report_revisions(tmp_path: Path) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
runtime.report_state = ReportState(run_name="test-run")
|
||||
runtime.report_state.vulnerability_reports = [{"id": "vuln-0001", "title": "Old title"}]
|
||||
runtime.report_state.get_run_dir = lambda: tmp_path # type: ignore[method-assign]
|
||||
|
||||
report = runtime.report_state.vulnerability_reports[0]
|
||||
timestamp = "2026-09-09 10:00:00 UTC"
|
||||
|
||||
before = runtime._runtime_sync_fingerprint()
|
||||
report.update(
|
||||
{
|
||||
"title": "New title",
|
||||
"updated_at": timestamp,
|
||||
"update_history": [{"timestamp": timestamp, "fields": ["title"]}],
|
||||
}
|
||||
)
|
||||
first_revision = runtime._runtime_sync_fingerprint()
|
||||
assert first_revision != before
|
||||
|
||||
report["title"] = "Newer title"
|
||||
report["update_history"].append({"timestamp": timestamp, "fields": ["title"]})
|
||||
|
||||
assert runtime._runtime_sync_fingerprint() != first_revision
|
||||
|
||||
@@ -2,27 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from agents.extensions.models.litellm_model import LitellmModel
|
||||
from agents.model_settings import ModelSettings
|
||||
from agents.models import _openai_shared
|
||||
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
||||
from agents.models.openai_responses import OpenAIResponsesModel
|
||||
|
||||
from strix.config.models import (
|
||||
RECOMMENDED_MODEL_NAMES,
|
||||
StrixProvider,
|
||||
_NonStreamingModel,
|
||||
_TurnGuardModel,
|
||||
configure_sdk_model_defaults,
|
||||
is_recommended_or_frontier_model,
|
||||
request_timeout_extra_args,
|
||||
routes_through_litellm,
|
||||
supports_strict_tool_schemas,
|
||||
uses_chat_completions_tool_schema,
|
||||
)
|
||||
from strix.config.settings import Settings
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
|
||||
@@ -86,14 +79,6 @@ def test_recommended_models_are_matched_case_insensitively() -> None:
|
||||
"zai/glm-5.3-flash",
|
||||
"openrouter/z-ai/glm-5.3",
|
||||
"novita/zai-org/glm-5.2",
|
||||
"openai/glm-5.3",
|
||||
"openai/zai-org/glm-5.3",
|
||||
"hosted_vllm/glm-5.3",
|
||||
"openai/claude-opus-4-8",
|
||||
"openai/deepseek-v4-pro",
|
||||
"custom-ollama/gpt-5-mini-local",
|
||||
"custom-provider/claude-opus-4-local",
|
||||
"custom-provider/glm-5.3-local",
|
||||
],
|
||||
)
|
||||
def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
||||
@@ -108,13 +93,15 @@ def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
||||
"anthropic/claude-3-5-sonnet-latest",
|
||||
"ollama/llama3.1",
|
||||
"deepseek/deepseek-chat",
|
||||
"custom-ollama/gpt-5-mini-local",
|
||||
"custom-provider/claude-opus-4-local",
|
||||
"xai/grok-4.5",
|
||||
"openrouter/x-ai/grok-4",
|
||||
"mistral/mistral-medium-3-5",
|
||||
"mistral/magistral-medium-latest",
|
||||
"zai/glm-4.7",
|
||||
"openai/glm-4.7",
|
||||
"openrouter/z-ai/glm-5",
|
||||
"custom-provider/glm-5.3-local",
|
||||
],
|
||||
)
|
||||
def test_non_frontier_models_are_rejected(model_name: str) -> None:
|
||||
@@ -175,46 +162,3 @@ def test_routes_through_litellm_matches_the_provider(
|
||||
while isinstance(model, _NonStreamingModel | _TurnGuardModel):
|
||||
model = model._inner
|
||||
assert isinstance(model, LitellmModel) is litellm
|
||||
|
||||
|
||||
def test_api_type_override_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "gpt-4")
|
||||
monkeypatch.setenv("STRIX_API_TYPE", "chat_completions")
|
||||
assert uses_chat_completions_tool_schema("gpt-4", Settings()) is True
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-4")
|
||||
monkeypatch.setenv("STRIX_API_TYPE", "responses")
|
||||
assert uses_chat_completions_tool_schema("openai/gpt-4", Settings()) is False
|
||||
monkeypatch.setenv("STRIX_LLM", "anthropic/claude-sonnet-4-5")
|
||||
assert uses_chat_completions_tool_schema("anthropic/claude-sonnet-4-5", Settings()) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_type", "expected"),
|
||||
[
|
||||
(None, OpenAIChatCompletionsModel),
|
||||
("chat_completions", OpenAIChatCompletionsModel),
|
||||
("responses", OpenAIResponsesModel),
|
||||
],
|
||||
)
|
||||
def test_api_type_overrides_the_api_base_route(
|
||||
monkeypatch: pytest.MonkeyPatch, api_type: str | None, expected: type
|
||||
) -> None:
|
||||
"""``LLM_API_BASE`` defaults to chat completions. ``STRIX_API_TYPE`` must win."""
|
||||
monkeypatch.setattr(_openai_shared, "_use_responses_by_default", True)
|
||||
monkeypatch.setattr(_openai_shared, "_default_openai_client", None)
|
||||
monkeypatch.setattr(_openai_shared, "_default_openai_key", None)
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.setattr(litellm, "api_base", None)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||
monkeypatch.setenv("OPENAI_BASE_URL", "")
|
||||
monkeypatch.setenv("STRIX_LLM", "gpt-5")
|
||||
monkeypatch.setenv("LLM_API_KEY", "test-key")
|
||||
monkeypatch.setenv("LLM_API_BASE", "https://gateway.example/v1")
|
||||
monkeypatch.delenv("STRIX_API_TYPE", raising=False)
|
||||
if api_type is not None:
|
||||
monkeypatch.setenv("STRIX_API_TYPE", api_type)
|
||||
configure_sdk_model_defaults(Settings())
|
||||
model = StrixProvider().get_model("gpt-5")
|
||||
while isinstance(model, _NonStreamingModel | _TurnGuardModel):
|
||||
model = model._inner
|
||||
assert isinstance(model, expected)
|
||||
|
||||
@@ -9,7 +9,6 @@ call at a time against the shared client.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import pytest
|
||||
@@ -228,27 +227,3 @@ async def test_ctx_client_degrades_when_bootstrap_failed() -> None:
|
||||
|
||||
handle = CaidoBootstrapHandle(asyncio.ensure_future(_bootstrap()))
|
||||
assert await tools._ctx_client(cast("Any", _Ctx({"caido_client": handle}))) is None
|
||||
|
||||
|
||||
async def test_existing_request_ids_queries_current_project(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
client = _FakeClient("host")
|
||||
looked_up: list[str] = []
|
||||
|
||||
async def get_request_with_client(passed_client: Any, request_id: str) -> Any:
|
||||
assert passed_client is client
|
||||
looked_up.append(request_id)
|
||||
if request_id == "1042":
|
||||
return SimpleNamespace(request=SimpleNamespace(id="1042"))
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(caido_api, "get_request_with_client", get_request_with_client)
|
||||
|
||||
existing = await tools.existing_request_ids(
|
||||
cast("Any", _Ctx({"caido_client": client})),
|
||||
["1042", "1088"],
|
||||
)
|
||||
|
||||
assert existing == {"1042"}
|
||||
assert looked_up == ["1042", "1088"]
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from agents.tool_context import ToolContext
|
||||
|
||||
from strix.report.dedupe import (
|
||||
_check_dependency_duplicate,
|
||||
@@ -15,13 +13,10 @@ from strix.report.dedupe import (
|
||||
)
|
||||
from strix.report.state import ReportState, set_global_report_state
|
||||
from strix.tools.finish.tool import finish_scan
|
||||
from strix.tools.reporting import tool as reporting_tool
|
||||
from strix.tools.reporting.tool import (
|
||||
_do_create,
|
||||
_do_create_dependency,
|
||||
_do_update,
|
||||
_normalize_http_exchange_ids,
|
||||
_verify_http_exchange_ids,
|
||||
create_dependency_report,
|
||||
create_vulnerability_report,
|
||||
update_vulnerability_report,
|
||||
@@ -120,7 +115,6 @@ async def test_create_report_persists_new_fields(report_state: ReportState) -> N
|
||||
cve=None,
|
||||
cwe="CWE-79",
|
||||
code_locations=None,
|
||||
http_exchange_ids=["1042", "1042", "1088"],
|
||||
fix_pr_body="## Fix\nEncode output.",
|
||||
)
|
||||
assert result["success"] is True
|
||||
@@ -133,51 +127,6 @@ async def test_create_report_persists_new_fields(report_state: ReportState) -> N
|
||||
assert report["counterevidence"] == "No output encoding or CSP observed on this response."
|
||||
assert report["confidence"] == "high"
|
||||
assert report["severity_change_conditions"] == "A strict CSP would lower the severity."
|
||||
assert report["http_exchange_ids"] == ["1042", "1088"]
|
||||
|
||||
|
||||
def test_create_report_does_not_commit_when_callback_fails(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
def fail_persistence(_report: dict[str, Any]) -> None:
|
||||
raise RuntimeError("persistence failed")
|
||||
|
||||
report_state.vulnerability_found_callback = fail_persistence
|
||||
|
||||
with pytest.raises(RuntimeError, match="persistence failed"):
|
||||
report_state.add_vulnerability_report(
|
||||
title="Unstored finding",
|
||||
severity="high",
|
||||
http_exchange_ids=["1042"],
|
||||
)
|
||||
|
||||
assert report_state.vulnerability_reports == []
|
||||
|
||||
|
||||
def test_failed_revision_keeps_old_evidence_and_can_be_retried(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
report_id = report_state.add_vulnerability_report(
|
||||
title="Original finding", severity="high", http_exchange_ids=["1042"]
|
||||
)
|
||||
original = dict(report_state.vulnerability_reports[0])
|
||||
|
||||
def fail_persistence(revised: dict[str, Any]) -> None:
|
||||
assert revised["http_exchange_ids"] == ["1088"]
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
raise RuntimeError("persistence failed")
|
||||
|
||||
report_state.vulnerability_updated_callback = fail_persistence
|
||||
changes = {"title": "Revised finding", "http_exchange_ids": ["1088"]}
|
||||
with pytest.raises(RuntimeError, match="persistence failed"):
|
||||
report_state.update_vulnerability_report(report_id, changes)
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
|
||||
report_state.vulnerability_updated_callback = None
|
||||
revised = report_state.update_vulnerability_report(report_id, changes)
|
||||
assert revised is not None
|
||||
assert revised["http_exchange_ids"] == ["1088"]
|
||||
assert len(revised["update_history"]) == 1
|
||||
|
||||
|
||||
async def test_create_report_requires_evidence_and_assumptions(
|
||||
@@ -1091,13 +1040,7 @@ def test_tool_descriptions_include_formatting_guidance() -> None:
|
||||
|
||||
def test_vuln_tool_exposes_new_params() -> None:
|
||||
props = create_vulnerability_report.params_json_schema["properties"]
|
||||
for field in (
|
||||
"evidence",
|
||||
"assumptions",
|
||||
"fix_effort",
|
||||
"fix_pr_body",
|
||||
"http_exchange_ids",
|
||||
):
|
||||
for field in ("evidence", "assumptions", "fix_effort", "fix_pr_body"):
|
||||
assert field in props
|
||||
|
||||
dep_props = create_dependency_report.params_json_schema["properties"]
|
||||
@@ -1412,158 +1355,6 @@ def test_update_vulnerability_report_records_chained_impact(report_state: Report
|
||||
assert report_state.update_vulnerability_report("vuln-0404", {"severity": "high"}) is None
|
||||
|
||||
|
||||
def test_update_replaces_http_exchange_ids(report_state: ReportState) -> None:
|
||||
_seed_weak_report(report_state)
|
||||
|
||||
result = _do_update(
|
||||
report_id="vuln-0009",
|
||||
update_reason="A replay produced a clearer proving exchange.",
|
||||
fields={"http_exchange_ids": ["204", "204", "205"]},
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert report_state.vulnerability_reports[0]["http_exchange_ids"] == ["204", "205"]
|
||||
|
||||
|
||||
async def test_create_rejects_invalid_http_exchange_ids(report_state: ReportState) -> None:
|
||||
result = await _do_create(
|
||||
**_CONFIRMED_KWARGS,
|
||||
http_exchange_ids=["ok", "contains space"],
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert any("visible ASCII" in error for error in result["errors"])
|
||||
assert report_state.vulnerability_reports == []
|
||||
|
||||
|
||||
def test_http_exchange_id_limit_applies_after_deduplication() -> None:
|
||||
request_ids, errors = _normalize_http_exchange_ids(["1042"] * 11)
|
||||
|
||||
assert errors == []
|
||||
assert request_ids == ["1042"]
|
||||
|
||||
|
||||
async def test_http_exchange_ids_must_exist_in_current_proxy_project(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def existing_request_ids(
|
||||
_ctx: Any,
|
||||
_request_ids: list[str],
|
||||
) -> set[str]:
|
||||
return {"1042"}
|
||||
|
||||
monkeypatch.setattr(reporting_tool, "existing_request_ids", existing_request_ids)
|
||||
|
||||
request_ids, errors, warning = await _verify_http_exchange_ids(
|
||||
cast("Any", object()),
|
||||
["1042", "1088"],
|
||||
)
|
||||
|
||||
assert request_ids is None
|
||||
assert errors == ["http_exchange_ids do not exist in the current proxy project: 1088"]
|
||||
assert warning is None
|
||||
|
||||
|
||||
async def test_http_exchange_ids_are_dropped_when_proxy_cannot_be_queried(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def existing_request_ids(
|
||||
_ctx: Any,
|
||||
_request_ids: list[str],
|
||||
) -> set[str]:
|
||||
raise RuntimeError("Caido client is not available")
|
||||
|
||||
monkeypatch.setattr(reporting_tool, "existing_request_ids", existing_request_ids)
|
||||
|
||||
request_ids, errors, warning = await _verify_http_exchange_ids(
|
||||
cast("Any", object()),
|
||||
["1042", "1042", "1088"],
|
||||
)
|
||||
|
||||
assert request_ids is None
|
||||
assert errors == []
|
||||
assert warning is not None
|
||||
assert "not stored" in warning
|
||||
assert "update_vulnerability_report" in warning
|
||||
|
||||
|
||||
async def test_create_reports_persistence_failure_as_tool_error(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
def fail_persistence(_report: dict[str, Any]) -> None:
|
||||
raise RuntimeError("persistence failed")
|
||||
|
||||
report_state.vulnerability_found_callback = fail_persistence
|
||||
|
||||
result = await _do_create(**_CONFIRMED_KWARGS, http_exchange_ids=["1042"])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "persistence failed" in result["error"]
|
||||
assert "file it again" in result["error"]
|
||||
assert report_state.vulnerability_reports == []
|
||||
|
||||
|
||||
async def test_evidence_only_update_reports_proxy_outage_as_retryable(
|
||||
report_state: ReportState,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_seed_weak_report(report_state)
|
||||
original = dict(report_state.vulnerability_reports[0])
|
||||
|
||||
async def existing_request_ids(
|
||||
_ctx: Any,
|
||||
_request_ids: list[str],
|
||||
) -> set[str]:
|
||||
raise RuntimeError("Caido client is not available")
|
||||
|
||||
monkeypatch.setattr(reporting_tool, "existing_request_ids", existing_request_ids)
|
||||
|
||||
ctx = ToolContext(
|
||||
context={"agent_id": "root"},
|
||||
tool_name="update_vulnerability_report",
|
||||
tool_call_id="call-1",
|
||||
tool_arguments="{}",
|
||||
)
|
||||
raw = await update_vulnerability_report.on_invoke_tool(
|
||||
ctx,
|
||||
json.dumps(
|
||||
{
|
||||
"report_id": "vuln-0009",
|
||||
"update_reason": "A replay produced a clearer proving exchange.",
|
||||
"http_exchange_ids": ["204"],
|
||||
}
|
||||
),
|
||||
)
|
||||
result = json.loads(raw)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "No fields to update" not in result["error"]
|
||||
assert "update_vulnerability_report" in result["error"]
|
||||
assert result["report_id"] == "vuln-0009"
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
|
||||
|
||||
def test_update_reports_persistence_failure_as_tool_error(report_state: ReportState) -> None:
|
||||
_seed_weak_report(report_state)
|
||||
original = dict(report_state.vulnerability_reports[0])
|
||||
|
||||
def fail_persistence(_report: dict[str, Any]) -> None:
|
||||
raise RuntimeError("persistence failed")
|
||||
|
||||
report_state.vulnerability_updated_callback = fail_persistence
|
||||
|
||||
result = _do_update(
|
||||
report_id="vuln-0009",
|
||||
update_reason="A replay produced a clearer proving exchange.",
|
||||
fields={"http_exchange_ids": ["204"]},
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "persistence failed" in result["error"]
|
||||
assert result["report_id"] == "vuln-0009"
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
|
||||
|
||||
def test_update_vulnerability_report_ignores_identical_content(report_state: ReportState) -> None:
|
||||
_seed_weak_report(report_state)
|
||||
assert report_state.update_vulnerability_report("vuln-0009", {"severity": "medium"}) is None
|
||||
|
||||
@@ -110,7 +110,7 @@ def test_exa_content_requests_summaries_and_renders_results(
|
||||
|
||||
assert captured["url"] == "https://api.exa.ai/search"
|
||||
assert captured["headers"]["x-api-key"] == "ek"
|
||||
assert captured["json"]["query"] == "OpenSSH 7.4 RCE?"
|
||||
assert "OpenSSH 7.4 RCE?" in captured["json"]["query"]
|
||||
assert captured["json"]["type"] == "auto"
|
||||
assert captured["json"]["numResults"] == 5
|
||||
assert captured["json"]["contents"] == {"summary": {"query": tool._EXA_SUMMARY_PROMPT}}
|
||||
|
||||
Reference in New Issue
Block a user