8000
Skip to content
VulnFuse shield logo

VulnFuse

Stop triaging the same vulnerability three times.

Local-first, explainable correlation for SARIF, Trivy, Grype, Snyk, CycloneDX, OpenVEX, OSV-Scanner, and CSV reports.

CI CodeQL Apache-2.0 No upload

Try the browser workbench · Why this exists · How matching works · Supported fields

VulnFuse browser workbench correlating reports from Trivy, Grype, and Snyk

Security tools rarely describe the same issue in the same way. One scanner reports a CVE against a package URL, another uses a vendor advisory and an image layer, and a third emits a SARIF rule at a file location. Counting rows inflates the queue; blindly deduplicating them can hide real differences.

VulnFuse converts those reports into one canonical evidence model, scores plausible pairs, blocks unsafe merges, and keeps every source record attached to the resulting cluster. It is a correlation layer—not a scanner, vulnerability database, or false-positive oracle.

What makes it useful

  • Eight input families, five outputs. Read SARIF 2.1, Trivy JSON, Grype JSON, Snyk JSON, CycloneDX VDR/VEX, standalone OpenVEX, OSV-Scanner JSON, and ordinary CSV. Write VulnFuse JSON, SARIF, CSV, Markdown, or one self-contained interactive HTML file.
  • Every merge is reviewable. Match edges retain the score, confidence, evidence, and exact reasons such as a shared CVE, PURL, asset, location, rule, or scanner fingerprint.
  • Conflicts are first-class. Explicitly different vulnerability IDs, packages, assets, or finding kinds can block a merge even when titles look similar.
  • A bridge cannot bypass a conflict. Candidate edges are considered strongest-first, and two clusters join only after every cross-cluster member pair passes the same hard-blocker policy.
  • Two honest scopes. instance keeps different assets separate. root-cause can connect the same vulnerable component across images, repositories, or applications.
  • Baseline-aware gates. Compare previous and current reports as new, updated, unchanged, or absent, then fail CI only when a genuinely new cluster crosses your severity threshold.
  • Producer drift stays visible. A stable scanner name and report count no longer hide changed or missing embedded producer-version evidence.
  • SARIF analysis identity stays visible. A category change or lost category on run.automationDetails.id is reported even when the tool, version, and finding set stay unchanged.
  • Alias-rich clusters stay within GitHub's SARIF rule-tag guidance. Exported rules keep at most nine display/filter tags while the result retains every parsed identifier and reports how many identifier tags were omitted.
  • Hosted SARIF text stays ingestible without hiding the source. Rule names are bounded to 255 UTF-16 code units and rule descriptions/result messages to 1,024, while exact over-limit originals remain in clearly named VulnFuse properties.
  • Locationless package evidence can be hosted deliberately. An explicit repository-relative fallback anchors only results that lack a physical file location, without replacing scanner locations or pretending the anchor is finding provenance.
  • SARIF suppression stays auditable. Preserve every suppression kind, status, and justification, but exclude a cluster from severity gates only when every source record is effectively suppressed.
  • SARIF outcomes remain distinct. Keep valid pass, informational, and notApplicable records as reviewable non-finding evidence instead of counting them as active vulnerabilities.
  • Portable SARIF paths correlate. Apply validated relative uriBaseId chains before matching file assets, while omitting producer-specific absolute roots and warning on malformed chains.
  • Source-report identity survives checkout moves. CLI globs and the Action label input reports relative to the working tree, so runner-specific roots do not leak into exports or change finding IDs.
  • CycloneDX JSON and XML take the same path. Consume supported VDR/VEX evidence directly from either standard serialization without a separate conversion runtime.
  • Scanner disagreement becomes measurable. See what each tool found alone, what several tools shared, and the pairwise overlap instead of comparing misleading raw totals.
  • A report people can actually review. Portable HTML needs no server or CDN and includes local search, severity/state/asset/scanner/coverage/disposition filters, evidence, blockers, and every source record.
  • No report upload. The hosted workbench runs entirely in the browser. The CLI and Action run in your own environment. No AI, API key, telemetry, or remote correlation service is required.
  • Deterministic output. Identical input and policy yield stable finding and cluster IDs, which makes diffs and CI review practical.

Quick start

Browser

Open the hosted workbench, drop two or more current reports, inspect the proposed clusters and scanner-overlap tables, and export the result. Add optional reports from a previous run to see a local baseline comparison. Choose Load safe demo to explore correlation, scanner divergence, and baseline states without using your own data.

CLI from a release

VulnFuse currently requires Node.js 22.12 or newer. Install the two checksummed v0.4.24 packages directly from the GitHub release:

npm install --global https://github.com/CAOShurong/vulnfuse/releases/download/v0.4.24/vulnfuse-core-0.4.24.tgz https://github.com/CAOShurong/vulnfuse/releases/download/v0.4.24/vulnfuse-0.4.24.tgz
vulnfuse --version

The paired install matters because the CLI and shared core are separate packages. Every release also includes SHA256SUMS.txt, a CycloneDX SBOM generated from a fresh install of those exact CLI/core archives, and a separate CycloneDX SBOM for the bundled Action runtime.

Download the assets and manifest into one directory, then verify every downloaded byte with a common checksum tool:

sha256sum -c SHA256SUMS.txt
# macOS: shasum -a 256 -c SHA256SUMS.txt

For online provenance verification, use a current GitHub CLI and constrain the expected repository, workflow, tag, and hosted-runner environment:

gh attestation verify vulnfuse-0.4.24.tgz \
  --repo CAOShurong/vulnfuse \
  --signer-workflow CAOShurong/vulnfuse/.github/workflows/release.yml \
  --source-ref refs/tags/v0.4.24 \
  --deny-self-hosted-runners

A matching checksum detects changed bytes only when you trust the manifest. The GitHub/Sigstore attestation adds build identity and provenance; neither check proves that the source, runner, dependencies, or program are vulnerability-free or safe for a particular use. This repository does not currently enable GitHub immutable releases, so gh release verify and gh release verify-asset are not claimed as valid verification paths for these assets.

CLI from source

git clone https://github.com/CAOShurong/vulnfuse.git
cd vulnfuse
npm ci
npm run build

node packages/cli/dist/index.js merge \
  trivy.json grype.json snyk.json \
  --format markdown \
  --output vulnfuse-report.md

Use a quoted glob when scanner jobs produce many reports. VulnFuse expands it consistently instead of relying on the current shell:

vulnfuse merge "reports/**/*.json" --format html --output vulnfuse-review.html

Use forward slashes in portable patterns, including on Windows. Existing paths are treated literally before glob syntax, overlapping paths are deduplicated, directories and symbolic-link traversal are excluded, and an unmatched pattern fails instead of silently producing an empty result. The 1,000-report limit is applied after expansion, but a very broad pattern can still spend time traversing its starting directory tree; scope patterns to the report directory.

Generated artifacts label reports inside the CLI working directory or GITHUB_WORKSPACE with a forward-slash relative path. An input outside that root is labeled external-report/<basename>; duplicate outside basenames gain a deterministic ordinal based on sorted input order. The actual path remains available in local diagnostics but is not copied into generated JSON, SARIF, CSV, Markdown, or HTML merely because it was the input filename. Scanner data can still contain its own sensitive paths. Moving from v0.4.22 or earlier can cause a one-time finding/cluster ID change when a prior label was absolute; pin the old release if alert continuity matters more than removing that path.

Inspect formats before merging:

node packages/cli/dist/index.js inspect trivy.json grype.json

Stream one input and fail CI when a high-severity active cluster remains:

cat osv-results.json | node packages/cli/dist/index.js merge - trivy.json \
  --scope root-cause \
  --format sarif \
  --output vulnfuse-results.sarif \
  --fail-on high

GitHub code scanning requires a physical location to display a SARIF result, while package-oriented OpenVEX and CycloneDX evidence often has no source file. If the repository contains a real tracked manifest or evidence file, opt in to using it only as a navigation anchor:

vulnfuse merge reports/openvex.json \
  --format sarif \
  --sarif-fallback-location package-lock.json \
  --output reports/vulnfuse.sarif

The path must be a safe forward-slash repository-relative URI. VulnFuse applies it at line 1 only where the source finding has no physical URI and records vulnfuseLocationProvenance: user-supplied-fallback; scanner-supplied locations remain unchanged. It does not open the path or prove that it exists, is tracked in the uploaded revision, contains the vulnerable component, or caused the finding. Without this option, locationless output remains locationless. The option is rejected for non-SARIF formats.

Run node packages/cli/dist/index.js merge --help for all policy and safety options.

For SARIF, --fail-on and --fail-on-new apply only to active clusters. A valid pass, informational, or notApplicable result with level: none or no explicit level is non-finding evidence; missing kind defaults to fail. A cluster is non-finding only when every member is non-finding. Separately, a non-empty suppression list containing only accepted or omitted statuses is effectively suppressed; any underReview, rejected, malformed metadata, or active corroborating record keeps the cluster active. Unknown, non-string, or contradictory result kinds also warn and remain active. These decisions trust producer metadata; VulnFuse does not rerun a rule, prove a check passed, validate applicability, or prove a finding safe or false.

VulnFuse also warns when SARIF run metadata says the producer failed, emitted an error-level tool/configuration notification, or did not provide inline results. Available partial findings remain visible. Add --fail-on-incomplete to inspect, merge, or diff when CI must return exit code 1 for that uncertainty; merge and diff still finish and atomically write the requested report first. This is an opt-in gate because partial results can still be useful. A report without run-health metadata is not proof that the scan was complete, and VulnFuse does not fetch external SARIF property files.

For SARIF file locations, VulnFuse prepends valid relative segments from artifactLocation.uriBaseId and run.originalUriBaseIds before correlation. It stops at an omitted/redacted or absolute producer root, so src/ can become part of a portable repository-relative path without copying file:///C:/Users/... into the canonical record. Unknown, circular, or invalid bases warn and leave the original URI unchanged. This does not map %SRCROOT% to a local checkout, perform complete absolute URI resolution, open source files, or prove that two workspaces have the same layout.

Report-input and runtime failures print one concise diagnostic to stderr and return exit code 1. Add the global --debug option before the command when a stack trace is needed, for example vulnfuse --debug inspect report.json. Debug output can include local filesystem paths, so review it before sharing logs publicly.

The CLI and Action reject a file whose known size exceeds --max-bytes before reading its contents, then enforce the same limit while reading to cover file growth and unusual file types. Accepted report text, parsed findings, correlation state, and exports still consume additional memory; the byte limit is per report and is not a total-process memory or processing-time guarantee.

Both Node entry points write a requested report to a unique temporary sibling, flush it, and rename it into place only after the write succeeds. A reported write failure therefore leaves an existing complete destination untouched. A hard process or host failure can still leave a .vulnfuse-*.tmp sibling, and rename/crash durability depends on the local filesystem; this is not a power-loss or network-filesystem guarantee.

Compare current reports with a previous run and block only newly introduced high-severity clusters. Repeat --baseline for each previous scanner report:

node packages/cli/dist/index.js diff \
  --baseline previous/trivy.json \
  --baseline previous/grype.json \
  current/trivy.json current/grype.json \
  --format markdown \
  --output vulnfuse-baseline.md \
  --fail-on-new high \
  --fail-on-scan-set-change

The comparison preserves matched evidence and labels every cluster as new, updated, unchanged, or absent. It also records added or removed scanner tools, changed per-tool report counts, changed or missing embedded producer-version evidence, and changed or missing SARIF automation-category evidence in scanSetChange. A detected change prints a warning by default because new or absent states may reflect coverage drift; --fail-on-scan-set-change returns exit code 1 only after the complete comparison is written. Versions and categories are compared as literal producer-supplied evidence sets; VulnFuse does not order them or infer an upgrade, broader scope, or safer run. Matching names, counts, versions, and categories still do not prove that the asset, configuration, rules, vulnerability database, or scan completion was identical. SARIF export writes the standard baselineState field for every result and the scan-set evidence in invocation properties.

Create a single portable report that a reviewer can open offline, search, filter, and expand without installing VulnFuse or sending evidence to a server:

node packages/cli/dist/index.js diff \
  --baseline previous/trivy.json \
  current/trivy.json current/grype.json \
  --format html \
  --output vulnfuse-review.html

The file contains its own styles and interaction code, makes no network requests, escapes report-controlled content, and keeps cluster and source-record evidence visible. Protect it like the original reports.

GitHub Action

The Action accepts paths or newline-separated glob patterns. Generate scanner reports in earlier steps, correlate them, then upload the result as SARIF or retain it as an artifact.

- name: Correlate scanner evidence
  id: vulnfuse
  uses: CAOShurong/vulnfuse@v0.4.24
  with:
    reports: |
      reports/trivy.json
      reports/grype.json
      reports/osv.json
    output: reports/vulnfuse.sarif
    format: sarif
    scope: instance
    fail-on: high
    fail-on-incomplete: "true"
    # Needed only when package/VEX results have no physical location.
    sarif-fallback-location: package-lock.json

- name: Upload correlated SARIF
  if: always()
  uses: github/codeql-action/upload-sarif@v4
  with:
    sarif_file: reports/vulnfuse.sarif

VulnFuse exports no more than nine properties.tags values for each SARIF rule, following GitHub's conservative troubleshooting guidance while avoiding the documented hard ceiling. The stable security and finding-kind tags come first; identifier tags are ordered by relationship and value. Every parsed identifier remains in results[].properties.identifiers, and vulnfuseOmittedIdentifierTagCount records any identifiers omitted from the rule tags. Both plain and baseline-comparison SARIF use the same rule builder.

Hosted code-scanning platforms impose text limits that the general SARIF 2.1 schema does not. VulnFuse conservatively counts JavaScript UTF-16 code units, never splits a surrogate pair, and adds an ellipsis within the limit: 255 for reportingDescriptor.name, and 1,024 for shortDescription.text, fullDescription.text, and result.message.text. When text is shortened, the exact value remains in vulnfuseOriginalName, vulnfuseOriginalShortDescription, vulnfuseOriginalFullDescription, or vulnfuseOriginalMessage; vulnfuseTruncatedFields lists shortened rule fields. GitHub or GitLab may ignore custom properties, so retain the SARIF/JSON artifact when the full text matters. These bounds prevent documented incompatibilities; they do not emulate every current ingestion rule or validate permissions, product enablement, compressed file size, result/rule/location counts, URI lengths, or upload success.

The optional line-1 fallback location is similarly narrow. It addresses GitHub's documented display requirement but is not a schema validator or hosted-upload emulator. Use a stable file that exists in the analyzed revision, and treat the anchor as workflow metadata rather than scanner evidence.

To gate only new findings, download or otherwise provide the previous raw scanner reports and add:

baseline-reports: previous-reports/*.json
fail-on-new: high
fail-on-scan-set-change: "true"

When a baseline is supplied, the selected output format contains the comparison instead of a plain correlation report. The Action warns when the scanner set changes or SARIF declares incomplete execution, and can fail after preserving the report. It writes a job summary and exposes findings, clusters, active, suppressed, non-finding, duplicates-collapsed, single-tool, multi-tool, new, updated, absent, unchanged, scan-set-changed, incomplete-reports, and report outputs.

Supported input

Format Parsed evidence Important boundary
SARIF 2.1 runs, tools, health, rules, kinds, portable URI-base locations, suppressions Absolute producer roots are omitted; malformed bases preserve raw paths
Trivy JSON vulnerabilities, misconfigurations, secrets, packages, image targets, fixes Table and template output are not report inputs
Grype JSON matches, artifacts, PURLs, locations, advisories, fixes The JSON schema has changed over time; fixtures cover the current common shape
Snyk JSON legacy vulnerabilities, identifiers, dependency paths, fixes Snyk Code SARIF should be supplied as SARIF
CycloneDX JSON/XML components, vulnerabilities, ratings, affects, analysis/VEX context Supported-field mapping only; no XSD validation, DTD, entity, or signature processing
OpenVEX JSON-LD products, subcomponents, vulnerability aliases, status, justification, actions Status is retained as evidence, never trusted as a suppression verdict
OSV-Scanner JSON sources, packages, aliases, affected ranges, fixed events Scanner output is accepted; arbitrary OSV records need the result wrapper
CSV common ID, severity, component, PURL, asset, location, rule, and fix columns Header aliases are documented in FORMATS.md

Matching at a glance

The default threshold is 70. Evidence adds points; incompatible evidence adds hard blockers.

Evidence Score
Shared vulnerability identifier +40
Stable fingerprint from the same scanner +55
Same canonical component or PURL +25
Same asset +15
Same file and nearby line +10 to +15
Same rule ID +10
Same finding kind +5
Similar meaningful title tokens up to +10 by default

An explicit mismatch is not “negative points”; it can stop the merge. That conservative rule matters because a shared package does not make two different CVEs the same vulnerability. See MATCHING.md for the complete policy, candidate indexing, confidence labels, and examples.

Repository layout

packages/core    canonical model, parsers, matching, correlation, exporters
packages/cli     Node.js command-line interface
packages/action  bundled Node 24 GitHub Action
apps/web         local-only React/Vite workbench deployed to GitHub Pages
docs             format contract, algorithm, architecture, and threat model

Security and data handling

Scanner reports can contain repository paths, package inventories, hostnames, code locations, and sometimes secret fragments. Treat them as sensitive.

  • The hosted app has no upload endpoint and makes no API call to process a report.
  • File size, report count, pair-comparison, output overwrite, symlink traversal, URL scheme, and CSV formula-injection safeguards are built in.
  • Uploaded strings are rendered as React text, not injected HTML.
  • A correlation result does not prove that a vulnerability is exploitable, reachable, fixed, or a false positive.
  • A SARIF suppression is producer-supplied review state. VulnFuse preserves and applies it to gates; it does not independently validate the justification or mutate an alert in GitHub or another platform.
  • A SARIF non-finding kind is producer-supplied rule outcome. VulnFuse preserves and applies it to local gates; it does not rerun the check or prove the outcome or applicability.
  • An OpenVEX status is a producer assertion. VulnFuse preserves it for review but does not authenticate the author, verify an attestation, prove reachability, or convert not_affected or fixed into a quiet gate bypass.
  • A user-supplied SARIF fallback location is a navigation aid. VulnFuse labels it, never overwrites scanner locations, and does not claim that the finding originated in the selected file.
  • GitHub code scanning does not document result.kind in its supported SARIF subset. VulnFuse therefore omits non-finding clusters from exported results[] and retains them under run.properties.nonFindingClusters; GitHub will not display those retained property records as alerts.

Read THREAT_MODEL.md before using untrusted reports in automation. Report suspected vulnerabilities through the private process in SECURITY.md.

Project status

v0.4.24 is a public, reasonably mature v0.4 release for the documented workflows. It provides explainable, cluster-safe cross-scanner correlation; standalone OpenVEX and CycloneDX JSON/XML VEX input; three-state SARIF disposition; portable URI-base paths; bounded hosted-SARIF rule tags and visible text; and explicit labeled fallback anchors for otherwise locationless hosted SARIF. It also includes incomplete-run gates, scanner coverage, scan-set-aware baselines, and self-contained offline HTML review across the core library, CLI, browser workbench, and GitHub Action. Source-report labels and IDs are portable across checkout roots for the same relative workflow layout. Trivy container SARIF also retains its run-level image digest as canonical product evidence, allowing same-product OpenVEX statements to be reviewed in the same active cluster without treating their status as a gate verdict.

Release checksums and GitHub build-provenance attestations establish byte integrity and tag-workflow provenance under their documented trust assumptions, not software safety. Correlation, VEX assertions, suppressions, result kinds, scanner versions/categories, URI bases, hosted-text bounds, and fallback anchors remain evidence with the limitations described above; none independently proves ground truth, exploitability, scan completeness, source causation, or hosted upload acceptance. Tests use synthetic cross-format fixtures plus pinned public OpenVEX, SARIF, CycloneDX, Microsoft SARIF Tutorials, and BinSkim samples, but real vendor output varies by version. The project is now in evidence-driven maintenance: new feature work should start from a sanitized user report, a failing CI workflow, a security report, or a reproducibility regression rather than speculative version churn. Please open a sanitized format compatibility issue when a legitimate report is not parsed correctly, or use Discussions for a sanitized workflow report. Independent adoption and compatibility with every vendor version are not claimed.

Contributing

The most useful contribution is a small, sanitized report fixture with the expected canonical fields. Read CONTRIBUTING.md before submitting data; never attach proprietary inventories, tokens, or live secrets.

Apache-2.0 licensed. Built to complement scanners and vulnerability-management systems, not replace them.

About

Local-first, explainable correlation for SARIF, Trivy, Grype, Snyk, CycloneDX, OSV and CSV vulnerability reports.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

0