@conterra/vuln-scan
v1.0.26
Published
con terra vulnerability scan process
Downloads
2,181
Maintainers
Keywords
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-scanAfter 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 ofct-vuln-add-project.ct-vuln-jira-report- Reads ascan-summary.jsonand 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) andindex.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.2A typical working directory layout:
/vuln-scan/ # working directory
├── vuln-scan-conf.json
├── input/
│ ├── sbom/ # sbom files
│ └── vex/ # vex files
└── output/ # aggregated scan resultsEnvironment 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=trueThese 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 byct-vuln-html-report.index.htmllists every scanned project with links to its output files and per-project report deep-links;report.htmlis the cross-project CVE report. Both load their data fromindex.jsonand must be served over HTTP (see Usage - HTML Report).auto-vex-result.json- Written byct-vuln-auto-vexnext 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-scan → ct-vuln-auto-vex → git 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:
Run the scan with
onNewVulnerabilities: "ignore"so it does not flip the build yellow on its own.After
ct-vuln-auto-vex, readoutput/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 |Emit the warning only when
residualVulnIdsis 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_STATEMENTSremoves 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-vexThe 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.2The 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-SNAPSHOTRemove 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-38820Usage - 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-SNAPSHOTUsage - 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-reportThe 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 outputOpen 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:
- Checks (via JQL) whether an issue with the same vulnerability ID already exists in the target Jira project.
- 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. - 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
doneit is transitioned to statusOpenand re-added to the active sprint of its routed scrum board (when one is configured).
- the missing
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-tokensThese 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ürsections (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:
- Each
matchingProject(an affected project that matches the rule'sreportProjects) resolves to an effective threshold — the strictest (lowest) matchingprojectOverridesentry, falling back toissues[].autoCloseBelowSeverityand finally the top-levelautoCloseBelowSeverity. - 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.
- 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 ifautoCloseBelowSeverityitself 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 helpExamples:
$ ct-vuln-jira-report
$ ct-vuln-jira-report --jira-config ./config/jira-conf.json --summary ./output/scan-summary.json
$ ct-vuln-jira-report --dry-runUsage - 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-markerBehaviour
- Only entries from
summary.vulnerabilitiesare 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 matchingprojectOverridesentry, ormaxSeveritywhen 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 recordedauto_vex_max_severitymarker (or the currentmaxSeverityfor legacy statements without a marker) and (b) the per-project effective threshold fromprojectOverrides. 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 aprojectOverridesentry 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
VexStorealso 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 nextct-vuln-auto-vexcleanup. Manual VEX statements (no marker) are unaffected by both checks. --write-missing-markeris a one-off migration mode: it stampsauto_vex_max_severity=<maxSeverity>into every statement under<vexDir>/<subDir>that lacks anystatus_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-runto preview.- If
<vexDir>/<subDir>/<CVE-ID>.jsonalready 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 configuredimpactStatement. Nojustificationis set. - Affected scan project identifiers (
name@version) are used as the product@id, matching whatVexStoreexpects when populated from the scan summary.
Contributing
See AGENTS.md for toolchain, build, lint and test conventions.
