Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/content/supported_tools/parsers/file/strix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: "Strix Scan"
toc_hide: true
---

The [Strix](https://github.com/usestrix/strix) parser for DefectDojo supports imports from the JSON report of a Strix security run. This document details how Strix findings are mapped into DefectDojo Findings, which fields are parsed, and the deduplication behavior.

## Supported File Types

The Strix parser accepts JSON files in the format of the `vulnerabilities.json` report produced by a Strix run.

To import Strix results into DefectDojo:

1. Run Strix against the target codebase
2. Export or copy the resulting `vulnerabilities.json` report
3. Upload the file to DefectDojo using the "Strix Scan" scan type

The report is a JSON array with one object per finding. A wrapped shape (`{"vulnerabilities": [...]}`) is also accepted for forward compatibility. Every field is optional: the set of keys varies by `finding_class` (code findings carry PoC and CVSS data, dependency findings carry package metadata), so missing fields are left unset on the Finding rather than filled with placeholders. A report that is not a JSON array (or a wrapped one) is rejected with an error instead of silently importing zero findings; a legitimately empty array imports zero findings.

## Default Deduplication Hashcode Fields

Strix findings deduplicate using the [unique id from tool algorithm](/triage_findings/finding_deduplication/about_deduplication/):

- vuln_id_from_tool (populated verbatim from the report's `id` field)

The Strix `id` is unique per finding and stable across scans of the same codebase, so it is used directly rather than a hash of finding fields. No hashcode fields are registered for this scan type.

### Sample Scan Data

Sample Strix scans can be found in the [sample scan data folder](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/strix).

## Link To Tool

- [Strix](https://github.com/usestrix/strix)

## JSON Format

### Total Fields in JSON

- Total data fields: 30
- Total data fields parsed into dedicated Finding fields, the structured description, or the mitigation: 28
- Remaining fields (`agent_id`, `agent_name`) identify the Strix agent that produced the finding and are not mapped

### JSON Format Field Mapping Details

<details>
<summary>Click to expand Field Mapping Table</summary>

| Source Field | DefectDojo Field | Notes |
| ---------------------- | ------------------------- | ------------------------------------------------------------------------ |
| title | title | Finding title |
| severity | severity | Mapped to DefectDojo severity levels; defaults to Info if unrecognized |
| description | description | First section of the structured description |
| impact | impact | Finding impact |
| target | description | Included as a "**Target:**" line in the description |
| confidence | description | Included as a "**Confidence:**" line in the description |
| cvss | cvssv3_score | Numeric CVSS score, set when the value is a number |
| cvss_breakdown | cvssv3 vector | Assembled into a CVSS:3.1 vector string shown next to the score |
| cwe | cwe | `CWE-<number>` extracted from the string |
| cve | unsaved_vulnerability_ids | Set as the finding's vulnerability reference |
| id | vuln_id_from_tool | Strix finding identifier, used verbatim; drives deduplication |
| timestamp | date | Parsed finding date |
| finding_class | static_finding / dynamic_finding | `dynamic` marks a dynamic finding; anything else marks a static finding |
| remediation_steps | mitigation | First part of the mitigation |
| fix_effort | mitigation | Included as a "**Fix effort:**" line in the mitigation |
| fix_pr_body | fix_available | Contributes (with remediation_steps) to a non-null mitigation and fix_available |
| poc_description | steps_to_reproduce | Reproduction steps |
| poc_script_code | steps_to_reproduce | PoC script, appended after the steps |
| technical_analysis | description | "## Technical analysis" description section |
| evidence | description | "## Evidence" description section |
| assumptions | description | "## Assumptions" description section |
| counterevidence | description | "## Counter-evidence" description section |
| severity_change_conditions | description | "## Conditions that would change severity" description section |
| fix_verification | not mapped | Verification note from the fix run; not currently mapped |
| code_locations | file_path / line | First code location anchors the finding's file and start line |
| dependency_metadata.package_name | component_name | Dependency finding component name |
| dependency_metadata.installed_version | component_version | Dependency finding component version |
| dependency_metadata (other keys) | not mapped | Advisory CVSS, ecosystem, manifest path, and reachability metadata are not currently mapped |
| agent_id / agent_name | not mapped | Strix agent identifiers |

</details>

### Additional Finding Field Settings (JSON Format)

| Finding Field | Default Value | Notes |
| --------------- | ------------- | -------------------------------------------------------------- |
| active | True | Standard default for imported findings |
| verified | True | Standard default for imported findings |
| static_finding | True | Unless `finding_class` is `dynamic` |
| dynamic_finding | True | Only when `finding_class` is `dynamic` |
| fix_available | True | When the report carries remediation_steps or a fix PR body |

## Special Processing Notes

### Severity Mapping

- `critical` → Critical
- `high` → High
- `medium` → Medium
- `low` → Low
- `info` / `informational` → Info

Any unrecognized or missing value defaults to Info.

### Description Construction

The description is assembled from the parts the report actually carries: the base description, then optional "**Target:**", "**Confidence:**", and "**CVSS:**" lines, followed by the analysis sections ("Technical analysis", "Evidence", "Assumptions", "Counter-evidence", "Conditions that would change severity") rendered as markdown headings. Sections without data are omitted, so a minimal finding gets a short description and a full report gets the complete analysis.
2 changes: 2 additions & 0 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,8 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param
# that key rewrites itself as time passes even though the report never changed.
"Xeol Parser": DEDUPE_ALGO_HASH_CODE,
"OPF Scan": DEDUPE_ALGO_HASH_CODE,
# Strix reports a stable per-finding id (verbatim into vuln_id_from_tool), so dedupe on it directly.
"Strix Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL,
}

# Override the hardcoded settings here via the env var
Expand Down
1 change: 1 addition & 0 deletions dojo/tools/strix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

154 changes: 154 additions & 0 deletions dojo/tools/strix/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import json
import re

from dateutil import parser

from dojo.models import Finding


class StrixParser:

"""
Parser for the vulnerabilities.json report of a Strix security run.

Strix reports one entry per finding, and the set of keys varies by
finding_class (code findings carry PoC and CVSS breakdowns, dependency
findings carry package metadata), so every field is optional.
"""

SEVERITIES = {
"critical": "Critical",
"high": "High",
"medium": "Medium",
"low": "Low",
"info": "Info",
"informational": "Info",
}

CWE_PATTERN = re.compile(r"CWE-(\d+)")

CVSS_METRICS = (
("attack_vector", "AV"),
("attack_complexity", "AC"),
("privileges_required", "PR"),
("user_interaction", "UI"),
("scope", "S"),
("confidentiality", "C"),
("integrity", "I"),
("availability", "A"),
)

DESCRIPTION_SECTIONS = (
("Technical analysis", "technical_analysis"),
("Evidence", "evidence"),
("Assumptions", "assumptions"),
("Counter-evidence", "counterevidence"),
("Conditions that would change severity", "severity_change_conditions"),
)

def get_scan_types(self):
return ["Strix Scan"]

def get_label_for_scan_types(self, scan_type):
return scan_type

def get_description_for_scan_types(self, scan_type):
return "Import findings from the vulnerabilities.json report of a Strix security run."

def get_findings(self, file, test):
data = json.load(file)
# The report is a bare array; the wrapped shape is accepted for
# forward compatibility in case Strix adds report-level metadata.
if isinstance(data, dict):
data = data.get("vulnerabilities")
if not isinstance(data, list):
msg = f"Strix reports are a JSON array; got a {type(data).__name__}."
raise TypeError(msg)
return [self._to_finding(item, test) for item in data if item]

def _to_finding(self, item, test):
dependency = item.get("dependency_metadata") or {}
code_location = (item.get("code_locations") or [{}])[0] or {}

finding = Finding(
test=test,
title=item.get("title"),
severity=self._severity(item.get("severity")),
description=self._description(item),
impact=item.get("impact"),
steps_to_reproduce=self._steps_to_reproduce(item),
mitigation=self._mitigation(item),
cwe=self._cwe(item.get("cwe")),
vuln_id_from_tool=item.get("id"),
date=self._date(item.get("timestamp")),
component_name=dependency.get("package_name"),
component_version=dependency.get("installed_version"),
file_path=code_location.get("file"),
line=code_location.get("start_line"),
static_finding=item.get("finding_class") != "dynamic",
dynamic_finding=item.get("finding_class") == "dynamic",
fix_available=bool(item.get("remediation_steps") or item.get("fix_pr_body")),
)
if item.get("cve"):
finding.unsaved_vulnerability_ids = [item["cve"]]
cvss = item.get("cvss")
if isinstance(cvss, int | float):
finding.cvssv3_score = cvss
return finding

def _severity(self, value):
return self.SEVERITIES.get(str(value).lower(), "Info")

def _cwe(self, value):
if not value:
return None
match = self.CWE_PATTERN.search(str(value))
return int(match.group(1)) if match else None

def _date(self, timestamp):
if not timestamp:
return None
return parser.parse(timestamp)

def _cvss_vector(self, breakdown):
if not isinstance(breakdown, dict) or not breakdown:
return None
metrics = []
for key, abbrev in self.CVSS_METRICS:
value = breakdown.get(key)
if value is None:
return None
metrics.append(f"{abbrev}:{value}")
return "CVSS:3.1/" + "/".join(metrics)

def _description(self, item):
parts = []
if item.get("description"):
parts.append(item["description"])
if item.get("target"):
parts.append(f"**Target:** {item['target']}")
if item.get("confidence"):
parts.append(f"**Confidence:** {item['confidence']}")
cvss = item.get("cvss")
if cvss is not None:
vector = self._cvss_vector(item.get("cvss_breakdown"))
parts.append(f"**CVSS:** {cvss} ({vector})" if vector else f"**CVSS:** {cvss}")
for heading, key in self.DESCRIPTION_SECTIONS:
if item.get(key):
parts.append(f"## {heading}\n{item[key]}")
return "\n\n".join(parts)

@staticmethod
def _steps_to_reproduce(item):
# poc_script_code arrives already wrapped in a fenced code block
parts = [value for value in (item.get("poc_description"), item.get("poc_script_code")) if value]
return "\n\n".join(parts) or None

@staticmethod
def _mitigation(item):
parts = []
if item.get("remediation_steps"):
parts.append(item["remediation_steps"])
if item.get("fix_effort"):
parts.append(f"**Fix effort:** {item['fix_effort']}")
return "\n\n".join(parts) or None
91 changes: 91 additions & 0 deletions unittests/scans/strix/strix_many_vulns.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
[
{
"id": "vuln-0002",
"title": "CVE-2026-48526 in pyjwt 2.12.1 (sample_service)",
"severity": "info",
"timestamp": "2026-09-23 07:22:42 UTC",
"description": "The sample service pins `pyjwt==2.12.1` as a direct dependency in `sample_service/uv.lock`, a version covered by CVE-2026-48526: PyJWT does not validate the use of JSON Web Keys with the HMAC algorithm, allowing an attacker to forge valid tokens. Fixed in 2.13.0.",
"impact": "In this codebase no runtime code path loads the installed pyjwt package, so the demonstrated in-context impact is nil; the finding is reported so the vulnerable pin is remediated.",
"target": "/workspace/sample-app (sample_service)",
"remediation_steps": "Bump the direct dependency from `pyjwt>=2.4.0` to `pyjwt>=2.13.0` and regenerate `uv.lock`.",
"evidence": "Advisory CVE-2026-48526 applies to pyjwt at installed version 2.12.1 and is fixed in 2.13.0. Dependency chain: sample-service@0.1.0 > pyjwt@2.12.1 (direct).",
"assumptions": "Analysis is static; the service was not executed in the sandbox.",
"fix_effort": "trivial",
"cvss": 0.0,
"cve": "CVE-2026-48526",
"cwe": "CWE-347",
"finding_class": "dependency_cve",
"dependency_metadata": {
"package_name": "pyjwt",
"installed_version": "2.12.1",
"advisory_cvss": 7.4,
"package_ecosystem": "pypi",
"manifest_path": "sample_service/pyproject.toml",
"fixed_version": "2.13.0",
"dependency_path": "sample-service@0.1.0 > pyjwt@2.12.1 (direct)",
"reachability": "not_imported_by_application_code",
"reachability_evidence": "No `import jwt` references exist under sample_service/."
},
"agent_id": "a1b2c3d4",
"agent_name": "Dependency Reviewer"
},
{
"id": "vuln-0017",
"title": "Insecure session token generated with os.urandom truncated to 8 bytes",
"severity": "high",
"timestamp": "2026-09-23 08:41:12 UTC",
"description": "The session token is generated by truncating `os.urandom` output to 8 bytes, leaving only 64 bits of entropy before it is hashed.",
"impact": "An attacker with a valid session id can attempt an offline brute force of the remaining token space for accounts whose sessions outlive the short rotation window.",
"target": "/workspace/sample-app",
"technical_analysis": "`server/services/sessions.py` calls `os.urandom(32)[:8]` and passes the result through `hashlib.sha256` before storing it. The truncation happens before hashing, so the stored value carries at most 64 bits of entropy.",
"poc_description": "1. Log in and capture the session cookie.\n2. Replay the cookie from a second client to confirm no server-side binding.\n3. Time an offline search over the 64-bit space on commodity GPU hardware.",
"poc_script_code": "```python\nimport os\n\ntoken = os.urandom(32)[:8]\nprint(f\"session token entropy: {len(token) * 8} bits\")\n```",
"remediation_steps": "Remove the `[:8]` truncation and use the full `os.urandom(32)` value.",
"evidence": "From `server/services/sessions.py` line 42: `token = os.urandom(32)[:8]`.",
"confidence": "high",
"fix_effort": "trivial",
"cvss": 7.5,
"cvss_breakdown": {
"attack_vector": "N",
"attack_complexity": "H",
"privileges_required": "N",
"user_interaction": "N",
"scope": "U",
"confidentiality": "H",
"integrity": "N",
"availability": "N"
},
"cwe": "CWE-331",
"finding_class": "static",
"fix_verification": "The patch removes the truncation; `python3 -m py_compile server/services/sessions.py` passes.",
"fix_pr_body": "Remove the 8 byte truncation of session tokens so the full 256 bits of entropy are retained.",
"code_locations": [
{
"file": "server/services/sessions.py",
"start_line": 42,
"end_line": 44,
"snippet": "token = os.urandom(32)[:8]",
"label": "token generation",
"fix_before": "token = os.urandom(32)[:8]",
"fix_after": "token = os.urandom(32)"
}
],
"agent_id": "e5f6a7b8",
"agent_name": "Auth Reviewer"
},
{
"id": "vuln-0020",
"title": "Verbose error responses leak internal stack traces to end users",
"severity": "low",
"timestamp": "2026-09-23 09:02:10 UTC",
"description": "Unhandled exceptions in the API server return the raw Python traceback to the client when debug mode is left enabled in the default configuration template.",
"impact": "Stack traces disclose internal module paths, dependency versions, and sometimes user data from the failing frame.",
"target": "/workspace/sample-app",
"remediation_steps": "Disable debug mode in the default template and return a generic error body to clients.",
"assumptions": "Deployment configurations that already override the debug flag are not affected.",
"fix_effort": "trivial",
"finding_class": "static",
"agent_id": "e5f6a7b8",
"agent_name": "Auth Reviewer"
}
]
1 change: 1 addition & 0 deletions unittests/scans/strix/strix_no_vuln.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Loading
Loading