diff --git a/strix/interface/tool_components/reporting_renderer.py b/strix/interface/tool_components/reporting_renderer.py index 98cc85d..898157d 100644 --- a/strix/interface/tool_components/reporting_renderer.py +++ b/strix/interface/tool_components/reporting_renderer.py @@ -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) diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 49a0489..cb1adff 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -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"): diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 8dbaebb..fe5bdfc 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -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: diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py index 25af62c..ecf3ce8 100644 --- a/strix/telemetry/tracer.py +++ b/strix/telemetry/tracer.py @@ -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") diff --git a/strix/tools/reporting/reporting_actions.py b/strix/tools/reporting/reporting_actions.py index c307d93..b84797f 100644 --- a/strix/tools/reporting/reporting_actions.py +++ b/strix/tools/reporting/reporting_actions.py @@ -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}>(.*?)", 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"(.*?)", 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}>(.*?)", 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 { diff --git a/strix/tools/reporting/reporting_actions_schema.xml b/strix/tools/reporting/reporting_actions_schema.xml index 56c6cc0..39e6a77 100644 --- a/strix/tools/reporting/reporting_actions_schema.xml +++ b/strix/tools/reporting/reporting_actions_schema.xml @@ -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. @@ -58,51 +58,28 @@ Professional, customer-facing report rules (PDF-ready): Specific, actionable steps to fix the vulnerability - - 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) - - - CVSS Attack Complexity - Conditions beyond attacker's control: -L = Low (no special conditions) -H = High (special conditions must exist) - - - CVSS Privileges Required - Level of privileges needed: -N = None (no privileges needed) -L = Low (basic user privileges) -H = High (admin privileges) - - - CVSS User Interaction - Does exploit require user action: -N = None (no user interaction needed) -R = Required (user must perform some action) - - - 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) - - - CVSS Confidentiality Impact - Impact to confidentiality: -N = None (no impact) -L = Low (some information disclosure) -H = High (all information disclosed) - - - 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) - - - CVSS Availability Impact - Impact to availability: -N = None (no impact) -L = Low (reduced performance or interruptions) -H = High (total loss of availability) + + 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) + + N + L + N + N + U + H + H + N + API endpoint(s) or URL path(s) (e.g., "/api/login") - for web vulnerabilities, or Git repository path(s) - for code vulnerabilities @@ -111,19 +88,64 @@ H = High (total loss of availability) HTTP method(s) (GET, POST, etc.) - for web vulnerabilities. - 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. + 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. - - MANDATORY for white-box testing: exact affected source file path(s). + + 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. - - MANDATORY for white-box testing: actual vulnerable code snippet(s) copied verbatim from the repository. - - - MANDATORY for white-box testing: corrected code snippet(s) exactly as they should appear after the fix. - - - 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. + + Nested XML list of code locations where the vulnerability exists. MANDATORY for white-box testing. + +Order: first location is where the issue manifests (typically the sink). Additional locations provide data flow context (source → propagation → sink). + +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 code begins. Must be a positive integer. You must be certain of this number — do not guess or approximate. Go back and verify against the actual file content if needed. +- end_line (REQUIRED): Exact 1-based line number where the vulnerable code ends. Must be >= start_line. Set equal to start_line if the vulnerability is on a single line. +- snippet (optional): The actual source code at this location, copied verbatim from the file. Do not paraphrase or summarize code — paste it exactly as it appears. +- label (optional): Short role description for this location in the data flow, e.g. "User input from request parameter (source)", "Unsanitized input passed to SQL query (sink)". +- fix_before (optional): The vulnerable code to be replaced, copied verbatim. Must match the actual source exactly — do not paraphrase, summarize, or add/remove whitespace. Only include on locations where a fix is proposed. +- fix_after (optional): The corrected code that should replace fix_before. Must be syntactically valid and ready to apply as a direct replacement. Only include on locations where a fix is proposed. + +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 for PR review suggestions). + + + src/db/queries.ts + 42 + 42 + db.query(`SELECT * FROM users WHERE id = ${id}`) + + db.query(`SELECT * FROM users WHERE id = ${id}`) + db.query('SELECT * FROM users WHERE id = $1', [id]) + + + src/routes/users.ts + 15 + 15 + const id = req.params.id + + + @@ -177,7 +199,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. import json -import sys import time from urllib.parse import urljoin @@ -262,16 +283,39 @@ 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. -N -L -L -N -C -H -H -L + + N + L + L + N + C + H + H + L + /api/v1/link-preview POST +CWE-918 + + + src/services/link-preview.ts + 47 + 47 + const response = await fetch(userUrl) + + const response = await fetch(userUrl) + const validated = await validateAndResolveUrl(userUrl) +if (!validated) throw new ForbiddenError('URL not allowed') +const response = await fetch(validated) + + + src/routes/api/v1/links.ts + 12 + 12 + const userUrl = req.body.url + + +