@conterra/vuln-scan 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -124
- package/dist/vuln-scan.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -32,30 +32,30 @@ or reference it in your `package.json` file:
|
|
|
32
32
|
|
|
33
33
|
```json
|
|
34
34
|
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@conterra/vuln-scan": "<current version>"
|
|
37
|
+
}
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
After that you have following commands available:
|
|
42
42
|
|
|
43
43
|
- `ct-vuln-scan`
|
|
44
|
-
|
|
44
|
+
- Triggers the vulnerability scan
|
|
45
45
|
- `ct-vuln-add-vex`
|
|
46
|
-
|
|
46
|
+
- Triggers a process to add a new vex statement to a file
|
|
47
47
|
- `ct-vuln-add-project`
|
|
48
|
-
|
|
48
|
+
- Adds a new project to the configuration file based on an existing project entry and adds it to all vex statements
|
|
49
49
|
- `ct-vuln-remove-project`
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
- Removes a project from the configuration file and all vex statements related to the project.
|
|
51
|
+
- Or removes a project from all vex statements matching a given vulnerability id.
|
|
52
52
|
- `ct-vuln-add-project-to-vex`
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
- Update vex files to include a new project version. The project is added to each vex file that references the existing project version given.
|
|
54
|
+
- This step is already included in the `ct-vuln-add-project` command, but it can be run separately to update the vex files for an already existing project entry in the configuration file.
|
|
55
55
|
- `ct-vuln-jira-report`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
- Reads a `scan-summary.json` produced by `ct-vuln-scan` and creates Jira issues for newly detected vulnerabilities.
|
|
57
|
+
- Issues are only created when no existing issue with the same vulnerability ID already exists in the target Jira project.
|
|
58
|
+
- Routing rules in a `jira.json` file control which scan projects are reported to which Jira projects.
|
|
59
59
|
|
|
60
60
|
### Vulnerability scan
|
|
61
61
|
|
|
@@ -81,70 +81,67 @@ In the output folder different files per project are generated, see below.
|
|
|
81
81
|
There are also tow summary files directly in the output folder:
|
|
82
82
|
|
|
83
83
|
- `scan-summary.txt`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
- A text file with a summary of the scan results.
|
|
85
|
+
- It contains the console "scan summary" output, which is printed to the console during the scan.
|
|
87
86
|
- `scan-summary.json`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"CVE-2025-48988": {
|
|
100
|
-
"id": "CVE-2025-48988",
|
|
101
|
-
"severity": "HIGH",
|
|
102
|
-
"title": "tomcat: Apache Tomcat DoS in multipart upload",
|
|
103
|
-
"description": "Allocation of Resources Without Limits or Throttling vulnerability in Apache Tomcat.\n\nThis issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.7, from 10.1.0-M1 through 10.1.41, from 9.0.0.M1 through 9.0.105.\n\nUsers are recommended to upgrade to version 11.0.8, 10.1.42 or 9.0.106, which fix the issue.",
|
|
104
|
-
"components": [
|
|
105
|
-
"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.41"
|
|
106
|
-
],
|
|
107
|
-
"refs": [
|
|
108
|
-
"https://avd.aquasec.com/nvd/cve-2025-48988",
|
|
109
|
-
"https://github.com/advisories/GHSA-h3gc-qfqq-6h8f"
|
|
110
|
-
]
|
|
111
|
-
},
|
|
112
|
-
"CVE-2025-49125": {
|
|
113
|
-
"id": "CVE-2025-49125",
|
|
114
|
-
"severity": "MEDIUM",
|
|
115
|
-
"title": "tomcat: Apache Tomcat: Security constraint bypass for pre/post-resources",
|
|
116
|
-
"description": "Authentication Bypass Using an Alternate Path or Channel vulnerability in Apache Tomcat. When using PreResources or PostResources mounted other than at the root of the web application, it was possible to access those resources via an unexpected path. That path was likely not to be protected by the same security constraints as the expected path, allowing those security constraints to be bypassed.\n\nThis issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.7, from 10.1.0-M1 through 10.1.41, from 9.0.0.M1 through 9.0.105.\n\nUsers are recommended to upgrade to version 11.0.8, 10.1.42 or 9.0.106, which fix the issue.",
|
|
117
|
-
"components": [
|
|
118
|
-
"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.41"
|
|
87
|
+
- A json file with a summary of the scan results.
|
|
88
|
+
- This can be used to post-process the scan results or to integrate them into other tools.
|
|
89
|
+
- Example:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"errors": [
|
|
94
|
+
{
|
|
95
|
+
"project": "mapapps@4.18.2",
|
|
96
|
+
"error": "Failed to fetch sbom file from maven repository: https://repository.conterra.de/repository/maven-mirror-ct/de/conterra/mapapps/ct-mapapps-rollout/4.18.2/ct-mapapps-rollout-4.18.2-sbom.cdx.json"
|
|
97
|
+
}
|
|
119
98
|
],
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
99
|
+
"vulnerabilities": {
|
|
100
|
+
"CVE-2025-48988": {
|
|
101
|
+
"id": "CVE-2025-48988",
|
|
102
|
+
"severity": "HIGH",
|
|
103
|
+
"title": "tomcat: Apache Tomcat DoS in multipart upload",
|
|
104
|
+
"description": "Allocation of Resources Without Limits or Throttling vulnerability in Apache Tomcat.\n\nThis issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.7, from 10.1.0-M1 through 10.1.41, from 9.0.0.M1 through 9.0.105.\n\nUsers are recommended to upgrade to version 11.0.8, 10.1.42 or 9.0.106, which fix the issue.",
|
|
105
|
+
"components": [
|
|
106
|
+
"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.41"
|
|
107
|
+
],
|
|
108
|
+
"refs": [
|
|
109
|
+
"https://avd.aquasec.com/nvd/cve-2025-48988",
|
|
110
|
+
"https://github.com/advisories/GHSA-h3gc-qfqq-6h8f"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"CVE-2025-49125": {
|
|
114
|
+
"id": "CVE-2025-49125",
|
|
115
|
+
"severity": "MEDIUM",
|
|
116
|
+
"title": "tomcat: Apache Tomcat: Security constraint bypass for pre/post-resources",
|
|
117
|
+
"description": "Authentication Bypass Using an Alternate Path or Channel vulnerability in Apache Tomcat. When using PreResources or PostResources mounted other than at the root of the web application, it was possible to access those resources via an unexpected path. That path was likely not to be protected by the same security constraints as the expected path, allowing those security constraints to be bypassed.\n\nThis issue affects Apache Tomcat: from 11.0.0-M1 through 11.0.7, from 10.1.0-M1 through 10.1.41, from 9.0.0.M1 through 9.0.105.\n\nUsers are recommended to upgrade to version 11.0.8, 10.1.42 or 9.0.106, which fix the issue.",
|
|
118
|
+
"components": [
|
|
119
|
+
"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.41"
|
|
120
|
+
],
|
|
121
|
+
"refs": [
|
|
122
|
+
"https://avd.aquasec.com/nvd/cve-2025-49125",
|
|
123
|
+
"https://github.com/advisories/GHSA-wc4r-xq3c-5cf3"
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"vulnerabilityToProject": {
|
|
128
|
+
"CVE-2025-48988": ["smartfinder-sdi@2.7.1-SNAPSHOT"],
|
|
129
|
+
"CVE-2025-49125": ["smartfinder-sdi@2.7.1-SNAPSHOT"]
|
|
130
|
+
},
|
|
131
|
+
"projectToVulnerability": {
|
|
132
|
+
"smartfinder-sdi@2.7.1-SNAPSHOT": [
|
|
133
|
+
"CVE-2025-48988",
|
|
134
|
+
"CVE-2025-49125"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"noLongerDetectedVulnerabilities": {
|
|
138
|
+
"CVE-2023-52070": [
|
|
139
|
+
"mapapps@4.19.3-SNAPSHOT",
|
|
140
|
+
"smartfinder-sdi@2.7.1-SNAPSHOT"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
145
143
|
}
|
|
146
|
-
|
|
147
|
-
```
|
|
144
|
+
```
|
|
148
145
|
|
|
149
146
|
## Configuration
|
|
150
147
|
|
|
@@ -154,15 +151,15 @@ A minimal configuration file specifies only the project to scan:
|
|
|
154
151
|
|
|
155
152
|
```json
|
|
156
153
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
"$schema": "./node_modules/@conterra/vuln-scan/dist/schema/conf-schema.json",
|
|
155
|
+
"projects": [
|
|
156
|
+
{
|
|
157
|
+
"name": "mapapps",
|
|
158
|
+
"version": "4.18.2",
|
|
159
|
+
"purl": "pkg:maven/de.conterra.mapapps/ct-mapapps@4.18.2",
|
|
160
|
+
"sbomFile": "./input/sboms/mapapps-4.18.2.cdx.json"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
166
163
|
}
|
|
167
164
|
```
|
|
168
165
|
|
|
@@ -171,7 +168,7 @@ The following sample lists all available options with their default values:
|
|
|
171
168
|
```json
|
|
172
169
|
{
|
|
173
170
|
"$schema": "./node_modules/@conterra/vuln-scan/dist/schema/conf-schema.json",
|
|
174
|
-
|
|
171
|
+
|
|
175
172
|
/* Defines if the process should fail or only log a warning or ignore vulnerabilities.
|
|
176
173
|
If "fail" exit 1 is called.
|
|
177
174
|
If "warn" the log statement "##vso[task.complete result=SucceededWithIssues;] is issued.
|
|
@@ -183,17 +180,17 @@ The following sample lists all available options with their default values:
|
|
|
183
180
|
If "warn" the log statement "##vso[task.complete result=SucceededWithIssues;] is issued.
|
|
184
181
|
If "ignore" nothing happens.
|
|
185
182
|
*/
|
|
186
|
-
"onNoLongerDetectedVulnerabilities": "ignore",
|
|
183
|
+
"onNoLongerDetectedVulnerabilities": "ignore",
|
|
187
184
|
/* The maven repository to fetch sboms from.
|
|
188
185
|
If the maven repository requires authentication, the environment variables MAVEN_REPO_USER and MAVEN_REPO_PW must be set.
|
|
189
186
|
*/
|
|
190
187
|
"mavenRepo": "https://repository.conterra.de/repository/maven-mirror-ct",
|
|
191
|
-
/* The list of scanners to use. Possible values are "grype", "trivy" and "ossindex".
|
|
188
|
+
/* The list of scanners to use. Possible values are "grype", "trivy" and "ossindex".
|
|
189
|
+
Use an optional version number to pin the scanner to a certain version, e.g. `trivy@0.69.3`.
|
|
190
|
+
*/
|
|
192
191
|
"scanners": ["grype", "trivy", "ossindex"],
|
|
193
192
|
/* (optional) The list of vex files to download. Default is empty array. */
|
|
194
|
-
"vexUrls": [
|
|
195
|
-
"https://example.com/my-vex.json"
|
|
196
|
-
],
|
|
193
|
+
"vexUrls": ["https://example.com/my-vex.json"],
|
|
197
194
|
/* The directory where the vex files are stored. */
|
|
198
195
|
"vexDir": "./input/vex",
|
|
199
196
|
/* The directory where the scan results are written to. */
|
|
@@ -255,7 +252,6 @@ The following sample lists all available options with their default values:
|
|
|
255
252
|
"purl": "pkg:maven/de.conterra.mapapps/ct-mapapps@4.17.2",
|
|
256
253
|
"sbomFile": "./input/sboms/mapapps-4.17.2.cdx.json"
|
|
257
254
|
}
|
|
258
|
-
|
|
259
255
|
]
|
|
260
256
|
}
|
|
261
257
|
```
|
|
@@ -312,19 +308,19 @@ NOTE: This variables can be set in the `.env` file in the root of the project.
|
|
|
312
308
|
For each project following files are created in the `output` folder:
|
|
313
309
|
|
|
314
310
|
- `<project-name>-<project-version>-sbom.cdx.json`
|
|
315
|
-
|
|
311
|
+
- Copy of the sbom file.
|
|
316
312
|
- `<project-name>-<project-version>-scan-grype-results.json`
|
|
317
|
-
|
|
313
|
+
- Result of the grype scanner in the grype json format.
|
|
318
314
|
- `<project-name>-<project-version>-scan-trivy-results.json`
|
|
319
|
-
|
|
315
|
+
- Result of the trivy scanner in the trivy json format.
|
|
320
316
|
- `<project-name>-<project-version>-scan-ossindex-results.json`
|
|
321
|
-
|
|
317
|
+
- Result of the sonatype oss index api requests in json format.
|
|
322
318
|
- `<project-name>-<project-version>-scan-aggregated-results.json`
|
|
323
|
-
|
|
319
|
+
- Aggregations of the results of all scanners in an own `@conterra/vuln-scan` json format.
|
|
324
320
|
- `<project-name>-<project-version>-scan-aggregated-results.sarif.json`
|
|
325
|
-
|
|
321
|
+
- Aggregations of the results of all scanners in an own [sarif json](https://sarifweb.azurewebsites.net/) format.
|
|
326
322
|
- `<project-name>-<project-version>-openvex.vex.json`
|
|
327
|
-
|
|
323
|
+
- Aggregation of all vex statements available for the project.
|
|
328
324
|
|
|
329
325
|
If the `createSubDirsForProject` flag is set to `true`, the scanner will create a sub directory in the `output` folder named `<project-name>-<project-version>` for each project scanned.
|
|
330
326
|
|
|
@@ -335,8 +331,8 @@ Attach the output directory as build artifact `CodeAnalysisLogs`.
|
|
|
335
331
|
```yaml
|
|
336
332
|
- task: PublishBuildArtifacts@1
|
|
337
333
|
inputs:
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
PathtoPublish: "output"
|
|
335
|
+
ArtifactName: "CodeAnalysisLogs"
|
|
340
336
|
```
|
|
341
337
|
|
|
342
338
|
Make sure that your Azure DevOps organization has the extension [SARIF SAST Scans Tab](https://marketplace.visualstudio.com/items?itemName=sariftools.scans) installed.
|
|
@@ -465,6 +461,7 @@ This will remove the product-id from all vex statements matching the given vulne
|
|
|
465
461
|
This is useful if a vulnerability is no longer detected for a project and you like to remove an existing statement.
|
|
466
462
|
|
|
467
463
|
## Usage - Add project to vex
|
|
464
|
+
|
|
468
465
|
Adds a new project version to all vex statements matching a reference project version.
|
|
469
466
|
This step is already included in the `ct-vuln-add-project` command, but it can be run separately to update the vex files for an already existing project entry in the configuration file.
|
|
470
467
|
|
|
@@ -498,6 +495,7 @@ $ ct-vuln-add-project mapapps 4.18.3 4.18.4-SNAPSHOT
|
|
|
498
495
|
The `ct-vuln-jira-report` command reads the `scan-summary.json` produced by `ct-vuln-scan` and creates Jira issues for every new vulnerability that has not already been reported.
|
|
499
496
|
|
|
500
497
|
For each vulnerability the tool:
|
|
498
|
+
|
|
501
499
|
1. Checks (via JQL) whether an issue with the same vulnerability ID already exists in the target Jira project.
|
|
502
500
|
2. If not, creates a new issue with a structured description including CVE details, affected artefacts, affected products, and placeholder sections for assessment and remediation.
|
|
503
501
|
|
|
@@ -517,35 +515,32 @@ Create a `jira-conf.json` file (path is configurable) that defines the Jira inst
|
|
|
517
515
|
|
|
518
516
|
```json
|
|
519
517
|
{
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
{
|
|
524
|
-
"jiraProject": "PLATFORM",
|
|
525
|
-
"issueType": "CVE",
|
|
526
|
-
"issueLabels": ["security"],
|
|
527
|
-
"addToBoard": [
|
|
518
|
+
"$schema": "./node_modules/@conterra/vuln-scan/dist/schema/jira-config-schema.json",
|
|
519
|
+
"jiraUrl": "https://myorg.atlassian.net",
|
|
520
|
+
"issues": [
|
|
528
521
|
{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
522
|
+
"jiraProject": "PLATFORM",
|
|
523
|
+
"issueType": "CVE",
|
|
524
|
+
"issueLabels": ["security"],
|
|
525
|
+
"addToBoard": [
|
|
526
|
+
{
|
|
527
|
+
"sprintBoardId": 10,
|
|
528
|
+
"issueLabels": ["team-a"],
|
|
529
|
+
"matchProjects": ["mapapps@*"]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"issueLabels": ["team-b"],
|
|
533
|
+
"matchProjects": ["smartfinder@*"]
|
|
534
|
+
}
|
|
535
|
+
],
|
|
536
|
+
"reportProjects": ["mapapps@*", "smartfinder@4.*"]
|
|
532
537
|
},
|
|
533
538
|
{
|
|
534
|
-
|
|
535
|
-
|
|
539
|
+
"jiraProject": "OTHER",
|
|
540
|
+
"issueType": "Bug",
|
|
541
|
+
"reportProjects": ["*"]
|
|
536
542
|
}
|
|
537
|
-
|
|
538
|
-
"reportProjects": [
|
|
539
|
-
"mapapps@*",
|
|
540
|
-
"smartfinder@4.*"
|
|
541
|
-
]
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
"jiraProject": "OTHER",
|
|
545
|
-
"issueType": "Bug",
|
|
546
|
-
"reportProjects": ["*"]
|
|
547
|
-
}
|
|
548
|
-
]
|
|
543
|
+
]
|
|
549
544
|
}
|
|
550
545
|
```
|
|
551
546
|
|
package/dist/vuln-scan.js
CHANGED
|
@@ -617,7 +617,7 @@ Usage tree:
|
|
|
617
617
|
${e.componentTree?.map(s=>gV(s)).join(`
|
|
618
618
|
`)}
|
|
619
619
|
\`\`\`
|
|
620
|
-
`}var xV=require("fs/promises");async function vBe(e,t,r){if("sbomFile"in e)return await(0,xV.copyFile)(e.sbomFile,t),e.sbomFile=t,e;let n=await r.downloadArtifact(e.sbomMavenCoordinates);return await(0,xV.writeFile)(t,n),{...e,sbomFile:t}}var vV=class{constructor(t,r){this.repo=t;this.authn=r}async downloadArtifact(t){let r=this.toArtifactInfo(t);if(this.isSnapshotVersion(r.version)&&!r.timestamp){let s=await this.fetchLatestSnapshotTimestamp(r);r={...r,timestamp:s}}let n=this.toArtifactUrl(r),a=await EI(n,this.authn);return Buffer.from(await a.arrayBuffer())}async fetchLatestSnapshotTimestamp(t){let r=`${this.toArtifactBaseUrl(t)}/maven-metadata.xml`,a=await(await EI(r,this.authn)).text();return Ogr(a,t)}toArtifactUrl(t){let r=this.toArtifactBaseUrl(t),n=this.isSnapshotVersion(t.version)&&t.timestamp?t.version.replace(/SNAPSHOT$/,t.timestamp):t.version;return`${r}/${t.artifactId}-${n}-${t.classifier}.${t.extension}`}toArtifactBaseUrl(t){return`${this.repo}/${t.groupId.replace(/\./g,"/")}/${t.artifactId}/${t.version}`}isSnapshotVersion(t){return t.endsWith("-SNAPSHOT")}toArtifactInfo(t){let r=t.split(":");return{groupId:r[0],artifactId:r[1],version:r[2],classifier:r[3],extension:"json"}}};function Ogr(e,t){if(t.classifier&&t.extension){let a=e.match(new RegExp(`<snapshotVersion>\\s*<classifier>${t.classifier}</classifier>\\s*<extension>${t.extension}</extension>\\s*<value>([^<]+)</value>\\s*<updated>[^<]+</updated>\\s*</snapshotVersion>`)),s=t.version.replace(/SNAPSHOT$/,"");if(a?.[1])return a[1].replace(s,"")}let r=e.match(/<timestamp>([^<]+)<\/timestamp>/),n=e.match(/<buildNumber>([^<]+)<\/buildNumber>/);if(!r||!n)throw new Error(`Failed to get timestamp and build number from ${e} for artifact '${t.artifactId}'`);return`${r[1]}-${n[1]}`}var bV=require("fs/promises"),Hle=require("path");var Kle=class{constructor(t){this.location=t}async read(){let t=await uf(this.location);if(!t)throw new Error("Invalid VEX file");return t}async write(t){await Ale(this.location,t)}async delete(){await(0,bV.unlink)(this.location)}};async function DV(e,t){for(let r of await(0,bV.readdir)(e,{withFileTypes:!0}))r.name.endsWith(".json")?await t(new Kle((0,Hle.resolve)(r.parentPath,r.name))):r.isDirectory()&&await DV((0,Hle.resolve)(r.parentPath,r.name),t)}function Xle(e){let t=e.vulnerability.name;return new Set([t,...e.vulnerability.aliases??[]])}var o5=class e{statements=[];filteredToProducts=[];static async fromUrl(t,r,n=new e){let s=await(await EI(t,r)).json();return n.add(s),n}static async fromFiles(t,r=new e){return await DV(t,async n=>{let a=await n.read();r.add(a)}),r}add(t){Mgr(t);let r=t.timestamp;for(let n of t.statements){n.timestamp=n.timestamp??r;let a=Date.parse(n.timestamp),s=n.vulnerability.name,u=Xle(n),c=new Set(n.products?.map(d=>d["@id"]).filter(d=>!!d)??[]),l=new Gle(a,s,u,c,n);this.statements.push(l)}this.sortStatementsByTimestampNewestFirst()}filterByProduct(t){if(this.assertCorrectProductId(t),this.filteredToProducts.length)return this;let r=new e;return r.filteredToProducts=t.slice(0),r.statements=this.statements.filter(n=>n.matchesAtLeastOneProduct(t)),r}findNotMatchingVulnerabilities(t,r){let n=this.filterByProduct(r??this.filteredToProducts),a=new Set;for(let s of n.statements)a.add(s.vulnName);for(let s of t)a.delete(s);return Array.from(a)}newestVulnerabilityStatement(t,r){let n=this.filterByProduct(r??this.filteredToProducts);for(let a of n.statements)if(a.matchesVulnerability(t))return this.reduceToProductInformation(a,n.filteredToProducts[0])}toVexFileForProduct(t){let r=this.filterByProduct(t??this.filteredToProducts),n=r.statements.map(a=>this.reduceToProductInformation(a,r.filteredToProducts[0]));return this.toVexFile(n)}toVexFile(t){let r=t.filter(a=>a.status!=="under_investigation");if(r.length===0)return;this.sortVexStatementsByTimestampNewestFirst(r);let n=r[0].timestamp;return{"@context":"https://openvex.dev/ns/v0.2.0","@id":"https://openvex.dev/docs/public/vex-fc763e6eb63bfbcda20b7cbbddd4e9f8feaaa317fe8d8a6f51a5663a1180343e",author:"conterra",timestamp:n,last_updated:n,version:1,statements:r}}assertCorrectProductId(t){if(!t||t.length===0)throw new Error("Product ID must be provided");if(this.filteredToProducts.length&&!Lgr(this.filteredToProducts,t))throw new Error("Cannot filter to a different product")}reduceToProductInformation(t,r){let n={timestamp:new Date(t.timestamp).toISOString(),...t.statement};if(!r)return n;let a=n.products?.filter(s=>s["@id"]===r);return a?.length||(a=[{"@id":r}]),{...n,products:a}}sortStatementsByTimestampNewestFirst(){this.statements.sort((t,r)=>r.timestamp-t.timestamp)}sortVexStatementsByTimestampNewestFirst(t){t.sort((r,n)=>Date.parse(n.timestamp)-Date.parse(r.timestamp))}};function Lgr(e,t){return e.length===t.length&&e.every((r,n)=>r===t[n])}var Gle=class{constructor(t,r,n,a,s){this.timestamp=t;this.vulnName=r;this.vulnAliases=n;this.products=a;this.statement=s}matchesProduct(t){return this.products.has(t)}matchesAtLeastOneProduct(t){return t.some(this.matchesProduct.bind(this))}matchesVulnerability(t){return this.vulnAliases.has(t)}};function Mgr(e){if(e?.["@context"]!=="https://openvex.dev/ns/v0.2.0")throw new Error("Invalid VEX file format")}var AV=require("path"),xBe=require("fs/promises");var EV=class{constructor(t,r,n){this.project=t;this.outputDir=r;this.useSubDirForProject=n}async lookupOutputFileForWriting(t){let r=this.useSubDirForProject?`${this.project.name}-${this.project.version}`:"",n=`${this.project.name}-${this.project.version}-`,a=this.getFileSuffix(t),s=(0,AV.resolve)(this.outputDir,r,`${n}${a}`),u=(0,AV.dirname)(s);return await cT(u)||await(0,xBe.mkdir)(u,{recursive:!0}),s}getFileSuffix(t){switch(t){case"grype-results":return"scan-grype-results.json";case"trivy-results":return"scan-trivy-results.json";case"ossindex-results":return"scan-ossindex-results.json";case"aggregate-results":return"scan-aggregate-results.json";case"aggregated-sarif-results":return"scan-aggregated-results.sarif.json";case"project-cyclonedx-sbom-file":return"sbom.cdx.json";case"project-openvex-file":return"openvex.vex.json";default:throw new Error(`Unknown output file key: ${t}`)}}};var Yle=require("zx");var CV=require("path"),bBe="anchore/grype";async function DBe(e,t,r){r&&console.log(`Initializing Grype DB in cache directory: ${t}`);let n=`${bBe}:${e||"latest"}`,a=await Yle.$`docker run --rm -v '${t}:/tmp/.cache' -e GRYPE_DB_CACHE_DIR=/tmp/.cache -e TMPDIR=/tmp ${n} db update`.nothrow();if(a.stdout&&console.log(` Grype DB Update: ${a.stdout}`),a.stderr&&console.log(` Grype DB Update error: ${a.stderr}`),a.exitCode!==0)throw new Error(`Grype DB update failed with exit code ${a.exitCode}: ${a.stderr}`)}async function EBe(e,t,r,n,a,s,u){let c=(0,CV.dirname)(r),l=(0,CV.basename)(r),d=[];s||d.push("--quiet");let _=`${bBe}:${e||"latest"}`,f=`${t}:/tmp/.cache`,v=`${c}:/tmp/output`,b=`${n.sbomFile}:/tmp/input.json`,D=`/tmp/output/${l}`,S=await Yle.$`docker run --rm -v ${f} -v ${v} -v ${b} -e GRYPE_DB_CACHE_DIR=/tmp/.cache -e TMPDIR=/tmp -e GRYPE_DB_AUTO_UPDATE=false ${_} sbom:/tmp/input.json -o json --file ${D} --by-cve ${d}`.nothrow();if(S.stdout&&u(` Scan log: ${S.stdout}`),S.stderr&&u(` Scan error: ${S.stderr}`),S.exitCode!==0)throw new Error(`Grype scan failed with exit code ${S.exitCode}: ${S.stderr}`);u(` Grype output file: ${r}`);try{let L=await uf(r);Rgr(a,L)}catch(L){throw new Error(`Grype scan results could not be read from '${r}': ${L}`,{cause:L})}}function Rgr(e,t){if(t)for(let r of t.matches){let n=jgr(r);e.addVulnerability(n,"grype")}}function jgr(e){let t=e.vulnerability,r=t.id,n=Z1(e.artifact.purl),a=Z1(t.dataSource),s=qgr(t.severity);return{id:r,severity:s,scannerSeverity:{grype:s},title:"",description:t.description,components:[n],detectedBy:["grype"],refs:[a]}}function qgr(e){switch(e){case"Negligible":return"LOW";case"Low":return"LOW";case"Medium":return"MEDIUM";case"High":return"HIGH";case"Critical":return"CRITICAL";default:return"UNKNOWN"}}var ABe=require("fs/promises");async function CBe(e,t,r,n,a,s){try{let u={},c="";r.user&&r.token&&(u.Authorization=`Basic ${Buffer.from(`${r.user}:${r.token}`).toString("base64")}`,c="/authorized");let l=`https://ossindex.sonatype.org/api/v3${c}/component-report`,d=await Ugr(t.sbomFile),_=new Set(d.map(b=>b.purl.replace(/\?.*$/,"")));Hgr(_);let f=Array.from(_),v=[];for(let b=0;b<f.length;b+=128){let D=JSON.stringify({coordinates:f.slice(b,b+128)});a&&s(` Sonatype OssIndex request ${l} with body: ${D}`);let L=await(await fetch(l,{method:"POST",headers:{...u,"Content-Type":"application/vnd.ossindex.component-report-request.v1+json"},body:D})).json();Array.isArray(L)?v.push(...L.filter(q=>q.vulnerabilities.length>0)):s(` Sonatype OssIndex Error: ${L.code} - ${L.message}`)}await(0,ABe.writeFile)(e,JSON.stringify(v,null,2)),s(` Sonatype OssIndex output file: ${e}`),Jgr(n,v)}catch(u){throw new Error(`Sonatype OSS Index scan failed: ${u}`,{cause:u})}}async function Ugr(e){return(await uf(e)).components.filter(r=>$gr(r)&&!Vgr(r))}function $gr(e){return!!e.purl}function Vgr(e){return e.group?e.group.startsWith("de.conterra")||e.group.startsWith("org.n52.security"):!1}function Jgr(e,t){if(t)for(let r of t)for(let n of r.vulnerabilities){let a=zgr(n,r.coordinates);e.addVulnerability(a,"ossindex")}}function zgr(e,t){let r=e.cve||e.id,n=Z1(t),a=Z1(e.reference),s=Wgr(e.cvssScore);return{id:r,severity:s,scannerSeverity:{ossindex:s},title:e.title,description:e.description,components:[n],detectedBy:["ossindex"],refs:a?[a]:[]}}function Wgr(e){return e===0?"UNKNOWN":e<4?"LOW":e<7?"MEDIUM":e<9?"HIGH":"CRITICAL"}function Hgr(e){let t=Array.from(e);for(let r of t){let n=r.replace(/(@[^-]+)-.*$/g,"$1");e.add(n)}}var Qle=require("zx");var TV=require("path"),TBe="aquasec/trivy";async function SBe(e,t,r,n){let a=[];r&&(a.push("-e"),a.push(`GITHUB_TOKEN=${r}`));let s=[];s.push("--db-repository"),s.push("public.ecr.aws/aquasecurity/trivy-db,aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db"),n||s.push("--quiet");let u=`${TBe}:${e||"latest"}`,c=await Qle.$`docker run --rm -v '${t}:/.cache' ${a} ${u} ${s} --cache-dir '/.cache' sbom --download-db-only`.nothrow();if(!c.stdout&&!c.stderr&&console.log(" Trivy DB Update: finished"),c.stdout&&console.log(` Trivy DB Update: ${c.stdout}`),c.stderr&&console.log(` Trivy DB Update error: ${c.stderr}`),c.exitCode!==0)throw new Error(`Trivy DB update failed with exit code ${c.exitCode}: ${c.stderr}`)}async function wBe(e,t,r,n,a,s,u){let c=(0,TV.dirname)(r),l=(0,TV.basename)(r),d=[];s||d.push("--quiet");let _=`${TBe}:${e||"latest"}`,f=`${t}:/.cache`,v=`${c}:/tmp/output`,b=`${n.sbomFile}:/tmp/input.json`,D=`/tmp/output/${l}`,S=await Qle.$`docker run --rm -v ${f} -v ${v} -v ${b} ${_} sbom --cache-dir '/.cache' --format json --output ${D} ${d} --skip-db-update /tmp/input.json`.nothrow();if(S.stdout&&u(` Scan log: ${S.stdout}`),S.stderr&&u(` Scan error: ${S.stderr}`),S.exitCode!==0)throw new Error(`Trivy scan failed with exit code ${S.exitCode}: ${S.stderr}`);u(` Trivy output file: ${r}`);try{let L=await uf(r);Kgr(a,L)}catch(L){throw new Error(`Trivy scan results could not be read from '${r}': ${L}`,{cause:L})}}function Kgr(e,t){if(t){for(let r of t.Results??[])if(r.Vulnerabilities)for(let n of r.Vulnerabilities){let a=Ggr(n);e.addVulnerability(a,"trivy")}}}function Ggr(e){let t=e.VulnerabilityID,r=Z1(e.PkgIdentifier.PURL),n=Z1(e.PrimaryURL),a=Xgr(e.Severity);return{id:t,severity:a,scannerSeverity:{trivy:a},title:e.Title,description:e.Description,components:[r],detectedBy:["trivy"],refs:[n]}}function Xgr(e){switch(e){case"UNKNOWN":case"LOW":case"MEDIUM":case"HIGH":case"CRITICAL":return e;default:return"UNKNOWN"}}var SV=class{constructor(t,r,n){this.scanners=t;this.cacheDir=r;this.options=n}async init(t){let r=[];for(let n of this.scanners)switch(n.name){case"grype":r.push(DBe(n.version,this.cacheDir,t));break;case"trivy":r.push(SBe(n.version,this.cacheDir,this.options.trivyGithubToken,t));break;case"ossindex":continue}await Promise.all(r)}async scanProject(t,r,n,a,s){let u=[];for(let c of this.scanners)u.push(this.scanProjectBy(c,t,r,this.cacheDir,n,this.options.ossIndexApiUser,this.options.ossIndexApiToken,a,s));await Promise.all(u)}async scanProjectBy(t,r,n,a,s,u,c,l,d){switch(t.name){case"grype":return EBe(t.version,a,await n.lookupOutputFileForWriting("grype-results"),r,s,l,d);case"trivy":return wBe(t.version,a,await n.lookupOutputFileForWriting("trivy-results"),r,s,l,d);case"ossindex":return CBe(await n.lookupOutputFileForWriting("ossindex-results"),r,{user:u,token:c},s,l,d)}}};async function kBe(e,t,r){await DV(e,async n=>{let a=await n.read(),s=!1,u=a.statements,c=[];for(let l of u){if(r&&!Xle(l).has(r)||!l.products)continue;let d=l.products.findIndex(_=>_["@id"]===t);d!==-1&&(l.products.splice(d,1),s=!0,l.products.length===0&&c.push(l))}if(s){for(let l of c){let d=u.indexOf(l);u.splice(d,1)}if(u.length===0){console.info(`Remove ${t} from ${n.location} and delete file.`),await n.delete();return}console.info(`Remove ${t} from ${n.location}`),await n.write(a)}})}var NI=require("fs/promises"),Zle=require("path");async function PBe(e,t){let{orderBy:r,reportNoLongerDetectedVulnerabilities:n,reportCveDetails:a,outputDirectory:s}={orderBy:"CVE",reportNoLongerDetectedVulnerabilities:!0,reportCveDetails:!1,...t},u=!1,c=!1,l=!1,d=new Map,_=new Map,f=new Map,v=new Map,b=new Map,D=[],S=new Map;for(let C of e.values()){let k=C.project,T=`${k.name}@${k.version}`;if(C.error&&(u=!0,D.push({project:T,error:C.error})),k.silent)continue;let O=Vd[k.minimumSeverity],M=C.newVulns.filter($=>O<=Vd[$.severity]);for(let $ of M){if(c=!0,!d.has($.id))d.set($.id,{id:$.id,title:$.title,severity:$.severity,description:$.description,components:$.components,refs:$.refs});else{let X=d.get($.id);t7r(X,$)}_.has($.id)||_.set($.id,[]),v.has(T)||v.set(T,[]),S.has(T)||S.set(T,new Map),v.get(T).push($.id),_.get($.id).push(T);for(let X of $.componentTree)X.purl&&(S.get(T).has(X.purl)||S.get(T).set(X.purl,X))}for(let $ of C.noLongerDetectedVulns)l=!0,f.has($)||f.set($,[]),b.has(T)||b.set(T,[]),b.get(T).push($),f.get($).push(T)}let L;s&&(L=(0,Zle.join)(s,"scan-summary.txt"),await(0,NI.writeFile)(L,"","utf8"));let q=async C=>{console.log(C),L&&await(0,NI.appendFile)(L,C+`
|
|
620
|
+
`}var xV=require("fs/promises");async function vBe(e,t,r){if("sbomFile"in e)return await(0,xV.copyFile)(e.sbomFile,t),e.sbomFile=t,e;let n=await r.downloadArtifact(e.sbomMavenCoordinates);return await(0,xV.writeFile)(t,n),{...e,sbomFile:t}}var vV=class{constructor(t,r){this.repo=t;this.authn=r}async downloadArtifact(t){let r=this.toArtifactInfo(t);if(this.isSnapshotVersion(r.version)&&!r.timestamp){let s=await this.fetchLatestSnapshotTimestamp(r);r={...r,timestamp:s}}let n=this.toArtifactUrl(r),a=await EI(n,this.authn);return Buffer.from(await a.arrayBuffer())}async fetchLatestSnapshotTimestamp(t){let r=`${this.toArtifactBaseUrl(t)}/maven-metadata.xml`,a=await(await EI(r,this.authn)).text();return Ogr(a,t)}toArtifactUrl(t){let r=this.toArtifactBaseUrl(t),n=this.isSnapshotVersion(t.version)&&t.timestamp?t.version.replace(/SNAPSHOT$/,t.timestamp):t.version;return`${r}/${t.artifactId}-${n}-${t.classifier}.${t.extension}`}toArtifactBaseUrl(t){return`${this.repo}/${t.groupId.replace(/\./g,"/")}/${t.artifactId}/${t.version}`}isSnapshotVersion(t){return t.endsWith("-SNAPSHOT")}toArtifactInfo(t){let r=t.split(":");return{groupId:r[0],artifactId:r[1],version:r[2],classifier:r[3],extension:"json"}}};function Ogr(e,t){if(t.classifier&&t.extension){let a=e.match(new RegExp(`<snapshotVersion>\\s*<classifier>${t.classifier}</classifier>\\s*<extension>${t.extension}</extension>\\s*<value>([^<]+)</value>\\s*<updated>[^<]+</updated>\\s*</snapshotVersion>`)),s=t.version.replace(/SNAPSHOT$/,"");if(a?.[1])return a[1].replace(s,"")}let r=e.match(/<timestamp>([^<]+)<\/timestamp>/),n=e.match(/<buildNumber>([^<]+)<\/buildNumber>/);if(!r||!n)throw new Error(`Failed to get timestamp and build number from ${e} for artifact '${t.artifactId}'`);return`${r[1]}-${n[1]}`}var bV=require("fs/promises"),Hle=require("path");var Kle=class{constructor(t){this.location=t}async read(){let t=await uf(this.location);if(!t)throw new Error("Invalid VEX file");return t}async write(t){await Ale(this.location,t)}async delete(){await(0,bV.unlink)(this.location)}};async function DV(e,t){for(let r of await(0,bV.readdir)(e,{withFileTypes:!0}))r.name.endsWith(".json")?await t(new Kle((0,Hle.resolve)(r.parentPath,r.name))):r.isDirectory()&&await DV((0,Hle.resolve)(r.parentPath,r.name),t)}function Xle(e){let t=e.vulnerability.name;return new Set([t,...e.vulnerability.aliases??[]])}var o5=class e{statements=[];filteredToProducts=[];static async fromUrl(t,r,n=new e){let s=await(await EI(t,r)).json();return n.add(s),n}static async fromFiles(t,r=new e){return await DV(t,async n=>{let a=await n.read();r.add(a)}),r}add(t){Mgr(t);let r=t.timestamp;for(let n of t.statements){n.timestamp=n.timestamp??r;let a=Date.parse(n.timestamp),s=n.vulnerability.name,u=Xle(n),c=new Set(n.products?.map(d=>d["@id"]).filter(d=>!!d)??[]),l=new Gle(a,s,u,c,n);this.statements.push(l)}this.sortStatementsByTimestampNewestFirst()}filterByProduct(t){if(this.assertCorrectProductId(t),this.filteredToProducts.length)return this;let r=new e;return r.filteredToProducts=t.slice(0),r.statements=this.statements.filter(n=>n.matchesAtLeastOneProduct(t)),r}findNotMatchingVulnerabilities(t,r){let n=this.filterByProduct(r??this.filteredToProducts),a=new Set;for(let s of n.statements)a.add(s.vulnName);for(let s of t)a.delete(s);return Array.from(a)}newestVulnerabilityStatement(t,r){let n=this.filterByProduct(r??this.filteredToProducts);for(let a of n.statements)if(a.matchesVulnerability(t))return this.reduceToProductInformation(a,n.filteredToProducts[0])}toVexFileForProduct(t){let r=this.filterByProduct(t??this.filteredToProducts),n=r.statements.map(a=>this.reduceToProductInformation(a,r.filteredToProducts[0]));return this.toVexFile(n)}toVexFile(t){let r=t.filter(a=>a.status!=="under_investigation");if(r.length===0)return;this.sortVexStatementsByTimestampNewestFirst(r);let n=r[0].timestamp;return{"@context":"https://openvex.dev/ns/v0.2.0","@id":"https://openvex.dev/docs/public/vex-fc763e6eb63bfbcda20b7cbbddd4e9f8feaaa317fe8d8a6f51a5663a1180343e",author:"conterra",timestamp:n,last_updated:n,version:1,statements:r}}assertCorrectProductId(t){if(!t||t.length===0)throw new Error("Product ID must be provided");if(this.filteredToProducts.length&&!Lgr(this.filteredToProducts,t))throw new Error("Cannot filter to a different product")}reduceToProductInformation(t,r){let n={timestamp:new Date(t.timestamp).toISOString(),...t.statement};if(!r)return n;let a=n.products?.filter(s=>s["@id"]===r);return a?.length||(a=[{"@id":r}]),{...n,products:a}}sortStatementsByTimestampNewestFirst(){this.statements.sort((t,r)=>r.timestamp-t.timestamp)}sortVexStatementsByTimestampNewestFirst(t){t.sort((r,n)=>Date.parse(n.timestamp)-Date.parse(r.timestamp))}};function Lgr(e,t){return e.length===t.length&&e.every((r,n)=>r===t[n])}var Gle=class{constructor(t,r,n,a,s){this.timestamp=t;this.vulnName=r;this.vulnAliases=n;this.products=a;this.statement=s}matchesProduct(t){return this.products.has(t)}matchesAtLeastOneProduct(t){return t.some(this.matchesProduct.bind(this))}matchesVulnerability(t){return this.vulnAliases.has(t)}};function Mgr(e){if(e?.["@context"]!=="https://openvex.dev/ns/v0.2.0")throw new Error("Invalid VEX file format")}var AV=require("path"),xBe=require("fs/promises");var EV=class{constructor(t,r,n){this.project=t;this.outputDir=r;this.useSubDirForProject=n}async lookupOutputFileForWriting(t){let r=this.useSubDirForProject?`${this.project.name}-${this.project.version}`:"",n=`${this.project.name}-${this.project.version}-`,a=this.getFileSuffix(t),s=(0,AV.resolve)(this.outputDir,r,`${n}${a}`),u=(0,AV.dirname)(s);return await cT(u)||await(0,xBe.mkdir)(u,{recursive:!0}),s}getFileSuffix(t){switch(t){case"grype-results":return"scan-grype-results.json";case"trivy-results":return"scan-trivy-results.json";case"ossindex-results":return"scan-ossindex-results.json";case"aggregate-results":return"scan-aggregate-results.json";case"aggregated-sarif-results":return"scan-aggregated-results.sarif.json";case"project-cyclonedx-sbom-file":return"sbom.cdx.json";case"project-openvex-file":return"openvex.vex.json";default:throw new Error(`Unknown output file key: ${t}`)}}};var Yle=require("zx");var CV=require("path"),bBe="anchore/grype";async function DBe(e,t,r){r&&console.log(`Initializing Grype DB in cache directory: ${t}`);let n=`${bBe}:${e||"latest"}`,a=await Yle.$`docker run --rm -v '${t}:/tmp/.cache' -e GRYPE_DB_CACHE_DIR=/tmp/.cache -e TMPDIR=/tmp ${n} db update`.nothrow();if(a.stdout&&console.log(` Grype DB Update: ${a.stdout}`),a.stderr&&console.log(` Grype DB Update error: ${a.stderr}`),a.exitCode!==0)throw new Error(`Grype DB update failed with exit code ${a.exitCode}: ${a.stderr}`)}async function EBe(e,t,r,n,a,s,u){let c=(0,CV.dirname)(r),l=(0,CV.basename)(r),d=[];s||d.push("--quiet");let _=`${bBe}:${e||"latest"}`,f=`${t}:/tmp/.cache`,v=`${c}:/tmp/output`,b=`${n.sbomFile}:/tmp/input.json`,D=`/tmp/output/${l}`,S=await Yle.$`docker run --rm -v ${f} -v ${v} -v ${b} -e GRYPE_DB_CACHE_DIR=/tmp/.cache -e TMPDIR=/tmp -e GRYPE_DB_AUTO_UPDATE=false ${_} sbom:/tmp/input.json -o json --file ${D} --by-cve ${d}`.nothrow();if(S.stdout&&u(` Scan log: ${S.stdout}`),S.stderr&&u(` Scan error: ${S.stderr}`),S.exitCode!==0)throw new Error(`Grype scan failed with exit code ${S.exitCode}: ${S.stderr}`);u(` Grype output file: ${r}`);try{let L=await uf(r);Rgr(a,L)}catch(L){throw new Error(`Grype scan results could not be read from '${r}': ${L}`,{cause:L})}}function Rgr(e,t){if(t)for(let r of t.matches){let n=jgr(r);e.addVulnerability(n,"grype")}}function jgr(e){let t=e.vulnerability,r=t.id,n=Z1(e.artifact.purl),a=Z1(t.dataSource),s=qgr(t.severity);return{id:r,severity:s,scannerSeverity:{grype:s},title:"",description:t.description,components:[n],detectedBy:["grype"],refs:[a]}}function qgr(e){switch(e){case"Negligible":return"LOW";case"Low":return"LOW";case"Medium":return"MEDIUM";case"High":return"HIGH";case"Critical":return"CRITICAL";default:return"UNKNOWN"}}var ABe=require("fs/promises");async function CBe(e,t,r,n,a,s){try{let u={},c="";r.user&&r.token&&(u.Authorization=`Basic ${Buffer.from(`${r.user}:${r.token}`).toString("base64")}`,c="/authorized");let l=`https://ossindex.sonatype.org/api/v3${c}/component-report`,d=await Ugr(t.sbomFile),_=new Set(d.map(b=>b.purl.replace(/\?.*$/,"")));Hgr(_);let f=Array.from(_),v=[];for(let b=0;b<f.length;b+=128){let D=JSON.stringify({coordinates:f.slice(b,b+128)});a&&s(` Sonatype OssIndex request ${l} with body: ${D}`);let L=await(await fetch(l,{method:"POST",headers:{...u,"Content-Type":"application/vnd.ossindex.component-report-request.v1+json"},body:D})).json();Array.isArray(L)?v.push(...L.filter(q=>q.vulnerabilities.length>0)):s(` Sonatype OssIndex Error: ${L.code} - ${L.message}`)}await(0,ABe.writeFile)(e,JSON.stringify(v,null,2)),s(` Sonatype OssIndex output file: ${e}`),Jgr(n,v)}catch(u){throw new Error(`Sonatype OSS Index scan failed: ${u}`,{cause:u})}}async function Ugr(e){return(await uf(e)).components.filter(r=>$gr(r)&&!Vgr(r))}function $gr(e){return!!e.purl}function Vgr(e){return e.group?e.group.startsWith("de.conterra")||e.group.startsWith("org.n52.security"):!1}function Jgr(e,t){if(t)for(let r of t)for(let n of r.vulnerabilities){let a=zgr(n,r.coordinates);e.addVulnerability(a,"ossindex")}}function zgr(e,t){let r=e.cve||e.id,n=Z1(t),a=Z1(e.reference),s=Wgr(e.cvssScore);return{id:r,severity:s,scannerSeverity:{ossindex:s},title:e.title,description:e.description,components:[n],detectedBy:["ossindex"],refs:a?[a]:[]}}function Wgr(e){return e===0?"UNKNOWN":e<4?"LOW":e<7?"MEDIUM":e<9?"HIGH":"CRITICAL"}function Hgr(e){let t=Array.from(e);for(let r of t){let n=r.replace(/(@[^-]+)-.*$/g,"$1");e.add(n)}}var Qle=require("zx");var TV=require("path"),TBe="aquasec/trivy";async function SBe(e,t,r,n){let a=[];r&&(a.push("-e"),a.push(`GITHUB_TOKEN=${r}`));let s=[];s.push("--db-repository"),s.push("public.ecr.aws/aquasecurity/trivy-db,aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db"),n||s.push("--quiet");let u=`${TBe}:${e||"0.69.3"}`,c=await Qle.$`docker run --rm -v '${t}:/.cache' ${a} ${u} ${s} --cache-dir '/.cache' sbom --download-db-only`.nothrow();if(!c.stdout&&!c.stderr&&console.log(" Trivy DB Update: finished"),c.stdout&&console.log(` Trivy DB Update: ${c.stdout}`),c.stderr&&console.log(` Trivy DB Update error: ${c.stderr}`),c.exitCode!==0)throw new Error(`Trivy DB update failed with exit code ${c.exitCode}: ${c.stderr}`)}async function wBe(e,t,r,n,a,s,u){let c=(0,TV.dirname)(r),l=(0,TV.basename)(r),d=[];s||d.push("--quiet");let _=`${TBe}:${e||"latest"}`,f=`${t}:/.cache`,v=`${c}:/tmp/output`,b=`${n.sbomFile}:/tmp/input.json`,D=`/tmp/output/${l}`,S=await Qle.$`docker run --rm -v ${f} -v ${v} -v ${b} ${_} sbom --cache-dir '/.cache' --format json --output ${D} ${d} --skip-db-update /tmp/input.json`.nothrow();if(S.stdout&&u(` Scan log: ${S.stdout}`),S.stderr&&u(` Scan error: ${S.stderr}`),S.exitCode!==0)throw new Error(`Trivy scan failed with exit code ${S.exitCode}: ${S.stderr}`);u(` Trivy output file: ${r}`);try{let L=await uf(r);Kgr(a,L)}catch(L){throw new Error(`Trivy scan results could not be read from '${r}': ${L}`,{cause:L})}}function Kgr(e,t){if(t){for(let r of t.Results??[])if(r.Vulnerabilities)for(let n of r.Vulnerabilities){let a=Ggr(n);e.addVulnerability(a,"trivy")}}}function Ggr(e){let t=e.VulnerabilityID,r=Z1(e.PkgIdentifier.PURL),n=Z1(e.PrimaryURL),a=Xgr(e.Severity);return{id:t,severity:a,scannerSeverity:{trivy:a},title:e.Title,description:e.Description,components:[r],detectedBy:["trivy"],refs:[n]}}function Xgr(e){switch(e){case"UNKNOWN":case"LOW":case"MEDIUM":case"HIGH":case"CRITICAL":return e;default:return"UNKNOWN"}}var SV=class{constructor(t,r,n){this.scanners=t;this.cacheDir=r;this.options=n}async init(t){let r=[];for(let n of this.scanners)switch(n.name){case"grype":r.push(DBe(n.version,this.cacheDir,t));break;case"trivy":r.push(SBe(n.version,this.cacheDir,this.options.trivyGithubToken,t));break;case"ossindex":continue}await Promise.all(r)}async scanProject(t,r,n,a,s){let u=[];for(let c of this.scanners)u.push(this.scanProjectBy(c,t,r,this.cacheDir,n,this.options.ossIndexApiUser,this.options.ossIndexApiToken,a,s));await Promise.all(u)}async scanProjectBy(t,r,n,a,s,u,c,l,d){switch(t.name){case"grype":return EBe(t.version,a,await n.lookupOutputFileForWriting("grype-results"),r,s,l,d);case"trivy":return wBe(t.version,a,await n.lookupOutputFileForWriting("trivy-results"),r,s,l,d);case"ossindex":return CBe(await n.lookupOutputFileForWriting("ossindex-results"),r,{user:u,token:c},s,l,d)}}};async function kBe(e,t,r){await DV(e,async n=>{let a=await n.read(),s=!1,u=a.statements,c=[];for(let l of u){if(r&&!Xle(l).has(r)||!l.products)continue;let d=l.products.findIndex(_=>_["@id"]===t);d!==-1&&(l.products.splice(d,1),s=!0,l.products.length===0&&c.push(l))}if(s){for(let l of c){let d=u.indexOf(l);u.splice(d,1)}if(u.length===0){console.info(`Remove ${t} from ${n.location} and delete file.`),await n.delete();return}console.info(`Remove ${t} from ${n.location}`),await n.write(a)}})}var NI=require("fs/promises"),Zle=require("path");async function PBe(e,t){let{orderBy:r,reportNoLongerDetectedVulnerabilities:n,reportCveDetails:a,outputDirectory:s}={orderBy:"CVE",reportNoLongerDetectedVulnerabilities:!0,reportCveDetails:!1,...t},u=!1,c=!1,l=!1,d=new Map,_=new Map,f=new Map,v=new Map,b=new Map,D=[],S=new Map;for(let C of e.values()){let k=C.project,T=`${k.name}@${k.version}`;if(C.error&&(u=!0,D.push({project:T,error:C.error})),k.silent)continue;let O=Vd[k.minimumSeverity],M=C.newVulns.filter($=>O<=Vd[$.severity]);for(let $ of M){if(c=!0,!d.has($.id))d.set($.id,{id:$.id,title:$.title,severity:$.severity,description:$.description,components:$.components,refs:$.refs});else{let X=d.get($.id);t7r(X,$)}_.has($.id)||_.set($.id,[]),v.has(T)||v.set(T,[]),S.has(T)||S.set(T,new Map),v.get(T).push($.id),_.get($.id).push(T);for(let X of $.componentTree)X.purl&&(S.get(T).has(X.purl)||S.get(T).set(X.purl,X))}for(let $ of C.noLongerDetectedVulns)l=!0,f.has($)||f.set($,[]),b.has(T)||b.set(T,[]),b.get(T).push($),f.get($).push(T)}let L;s&&(L=(0,Zle.join)(s,"scan-summary.txt"),await(0,NI.writeFile)(L,"","utf8"));let q=async C=>{console.log(C),L&&await(0,NI.appendFile)(L,C+`
|
|
621
621
|
`,"utf8")};switch(await q("=== Scan summary ==="),D.length&&await Ygr(q,D),a&&await e7r(q,d,S),r){case"CVE":{await Zgr(q,_,d,n?f:new Map);break}case"PROJECT":{await Qgr(q,v,n?b:new Map,d);break}}!u&&!c&&(!l||!n)&&await q(" No issues found");let F={errors:D,vulnerabilities:Object.fromEntries(d),vulnerabilityToProject:Object.fromEntries(_.entries()),projectToVulnerability:Object.fromEntries(v.entries()),projectsToComponents:Object.fromEntries(Array.from(S.entries()).map(([C,k])=>[C,Object.fromEntries(k.entries())])),noLongerDetectedVulnerabilities:Object.fromEntries(f)};if(s){let C=(0,Zle.join)(s,"scan-summary.json");await(0,NI.writeFile)(C,JSON.stringify(F,null,2),"utf8")}return{hasError:u,hasNewVuln:c,hasNoLongerDetectedVuln:l}}async function Ygr(e,t){await e(""),await e("- Errors during scan:");for(let{project:r,error:n}of t)await e(` - ${r}:`),await e(` Error: ${n}`)}async function Qgr(e,t,r,n){let a=new Set([...Array.from(t.keys()),...Array.from(r.keys())]),s=Array.from(a.keys()).sort();if(s.length){await e(""),await e("- Vulnerabilities by project:");for(let u of s){await e(` - ${u}:`);let c=t.get(u)??[];for(let d of c.slice().sort(e0e(_=>n.get(_).severity))){let{severity:_}=n.get(d);await e(` - [${_}] ${d}`)}let l=(r.get(u)??[]).slice().sort();for(let d of l)await e(` - ${d} (no longer detected)`)}}}async function Zgr(e,t,r,n){let a=Array.from(t.keys()).sort(e0e(u=>r.get(u).severity));if(a.length){await e(""),await e("- New vulnerabilities:");for(let u of a){let c=t.get(u),{severity:l}=r.get(u);await e(` - [${l}] ${u}`);for(let d of c.slice().sort())await e(` - ${d}`)}}let s=Array.from(n.keys()).sort();if(s.length){await e(""),await e("- Declared but no longer detected vulnerabilities:");for(let u of s){let c=n.get(u);await e(` - ${u}`);for(let l of c.slice().sort())await e(` - ${l}`)}}}async function e7r(e,t,r){if(t.size===0)return;await e(""),await e("- Vulnerability details:");let n=Array.from(t.keys()).sort(e0e(a=>t.get(a).severity));for(let a of n){let s=t.get(a);if(await e(` - [${s.severity}] ${s.id} - ${s.title||""}:`),s.description){await e(" Description:");let u=s.description.split(/\r?\n/);for(let c of u)await e(` ${c}`)}if(s.components.length){await e(" Components:");for(let u of s.components)await e(" - "+u);if(r){for(let u of s.components)for(let[c,l]of r.entries())if(l.has(u)){await e(` Usage Tree of ${u} by ${c} (as example):`),await e(gV(l.get(u)," "));break}}}if(s.refs.length){await e(" Links:");for(let u of s.refs)await e(` - ${u}`)}}}function t7r(e,t){e.title=e.title||t.title,e.severity=Vd[e.severity]>Vd[t.severity]?e.severity:t.severity,e.description=e.description||t.description,e.components=FBe(e.components,t.components),e.refs=FBe(e.refs,t.refs)}function FBe(e,t){let r=new Set([...e,...t]);return Array.from(r)}function e0e(e){return(t,r)=>{let n=e(t),a=e(r),s=Vd[n],u=Vd[a];return s!==u?s>u?-1:1:t.localeCompare(r)}}wV.os.type().startsWith("Windows")&&(0,wV.usePowerShell)();async function NBe(e){let t=await Sle(e),r=t.cacheDir;await cT(r)||await(0,_T.mkdir)(r);let n=t.outDir;await cT(n)&&await Cle(n),await(0,_T.mkdir)(n);let a=await a7r(t,{user:e.vexUrlsUser,pw:e.vexUrlsPw}),s=new vV(t.mavenRepo,{user:e.mavenRepoUser,pw:e.mavenRepoPw}),u=s7r(t,e,r),c=Date.now();console.log("Initializing scanner engines ..."),await u.init(e.verbose),console.log(`Scanner engines initialization took ${Date.now()-c}ms
|
|
622
622
|
`);let l=o7r(e),d=t.projects,_=new Map,f=Date.now();console.log("Scanning projects ...");let v=d.filter(l).map(D=>{let S=new EV(D,n,t.createSubDirsForProject),L=[],q=C=>{L.push(C)};return{scan(){return r7r(D,S,s,u,a,q,_,e,L)}}});await c7r(v),console.log(`Scanning took ${Date.now()-f}ms`),e.autoRemoveUnusedStatements&&await u7r(_,t.vexDir);let b=await PBe(_,{orderBy:t.consoleReport.startsWith("BY_CVE")?"CVE":"PROJECT",reportCveDetails:t.consoleReport.endsWith("_DETAILED"),reportNoLongerDetectedVulnerabilities:t.reportNoLongerDetectedVulnerabilities,outputDirectory:n});if(b.hasError)return 1;if(b.hasNewVuln){if(t.onNewVulnerabilities==="fail")return 1;t.onNewVulnerabilities==="warn"&&(console.log("##vso[task.logissue type=warning;]NEW_VULNERABILITIES_DETECTED"),console.log("##vso[task.complete result=SucceededWithIssues;]"))}if(b.hasNoLongerDetectedVuln){if(t.onNoLongerDetectedVulnerabilities==="fail")return 1;t.onNoLongerDetectedVulnerabilities==="warn"&&(console.log("##vso[task.logissue type=warning;]NO_LONGER_DETECTED_VULNERABILITIES"),console.log("##vso[task.complete result=SucceededWithIssues;]"))}return 0}async function r7r(e,t,r,n,a,s,u,c,l){try{let{unmatchedVulnerabilities:d,noLongerDetectedVulnerabilities:_}=await n7r(e,t,r,n,a,c.verbose,s);u.set(e.purl,{project:e,newVulns:d,noLongerDetectedVulns:_})}catch(d){let _=d;u.set(e.purl,{project:e,error:`${_}`,newVulns:[],noLongerDetectedVulns:[]})}if(c.verbose)for(let d of l)console.log(d)}async function n7r(e,t,r,n,a,s,u){let c=await vBe(e,await t.lookupOutputFileForWriting("project-cyclonedx-sbom-file"),r);u(`Analyzing project: ${c.name} - ${c.version} - ${c.sbomFile}`);let l=await uf(c.sbomFile);if(!l)throw new Error(`Could not read SBOM file ${c.sbomFile} for project ${c.name}`);if(!l.components||l.components.length===0)throw new Error(`SBOM file ${c.sbomFile} for project ${c.name} does not contain any components`);let d=new JR(yV.createForSbom(l));await n.scanProject(c,t,d,s,u);let _=a.filterByProduct([c.purl,...c.purlAliases]),{unmatchedVulnerabilities:f}=d.enhanceWithVexData(_),v=c.detectNoLongerUsedVulnerabilities?_.findNotMatchingVulnerabilities(d.getAllVulnIds()):[];return await i7r(t,c,d,_,u),{unmatchedVulnerabilities:f,noLongerDetectedVulnerabilities:v}}async function i7r(e,t,r,n,a){let s=r.getAllVulnerabilitiesSortedBySeverity(),u=await e.lookupOutputFileForWriting("aggregate-results");await(0,_T.writeFile)(u,JSON.stringify(s,null,2)),a(` Vulnerabilities output file: ${u}`);let c=await e.lookupOutputFileForWriting("aggregated-sarif-results"),l=gBe(s,t);await(0,_T.writeFile)(c,JSON.stringify(l,null,2)),a(` Vuln Sarif output file: ${c}`);let d=n.toVexFile(s.filter(f=>f.vexStatement).map(f=>{let v=f.vexStatement;for(let b of f.components??[])v.products.push({"@id":b});return v}));if(!d)return;let _=await e.lookupOutputFileForWriting("project-openvex-file");await(0,_T.writeFile)(_,JSON.stringify(d,null,2)),a(` OpenVex file: ${_}`)}async function a7r(e,t){let r=e.vexDir,n;await cT(r)?n=await o5.fromFiles(r):n=new o5;for(let a of e.vexUrls)await o5.fromUrl(a,t,n);return n}function s7r(e,t,r){if(t.scannerEngine)return t.scannerEngine;if(e.scanners.length===0)throw new Error("No scanners configured");return new SV(e.scanners,r,t)}function o7r(e){let{reduceToProjectName:t,reduceToProjectVersion:r}=e;return n=>!(!n.enabled||t&&(n.name!==t||r&&n.version!==r))}async function u7r(e,t){for(let r of e.values())if(!r.project.silent)for(let n of r.noLongerDetectedVulns)await kBe(t,r.project.purl,n)}async function c7r(e,t=5){let r=0,n=0,a=0,s=e.length,u,c=new Promise(d=>{u=d});function l(){for(;r<t&&a<s;){let d=e[a++];r++,d.scan().finally(()=>{r--,n++,n>=s?u():l()})}}l(),await c}var IBe=require("zx");IBe.dotenv.config();var BD=process.env;(async()=>{let e=await NBe({workingDir:process.cwd(),reduceToProjectName:process.argv[2],reduceToProjectVersion:process.argv[3],ossIndexApiUser:BD.OSS_INDEX_API_USER,ossIndexApiToken:BD.OSS_INDEX_API_TOKEN,mavenRepoUser:BD.MAVEN_REPO_USER,mavenRepoPw:BD.MAVEN_REPO_PW,trivyGithubToken:BD.TRIVY_GITHUB_TOKEN,vexUrlsUser:BD.VEX_URLS_USER,vexUrlsPw:BD.VEX_URLS_PW,verbose:BD.VERBOSE==="true",autoRemoveUnusedStatements:BD.AUTO_REMOVE_UNUSED_STATEMENTS==="true"});process.exitCode=e})();
|
|
623
623
|
/*! Bundled license information:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conterra/vuln-scan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "con terra vulnerability scan process",
|
|
5
5
|
"author": "con terra GmbH",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/escape-html": "1.0.4",
|
|
39
39
|
"@types/sarif": "^2.1.7",
|
|
40
40
|
"copyfiles": "2.4.1",
|
|
41
|
-
"esbuild": "0.27.
|
|
41
|
+
"esbuild": "0.27.4",
|
|
42
42
|
"escape-html": "1.0.3",
|
|
43
43
|
"eslint-plugin-headers": "1.3.4",
|
|
44
44
|
"eslint": "^9.39.1",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"prettier": "^3.8.1",
|
|
49
49
|
"rimraf": "6.1.3",
|
|
50
50
|
"typescript": "5.9.3",
|
|
51
|
-
"typescript-eslint": "8.
|
|
52
|
-
"vitest": "4.0
|
|
53
|
-
"@inquirer/prompts": "8.3.
|
|
51
|
+
"typescript-eslint": "8.57.1",
|
|
52
|
+
"vitest": "4.1.0",
|
|
53
|
+
"@inquirer/prompts": "8.3.2"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"full-build": "pnpm run clean && pnpm run type-check && pnpm run lint && pnpm run prod",
|