resolve: merge conflict resolution, llm api base resolution
This commit is contained in:
@@ -5,6 +5,9 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
STRIX_API_BASE = "https://models.strix.ai/api/v1"
|
||||
|
||||
|
||||
class Config:
|
||||
"""Configuration Manager for Strix."""
|
||||
|
||||
@@ -177,3 +180,31 @@ def apply_saved_config(force: bool = False) -> dict[str, str]:
|
||||
|
||||
def save_current_config() -> bool:
|
||||
return Config.save_current()
|
||||
|
||||
|
||||
def resolve_llm_config() -> tuple[str | None, str | None, str | None]:
|
||||
"""Resolve LLM model, api_key, and api_base based on STRIX_LLM prefix.
|
||||
|
||||
Returns:
|
||||
tuple: (model_name, api_key, api_base)
|
||||
- model_name: Original model name (strix/ prefix preserved for display)
|
||||
- api_key: LLM API key
|
||||
- api_base: API base URL (auto-set to STRIX_API_BASE for strix/ models)
|
||||
"""
|
||||
model = Config.get("strix_llm")
|
||||
if not model:
|
||||
return None, None, None
|
||||
|
||||
api_key = Config.get("llm_api_key")
|
||||
|
||||
if model.startswith("strix/"):
|
||||
api_base: str | None = STRIX_API_BASE
|
||||
else:
|
||||
api_base = (
|
||||
Config.get("llm_api_base")
|
||||
or Config.get("openai_api_base")
|
||||
or Config.get("litellm_base_url")
|
||||
or Config.get("ollama_api_base")
|
||||
)
|
||||
|
||||
return model, api_key, api_base
|
||||
|
||||
@@ -18,7 +18,8 @@ from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import Config, apply_saved_config, save_current_config
|
||||
from strix.llm.utils import get_litellm_model_name, get_strix_api_base
|
||||
from strix.config.config import resolve_llm_config
|
||||
from strix.llm.utils import get_litellm_model_name
|
||||
|
||||
|
||||
apply_saved_config()
|
||||
@@ -52,10 +53,13 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
||||
missing_required_vars = []
|
||||
missing_optional_vars = []
|
||||
|
||||
if not Config.get("strix_llm"):
|
||||
strix_llm = Config.get("strix_llm")
|
||||
uses_strix_models = strix_llm and strix_llm.startswith("strix/")
|
||||
|
||||
if not strix_llm:
|
||||
missing_required_vars.append("STRIX_LLM")
|
||||
|
||||
has_base_url = any(
|
||||
has_base_url = uses_strix_models or any(
|
||||
[
|
||||
Config.get("llm_api_base"),
|
||||
Config.get("openai_api_base"),
|
||||
@@ -97,7 +101,7 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("STRIX_LLM", style="bold cyan")
|
||||
error_text.append(
|
||||
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
||||
" - Model name to use with litellm (e.g., 'anthropic/claude-sonnet-4-6')\n",
|
||||
style="white",
|
||||
)
|
||||
|
||||
@@ -136,7 +140,10 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
||||
)
|
||||
|
||||
error_text.append("\nExample setup:\n", style="white")
|
||||
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
||||
if uses_strix_models:
|
||||
error_text.append("export STRIX_LLM='strix/claude-sonnet-4.6'\n", style="dim white")
|
||||
else:
|
||||
error_text.append("export STRIX_LLM='anthropic/claude-sonnet-4-6'\n", style="dim white")
|
||||
|
||||
if missing_optional_vars:
|
||||
for var in missing_optional_vars:
|
||||
@@ -202,15 +209,7 @@ async def warm_up_llm() -> None:
|
||||
console = Console()
|
||||
|
||||
try:
|
||||
model_name = Config.get("strix_llm")
|
||||
api_key = Config.get("llm_api_key")
|
||||
api_base = (
|
||||
Config.get("llm_api_base")
|
||||
or Config.get("openai_api_base")
|
||||
or Config.get("litellm_base_url")
|
||||
or Config.get("ollama_api_base")
|
||||
or get_strix_api_base(model_name)
|
||||
)
|
||||
model_name, api_key, api_base = resolve_llm_config()
|
||||
|
||||
test_messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
|
||||
@@ -6,6 +6,11 @@ from pygments.styles import get_style_by_name
|
||||
from rich.text import Text
|
||||
from textual.widgets import Static
|
||||
|
||||
from strix.tools.reporting.reporting_actions import (
|
||||
parse_code_locations_xml,
|
||||
parse_cvss_xml,
|
||||
)
|
||||
|
||||
from .base_renderer import BaseToolRenderer
|
||||
from .registry import register_tool_renderer
|
||||
|
||||
@@ -17,6 +22,13 @@ def _get_style_colors() -> dict[Any, str]:
|
||||
|
||||
|
||||
FIELD_STYLE = "bold #4ade80"
|
||||
DIM_STYLE = "dim"
|
||||
FILE_STYLE = "bold #60a5fa"
|
||||
LINE_STYLE = "#facc15"
|
||||
LABEL_STYLE = "italic #a1a1aa"
|
||||
CODE_STYLE = "#e2e8f0"
|
||||
BEFORE_STYLE = "#ef4444"
|
||||
AFTER_STYLE = "#22c55e"
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
@@ -80,18 +92,13 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
||||
poc_script_code = args.get("poc_script_code", "")
|
||||
remediation_steps = args.get("remediation_steps", "")
|
||||
|
||||
attack_vector = args.get("attack_vector", "")
|
||||
attack_complexity = args.get("attack_complexity", "")
|
||||
privileges_required = args.get("privileges_required", "")
|
||||
user_interaction = args.get("user_interaction", "")
|
||||
scope = args.get("scope", "")
|
||||
confidentiality = args.get("confidentiality", "")
|
||||
integrity = args.get("integrity", "")
|
||||
availability = args.get("availability", "")
|
||||
cvss_breakdown_xml = args.get("cvss_breakdown", "")
|
||||
code_locations_xml = args.get("code_locations", "")
|
||||
|
||||
endpoint = args.get("endpoint", "")
|
||||
method = args.get("method", "")
|
||||
cve = args.get("cve", "")
|
||||
cwe = args.get("cwe", "")
|
||||
|
||||
severity = ""
|
||||
cvss_score = None
|
||||
@@ -140,38 +147,30 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
||||
text.append("CVE: ", style=FIELD_STYLE)
|
||||
text.append(cve)
|
||||
|
||||
if any(
|
||||
[
|
||||
attack_vector,
|
||||
attack_complexity,
|
||||
privileges_required,
|
||||
user_interaction,
|
||||
scope,
|
||||
confidentiality,
|
||||
integrity,
|
||||
availability,
|
||||
]
|
||||
):
|
||||
if cwe:
|
||||
text.append("\n\n")
|
||||
text.append("CWE: ", style=FIELD_STYLE)
|
||||
text.append(cwe)
|
||||
|
||||
parsed_cvss = parse_cvss_xml(cvss_breakdown_xml) if cvss_breakdown_xml else None
|
||||
if parsed_cvss:
|
||||
text.append("\n\n")
|
||||
cvss_parts = []
|
||||
if attack_vector:
|
||||
cvss_parts.append(f"AV:{attack_vector}")
|
||||
if attack_complexity:
|
||||
cvss_parts.append(f"AC:{attack_complexity}")
|
||||
if privileges_required:
|
||||
cvss_parts.append(f"PR:{privileges_required}")
|
||||
if user_interaction:
|
||||
cvss_parts.append(f"UI:{user_interaction}")
|
||||
if scope:
|
||||
cvss_parts.append(f"S:{scope}")
|
||||
if confidentiality:
|
||||
cvss_parts.append(f"C:{confidentiality}")
|
||||
if integrity:
|
||||
cvss_parts.append(f"I:{integrity}")
|
||||
if availability:
|
||||
cvss_parts.append(f"A:{availability}")
|
||||
for key, prefix in [
|
||||
("attack_vector", "AV"),
|
||||
("attack_complexity", "AC"),
|
||||
("privileges_required", "PR"),
|
||||
("user_interaction", "UI"),
|
||||
("scope", "S"),
|
||||
("confidentiality", "C"),
|
||||
("integrity", "I"),
|
||||
("availability", "A"),
|
||||
]:
|
||||
val = parsed_cvss.get(key)
|
||||
if val:
|
||||
cvss_parts.append(f"{prefix}:{val}")
|
||||
text.append("CVSS Vector: ", style=FIELD_STYLE)
|
||||
text.append("/".join(cvss_parts), style="dim")
|
||||
text.append("/".join(cvss_parts), style=DIM_STYLE)
|
||||
|
||||
if description:
|
||||
text.append("\n\n")
|
||||
@@ -191,6 +190,40 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
||||
text.append("\n")
|
||||
text.append(technical_analysis)
|
||||
|
||||
parsed_locations = (
|
||||
parse_code_locations_xml(code_locations_xml) if code_locations_xml else None
|
||||
)
|
||||
if parsed_locations:
|
||||
text.append("\n\n")
|
||||
text.append("Code Locations", style=FIELD_STYLE)
|
||||
for i, loc in enumerate(parsed_locations):
|
||||
text.append("\n\n")
|
||||
text.append(f" Location {i + 1}: ", style=DIM_STYLE)
|
||||
text.append(loc.get("file", "unknown"), style=FILE_STYLE)
|
||||
start = loc.get("start_line")
|
||||
end = loc.get("end_line")
|
||||
if start is not None:
|
||||
if end and end != start:
|
||||
text.append(f":{start}-{end}", style=LINE_STYLE)
|
||||
else:
|
||||
text.append(f":{start}", style=LINE_STYLE)
|
||||
if loc.get("label"):
|
||||
text.append(f"\n {loc['label']}", style=LABEL_STYLE)
|
||||
if loc.get("snippet"):
|
||||
text.append("\n ")
|
||||
text.append(loc["snippet"], style=CODE_STYLE)
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
text.append("\n ")
|
||||
text.append("Fix:", style=DIM_STYLE)
|
||||
if loc.get("fix_before"):
|
||||
text.append("\n ")
|
||||
text.append("- ", style=BEFORE_STYLE)
|
||||
text.append(loc["fix_before"], style=BEFORE_STYLE)
|
||||
if loc.get("fix_after"):
|
||||
text.append("\n ")
|
||||
text.append("+ ", style=AFTER_STYLE)
|
||||
text.append(loc["fix_after"], style=AFTER_STYLE)
|
||||
|
||||
if poc_description:
|
||||
text.append("\n\n")
|
||||
text.append("PoC Description", style=FIELD_STYLE)
|
||||
|
||||
@@ -531,16 +531,30 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
lines.append("```")
|
||||
|
||||
# Code Analysis
|
||||
if vuln.get("code_file") or vuln.get("code_diff"):
|
||||
if vuln.get("code_locations"):
|
||||
lines.extend(["", "## Code Analysis", ""])
|
||||
if vuln.get("code_file"):
|
||||
lines.append(f"**File:** {vuln['code_file']}")
|
||||
for i, loc in enumerate(vuln["code_locations"]):
|
||||
file_ref = loc.get("file", "unknown")
|
||||
line_ref = ""
|
||||
if loc.get("start_line") is not None:
|
||||
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
|
||||
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
|
||||
else:
|
||||
line_ref = f" (line {loc['start_line']})"
|
||||
lines.append(f"**Location {i + 1}:** `{file_ref}`{line_ref}")
|
||||
if loc.get("label"):
|
||||
lines.append(f" {loc['label']}")
|
||||
if loc.get("snippet"):
|
||||
lines.append(f"```\n{loc['snippet']}\n```")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
lines.append("**Suggested Fix:**")
|
||||
lines.append("```diff")
|
||||
if loc.get("fix_before"):
|
||||
lines.extend(f"- {line}" for line in loc["fix_before"].splitlines())
|
||||
if loc.get("fix_after"):
|
||||
lines.extend(f"+ {line}" for line in loc["fix_after"].splitlines())
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
if vuln.get("code_diff"):
|
||||
lines.append("**Changes:**")
|
||||
lines.append("```diff")
|
||||
lines.append(vuln["code_diff"])
|
||||
lines.append("```")
|
||||
|
||||
# Remediation
|
||||
if vuln.get("remediation_steps"):
|
||||
|
||||
@@ -163,32 +163,34 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
|
||||
text.append("\n")
|
||||
text.append(poc_script_code, style="dim")
|
||||
|
||||
code_file = report.get("code_file")
|
||||
if code_file:
|
||||
code_locations = report.get("code_locations")
|
||||
if code_locations:
|
||||
text.append("\n\n")
|
||||
text.append("Code File: ", style=field_style)
|
||||
text.append(code_file)
|
||||
|
||||
code_before = report.get("code_before")
|
||||
if code_before:
|
||||
text.append("\n\n")
|
||||
text.append("Code Before", style=field_style)
|
||||
text.append("\n")
|
||||
text.append(code_before, style="dim")
|
||||
|
||||
code_after = report.get("code_after")
|
||||
if code_after:
|
||||
text.append("\n\n")
|
||||
text.append("Code After", style=field_style)
|
||||
text.append("\n")
|
||||
text.append(code_after, style="dim")
|
||||
|
||||
code_diff = report.get("code_diff")
|
||||
if code_diff:
|
||||
text.append("\n\n")
|
||||
text.append("Code Diff", style=field_style)
|
||||
text.append("\n")
|
||||
text.append(code_diff, style="dim")
|
||||
text.append("Code Locations", style=field_style)
|
||||
for i, loc in enumerate(code_locations):
|
||||
text.append("\n\n")
|
||||
text.append(f" Location {i + 1}: ", style="dim")
|
||||
text.append(loc.get("file", "unknown"), style="bold")
|
||||
start = loc.get("start_line")
|
||||
end = loc.get("end_line")
|
||||
if start is not None:
|
||||
if end and end != start:
|
||||
text.append(f":{start}-{end}")
|
||||
else:
|
||||
text.append(f":{start}")
|
||||
if loc.get("label"):
|
||||
text.append(f"\n {loc['label']}", style="italic dim")
|
||||
if loc.get("snippet"):
|
||||
text.append("\n ")
|
||||
text.append(loc["snippet"], style="dim")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
text.append("\n Fix:")
|
||||
if loc.get("fix_before"):
|
||||
text.append("\n - ", style="dim")
|
||||
text.append(loc["fix_before"], style="dim")
|
||||
if loc.get("fix_after"):
|
||||
text.append("\n + ", style="dim")
|
||||
text.append(loc["fix_after"], style="dim")
|
||||
|
||||
remediation_steps = report.get("remediation_steps")
|
||||
if remediation_steps:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from strix.config import Config
|
||||
from strix.config.config import resolve_llm_config
|
||||
|
||||
|
||||
class LLMConfig:
|
||||
@@ -10,7 +11,8 @@ class LLMConfig:
|
||||
timeout: int | None = None,
|
||||
scan_mode: str = "deep",
|
||||
):
|
||||
self.model_name = model_name or Config.get("strix_llm")
|
||||
resolved_model, self.api_key, self.api_base = resolve_llm_config()
|
||||
self.model_name = model_name or resolved_model
|
||||
|
||||
if not self.model_name:
|
||||
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
||||
|
||||
@@ -5,8 +5,8 @@ from typing import Any
|
||||
|
||||
import litellm
|
||||
|
||||
from strix.config import Config
|
||||
from strix.llm.utils import get_litellm_model_name, get_strix_api_base
|
||||
from strix.config.config import resolve_llm_config
|
||||
from strix.llm.utils import get_litellm_model_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -156,15 +156,7 @@ def check_duplicate(
|
||||
|
||||
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
||||
|
||||
model_name = Config.get("strix_llm")
|
||||
api_key = Config.get("llm_api_key")
|
||||
api_base = (
|
||||
Config.get("llm_api_base")
|
||||
or Config.get("openai_api_base")
|
||||
or Config.get("litellm_base_url")
|
||||
or Config.get("ollama_api_base")
|
||||
or get_strix_api_base(model_name)
|
||||
)
|
||||
model_name, api_key, api_base = resolve_llm_config()
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
||||
|
||||
@@ -15,7 +15,6 @@ from strix.llm.utils import (
|
||||
_truncate_to_first_function,
|
||||
fix_incomplete_tool_call,
|
||||
get_litellm_model_name,
|
||||
get_strix_api_base,
|
||||
parse_tool_invocations,
|
||||
)
|
||||
from strix.skills import load_skills
|
||||
@@ -206,18 +205,10 @@ class LLM:
|
||||
"stream_options": {"include_usage": True},
|
||||
}
|
||||
|
||||
if api_key := Config.get("llm_api_key"):
|
||||
args["api_key"] = api_key
|
||||
|
||||
api_base = (
|
||||
Config.get("llm_api_base")
|
||||
or Config.get("openai_api_base")
|
||||
or Config.get("litellm_base_url")
|
||||
or Config.get("ollama_api_base")
|
||||
or get_strix_api_base(self.config.model_name)
|
||||
)
|
||||
if api_base:
|
||||
args["api_base"] = api_base
|
||||
if self.config.api_key:
|
||||
args["api_key"] = self.config.api_key
|
||||
if self.config.api_base:
|
||||
args["api_base"] = self.config.api_base
|
||||
if self._supports_reasoning():
|
||||
args["reasoning_effort"] = self._reasoning_effort
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import Any
|
||||
|
||||
import litellm
|
||||
|
||||
from strix.config import Config
|
||||
from strix.llm.utils import get_litellm_model_name, get_strix_api_base
|
||||
from strix.config.config import Config, resolve_llm_config
|
||||
from strix.llm.utils import get_litellm_model_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -106,14 +106,7 @@ def _summarize_messages(
|
||||
conversation = "\n".join(formatted)
|
||||
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
||||
|
||||
api_key = Config.get("llm_api_key")
|
||||
api_base = (
|
||||
Config.get("llm_api_base")
|
||||
or Config.get("openai_api_base")
|
||||
or Config.get("litellm_base_url")
|
||||
or Config.get("ollama_api_base")
|
||||
or get_strix_api_base(model)
|
||||
)
|
||||
_, api_key, api_base = resolve_llm_config()
|
||||
|
||||
try:
|
||||
litellm_model = get_litellm_model_name(model) or model
|
||||
|
||||
@@ -3,8 +3,6 @@ import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
STRIX_API_BASE = "https://models.strix.ai/api/v1"
|
||||
|
||||
STRIX_PROVIDER_PREFIXES: dict[str, str] = {
|
||||
"claude-": "anthropic",
|
||||
"gpt-": "openai",
|
||||
@@ -12,18 +10,6 @@ STRIX_PROVIDER_PREFIXES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def is_strix_model(model_name: str | None) -> bool:
|
||||
"""Check if model uses strix/ prefix."""
|
||||
return bool(model_name and model_name.startswith("strix/"))
|
||||
|
||||
|
||||
def get_strix_api_base(model_name: str | None) -> str | None:
|
||||
"""Return Strix API base URL if using strix/ model, None otherwise."""
|
||||
if is_strix_model(model_name):
|
||||
return STRIX_API_BASE
|
||||
return None
|
||||
|
||||
|
||||
def get_litellm_model_name(model_name: str | None) -> str | None:
|
||||
"""Convert strix/ prefixed model to litellm-compatible provider/model format.
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ def load_skills(skill_names: list[str]) -> dict[str, str]:
|
||||
if skill_path and (skills_dir / skill_path).exists():
|
||||
full_path = skills_dir / skill_path
|
||||
var_name = skill_name.split("/")[-1]
|
||||
content = full_path.read_text()
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
content = _FRONTMATTER_PATTERN.sub("", content).lstrip()
|
||||
skill_content[var_name] = content
|
||||
logger.info(f"Loaded skill: {skill_name} -> {var_name}")
|
||||
|
||||
@@ -89,10 +89,8 @@ class Tracer:
|
||||
endpoint: str | None = None,
|
||||
method: str | None = None,
|
||||
cve: str | None = None,
|
||||
code_file: str | None = None,
|
||||
code_before: str | None = None,
|
||||
code_after: str | None = None,
|
||||
code_diff: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
|
||||
|
||||
@@ -127,14 +125,10 @@ class Tracer:
|
||||
report["method"] = method.strip()
|
||||
if cve:
|
||||
report["cve"] = cve.strip()
|
||||
if code_file:
|
||||
report["code_file"] = code_file.strip()
|
||||
if code_before:
|
||||
report["code_before"] = code_before.strip()
|
||||
if code_after:
|
||||
report["code_after"] = code_after.strip()
|
||||
if code_diff:
|
||||
report["code_diff"] = code_diff.strip()
|
||||
if cwe:
|
||||
report["cwe"] = cwe.strip()
|
||||
if code_locations:
|
||||
report["code_locations"] = code_locations
|
||||
|
||||
self.vulnerability_reports.append(report)
|
||||
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
||||
@@ -323,6 +317,7 @@ class Tracer:
|
||||
("Endpoint", report.get("endpoint")),
|
||||
("Method", report.get("method")),
|
||||
("CVE", report.get("cve")),
|
||||
("CWE", report.get("cwe")),
|
||||
]
|
||||
cvss_score = report.get("cvss")
|
||||
if cvss_score is not None:
|
||||
@@ -353,15 +348,33 @@ class Tracer:
|
||||
f.write(f"{report['poc_script_code']}\n")
|
||||
f.write("```\n\n")
|
||||
|
||||
if report.get("code_file") or report.get("code_diff"):
|
||||
if report.get("code_locations"):
|
||||
f.write("## Code Analysis\n\n")
|
||||
if report.get("code_file"):
|
||||
f.write(f"**File:** {report['code_file']}\n\n")
|
||||
if report.get("code_diff"):
|
||||
f.write("**Changes:**\n")
|
||||
f.write("```diff\n")
|
||||
f.write(f"{report['code_diff']}\n")
|
||||
f.write("```\n\n")
|
||||
for i, loc in enumerate(report["code_locations"]):
|
||||
prefix = f"**Location {i + 1}:**"
|
||||
file_ref = loc.get("file", "unknown")
|
||||
line_ref = ""
|
||||
if loc.get("start_line") is not None:
|
||||
if loc.get("end_line") and loc["end_line"] != loc["start_line"]:
|
||||
line_ref = f" (lines {loc['start_line']}-{loc['end_line']})"
|
||||
else:
|
||||
line_ref = f" (line {loc['start_line']})"
|
||||
f.write(f"{prefix} `{file_ref}`{line_ref}\n")
|
||||
if loc.get("label"):
|
||||
f.write(f" {loc['label']}\n")
|
||||
if loc.get("snippet"):
|
||||
f.write(f" ```\n {loc['snippet']}\n ```\n")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
f.write("\n **Suggested Fix:**\n")
|
||||
f.write("```diff\n")
|
||||
if loc.get("fix_before"):
|
||||
for line in loc["fix_before"].splitlines():
|
||||
f.write(f"- {line}\n")
|
||||
if loc.get("fix_after"):
|
||||
for line in loc["fix_after"].splitlines():
|
||||
f.write(f"+ {line}\n")
|
||||
f.write("```\n")
|
||||
f.write("\n")
|
||||
|
||||
if report.get("remediation_steps"):
|
||||
f.write("## Remediation\n\n")
|
||||
|
||||
@@ -48,7 +48,7 @@ def _load_xml_schema(path: Path) -> Any:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
content = path.read_text()
|
||||
content = path.read_text(encoding="utf-8")
|
||||
|
||||
content = _process_dynamic_content(content)
|
||||
|
||||
|
||||
@@ -1,8 +1,120 @@
|
||||
import contextlib
|
||||
import re
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from strix.tools.registry import register_tool
|
||||
|
||||
|
||||
_CVSS_FIELDS = (
|
||||
"attack_vector",
|
||||
"attack_complexity",
|
||||
"privileges_required",
|
||||
"user_interaction",
|
||||
"scope",
|
||||
"confidentiality",
|
||||
"integrity",
|
||||
"availability",
|
||||
)
|
||||
|
||||
|
||||
def parse_cvss_xml(xml_str: str) -> dict[str, str] | None:
|
||||
if not xml_str or not xml_str.strip():
|
||||
return None
|
||||
result = {}
|
||||
for field in _CVSS_FIELDS:
|
||||
match = re.search(rf"<{field}>(.*?)</{field}>", xml_str, re.DOTALL)
|
||||
if match:
|
||||
result[field] = match.group(1).strip()
|
||||
return result if result else None
|
||||
|
||||
|
||||
def parse_code_locations_xml(xml_str: str) -> list[dict[str, Any]] | None:
|
||||
if not xml_str or not xml_str.strip():
|
||||
return None
|
||||
locations = []
|
||||
for loc_match in re.finditer(r"<location>(.*?)</location>", xml_str, re.DOTALL):
|
||||
loc: dict[str, Any] = {}
|
||||
loc_content = loc_match.group(1)
|
||||
for field in (
|
||||
"file",
|
||||
"start_line",
|
||||
"end_line",
|
||||
"snippet",
|
||||
"label",
|
||||
"fix_before",
|
||||
"fix_after",
|
||||
):
|
||||
field_match = re.search(rf"<{field}>(.*?)</{field}>", loc_content, re.DOTALL)
|
||||
if field_match:
|
||||
raw = field_match.group(1)
|
||||
value = (
|
||||
raw.strip("\n")
|
||||
if field in ("snippet", "fix_before", "fix_after")
|
||||
else raw.strip()
|
||||
)
|
||||
if field in ("start_line", "end_line"):
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
loc[field] = int(value)
|
||||
elif value:
|
||||
loc[field] = value
|
||||
if loc.get("file") and loc.get("start_line") is not None:
|
||||
locations.append(loc)
|
||||
return locations if locations else None
|
||||
|
||||
|
||||
def _validate_file_path(path: str) -> str | None:
|
||||
if not path or not path.strip():
|
||||
return "file path cannot be empty"
|
||||
p = PurePosixPath(path)
|
||||
if p.is_absolute():
|
||||
return f"file path must be relative, got absolute: '{path}'"
|
||||
if ".." in p.parts:
|
||||
return f"file path must not contain '..': '{path}'"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_code_locations(locations: list[dict[str, Any]]) -> list[str]:
|
||||
errors = []
|
||||
for i, loc in enumerate(locations):
|
||||
path_err = _validate_file_path(loc.get("file", ""))
|
||||
if path_err:
|
||||
errors.append(f"code_locations[{i}]: {path_err}")
|
||||
start = loc.get("start_line")
|
||||
if not isinstance(start, int) or start < 1:
|
||||
errors.append(f"code_locations[{i}]: start_line must be a positive integer")
|
||||
end = loc.get("end_line")
|
||||
if end is None:
|
||||
errors.append(f"code_locations[{i}]: end_line is required")
|
||||
elif not isinstance(end, int) or end < 1:
|
||||
errors.append(f"code_locations[{i}]: end_line must be a positive integer")
|
||||
elif isinstance(start, int) and end < start:
|
||||
errors.append(f"code_locations[{i}]: end_line ({end}) must be >= start_line ({start})")
|
||||
return errors
|
||||
|
||||
|
||||
def _extract_cve(cve: str) -> str:
|
||||
match = re.search(r"CVE-\d{4}-\d{4,}", cve)
|
||||
return match.group(0) if match else cve.strip()
|
||||
|
||||
|
||||
def _validate_cve(cve: str) -> str | None:
|
||||
if not re.match(r"^CVE-\d{4}-\d{4,}$", cve):
|
||||
return f"invalid CVE format: '{cve}' (expected 'CVE-YYYY-NNNNN')"
|
||||
return None
|
||||
|
||||
|
||||
def _extract_cwe(cwe: str) -> str:
|
||||
match = re.search(r"CWE-\d+", cwe)
|
||||
return match.group(0) if match else cwe.strip()
|
||||
|
||||
|
||||
def _validate_cwe(cwe: str) -> str | None:
|
||||
if not re.match(r"^CWE-\d+$", cwe):
|
||||
return f"invalid CWE format: '{cwe}' (expected 'CWE-NNN')"
|
||||
return None
|
||||
|
||||
|
||||
def calculate_cvss_and_severity(
|
||||
attack_vector: str,
|
||||
attack_complexity: str,
|
||||
@@ -87,7 +199,7 @@ def _validate_cvss_parameters(**kwargs: str) -> list[str]:
|
||||
|
||||
|
||||
@register_tool(sandbox_execution=False)
|
||||
def create_vulnerability_report(
|
||||
def create_vulnerability_report( # noqa: PLR0912
|
||||
title: str,
|
||||
description: str,
|
||||
impact: str,
|
||||
@@ -96,23 +208,12 @@ def create_vulnerability_report(
|
||||
poc_description: str,
|
||||
poc_script_code: str,
|
||||
remediation_steps: str,
|
||||
# CVSS Breakdown Components
|
||||
attack_vector: str,
|
||||
attack_complexity: str,
|
||||
privileges_required: str,
|
||||
user_interaction: str,
|
||||
scope: str,
|
||||
confidentiality: str,
|
||||
integrity: str,
|
||||
availability: str,
|
||||
# Optional fields
|
||||
cvss_breakdown: str,
|
||||
endpoint: str | None = None,
|
||||
method: str | None = None,
|
||||
cve: str | None = None,
|
||||
code_file: str | None = None,
|
||||
code_before: str | None = None,
|
||||
code_after: str | None = None,
|
||||
code_diff: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
validation_errors = _validate_required_fields(
|
||||
title=title,
|
||||
@@ -125,32 +226,32 @@ def create_vulnerability_report(
|
||||
remediation_steps=remediation_steps,
|
||||
)
|
||||
|
||||
validation_errors.extend(
|
||||
_validate_cvss_parameters(
|
||||
attack_vector=attack_vector,
|
||||
attack_complexity=attack_complexity,
|
||||
privileges_required=privileges_required,
|
||||
user_interaction=user_interaction,
|
||||
scope=scope,
|
||||
confidentiality=confidentiality,
|
||||
integrity=integrity,
|
||||
availability=availability,
|
||||
)
|
||||
)
|
||||
parsed_cvss = parse_cvss_xml(cvss_breakdown)
|
||||
if not parsed_cvss:
|
||||
validation_errors.append("cvss: could not parse CVSS breakdown XML")
|
||||
else:
|
||||
validation_errors.extend(_validate_cvss_parameters(**parsed_cvss))
|
||||
|
||||
parsed_locations = parse_code_locations_xml(code_locations) if code_locations else None
|
||||
|
||||
if parsed_locations:
|
||||
validation_errors.extend(_validate_code_locations(parsed_locations))
|
||||
if cve:
|
||||
cve = _extract_cve(cve)
|
||||
cve_err = _validate_cve(cve)
|
||||
if cve_err:
|
||||
validation_errors.append(cve_err)
|
||||
if cwe:
|
||||
cwe = _extract_cwe(cwe)
|
||||
cwe_err = _validate_cwe(cwe)
|
||||
if cwe_err:
|
||||
validation_errors.append(cwe_err)
|
||||
|
||||
if validation_errors:
|
||||
return {"success": False, "message": "Validation failed", "errors": validation_errors}
|
||||
|
||||
cvss_score, severity, cvss_vector = calculate_cvss_and_severity(
|
||||
attack_vector,
|
||||
attack_complexity,
|
||||
privileges_required,
|
||||
user_interaction,
|
||||
scope,
|
||||
confidentiality,
|
||||
integrity,
|
||||
availability,
|
||||
)
|
||||
assert parsed_cvss is not None
|
||||
cvss_score, severity, cvss_vector = calculate_cvss_and_severity(**parsed_cvss)
|
||||
|
||||
try:
|
||||
from strix.telemetry.tracer import get_global_tracer
|
||||
@@ -196,17 +297,6 @@ def create_vulnerability_report(
|
||||
"reason": dedupe_result.get("reason", ""),
|
||||
}
|
||||
|
||||
cvss_breakdown = {
|
||||
"attack_vector": attack_vector,
|
||||
"attack_complexity": attack_complexity,
|
||||
"privileges_required": privileges_required,
|
||||
"user_interaction": user_interaction,
|
||||
"scope": scope,
|
||||
"confidentiality": confidentiality,
|
||||
"integrity": integrity,
|
||||
"availability": availability,
|
||||
}
|
||||
|
||||
report_id = tracer.add_vulnerability_report(
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -218,14 +308,12 @@ def create_vulnerability_report(
|
||||
poc_script_code=poc_script_code,
|
||||
remediation_steps=remediation_steps,
|
||||
cvss=cvss_score,
|
||||
cvss_breakdown=cvss_breakdown,
|
||||
cvss_breakdown=parsed_cvss,
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
cve=cve,
|
||||
code_file=code_file,
|
||||
code_before=code_before,
|
||||
code_after=code_after,
|
||||
code_diff=code_diff,
|
||||
cwe=cwe,
|
||||
code_locations=parsed_locations,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,7 @@ DO NOT USE:
|
||||
- For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
|
||||
- To re-report a vulnerability that was already reported (even with different details)
|
||||
|
||||
White-box requirement (when you have access to the code): You MUST include code_file, code_before, code_after, and code_diff. These must contain the actual code (before/after) and a complete, apply-able unified diff.
|
||||
White-box requirement (when you have access to the code): You MUST include code_locations with nested XML, including fix_before/fix_after on locations where a fix is proposed.
|
||||
|
||||
DEDUPLICATION: If this tool returns with success=false and mentions a duplicate, DO NOT attempt to re-submit. The vulnerability has already been reported. Move on to testing other areas.
|
||||
|
||||
@@ -30,7 +30,7 @@ Professional, customer-facing report rules (PDF-ready):
|
||||
6) Impact
|
||||
7) Remediation
|
||||
8) Evidence (optional request/response excerpts, etc.) in the technical analysis field.
|
||||
- Numbered steps are allowed ONLY within the proof of concept. Elsewhere, use clear, concise paragraphs suitable for customer-facing reports.
|
||||
- Numbered steps are allowed ONLY within the proof of concept and remediation sections. Elsewhere, use clear, concise paragraphs suitable for customer-facing reports.
|
||||
- Language must be precise and non-vague; avoid hedging.
|
||||
</description>
|
||||
<parameters>
|
||||
@@ -58,51 +58,28 @@ Professional, customer-facing report rules (PDF-ready):
|
||||
<parameter name="remediation_steps" type="string" required="true">
|
||||
<description>Specific, actionable steps to fix the vulnerability</description>
|
||||
</parameter>
|
||||
<parameter name="attack_vector" type="string" required="true">
|
||||
<description>CVSS Attack Vector - How the vulnerability is exploited:
|
||||
N = Network (remotely exploitable)
|
||||
A = Adjacent (same network segment)
|
||||
L = Local (local access required)
|
||||
P = Physical (physical access required)</description>
|
||||
</parameter>
|
||||
<parameter name="attack_complexity" type="string" required="true">
|
||||
<description>CVSS Attack Complexity - Conditions beyond attacker's control:
|
||||
L = Low (no special conditions)
|
||||
H = High (special conditions must exist)</description>
|
||||
</parameter>
|
||||
<parameter name="privileges_required" type="string" required="true">
|
||||
<description>CVSS Privileges Required - Level of privileges needed:
|
||||
N = None (no privileges needed)
|
||||
L = Low (basic user privileges)
|
||||
H = High (admin privileges)</description>
|
||||
</parameter>
|
||||
<parameter name="user_interaction" type="string" required="true">
|
||||
<description>CVSS User Interaction - Does exploit require user action:
|
||||
N = None (no user interaction needed)
|
||||
R = Required (user must perform some action)</description>
|
||||
</parameter>
|
||||
<parameter name="scope" type="string" required="true">
|
||||
<description>CVSS Scope - Can the vulnerability affect resources beyond its security scope:
|
||||
U = Unchanged (only affects the vulnerable component)
|
||||
C = Changed (affects resources beyond vulnerable component)</description>
|
||||
</parameter>
|
||||
<parameter name="confidentiality" type="string" required="true">
|
||||
<description>CVSS Confidentiality Impact - Impact to confidentiality:
|
||||
N = None (no impact)
|
||||
L = Low (some information disclosure)
|
||||
H = High (all information disclosed)</description>
|
||||
</parameter>
|
||||
<parameter name="integrity" type="string" required="true">
|
||||
<description>CVSS Integrity Impact - Impact to integrity:
|
||||
N = None (no impact)
|
||||
L = Low (data can be modified but scope is limited)
|
||||
H = High (total loss of integrity)</description>
|
||||
</parameter>
|
||||
<parameter name="availability" type="string" required="true">
|
||||
<description>CVSS Availability Impact - Impact to availability:
|
||||
N = None (no impact)
|
||||
L = Low (reduced performance or interruptions)
|
||||
H = High (total loss of availability)</description>
|
||||
<parameter name="cvss_breakdown" type="string" required="true">
|
||||
<description>CVSS 3.1 base score breakdown as nested XML. All 8 metrics are required.
|
||||
|
||||
Each metric element contains a single uppercase letter value:
|
||||
- attack_vector: N (Network), A (Adjacent), L (Local), P (Physical)
|
||||
- attack_complexity: L (Low), H (High)
|
||||
- privileges_required: N (None), L (Low), H (High)
|
||||
- user_interaction: N (None), R (Required)
|
||||
- scope: U (Unchanged), C (Changed)
|
||||
- confidentiality: N (None), L (Low), H (High)
|
||||
- integrity: N (None), L (Low), H (High)
|
||||
- availability: N (None), L (Low), H (High)</description>
|
||||
<format>
|
||||
<attack_vector>N</attack_vector>
|
||||
<attack_complexity>L</attack_complexity>
|
||||
<privileges_required>N</privileges_required>
|
||||
<user_interaction>N</user_interaction>
|
||||
<scope>U</scope>
|
||||
<confidentiality>H</confidentiality>
|
||||
<integrity>H</integrity>
|
||||
<availability>N</availability>
|
||||
</format>
|
||||
</parameter>
|
||||
<parameter name="endpoint" type="string" required="false">
|
||||
<description>API endpoint(s) or URL path(s) (e.g., "/api/login") - for web vulnerabilities, or Git repository path(s) - for code vulnerabilities</description>
|
||||
@@ -111,19 +88,93 @@ H = High (total loss of availability)</description>
|
||||
<description>HTTP method(s) (GET, POST, etc.) - for web vulnerabilities.</description>
|
||||
</parameter>
|
||||
<parameter name="cve" type="string" required="false">
|
||||
<description>CVE identifier (e.g., "CVE-2024-1234"). Make sure it's a valid CVE. Use web search or vulnerability databases to make sure it's a valid CVE number.</description>
|
||||
<description>CVE identifier. ONLY the ID, e.g. "CVE-2024-1234" — do NOT include the name or description.
|
||||
You must be 100% certain of the exact CVE number. Do NOT guess, approximate, or hallucinate CVE IDs.
|
||||
If web_search is available, use it to verify the CVE exists and matches this vulnerability. If you cannot verify it, omit this field entirely.</description>
|
||||
</parameter>
|
||||
<parameter name="code_file" type="string" required="false">
|
||||
<description>MANDATORY for white-box testing: exact affected source file path(s).</description>
|
||||
<parameter name="cwe" type="string" required="false">
|
||||
<description>CWE identifier. ONLY the ID, e.g. "CWE-89" — do NOT include the name or parenthetical (wrong: "CWE-89 (SQL Injection)").
|
||||
|
||||
You must be 100% certain of the exact CWE number. Do NOT guess or approximate.
|
||||
If web_search is available and you are unsure, use it to look up the correct CWE. If you cannot be certain, omit this field entirely.
|
||||
Always prefer the most specific child CWE over a broad parent.
|
||||
For example, use CWE-89 instead of CWE-74, or CWE-78 instead of CWE-77.
|
||||
|
||||
Reference (ID only — names here are just for your reference, do NOT include them in the value):
|
||||
- Injection: CWE-79 XSS, CWE-89 SQLi, CWE-78 OS Command Injection, CWE-94 Code Injection, CWE-77 Command Injection
|
||||
- Auth/Access: CWE-287 Improper Authentication, CWE-862 Missing Authorization, CWE-863 Incorrect Authorization, CWE-306 Missing Authentication for Critical Function, CWE-639 Authorization Bypass Through User-Controlled Key
|
||||
- Web: CWE-352 CSRF, CWE-918 SSRF, CWE-601 Open Redirect, CWE-434 Unrestricted Upload of File with Dangerous Type
|
||||
- Memory: CWE-787 Out-of-bounds Write, CWE-125 Out-of-bounds Read, CWE-416 Use After Free, CWE-120 Classic Buffer Overflow
|
||||
- Data: CWE-502 Deserialization of Untrusted Data, CWE-22 Path Traversal, CWE-611 XXE
|
||||
- Crypto/Config: CWE-798 Use of Hard-coded Credentials, CWE-327 Use of Broken or Risky Cryptographic Algorithm, CWE-311 Missing Encryption of Sensitive Data, CWE-916 Password Hash With Insufficient Computational Effort
|
||||
|
||||
Do NOT use broad/parent CWEs like CWE-74, CWE-20, CWE-200, CWE-284, or CWE-693.</description>
|
||||
</parameter>
|
||||
<parameter name="code_before" type="string" required="false">
|
||||
<description>MANDATORY for white-box testing: actual vulnerable code snippet(s) copied verbatim from the repository.</description>
|
||||
</parameter>
|
||||
<parameter name="code_after" type="string" required="false">
|
||||
<description>MANDATORY for white-box testing: corrected code snippet(s) exactly as they should appear after the fix.</description>
|
||||
</parameter>
|
||||
<parameter name="code_diff" type="string" required="false">
|
||||
<description>MANDATORY for white-box testing: unified diff showing the code changes. Must be a complete, apply-able unified diff (git format) covering all affected files, with proper file headers, line numbers, and sufficient context.</description>
|
||||
<parameter name="code_locations" type="string" required="false">
|
||||
<description>Nested XML list of code locations where the vulnerability exists. MANDATORY for white-box testing.
|
||||
|
||||
CRITICAL — HOW fix_before/fix_after WORK:
|
||||
fix_before and fix_after are LITERAL BLOCK-LEVEL REPLACEMENTS used directly for GitHub/GitLab PR suggestion blocks. When a reviewer clicks "Accept suggestion", the platform replaces the EXACT lines from start_line to end_line with the fix_after content. This means:
|
||||
|
||||
1. fix_before MUST be an EXACT, VERBATIM copy of the source code at lines start_line through end_line. Same whitespace, same indentation, same line breaks. If fix_before does not match the actual file content character-for-character, the suggestion will be wrong or will corrupt the code when accepted.
|
||||
|
||||
2. fix_after is the COMPLETE replacement for that entire block. It replaces ALL lines from start_line to end_line. It can be more lines, fewer lines, or the same number of lines as fix_before.
|
||||
|
||||
3. start_line and end_line define the EXACT line range being replaced. They must precisely cover the lines in fix_before — no more, no less. If the vulnerable code spans lines 45-48, then start_line=45 and end_line=48, and fix_before must contain all 4 lines exactly as they appear in the file.
|
||||
|
||||
MULTI-PART FIXES:
|
||||
Many fixes require changes in multiple non-contiguous parts of a file (e.g., adding an import at the top AND changing code lower down), or across multiple files. Since each fix_before/fix_after pair covers ONE contiguous block, you MUST create SEPARATE location entries for each part of the fix:
|
||||
|
||||
- Each location covers one contiguous block of lines to change
|
||||
- Use the label field to describe how each part relates to the overall fix (e.g., "Add import for parameterized query library", "Replace string interpolation with parameterized query")
|
||||
- Order fix locations logically: primary fix first (where the vulnerability manifests), then supporting changes (imports, config, etc.)
|
||||
|
||||
COMMON MISTAKES TO AVOID:
|
||||
- Do NOT guess line numbers. Read the file and verify the exact lines before reporting.
|
||||
- Do NOT paraphrase or reformat code in fix_before. It must be a verbatim copy.
|
||||
- Do NOT set start_line=end_line when the vulnerable code spans multiple lines. Cover the full range.
|
||||
- Do NOT put an import addition and a code change in the same fix_before/fix_after if they are not on adjacent lines. Split them into separate locations.
|
||||
- Do NOT include lines outside the vulnerable/fixed code in fix_before just to "pad" the range.
|
||||
- Do NOT duplicate changes across locations. Each location's fix_after must ONLY contain changes for its own line range. Never repeat a change that is already covered by another location.
|
||||
|
||||
Each location element fields:
|
||||
- file (REQUIRED): Path relative to repository root. No leading slash, no absolute paths, no ".." traversal.
|
||||
Correct: "src/db/queries.ts" or "app/routes/users.py"
|
||||
Wrong: "/workspace/repo/src/db/queries.ts", "./src/db/queries.ts", "../../etc/passwd"
|
||||
- start_line (REQUIRED): Exact 1-based line number where the vulnerable/affected code begins. Must be a positive integer. You must be certain of this number — go back and verify against the actual file content if needed.
|
||||
- end_line (REQUIRED): Exact 1-based line number where the vulnerable/affected code ends. Must be >= start_line. Set equal to start_line ONLY if the code is truly on a single line.
|
||||
- snippet (optional): The actual source code at this location, copied verbatim from the file.
|
||||
- label (optional): Short role description for this location. For multi-part fixes, use this to explain the purpose of each change (e.g., "Add import for escape utility", "Sanitize user input before SQL query").
|
||||
- fix_before (optional): The vulnerable code to be replaced — VERBATIM copy of lines start_line through end_line. Must match the actual source character-for-character including whitespace and indentation.
|
||||
- fix_after (optional): The corrected code that replaces the entire fix_before block. Must be syntactically valid and ready to apply as a direct replacement.
|
||||
|
||||
Locations without fix_before/fix_after are informational context (e.g. showing the source of tainted data).
|
||||
Locations with fix_before/fix_after are actionable fixes (used directly for PR suggestion blocks).</description>
|
||||
<format>
|
||||
<location>
|
||||
<file>src/db/queries.ts</file>
|
||||
<start_line>42</start_line>
|
||||
<end_line>45</end_line>
|
||||
<snippet>const query = (
|
||||
`SELECT * FROM users ` +
|
||||
`WHERE id = ${id}`
|
||||
);</snippet>
|
||||
<label>Unsanitized input used in SQL query (sink)</label>
|
||||
<fix_before>const query = (
|
||||
`SELECT * FROM users ` +
|
||||
`WHERE id = ${id}`
|
||||
);</fix_before>
|
||||
<fix_after>const query = 'SELECT * FROM users WHERE id = $1';
|
||||
const result = await db.query(query, [id]);</fix_after>
|
||||
</location>
|
||||
<location>
|
||||
<file>src/routes/users.ts</file>
|
||||
<start_line>15</start_line>
|
||||
<end_line>15</end_line>
|
||||
<snippet>const id = req.params.id</snippet>
|
||||
<label>User input from request parameter (source)</label>
|
||||
</location>
|
||||
</format>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<returns type="Dict[str, Any]">
|
||||
@@ -177,7 +228,6 @@ Impact validation:
|
||||
- Use a controlled internal endpoint (or a benign endpoint that returns a distinct marker) to demonstrate that the request is performed by the server, not the client.
|
||||
- If the application follows redirects, validate whether an allowlisted URL can redirect to a disallowed destination, and whether the redirected-to destination is still fetched.</parameter>
|
||||
<parameter=poc_script_code>import json
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -262,16 +312,58 @@ if __name__ == "__main__":
|
||||
|
||||
6. Monitoring and alerting
|
||||
- Log and alert on preview attempts to unusual destinations, repeated failures, high-frequency requests, or attempts to access blocked ranges.</parameter>
|
||||
<parameter=attack_vector>N</parameter>
|
||||
<parameter=attack_complexity>L</parameter>
|
||||
<parameter=privileges_required>L</parameter>
|
||||
<parameter=user_interaction>N</parameter>
|
||||
<parameter=scope>C</parameter>
|
||||
<parameter=confidentiality>H</parameter>
|
||||
<parameter=integrity>H</parameter>
|
||||
<parameter=availability>L</parameter>
|
||||
<parameter=cvss_breakdown>
|
||||
<attack_vector>N</attack_vector>
|
||||
<attack_complexity>L</attack_complexity>
|
||||
<privileges_required>L</privileges_required>
|
||||
<user_interaction>N</user_interaction>
|
||||
<scope>C</scope>
|
||||
<confidentiality>H</confidentiality>
|
||||
<integrity>H</integrity>
|
||||
<availability>L</availability>
|
||||
</parameter>
|
||||
<parameter=endpoint>/api/v1/link-preview</parameter>
|
||||
<parameter=method>POST</parameter>
|
||||
<parameter=cwe>CWE-918</parameter>
|
||||
<parameter=code_locations>
|
||||
<location>
|
||||
<file>src/services/link-preview.ts</file>
|
||||
<start_line>45</start_line>
|
||||
<end_line>48</end_line>
|
||||
<snippet> const options = { timeout: 5000 };
|
||||
const response = await fetch(userUrl, options);
|
||||
const html = await response.text();
|
||||
return extractMetadata(html);</snippet>
|
||||
<label>Unvalidated user URL passed to server-side fetch (sink)</label>
|
||||
<fix_before> const options = { timeout: 5000 };
|
||||
const response = await fetch(userUrl, options);
|
||||
const html = await response.text();
|
||||
return extractMetadata(html);</fix_before>
|
||||
<fix_after> const validated = await validateAndResolveUrl(userUrl);
|
||||
if (!validated) throw new ForbiddenError('URL not allowed');
|
||||
const options = { timeout: 5000 };
|
||||
const response = await fetch(validated, options);
|
||||
const html = await response.text();
|
||||
return extractMetadata(html);</fix_after>
|
||||
</location>
|
||||
<location>
|
||||
<file>src/services/link-preview.ts</file>
|
||||
<start_line>2</start_line>
|
||||
<end_line>2</end_line>
|
||||
<snippet>import { extractMetadata } from '../utils/html';</snippet>
|
||||
<label>Add import for URL validation utility</label>
|
||||
<fix_before>import { extractMetadata } from '../utils/html';</fix_before>
|
||||
<fix_after>import { extractMetadata } from '../utils/html';
|
||||
import { validateAndResolveUrl } from '../utils/url-validator';</fix_after>
|
||||
</location>
|
||||
<location>
|
||||
<file>src/routes/api/v1/links.ts</file>
|
||||
<start_line>12</start_line>
|
||||
<end_line>12</end_line>
|
||||
<snippet>const userUrl = req.body.url</snippet>
|
||||
<label>User-controlled URL from request body (source)</label>
|
||||
</location>
|
||||
</parameter>
|
||||
</function>
|
||||
</examples>
|
||||
</tool>
|
||||
|
||||
Reference in New Issue
Block a user