@conterra/vuln-scan
v1.0.11
Published
con terra vulnerability scan process
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.
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. Pin a version with `<scanner>@<version>`, e.g. `[email protected]`.
"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
Two summary files are written directly into outDir:
scan-summary.txt- The console "scan summary" output as plain text.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]"] } }
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).
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 - 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:
- 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.
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. |
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.
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.
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. |
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-runBehaviour
- Only entries from
summary.vulnerabilitiesare processed; this map already contains only newly detected CVEs, so previously declared vulnerabilities are never re-processed. - 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.
