mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
fix(reporting): require advisory_cvss for dependency findings + add SCA TUI renderer (#753)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
This commit is contained in:
@@ -371,6 +371,19 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
text.append("Target: ", style=self.FIELD_STYLE)
|
||||
text.append(target)
|
||||
|
||||
dep_meta = vuln.get("dependency_metadata") or {}
|
||||
for label, key in (
|
||||
("Package", "package_name"),
|
||||
("Ecosystem", "package_ecosystem"),
|
||||
("Installed Version", "installed_version"),
|
||||
("Fixed Version", "fixed_version"),
|
||||
):
|
||||
value = dep_meta.get(key)
|
||||
if value:
|
||||
text.append("\n\n")
|
||||
text.append(f"{label}: ", style=self.FIELD_STYLE)
|
||||
text.append(str(value))
|
||||
|
||||
endpoint = vuln.get("endpoint", "")
|
||||
if endpoint:
|
||||
text.append("\n\n")
|
||||
@@ -389,6 +402,18 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
text.append("CVE: ", style=self.FIELD_STYLE)
|
||||
text.append(cve)
|
||||
|
||||
cwe = vuln.get("cwe", "")
|
||||
if cwe:
|
||||
text.append("\n\n")
|
||||
text.append("CWE: ", style=self.FIELD_STYLE)
|
||||
text.append(cwe)
|
||||
|
||||
fix_effort = vuln.get("fix_effort", "")
|
||||
if fix_effort:
|
||||
text.append("\n\n")
|
||||
text.append("Fix Effort: ", style=self.FIELD_STYLE)
|
||||
text.append(str(fix_effort).title())
|
||||
|
||||
cvss_breakdown = vuln.get("cvss_breakdown", {})
|
||||
if cvss_breakdown:
|
||||
cvss_parts = []
|
||||
@@ -434,6 +459,13 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
text.append("\n")
|
||||
text.append(technical_analysis)
|
||||
|
||||
evidence = vuln.get("evidence", "")
|
||||
if evidence:
|
||||
text.append("\n\n")
|
||||
text.append("Evidence", style=self.FIELD_STYLE)
|
||||
text.append("\n")
|
||||
text.append(evidence)
|
||||
|
||||
poc_description = vuln.get("poc_description", "")
|
||||
if poc_description:
|
||||
text.append("\n\n")
|
||||
@@ -455,6 +487,13 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
text.append("\n")
|
||||
text.append(remediation_steps)
|
||||
|
||||
assumptions = vuln.get("assumptions", "")
|
||||
if assumptions:
|
||||
text.append("\n\n")
|
||||
text.append("Assumptions", style=self.FIELD_STYLE)
|
||||
text.append("\n")
|
||||
text.append(assumptions)
|
||||
|
||||
return text
|
||||
|
||||
def _get_markdown_report(self) -> str:
|
||||
@@ -476,14 +515,27 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
lines.append(f"**Agent:** {vuln['agent_name']}")
|
||||
if vuln.get("target"):
|
||||
lines.append(f"**Target:** {vuln['target']}")
|
||||
dep_meta = vuln.get("dependency_metadata") or {}
|
||||
if dep_meta.get("package_name"):
|
||||
lines.append(f"**Package:** {dep_meta['package_name']}")
|
||||
if dep_meta.get("package_ecosystem"):
|
||||
lines.append(f"**Ecosystem:** {dep_meta['package_ecosystem']}")
|
||||
if dep_meta.get("installed_version"):
|
||||
lines.append(f"**Installed Version:** {dep_meta['installed_version']}")
|
||||
if dep_meta.get("fixed_version"):
|
||||
lines.append(f"**Fixed Version:** {dep_meta['fixed_version']}")
|
||||
if vuln.get("endpoint"):
|
||||
lines.append(f"**Endpoint:** {vuln['endpoint']}")
|
||||
if vuln.get("method"):
|
||||
lines.append(f"**Method:** {vuln['method']}")
|
||||
if vuln.get("cve"):
|
||||
lines.append(f"**CVE:** {vuln['cve']}")
|
||||
if vuln.get("cwe"):
|
||||
lines.append(f"**CWE:** {vuln['cwe']}")
|
||||
if vuln.get("cvss") is not None:
|
||||
lines.append(f"**CVSS:** {vuln['cvss']}")
|
||||
if vuln.get("fix_effort"):
|
||||
lines.append(f"**Fix Effort:** {str(vuln['fix_effort']).title()}")
|
||||
|
||||
cvss_breakdown = vuln.get("cvss_breakdown", {})
|
||||
if cvss_breakdown:
|
||||
@@ -514,6 +566,9 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
if vuln.get("technical_analysis"):
|
||||
lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]])
|
||||
|
||||
if vuln.get("evidence"):
|
||||
lines.extend(["", "## Evidence", "", vuln["evidence"]])
|
||||
|
||||
if vuln.get("poc_description") or vuln.get("poc_script_code"):
|
||||
lines.extend(["", "## Proof of Concept", ""])
|
||||
if vuln.get("poc_description"):
|
||||
@@ -552,6 +607,9 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
|
||||
if vuln.get("remediation_steps"):
|
||||
lines.extend(["", "## Remediation", "", vuln["remediation_steps"]])
|
||||
|
||||
if vuln.get("assumptions"):
|
||||
lines.extend(["", "## Assumptions", "", vuln["assumptions"]])
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -256,3 +256,176 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
||||
|
||||
css_classes = cls.get_css_classes("completed")
|
||||
return Static(padded, classes=css_classes)
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
class CreateDependencyReportRenderer(BaseToolRenderer):
|
||||
tool_name: ClassVar[str] = "create_dependency_report"
|
||||
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
|
||||
|
||||
SEVERITY_COLORS: ClassVar[dict[str, str]] = {
|
||||
"critical": "#dc2626",
|
||||
"high": "#ea580c",
|
||||
"medium": "#d97706",
|
||||
"low": "#65a30d",
|
||||
"info": "#0284c7",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_cvss_color(cls, cvss_score: float) -> str:
|
||||
if cvss_score >= 9.0:
|
||||
return "#dc2626"
|
||||
if cvss_score >= 7.0:
|
||||
return "#ea580c"
|
||||
if cvss_score >= 4.0:
|
||||
return "#d97706"
|
||||
if cvss_score >= 0.1:
|
||||
return "#65a30d"
|
||||
return "#6b7280"
|
||||
|
||||
@classmethod
|
||||
def _render_unsuccessful(cls, args: dict[str, Any], result: dict[str, Any]) -> Static:
|
||||
text = Text()
|
||||
text.append("📦 ")
|
||||
text.append("Dependency (SCA) Report", style="bold #ea580c")
|
||||
title = args.get("title", "")
|
||||
if title:
|
||||
text.append("\n\n")
|
||||
text.append("Title: ", style=FIELD_STYLE)
|
||||
text.append(title)
|
||||
|
||||
warning = result.get("warning")
|
||||
if result.get("success") is False:
|
||||
errors = result.get("errors")
|
||||
detail = (
|
||||
"; ".join(errors) if isinstance(errors, list) and errors else result.get("error")
|
||||
)
|
||||
label, style = "✗ Not created: ", "bold #dc2626"
|
||||
fallback = "Report was not created."
|
||||
else:
|
||||
detail = warning
|
||||
label, style = "⚠ Not persisted: ", "bold #d97706"
|
||||
fallback = "Report could not be persisted."
|
||||
text.append("\n\n")
|
||||
text.append(label, style=style)
|
||||
text.append(str(detail or fallback))
|
||||
|
||||
padded = Text()
|
||||
padded.append("\n\n")
|
||||
padded.append_text(text)
|
||||
padded.append("\n\n")
|
||||
return Static(padded, classes=cls.get_css_classes("failed"))
|
||||
|
||||
@classmethod
|
||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||
args = tool_data.get("args", {})
|
||||
result = tool_data.get("result", {})
|
||||
|
||||
if isinstance(result, dict) and (result.get("success") is False or result.get("warning")):
|
||||
return cls._render_unsuccessful(args, result)
|
||||
|
||||
title = args.get("title", "")
|
||||
description = args.get("description", "")
|
||||
impact = args.get("impact", "")
|
||||
target = args.get("target", "")
|
||||
technical_analysis = args.get("technical_analysis", "")
|
||||
remediation_steps = args.get("remediation_steps", "")
|
||||
assumptions = args.get("assumptions", "")
|
||||
|
||||
package_name = args.get("package_name", "")
|
||||
package_ecosystem = args.get("package_ecosystem", "")
|
||||
installed_version = args.get("installed_version", "")
|
||||
fixed_version = args.get("fixed_version", "")
|
||||
cve = args.get("cve", "")
|
||||
cwe = args.get("cwe", "")
|
||||
advisory_cvss = args.get("advisory_cvss")
|
||||
fix_effort = args.get("fix_effort", "")
|
||||
|
||||
severity = ""
|
||||
if isinstance(result, dict):
|
||||
severity = result.get("severity", "")
|
||||
|
||||
text = Text()
|
||||
text.append("📦 ")
|
||||
text.append("Dependency (SCA) Report", style="bold #ea580c")
|
||||
|
||||
if title:
|
||||
text.append("\n\n")
|
||||
text.append("Title: ", style=FIELD_STYLE)
|
||||
text.append(title)
|
||||
|
||||
if severity:
|
||||
text.append("\n\n")
|
||||
text.append("Severity: ", style=FIELD_STYLE)
|
||||
severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280")
|
||||
text.append(severity.upper(), style=f"bold {severity_color}")
|
||||
|
||||
if advisory_cvss is not None:
|
||||
text.append("\n\n")
|
||||
text.append("Advisory CVSS: ", style=FIELD_STYLE)
|
||||
try:
|
||||
score = float(advisory_cvss)
|
||||
text.append(str(score), style=f"bold {cls._get_cvss_color(score)}")
|
||||
except (TypeError, ValueError):
|
||||
text.append(str(advisory_cvss), style=DIM_STYLE)
|
||||
|
||||
if cve:
|
||||
text.append("\n\n")
|
||||
text.append("CVE: ", style=FIELD_STYLE)
|
||||
text.append(cve)
|
||||
|
||||
if cwe:
|
||||
text.append("\n\n")
|
||||
text.append("CWE: ", style=FIELD_STYLE)
|
||||
text.append(cwe)
|
||||
|
||||
if package_name:
|
||||
text.append("\n\n")
|
||||
text.append("Package: ", style=FIELD_STYLE)
|
||||
text.append(package_name, style=FILE_STYLE)
|
||||
if package_ecosystem:
|
||||
text.append(f" ({package_ecosystem})", style=DIM_STYLE)
|
||||
|
||||
if installed_version:
|
||||
text.append("\n\n")
|
||||
text.append("Installed: ", style=FIELD_STYLE)
|
||||
text.append(installed_version, style=BEFORE_STYLE)
|
||||
if fixed_version:
|
||||
text.append(" → ", style=DIM_STYLE)
|
||||
text.append("Fixed: ", style=FIELD_STYLE)
|
||||
text.append(fixed_version, style=AFTER_STYLE)
|
||||
|
||||
if fix_effort:
|
||||
text.append("\n\n")
|
||||
text.append("Fix Effort: ", style=FIELD_STYLE)
|
||||
text.append(fix_effort)
|
||||
|
||||
if target:
|
||||
text.append("\n\n")
|
||||
text.append("Target: ", style=FIELD_STYLE)
|
||||
text.append(target)
|
||||
|
||||
for label, value in [
|
||||
("Description", description),
|
||||
("Impact", impact),
|
||||
("Technical Analysis", technical_analysis),
|
||||
("Assumptions", assumptions),
|
||||
("Remediation", remediation_steps),
|
||||
]:
|
||||
if value:
|
||||
text.append("\n\n")
|
||||
text.append(label, style=FIELD_STYLE)
|
||||
text.append("\n")
|
||||
text.append(value)
|
||||
|
||||
if not title:
|
||||
text.append("\n ")
|
||||
text.append("Creating dependency report...", style="dim")
|
||||
|
||||
padded = Text()
|
||||
padded.append("\n\n")
|
||||
padded.append_text(text)
|
||||
padded.append("\n\n")
|
||||
|
||||
css_classes = cls.get_css_classes("completed")
|
||||
return Static(padded, classes=css_classes)
|
||||
|
||||
@@ -124,8 +124,13 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
f"**Found:** {report.get('timestamp', 'unknown')}",
|
||||
]
|
||||
|
||||
dep_meta = report.get("dependency_metadata") or {}
|
||||
metadata: list[tuple[str, Any]] = [
|
||||
("Target", report.get("target")),
|
||||
("Package", dep_meta.get("package_name")),
|
||||
("Ecosystem", dep_meta.get("package_ecosystem")),
|
||||
("Installed Version", dep_meta.get("installed_version")),
|
||||
("Fixed Version", dep_meta.get("fixed_version")),
|
||||
("Endpoint", report.get("endpoint")),
|
||||
("Method", report.get("method")),
|
||||
("CVE", report.get("cve")),
|
||||
@@ -134,6 +139,8 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
cvss = report.get("cvss")
|
||||
if cvss is not None:
|
||||
metadata.append(("CVSS", cvss))
|
||||
if report.get("fix_effort"):
|
||||
metadata.append(("Fix Effort", str(report["fix_effort"]).title()))
|
||||
for label, value in metadata:
|
||||
if value:
|
||||
lines.append(f"**{label}:** {value}")
|
||||
@@ -143,6 +150,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append(report.get("description") or "No description provided.")
|
||||
lines.append("")
|
||||
|
||||
if report.get("evidence"):
|
||||
lines.append("## Evidence\n")
|
||||
lines.append(str(report["evidence"]))
|
||||
lines.append("")
|
||||
|
||||
if report.get("impact"):
|
||||
lines.append("## Impact\n")
|
||||
lines.append(str(report["impact"]))
|
||||
@@ -194,4 +206,9 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append(str(report["remediation_steps"]))
|
||||
lines.append("")
|
||||
|
||||
if report.get("assumptions"):
|
||||
lines.append("## Assumptions\n")
|
||||
lines.append(str(report["assumptions"]))
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -41,6 +41,7 @@ The skills are dynamically injected into the agent's system prompt, allowing it
|
||||
Notable source-aware skills:
|
||||
- `source_aware_whitebox` (coordination): white-box orchestration playbook
|
||||
- `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow
|
||||
- `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: dependency-cve-scanning
|
||||
description: Supply-chain / SCA playbook — scan repository lockfiles for known dependency CVEs and report them with create_dependency_report (no dynamic PoC required)
|
||||
---
|
||||
|
||||
# Dependency / Supply-Chain CVE Scanning (SCA)
|
||||
|
||||
Use this skill on white-box / repository scans to make sure a repository pinning a
|
||||
**known-vulnerable dependency** is actually reported as a finding, instead of being
|
||||
discovered and then silently dropped because it cannot be dynamically exploited.
|
||||
|
||||
Known-CVE dependency findings are a first-class deliverable. Report each one with
|
||||
the dedicated `create_dependency_report` tool.
|
||||
|
||||
## Why this skill exists
|
||||
|
||||
A vulnerable dependency pinned in a lockfile (e.g. `lodash@4.17.4` with a known
|
||||
prototype-pollution CVE) usually cannot be dynamically PoC'd from the outside —
|
||||
the vulnerable code path may not even be reachable from a running endpoint. The
|
||||
normal "no report without a dynamic PoC" rule would suppress it. For these
|
||||
findings the proof is the **lockfile entry + scanner output + published
|
||||
advisory**, not an exploit script. This is the one explicit exception to the
|
||||
dynamic-validation rule, and it exists only for `create_dependency_report`.
|
||||
|
||||
## Scan procedure
|
||||
|
||||
Run from the repo root and store output in the shared artifact directory used by
|
||||
the source-aware pass:
|
||||
|
||||
```bash
|
||||
ART=/workspace/.strix-source-aware
|
||||
mkdir -p "$ART"
|
||||
|
||||
# Record the vuln DB age so a stale DB is a visible signal, not a silent clean scan.
|
||||
trivy version --format json 2>/dev/null | tee "$ART/trivy-version.json"
|
||||
# inspect .VulnerabilityDB.UpdatedAt / NextUpdate
|
||||
|
||||
# Lockfile/manifest -> known-CVE matching. Try a best-effort DB refresh first so a
|
||||
# sandbox with egress gets the freshest CVEs; if the update fails, fall back to the
|
||||
# cached DB instead of failing the scan. --offline-scan keeps per-package advisory
|
||||
# lookups offline.
|
||||
trivy fs --scanners vuln --timeout 30m --offline-scan \
|
||||
--format json --output "$ART/trivy-sca.json" . \
|
||||
|| trivy fs --scanners vuln --timeout 30m --offline-scan --skip-db-update \
|
||||
--format json --output "$ART/trivy-sca.json" . \
|
||||
|| true
|
||||
```
|
||||
|
||||
If `.VulnerabilityDB.UpdatedAt` is more than a few weeks old (the sandbox had no
|
||||
egress to refresh it), treat it as a scan limitation and note it in the
|
||||
`assumptions` of dependency findings — a stale DB that still returns *some* results
|
||||
will not trip the "zero results is suspicious" heuristic, so its age is the only
|
||||
staleness signal.
|
||||
|
||||
Trivy reads the lockfiles/manifests it finds, including:
|
||||
`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`,
|
||||
`requirements.txt`, `Pipfile.lock`, `go.mod`/`go.sum`, `Gemfile.lock`,
|
||||
`pom.xml`/`gradle.lockfile`, `Cargo.lock`, `composer.lock`, etc.
|
||||
|
||||
If trivy returns zero vulnerabilities on a repo with dependencies, treat it as
|
||||
suspicious: confirm the vuln DB is present (`trivy-version.json`) and that
|
||||
lockfiles exist.
|
||||
|
||||
## Interpreting results
|
||||
|
||||
For each entry under `.Results[].Vulnerabilities[]` in `trivy-sca.json`, collect:
|
||||
|
||||
- `VulnerabilityID` — the CVE (or GHSA; prefer the CVE if both are present)
|
||||
- `PkgName` and `InstalledVersion` — the affected package + pinned version
|
||||
- `FixedVersion` — the version that resolves it
|
||||
- `Target` — the lockfile path it came from
|
||||
- `.Results[].Type` (e.g. `npm`, `pip`, `gomod`, `pom`, `gemspec`, `cargo`) — the
|
||||
package ecosystem; normalize to the registry name lowercased (`npm`, `pypi`,
|
||||
`go`, `maven`, `rubygems`, `cargo`, `composer`, `nuget`, ...)
|
||||
- `CVSS` — the published advisory base score
|
||||
- `PrimaryURL` / references — to verify the advisory
|
||||
|
||||
Deduplicate by `(CVE, PkgName, InstalledVersion)`. File one
|
||||
`create_dependency_report` per CVE — do not batch multiple CVEs into one report.
|
||||
|
||||
### Reachability is a confidence modifier, not a gate
|
||||
|
||||
Do NOT suppress or downgrade a known CVE just because you could not prove the
|
||||
vulnerable code path is reachable. Report it, set `advisory_cvss` from the
|
||||
advisory, and use `assumptions` to note reachability (e.g. "the vulnerable
|
||||
`template()` API does not appear to be imported in application code, so practical
|
||||
exploitability is uncertain"). If you *can* show reachability or chain it into a
|
||||
dynamic exploit, do that and report it as a normal dynamic finding with
|
||||
`create_vulnerability_report` instead.
|
||||
|
||||
## Reporting
|
||||
|
||||
Report each confirmed known CVE with the dedicated `create_dependency_report`
|
||||
tool (NOT `create_vulnerability_report` — that tool is for dynamically validated
|
||||
findings and rejects empty PoC fields):
|
||||
|
||||
- Set `cve` to the verified `CVE-YYYY-NNNNN` id (required). If you only have a
|
||||
GHSA, look up the mapped CVE; if there is genuinely no CVE, do not report it
|
||||
with this tool.
|
||||
- There are no PoC fields — `create_dependency_report` does not take
|
||||
`poc_description` / `poc_script_code` / `code_locations`. The proof lives in
|
||||
`description` and `technical_analysis` (scanner output + advisory).
|
||||
- **Always fill the structured dependency fields** (they power the dedicated
|
||||
dependency-report card; do not leave them only in free-text):
|
||||
- `package_name` — `PkgName` (required).
|
||||
- `installed_version` — `InstalledVersion` (required).
|
||||
- `package_ecosystem` — normalized ecosystem from `.Results[].Type` (lowercased,
|
||||
e.g. `npm`, `pypi`, `go`, `maven`, `rubygems`, `cargo`) (required).
|
||||
- `fixed_version` — `FixedVersion` (leave empty only if no fix is published).
|
||||
- Reference the repo-relative `Target` lockfile path in `description` /
|
||||
`technical_analysis` (no leading slash) so the finding is traceable.
|
||||
- Put the concrete proof in `description` / `technical_analysis`: package name,
|
||||
installed/affected version, fixed version, lockfile path, and the relevant
|
||||
trivy output excerpt.
|
||||
- **Always set `advisory_cvss` to the published advisory base score (0.0–10.0).**
|
||||
Severity is derived *solely* from this number: read it off the advisory (`CVSS`
|
||||
in trivy output, or the NVD/GHSA page) and pass the real value. The tool rejects
|
||||
a call that omits it, because guessing a score both inflates low CVEs and
|
||||
deflates critical ones.
|
||||
- Set `cwe` to the most specific `CWE-NNN` when the advisory names one.
|
||||
- Do NOT cap severity at LOW just because there is no dynamic reproduction — use
|
||||
the advisory score.
|
||||
- Use `assumptions` for reachability/exploitability caveats.
|
||||
|
||||
Verify the CVE with `web_search` when available before reporting. Never guess or
|
||||
hallucinate a CVE id.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Do not report a dependency CVE with `create_vulnerability_report`; use
|
||||
`create_dependency_report`.
|
||||
- Do not report a finding without a verified CVE id.
|
||||
- Do not batch multiple CVEs into one report.
|
||||
- Do not omit `advisory_cvss` — the tool rejects it, and it is the single input
|
||||
that determines dependency severity.
|
||||
- Do not silently drop a known CVE because it lacks a dynamic PoC — that is the
|
||||
exact failure this skill prevents.
|
||||
- Do not downgrade advisory severity for lack of dynamic reproduction.
|
||||
@@ -121,6 +121,11 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
|
||||
--format json --output /workspace/.strix-source-aware/trivy-fs.json . || true
|
||||
```
|
||||
|
||||
Known-CVE dependency findings are the one exception to the "report only after
|
||||
dynamic validation" rule below: report each one with `create_dependency_report`
|
||||
(not `create_vulnerability_report`), setting `advisory_cvss` from the published
|
||||
advisory. `load_skill(["dependency_cve_scanning"])` for the full SCA workflow.
|
||||
|
||||
## JavaScript-Side Coverage
|
||||
|
||||
For frontends and Node services, layer these on top of the language-agnostic
|
||||
|
||||
@@ -198,8 +198,7 @@ async def _do_create( # noqa: PLR0912
|
||||
fix_effort = (fix_effort or "").strip().lower()
|
||||
if fix_effort not in _VALID_FIX_EFFORT:
|
||||
errors.append(
|
||||
f"Invalid fix_effort: {fix_effort!r}. "
|
||||
f"Must be one of: {sorted(_VALID_FIX_EFFORT)}"
|
||||
f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
|
||||
)
|
||||
|
||||
if not isinstance(cvss_breakdown, dict) or not cvss_breakdown:
|
||||
@@ -610,7 +609,7 @@ _DEP_SEVERITY_FROM_CVSS = {
|
||||
|
||||
def _dependency_severity(advisory_cvss: float | None) -> tuple[float, str]:
|
||||
if advisory_cvss is None:
|
||||
return 0.0, "medium"
|
||||
return 0.0, "info"
|
||||
score = max(0.0, min(10.0, advisory_cvss))
|
||||
for (lo, hi), label in _DEP_SEVERITY_FROM_CVSS.items():
|
||||
if lo <= score < hi or (hi == 10.0 and score == 10.0):
|
||||
@@ -652,7 +651,7 @@ def _build_dependency_evidence(
|
||||
return evidence
|
||||
|
||||
|
||||
async def _do_create_dependency(
|
||||
async def _do_create_dependency( # noqa: PLR0912
|
||||
*,
|
||||
title: str,
|
||||
description: str,
|
||||
@@ -705,7 +704,13 @@ async def _do_create_dependency(
|
||||
f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
|
||||
)
|
||||
|
||||
if advisory_cvss is not None and not 0.0 <= advisory_cvss <= 10.0:
|
||||
if advisory_cvss is None:
|
||||
errors.append(
|
||||
"advisory_cvss is required: read the published advisory base score "
|
||||
"(0.0-10.0) off the advisory (trivy CVSS / NVD / GHSA). Severity is "
|
||||
"derived solely from it — do not omit it or the finding cannot be rated."
|
||||
)
|
||||
elif not 0.0 <= advisory_cvss <= 10.0:
|
||||
errors.append(f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}")
|
||||
|
||||
if errors:
|
||||
@@ -810,13 +815,13 @@ async def create_dependency_report(
|
||||
cve: str,
|
||||
package_name: str,
|
||||
installed_version: str,
|
||||
advisory_cvss: float,
|
||||
impact: str,
|
||||
remediation_steps: str,
|
||||
assumptions: str,
|
||||
package_ecosystem: str,
|
||||
fixed_version: str | None = None,
|
||||
cwe: str | None = None,
|
||||
advisory_cvss: float | None = None,
|
||||
technical_analysis: str | None = None,
|
||||
fix_effort: str = "low",
|
||||
) -> str:
|
||||
@@ -864,8 +869,10 @@ async def create_dependency_report(
|
||||
package_ecosystem: e.g. ``npm`` / ``pypi`` / ``maven`` / ``go``.
|
||||
fixed_version: First non-vulnerable version, if known.
|
||||
cwe: ``CWE-NNN`` (most specific) if certain, else omit.
|
||||
advisory_cvss: Published advisory base score (0.0-10.0) if known;
|
||||
drives severity. Omit if unknown (defaults to medium).
|
||||
advisory_cvss: **Required.** Published advisory base score
|
||||
(0.0-10.0) — read it off the advisory (trivy CVSS / NVD / GHSA).
|
||||
Severity is derived solely from this score, so it must be the
|
||||
real published value; do not guess or omit it.
|
||||
technical_analysis: Optional deeper mechanism/root-cause detail.
|
||||
fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high``
|
||||
(dependency upgrades are usually ``trivial``/``low``).
|
||||
|
||||
@@ -79,6 +79,40 @@ def test_render_vulnerability_md_includes_core_sections() -> None:
|
||||
assert "**Endpoint:** /api/login" in md
|
||||
|
||||
|
||||
def test_render_vulnerability_md_includes_dependency_fields() -> None:
|
||||
md = render_vulnerability_md(
|
||||
_sample_report(
|
||||
title="CVE-2021-23337 in lodash 4.17.20",
|
||||
severity="high",
|
||||
target="repo/package.json",
|
||||
endpoint=None,
|
||||
method=None,
|
||||
cve="CVE-2021-23337",
|
||||
cwe="CWE-94",
|
||||
cvss=7.2,
|
||||
fix_effort="trivial",
|
||||
finding_class="dependency_cve",
|
||||
evidence="**Advisory evidence:** `CVE-2021-23337` applies to `lodash`.",
|
||||
assumptions="Assumes lodash ships in deployed builds.",
|
||||
dependency_metadata={
|
||||
"package_name": "lodash",
|
||||
"package_ecosystem": "npm",
|
||||
"installed_version": "4.17.20",
|
||||
"fixed_version": "4.17.21",
|
||||
},
|
||||
remediation_steps="Upgrade to 4.17.21.",
|
||||
),
|
||||
)
|
||||
assert "**Package:** lodash" in md
|
||||
assert "**Ecosystem:** npm" in md
|
||||
assert "**Installed Version:** 4.17.20" in md
|
||||
assert "**Fixed Version:** 4.17.21" in md
|
||||
assert "**CWE:** CWE-94" in md
|
||||
assert "**Fix Effort:** Trivial" in md
|
||||
assert "## Evidence" in md
|
||||
assert "## Assumptions" in md
|
||||
|
||||
|
||||
def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None:
|
||||
reports = [
|
||||
_sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"),
|
||||
|
||||
@@ -192,6 +192,30 @@ async def test_dependency_report_with_zero_cvss_remains_low_severity(
|
||||
assert report["cvss"] == 0.0
|
||||
|
||||
|
||||
async def test_dependency_report_requires_advisory_cvss(report_state: ReportState) -> None:
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2024-0001 in sample 1.0.0",
|
||||
description="Published advisory affects the pinned version.",
|
||||
target="repo/package.json",
|
||||
cve="CVE-2024-0001",
|
||||
package_name="sample",
|
||||
installed_version="1.0.0",
|
||||
impact="Some impact.",
|
||||
remediation_steps="Upgrade to 1.0.1.",
|
||||
assumptions="Assumes the package ships in deployed builds.",
|
||||
package_ecosystem="npm",
|
||||
fixed_version="1.0.1",
|
||||
cwe=None,
|
||||
advisory_cvss=None,
|
||||
technical_analysis=None,
|
||||
fix_effort="low",
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert any("advisory_cvss is required" in e for e in result["errors"])
|
||||
assert not report_state.vulnerability_reports
|
||||
|
||||
|
||||
async def test_dependency_report_dedupe_candidate_includes_dependency_metadata(
|
||||
report_state: ReportState,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -535,4 +559,6 @@ def test_vuln_tool_exposes_new_params() -> None:
|
||||
dep_props = create_dependency_report.params_json_schema["properties"]
|
||||
for field in ("package_name", "installed_version", "cve", "advisory_cvss"):
|
||||
assert field in dep_props
|
||||
assert "package_ecosystem" in create_dependency_report.params_json_schema["required"]
|
||||
dep_required = create_dependency_report.params_json_schema["required"]
|
||||
assert "package_ecosystem" in dep_required
|
||||
assert "advisory_cvss" in dep_required
|
||||
|
||||
Reference in New Issue
Block a user