Vulnetix Code Scanner
OSS (AGPL-3.0) · Vulnetix/cli · CLI docs · Maintained by Vulnetix, whose other AppSec products are commercial; the CLI itself is open source · Free Community VDB access; paid VDB plans for higher rate limits.
Rule registry · 300+ built-in
VNX-*Rego rules ship in the CLI · Canonical registry of external rule packs: vulnetix.com/rules-registry · Load any registered pack via--rule org/repo(override the resolution host with--rule-registry) · Vulnetix/community-rules is one of the registered packs · Built-in rule test cases: Vulnetix/sast-rule-evals (Apache-2.0)
What Vulnetix does
The Vulnetix CLI is a single binary that runs five evaluators against the same source tree in one invocation: SCA (dependency vulnerabilities), SAST (300+ built-in Rego rules across 20+ languages, plus external rule packs via --rule org/repo), secrets, containers (Dockerfile / Containerfile), IaC (Terraform, Nix flakes), and licence compliance. It produces a CycloneDX 1.7 SBOM, a SARIF 2.1 findings report, and a state file — all in one run, in the same .vulnetix/ directory. The CLI also embeds a vulnerability-database client (vulnetix vdb) for ad-hoc CVE lookups, fix suggestions, and remediation-plan generation against MITRE, NVD, CISA KEV, EPSS, OSV, GHSA, OS-distribution advisories, and 70+ other sources.
Reading the output
Three artefacts in .vulnetix/:
sbom.cdx.json— CycloneDX 1.7 SBOM. The canonical input to CycloneDX VEX. Each entry undercomponents[]has apurl,version,hashes[], and licensing metadata;dependencies[]carries the resolved graph (top-level → transitive). SCA findings appear asvulnerabilities[]entries cross-referencing componentbom-refs.sast.sarif— SARIF 2.1.0. The canonical input to OpenVEX. Eachruns[].results[]entry carriesruleId(under theVNX-*namespace),level,locations[].physicalLocation(file path and line),message, and a CWE mapping. SAST, secrets, IaC, and Dockerfile findings all land here.memory.yaml— scan metadata: timestamps, finding counts, git context, scan version. Used by--from-memoryto reconstruct a previous run without hitting the API.
What you can act on
Every field below is greppable with jq against the artefact. The recipes here are the ones you’ll keep reaching for during triage.
From the SBOM (.vulnetix/sbom.cdx.json)
components[].purl— the canonical identity of a component. Use this asaffects[].refin CycloneDX VEX.components[].version,components[].hashes[]— for pinning and integrity.dependencies[]— the resolved dependency graph as{ref, dependsOn[]}records. Walk it backwards to find the top-level dep that pulled in a transitive.vulnerabilities[](when present inline) — Vulnetix-resolved advisory metadata:id,source,ratings[],affects[],analysis.
# List every component as {purl, version, name}
jq '.components[] | {purl, version, name}' .vulnetix/sbom.cdx.json
# Find one specific component by name
jq '.components[] | select(.name == "log4j-core")' .vulnetix/sbom.cdx.json
# Find every component matching a PURL prefix (e.g. all npm packages)
jq '.components[] | select(.purl | startswith("pkg:npm/")) | .purl' .vulnetix/sbom.cdx.json
# Walk the dep graph forward — what does X depend on?
jq --arg ref "log4j-core@2.14.1" \
'.dependencies[] | select(.ref == $ref) | .dependsOn' \
.vulnetix/sbom.cdx.json
# Walk the dep graph BACKWARD — what pulled X in? (the triage query)
jq --arg target "log4j-core@2.14.1" \
'.dependencies[] | select(.dependsOn | index($target)) | .ref' \
.vulnetix/sbom.cdx.json
# List every vulnerability with its severity and the PURLs it affects
jq '.vulnerabilities[] | {
id,
severity: .ratings[0].severity,
affects: [.affects[].ref]
}' .vulnetix/sbom.cdx.json
# Filter to critical only
jq '.vulnerabilities[] | select(.ratings[]?.severity == "critical")' \
.vulnetix/sbom.cdx.json
# All PURLs affected by one specific CVE
jq --arg cve "CVE-2021-44228" \
'.vulnerabilities[] | select(.id == $cve) | .affects[].ref' \
.vulnetix/sbom.cdx.json
From vdb vuln per CVE (vulnetix vdb vuln <id>)
The CLI’s enriched vuln-detail response carries two fields that the on-disk SBOM doesn’t — useful for reachability work and detection-rule selection.
containers.adp[0].x_affectedRoutines— deduplicated list of affected functions and files. Aggregates per-affected-entryprogramRoutinesandprogramFilesfrom the CVE record withx_affectedFunctions. The canonical “what to grep your codebase for” list.containers.adp[0].x_attackPaths— tactic → technique mapping (each technique hasid,name,relation). Populated alongsidecontainers.cna.taxonomyMappings. Useful for picking which detection rules to deploy and for IR playbooks.
# Cache the vuln-detail response once per CVE
vulnetix vdb vuln CVE-2021-44228 --output json > /tmp/cve.json
# Affected functions/files — the grep targets
jq '.[0].containers.adp[0].x_affectedRoutines' /tmp/cve.json
# → [
# { "kind": "function", "name": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup" },
# { "kind": "file", "path": "log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/JndiLookup.java" },
# ...
# ]
# Pull just the names for direct grep input
jq -r '.[0].containers.adp[0].x_affectedRoutines[]
| select(.kind == "function") | .name' /tmp/cve.json \
| xargs -I{} git grep -nE '\b{}\b' src/
# Attack paths — tactic → techniques
jq '.[0].containers.adp[0].x_attackPaths' /tmp/cve.json
# → [
# { "tactic": "Initial Access",
# "techniques": [
# { "id": "T1190", "name": "Exploit Public-Facing Application", "relation": "primary_method" }
# ] },
# { "tactic": "Execution",
# "techniques": [
# { "id": "T1059", "name": "Command and Scripting Interpreter", "relation": "post_exploit" }
# ] }
# ]
# Map an attack path to detection-rule selection
jq -r '.[0].containers.adp[0].x_attackPaths[]
| .techniques[] | .id' /tmp/cve.json \
| while read tid; do
vulnetix vdb snort-rules list --technique "$tid" --severity high
done
From the SARIF (.vulnetix/sast.sarif)
runs[].results[].ruleId— theVNX-<lang>-<n>identifier. The same ID resolves on docs.cli.vulnetix.com — the rule page is the source of truth for bad pattern, good pattern, and remediation.runs[].results[].locations[].physicalLocation.artifactLocation.uri+region.startLine— where in source.runs[].results[].properties.cwe— for cross-referencing classifications.
# Every finding flattened to {ruleId, level, file, line, message}
jq '.runs[].results[] | {
ruleId,
level,
file: .locations[0].physicalLocation.artifactLocation.uri,
line: .locations[0].physicalLocation.region.startLine,
message: .message.text
}' .vulnetix/sast.sarif
# Count findings per rule
jq '[.runs[].results[].ruleId]
| group_by(.)
| map({rule: .[0], count: length})
| sort_by(-.count)' .vulnetix/sast.sarif
# Filter to one rule family (secrets, IaC, containers, language-specific)
jq '.runs[].results[]
| select(.ruleId | startswith("VNX-SEC-"))' .vulnetix/sast.sarif
# Find a specific rule's findings
jq '.runs[].results[]
| select(.ruleId == "VNX-JAVA-001")' .vulnetix/sast.sarif
# Pull CWE classification per finding
jq '.runs[].results[] | {
ruleId,
cwe: (.properties.cwe // [])
}' .vulnetix/sast.sarif
Reachability — two paths
Vulnetix offers two complementary reachability workflows. Pick the one that matches the cost you’re willing to pay and the audience the evidence has to convince.
Path A — CLI-driven (recommended default)
For most findings, the Vulnetix CLI’s --reachability flag is the fastest evidence path. The CLI fetches precise per-CVE detection patterns from the VDB and evaluates them locally against your source using language-aware grammars for 17 languages: JavaScript, TypeScript, TSX, Python, Go, Java, Ruby, Rust, C, C++, C#, PHP, Swift, Kotlin, Scala, Bash, and Lua. Matches land under x_reachability in the JSON output — split into direct (your first-party code) and transitive (vendored or fetched dep source).
# Per-CVE reachability evidence — direct + transitive
vulnetix vdb vuln CVE-2021-44228 --reachability=both --output json \
| jq '.[0].x_reachability'
# Whole-scan: reachability merged into every SCA finding
vulnetix scan --reachability=both --severity high
# Pull just the matched call-sites for one CVE from a SARIF scan artefact
jq --arg cve "CVE-2021-44228" \
'.runs[].results[]
| select(.ruleId == $cve)
| .properties.x_reachability.direct[]?
| { file: .file, line: .line, evidence: .evidence }' \
.vulnetix/sast.sarif
# Disable reachability entirely (fall back to package-presence only)
vulnetix scan --reachability=off
Modes available on --reachability=<mode>:
| Mode | Behaviour |
|---|---|
direct | Match only first-party source files. Fastest. |
transitive | Walk dependency source where available. Catches the case where a transitive dep is the one calling through to the affected routine. |
both | Union of direct + transitive. Default. |
off | Disable reachability entirely; fall back to Tier-1 package-presence findings only. |
This is Tier 3 evidence — semantic intent-to-use — graded against the three-tier model. Cost: seconds to a minute per repo. Treat it as the SSVC Engineer Triage Reachability input on every scan.
Path B — manual Tier 1 / 2 / 3 evidence (when you need to defend an audit)
When Path A’s verdict isn’t enough — typically for KEV-listed criticals on the audit critical path, or for analysis.detail text in a VEX statement that a third-party will review — fall through to the full three-tier evidence collection documented in the reachability deep-dive. The per-language manual recipes (Tier 1 linkage commands, Tier 2 call-graph tooling, Tier 3 framework-wiring inspection) remain unchanged and complement Path A: the CLI gives you the headline answer, the manual workflow gives you the auditor-grade narrative.
Use Path B when the SSVC decision is Act, the finding is on the critical path, and an auditor or incident responder will read the VEX analysis.detail. The manual x_affectedRoutines + git grep workflow described above is also part of Path B for users who want full control over the matching step.
Decision tree
Two decisions to make, in order. The first is about what action to take — prioritise this finding via SSVC Engineer Triage. The second is about what format to record it in — CycloneDX VEX or OpenVEX.
Action — Engineer Triage
Vulnetix’s vdb vuln returns the CISA Coordinator SSVC decision (Act / Attend / Track* / Track), but Coordinator answers a coordinator’s question (whether to issue an advisory), not a developer’s. The developer’s framework is Engineer Triage — four inputs (reachability, remediation, mitigation, priority) producing one of four actions: NIGHTLY_AUTO_PATCH, BACKLOG, SPIKE_EFFORT, DROP_TOOLS. See the SSVC appendix for the framework, the inputs, and the decision-tree summary.
Pull the Coordinator output to inform the priority input of Engineer Triage:
vulnetix vdb vuln <CVE-ID> --output json \
| jq '.[0].containers.adp[0] | {
coordinator: .x_ssvc.decision,
exploitation: .x_exploitationMaturity.level,
kev: .x_kev.knownRansomwareCampaignUse,
epss: .x_exploitationMaturity.factors.epss
}'
Format — CycloneDX VEX vs OpenVEX
Vulnetix emits both an SBOM and SARIF in the same run, so the format choice splits along the artefact line.
For SCA findings (sourced from `sbom.cdx.json`):
→ CycloneDX VEX entry referencing the PURL from the SBOM
For SAST / secrets / IaC / Dockerfile findings (sourced from `sast.sarif`):
→ OpenVEX statement, subject is the repo at the scanned commit
Need a WAF / IPS / SIEM mitigation rather than a code fix?
(Engineer Triage's MitigationOption = INFRASTRUCTURE — Vulnetix supplies the rule)
vulnetix vdb traffic-filters # Snort / Suricata IPS signatures per CVE
vulnetix vdb snort-rules get # idem, richer filtering on classtype / port / content
vulnetix vdb nuclei get # Nuclei templates for exploit verification
vulnetix vdb iocs # IOC pivots (IPs, ASNs, ATT&CK techniques)
Then: status is `affected` with `workaround_available` and the rule reference
Category guides
Each finding category has its own walkthrough — what the finding looks like, how to trace it to a root cause, the exact fix patterns, and the VEX statement to produce afterwards.
- SCA — dependency vulnerabilities — every package manager, lockfile mechanics during patching, transitive-dependency coercion, reachability analysis per language. Worked examples on Log4Shell, jsonwebtoken, and the xz-utils backdoor.
- SAST — static analysis — the
VNX-*rule namespace, per-language worked examples (Java, Python, Node.js, Go, PHP, Ruby, C#, Rust), plus the cross-cuttingVNX-CRYPTO-*,VNX-JWT-*,VNX-LLM-*rules. - Secrets — all 32
VNX-SEC-*rules, the five-step rotation playbook, worked examples for AWS keys, GitHub PATs, and private keys. - Containers — all 8
VNX-DOCKER-*rules, plus image-layer scanning that crosses back into SCA. - IaC — all 8
VNX-TF-*rules for Terraform, plus Nix flake support. - Licence compliance — the five finding types, the six-step resolution pipeline, allow-list configuration.
CI invocation
vulnetix:
stage: test
script:
- vulnetix scan
--output .vulnetix/sbom.cdx.json
--output .vulnetix/sast.sarif
--severity high
--block-malware
artifacts:
paths:
- .vulnetix/sbom.cdx.json
- .vulnetix/sast.sarif
- .vulnetix/memory.yaml
expire_in: 90 days
- name: Vulnetix scan
run: |
vulnetix scan \
--output .vulnetix/sbom.cdx.json \
--output .vulnetix/sast.sarif \
--severity high \
--block-malware
- name: Upload scan artefacts
uses: actions/upload-artifact@v4
with:
name: vulnetix
path: .vulnetix/
retention-days: 90
# Validate which files would be scanned without touching the API
vulnetix scan --dry-run
# Full scan, all evaluators, quieter output
vulnetix scan --results-only
# Re-process a previous scan's SBOM without re-resolving
vulnetix scan --from-memory
Gating flags reference
Flags that turn a finding into a non-zero exit code, useful in CI to block merges.
| Flag | Behaviour |
|---|---|
--severity low|medium|high|critical | Exit 1 if any finding meets or exceeds the threshold |
--block-malware | Exit 1 if a known malicious package is in the dependency graph |
--block-eol | Exit 1 if a runtime or package is past end-of-life |
--block-unpinned | Exit 1 if any direct dependency uses a version range rather than a pin |
--exploits poc|active|weaponized | Exit 1 if any finding has exploit maturity at or above the level |
--version-lag N | Exit 1 if a dependency is N or more releases behind the latest |
--cooldown N | Exit 1 if a dependency was published within the last N days (mitigation for typosquats) |
Producing a CycloneDX VEX
SCA findings tie cleanly to SBOM components, so the VEX can be embedded inside the same CycloneDX document or shipped alongside, referencing the SBOM by serialNumber. Example for the Log4Shell finding once it’s been resolved by upgrading:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:c2b7e9c1-1a44-4f1f-bf3a-1b9e02f76d61",
"vulnerabilities": [
{
"id": "CVE-2021-44228",
"source": { "name": "NVD", "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" },
"ratings": [{ "source": { "name": "NVD" }, "severity": "critical", "method": "CVSSv3" }],
"affects": [
{
"ref": "pkg:maven/org.apache.logging.log4j/log4j-core@2.17.1",
"versions": [
{ "version": "2.14.1", "status": "affected" },
{ "version": "2.17.1", "status": "unaffected" }
]
}
],
"analysis": {
"state": "resolved",
"detail": "Pinned log4j-core to 2.17.1 in pom.xml's <dependencyManagement>. Confirmed via mvn dependency:tree that no transitive still resolves a vulnerable version. See MR !128."
}
}
]
}
Producing an OpenVEX
For SAST, secrets, IaC, and Dockerfile findings — the SARIF entries that don’t tie back to an SBOM component. Subject is the repo at a specific commit; vulnerability.name combines the VNX- rule ID with the CWE.
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://github.com/yourorg/yourrepo/vex/2026-05-14-sast-001.json",
"author": "developer@example.com",
"timestamp": "2026-05-14T10:00:00Z",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "VNX-JAVA-001",
"description": "Command injection via Runtime.exec() — CWE-78. See https://docs.cli.vulnetix.com/docs/sast-rules/vnx-java-001/"
},
"products": [
{
"@id": "https://github.com/yourorg/yourrepo",
"identifiers": { "purl": "pkg:github/yourorg/yourrepo@abc1234" }
}
],
"status": "fixed",
"action_statement": "Replaced Runtime.getRuntime().exec(\"convert \" + filename + \" output.png\") with ProcessBuilder(\"convert\", filename, \"output.png\") and added an allow-list validation on filename in MR !55."
}
]
}
Capability snapshot
See the capability matrix for the full side-by-side. Vulnetix’s row is the baseline; the matrix is honest about the two areas where the baseline has drawbacks:
- Reachability: Tier 3 semantic + intent-to-use, not call-graph. Catches framework wiring / reflection / DI patterns that Tier-2 SAST tools miss; less precise on traditional code paths than CodeQL or Snyk SAST. Surfaced via the CLI’s
--reachability=direct\|transitive\|both\|offflag (17 languages) — see Reachability — two paths for the CLI-driven workflow alongside the manual Tier-1/2/3 evidence path. - Container scanning: unpacked-layer + packages-inside, not binary-image analysis. See Vulnetix containers for the model.
Everywhere else, Vulnetix is the most-feature-complete tool in the matrix:
- Database quality: Vulnetix VDB — full feed coverage plus first-party enrichment.
- Exploit maturity:
ACTIVE/POC/WEAPONISEDlevels + honeypot sightings + CrowdSec community sightings + IOC pivots. - EOL: native
lifecycleStageper dep/runtime/base-image plus--block-eolgate. - Supply-chain threats: proactive typosquat detection, AI-malware family detection, maintainer-health (OpenSSF Scorecard + account age + 2FA),
dep-add-guardpre-add risk gate. - Outputs: CycloneDX SBOM, SPDX SBOM, SARIF, CycloneDX VEX, OpenVEX, detection rules (Snort/YARA/Nuclei/Sigma).