spectrum-cli 5.0.2 → 5.1.0

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.
@@ -0,0 +1,180 @@
1
+ const { ensure, stable } = require("../automation");
2
+ const { API } = require("./runtime");
3
+ const HIGH = new Set(["HIGH", "CRITICAL"]);
4
+ const ID_SOURCE =
5
+ "(?:CVE-\\d{4}-\\d{4,}|GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}|NPM-\\d+|NSWG-COR-\\d+)";
6
+ const identifier = new RegExp("^" + ID_SOURCE + "(?![\\s\\S])", "i");
7
+ const ids = (text) =>
8
+ typeof text === "string" ? text.match(new RegExp(ID_SOURCE, "gi")) || [] : [];
9
+ const intersects = (a, b) => {
10
+ const set = new Set(b);
11
+ return a.some((value) => set.has(value));
12
+ };
13
+ function canonical(aliases) {
14
+ const values = [
15
+ ...new Set(
16
+ aliases
17
+ .filter((value) => typeof value === "string" && identifier.test(value))
18
+ .map((value) => value.toUpperCase()),
19
+ ),
20
+ ].sort();
21
+ ensure(values.length, "Advisory has no stable CVE/GHSA/npm identifier");
22
+ return [
23
+ values.find((value) => value.startsWith("CVE-")) || values[0],
24
+ values,
25
+ ];
26
+ }
27
+ function finding(aliases, severity, source, occurrence, extra = {}) {
28
+ const [id, values] = canonical(aliases);
29
+ return {
30
+ id,
31
+ aliases: values,
32
+ severity: severity.toUpperCase(),
33
+ sources: [source],
34
+ occurrences: [occurrence],
35
+ ...extra,
36
+ };
37
+ }
38
+ function mergeFindings(values) {
39
+ let result = [];
40
+ for (const value of values) {
41
+ const matches = result.filter((old) =>
42
+ intersects(old.aliases, value.aliases),
43
+ ),
44
+ combined = structuredClone(value);
45
+ for (const old of matches) {
46
+ result = result.filter((item) => item !== old);
47
+ combined.aliases.push(...old.aliases);
48
+ combined.sources.push(...old.sources);
49
+ combined.occurrences.push(...old.occurrences);
50
+ if (old.severity === "CRITICAL") combined.severity = "CRITICAL";
51
+ }
52
+ [combined.id, combined.aliases] = canonical(combined.aliases);
53
+ combined.sources = [...new Set(combined.sources)].sort();
54
+ combined.occurrences = [
55
+ ...new Map(
56
+ combined.occurrences.map((entry) => [stable(entry), entry]),
57
+ ).values(),
58
+ ];
59
+ result.push(combined);
60
+ }
61
+ return result.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
62
+ }
63
+ async function osvAliases(identifier) {
64
+ if (identifier.toUpperCase().startsWith("GHSA-")) {
65
+ const data = await new API("https://api.osv.dev/v1").get(
66
+ "vulns/" + identifier,
67
+ );
68
+ return [identifier, ...(data.aliases || [])];
69
+ }
70
+ return [identifier];
71
+ }
72
+ async function npmFindings(report, aliases = osvAliases) {
73
+ ensure(
74
+ report &&
75
+ report.vulnerabilities &&
76
+ typeof report.vulnerabilities === "object" &&
77
+ !Array.isArray(report.vulnerabilities) &&
78
+ !report.error,
79
+ "npm audit did not return a valid report",
80
+ );
81
+ const result = [];
82
+ for (const [pkg, entry] of Object.entries(report.vulnerabilities))
83
+ for (const advisory of entry.via || []) {
84
+ if (
85
+ !advisory ||
86
+ typeof advisory !== "object" ||
87
+ !HIGH.has((advisory.severity || "").toUpperCase())
88
+ )
89
+ continue;
90
+ let identifiers = ids(advisory.url || "");
91
+ if (!identifiers.length) {
92
+ ensure(
93
+ Number.isInteger(advisory.source),
94
+ "npm advisory has no identity",
95
+ );
96
+ identifiers = ["NPM-" + advisory.source];
97
+ }
98
+ const expanded = (
99
+ await Promise.all(identifiers.map((key) => aliases(key)))
100
+ ).flat();
101
+ result.push(
102
+ finding(expanded, advisory.severity, "npm", {
103
+ package: advisory.name || pkg,
104
+ entry: pkg,
105
+ nodes: entry.nodes || [],
106
+ range: advisory.range || "",
107
+ fix: entry.fixAvailable ?? false,
108
+ url: advisory.url || "",
109
+ }),
110
+ );
111
+ }
112
+ return mergeFindings(result);
113
+ }
114
+ function trivyFindings(report) {
115
+ ensure(
116
+ report?.SchemaVersion === 2 && Array.isArray(report.Results || []),
117
+ "Invalid Trivy report",
118
+ );
119
+ const result = [];
120
+ for (const target of report.Results || [])
121
+ for (const item of target.Vulnerabilities || [])
122
+ if (HIGH.has(item.Severity))
123
+ result.push(
124
+ finding([item.VulnerabilityID], item.Severity, "runtime", {
125
+ package: item.PkgName,
126
+ installed: item.InstalledVersion || "",
127
+ fixed: item.FixedVersion || "",
128
+ target: target.Target || "",
129
+ url: item.PrimaryURL || "",
130
+ }),
131
+ );
132
+ return mergeFindings(result);
133
+ }
134
+ function nodeFindings(version, feed) {
135
+ const semver = require("semver"),
136
+ result = [];
137
+ ensure(
138
+ feed && typeof feed === "object" && !Array.isArray(feed),
139
+ "Invalid Node feed",
140
+ );
141
+ for (const [key, value] of Object.entries(feed)) {
142
+ ensure(
143
+ value &&
144
+ typeof value.vulnerable === "string" &&
145
+ typeof value.patched === "string",
146
+ "Invalid Node feed",
147
+ );
148
+ if (
149
+ !semver.satisfies(version, value.vulnerable) ||
150
+ semver.satisfies(version, value.patched) ||
151
+ (value.affectedEnvironments &&
152
+ !value.affectedEnvironments.some((x) => ["all", "linux"].includes(x)))
153
+ )
154
+ continue;
155
+ if (!HIGH.has((value.severity || "").toUpperCase())) continue;
156
+ for (const id of value.cve?.length ? value.cve : ["NSWG-COR-" + key])
157
+ result.push(
158
+ finding([id], value.severity, "node", {
159
+ package: "node",
160
+ installed: version,
161
+ fixed: value.patched,
162
+ url: value.ref || "",
163
+ }),
164
+ );
165
+ }
166
+ return result;
167
+ }
168
+ module.exports = {
169
+ HIGH,
170
+ ids,
171
+ identifier,
172
+ intersects,
173
+ canonical,
174
+ finding,
175
+ mergeFindings,
176
+ osvAliases,
177
+ npmFindings,
178
+ trivyFindings,
179
+ nodeFindings,
180
+ };
@@ -0,0 +1,36 @@
1
+ const { registryAuth } = require("./runtime");
2
+ const { Scanner } = require("./scan");
3
+ const { Repairer } = require("./repair");
4
+ const {
5
+ track: trackFindings,
6
+ reconcile: reconcileFindings,
7
+ } = require("./tracking");
8
+
9
+ function withRegistry(action, options = {}) {
10
+ const env = { ...process.env, ...(options.env || {}) };
11
+ return registryAuth(env, (runtime) => action({ ...options, env: runtime }));
12
+ }
13
+
14
+ function scan(options) {
15
+ return withRegistry(
16
+ ({ env, deps, cwd, dry }) => new Scanner(env, deps).scan(cwd, { dry }),
17
+ options,
18
+ );
19
+ }
20
+
21
+ function track(options) {
22
+ return trackFindings(options);
23
+ }
24
+
25
+ function reconcile(options) {
26
+ return reconcileFindings(options);
27
+ }
28
+
29
+ function repair(options) {
30
+ return withRegistry(
31
+ ({ env, deps, cwd, dry }) => new Repairer(env, deps).repair(cwd, { dry }),
32
+ options,
33
+ );
34
+ }
35
+
36
+ module.exports = { scan, track, reconcile, repair };