@cdxgen/cdxgen 13.2.0 → 13.3.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.
- package/README.md +28 -12
- package/bin/cdxgen.js +227 -119
- package/bin/evinse.js +5 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/remediations.json +24 -0
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +43 -4
- package/lib/cli/jsBom.js +49 -24
- package/lib/cli/jvmBom.js +46 -12
- package/lib/cli/managedBom.js +112 -41
- package/lib/cli/nativeBom.js +125 -14
- package/lib/core/env.js +5 -6
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +64 -31
- package/lib/ecosystems/ecosystems.js +19 -8
- package/lib/ecosystems/gradleutils.js +42 -7
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +10 -3
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-dotnet.js +51 -10
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-misc.js +747 -161
- package/lib/ecosystems/parsers-python.js +26 -14
- package/lib/ecosystems/parsers-rust.js +24 -18
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +32 -1
- package/lib/evinser/evinser.js +436 -128
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +550 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/evidenceUtils.js +14 -6
- package/lib/inventory/fetchBatch.js +116 -17
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +37 -19
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/stages/postgen/postgen.js +68 -15
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +20 -20
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +59 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +64 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Buffer, constants } from "node:buffer";
|
|
2
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The largest file that `JSON.parse(readFileSync(file, "utf-8"))` can always
|
|
6
|
+
* decode. V8 caps a string at this many UTF-16 code units, and UTF-8 never
|
|
7
|
+
* decodes to more code units than it has bytes.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_JSON_TEXT_BYTES = constants.MAX_STRING_LENGTH;
|
|
10
|
+
|
|
11
|
+
// A value at most this large is decoded with one JSON.parse. A larger object is
|
|
12
|
+
// decoded member by member, and a larger array in runs of elements of about
|
|
13
|
+
// this size, so no string ever holds more than one run.
|
|
14
|
+
const BATCH_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
const CHUNK_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
const QUOTE = 0x22;
|
|
18
|
+
const BACKSLASH = 0x5c;
|
|
19
|
+
const COMMA = 0x2c;
|
|
20
|
+
const COLON = 0x3a;
|
|
21
|
+
const OPEN_BRACE = 0x7b;
|
|
22
|
+
const CLOSE_BRACE = 0x7d;
|
|
23
|
+
const OPEN_BRACKET = 0x5b;
|
|
24
|
+
const CLOSE_BRACKET = 0x5d;
|
|
25
|
+
|
|
26
|
+
const isWhitespace = (byte) =>
|
|
27
|
+
byte === 0x20 || byte === 0x0a || byte === 0x0d || byte === 0x09;
|
|
28
|
+
|
|
29
|
+
// "-", a digit, or the first letter of true, false or null.
|
|
30
|
+
const isScalarStart = (byte) =>
|
|
31
|
+
byte === 0x2d ||
|
|
32
|
+
(byte >= 0x30 && byte <= 0x39) ||
|
|
33
|
+
byte === 0x74 ||
|
|
34
|
+
byte === 0x66 ||
|
|
35
|
+
byte === 0x6e;
|
|
36
|
+
|
|
37
|
+
// Assign like JSON.parse does: "__proto__" becomes an own property instead of
|
|
38
|
+
// replacing the prototype.
|
|
39
|
+
function defineMember(target, key, value) {
|
|
40
|
+
if (key === "__proto__") {
|
|
41
|
+
Object.defineProperty(target, key, {
|
|
42
|
+
configurable: true,
|
|
43
|
+
enumerable: true,
|
|
44
|
+
value,
|
|
45
|
+
writable: true,
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
target[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applySpec(value, spec) {
|
|
53
|
+
if (!spec || value === null || typeof value !== "object") {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return spec.filter
|
|
58
|
+
? value.filter((element) => spec.filter(element))
|
|
59
|
+
: value;
|
|
60
|
+
}
|
|
61
|
+
if (!spec.members) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const picked = {};
|
|
65
|
+
for (const key of Object.keys(value)) {
|
|
66
|
+
if (Object.hasOwn(spec.members, key)) {
|
|
67
|
+
defineMember(picked, key, applySpec(value[key], spec.members[key]));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return picked;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reads bytes [start, end) of an open file a chunk at a time, and can capture
|
|
75
|
+
* the bytes of a span that crosses chunk boundaries.
|
|
76
|
+
*/
|
|
77
|
+
class ByteCursor {
|
|
78
|
+
constructor(fd, start, end, chunkBytes) {
|
|
79
|
+
this.fd = fd;
|
|
80
|
+
this.end = end;
|
|
81
|
+
this.buf = Buffer.allocUnsafe(
|
|
82
|
+
Math.max(1, Math.min(chunkBytes, end - start)),
|
|
83
|
+
);
|
|
84
|
+
this.bufStart = start;
|
|
85
|
+
this.len = 0;
|
|
86
|
+
this.pos = 0;
|
|
87
|
+
this.captureFrom = -1;
|
|
88
|
+
this.parts = [];
|
|
89
|
+
this.partsBytes = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get offset() {
|
|
93
|
+
return this.bufStart + this.pos;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fill() {
|
|
97
|
+
if (this.captureFrom >= 0) {
|
|
98
|
+
if (this.len > this.captureFrom) {
|
|
99
|
+
const part = Buffer.from(this.buf.subarray(this.captureFrom, this.len));
|
|
100
|
+
this.parts.push(part);
|
|
101
|
+
this.partsBytes += part.length;
|
|
102
|
+
}
|
|
103
|
+
this.captureFrom = 0;
|
|
104
|
+
}
|
|
105
|
+
this.bufStart += this.len;
|
|
106
|
+
const want = Math.min(this.buf.length, this.end - this.bufStart);
|
|
107
|
+
this.len =
|
|
108
|
+
want > 0 ? readSync(this.fd, this.buf, 0, want, this.bufStart) : 0;
|
|
109
|
+
this.pos = 0;
|
|
110
|
+
return this.len > 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
peek() {
|
|
114
|
+
if (this.pos >= this.len && !this.fill()) {
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
return this.buf[this.pos];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The next byte that is not whitespace, left unconsumed; -1 at the end.
|
|
121
|
+
peekToken() {
|
|
122
|
+
for (;;) {
|
|
123
|
+
const byte = this.peek();
|
|
124
|
+
if (!isWhitespace(byte)) {
|
|
125
|
+
return byte;
|
|
126
|
+
}
|
|
127
|
+
this.pos++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
nextToken() {
|
|
132
|
+
const byte = this.peekToken();
|
|
133
|
+
if (byte >= 0) {
|
|
134
|
+
this.pos++;
|
|
135
|
+
}
|
|
136
|
+
return byte;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fail(message) {
|
|
140
|
+
throw new SyntaxError(`${message} at byte ${this.offset} of the JSON file`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
beginCapture() {
|
|
144
|
+
this.peek();
|
|
145
|
+
this.captureFrom = this.pos;
|
|
146
|
+
this.parts = [];
|
|
147
|
+
this.partsBytes = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
capturedBytes() {
|
|
151
|
+
return this.partsBytes + this.pos - this.captureFrom;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The captured text up to `until`, an index into the current chunk.
|
|
155
|
+
endCapture(until = this.pos) {
|
|
156
|
+
const tail = this.buf.subarray(this.captureFrom, until);
|
|
157
|
+
const text = this.parts.length
|
|
158
|
+
? Buffer.concat([...this.parts, tail]).toString("utf8")
|
|
159
|
+
: tail.toString("utf8");
|
|
160
|
+
this.captureFrom = -1;
|
|
161
|
+
this.parts = [];
|
|
162
|
+
this.partsBytes = 0;
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Move past one JSON value without decoding it. Brackets are balanced and
|
|
167
|
+
// strings are honoured, but the value is not otherwise validated; whatever
|
|
168
|
+
// decodes it later does that.
|
|
169
|
+
skipValue() {
|
|
170
|
+
const first = this.peekToken();
|
|
171
|
+
if (first === QUOTE) {
|
|
172
|
+
this.pos++;
|
|
173
|
+
this.skipStringBody();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (first === OPEN_BRACE || first === OPEN_BRACKET) {
|
|
177
|
+
this.skipContainer();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!isScalarStart(first)) {
|
|
181
|
+
this.fail("Unexpected token");
|
|
182
|
+
}
|
|
183
|
+
// A number, true, false or null runs to the next delimiter.
|
|
184
|
+
for (;;) {
|
|
185
|
+
const byte = this.peek();
|
|
186
|
+
if (
|
|
187
|
+
byte < 0 ||
|
|
188
|
+
byte === COMMA ||
|
|
189
|
+
byte === CLOSE_BRACE ||
|
|
190
|
+
byte === CLOSE_BRACKET ||
|
|
191
|
+
isWhitespace(byte)
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.pos++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
skipStringBody() {
|
|
200
|
+
let escaped = false;
|
|
201
|
+
for (;;) {
|
|
202
|
+
if (this.pos >= this.len && !this.fill()) {
|
|
203
|
+
this.fail("Unterminated string");
|
|
204
|
+
}
|
|
205
|
+
const { buf, len } = this;
|
|
206
|
+
for (let i = this.pos; i < len; i++) {
|
|
207
|
+
const byte = buf[i];
|
|
208
|
+
if (escaped) {
|
|
209
|
+
escaped = false;
|
|
210
|
+
} else if (byte === BACKSLASH) {
|
|
211
|
+
escaped = true;
|
|
212
|
+
} else if (byte === QUOTE) {
|
|
213
|
+
this.pos = i + 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.pos = len;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
skipContainer() {
|
|
222
|
+
let depth = 0;
|
|
223
|
+
let inString = false;
|
|
224
|
+
let escaped = false;
|
|
225
|
+
for (;;) {
|
|
226
|
+
if (this.pos >= this.len && !this.fill()) {
|
|
227
|
+
this.fail("Unterminated container");
|
|
228
|
+
}
|
|
229
|
+
const { buf, len } = this;
|
|
230
|
+
for (let i = this.pos; i < len; i++) {
|
|
231
|
+
const byte = buf[i];
|
|
232
|
+
if (inString) {
|
|
233
|
+
if (escaped) {
|
|
234
|
+
escaped = false;
|
|
235
|
+
} else if (byte === BACKSLASH) {
|
|
236
|
+
escaped = true;
|
|
237
|
+
} else if (byte === QUOTE) {
|
|
238
|
+
inString = false;
|
|
239
|
+
}
|
|
240
|
+
} else if (byte === QUOTE) {
|
|
241
|
+
inString = true;
|
|
242
|
+
} else if (byte === OPEN_BRACE || byte === OPEN_BRACKET) {
|
|
243
|
+
depth++;
|
|
244
|
+
} else if (byte === CLOSE_BRACE || byte === CLOSE_BRACKET) {
|
|
245
|
+
depth--;
|
|
246
|
+
if (depth === 0) {
|
|
247
|
+
this.pos = i + 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.pos = len;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The members of the object at the cursor, as { key, start, end } byte ranges
|
|
258
|
+
// of their values, in file order. A repeated key keeps its first position and
|
|
259
|
+
// its last range, which is what JSON.parse does.
|
|
260
|
+
function scanObjectMembers(cursor) {
|
|
261
|
+
if (cursor.nextToken() !== OPEN_BRACE) {
|
|
262
|
+
cursor.fail("Expected an object");
|
|
263
|
+
}
|
|
264
|
+
const members = new Map();
|
|
265
|
+
if (cursor.peekToken() === CLOSE_BRACE) {
|
|
266
|
+
cursor.pos++;
|
|
267
|
+
return members;
|
|
268
|
+
}
|
|
269
|
+
for (;;) {
|
|
270
|
+
if (cursor.peekToken() !== QUOTE) {
|
|
271
|
+
cursor.fail("Expected a member name");
|
|
272
|
+
}
|
|
273
|
+
cursor.beginCapture();
|
|
274
|
+
cursor.skipValue();
|
|
275
|
+
const key = JSON.parse(cursor.endCapture());
|
|
276
|
+
if (cursor.nextToken() !== COLON) {
|
|
277
|
+
cursor.fail("Expected ':'");
|
|
278
|
+
}
|
|
279
|
+
cursor.peekToken();
|
|
280
|
+
const start = cursor.offset;
|
|
281
|
+
cursor.skipValue();
|
|
282
|
+
members.set(key, { start, end: cursor.offset });
|
|
283
|
+
const delimiter = cursor.nextToken();
|
|
284
|
+
if (delimiter === CLOSE_BRACE) {
|
|
285
|
+
return members;
|
|
286
|
+
}
|
|
287
|
+
if (delimiter !== COMMA) {
|
|
288
|
+
cursor.fail("Expected ',' or '}'");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Decode the array at the cursor in runs of whole elements, keeping those that
|
|
294
|
+
// `filter` accepts.
|
|
295
|
+
function readArrayElements(cursor, filter, batchBytes) {
|
|
296
|
+
if (cursor.nextToken() !== OPEN_BRACKET) {
|
|
297
|
+
cursor.fail("Expected an array");
|
|
298
|
+
}
|
|
299
|
+
const elements = [];
|
|
300
|
+
if (cursor.peekToken() === CLOSE_BRACKET) {
|
|
301
|
+
cursor.pos++;
|
|
302
|
+
return elements;
|
|
303
|
+
}
|
|
304
|
+
const flush = () => {
|
|
305
|
+
// Everything captured but the delimiter just consumed.
|
|
306
|
+
for (const element of JSON.parse(
|
|
307
|
+
`[${cursor.endCapture(cursor.pos - 1)}]`,
|
|
308
|
+
)) {
|
|
309
|
+
if (!filter || filter(element)) {
|
|
310
|
+
elements.push(element);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
cursor.beginCapture();
|
|
315
|
+
for (;;) {
|
|
316
|
+
cursor.skipValue();
|
|
317
|
+
const delimiter = cursor.nextToken();
|
|
318
|
+
if (delimiter === CLOSE_BRACKET) {
|
|
319
|
+
flush();
|
|
320
|
+
return elements;
|
|
321
|
+
}
|
|
322
|
+
if (delimiter !== COMMA) {
|
|
323
|
+
cursor.fail("Expected ',' or ']'");
|
|
324
|
+
}
|
|
325
|
+
if (cursor.capturedBytes() >= batchBytes) {
|
|
326
|
+
flush();
|
|
327
|
+
cursor.peekToken();
|
|
328
|
+
cursor.beginCapture();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function readBytes(fd, start, end) {
|
|
334
|
+
const bytes = Buffer.allocUnsafe(end - start);
|
|
335
|
+
let read = 0;
|
|
336
|
+
while (read < bytes.length) {
|
|
337
|
+
const count = readSync(fd, bytes, read, bytes.length - read, start + read);
|
|
338
|
+
if (!count) {
|
|
339
|
+
throw new SyntaxError("Unexpected end of the JSON file");
|
|
340
|
+
}
|
|
341
|
+
read += count;
|
|
342
|
+
}
|
|
343
|
+
return bytes.toString("utf8");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function decodeRange(fd, start, end, spec, options) {
|
|
347
|
+
if (end - start <= options.batchBytes) {
|
|
348
|
+
return applySpec(JSON.parse(readBytes(fd, start, end)), spec);
|
|
349
|
+
}
|
|
350
|
+
const cursor = new ByteCursor(fd, start, end, options.chunkBytes);
|
|
351
|
+
const first = cursor.peekToken();
|
|
352
|
+
let value;
|
|
353
|
+
if (first === OPEN_BRACE) {
|
|
354
|
+
value = {};
|
|
355
|
+
for (const [key, range] of scanObjectMembers(cursor)) {
|
|
356
|
+
if (spec?.members && !Object.hasOwn(spec.members, key)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
defineMember(
|
|
360
|
+
value,
|
|
361
|
+
key,
|
|
362
|
+
decodeRange(fd, range.start, range.end, spec?.members?.[key], options),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
} else if (first === OPEN_BRACKET) {
|
|
366
|
+
value = readArrayElements(cursor, spec?.filter, options.batchBytes);
|
|
367
|
+
} else {
|
|
368
|
+
// A string or number this large has no smaller parts; JSON.parse reports
|
|
369
|
+
// it the way it would for the whole file.
|
|
370
|
+
return JSON.parse(readBytes(fd, start, end));
|
|
371
|
+
}
|
|
372
|
+
if (cursor.peekToken() !== -1) {
|
|
373
|
+
cursor.fail("Unexpected data after the JSON value");
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function withFile(filePath, callback) {
|
|
379
|
+
const fd = openSync(filePath, "r");
|
|
380
|
+
try {
|
|
381
|
+
return callback(fd);
|
|
382
|
+
} finally {
|
|
383
|
+
closeSync(fd);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resolveOptions(options = {}) {
|
|
388
|
+
return {
|
|
389
|
+
batchBytes: options.batchBytes ?? BATCH_BYTES,
|
|
390
|
+
chunkBytes: options.chunkBytes ?? CHUNK_BYTES,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Locate the value of each member of the top-level object in a JSON file,
|
|
396
|
+
* without decoding any of them.
|
|
397
|
+
*
|
|
398
|
+
* @param {string} filePath JSON file whose top-level value is an object
|
|
399
|
+
* @param {Object} [options] Chunk size override, for tests
|
|
400
|
+
* @returns {Map<string, {start: number, end: number}>} Byte range of each member's value, in file order. A repeated key keeps its last value, as JSON.parse does.
|
|
401
|
+
* @throws {SyntaxError} When the top-level value is not a well-formed object
|
|
402
|
+
*/
|
|
403
|
+
export function indexJsonObject(filePath, options = {}) {
|
|
404
|
+
const { chunkBytes } = resolveOptions(options);
|
|
405
|
+
return withFile(filePath, (fd) => {
|
|
406
|
+
const cursor = new ByteCursor(fd, 0, statSync(filePath).size, chunkBytes);
|
|
407
|
+
const members = scanObjectMembers(cursor);
|
|
408
|
+
if (cursor.peekToken() !== -1) {
|
|
409
|
+
cursor.fail("Unexpected data after the JSON value");
|
|
410
|
+
}
|
|
411
|
+
return members;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Decode the JSON value stored in a byte range of a file, without ever holding
|
|
417
|
+
* more than a bounded run of it in one string.
|
|
418
|
+
*
|
|
419
|
+
* `spec` narrows what is kept. `{ members: { name: spec } }` keeps only the
|
|
420
|
+
* named members of an object, each narrowed by its own spec. `{ filter }`
|
|
421
|
+
* keeps the elements of an array that `filter(element)` accepts. Anything else
|
|
422
|
+
* keeps the whole value.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} filePath JSON file
|
|
425
|
+
* @param {{start: number, end: number}} range Byte range of the value, such as one returned by indexJsonObject
|
|
426
|
+
* @param {Object} [spec] What to keep
|
|
427
|
+
* @param {Object} [options] Batch and chunk size overrides, for tests
|
|
428
|
+
* @returns {*} The decoded value
|
|
429
|
+
* @throws {SyntaxError} When the value is malformed
|
|
430
|
+
*/
|
|
431
|
+
export function readJsonRange(filePath, range, spec = undefined, options = {}) {
|
|
432
|
+
return withFile(filePath, (fd) =>
|
|
433
|
+
decodeRange(fd, range.start, range.end, spec, resolveOptions(options)),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Parse a JSON file of any size. A file that fits in one string is decoded
|
|
439
|
+
* with JSON.parse, and a larger one is streamed through readJsonRange, so the
|
|
440
|
+
* result is the same either way.
|
|
441
|
+
*
|
|
442
|
+
* @param {string} filePath JSON file
|
|
443
|
+
* @param {Object} [spec] What to keep, as for readJsonRange
|
|
444
|
+
* @param {Object} [options] Size overrides, for tests: maxTextBytes, batchBytes, chunkBytes
|
|
445
|
+
* @returns {*} The decoded value
|
|
446
|
+
* @throws {SyntaxError} When the file is malformed
|
|
447
|
+
*/
|
|
448
|
+
export function readJsonFile(filePath, spec = undefined, options = {}) {
|
|
449
|
+
const size = statSync(filePath).size;
|
|
450
|
+
if (size <= (options.maxTextBytes ?? MAX_JSON_TEXT_BYTES)) {
|
|
451
|
+
return applySpec(JSON.parse(readFileSync(filePath, "utf-8")), spec);
|
|
452
|
+
}
|
|
453
|
+
return readJsonRange(filePath, { start: 0, end: size }, spec, options);
|
|
454
|
+
}
|
package/lib/parsers/npmrc.js
CHANGED
|
@@ -23,9 +23,24 @@ export const DEFAULT_NPMRC_BLOCKLIST = new Set([
|
|
|
23
23
|
"rebuild-bundle",
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Keys that collide with JavaScript object internals. A crafted .npmrc using
|
|
28
|
+
* them must never reach assignment on the result object: `result.__proto__`
|
|
29
|
+
* reads or replaces Object.prototype instead of creating an own property,
|
|
30
|
+
* which historically crashed the array branch with a TypeError.
|
|
31
|
+
*
|
|
32
|
+
* @type {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
const UNSAFE_NPMRC_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
35
|
+
|
|
26
36
|
/**
|
|
27
37
|
* Parse .npmrc content into a plain key-value object.
|
|
28
38
|
*
|
|
39
|
+
* Keys that collide with JavaScript object internals are skipped: assigning
|
|
40
|
+
* them on a plain object either silently mutates Object.prototype semantics or,
|
|
41
|
+
* for the array syntax, crashed the parser with a TypeError because
|
|
42
|
+
* `result.__proto__` resolves to Object.prototype instead of an own property.
|
|
43
|
+
*
|
|
29
44
|
* @param {string} content - Raw .npmrc file content
|
|
30
45
|
* @returns {Object} Parsed key-value pairs
|
|
31
46
|
*/
|
|
@@ -46,6 +61,9 @@ export function parseNpmrc(content) {
|
|
|
46
61
|
if (isArray) {
|
|
47
62
|
key = key.slice(0, -2);
|
|
48
63
|
}
|
|
64
|
+
if (UNSAFE_NPMRC_KEYS.has(key)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
49
67
|
if (
|
|
50
68
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
51
69
|
(value.startsWith("'") && value.endsWith("'"))
|
|
@@ -53,7 +71,7 @@ export function parseNpmrc(content) {
|
|
|
53
71
|
value = value.slice(1, -1);
|
|
54
72
|
}
|
|
55
73
|
if (isArray) {
|
|
56
|
-
if (!result[key]) result[key] = [];
|
|
74
|
+
if (!Array.isArray(result[key])) result[key] = [];
|
|
57
75
|
result[key].push(value);
|
|
58
76
|
} else {
|
|
59
77
|
result[key] = value;
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../../core/fs.js";
|
|
23
23
|
import { thoughtLog } from "../../core/logger.js";
|
|
24
24
|
import { dirNameStr } from "../../core/paths.js";
|
|
25
|
+
import { sanitizeBomPropertyValue } from "../../core/propertySanitizer.js";
|
|
25
26
|
import { CDXGEN_TOOL_GROUP, CDXGEN_TOOL_NAME } from "../../core/state.js";
|
|
26
27
|
import {
|
|
27
28
|
AI_INVENTORY_PROJECT_TYPES,
|
|
@@ -741,24 +742,18 @@ function applyReleaseNotes(bomJson, options, filePath) {
|
|
|
741
742
|
*/
|
|
742
743
|
export function applyMetadata(bomJson, options) {
|
|
743
744
|
if (!bomJson?.components) {
|
|
744
|
-
return bomJson;
|
|
745
|
+
return applyCustomMetadataProperties(bomJson, options);
|
|
745
746
|
}
|
|
746
747
|
const bomPkgTypes = new Set();
|
|
747
748
|
const bomPkgNamespaces = new Set();
|
|
748
749
|
const bomSrcFiles = new Set();
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (purlObj?.namespace) {
|
|
757
|
-
bomPkgNamespaces.add(purlObj.namespace);
|
|
758
|
-
}
|
|
759
|
-
} catch (_e) {
|
|
760
|
-
// ignore
|
|
761
|
-
}
|
|
750
|
+
// Collect the manifests a component was identified from, rewriting any
|
|
751
|
+
// absolute path to a relative one as a side effect. Shared by the components
|
|
752
|
+
// and by metadata.component, which a collector such as the ruby one describes
|
|
753
|
+
// from the project's own gemspec (discussion 4410).
|
|
754
|
+
const collectSrcFiles = (comp) => {
|
|
755
|
+
if (!comp) {
|
|
756
|
+
return;
|
|
762
757
|
}
|
|
763
758
|
if (comp.properties) {
|
|
764
759
|
for (const aprop of comp.properties) {
|
|
@@ -790,6 +785,10 @@ export function applyMetadata(bomJson, options) {
|
|
|
790
785
|
Array.isArray(aidentityEvidence.methods)
|
|
791
786
|
) {
|
|
792
787
|
for (const amethod of aidentityEvidence.methods) {
|
|
788
|
+
// `value` is optional: an attestation, for one, names no file.
|
|
789
|
+
if (typeof amethod?.value !== "string") {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
793
792
|
const rdir = relativeDir(amethod.value, options);
|
|
794
793
|
if (
|
|
795
794
|
comp.type !== "file" &&
|
|
@@ -806,7 +805,24 @@ export function applyMetadata(bomJson, options) {
|
|
|
806
805
|
}
|
|
807
806
|
}
|
|
808
807
|
}
|
|
808
|
+
};
|
|
809
|
+
for (const comp of bomJson.components) {
|
|
810
|
+
if (comp.purl) {
|
|
811
|
+
try {
|
|
812
|
+
const purlObj = Purl.parse(comp.purl);
|
|
813
|
+
if (purlObj?.type) {
|
|
814
|
+
bomPkgTypes.add(purlObj.type);
|
|
815
|
+
}
|
|
816
|
+
if (purlObj?.namespace) {
|
|
817
|
+
bomPkgNamespaces.add(purlObj.namespace);
|
|
818
|
+
}
|
|
819
|
+
} catch (_e) {
|
|
820
|
+
// ignore
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
collectSrcFiles(comp);
|
|
809
824
|
}
|
|
825
|
+
collectSrcFiles(bomJson.metadata?.component);
|
|
810
826
|
if (!bomJson.metadata.properties) {
|
|
811
827
|
bomJson.metadata.properties = [];
|
|
812
828
|
}
|
|
@@ -862,6 +878,43 @@ export function applyMetadata(bomJson, options) {
|
|
|
862
878
|
thoughtLog("BOM lacks package manifest details. Please help us improve!");
|
|
863
879
|
}
|
|
864
880
|
}
|
|
881
|
+
return applyCustomMetadataProperties(bomJson, options);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Add the metadata properties the user asked for on the command line, in the
|
|
886
|
+
* environment, or in a config file (discussion 4391), after whatever the scan
|
|
887
|
+
* itself discovered. They never replace a discovered property: the scan's own
|
|
888
|
+
* names, such as `cdx:bom:componentSrcFiles`, keep their values. A BOM without
|
|
889
|
+
* components still gets them.
|
|
890
|
+
*
|
|
891
|
+
* @param {Object} bomJson BOM JSON Object
|
|
892
|
+
* @param {Object} options CLI options, with `metadataProperties` as
|
|
893
|
+
* `[{name, value}]`
|
|
894
|
+
*
|
|
895
|
+
* @returns {Object} BOM JSON Object
|
|
896
|
+
*/
|
|
897
|
+
function applyCustomMetadataProperties(bomJson, options) {
|
|
898
|
+
if (!bomJson?.metadata || !options?.metadataProperties?.length) {
|
|
899
|
+
return bomJson;
|
|
900
|
+
}
|
|
901
|
+
bomJson.metadata.properties = bomJson.metadata.properties || [];
|
|
902
|
+
for (const aprop of options.metadataProperties) {
|
|
903
|
+
if (!aprop?.name?.length || aprop.value === undefined) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const value = sanitizeBomPropertyValue(aprop.name, `${aprop.value}`);
|
|
907
|
+
if (!value?.length) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (
|
|
911
|
+
!bomJson.metadata.properties.some(
|
|
912
|
+
(p) => p.name === aprop.name && p.value === value,
|
|
913
|
+
)
|
|
914
|
+
) {
|
|
915
|
+
bomJson.metadata.properties.push({ name: aprop.name, value });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
865
918
|
return bomJson;
|
|
866
919
|
}
|
|
867
920
|
|
|
@@ -1503,7 +1556,7 @@ export function annotate(bomJson, options) {
|
|
|
1503
1556
|
for (const dep of bomJson.dependencies) {
|
|
1504
1557
|
const newRef = `${bomLinkPrefix}${stripBomLink(bomJson.serialNumber, bomJson.version, dep.ref)}`;
|
|
1505
1558
|
const newDependsOn = [];
|
|
1506
|
-
for (const adon of dep.dependsOn) {
|
|
1559
|
+
for (const adon of dep.dependsOn || []) {
|
|
1507
1560
|
newDependsOn.push(
|
|
1508
1561
|
`${bomLinkPrefix}${stripBomLink(bomJson.serialNumber, bomJson.version, adon)}`,
|
|
1509
1562
|
);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
toCycloneDxSpecVersionString,
|
|
16
16
|
} from "../inventory/bomUtils.js";
|
|
17
17
|
import { isPartialTree } from "../inventory/depsUtils.js";
|
|
18
|
-
import { isValidPurl } from "../inventory/purl.js";
|
|
18
|
+
import { isValidPurl, parsePurlLoose } from "../inventory/purl.js";
|
|
19
19
|
|
|
20
20
|
const dirName = dirNameStr;
|
|
21
21
|
// The v13 floor is spec 1.6. 1.4 and 1.5 were dropped here so that the JS and
|
|
@@ -790,17 +790,13 @@ export const validateRefs = (bomJson) => {
|
|
|
790
790
|
if (!refMap[dep.ref]) {
|
|
791
791
|
warningsList.push(`Invalid ref in dependencies ${dep.ref}`);
|
|
792
792
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
parentPurlType = purlObj.type;
|
|
797
|
-
} catch (_e) {
|
|
798
|
-
// pass
|
|
799
|
-
}
|
|
793
|
+
// A ref is the decoded form of a purl, so a version with `+` does not
|
|
794
|
+
// parse strictly; the type is all that is needed here.
|
|
795
|
+
const parentPurlType = parsePurlLoose(dep.ref)?.type;
|
|
800
796
|
if (
|
|
801
797
|
parentComponentRef &&
|
|
802
798
|
dep.ref === parentComponentRef &&
|
|
803
|
-
dep.dependsOn
|
|
799
|
+
!dep.dependsOn?.length &&
|
|
804
800
|
bomJson.dependencies.length > 1
|
|
805
801
|
) {
|
|
806
802
|
warningsList.push(
|
|
@@ -814,13 +810,7 @@ export const validateRefs = (bomJson) => {
|
|
|
814
810
|
`Invalid ref in dependencies.dependsOn ${don}. Parent: ${dep.ref}`,
|
|
815
811
|
);
|
|
816
812
|
}
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
const purlObj = Purl.parse(don);
|
|
820
|
-
childPurlType = purlObj.type;
|
|
821
|
-
} catch (_e) {
|
|
822
|
-
// pass
|
|
823
|
-
}
|
|
813
|
+
const childPurlType = parsePurlLoose(don)?.type;
|
|
824
814
|
if (
|
|
825
815
|
parentPurlType &&
|
|
826
816
|
childPurlType &&
|