npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@conterra/vuln-scan

v1.0.26

Published

con terra vulnerability scan process

Downloads

2,181

Readme

ct-vuln-scan

The utility is a wrapper around the following tools:

  • grype - Vulnerability scanner for container images and filesystems.
  • trivy - Comprehensive and versatile security scanner.
  • oss index - Free catalogue of open source components and scanning tools to help developers identify vulnerabilities.

It also supports the OpenVex specification for vulnerability status and justification.

For an architectural overview see ARCHITECTURE.md. For build, test and contribution guidelines see AGENTS.md.

Pre-Requisites

You need to have docker and node (>= 20) installed on your machine.

Installation

The utility is published as an npm package and can be installed globally or locally.

# global
$ npm install -g @conterra/vuln-scan

# local
$ npm install @conterra/vuln-scan

After installation the following commands are available:

  • ct-vuln-scan - Triggers the vulnerability scan.
  • ct-vuln-add-vex - Interactive helper to add a new vex statement to a file.
  • ct-vuln-add-project - Adds a new project to the configuration file based on an existing project entry and adds it to all vex statements.
  • ct-vuln-remove-project - Removes a project from the configuration file and all vex statements (or only from vex statements matching a given vulnerability id).
  • ct-vuln-add-project-to-vex - Updates vex files to include a new project version. Already part of ct-vuln-add-project.
  • ct-vuln-jira-report - Reads a scan-summary.json and creates Jira issues for newly detected vulnerabilities.
  • ct-vuln-auto-vex - Auto-creates OpenVEX statements for newly detected vulnerabilities below a configurable severity threshold.
  • ct-vuln-html-report - Post-processes the scan output directory into a browsable report: index.html (project index with links to all output files and per-project report deep-links), report.html (cross-project CVE report) and index.json (data manifest). Served over HTTP.

Configuration

Create a vuln-scan-conf.json file in the working directory.

Minimal configuration:

{
    "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/conf-schema.json",
    "projects": [
        {
            "name": "mapapps",
            "version": "4.18.2",
            "purl": "pkg:maven/de.conterra.mapapps/[email protected]",
            "sbomFile": "./input/sboms/mapapps-4.18.2.cdx.json"
        }
    ]
}

Full configuration with all available options and their defaults:

{
    "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/conf-schema.json",

    // Action on newly detected vulnerabilities: "fail" | "warn" | "ignore"
    // - "fail": exits with code 1
    // - "warn": logs ##vso[task.complete result=SucceededWithIssues;]
    // - "ignore": no effect
    "onNewVulnerabilities": "fail",
    // Action on vulnerabilities no longer detected, same values as above.
    "onNoLongerDetectedVulnerabilities": "ignore",
    // Maven repository to fetch sboms from.
    // Set MAVEN_REPO_USER / MAVEN_REPO_PW for authentication.
    "mavenRepo": "https://repository.conterra.de/repository/maven-mirror-ct",
    // Scanners to use. `grype` and `trivy` run as Docker images (anchore/grype,
    // aquasec/trivy). Pin a version (tag) with `<scanner>@<version>`, e.g.
    // `[email protected]`. Pin the image by digest with `<scanner>@sha256:<64 hex>`,
    // or combine both as `<scanner>:<version>@sha256:<64 hex>`. Digest pinning is
    // intended to be maintained automatically by Renovate.
    "scanners": ["grype", "trivy", "ossindex"],
    // (optional) URLs of additional vex files to download.
    "vexUrls": ["https://example.com/my-vex.json"],
    // Directory containing vex files.
    "vexDir": "./input/vex",
    // Directory for scan output.
    "outDir": "./output",
    // Cache directory for grype/trivy databases.
    "cacheDir": "./cache",
    // If true, results for each project are written to a sub-directory of outDir.
    "createSubDirsForProject": true,
    // Console output style:
    //   "BY_PROJECT_DETAILED" (default) - group by project, print CVE details
    //   "BY_PROJECT"                    - group by project, no CVE details
    //   "BY_CVE"                        - group by CVE, no CVE details
    //   "BY_CVE_DETAILED"               - group by CVE, print CVE details
    "consoleReport": "BY_PROJECT_DETAILED",
    "projects": [
        {
            "name": "mapapps",
            "version": "4.18.3",
            // identifier used to match vex statements
            "purl": "pkg:maven/de.conterra.mapapps/[email protected]",
            // maven coordinates used to download the sbom from mavenRepo
            "sbomMavenCoordinates": "de.conterra.mapapps:ct-mapapps-rollout:4.18.3:sbom"
        },
        {
            "name": "mapapps",
            "version": "4.18.2",
            "purl": "pkg:maven/de.conterra.mapapps/[email protected]",
            // local sbom file
            "sbomFile": "./input/sboms/mapapps-4.18.2.cdx.json",
            // (optional) skip this project
            "enabled": true,
            // (optional) scan but never break the build
            "silent": false,
            // (optional) only break the build for severities >= this value.
            // One of: "CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"
            "minimumSeverity": "HIGH"
        }
    ]
}

Usage - Scan

Run from the directory containing vuln-scan-conf.json:

# scan all projects
$ ct-vuln-scan

# scan all versions of one project
$ ct-vuln-scan mapapps

# scan a specific project version
$ ct-vuln-scan mapapps 4.18.2

A typical working directory layout:

/vuln-scan/              # working directory
├── vuln-scan-conf.json
├── input/
│   ├── sbom/            # sbom files
│   └── vex/             # vex files
└── output/              # aggregated scan results

Environment variables

# (optional) maven repo authentication for sbom download
MAVEN_REPO_USER=[username]
MAVEN_REPO_PW=[password]

# (optional) sonatype OSS Index authenticated API
OSS_INDEX_API_USER=[username]
OSS_INDEX_API_TOKEN=[token]
OSS_INDEX_BASE_URL=[url]   # default: https://api.guide.sonatype.com (since 1.0.8)

# (optional) authentication for vex files referenced via vexUrls
VEX_URLS_USER=[username]
VEX_URLS_PW=[password]

# (optional) helps with the trivy DB GitHub rate limit
# https://aquasecurity.github.io/trivy/v0.38/docs/references/troubleshooting/#github-rate-limiting
TRIVY_GITHUB_TOKEN=[token]

# (optional) verbose logging
VERBOSE=true

# (optional) if "true", a project's product-id is automatically removed
# from vex statements covering vulnerabilities no longer detected for that project.
AUTO_REMOVE_UNUSED_STATEMENTS=true

These variables can also be defined in a .env file in the working directory.

Output files

Three summary files are written directly into outDir:

  • scan-summary.txt - The console "scan summary" output as plain text. New vulnerabilities are annotated with a VEX-lookup hint so it is clear that "new" means "no project-level VEX entry yet":

    • (VEX entry missing) - the VEX store contains a statement for this CVE (for another product), but none that applies to this project. Adding/importing a project-level entry will silence it.
    • (VEX file missing) - the CVE has not been triaged anywhere in the VEX store yet.
  • scan-summary.json - Machine-readable summary, for post-processing or downstream tools. Example (truncated):

    {
        "errors": [
            {
                "project": "[email protected]",
                "error": "Failed to fetch sbom file ..."
            }
        ],
        "vulnerabilities": {
            "CVE-2025-48988": {
                "id": "CVE-2025-48988",
                "severity": "HIGH",
                "title": "tomcat: Apache Tomcat DoS in multipart upload",
                "description": "...",
                "components": ["pkg:maven/org.apache.tomcat.embed/[email protected]"],
                "refs": ["https://avd.aquasec.com/nvd/cve-2025-48988"]
            }
        },
        "vulnerabilityToProject": {
            "CVE-2025-48988": ["[email protected]"]
        },
        "projectToVulnerability": {
            "[email protected]": ["CVE-2025-48988"]
        },
        "noLongerDetectedVulnerabilities": {
            "CVE-2023-52070": ["[email protected]"]
        }
    }
  • index.json / index.html / report.html - Written by ct-vuln-html-report. index.html lists every scanned project with links to its output files and per-project report deep-links; report.html is the cross-project CVE report. Both load their data from index.json and must be served over HTTP (see Usage - HTML Report).

  • auto-vex-result.json - Written by ct-vuln-auto-vex next to the scan summary. It records which of the new vulnerabilities the run covered, so a pipeline can decide the build result after post-processing (see Keeping the build green after auto-vex). Example:

    {
        "residualVulnIds": [],
        "coveredVulnIds": ["CVE-2026-12143", "CVE-2026-13149"],
        "partialVulnIds": [],
        "created": ["CVE-2026-12143", "CVE-2026-13149"],
        "revokedProducts": 0,
        "errors": []
    }

For each scanned project, raw scanner output, an aggregated JSON, a SARIF file (used by the Azure DevOps SARIF tab) and the matching OpenVEX statements are written to a <project-name>-<project-version>/ sub-directory (when createSubDirsForProject is true). See ARCHITECTURE.md §5 for the full file layout.

Azure DevOps Pipelines integration

Publish the output directory as build artifact CodeAnalysisLogs:

- task: PublishBuildArtifacts@1
  inputs:
      PathtoPublish: "output"
      ArtifactName: "CodeAnalysisLogs"

The SARIF SAST Scans Tab extension visualizes the .sarif.json files in the artifact.

If onNewVulnerabilities is set to warn, the pipeline will be marked as SucceededWithIssues when new vulnerabilities are found. With the default fail, the pipeline fails (exit code 1).

Keeping the build green after auto-vex

A pipeline that runs ct-vuln-scanct-vuln-auto-vexgit commit fixes most of its own findings: everything below the auto-VEX threshold receives a VEX statement and is committed in the same run. If the scan is left to emit the SucceededWithIssues result itself, the run stays yellow forever — Azure DevOps cannot take a task.complete back, and the scan runs before auto-vex.

ct-vuln-scan therefore reports facts only and knows nothing about any post-processing step. To keep such a run green, let the pipeline decide the result after ct-vuln-auto-vex:

  1. Run the scan with onNewVulnerabilities: "ignore" so it does not flip the build yellow on its own.

  2. After ct-vuln-auto-vex, read output/auto-vex-result.json. It lists the vulnerabilities that survived post-processing:

    | Field | Meaning | | ----------------- | ------------------------------------------------------------------------- | | coveredVulnIds | covered for every affected project — will not re-surface next scan | | partialVulnIds | a statement is written but at least one project stays uncovered | | residualVulnIds | not fully covered (partial + none) — what the next scan will still report |

  3. Emit the warning only when residualVulnIds is non-empty, e.g.:

    pnpm scan            # onNewVulnerabilities: "ignore" -> stays silent
    pnpm auto-vex        # writes output/auto-vex-result.json
    residual=$(jq '.residualVulnIds | length' output/auto-vex-result.json)
    if [ "$residual" -gt 0 ]; then
        echo "##vso[task.logissue type=warning;]NEW_VULNERABILITIES_DETECTED"
        echo "##vso[task.complete result=SucceededWithIssues;]"
    fi

This keeps the scan and the post-processor fully decoupled: the pipeline (or the auto-vex step) owns the verdict, and the direction of knowledge only ever flows from post-processing to the scan's output — never the reverse.

Notes:

  • The residual vulnerabilities remain visible in scan-summary.json, the SARIF report and Jira regardless of the pipeline verdict.
  • No-longer-detected vulnerabilities that AUTO_REMOVE_UNUSED_STATEMENTS removes in the same run can be treated the same way: the removal is already committed, so the pipeline need not warn about them.

Usage - Add Vex

Experimental maintenance workflow for recording a decision about whether a vulnerability affects a project.

$ ct-vuln-add-vex

The interactive prompt creates a CVE-<id>.json file in vexDir, e.g.:

{
    "@context": "https://openvex.dev/ns/v0.2.0",
    "@id": "https://openvex.dev/docs/public/vex-fc763e6e...",
    "author": "conterra",
    "timestamp": "2024-10-01T18:54:23+02:00",
    "last_updated": "2024-10-01T19:58:13+02:00",
    "version": 2,
    "statements": [
        {
            "vulnerability": { "name": "CVE-2023-52070" },
            "timestamp": "2024-10-01T19:58:13+02:00",
            "products": [
                { "@id": "pkg:maven/de.conterra.mapapps/[email protected]" },
                { "@id": "pkg:maven/de.conterra.mapapps/[email protected]" }
            ],
            "status": "not_affected",
            "impact_statement": "<short rationale>"
        }
    ]
}

For details see the OpenVex spec.

Valid status values:

| Status | Description | | ------------------- | -------------------------------------------------------------------------- | | not_affected | The product is known to be not affected by this vulnerability. | | affected | The product is known to be affected by this vulnerability. | | fixed | The product contains a fix for this vulnerability. | | under_investigation | It is not yet known whether the product is affected; still being assessed. |

Valid justification values when status is not_affected (full text in the VEX Status Justification PDF):

| Justification | Short description | | ------------------------------------------------- | ------------------------------------------------------------ | | component_not_present | Vulnerable component is not present in the product. | | vulnerable_code_not_present | Vulnerable code is excluded by configuration or build. | | vulnerable_code_not_in_execute_path | Vulnerable code is shipped but never called. | | vulnerable_code_cannot_be_controlled_by_adversary | Attacker cannot influence the vulnerable code path. | | inline_mitigations_already_exist | Built-in, non-disable-able mitigations prevent exploitation. |

Usage - Add Project

Add a new project entry to the configuration and propagate it to vex statements:

$ ct-vuln-add-project <reference-name> <reference-version> <new-version>

# example: clone [email protected] into a new [email protected] entry
$ ct-vuln-add-project mapapps 4.18.2-SNAPSHOT 4.18.2

The new entry inherits all options from the reference project, and its product-id is added to every vex statement that already references the reference project.

Usage - Remove Project

Remove a project entry from the configuration and all matching vex statements:

$ ct-vuln-remove-project <name> <version>

$ ct-vuln-remove-project mapapps 4.18.2-SNAPSHOT

Remove project from vex files matching a vulnerability id

Useful when a single vulnerability is no longer relevant for a project but the project itself remains:

$ ct-vuln-remove-project <name> <version> <vuln-id>

$ ct-vuln-remove-project mapapps 4.18.2-SNAPSHOT CVE-2024-38820

Usage - Add Project to Vex

Add a new project version to all vex statements that already reference an existing version. Already part of ct-vuln-add-project; useful when the configuration entry already exists.

$ ct-vuln-add-project-to-vex <existingPurl> <newPurl>

$ ct-vuln-add-project-to-vex \
    pkg:maven/de.conterra.mapapps/[email protected] \
    pkg:maven/de.conterra.mapapps/[email protected]

Usage - Simulate a release

Combine the project commands to simulate releasing 4.18.3 and preparing the next dev version 4.18.4-SNAPSHOT:

$ ct-vuln-add-project mapapps 4.18.3-SNAPSHOT 4.18.3
$ ct-vuln-remove-project mapapps 4.18.3-SNAPSHOT
$ ct-vuln-add-project mapapps 4.18.3 4.18.4-SNAPSHOT

Usage - HTML Report

ct-vuln-html-report post-processes the scan output directory and writes three artefacts into it:

  • index.json - a machine-readable manifest listing every scanned project (including projects without vulnerabilities) and all generated output files (SBOM, aggregated JSON/SARIF, per-scanner JSON, OpenVEX).
  • index.html - a landing page listing every project with direct links to all of its output files, a per-project deep-link into the report (report.html#project=<name@version>, which auto-filters the report for that project) and a link to the full cross-project report.
  • report.html - the cross-project CVE report.
$ ct-vuln-html-report

The output directory is read from the outDir setting in vuln-scan-conf.json (default ./output), i.e. the same directory ct-vuln-scan wrote to. Project identity (name, version and purl) is likewise taken from vuln-scan-conf.json in the current working directory, which is therefore required. Using the configuration as the authoritative source keeps identities stable even for versions that contain a - (e.g. 1.6.3-SNAPSHOT), which cannot be reconstructed reliably from output file names alone. A project is included when it was actually scanned in the run (i.e. its *-scan-aggregate-results.json exists); configured-but-disabled projects are skipped.

Both HTML pages load their data at runtime: index.html fetches index.json, and report.html fetches index.json plus each project's *-scan-aggregate-results.json in parallel and computes the cross-project view in the browser. Because browsers block fetch() for file:// URLs, the output directory must be served over HTTP rather than opened directly from disk, e.g.:

$ npx serve output            # then open http://localhost:3000/index.html
# or
$ python -m http.server -d output

Open index.html for the project index, or report.html for the full report. The report lets you:

  • see all CVEs of a project and their assessments (VEX statements),
  • filter by CVE (id, title, project or component) across all projects,
  • filter to a single project - either via the dropdown or by opening report.html#project=<name@version> (the selected project is kept in the URL hash so the view can be shared),
  • hide all CVEs that already have an assessment ("Hide assessed CVEs"),
  • click a CVE to quickly find every affected project (with its purl), its dependency tree and assessment.

A CVE is shown as assessed when every affected project (respecting the active project filter) carries a VEX statement; otherwise it is open.

Usage - Jira Report

ct-vuln-jira-report reads scan-summary.json produced by ct-vuln-scan and creates Jira issues for new vulnerabilities. For each vulnerability the tool: produced by ct-vuln-scan and creates Jira issues for new vulnerabilities. For each vulnerability the tool:

  1. Checks (via JQL) whether an issue with the same vulnerability ID already exists in the target Jira project.
  2. If not, creates a new issue with a structured description (CVE details, affected artefacts and products, placeholders for assessment and remediation) and tags every currently-affected scan project as a proj:<name>_v<version> label on the issue.
  3. If an issue already exists, the project tags are compared with the current scan result. For every newly-affected project the issue is updated:
    • the missing proj: label is added,
    • a comment is posted listing only the additional projects (without re-emitting the CVE details),
    • and if the issue is currently in status category done it is transitioned to status Open and re-added to the active sprint of its routed scrum board (when one is configured).

Project tags use the form proj:<name>_v<version> (the @ separator is replaced with _v to keep labels JQL-friendly). On the very first run after upgrading, existing Jira issues will not yet carry these labels, so every currently-matching project will be added at once and a single follow-up comment will be posted.

When an issue is updated because new projects became affected, any addToBoard issueLabels that would now match (per the same first-sprint-wins / Kanban-aggregate rules used at creation) are added to the issue retroactively. Labels already present on the issue are not re-emitted. Sprint placement is not changed for issues that are still in progress; only when a closed issue is re-opened is it placed in the active sprint of whichever scrum entry currently matches first — which may differ from the original sprint if the routing config has been edited since the issue was created.

Severity-change handling

When the live severity reported for a CVE differs from the severity encoded in the existing issue's title (typically because the upstream feed rerated it, or because an auto-VEX statement was just invalidated and the CVE resurfaced), the reporter will:

  • update the issue summary to the new [SEVERITY] CVE-ID: … form,
  • post a short German comment explaining the change,
  • re-open the issue (and re-add to the active sprint) if it is currently closed and the new severity is not below autoCloseBelowSeverity. Closed sub-threshold issues stay closed but still get retitled and commented.

Issues whose summary was hand-edited and no longer matches the [SEVERITY] CVE-ID shape are silently skipped for severity-change detection.

Environment variables

[email protected]   # Jira account e-mail
JIRA_API_TOKEN=<token>       # https://id.atlassian.com/manage-profile/security/api-tokens

These can also be set in a .env file in the working directory.

Configuration - jira-conf.json

{
    "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/jira-config-schema.json",
    "jiraUrl": "https://myorg.atlassian.net",
    "issues": [
        {
            "jiraProject": "PLATFORM",
            "issueType": "CVE",
            "issueLabels": ["security"],
            "addToBoard": [
                {
                    "sprintBoardId": 10,
                    "issueLabels": ["team-a"],
                    "matchProjects": ["mapapps@*"]
                },
                {
                    "issueLabels": ["team-b"],
                    "matchProjects": ["smartfinder@*"]
                }
            ],
            "reportProjects": ["mapapps@*", "smartfinder@4.*"]
        },
        {
            "jiraProject": "OTHER",
            "issueType": "Bug",
            "reportProjects": ["*"]
        }
    ]
}

| Field | Required | Description | | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | jiraUrl | yes | Base URL of the Jira instance. | | issues | yes | Routing rules. Each rule is evaluated independently, so a vulnerability can be reported to multiple projects. | | issues[].jiraProject | yes | Jira project key, e.g. PLATFORM. | | issues[].issueType | yes | Jira issue type, e.g. Bug, CVE. | | issues[].reportProjects | yes | Glob patterns (* wildcard) matched against scan project identifiers (name@version). | | issues[].issueLabels | no | Labels added to every issue created by this rule, merged with labels from matching addToBoard entries. | | issues[].addToBoard | no | Per-board routing rules; see below. | | addToBoard[].sprintBoardId | no | Scrum board id. If set, the issue is assigned to the board's active sprint via customfield_10020. | | addToBoard[].issueLabels | no | Labels contributed by this entry. | | addToBoard[].matchProjects | no | Glob patterns to activate this entry. Defaults to ["*"]. | | autoCloseBelowSeverity | no | Default auto-close threshold inherited by every issues[] entry that does not override it. One of UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL. | | issues[].autoCloseBelowSeverity | no | Per-rule override of the top-level threshold. | | issues[].projectOverrides | no | Per-project overrides of this rule's autoCloseBelowSeverity. See below. |

addToBoard resolution: only entries whose matchProjects cover the vulnerability's affected scan projects are considered. The first matching scrum entry (with sprintBoardId) claims the sprint; further scrum entries are ignored. Kanban entries (no sprintBoardId) all contribute their labels.

Auto-close low-severity issues

When autoCloseBelowSeverity (top-level or per-issue rule) is set, newly-created issues whose vulnerability severity is strictly below the threshold are immediately transitioned to a status whose Jira status category is done (the first such transition exposed by the workflow is used, regardless of the status's display name and locale). The board/sprint assignment, labels and comment described above still happen before the transition, so the issue is reachable on the configured board even though it starts out closed. A short German-language comment is added explaining the auto-closure (see below).

For existing issues, the threshold only suppresses re-opening: when a closed sub-threshold issue gains a newly-affected project, the project label and "Zusätzlich gemeldet für" comment are added as usual, but the issue is not re-opened (and never re-closed). Issues at or above the threshold keep the existing re-open + re-add-to-active-sprint behavior.

Threshold semantics mirror ct-vuln-auto-vex: with "HIGH", severities MEDIUM, LOW and UNKNOWN are auto-closed, while HIGH and CRITICAL stay open.

Auto-acknowledgement comments

The Jira reporter posts one of three German comment shapes depending on the per-project resolver outcome (see projectOverrides below for the bucket definitions):

  • Auto-close comment — posted after a successful close transition. Lists every project that was auto-acknowledged together with its effective threshold:

    Automatisch geschlossen: Schwere MEDIUM liegt unterhalb der konfigurierten Auto-Close-Schwelle.
    Auto-akzeptiert für:
     - [email protected] (Schwelle HIGH)
     - [email protected] (Schwelle CRITICAL)
  • Partial-ack comment (Teilweise auto-akzeptiert) — posted on a newly-created issue that stays open because the issue's matching projects split across both buckets. Names which projects are implicitly auto-acknowledged and which still require manual evaluation:

    Teilweise auto-akzeptiert. Schwere HIGH.
    Auto-akzeptiert für:
     - [email protected] (Schwelle CRITICAL)
    Manuelle Bewertung erforderlich für:
     - [email protected] (Schwelle HIGH)

    The comment is suppressed when all matching projects fall in a single bucket (the auto-close path covers all-auto-acknowledged; the all-manual case posts nothing).

  • Reopen-reason append — when a closed sub-threshold issue is re-opened because newly-affected projects raise the bar, the consolidated reopen-reason comment is extended with the same Auto-akzeptiert für / Manuelle Bewertung erforderlich für sections (scoped to the newly added projects). The reporter does not post a second comment for the bucket split.

Projects that have no effective threshold configured (no projectOverrides match, no rule-level / top-level autoCloseBelowSeverity) are omitted from these listings — they neither block nor enable the auto-close decision in any new way.

issues[].projectOverrides — per-project stricter thresholds

projectOverrides mirrors the projectOverrides block of auto-vex-conf.json, so the same LTSS / regular-release-line policy can be expressed in both tools. Each entry binds one severity threshold to a list of glob patterns matched against the name@version project identifier (same shape used in vuln-scan-conf.json#projects[] and in Jira reportProjects). Only * wildcards are supported.

For every vulnerability covered by a rule:

  1. Each matchingProject (an affected project that matches the rule's reportProjects) resolves to an effective threshold — the strictest (lowest) matching projectOverrides entry, falling back to issues[].autoCloseBelowSeverity and finally the top-level autoCloseBelowSeverity.
  2. The issue's threshold is the strictest across all matchingProjects. A new issue is therefore only auto-closed when the vulnerability is strictly below the threshold for every matching project; any project that still requires deeper analysis keeps the issue open.
  3. Existing closed sub-threshold issues are never re-opened by projectOverrides — but raising the bar (e.g. via an LTSS override) above the live severity does re-open them, just as if autoCloseBelowSeverity itself had been raised.

Example mirroring the matching auto-vex-conf.json:

{
    "jiraProject": "PLATFORM",
    "issueType": "CVE",
    "reportProjects": ["*"],
    "autoCloseBelowSeverity": "CRITICAL",
    "projectOverrides": [
        {
            "projects": ["[email protected].*", "[email protected].*"],
            "autoCloseBelowSeverity": "HIGH",
            "comment": "LTSS means to analyze 'HIGH and CRITICAL'"
        }
    ]
}

With this rule, a MEDIUM CVE that affects only [email protected] is auto-closed (below HIGH); the same CVE bleeding into a non-LTSS project keeps the issue open if its effective threshold treats MEDIUM as actionable.

CLI

Options:
  -c, --jira-config <path>   Path to jira-conf.json (default: ./jira-conf.json)
  -s, --summary <path>       Path to scan-summary.json (default: ./output/scan-summary.json)
      --dry-run              Print what would be created without calling Jira
  -h, --help                 Show help

Examples:

$ ct-vuln-jira-report
$ ct-vuln-jira-report --jira-config ./config/jira-conf.json --summary ./output/scan-summary.json
$ ct-vuln-jira-report --dry-run

Usage - Auto VEX

ct-vuln-auto-vex is a post-processor for ct-vuln-scan. It auto-creates OpenVEX statements for newly detected vulnerabilities whose severity is strictly below a configurable threshold, reducing manual triage noise for low-impact CVEs.

Generated files are written to <vexDir>/<subDir>/<CVE-ID>.json and are picked up automatically by the next scan run (the vex store recurses into sub-directories). Manual VEX files in vexDir always take precedence: any existing statement covering a CVE suppresses it from the "new vulnerabilities" list before this tool runs.

Configuration - auto-vex-conf.json

Place this file next to vuln-scan-conf.json.

{
    "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/auto-vex-config.schema.json",
    "impactStatement": "Auto-acknowledged: no evidence of exploitability in our deployment context.",
    "maxSeverity": "HIGH",
    "author": "conterra",
    "subDir": "auto"
}

| Field | Required | Default | Description | | ------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | impactStatement | yes | - | impact_statement written into every auto-generated VEX statement. | | maxSeverity | no | "HIGH" | Exclusive threshold; CVEs strictly below this severity get a statement. One of UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL. With the default, MEDIUM is auto-vexed but HIGH is not. | | author | no | "conterra" | author field of the OpenVEX document. | | subDir | no | "auto" | Sub-directory of vexDir for generated files. | | projectOverrides | no | [] | Per-project stricter thresholds. See below. |

projectOverrides — per-project stricter thresholds

By default the global maxSeverity applies uniformly. projectOverrides lets you raise the bar for selected projects so they require manual review at lower severities. Each entry binds one severity threshold to a list of glob patterns matched against the name@version project identifier (same shape used in vuln-scan-conf.json#projects[] and in Jira reportProjects). Only * wildcards are supported. If several entries match the same project, the strictest (lowest) maxSeverity wins.

{
    "impactStatement": "Auto-acknowledged: no evidence of exploitability.",
    "maxSeverity": "CRITICAL",
    "projectOverrides": [
        {
            "projects": ["[email protected].*"],
            "maxSeverity": "HIGH",
            "comment": "Stricter review for the upcoming release line."
        }
    ]
}

With the example above, all projects auto-VEX everything below CRITICAL, but for [email protected] only severities below HIGH are auto-VEXed — HIGH and CRITICAL keep reaching manual triage (and the Jira reporter, since they remain in scan-summary.json).

When a single CVE is associated with both a "strict" and a "lax" project, the generated statement covers only the lax projects' PURLs. The strict project's PURL is left out, so the CVE re-surfaces for that project on the next scan and flows into Jira normally. Adding a new (or tighter) override on a subsequent run invalidates any pre-existing auto-VEX product whose owning project now resolves to a stricter effective threshold — even if the live severity itself hasn't changed.

Usage

# defaults: ./auto-vex-conf.json + ./output/scan-summary.json + vexDir from vuln-scan-conf.json
$ ct-vuln-auto-vex

# custom paths
$ ct-vuln-auto-vex --auto-vex-config ./config/auto-vex-conf.json --summary ./output/scan-summary.json

# preview without writing files
$ ct-vuln-auto-vex --dry-run

# one-off: backfill the auto_vex_max_severity marker into pre-marker files
$ ct-vuln-auto-vex --write-missing-marker

Behaviour

  • Only entries from summary.vulnerabilities are processed; this map already contains only newly detected CVEs, so previously declared vulnerabilities are never re-processed.
  • Each generated statement records the authoritative severity threshold via status_notes: "auto_vex_max_severity=<threshold>", where <threshold> is the effective per-project threshold for the PURLs in that specific statement (the strictest matching projectOverrides entry, or maxSeverity when none matches). When a single CVE affects multiple projects whose effective thresholds differ, one statement is emitted per threshold bucket so the marker stays accurate per PURL. The marker is parsed by the scanner and the auto-vex tool itself on subsequent runs.
  • Severity-escalation invalidation: on every run, all statements under <vexDir>/<subDir> are inspected. For each product on each statement, the effective threshold is the strictest of (a) the recorded auto_vex_max_severity marker (or the current maxSeverity for legacy statements without a marker) and (b) the per-project effective threshold from projectOverrides. If the live severity is no longer strictly below that threshold, the product is removed (and the file is deleted if no products remain). This means adding or tightening a projectOverrides entry invalidates pre-existing auto-VEX products for the matched projects on the next run, even when the live severity hasn't changed.
  • During the next scan, the matcher in VexStore also skips auto VEX statements whose recorded threshold is exceeded — so an escalated CVE re-surfaces on the same run that detects the escalation, even before the next ct-vuln-auto-vex cleanup. Manual VEX statements (no marker) are unaffected by both checks.
  • --write-missing-marker is a one-off migration mode: it stamps auto_vex_max_severity=<maxSeverity> into every statement under <vexDir>/<subDir> that lacks any status_notes. Existing markers are preserved; unrelated manual notes are left alone (with a warning). The invalidation pass and statement creation are skipped in this mode; combine with --dry-run to preview.
  • If <vexDir>/<subDir>/<CVE-ID>.json already exists, the new statement is appended (document version is bumped). Manual edits in the same file are preserved.
  • The generated statement uses status: "not_affected" and the configured impactStatement. No justification is set.
  • Affected scan project identifiers (name@version) are used as the product @id, matching what VexStore expects when populated from the scan summary.

Contributing

See AGENTS.md for toolchain, build, lint and test conventions.