@cdxgen/cdxgen 13.1.0 → 13.2.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 +8 -7
- package/bin/cdxgen.js +16 -3
- package/bin/evinse.js +2 -0
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +23 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +12 -0
- package/lib/cli/index.js +192 -1
- package/lib/cli/jsBom.js +59 -2
- package/lib/cli/jvmBom.js +22 -0
- package/lib/cli/managedBom.js +152 -0
- package/lib/cli/nativeBom.js +798 -1
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +39 -1
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +241 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +143 -2
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +172 -0
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +1 -1
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +932 -17
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +111 -1
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/dosai.js +77 -0
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +74 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +169 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/package.json +25 -25
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- 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 +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.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 +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +29 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +23 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// An ONNX model is a protobuf-encoded ModelProto. Only the header fields are
|
|
4
|
+
// read here; the graph payload is skipped by offset, so inspection stays
|
|
5
|
+
// cheap for multi-gigabyte files and never loads tensors.
|
|
6
|
+
//
|
|
7
|
+
// ModelProto fields (https://onnx.ai/onnx/repo-docs/Model.html):
|
|
8
|
+
// 1 ir_version (varint), 2 producer_name (string),
|
|
9
|
+
// 3 producer_version (string), 4 domain (string),
|
|
10
|
+
// 5 model_version (varint), 6 doc_string (string),
|
|
11
|
+
// 7 graph (message, skipped), 8 opset_import (repeated message).
|
|
12
|
+
// Each opset_import entry is an OperatorSetIdProto:
|
|
13
|
+
// 1 domain (string), 2 version (varint).
|
|
14
|
+
|
|
15
|
+
/** Fields are walked from the front; a hostile file cannot force a scan
|
|
16
|
+
* deeper than this many top-level entries. */
|
|
17
|
+
const MAX_TOP_LEVEL_FIELDS = 128;
|
|
18
|
+
/** Largest single field payload that will actually be read into memory. */
|
|
19
|
+
const MAX_FIELD_BYTES = 4096;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read the identifying header fields of an ONNX model file.
|
|
23
|
+
*
|
|
24
|
+
* Only `ir_version`, `producer_name`, `producer_version`, `model_version`,
|
|
25
|
+
* and the `opset_import` entries are extracted. The graph and every other
|
|
26
|
+
* payload are skipped by offset without being read, so no tensor data is
|
|
27
|
+
* ever loaded and no model code is executed.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} filePath Path to the `.onnx` file
|
|
30
|
+
* @returns {{
|
|
31
|
+
* irVersion: number,
|
|
32
|
+
* producerName: string,
|
|
33
|
+
* producerVersion: string,
|
|
34
|
+
* modelVersion: number,
|
|
35
|
+
* opsets: Array<{domain: string, version: number}>,
|
|
36
|
+
* } | undefined} Header summary, or undefined when it cannot be read
|
|
37
|
+
*/
|
|
38
|
+
export function readOnnxHeader(filePath) {
|
|
39
|
+
let fd;
|
|
40
|
+
try {
|
|
41
|
+
const stats = statSync(filePath);
|
|
42
|
+
if (!stats.isFile() || stats.size < 2) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
fd = openSync(filePath, "r");
|
|
46
|
+
// The whole file is not buffered; fields are fetched by offset. A small
|
|
47
|
+
// sliding buffer is re-used for varints and short strings.
|
|
48
|
+
const reader = new OffsetReader(fd, stats.size);
|
|
49
|
+
const result = {
|
|
50
|
+
irVersion: 0,
|
|
51
|
+
producerName: "",
|
|
52
|
+
producerVersion: "",
|
|
53
|
+
modelVersion: 0,
|
|
54
|
+
opsets: [],
|
|
55
|
+
};
|
|
56
|
+
let fieldsRead = 0;
|
|
57
|
+
for (let i = 0; i < MAX_TOP_LEVEL_FIELDS; i += 1) {
|
|
58
|
+
const tag = reader.readVarint();
|
|
59
|
+
if (tag === undefined) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const fieldNumber = Number(tag >> 3n);
|
|
63
|
+
const wireType = Number(tag & 7n);
|
|
64
|
+
if (fieldNumber === 0) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (wireType === 0) {
|
|
68
|
+
const value = reader.readVarint();
|
|
69
|
+
if (value === undefined) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
fieldsRead += 1;
|
|
73
|
+
if (fieldNumber === 1) {
|
|
74
|
+
result.irVersion = Number(value);
|
|
75
|
+
} else if (fieldNumber === 5) {
|
|
76
|
+
result.modelVersion = Number(value);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (wireType !== 2) {
|
|
81
|
+
// Group and fixed-width wire types do not appear in ModelProto at the
|
|
82
|
+
// top level; stop rather than guess their length.
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const length = reader.readVarint();
|
|
86
|
+
if (length === undefined) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const byteLength = Number(length);
|
|
90
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (fieldNumber === 8 && byteLength <= MAX_FIELD_BYTES) {
|
|
94
|
+
const payload = reader.readBytes(byteLength);
|
|
95
|
+
if (!payload) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const opset = parseOperatorSetEntry(payload);
|
|
99
|
+
if (opset) {
|
|
100
|
+
result.opsets.push(opset);
|
|
101
|
+
}
|
|
102
|
+
fieldsRead += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
(fieldNumber === 2 || fieldNumber === 3) &&
|
|
107
|
+
byteLength <= MAX_FIELD_BYTES
|
|
108
|
+
) {
|
|
109
|
+
const payload = reader.readBytes(byteLength);
|
|
110
|
+
if (!payload) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (fieldNumber === 2) {
|
|
114
|
+
result.producerName = payload.toString("utf-8");
|
|
115
|
+
} else {
|
|
116
|
+
result.producerVersion = payload.toString("utf-8");
|
|
117
|
+
}
|
|
118
|
+
fieldsRead += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Skip anything else (notably the graph) by offset.
|
|
122
|
+
if (!reader.skip(byteLength)) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
fieldsRead += 1;
|
|
126
|
+
}
|
|
127
|
+
// A real ModelProto always carries at least an ir_version; zero consumed
|
|
128
|
+
// fields means the file is not an ONNX model at all.
|
|
129
|
+
if (fieldsRead === 0) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
} finally {
|
|
136
|
+
if (fd !== undefined) {
|
|
137
|
+
closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reader that fetches bytes from a file descriptor by absolute offset.
|
|
144
|
+
*
|
|
145
|
+
* @param {number} fd Open file descriptor
|
|
146
|
+
* @param {number} fileSize Total file size
|
|
147
|
+
*/
|
|
148
|
+
class OffsetReader {
|
|
149
|
+
constructor(fd, fileSize) {
|
|
150
|
+
this.fd = fd;
|
|
151
|
+
this.fileSize = fileSize;
|
|
152
|
+
this.offset = 0;
|
|
153
|
+
this.buffer = Buffer.alloc(16);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read a protobuf varint at the cursor.
|
|
158
|
+
*
|
|
159
|
+
* @returns {bigint|undefined} Value, or undefined at end of input
|
|
160
|
+
*/
|
|
161
|
+
readVarint() {
|
|
162
|
+
let result = 0n;
|
|
163
|
+
let shift = 0n;
|
|
164
|
+
for (let i = 0; i < 10; i += 1) {
|
|
165
|
+
if (!this.fillBuffer(1)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
const byte = this.buffer[0];
|
|
169
|
+
this.offset += 1;
|
|
170
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
171
|
+
if ((byte & 0x80) === 0) {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
shift += 7n;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read a length-delimited payload at the cursor.
|
|
181
|
+
*
|
|
182
|
+
* @param {number} length Payload length
|
|
183
|
+
* @returns {Buffer|undefined} Payload, or undefined when unavailable
|
|
184
|
+
*/
|
|
185
|
+
readBytes(length) {
|
|
186
|
+
const payload = Buffer.alloc(length);
|
|
187
|
+
const bytesRead = readSync(this.fd, payload, 0, length, this.offset);
|
|
188
|
+
if (bytesRead !== length) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
this.offset += length;
|
|
192
|
+
return payload;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Advance the cursor without reading the payload.
|
|
197
|
+
*
|
|
198
|
+
* @param {number} length Bytes to skip
|
|
199
|
+
* @returns {boolean} true when the skip stayed within the file
|
|
200
|
+
*/
|
|
201
|
+
skip(length) {
|
|
202
|
+
const next = this.offset + length;
|
|
203
|
+
if (next > this.fileSize) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
this.offset = next;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Ensure up to `length` bytes are available in the scratch buffer.
|
|
212
|
+
*
|
|
213
|
+
* @param {number} length Bytes needed
|
|
214
|
+
* @returns {boolean} true when the read succeeded
|
|
215
|
+
*/
|
|
216
|
+
fillBuffer(length) {
|
|
217
|
+
if (this.offset + length > this.fileSize) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const bytesRead = readSync(this.fd, this.buffer, 0, length, this.offset);
|
|
221
|
+
return bytesRead === length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse one OperatorSetIdProto payload.
|
|
227
|
+
*
|
|
228
|
+
* @param {Buffer} payload Field payload
|
|
229
|
+
* @returns {{domain: string, version: number}|undefined} Parsed entry
|
|
230
|
+
*/
|
|
231
|
+
function parseOperatorSetEntry(payload) {
|
|
232
|
+
let pos = 0;
|
|
233
|
+
let domain = "";
|
|
234
|
+
let version = 0;
|
|
235
|
+
while (pos < payload.length) {
|
|
236
|
+
const tag = readVarintFrom(payload, pos);
|
|
237
|
+
if (!tag) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
pos = tag.next;
|
|
241
|
+
const fieldNumber = Number(tag.value >> 3n);
|
|
242
|
+
const wireType = Number(tag.value & 7n);
|
|
243
|
+
if (wireType === 0) {
|
|
244
|
+
const value = readVarintFrom(payload, pos);
|
|
245
|
+
if (!value) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
pos = value.next;
|
|
249
|
+
if (fieldNumber === 2) {
|
|
250
|
+
version = Number(value.value);
|
|
251
|
+
}
|
|
252
|
+
} else if (wireType === 2) {
|
|
253
|
+
const length = readVarintFrom(payload, pos);
|
|
254
|
+
if (!length) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
pos = length.next;
|
|
258
|
+
const byteLength = Number(length.value);
|
|
259
|
+
if (pos + byteLength > payload.length) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (fieldNumber === 1) {
|
|
263
|
+
domain = payload.toString("utf-8", pos, pos + byteLength);
|
|
264
|
+
}
|
|
265
|
+
pos += byteLength;
|
|
266
|
+
} else {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { domain, version };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read a varint from a buffer at a position.
|
|
275
|
+
*
|
|
276
|
+
* @param {Buffer} buffer Source buffer
|
|
277
|
+
* @param {number} pos Start position
|
|
278
|
+
* @returns {{value: bigint, next: number}|undefined} Result, or undefined
|
|
279
|
+
*/
|
|
280
|
+
function readVarintFrom(buffer, pos) {
|
|
281
|
+
let value = 0n;
|
|
282
|
+
let shift = 0n;
|
|
283
|
+
let cursor = pos;
|
|
284
|
+
for (let i = 0; i < 10 && cursor < buffer.length; i += 1) {
|
|
285
|
+
const byte = buffer[cursor];
|
|
286
|
+
cursor += 1;
|
|
287
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
288
|
+
if ((byte & 0x80) === 0) {
|
|
289
|
+
return { value, next: cursor };
|
|
290
|
+
}
|
|
291
|
+
shift += 7n;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// A safetensors file begins with an 8-byte little-endian unsigned integer
|
|
4
|
+
// giving the byte length of a JSON header, followed by that JSON header, and
|
|
5
|
+
// then the raw tensor buffers. Only the header is parsed here: tensor data is
|
|
6
|
+
// never read, so inspection stays cheap and side-effect free.
|
|
7
|
+
const HEADER_LENGTH_BYTES = 8;
|
|
8
|
+
// A header longer than this almost certainly indicates a non-safetensors file
|
|
9
|
+
// or a corrupt/hostile input; refuse to allocate for it.
|
|
10
|
+
const MAX_HEADER_BYTES = 100 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read and parse the JSON header of a safetensors model file.
|
|
14
|
+
*
|
|
15
|
+
* Only the leading header is read; tensor payloads are ignored. The returned
|
|
16
|
+
* object separates the optional `__metadata__` map from the per-tensor
|
|
17
|
+
* descriptors.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} filePath Path to a `.safetensors` file
|
|
20
|
+
* @returns {{
|
|
21
|
+
* metadata: Object,
|
|
22
|
+
* tensorCount: number,
|
|
23
|
+
* dtypes: string[],
|
|
24
|
+
* totalParameters: number,
|
|
25
|
+
* } | undefined} Parsed header summary, or undefined when it cannot be read
|
|
26
|
+
*/
|
|
27
|
+
export function readSafetensorsHeader(filePath) {
|
|
28
|
+
let fd;
|
|
29
|
+
try {
|
|
30
|
+
const stats = statSync(filePath);
|
|
31
|
+
if (stats.size < HEADER_LENGTH_BYTES) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
fd = openSync(filePath, "r");
|
|
35
|
+
const lengthBuffer = Buffer.alloc(HEADER_LENGTH_BYTES);
|
|
36
|
+
readSync(fd, lengthBuffer, 0, HEADER_LENGTH_BYTES, 0);
|
|
37
|
+
const headerLength = Number(lengthBuffer.readBigUInt64LE(0));
|
|
38
|
+
if (
|
|
39
|
+
!Number.isSafeInteger(headerLength) ||
|
|
40
|
+
headerLength <= 0 ||
|
|
41
|
+
headerLength > MAX_HEADER_BYTES ||
|
|
42
|
+
headerLength + HEADER_LENGTH_BYTES > stats.size
|
|
43
|
+
) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const headerBuffer = Buffer.alloc(headerLength);
|
|
47
|
+
readSync(fd, headerBuffer, 0, headerLength, HEADER_LENGTH_BYTES);
|
|
48
|
+
const header = JSON.parse(headerBuffer.toString("utf-8"));
|
|
49
|
+
if (!header || typeof header !== "object") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return summarizeHeader(header);
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
} finally {
|
|
56
|
+
if (fd !== undefined) {
|
|
57
|
+
try {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore close failures
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Summarize a parsed safetensors header into metadata and tensor statistics.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} header Parsed safetensors JSON header
|
|
70
|
+
* @returns {{
|
|
71
|
+
* metadata: Object,
|
|
72
|
+
* tensorCount: number,
|
|
73
|
+
* dtypes: string[],
|
|
74
|
+
* totalParameters: number,
|
|
75
|
+
* }} Header summary
|
|
76
|
+
*/
|
|
77
|
+
function summarizeHeader(header) {
|
|
78
|
+
const metadata =
|
|
79
|
+
header.__metadata__ && typeof header.__metadata__ === "object"
|
|
80
|
+
? header.__metadata__
|
|
81
|
+
: {};
|
|
82
|
+
const dtypes = new Set();
|
|
83
|
+
let tensorCount = 0;
|
|
84
|
+
let totalParameters = 0;
|
|
85
|
+
for (const [key, descriptor] of Object.entries(header)) {
|
|
86
|
+
if (
|
|
87
|
+
key === "__metadata__" ||
|
|
88
|
+
!descriptor ||
|
|
89
|
+
typeof descriptor !== "object"
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
tensorCount++;
|
|
94
|
+
if (typeof descriptor.dtype === "string") {
|
|
95
|
+
dtypes.add(descriptor.dtype);
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(descriptor.shape) && descriptor.shape.length) {
|
|
98
|
+
const elements = descriptor.shape.reduce(
|
|
99
|
+
(product, dimension) =>
|
|
100
|
+
Number.isFinite(dimension) ? product * dimension : product,
|
|
101
|
+
1,
|
|
102
|
+
);
|
|
103
|
+
if (Number.isFinite(elements)) {
|
|
104
|
+
totalParameters += elements;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
metadata,
|
|
110
|
+
tensorCount,
|
|
111
|
+
dtypes: Array.from(dtypes).sort(),
|
|
112
|
+
totalParameters,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -28,8 +28,9 @@ const MAX_SUBDIRS_SCANNED = 100;
|
|
|
28
28
|
* Marker table: ecosystem → manifest and lockfile names that indicate the
|
|
29
29
|
* ecosystem is part of the project. Entries are either exact file names or
|
|
30
30
|
* `{ suffix }` matchers for extension-driven ecosystems. Ecosystems cdxgen
|
|
31
|
-
* cannot parse (
|
|
32
|
-
* can report them as coverage gaps instead of silently
|
|
31
|
+
* cannot parse (currently only dub, the D package manager) are included so
|
|
32
|
+
* the reflection can report them as coverage gaps instead of silently
|
|
33
|
+
* ignoring them.
|
|
33
34
|
*
|
|
34
35
|
* @type {Readonly<Record<string, {names: string[], suffixes: string[]}>>}
|
|
35
36
|
*/
|
|
@@ -103,18 +104,40 @@ export const ECOSYSTEM_MARKERS = Object.freeze({
|
|
|
103
104
|
swift: { names: ["Package.swift", "Package.resolved"], suffixes: [] },
|
|
104
105
|
elixir: { names: ["mix.exs", "mix.lock"], suffixes: [] },
|
|
105
106
|
c: {
|
|
106
|
-
names: [
|
|
107
|
+
names: [
|
|
108
|
+
"CMakeLists.txt",
|
|
109
|
+
"conanfile.txt",
|
|
110
|
+
"conanfile.py",
|
|
111
|
+
"xmake-requires.lock",
|
|
112
|
+
],
|
|
107
113
|
suffixes: [],
|
|
108
114
|
},
|
|
109
|
-
haskell: {
|
|
110
|
-
|
|
115
|
+
haskell: {
|
|
116
|
+
names: ["cabal.project", "stack.yaml", "stack.yaml.lock", "stack.lock"],
|
|
117
|
+
suffixes: [".cabal"],
|
|
118
|
+
},
|
|
119
|
+
elm: { names: ["elm.json", "elm-package.json"], suffixes: [] },
|
|
120
|
+
dub: { names: ["dub.json", "dub.sdl"], suffixes: [] },
|
|
111
121
|
crystal: { names: ["shard.yml", "shard.lock"], suffixes: [] },
|
|
112
122
|
nim: { names: ["nim.cfg", "config.nims"], suffixes: [".nimble"] },
|
|
113
123
|
perl: {
|
|
114
|
-
names: [
|
|
124
|
+
names: [
|
|
125
|
+
"cpanfile",
|
|
126
|
+
"cpanfile.snapshot",
|
|
127
|
+
"META.json",
|
|
128
|
+
"META.yml",
|
|
129
|
+
"Makefile.PL",
|
|
130
|
+
"Build.PL",
|
|
131
|
+
],
|
|
115
132
|
suffixes: [],
|
|
116
133
|
},
|
|
117
134
|
r: { names: ["DESCRIPTION", "renv.lock"], suffixes: [] },
|
|
135
|
+
julia: { names: ["Project.toml", "Manifest.toml"], suffixes: [] },
|
|
136
|
+
erlang: { names: ["rebar.config", "rebar.lock"], suffixes: [] },
|
|
137
|
+
ocaml: { names: ["dune-project"], suffixes: [".opam"] },
|
|
138
|
+
lua: { names: ["luarocks.lock"], suffixes: [".rockspec"] },
|
|
139
|
+
spack: { names: ["spack.yaml", "spack.lock"], suffixes: [] },
|
|
140
|
+
terraform: { names: [".terraform.lock.hcl"], suffixes: [] },
|
|
118
141
|
});
|
|
119
142
|
|
|
120
143
|
/** Marker file name → ecosystems claiming it, and marker suffix → claiming
|
|
@@ -565,11 +565,21 @@ export async function postProcess(bomNSData, options, filePath) {
|
|
|
565
565
|
bomNSData.bomJson = applyMetadata(bomNSData.bomJson, options);
|
|
566
566
|
bomNSData.bomJson = applyLicenseEnhancement(bomNSData.bomJson, options);
|
|
567
567
|
bomNSData.bomJson = applyContainerInventoryMetadata(bomNSData.bomJson);
|
|
568
|
+
const buildOnlyComponents = extractBuildOnlyComponents(
|
|
569
|
+
bomNSData.bomJson,
|
|
570
|
+
options,
|
|
571
|
+
);
|
|
568
572
|
bomNSData.bomJson = applyFormulation(
|
|
569
573
|
bomNSData.bomJson,
|
|
570
574
|
options,
|
|
571
575
|
filePath,
|
|
572
|
-
|
|
576
|
+
buildOnlyComponents.length
|
|
577
|
+
? [...(bomNSData.formulationList || []), ...buildOnlyComponents]
|
|
578
|
+
: bomNSData.formulationList,
|
|
579
|
+
);
|
|
580
|
+
bomNSData.bomJson = applyBuildOnlyFormulation(
|
|
581
|
+
bomNSData.bomJson,
|
|
582
|
+
buildOnlyComponents,
|
|
573
583
|
);
|
|
574
584
|
bomNSData.bomJson = applyReleaseNotes(bomNSData.bomJson, options, filePath);
|
|
575
585
|
bomNSData.bomJson = applyComponentTypeFilter(bomNSData.bomJson, options);
|
|
@@ -964,6 +974,154 @@ function getIdentityTechniques(comp) {
|
|
|
964
974
|
return techniques;
|
|
965
975
|
}
|
|
966
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Ensure the components moved out of the assembly have landed in the
|
|
979
|
+
* formulation section.
|
|
980
|
+
*
|
|
981
|
+
* With `--include-formulation` they were handed to the full formulation pass.
|
|
982
|
+
* Without it there is no formulation section, so a minimal one is created for
|
|
983
|
+
* them alone: moving a component out of `components[]` and then dropping it
|
|
984
|
+
* would lose it from the document.
|
|
985
|
+
*
|
|
986
|
+
* @param {Object} bomJson BOM JSON object
|
|
987
|
+
* @param {Object[]} buildOnlyComponents Components removed from `components[]`
|
|
988
|
+
*
|
|
989
|
+
* @returns {Object} The same `bomJson`
|
|
990
|
+
*/
|
|
991
|
+
function applyBuildOnlyFormulation(bomJson, buildOnlyComponents) {
|
|
992
|
+
if (!buildOnlyComponents?.length || !bomJson) {
|
|
993
|
+
return bomJson;
|
|
994
|
+
}
|
|
995
|
+
const alreadyPlaced = (bomJson.formulation || []).some((formula) =>
|
|
996
|
+
formula?.components?.some((comp) =>
|
|
997
|
+
buildOnlyComponents.some((moved) => moved["bom-ref"] === comp["bom-ref"]),
|
|
998
|
+
),
|
|
999
|
+
);
|
|
1000
|
+
if (alreadyPlaced) {
|
|
1001
|
+
return bomJson;
|
|
1002
|
+
}
|
|
1003
|
+
if (!bomJson.formulation) {
|
|
1004
|
+
bomJson.formulation = [];
|
|
1005
|
+
}
|
|
1006
|
+
bomJson.formulation.push({
|
|
1007
|
+
"bom-ref": `${bomJson.metadata?.component?.["bom-ref"] || "cdxgen"}#build-dependencies`,
|
|
1008
|
+
components: buildOnlyComponents,
|
|
1009
|
+
});
|
|
1010
|
+
return bomJson;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Report whether a component exists only to produce the build, rather than
|
|
1015
|
+
* being part of the delivered assembly. A cargo build dependency and a
|
|
1016
|
+
* proc-macro crate are both marked with `cdx:cargo:hostOnly`.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {Object} comp Component
|
|
1019
|
+
* @returns {boolean} True for a build-time-only component
|
|
1020
|
+
*/
|
|
1021
|
+
function isBuildOnlyComponent(comp) {
|
|
1022
|
+
return !!comp?.properties?.some(
|
|
1023
|
+
(property) =>
|
|
1024
|
+
property.name === "cdx:cargo:hostOnly" && property.value === "true",
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Move the build-time-only components out of `components[]`, which describes
|
|
1030
|
+
* the delivered assembly, and hand them to the formulation section, which is
|
|
1031
|
+
* where CycloneDX describes how the assembly was produced.
|
|
1032
|
+
*
|
|
1033
|
+
* Their bom-refs stay valid because the formulation components live in the same
|
|
1034
|
+
* document, so the dependency graph is left intact. `formulation` exists from
|
|
1035
|
+
* CycloneDX 1.5, so an older document keeps them in `components[]` with their
|
|
1036
|
+
* `cdx:cargo:hostOnly` marker rather than losing them.
|
|
1037
|
+
*
|
|
1038
|
+
* @param {Object} bomJson BOM JSON object
|
|
1039
|
+
* @param {Object} options CLI options
|
|
1040
|
+
*
|
|
1041
|
+
* @returns {Object[]} The components removed from `components[]`
|
|
1042
|
+
*/
|
|
1043
|
+
export function extractBuildOnlyComponents(bomJson, options) {
|
|
1044
|
+
if (options?.specVersion < 1.5 || !bomJson?.components?.length) {
|
|
1045
|
+
return [];
|
|
1046
|
+
}
|
|
1047
|
+
const buildOnlyComponents = [];
|
|
1048
|
+
const retainedComponents = [];
|
|
1049
|
+
for (const comp of bomJson.components) {
|
|
1050
|
+
if (isBuildOnlyComponent(comp)) {
|
|
1051
|
+
buildOnlyComponents.push(comp);
|
|
1052
|
+
} else {
|
|
1053
|
+
retainedComponents.push(comp);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (buildOnlyComponents.length) {
|
|
1057
|
+
bomJson.components = retainedComponents;
|
|
1058
|
+
}
|
|
1059
|
+
return buildOnlyComponents;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Report whether a component carries evidence that it is actually used: an
|
|
1064
|
+
* occurrence in the analyzed sources, or a frame in a callstack.
|
|
1065
|
+
*
|
|
1066
|
+
* @param {Object} comp Component
|
|
1067
|
+
* @returns {boolean} True when usage evidence is attached
|
|
1068
|
+
*/
|
|
1069
|
+
function hasUsageEvidence(comp) {
|
|
1070
|
+
if (comp?.evidence?.occurrences?.length) {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
return !!comp?.evidence?.callstack?.frames?.length;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Re-apply the `--required-only` filter once the evidence stage has attached
|
|
1078
|
+
* occurrence and callstack data.
|
|
1079
|
+
*
|
|
1080
|
+
* A manifest can only say that a dependency is optional; the evidence says
|
|
1081
|
+
* whether it is reached. A component the analyzers observed in the sources is
|
|
1082
|
+
* promoted to `required` so it survives the filter, while an optional component
|
|
1083
|
+
* with no observed usage is dropped as before. A component scoped `excluded` -
|
|
1084
|
+
* a dev or test dependency - keeps that scope: an occurrence inside the test
|
|
1085
|
+
* sources is not evidence that it ships.
|
|
1086
|
+
*
|
|
1087
|
+
* @param {Object} bomJson BOM JSON object carrying evidence
|
|
1088
|
+
* @param {Object} options CLI options
|
|
1089
|
+
*
|
|
1090
|
+
* @returns {Object} Filtered BOM JSON
|
|
1091
|
+
*/
|
|
1092
|
+
export function applyEvidenceBasedFilter(bomJson, options) {
|
|
1093
|
+
if (!options?.requiredOnly || !bomJson?.components?.length) {
|
|
1094
|
+
return bomJson;
|
|
1095
|
+
}
|
|
1096
|
+
if (!(options.evidence || options.includeCrypto)) {
|
|
1097
|
+
return bomJson;
|
|
1098
|
+
}
|
|
1099
|
+
for (const comp of bomJson.components) {
|
|
1100
|
+
if (!hasUsageEvidence(comp)) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (!comp.properties) {
|
|
1104
|
+
comp.properties = [];
|
|
1105
|
+
}
|
|
1106
|
+
const observed = comp.evidence?.occurrences?.length
|
|
1107
|
+
? "occurrence"
|
|
1108
|
+
: "callstack";
|
|
1109
|
+
if (
|
|
1110
|
+
!comp.properties.some(
|
|
1111
|
+
(property) => property.name === "cdx:evidence:usage",
|
|
1112
|
+
)
|
|
1113
|
+
) {
|
|
1114
|
+
comp.properties.push({ name: "cdx:evidence:usage", value: observed });
|
|
1115
|
+
}
|
|
1116
|
+
if (comp.scope === "optional") {
|
|
1117
|
+
comp.scope = "required";
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
// `evidenceFilterStage` tells filterBom that the evidence is in hand, so the
|
|
1121
|
+
// scope filter it deferred earlier must run now.
|
|
1122
|
+
return filterBom(bomJson, { ...options, evidenceFilterStage: true });
|
|
1123
|
+
}
|
|
1124
|
+
|
|
967
1125
|
/**
|
|
968
1126
|
* Filter BOM based on options
|
|
969
1127
|
*
|
|
@@ -983,6 +1141,15 @@ export function filterBom(bomJson, options) {
|
|
|
983
1141
|
const allowedComponentTypes = new Set(
|
|
984
1142
|
normalizeCycloneDxComponentTypeFilter(options?.componentType),
|
|
985
1143
|
);
|
|
1144
|
+
// Evidence generation runs after this stage, so dropping the optional
|
|
1145
|
+
// components here would deny the analyzers the chance to prove that one is
|
|
1146
|
+
// used. The scope filter is re-applied by applyEvidenceBasedFilter once the
|
|
1147
|
+
// occurrence and callstack evidence is attached.
|
|
1148
|
+
const deferRequiredOnly = !!(
|
|
1149
|
+
options.requiredOnly &&
|
|
1150
|
+
!options.evidenceFilterStage &&
|
|
1151
|
+
(options.evidence || options.includeCrypto)
|
|
1152
|
+
);
|
|
986
1153
|
for (const comp of bomJson.components) {
|
|
987
1154
|
if (shouldExcludeInventoryType(comp, options)) {
|
|
988
1155
|
filtered = true;
|
|
@@ -1025,6 +1192,7 @@ export function filterBom(bomJson, options) {
|
|
|
1025
1192
|
}
|
|
1026
1193
|
if (
|
|
1027
1194
|
options.requiredOnly &&
|
|
1195
|
+
!deferRequiredOnly &&
|
|
1028
1196
|
comp.scope &&
|
|
1029
1197
|
["optional", "excluded"].includes(comp.scope)
|
|
1030
1198
|
) {
|