@conterra/vuln-scan 0.0.36 → 0.1.1
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 +38 -41
- package/dist/add-statement.js +120 -120
- package/dist/jira-reporter.js +7 -7
- package/dist/schema/jira-config-schema.json +32 -9
- package/dist/schema/summary-schema.json +12 -8
- package/dist/vuln-scan.js +292 -292
- package/package.json +3 -3
package/dist/jira-reporter.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
3
|
-
`,s+=o)}return s}async function
|
|
4
|
-
`),a));continue}let o=s.match(/^(#{1,3})\s+(.+)$/);if(o){let a=Math.min(o[1].length,3);
|
|
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(`
|
|
5
5
|
|
|
6
|
-
`)}function
|
|
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]
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
-c, --jira-config <path> Path to jira.json configuration file
|
|
10
|
-
(default: ./jira.json)
|
|
9
|
+
-c, --jira-config <path> Path to jira-conf.json configuration file
|
|
10
|
+
(default: ./jira-conf.json)
|
|
11
11
|
-s, --summary <path> Path to scan-summary.json produced by ct-vuln-scan
|
|
12
12
|
(default: ./output/scan-summary.json)
|
|
13
13
|
--dry-run Print what would be created without calling Jira
|
|
@@ -16,4 +16,4 @@ Options:
|
|
|
16
16
|
Required environment variables:
|
|
17
17
|
JIRA_USER Jira account e-mail address
|
|
18
18
|
JIRA_API_TOKEN Jira API token (see https://id.atlassian.com/manage-profile/security/api-tokens)
|
|
19
|
-
`)}(async()=>{let{jiraConfigPath:
|
|
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)}})();
|
|
@@ -41,20 +41,18 @@
|
|
|
41
41
|
"type": "string"
|
|
42
42
|
},
|
|
43
43
|
"issueLabels": {
|
|
44
|
-
"description": "
|
|
44
|
+
"description": "Global Jira labels applied to every issue created by this rule, regardless of addToBoard matching. Merged with any labels from matching addToBoard entries. Example: [\"security\", \"cve\"].",
|
|
45
45
|
"type": "array",
|
|
46
46
|
"items": {
|
|
47
47
|
"type": "string"
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
|
-
"
|
|
51
|
-
"description": "
|
|
52
|
-
"type": "
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"type": "boolean",
|
|
57
|
-
"default": false
|
|
50
|
+
"addToBoard": {
|
|
51
|
+
"description": "Per-board routing rules. Each entry can target a Scrum sprint board (via sprintBoardId) and/or add labels for Kanban board routing. Sprint assignment: the first matching entry with a sprintBoardId wins; subsequent matching sprint entries and their labels are ignored. Kanban entries (no sprintBoardId) always contribute their labels.",
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"$ref": "#/$defs/BoardConfig"
|
|
55
|
+
}
|
|
58
56
|
},
|
|
59
57
|
"reportProjects": {
|
|
60
58
|
"description": "Glob patterns for project identifiers (name@version) whose vulnerabilities should be routed to this Jira project. Use \"*\" to match any string. Examples: \"mapapps@*\", \"smartfinder@4.*\", \"*\".",
|
|
@@ -66,6 +64,31 @@
|
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
}
|
|
67
|
+
},
|
|
68
|
+
"BoardConfig": {
|
|
69
|
+
"description": "Per-board routing rule within addToBoard. Controls which projects activate this entry, whether to assign the issue to an active Scrum sprint, and which labels to attach.",
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"properties": {
|
|
73
|
+
"sprintBoardId": {
|
|
74
|
+
"description": "Jira Scrum board ID. When set, the issue is assigned to the currently active sprint of that board. Omit for Kanban boards (routing is handled by Jira's board JQL filter).",
|
|
75
|
+
"type": "integer"
|
|
76
|
+
},
|
|
77
|
+
"issueLabels": {
|
|
78
|
+
"description": "Labels to attach when this entry matches. For sprint entries: included only if this is the first matching sprint entry. For Kanban entries (no sprintBoardId): always aggregated.",
|
|
79
|
+
"type": "array",
|
|
80
|
+
"items": {
|
|
81
|
+
"type": "string"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"matchProjects": {
|
|
85
|
+
"description": "Glob patterns for project identifiers (name@version) that activate this board entry. Defaults to [\"*\"] when omitted (matches any project). Example: [\"mapapps@*\", \"smartfinder@4.*\"].",
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
69
92
|
}
|
|
70
93
|
}
|
|
71
94
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"vulnerabilities",
|
|
10
10
|
"vulnerabilityToProject",
|
|
11
11
|
"projectToVulnerability",
|
|
12
|
+
"projectsToComponents",
|
|
12
13
|
"noLongerDetectedVulnerabilities"
|
|
13
14
|
],
|
|
14
15
|
"additionalProperties": false,
|
|
@@ -51,6 +52,16 @@
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
},
|
|
55
|
+
"projectsToComponents": {
|
|
56
|
+
"description": "Map of project identifier (name@version) to a map of component purl to its reverse dependency tree node. Contains the component usage trees for all vulnerable components per project.",
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": {
|
|
61
|
+
"$ref": "#/$defs/ComponentNode"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
54
65
|
"noLongerDetectedVulnerabilities": {
|
|
55
66
|
"description": "Map of CVE/vulnerability ID to the list of project identifiers (name@version) where the vulnerability was declared but is no longer detected.",
|
|
56
67
|
"type": "object",
|
|
@@ -90,7 +101,7 @@
|
|
|
90
101
|
"ShortVulnInfo": {
|
|
91
102
|
"description": "Condensed vulnerability information for a single CVE/vulnerability.",
|
|
92
103
|
"type": "object",
|
|
93
|
-
"required": ["id", "severity", "title", "description", "components", "
|
|
104
|
+
"required": ["id", "severity", "title", "description", "components", "refs"],
|
|
94
105
|
"additionalProperties": false,
|
|
95
106
|
"properties": {
|
|
96
107
|
"id": {
|
|
@@ -115,13 +126,6 @@
|
|
|
115
126
|
"type": "string"
|
|
116
127
|
}
|
|
117
128
|
},
|
|
118
|
-
"componentTree": {
|
|
119
|
-
"description": "Reverse dependency tree nodes showing how affected components are used.",
|
|
120
|
-
"type": "array",
|
|
121
|
-
"items": {
|
|
122
|
-
"$ref": "#/$defs/ComponentNode"
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
129
|
"refs": {
|
|
126
130
|
"description": "List of reference URLs (advisories, NVD entries, etc.).",
|
|
127
131
|
"type": "array",
|