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 under components[] has a purl, version, hashes[], and licensing metadata; dependencies[] carries the resolved graph (top-level → transitive). SCA findings appear as vulnerabilities[] entries cross-referencing component bom-refs.
  • sast.sarif — SARIF 2.1.0. The canonical input to OpenVEX. Each runs[].results[] entry carries ruleId (under the VNX-* 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-memory to 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 as affects[].ref in 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-entry programRoutines and programFiles from the CVE record with x_affectedFunctions. The canonical “what to grep your codebase for” list.
  • containers.adp[0].x_attackPaths — tactic → technique mapping (each technique has id, name, relation). Populated alongside containers.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 — the VNX-<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.

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>:

ModeBehaviour
directMatch only first-party source files. Fastest.
transitiveWalk dependency source where available. Catches the case where a transitive dep is the one calling through to the affected routine.
bothUnion of direct + transitive. Default.
offDisable 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.

Decision tree
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-cutting VNX-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.

FlagBehaviour
--severity low|medium|high|criticalExit 1 if any finding meets or exceeds the threshold
--block-malwareExit 1 if a known malicious package is in the dependency graph
--block-eolExit 1 if a runtime or package is past end-of-life
--block-unpinnedExit 1 if any direct dependency uses a version range rather than a pin
--exploits poc|active|weaponizedExit 1 if any finding has exploit maturity at or above the level
--version-lag NExit 1 if a dependency is N or more releases behind the latest
--cooldown NExit 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:

CycloneDX VEX outcome
{
  "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.

OpenVEX outcome
{
  "@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:

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 / WEAPONISED levels + honeypot sightings + CrowdSec community sightings + IOC pivots.
  • EOL: native lifecycleStage per dep/runtime/base-image plus --block-eol gate.
  • Supply-chain threats: proactive typosquat detection, AI-malware family detection, maintainer-health (OpenSSF Scorecard + account age + 2FA), dep-add-guard pre-add risk gate.
  • Outputs: CycloneDX SBOM, SPDX SBOM, SARIF, CycloneDX VEX, OpenVEX, detection rules (Snort/YARA/Nuclei/Sigma).

See also