codekingpro/portable-devtools
114k
1// An object representing a vulnerability either as the result of an
2// advisory or due to the package in question depending exclusively on
3// vulnerable versions of a dep.
4//
5// - name: package name
6// - range: Set of vulnerable versions
7// - nodes: Set of nodes affected
8// - effects: Set of vulns triggered by this one
9// - advisories: Set of advisories (including metavulns) causing this vuln.
10// All of the entries in via are vulnerability objects returned by
11// @npmcli/metavuln-calculator
12// - via: dependency vulns which cause this one
13
14const { satisfies, simplifyRange } = require('semver')
15const semverOpt = { loose: true, includePrerelease: true }
16
17const localeCompare = require('@isaacs/string-locale-compare')('en')
18const npa = require('npm-package-arg')
19
20const severities = new Map([
21 ['info', 0], [0, 'info'],
22 ['low', 1], [1, 'low'],
23 ['moderate', 2], [2, 'moderate'],
24 ['high', 3], [3, 'high'],
25 ['critical', 4], [4, 'critical'],
26 [null, -1], [-1, null],
27])
28
29class Vuln {
30 #range = null
31 #simpleRange = null
32 // assume a fix is available unless it hits a top node
33 // that locks it in place, setting this false or {isSemVerMajor, version}.
34 #fixAvailable = true
35
36 constructor ({ name, advisory }) {
37 this.name = name
38 this.via = new Set()
39 this.advisories = new Set()
40 this.severity = null
41 this.effects = new Set()
42 this.topNodes = new Set()
43 this.nodes = new Set()
44 this.addAdvisory(advisory)
45 this.packument = advisory.packument
46 this.versions = advisory.versions
47 }
48
49 get fixAvailable () {
50 return this.#fixAvailable
51 }
52
53 set fixAvailable (f) {
54 this.#fixAvailable = f
55 // if there's a fix available for this at the top level, it means that
56 // it will also fix the vulns that led to it being there. to get there,
57 // we set the vias to the most "strict" of fix availables.
58 // - false: no fix is available
59 // - {name, version, isSemVerMajor} fix requires -f, is semver major
60 // - {name, version} fix requires -f, not semver major
61 // - true: fix does not require -f
62 // TODO: duped entries may require different fixes but the current
63 // structure does not support this, so the case were a top level fix
64 // corrects a duped entry may mean you have to run fix more than once
65 for (const v of this.via) {
66 // don't blow up on loops
67 if (v.fixAvailable === f) {
68 continue
69 }
70
71 if (f === false) {
72 v.fixAvailable = f
73 } else if (v.fixAvailable === true) {
74 v.fixAvailable = f
75 } else if (typeof f === 'object' && (
76 typeof v.fixAvailable !== 'object' || !v.fixAvailable.isSemVerMajor)) {
77 v.fixAvailable = f
78 }
79 }
80 }
81
82 get isDirect () {
83 for (const node of this.nodes.values()) {
84 for (const edge of node.edgesIn) {
85 if (edge.from.isProjectRoot || edge.from.isWorkspace) {
86 return true
87 }
88 }
89 }
90 return false
91 }
92
93 testSpec (spec) {
94 const specObj = npa(spec)
95 if (!specObj.registry) {
96 return true
97 }
98
99 if (specObj.subSpec) {
100 spec = specObj.subSpec.rawSpec
101 }
102
103 for (const v of this.versions) {
104 if (satisfies(v, spec) && !satisfies(v, this.range, semverOpt)) {
105 return false
106 }
107 }
108 return true
109 }
110
111 toJSON () {
112 return {
113 name: this.name,
114 severity: this.severity,
115 isDirect: this.isDirect,
116 // just loop over the advisories, since via is only Vuln objects,
117 // and calculated advisories have all the info we need
118 via: [...this.advisories].map(v => v.type === 'metavuln' ? v.dependency : {
119 ...v,
120 versions: undefined,
121 vulnerableVersions: undefined,
122 id: undefined,
123 }).sort((a, b) =>
124 localeCompare(String(a.source || a), String(b.source || b))),
125 effects: [...this.effects].map(v => v.name).sort(localeCompare),
126 range: this.simpleRange,
127 nodes: [...this.nodes].map(n => n.location).sort(localeCompare),
128 fixAvailable: this.#fixAvailable,
129 }
130 }
131
132 addVia (v) {
133 this.via.add(v)
134 v.effects.add(this)
135 // call the setter since we might add vias _after_ setting fixAvailable
136 this.fixAvailable = this.fixAvailable
137 }
138
139 deleteVia (v) {
140 this.via.delete(v)
141 v.effects.delete(this)
142 }
143
144 deleteAdvisory (advisory) {
145 this.advisories.delete(advisory)
146 // make sure we have the max severity of all the vulns causing this one
147 this.severity = null
148 this.#range = null
149 this.#simpleRange = null
150 // refresh severity
151 for (const advisory of this.advisories) {
152 this.addAdvisory(advisory)
153 }
154
155 // remove any effects that are no longer relevant
156 const vias = new Set([...this.advisories].map(a => a.dependency))
157 for (const via of this.via) {
158 if (!vias.has(via.name)) {
159 this.deleteVia(via)
160 }
161 }
162 }
163
164 addAdvisory (advisory) {
165 this.advisories.add(advisory)
166 const sev = severities.get(advisory.severity)
167 this.#range = null
168 this.#simpleRange = null
169 if (sev > severities.get(this.severity)) {
170 this.severity = advisory.severity
171 }
172 }
173
174 get range () {
175 if (!this.#range) {
176 this.#range = [...this.advisories].map(v => v.range).join(' || ')
177 }
178 return this.#range
179 }
180
181 get simpleRange () {
182 if (this.#simpleRange && this.#simpleRange === this.#range) {
183 return this.#simpleRange
184 }
185
186 const versions = [...this.advisories][0].versions
187 const range = this.range
188 this.#simpleRange = simplifyRange(versions, range, semverOpt)
189 this.#range = this.#simpleRange
190 return this.#simpleRange
191 }
192
193 isVulnerable (node) {
194 if (this.nodes.has(node)) {
195 return true
196 }
197
198 const { version } = node.package
199 if (!version) {
200 return false
201 }
202
203 for (const v of this.advisories) {
204 if (v.testVersion(version)) {
205 this.nodes.add(node)
206 return true
207 }
208 }
209
210 return false
211 }
212}
213
214module.exports = Vuln
215 