@conterra/vuln-scan 0.1.1 → 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 CHANGED
@@ -32,30 +32,30 @@ or reference it in your `package.json` file:
32
32
 
33
33
  ```json
34
34
  {
35
- "dependencies": {
36
- "@conterra/vuln-scan": "<current version>"
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
- - Triggers the vulnerability scan
44
+ - Triggers the vulnerability scan
45
45
  - `ct-vuln-add-vex`
46
- - Triggers a process to add a new vex statement to a file
46
+ - Triggers a process to add a new vex statement to a file
47
47
  - `ct-vuln-add-project`
48
- - Adds a new project to the configuration file based on an existing project entry and adds it to all vex statements
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
- - 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.
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
- - 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.
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
- - 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.
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
- - 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.
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
- - A json file with a summary of the scan results.
89
- - This can be used to post-process the scan results or to integrate them into other tools.
90
- - Example:
91
-
92
- ```json
93
- {
94
- "errors": [{
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
- }],
98
- "vulnerabilities": {
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
- "refs": [
121
- "https://avd.aquasec.com/nvd/cve-2025-49125",
122
- "https://github.com/advisories/GHSA-wc4r-xq3c-5cf3"
123
- ]
124
- }
125
- },
126
- "vulnerabilityToProject": {
127
- "CVE-2025-48988": [
128
- "smartfinder-sdi@2.7.1-SNAPSHOT"
129
- ],
130
- "CVE-2025-49125": [
131
- "smartfinder-sdi@2.7.1-SNAPSHOT"
132
- ]
133
- },
134
- "projectToVulnerability": {
135
- "smartfinder-sdi@2.7.1-SNAPSHOT": [
136
- "CVE-2025-48988",
137
- "CVE-2025-49125"
138
- ]
139
- },
140
- "noLongerDetectedVulnerabilities": {
141
- "CVE-2023-52070": [
142
- "mapapps@4.19.3-SNAPSHOT",
143
- "smartfinder-sdi@2.7.1-SNAPSHOT"
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
- "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/conf-schema.json",
158
- "projects": [
159
- {
160
- "name": "mapapps",
161
- "version": "4.18.2",
162
- "purl": "pkg:maven/de.conterra.mapapps/ct-mapapps@4.18.2",
163
- "sbomFile": "./input/sboms/mapapps-4.18.2.cdx.json"
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
- - Copy of the sbom file.
311
+ - Copy of the sbom file.
316
312
  - `<project-name>-<project-version>-scan-grype-results.json`
317
- - Result of the grype scanner in the grype json format.
313
+ - Result of the grype scanner in the grype json format.
318
314
  - `<project-name>-<project-version>-scan-trivy-results.json`
319
- - Result of the trivy scanner in the trivy json format.
315
+ - Result of the trivy scanner in the trivy json format.
320
316
  - `<project-name>-<project-version>-scan-ossindex-results.json`
321
- - Result of the sonatype oss index api requests in json format.
317
+ - Result of the sonatype oss index api requests in json format.
322
318
  - `<project-name>-<project-version>-scan-aggregated-results.json`
323
- - Aggregations of the results of all scanners in an own `@conterra/vuln-scan` json format.
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
- - Aggregations of the results of all scanners in an own [sarif json](https://sarifweb.azurewebsites.net/) format.
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
- - Aggregation of all vex statements available for the project.
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
- PathtoPublish: "output"
339
- ArtifactName: "CodeAnalysisLogs"
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
- "$schema": "./node_modules/@conterra/vuln-scan/dist/schema/jira-config-schema.json",
521
- "jiraUrl": "https://myorg.atlassian.net",
522
- "issues": [
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
- "sprintBoardId": 10,
530
- "issueLabels": ["team-a"],
531
- "matchProjects": ["mapapps@*"]
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
- "issueLabels": ["team-b"],
535
- "matchProjects": ["smartfinder@*"]
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
 
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var A=require("path"),K=require("zx");var N=require("fs/promises");function E(r,e=""){return B(r,e)}function B(r,e="",n=new Set){if(n.has(r.purl))return`${e}- ${r.purl} (cycle detected)`;n.add(r.purl);let t=n.size===1,s=`${e}- ${r.purl}`;for(let i of r.usedBy){let o=B(i,e+(t?" ":" "),new Set(n));o!==""&&(s+=`
3
- `,s+=o)}return s}async function M(r){let{scanSummaryPath:e,jiraConfigPath:n,jiraUser:t,jiraApiToken:s,dryRun:i=!1}=r,[o,c]=await Promise.all([H(e),W(n)]),a={created:[],skipped:[],errors:[]},u=new k(c.jiraUrl,t,s),x=new Map;for(let[g,P]of Object.entries(o.vulnerabilities)){let _=o.vulnerabilityToProject[g]??[];for(let p of c.issues){let m=_.filter(h=>O(h,p.reportProjects));if(m.length===0)continue;let $=Q(P);try{let h=await u.issueExists(p.jiraProject,g);if(h){console.log(`[skip] Issue for ${g} already exists: ${h}`),a.skipped.push(`${p.jiraProject}::${g}`);continue}if(i){console.log(`[dry-run] Would create issue in ${p.jiraProject}: "${$}"`),console.log(` Matching projects: ${m.join(", ")}`),a.created.push(`${p.jiraProject}::${g}`);continue}let y,C=[],I=!1;for(let l of p.addToBoard??[]){let T=l.matchProjects??["*"];if(m.some(d=>O(d,T)))if(l.sprintBoardId!=null){if(!I){I=!0;let d=l.sprintBoardId;if(!x.has(d)){let b=await u.getActiveSprintId(d).catch(z=>(console.warn(`[warn] Could not resolve active sprint for boardId ${d}: ${String(z)}`),null));x.set(d,b)}y=x.get(d)??void 0,l.issueLabels&&C.push(...l.issueLabels)}}else l.issueLabels&&C.push(...l.issueLabels)}let R=[...p.issueLabels??[],...C],J=new Map;if(o.projectsToComponents)for(let l of m){let T=o.projectsToComponents[l]??{},j=[];for(let d of P.components){let b=T[d];b&&j.push(b)}j.length>0&&J.set(l,j)}let V=re(P,m,J),v=await u.createIssue({projectKey:p.jiraProject,issueType:p.issueType,summary:$,description:V,labels:R.length>0?R:void 0,sprintId:y});console.log(`[created] ${v} in ${p.jiraProject}: "${$}"`),a.created.push(v)}catch(h){let y=`Failed to process ${g} for project ${p.jiraProject}: ${String(h)}`;console.error(`[error] ${y}`),a.errors.push(y)}}}return a}async function H(r){let e=await(0,N.readFile)(r,"utf8");return JSON.parse(e)}async function W(r){let e=await(0,N.readFile)(r,"utf8"),n=JSON.parse(e);if(!n.issues&&n.issue&&(n.issues=n.issue),!n.jiraUrl||typeof n.jiraUrl!="string")throw new Error("jira-conf.json: missing or invalid 'jiraUrl' field");if(!Array.isArray(n.issues))throw new Error("jira-conf.json: missing or invalid 'issues' array");return n}function q(r){return r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")}function G(r,e){let n=e.split("*").map(q).join(".*");return new RegExp(`^${n}$`).test(r)}function O(r,e){return e.some(n=>G(r,n))}function Q(r){let e=r.title?`: ${r.title}`:"";return`[${r.severity}] ${r.id}${e}`}function X(...r){return{type:"doc",version:1,content:r}}function f(r,e){return{type:"heading",attrs:{level:r},content:[{type:"text",text:e}]}}function Y(r){let e=r.split(/\r?\n/),n=[],t=0;for(;t<e.length;){let s=e[t],i=s.match(/^```(\w*)$/);if(i){let a=i[1]??"text",u=[];for(t++;t<e.length&&!e[t].startsWith("```");)u.push(e[t]),t++;t++,n.push(L(u.join(`
4
- `),a));continue}let o=s.match(/^(#{1,3})\s+(.+)$/);if(o){let a=Math.min(o[1].length,3);n.push(f(a,o[2].trim())),t++;continue}if(/^[-*]\s/.test(s)){let a=[];for(;t<e.length&&/^[-*]\s/.test(e[t]);){let u=e[t].replace(/^[-*]\s+/,"");a.push({type:"listItem",content:[{type:"paragraph",content:S(u)}]}),t++}n.push({type:"bulletList",content:a});continue}if(/^\d+\.\s/.test(s)){let a=[];for(;t<e.length&&/^\d+\.\s/.test(e[t]);){let u=e[t].replace(/^\d+\.\s+/,"");a.push({type:"listItem",content:[{type:"paragraph",content:S(u)}]}),t++}n.push({type:"orderedList",content:a});continue}if(s.trim()===""){t++;continue}let c=[];for(;t<e.length&&e[t].trim()!==""&&!/^(#{1,3}\s|[-*]\s|\d+\.\s|```)/.test(e[t]);)c.push(e[t]),t++;if(c.length>0){let a=[];for(let u=0;u<c.length;u++)a.push(...S(c[u])),u<c.length-1&&a.push({type:"hardBreak"});n.push({type:"paragraph",content:a})}}return n}function S(r){let e=[],n=/(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|_(.+?)_)/g,t=0,s;for(;(s=n.exec(r))!==null;)s.index>t&&e.push({type:"text",text:r.slice(t,s.index)}),s[2]!==void 0?e.push({type:"text",text:s[2],marks:[{type:"strong"}]}):s[3]!==void 0?e.push({type:"text",text:s[3],marks:[{type:"em"}]}):s[4]!==void 0?e.push({type:"text",text:s[4],marks:[{type:"code"}]}):s[5]!==void 0&&e.push({type:"text",text:s[5],marks:[{type:"em"}]}),t=s.index+s[0].length;return t<r.length&&e.push({type:"text",text:r.slice(t)}),e.length>0?e:[{type:"text",text:r}]}function L(r,e="text"){return{type:"codeBlock",attrs:{language:e},content:[{type:"text",text:r}]}}function Z(r){return{type:"bulletList",content:r.map(e=>({type:"listItem",content:[{type:"paragraph",content:[{type:"text",text:e}]}]}))}}function ee(r){return{type:"bulletList",content:r.map(e=>({type:"listItem",content:[{type:"paragraph",content:[{type:"text",text:e,marks:[{type:"link",attrs:{href:e}}]}]}]}))}}function U(r,...e){return{type:"expand",attrs:{title:r},content:e}}function w(r){return{type:"paragraph",content:[{type:"text",text:r,marks:[{type:"em"}]}]}}function te(r){return!r||r.length===0?"":r.map(e=>E(e,"")).join(`
2
+ "use strict";var J=require("path"),V=require("zx");var R=require("fs/promises");function E(r,e=""){return O(r,e)}function O(r,e="",n=new Set){if(n.has(r.purl))return`${e}- ${r.purl} (cycle detected)`;n.add(r.purl);let t=n.size===1,s=`${e}- ${r.purl}`;for(let i of r.usedBy){let o=O(i,e+(t?" ":" "),new Set(n));o!==""&&(s+=`
3
+ `,s+=o)}return s}async function U(r){let{scanSummaryPath:e,jiraConfigPath:n,jiraUser:t,jiraApiToken:s,dryRun:i=!1}=r,[o,c]=await Promise.all([W(e),q(n)]),a={created:[],skipped:[],errors:[]},u=new I(c.jiraUrl,t,s),C=new Map;for(let[f,T]of Object.entries(o.vulnerabilities)){let K=o.vulnerabilityToProject[f]??[];for(let l of c.issues){let w=K.filter(h=>M(h,l.reportProjects));if(w.length===0)continue;let k=X(T);try{let h=await u.issueExists(l.jiraProject,f);if(h){console.log(`[skip] Issue for ${f} already exists: ${h}`),a.skipped.push(`${l.jiraProject}::${f}`);continue}if(i){console.log(`[dry-run] Would create issue in ${l.jiraProject}: "${k}"`),console.log(` Matching projects: ${w.join(", ")}`),a.created.push(`${l.jiraProject}::${f}`);continue}let b,S=[],v=!1;for(let p of l.addToBoard??[]){let m=p.matchProjects??["*"];if(w.some(d=>M(d,m)))if(p.sprintBoardId!=null){if(!v){v=!0;let d=p.sprintBoardId;if(!C.has(d)){let P=await u.getActiveSprintId(d).catch(H=>(console.warn(`[warn] Could not resolve active sprint for boardId ${d}: ${String(H)}`),null));C.set(d,P)}b=C.get(d)??void 0,p.issueLabels&&S.push(...p.issueLabels)}}else p.issueLabels&&S.push(...p.issueLabels)}let B=[...l.issueLabels??[],...S],j=new Map;if(o.projectsToComponents)for(let p of w){let m=o.projectsToComponents[p]??{},x=[];for(let d of T.components){let P=m[d];P&&x.push(P)}x.length>0&&j.set(p,x)}let _=ne(T,w,j),$=await u.createIssue({projectKey:l.jiraProject,issueType:l.issueType,summary:k,description:_,labels:B.length>0?B:void 0,sprintId:b});if(j.size>0){let p=re(f,j);if(p){let m=`${f}-dependency-trees.md`;await u.attachMarkdown($,m,p),console.log(`[attached] ${m} to ${$}`)}}console.log(`[created] ${$} in ${l.jiraProject}: "${k}"`),a.created.push($)}catch(h){let b=`Failed to process ${f} for project ${l.jiraProject}: ${String(h)}`;console.error(`[error] ${b}`),a.errors.push(b)}}}return a}async function W(r){let e=await(0,R.readFile)(r,"utf8");return JSON.parse(e)}async function q(r){let e=await(0,R.readFile)(r,"utf8"),n=JSON.parse(e);if(!n.issues&&n.issue&&(n.issues=n.issue),!n.jiraUrl||typeof n.jiraUrl!="string")throw new Error("jira-conf.json: missing or invalid 'jiraUrl' field");if(!Array.isArray(n.issues))throw new Error("jira-conf.json: missing or invalid 'issues' array");return n}function G(r){return r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")}function Q(r,e){let n=e.split("*").map(G).join(".*");return new RegExp(`^${n}$`).test(r)}function M(r,e){return e.some(n=>Q(r,n))}function X(r){let e=r.title?`: ${r.title}`:"";return`[${r.severity}] ${r.id}${e}`}function Y(...r){return{type:"doc",version:1,content:r}}function g(r,e){return{type:"heading",attrs:{level:r},content:[{type:"text",text:e}]}}function Z(r){let e=r.split(/\r?\n/),n=[],t=0;for(;t<e.length;){let s=e[t],i=s.match(/^```(\w*)$/);if(i){let a=i[1]??"text",u=[];for(t++;t<e.length&&!e[t].startsWith("```");)u.push(e[t]),t++;t++,n.push(L(u.join(`
4
+ `),a));continue}let o=s.match(/^(#{1,3})\s+(.+)$/);if(o){let a=Math.min(o[1].length,3);n.push(g(a,o[2].trim())),t++;continue}if(/^[-*]\s/.test(s)){let a=[];for(;t<e.length&&/^[-*]\s/.test(e[t]);){let u=e[t].replace(/^[-*]\s+/,"");a.push({type:"listItem",content:[{type:"paragraph",content:A(u)}]}),t++}n.push({type:"bulletList",content:a});continue}if(/^\d+\.\s/.test(s)){let a=[];for(;t<e.length&&/^\d+\.\s/.test(e[t]);){let u=e[t].replace(/^\d+\.\s+/,"");a.push({type:"listItem",content:[{type:"paragraph",content:A(u)}]}),t++}n.push({type:"orderedList",content:a});continue}if(s.trim()===""){t++;continue}let c=[];for(;t<e.length&&e[t].trim()!==""&&!/^(#{1,3}\s|[-*]\s|\d+\.\s|```)/.test(e[t]);)c.push(e[t]),t++;if(c.length>0){let a=[];for(let u=0;u<c.length;u++)a.push(...A(c[u])),u<c.length-1&&a.push({type:"hardBreak"});n.push({type:"paragraph",content:a})}}return n}function A(r){let e=[],n=/(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|_(.+?)_)/g,t=0,s;for(;(s=n.exec(r))!==null;)s.index>t&&e.push({type:"text",text:r.slice(t,s.index)}),s[2]!==void 0?e.push({type:"text",text:s[2],marks:[{type:"strong"}]}):s[3]!==void 0?e.push({type:"text",text:s[3],marks:[{type:"em"}]}):s[4]!==void 0?e.push({type:"text",text:s[4],marks:[{type:"code"}]}):s[5]!==void 0&&e.push({type:"text",text:s[5],marks:[{type:"em"}]}),t=s.index+s[0].length;return t<r.length&&e.push({type:"text",text:r.slice(t)}),e.length>0?e:[{type:"text",text:r}]}function L(r,e="text"){return{type:"codeBlock",attrs:{language:e},content:[{type:"text",text:r}]}}function ee(r){return{type:"bulletList",content:r.map(e=>({type:"listItem",content:[{type:"paragraph",content:[{type:"text",text:e}]}]}))}}function te(r){return{type:"bulletList",content:r.map(e=>({type:"listItem",content:[{type:"paragraph",content:[{type:"text",text:e,marks:[{type:"link",attrs:{href:e}}]}]}]}))}}function N(r,...e){return{type:"expand",attrs:{title:r},content:e}}function y(r){return{type:"paragraph",content:[{type:"text",text:r,marks:[{type:"em"}]}]}}function D(r){return!r||r.length===0?"":r.map(e=>E(e,"")).join(`
5
5
 
6
- `)}function re(r,e,n=new Map){let t=[];if(t.push(f(1,"Informationen zur Schwachstelle")),t.push(w("Diese Informationen wurden automatisch generiert.")),r.refs&&r.refs.length>0&&t.push(ee(r.refs)),r.description){let s=Y(r.description);t.push(U("Beschreibung",...s))}t.push(f(2,"Betroffene Artefakte")),r.components&&r.components.length>0?t.push(Z(r.components)):t.push(w("Keine Komponenten bekannt.")),t.push(f(2,"Gemeldet f\xFCr"));for(let s of e.slice().sort()){t.push({type:"paragraph",content:[{type:"text",text:s,marks:[{type:"strong"}]}]});let i=n.get(s);if(i&&i.length>0){let o=te(i);o&&t.push(U("Verwendungsbaum",L(o)))}}return t.push(f(1,"Einwertung")),t.push(w("Einwertung des CVE in Bezug auf die gemeldeten Projekte/Produkte. Dies kann eine Bewertung der Schwere, der Auswirkungen und der Wahrscheinlichkeit eines Angriffs umfassen.")),t.push(f(1,"Betroffen sind")),t.push(w("Abschlie\xDFende Darstellung, welche Projekte/Produkte tats\xE4chlich betroffen oder nicht betroffen sind.")),t.push(f(1,"Ma\xDFnahmen")),t.push(w("\xDCbersicht der zu ergreifenden Ma\xDFnahmen, um den CVE zu beheben oder zu mitigieren. Dies kann Patches, Updates oder Konfigurations\xE4nderungen umfassen.")),X(...t)}var k=class{authHeader;baseUrl;constructor(e,n,t){this.baseUrl=e.replace(/\/$/,""),this.authHeader=`Basic ${Buffer.from(`${n}:${t}`).toString("base64")}`}get headers(){return{Authorization:this.authHeader,"Content-Type":"application/json",Accept:"application/json"}}async safeJson(e,n){let t=await e.text();try{return JSON.parse(t)}catch{let s=t.slice(0,500).replace(/\s+/g," ").trim();throw new Error(`${n}: expected JSON but got (HTTP ${e.status}): ${s}`)}}async issueExists(e,n){let t=n.replace(/"/g,'\\"'),s=`project = "${e}" AND summary ~ "\\"${t}\\""`,i=`${this.baseUrl}/rest/api/3/search/jql?jql=${encodeURIComponent(s)}&maxResults=1&fields=summary`,o=await fetch(i,{method:"GET",headers:this.headers});if(!o.ok){let a=await o.text();throw new Error(`Jira search failed (${o.status}): ${a}`)}return(await this.safeJson(o,"Jira search")).issues[0]?.key??null}async createIssue(e){let n={project:{key:e.projectKey},issuetype:{name:e.issueType},summary:e.summary,description:e.description};e.labels&&e.labels.length>0&&(n.labels=e.labels),e.sprintId!=null&&(n.customfield_10020=e.sprintId);let t={fields:n},s=await fetch(`${this.baseUrl}/rest/api/3/issue`,{method:"POST",headers:this.headers,body:JSON.stringify(t)});if(!s.ok){let o=await s.text();throw new Error(`Jira create issue failed (${s.status}): ${o}`)}return(await this.safeJson(s,"Jira create issue")).key}async getActiveSprintId(e){let n=`${this.baseUrl}/rest/agile/1.0/board/${e}/sprint?state=active&maxResults=1`,t=await fetch(n,{method:"GET",headers:this.headers});if(!t.ok){let i=await t.text();throw new Error(`Jira agile sprint lookup failed (${t.status}): ${i}`)}return(await this.safeJson(t,"Jira sprint lookup")).values[0]?.id??null}};K.dotenv.config();var F=process.env;function ne(r){let e=r.slice(2),n=process.cwd(),t=(0,A.join)(n,"jira-conf.json"),s=(0,A.join)(n,"output","scan-summary.json"),i=!1;for(let o=0;o<e.length;o++){let c=e[o];(c==="--jira-config"||c==="-c")&&e[o+1]?t=e[++o]:(c==="--summary"||c==="-s")&&e[o+1]?s=e[++o]:c==="--dry-run"?i=!0:c==="--help"||c==="-h"?(D(),process.exit(0)):(console.error(`Unknown argument: ${c}`),D(),process.exit(1))}return{jiraConfigPath:t,scanSummaryPath:s,dryRun:i}}function D(){console.log(`Usage: ct-vuln-jira-report [options]
6
+ `)}function re(r,e){if(e.size===0)return"";let n=[`# Verwendungsb\xE4ume f\xFCr ${r}`,""];for(let[t,s]of[...e.entries()].sort(([i],[o])=>i.localeCompare(o))){n.push(`## ${t}`,"");let i=D(s);i&&n.push("```",i,"```"),n.push("")}return n.join(`
7
+ `)}function ne(r,e,n=new Map){let t=[],i=0;if(t.push(g(1,"Informationen zur Schwachstelle")),t.push(y("Diese Informationen wurden automatisch generiert.")),r.refs&&r.refs.length>0&&t.push(te(r.refs)),r.description){let o=Z(r.description);t.push(N("Beschreibung",...o))}t.push(g(2,"Betroffene Artefakte")),r.components&&r.components.length>0?t.push(ee(r.components)):t.push(y("Keine Komponenten bekannt.")),t.push(g(2,"Gemeldet f\xFCr"));for(let o of e.slice().sort()){t.push({type:"paragraph",content:[{type:"text",text:o,marks:[{type:"strong"}]}]});let c=n.get(o);if(c&&c.length>0){let a=D(c);a&&(i+=a.length,i>2e4?t.push(N("Verwendungsbaum",y("Gesamtgr\xF6\xDFe der Verwendungsb\xE4ume \xFCberschreitet Limit. Siehe Issue-Attachments f\xFCr vollst\xE4ndige B\xE4ume."))):t.push(N("Verwendungsbaum",L(a))))}}return t.push(g(1,"Einwertung")),t.push(y("Einwertung des CVE in Bezug auf die gemeldeten Projekte/Produkte. Dies kann eine Bewertung der Schwere, der Auswirkungen und der Wahrscheinlichkeit eines Angriffs umfassen.")),t.push(g(1,"Betroffen sind")),t.push(y("Abschlie\xDFende Darstellung, welche Projekte/Produkte tats\xE4chlich betroffen oder nicht betroffen sind.")),t.push(g(1,"Ma\xDFnahmen")),t.push(y("\xDCbersicht der zu ergreifenden Ma\xDFnahmen, um den CVE zu beheben oder zu mitigieren. Dies kann Patches, Updates oder Konfigurations\xE4nderungen umfassen.")),Y(...t)}var I=class{authHeader;baseUrl;constructor(e,n,t){this.baseUrl=e.replace(/\/$/,""),this.authHeader=`Basic ${Buffer.from(`${n}:${t}`).toString("base64")}`}get headers(){return{Authorization:this.authHeader,"Content-Type":"application/json",Accept:"application/json"}}async safeJson(e,n){let t=await e.text();try{return JSON.parse(t)}catch{let s=t.slice(0,500).replace(/\s+/g," ").trim();throw new Error(`${n}: expected JSON but got (HTTP ${e.status}): ${s}`)}}async issueExists(e,n){let t=n.replace(/"/g,'\\"'),s=`project = "${e}" AND summary ~ "\\"${t}\\""`,i=`${this.baseUrl}/rest/api/3/search/jql?jql=${encodeURIComponent(s)}&maxResults=1&fields=summary`,o=await fetch(i,{method:"GET",headers:this.headers});if(!o.ok){let a=await o.text();throw new Error(`Jira search failed (${o.status}): ${a}`)}return(await this.safeJson(o,"Jira search")).issues[0]?.key??null}async createIssue(e){let n={project:{key:e.projectKey},issuetype:{name:e.issueType},summary:e.summary,description:e.description};e.labels&&e.labels.length>0&&(n.labels=e.labels),e.sprintId!=null&&(n.customfield_10020=e.sprintId);let t={fields:n},s=await fetch(`${this.baseUrl}/rest/api/3/issue`,{method:"POST",headers:this.headers,body:JSON.stringify(t)});if(!s.ok){let o=await s.text();throw new Error(`Jira create issue failed (${s.status}): ${o}`)}return(await this.safeJson(s,"Jira create issue")).key}async attachMarkdown(e,n,t){let s=`${this.baseUrl}/rest/api/3/issue/${e}/attachments`,i=new FormData;i.append("file",new Blob([t],{type:"text/markdown"}),n);let o=await fetch(s,{method:"POST",headers:{Authorization:this.authHeader,"X-Atlassian-Token":"no-check"},body:i});if(!o.ok){let c=await o.text();throw new Error(`Jira attach file failed (${o.status}): ${c}`)}}async getActiveSprintId(e){let n=`${this.baseUrl}/rest/agile/1.0/board/${e}/sprint?state=active&maxResults=1`,t=await fetch(n,{method:"GET",headers:this.headers});if(!t.ok){let i=await t.text();throw new Error(`Jira agile sprint lookup failed (${t.status}): ${i}`)}return(await this.safeJson(t,"Jira sprint lookup")).values[0]?.id??null}};V.dotenv.config();var F=process.env;function se(r){let e=r.slice(2),n=process.cwd(),t=(0,J.join)(n,"jira-conf.json"),s=(0,J.join)(n,"output","scan-summary.json"),i=!1;for(let o=0;o<e.length;o++){let c=e[o];(c==="--jira-config"||c==="-c")&&e[o+1]?t=e[++o]:(c==="--summary"||c==="-s")&&e[o+1]?s=e[++o]:c==="--dry-run"?i=!0:c==="--help"||c==="-h"?(z(),process.exit(0)):(console.error(`Unknown argument: ${c}`),z(),process.exit(1))}return{jiraConfigPath:t,scanSummaryPath:s,dryRun:i}}function z(){console.log(`Usage: ct-vuln-jira-report [options]
7
8
 
8
9
  Options:
9
10
  -c, --jira-config <path> Path to jira-conf.json configuration file
@@ -16,4 +17,4 @@ Options:
16
17
  Required environment variables:
17
18
  JIRA_USER Jira account e-mail address
18
19
  JIRA_API_TOKEN Jira API token (see https://id.atlassian.com/manage-profile/security/api-tokens)
19
- `)}(async()=>{let{jiraConfigPath:r,scanSummaryPath:e,dryRun:n}=ne(process.argv),t=F.JIRA_USER,s=F.JIRA_API_TOKEN;(!t||!s)&&(console.error("Error: JIRA_USER and JIRA_API_TOKEN environment variables must be set."),process.exit(1)),console.log(`Jira reporter${n?" [dry-run]":""}`),console.log(` config: ${r}`),console.log(` summary: ${e}`),console.log("");try{let i=await M({scanSummaryPath:e,jiraConfigPath:r,jiraUser:t,jiraApiToken:s,dryRun:n});console.log(""),console.log("=== Jira Report Summary ==="),console.log(` Created : ${i.created.length}`),console.log(` Skipped : ${i.skipped.length}`),console.log(` Errors : ${i.errors.length}`),i.errors.length>0&&(process.exitCode=1)}catch(i){console.error(`Fatal error: ${String(i)}`),process.exit(1)}})();
20
+ `)}(async()=>{let{jiraConfigPath:r,scanSummaryPath:e,dryRun:n}=se(process.argv),t=F.JIRA_USER,s=F.JIRA_API_TOKEN;(!t||!s)&&(console.error("Error: JIRA_USER and JIRA_API_TOKEN environment variables must be set."),process.exit(1)),console.log(`Jira reporter${n?" [dry-run]":""}`),console.log(` config: ${r}`),console.log(` summary: ${e}`),console.log("");try{let i=await U({scanSummaryPath:e,jiraConfigPath:r,jiraUser:t,jiraApiToken:s,dryRun:n});console.log(""),console.log("=== Jira Report Summary ==="),console.log(` Created : ${i.created.length}`),console.log(` Skipped : ${i.skipped.length}`),console.log(` Errors : ${i.errors.length}`),i.errors.length>0&&(process.exitCode=1)}catch(i){console.error(`Fatal error: ${String(i)}`),process.exit(1)}})();
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.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.3",
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.56.1",
52
- "vitest": "4.0.18",
53
- "@inquirer/prompts": "8.3.0"
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",