@euconform/core 1.4.0 → 1.6.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +46 -0
- package/dist/datasets/crows-pairs.d.ts +18 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -1
- package/dist/datasets/crows-pairs.js +58 -0
- package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
- package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
- package/dist/evidence/bias-comparison-markdown.js +120 -0
- package/dist/evidence/bias-comparison.d.ts +133 -0
- package/dist/evidence/bias-comparison.d.ts.map +1 -0
- package/dist/evidence/bias-comparison.js +274 -0
- package/dist/evidence/bias-statistics.d.ts +72 -0
- package/dist/evidence/bias-statistics.d.ts.map +1 -0
- package/dist/evidence/bias-statistics.js +122 -0
- package/dist/evidence/bias-v2-semantics.d.ts +19 -0
- package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
- package/dist/evidence/bias-v2-semantics.js +133 -0
- package/dist/evidence/browser.d.ts +18 -0
- package/dist/evidence/browser.d.ts.map +1 -0
- package/dist/evidence/browser.js +14 -0
- package/dist/evidence/bundle.d.ts +1 -0
- package/dist/evidence/bundle.d.ts.map +1 -1
- package/dist/evidence/bundle.js +2 -0
- package/dist/evidence/index.d.ts +9 -2
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +5 -1
- package/dist/evidence/output.d.ts.map +1 -1
- package/dist/evidence/output.js +1 -0
- package/dist/evidence/types.d.ts +183 -3
- package/dist/evidence/types.d.ts.map +1 -1
- package/dist/evidence/validate-bias-v2.d.ts +31 -0
- package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
- package/dist/evidence/validate-bias-v2.js +40 -0
- package/dist/evidence/validate.d.ts +29 -2
- package/dist/evidence/validate.d.ts.map +1 -1
- package/dist/evidence/validate.js +96 -11
- package/dist/fairness-metrics.d.ts +15 -11
- package/dist/fairness-metrics.d.ts.map +1 -1
- package/dist/fairness-metrics.js +59 -183
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inference/browser-inference.d.ts +6 -57
- package/dist/inference/browser-inference.d.ts.map +1 -1
- package/dist/inference/browser-inference.js +6 -219
- package/dist/inference/capability-detection.d.ts +9 -2
- package/dist/inference/capability-detection.d.ts.map +1 -1
- package/dist/inference/capability-detection.js +18 -16
- package/dist/inference/index.d.ts +15 -2
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +12 -3
- package/dist/inference/inference-factory.d.ts +6 -3
- package/dist/inference/inference-factory.d.ts.map +1 -1
- package/dist/inference/inference-factory.js +7 -26
- package/dist/inference/ollama-client.d.ts +29 -0
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +29 -0
- package/dist/inference/scored-log-prob-types.d.ts +64 -0
- package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
- package/dist/inference/scored-log-prob-types.js +12 -0
- package/dist/inference/sequence-logprob.d.ts +34 -0
- package/dist/inference/sequence-logprob.d.ts.map +1 -0
- package/dist/inference/sequence-logprob.js +87 -0
- package/dist/inference/template-scoring.d.ts +124 -0
- package/dist/inference/template-scoring.d.ts.map +1 -0
- package/dist/inference/template-scoring.js +164 -0
- package/dist/inference/transformers-inference.d.ts +124 -0
- package/dist/inference/transformers-inference.d.ts.map +1 -0
- package/dist/inference/transformers-inference.js +392 -0
- package/dist/legal-checks/bias-metrics.d.ts +5 -7
- package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
- package/dist/legal-checks/bias-metrics.js +8 -101
- package/dist/metrics/crows-pairs-bias.d.ts +17 -9
- package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
- package/dist/metrics/crows-pairs-bias.js +147 -59
- package/dist/metrics/stereotype-bias.d.ts +13 -5
- package/dist/metrics/stereotype-bias.d.ts.map +1 -1
- package/dist/metrics/stereotype-bias.js +16 -99
- package/dist/sbom/component-mapper.d.ts +26 -0
- package/dist/sbom/component-mapper.d.ts.map +1 -0
- package/dist/sbom/component-mapper.js +83 -0
- package/dist/sbom/cyclonedx-import.d.ts +34 -0
- package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
- package/dist/sbom/cyclonedx-import.js +131 -0
- package/dist/sbom/index.d.ts +6 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +4 -0
- package/dist/sbom/known-packages.d.ts +13 -0
- package/dist/sbom/known-packages.d.ts.map +1 -0
- package/dist/sbom/known-packages.js +129 -0
- package/dist/sbom/purl.d.ts +14 -0
- package/dist/sbom/purl.d.ts.map +1 -0
- package/dist/sbom/purl.js +56 -0
- package/dist/sbom/types.d.ts +39 -0
- package/dist/sbom/types.d.ts.map +1 -0
- package/dist/sbom/types.js +5 -0
- package/dist/scanner/detectors/ai-framework-detector.js +1 -1
- package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/local-inference-detector.js +1 -1
- package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/package-json-detector.js +1 -0
- package/dist/types.d.ts +103 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.d.ts +18 -0
- package/dist/validation/schema-validator.d.ts.map +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/package.json +9 -3
- package/scripts/seed-logprob-fixture.mjs +179 -0
- package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
- package/src/datasets/crows-pairs.ts +74 -0
- package/src/evidence/bias-comparison-markdown.ts +162 -0
- package/src/evidence/bias-comparison.ts +432 -0
- package/src/evidence/bias-statistics.ts +180 -0
- package/src/evidence/bias-v2-semantics.ts +214 -0
- package/src/evidence/browser.ts +35 -0
- package/src/evidence/bundle.ts +2 -0
- package/src/evidence/index.ts +31 -0
- package/src/evidence/output.ts +1 -0
- package/src/evidence/types.ts +213 -3
- package/src/evidence/validate-bias-v2.ts +47 -0
- package/src/evidence/validate.ts +151 -17
- package/src/fairness-metrics.ts +79 -222
- package/src/index.ts +7 -0
- package/src/inference/browser-inference.ts +14 -256
- package/src/inference/capability-detection.ts +32 -18
- package/src/inference/index.ts +33 -5
- package/src/inference/inference-factory.ts +9 -30
- package/src/inference/ollama-client.ts +29 -0
- package/src/inference/scored-log-prob-types.ts +66 -0
- package/src/inference/sequence-logprob.ts +136 -0
- package/src/inference/template-scoring.ts +248 -0
- package/src/inference/transformers-inference.ts +494 -0
- package/src/legal-checks/bias-metrics.ts +20 -118
- package/src/metrics/crows-pairs-bias.ts +180 -83
- package/src/metrics/stereotype-bias.ts +27 -119
- package/src/sbom/component-mapper.ts +107 -0
- package/src/sbom/cyclonedx-import.ts +198 -0
- package/src/sbom/index.ts +12 -0
- package/src/sbom/known-packages.ts +142 -0
- package/src/sbom/purl.ts +66 -0
- package/src/sbom/types.ts +34 -0
- package/src/scanner/detectors/ai-framework-detector.ts +1 -1
- package/src/scanner/detectors/local-inference-detector.ts +2 -1
- package/src/scanner/detectors/package-json-detector.ts +1 -0
- package/src/types.ts +108 -2
- package/src/validation/index.ts +7 -0
- package/src/validation/schema-validator.ts +147 -0
- package/tests/annex-iv-report.test.ts +1 -1
- package/tests/capability-detection.test.ts +57 -0
- package/tests/crows-pairs-bias.test.ts +286 -186
- package/tests/custom-dataset.test.ts +80 -0
- package/tests/evidence/bias-artifact-v2.test.ts +135 -0
- package/tests/evidence/bias-artifact.test.ts +78 -0
- package/tests/evidence/bias-comparison.test.ts +397 -0
- package/tests/evidence/bias-statistics.test.ts +191 -0
- package/tests/evidence/external-observation.test.ts +167 -0
- package/tests/evidence/spec-schemas.test.ts +35 -2
- package/tests/evidence/validate-bias-v2.test.ts +180 -0
- package/tests/fairness-metrics.test.ts +77 -1
- package/tests/fixtures/cyclonedx-minimal.json +13 -0
- package/tests/fixtures/cyclonedx-mixed.json +17 -0
- package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
- package/tests/inference/browser-inference-scoring.test.ts +159 -0
- package/tests/inference/fixtures/logprob-golden.json +69 -0
- package/tests/inference/gemma-template.integration.test.ts +81 -0
- package/tests/inference/logprob-golden.integration.test.ts +128 -0
- package/tests/inference/synthetic-logits.test.ts +188 -0
- package/tests/inference/template-scoring.test.ts +249 -0
- package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
- package/tests/integration/bias-calculation.integration.test.ts +119 -599
- package/tests/sbom/component-mapper.test.ts +258 -0
- package/tests/sbom/cyclonedx-import.test.ts +329 -0
- package/tests/sbom/purl.test.ts +102 -0
- package/tests/stilled-ollama-paths.test.ts +76 -0
- package/tests/validation/schema-validator.test.ts +219 -0
- package/src/inference/transformers.d.ts +0 -62
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mapComponent } from "../../src/sbom/component-mapper";
|
|
3
|
+
import { lookupKnownPackage } from "../../src/sbom/known-packages";
|
|
4
|
+
import type { CycloneDxComponent } from "../../src/sbom/types";
|
|
5
|
+
|
|
6
|
+
describe("known-packages", () => {
|
|
7
|
+
it("recognizes torch as ai-framework", () => {
|
|
8
|
+
expect(lookupKnownPackage("torch")).toBe("ai-framework");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("recognizes openai as inference-provider", () => {
|
|
12
|
+
expect(lookupKnownPackage("openai")).toBe("inference-provider");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("recognizes chromadb as vector-store", () => {
|
|
16
|
+
expect(lookupKnownPackage("chromadb")).toBe("vector-store");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("recognizes langchain as tool", () => {
|
|
20
|
+
expect(lookupKnownPackage("langchain")).toBe("tool");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns null for unknown packages", () => {
|
|
24
|
+
expect(lookupKnownPackage("express")).toBeNull();
|
|
25
|
+
expect(lookupKnownPackage("lodash")).toBeNull();
|
|
26
|
+
expect(lookupKnownPackage("react")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("is case-insensitive", () => {
|
|
30
|
+
expect(lookupKnownPackage("Transformers")).toBe("ai-framework");
|
|
31
|
+
expect(lookupKnownPackage("OPENAI")).toBe("inference-provider");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("recognizes @langchain/core as tool", () => {
|
|
35
|
+
expect(lookupKnownPackage("@langchain/core")).toBe("tool");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("recognizes @huggingface/transformers as ai-framework", () => {
|
|
39
|
+
expect(lookupKnownPackage("@huggingface/transformers")).toBe("ai-framework");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("recognizes @google/generative-ai as inference-provider", () => {
|
|
43
|
+
expect(lookupKnownPackage("@google/generative-ai")).toBe("inference-provider");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to scope lookup for unknown @langchain/* packages", () => {
|
|
47
|
+
expect(lookupKnownPackage("@langchain/unknown-new-package")).toBe("tool");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("falls back to stripping scope and checking name", () => {
|
|
51
|
+
expect(lookupKnownPackage("@someorg/transformers")).toBe("ai-framework");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null for unknown scoped packages", () => {
|
|
55
|
+
expect(lookupKnownPackage("@types/node")).toBeNull();
|
|
56
|
+
expect(lookupKnownPackage("@testing-library/react")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("component-mapper", () => {
|
|
61
|
+
describe("tier 1: ML-BOM extensions", () => {
|
|
62
|
+
it("maps component with modelCard to model", () => {
|
|
63
|
+
const comp: CycloneDxComponent = {
|
|
64
|
+
type: "library",
|
|
65
|
+
name: "my-custom-model",
|
|
66
|
+
modelCard: { modelParameters: {} },
|
|
67
|
+
};
|
|
68
|
+
const result = mapComponent(comp);
|
|
69
|
+
expect(result).not.toBeNull();
|
|
70
|
+
expect(result?.kind).toBe("model");
|
|
71
|
+
expect(result?.confidence).toBe("high");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("maps component with data array containing ml type to dataset", () => {
|
|
75
|
+
const comp: CycloneDxComponent = {
|
|
76
|
+
type: "library",
|
|
77
|
+
name: "training-data",
|
|
78
|
+
data: [{ type: "dataset" }],
|
|
79
|
+
};
|
|
80
|
+
const result = mapComponent(comp);
|
|
81
|
+
expect(result).not.toBeNull();
|
|
82
|
+
expect(result?.kind).toBe("dataset");
|
|
83
|
+
expect(result?.confidence).toBe("high");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("tier 2: CycloneDX component type", () => {
|
|
88
|
+
it("maps machine-learning-model to model", () => {
|
|
89
|
+
const comp: CycloneDxComponent = {
|
|
90
|
+
type: "machine-learning-model",
|
|
91
|
+
name: "llama-3",
|
|
92
|
+
};
|
|
93
|
+
const result = mapComponent(comp);
|
|
94
|
+
expect(result).not.toBeNull();
|
|
95
|
+
expect(result?.kind).toBe("model");
|
|
96
|
+
expect(result?.confidence).toBe("high");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("maps data type to dataset", () => {
|
|
100
|
+
const comp: CycloneDxComponent = {
|
|
101
|
+
type: "data",
|
|
102
|
+
name: "training-corpus",
|
|
103
|
+
};
|
|
104
|
+
const result = mapComponent(comp);
|
|
105
|
+
expect(result).not.toBeNull();
|
|
106
|
+
expect(result?.kind).toBe("dataset");
|
|
107
|
+
expect(result?.confidence).toBe("medium");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("maps platform type to inference-provider", () => {
|
|
111
|
+
const comp: CycloneDxComponent = {
|
|
112
|
+
type: "platform",
|
|
113
|
+
name: "inference-platform",
|
|
114
|
+
};
|
|
115
|
+
const result = mapComponent(comp);
|
|
116
|
+
expect(result).not.toBeNull();
|
|
117
|
+
expect(result?.kind).toBe("inference-provider");
|
|
118
|
+
expect(result?.confidence).toBe("medium");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("maps service type to inference-provider", () => {
|
|
122
|
+
const comp: CycloneDxComponent = {
|
|
123
|
+
type: "service",
|
|
124
|
+
name: "model-api",
|
|
125
|
+
};
|
|
126
|
+
const result = mapComponent(comp);
|
|
127
|
+
expect(result).not.toBeNull();
|
|
128
|
+
expect(result?.kind).toBe("inference-provider");
|
|
129
|
+
expect(result?.confidence).toBe("medium");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("tier 3: known-package registry", () => {
|
|
134
|
+
it("maps known framework-type package to ai-framework", () => {
|
|
135
|
+
const comp: CycloneDxComponent = {
|
|
136
|
+
type: "framework",
|
|
137
|
+
name: "transformers",
|
|
138
|
+
version: "4.38.0",
|
|
139
|
+
};
|
|
140
|
+
const result = mapComponent(comp);
|
|
141
|
+
expect(result).not.toBeNull();
|
|
142
|
+
expect(result?.kind).toBe("ai-framework");
|
|
143
|
+
expect(result?.confidence).toBe("high");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("maps known library-type package", () => {
|
|
147
|
+
const comp: CycloneDxComponent = {
|
|
148
|
+
type: "library",
|
|
149
|
+
name: "openai",
|
|
150
|
+
version: "1.12.0",
|
|
151
|
+
};
|
|
152
|
+
const result = mapComponent(comp);
|
|
153
|
+
expect(result).not.toBeNull();
|
|
154
|
+
expect(result?.kind).toBe("inference-provider");
|
|
155
|
+
expect(result?.confidence).toBe("high");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("non-AI components", () => {
|
|
160
|
+
it("returns null for unknown library", () => {
|
|
161
|
+
const comp: CycloneDxComponent = {
|
|
162
|
+
type: "library",
|
|
163
|
+
name: "express",
|
|
164
|
+
version: "4.18.0",
|
|
165
|
+
};
|
|
166
|
+
expect(mapComponent(comp)).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("returns null for unknown framework", () => {
|
|
170
|
+
const comp: CycloneDxComponent = {
|
|
171
|
+
type: "framework",
|
|
172
|
+
name: "react",
|
|
173
|
+
version: "18.2.0",
|
|
174
|
+
};
|
|
175
|
+
expect(mapComponent(comp)).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("source field", () => {
|
|
180
|
+
it("always sets source to sbom-import", () => {
|
|
181
|
+
const comp: CycloneDxComponent = {
|
|
182
|
+
type: "machine-learning-model",
|
|
183
|
+
name: "test-model",
|
|
184
|
+
};
|
|
185
|
+
const result = mapComponent(comp);
|
|
186
|
+
expect(result?.source).toBe("sbom-import");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("name validation", () => {
|
|
191
|
+
it("returns null for component with empty name", () => {
|
|
192
|
+
const comp: CycloneDxComponent = {
|
|
193
|
+
type: "machine-learning-model",
|
|
194
|
+
name: "",
|
|
195
|
+
};
|
|
196
|
+
expect(mapComponent(comp)).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("purl fallback", () => {
|
|
201
|
+
it("detects AI package via purl when name is unrecognized", () => {
|
|
202
|
+
const comp: CycloneDxComponent = {
|
|
203
|
+
type: "library",
|
|
204
|
+
name: "custom-wrapper",
|
|
205
|
+
purl: "pkg:pypi/transformers@4.38.0",
|
|
206
|
+
};
|
|
207
|
+
const result = mapComponent(comp);
|
|
208
|
+
expect(result).not.toBeNull();
|
|
209
|
+
expect(result?.kind).toBe("ai-framework");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("detects scoped package via purl namespace/name", () => {
|
|
213
|
+
const comp: CycloneDxComponent = {
|
|
214
|
+
type: "library",
|
|
215
|
+
name: "some-alias",
|
|
216
|
+
purl: "pkg:npm/%40langchain/core@0.1.22",
|
|
217
|
+
};
|
|
218
|
+
const result = mapComponent(comp);
|
|
219
|
+
expect(result).not.toBeNull();
|
|
220
|
+
expect(result?.kind).toBe("tool");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("does not match purl when name already matched", () => {
|
|
224
|
+
const comp: CycloneDxComponent = {
|
|
225
|
+
type: "library",
|
|
226
|
+
name: "openai",
|
|
227
|
+
purl: "pkg:pypi/openai@1.0.0",
|
|
228
|
+
};
|
|
229
|
+
const result = mapComponent(comp);
|
|
230
|
+
expect(result).not.toBeNull();
|
|
231
|
+
expect(result?.kind).toBe("inference-provider");
|
|
232
|
+
expect(result?.confidence).toBe("high");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("version extraction from purl", () => {
|
|
237
|
+
it("extracts version from purl via purlVersion", () => {
|
|
238
|
+
const comp: CycloneDxComponent = {
|
|
239
|
+
type: "machine-learning-model",
|
|
240
|
+
name: "my-model",
|
|
241
|
+
purl: "pkg:generic/my-model@3.0.0",
|
|
242
|
+
};
|
|
243
|
+
const result = mapComponent(comp);
|
|
244
|
+
expect(result).not.toBeNull();
|
|
245
|
+
expect(result?.purlVersion).toBe("3.0.0");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns undefined purlVersion when no purl", () => {
|
|
249
|
+
const comp: CycloneDxComponent = {
|
|
250
|
+
type: "machine-learning-model",
|
|
251
|
+
name: "my-model",
|
|
252
|
+
};
|
|
253
|
+
const result = mapComponent(comp);
|
|
254
|
+
expect(result).not.toBeNull();
|
|
255
|
+
expect(result?.purlVersion).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { importCycloneDx } from "../../src/sbom/cyclonedx-import";
|
|
3
|
+
import minimalBom from "../fixtures/cyclonedx-minimal.json";
|
|
4
|
+
import mixedBom from "../fixtures/cyclonedx-mixed.json";
|
|
5
|
+
import mlBom from "../fixtures/cyclonedx-ml-bom.json";
|
|
6
|
+
|
|
7
|
+
describe("cyclonedx-import", () => {
|
|
8
|
+
describe("basic import", () => {
|
|
9
|
+
it("imports a minimal CycloneDX BOM", () => {
|
|
10
|
+
const result = importCycloneDx(minimalBom);
|
|
11
|
+
expect(result.aibom.schemaVersion).toBe("euconform.aibom.v1.1");
|
|
12
|
+
expect(result.aibom.components).toHaveLength(1);
|
|
13
|
+
expect(result.aibom.components[0].kind).toBe("model");
|
|
14
|
+
expect(result.aibom.components[0].name).toBe("llama-3.2-1b");
|
|
15
|
+
expect(result.aibom.components[0].source).toBe("sbom-import");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("produces a valid aibom", () => {
|
|
19
|
+
const result = importCycloneDx(minimalBom);
|
|
20
|
+
expect(result.validation.valid).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("filtering", () => {
|
|
25
|
+
it("filters AI-relevant components from mixed BOM", () => {
|
|
26
|
+
const result = importCycloneDx(mixedBom);
|
|
27
|
+
expect(result.summary.totalComponents).toBe(10);
|
|
28
|
+
expect(result.summary.aiRelevant).toBe(5);
|
|
29
|
+
expect(result.summary.skipped).toBe(5);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("correctly maps component kinds in mixed BOM", () => {
|
|
33
|
+
const result = importCycloneDx(mixedBom);
|
|
34
|
+
const kinds = result.aibom.components.map((c) => c.kind).sort();
|
|
35
|
+
expect(kinds).toEqual([
|
|
36
|
+
"ai-framework",
|
|
37
|
+
"inference-provider",
|
|
38
|
+
"model",
|
|
39
|
+
"tool",
|
|
40
|
+
"vector-store",
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("provides byKind summary", () => {
|
|
45
|
+
const result = importCycloneDx(mixedBom);
|
|
46
|
+
expect(result.summary.byKind.model).toBe(1);
|
|
47
|
+
expect(result.summary.byKind["ai-framework"]).toBe(1);
|
|
48
|
+
expect(result.summary.byKind["inference-provider"]).toBe(1);
|
|
49
|
+
expect(result.summary.byKind.tool).toBe(1);
|
|
50
|
+
expect(result.summary.byKind["vector-store"]).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("ML-BOM extensions", () => {
|
|
55
|
+
it("detects modelCard extension", () => {
|
|
56
|
+
const result = importCycloneDx(mlBom);
|
|
57
|
+
const model = result.aibom.components.find((c) => c.name === "custom-bert-model");
|
|
58
|
+
expect(model).toBeDefined();
|
|
59
|
+
expect(model?.kind).toBe("model");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("detects data extension as dataset", () => {
|
|
63
|
+
const result = importCycloneDx(mlBom);
|
|
64
|
+
const dataset = result.aibom.components.find((c) => c.name === "sentiment-dataset");
|
|
65
|
+
expect(dataset).toBeDefined();
|
|
66
|
+
expect(dataset?.kind).toBe("dataset");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("skips non-AI components even in ML BOM", () => {
|
|
70
|
+
const result = importCycloneDx(mlBom);
|
|
71
|
+
expect(result.summary.totalComponents).toBe(3);
|
|
72
|
+
expect(result.summary.aiRelevant).toBe(2);
|
|
73
|
+
expect(result.summary.skipped).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("input validation", () => {
|
|
78
|
+
it("rejects non-CycloneDX input", () => {
|
|
79
|
+
expect(() => importCycloneDx({ foo: "bar" })).toThrow("bomFormat");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("rejects unsupported specVersion", () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
importCycloneDx({
|
|
85
|
+
bomFormat: "CycloneDX",
|
|
86
|
+
specVersion: "1.0",
|
|
87
|
+
components: [],
|
|
88
|
+
})
|
|
89
|
+
).toThrow("specVersion");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("handles BOM with no components", () => {
|
|
93
|
+
const result = importCycloneDx({
|
|
94
|
+
bomFormat: "CycloneDX",
|
|
95
|
+
specVersion: "1.5",
|
|
96
|
+
components: [],
|
|
97
|
+
});
|
|
98
|
+
expect(result.aibom.components).toEqual([]);
|
|
99
|
+
expect(result.summary.totalComponents).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("scope filtering", () => {
|
|
104
|
+
it("excludes optional-scope components when scope is production", () => {
|
|
105
|
+
const bom = {
|
|
106
|
+
bomFormat: "CycloneDX",
|
|
107
|
+
specVersion: "1.5",
|
|
108
|
+
components: [
|
|
109
|
+
{ type: "machine-learning-model", name: "prod-model", scope: "required" },
|
|
110
|
+
{ type: "machine-learning-model", name: "dev-model", scope: "optional" },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
const result = importCycloneDx(bom, { scope: "production" });
|
|
114
|
+
expect(result.aibom.components).toHaveLength(1);
|
|
115
|
+
expect(result.aibom.components[0].name).toBe("prod-model");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("excludes excluded-scope components when scope is production", () => {
|
|
119
|
+
const bom = {
|
|
120
|
+
bomFormat: "CycloneDX",
|
|
121
|
+
specVersion: "1.5",
|
|
122
|
+
components: [
|
|
123
|
+
{ type: "machine-learning-model", name: "prod-model", scope: "required" },
|
|
124
|
+
{ type: "machine-learning-model", name: "excluded-model", scope: "excluded" },
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
const result = importCycloneDx(bom, { scope: "production" });
|
|
128
|
+
expect(result.aibom.components).toHaveLength(1);
|
|
129
|
+
expect(result.aibom.components[0].name).toBe("prod-model");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("includes components without scope in production mode", () => {
|
|
133
|
+
const bom = {
|
|
134
|
+
bomFormat: "CycloneDX",
|
|
135
|
+
specVersion: "1.5",
|
|
136
|
+
components: [{ type: "machine-learning-model", name: "no-scope-model" }],
|
|
137
|
+
};
|
|
138
|
+
const result = importCycloneDx(bom, { scope: "production" });
|
|
139
|
+
expect(result.aibom.components).toHaveLength(1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("complianceCapabilities", () => {
|
|
144
|
+
it("sets all capabilities to false for imports", () => {
|
|
145
|
+
const result = importCycloneDx(minimalBom);
|
|
146
|
+
const caps = result.aibom.complianceCapabilities;
|
|
147
|
+
expect(caps.biasEvaluation).toBe(false);
|
|
148
|
+
expect(caps.jsonExport).toBe(false);
|
|
149
|
+
expect(caps.pdfExport).toBe(false);
|
|
150
|
+
expect(caps.loggingInfrastructure).toBe(false);
|
|
151
|
+
expect(caps.humanReviewFlow).toBe(false);
|
|
152
|
+
expect(caps.incidentHandling).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("component ID generation", () => {
|
|
157
|
+
it("generates id as kind:name", () => {
|
|
158
|
+
const result = importCycloneDx(minimalBom);
|
|
159
|
+
expect(result.aibom.components[0].id).toBe("model:llama-3.2-1b:1.0.0");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("deduplication", () => {
|
|
164
|
+
it("removes exact duplicates (same kind+name+version)", () => {
|
|
165
|
+
const bom = {
|
|
166
|
+
bomFormat: "CycloneDX",
|
|
167
|
+
specVersion: "1.5",
|
|
168
|
+
components: [
|
|
169
|
+
{ type: "library", name: "openai", version: "1.12.0" },
|
|
170
|
+
{ type: "library", name: "openai", version: "1.12.0" },
|
|
171
|
+
{ type: "library", name: "openai", version: "1.12.0" },
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const result = importCycloneDx(bom);
|
|
175
|
+
expect(result.aibom.components).toHaveLength(1);
|
|
176
|
+
expect(result.summary.duplicatesRemoved).toBe(2);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("keeps different versions of the same package", () => {
|
|
180
|
+
const bom = {
|
|
181
|
+
bomFormat: "CycloneDX",
|
|
182
|
+
specVersion: "1.5",
|
|
183
|
+
components: [
|
|
184
|
+
{ type: "library", name: "torch", version: "2.0.0" },
|
|
185
|
+
{ type: "library", name: "torch", version: "2.2.0" },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
const result = importCycloneDx(bom);
|
|
189
|
+
expect(result.aibom.components).toHaveLength(2);
|
|
190
|
+
expect(result.summary.duplicatesRemoved).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("ID format", () => {
|
|
195
|
+
it("uses kind:name:version when version present", () => {
|
|
196
|
+
const result = importCycloneDx(minimalBom);
|
|
197
|
+
expect(result.aibom.components[0].id).toBe("model:llama-3.2-1b:1.0.0");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("uses kind:name when version absent", () => {
|
|
201
|
+
const bom = {
|
|
202
|
+
bomFormat: "CycloneDX",
|
|
203
|
+
specVersion: "1.5",
|
|
204
|
+
components: [{ type: "machine-learning-model", name: "some-model" }],
|
|
205
|
+
};
|
|
206
|
+
const result = importCycloneDx(bom);
|
|
207
|
+
expect(result.aibom.components[0].id).toBe("model:some-model");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("provenance", () => {
|
|
212
|
+
it("sets metadata from BOM metadata", () => {
|
|
213
|
+
const bom = {
|
|
214
|
+
bomFormat: "CycloneDX",
|
|
215
|
+
specVersion: "1.5",
|
|
216
|
+
metadata: {
|
|
217
|
+
timestamp: "2026-04-17T10:00:00Z",
|
|
218
|
+
tools: [{ name: "cdxgen", version: "10.0.0" }],
|
|
219
|
+
component: { type: "application", name: "my-ai-app" },
|
|
220
|
+
},
|
|
221
|
+
components: [{ type: "machine-learning-model", name: "test-model", version: "1.0" }],
|
|
222
|
+
};
|
|
223
|
+
const result = importCycloneDx(bom);
|
|
224
|
+
expect(result.aibom.metadata).toEqual({
|
|
225
|
+
importSource: "cyclonedx",
|
|
226
|
+
importTool: "cdxgen 10.0.0",
|
|
227
|
+
originalTimestamp: "2026-04-17T10:00:00Z",
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("omits importTool when no tools in metadata", () => {
|
|
232
|
+
const bom = {
|
|
233
|
+
bomFormat: "CycloneDX",
|
|
234
|
+
specVersion: "1.5",
|
|
235
|
+
components: [{ type: "machine-learning-model", name: "test-model", version: "1.0" }],
|
|
236
|
+
};
|
|
237
|
+
const result = importCycloneDx(bom);
|
|
238
|
+
expect(result.aibom.metadata).toEqual({
|
|
239
|
+
importSource: "cyclonedx",
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("includes source info in summary", () => {
|
|
244
|
+
const bom = {
|
|
245
|
+
bomFormat: "CycloneDX",
|
|
246
|
+
specVersion: "1.5",
|
|
247
|
+
metadata: {
|
|
248
|
+
timestamp: "2026-04-17T10:00:00Z",
|
|
249
|
+
tools: [{ name: "trivy", version: "0.50.0" }],
|
|
250
|
+
},
|
|
251
|
+
components: [],
|
|
252
|
+
};
|
|
253
|
+
const result = importCycloneDx(bom);
|
|
254
|
+
expect(result.summary.source.bomFormat).toBe("CycloneDX");
|
|
255
|
+
expect(result.summary.source.specVersion).toBe("1.5");
|
|
256
|
+
expect(result.summary.source.importTool).toBe("trivy 0.50.0");
|
|
257
|
+
expect(result.summary.source.originalTimestamp).toBe("2026-04-17T10:00:00Z");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("project name", () => {
|
|
262
|
+
it("uses metadata.component.name when available", () => {
|
|
263
|
+
const bom = {
|
|
264
|
+
bomFormat: "CycloneDX",
|
|
265
|
+
specVersion: "1.5",
|
|
266
|
+
metadata: {
|
|
267
|
+
component: { type: "application", name: "my-ai-app" },
|
|
268
|
+
},
|
|
269
|
+
components: [],
|
|
270
|
+
};
|
|
271
|
+
const result = importCycloneDx(bom);
|
|
272
|
+
expect(result.aibom.project.name).toBe("my-ai-app");
|
|
273
|
+
expect(result.summary.source.projectNameSource).toBe("metadata.component.name");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("falls back to sourcePath basename", () => {
|
|
277
|
+
const bom = {
|
|
278
|
+
bomFormat: "CycloneDX",
|
|
279
|
+
specVersion: "1.5",
|
|
280
|
+
components: [],
|
|
281
|
+
};
|
|
282
|
+
const result = importCycloneDx(bom, { sourcePath: "/path/to/cyclonedx-bom.json" });
|
|
283
|
+
expect(result.aibom.project.name).toBe("cyclonedx-bom");
|
|
284
|
+
expect(result.summary.source.projectNameSource).toBe("sourcePath");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("falls back to sbom-import when no metadata or sourcePath", () => {
|
|
288
|
+
const bom = {
|
|
289
|
+
bomFormat: "CycloneDX",
|
|
290
|
+
specVersion: "1.5",
|
|
291
|
+
components: [],
|
|
292
|
+
};
|
|
293
|
+
const result = importCycloneDx(bom);
|
|
294
|
+
expect(result.aibom.project.name).toBe("sbom-import");
|
|
295
|
+
expect(result.summary.source.projectNameSource).toBe("fallback");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("filteredByScope in summary", () => {
|
|
300
|
+
it("counts scope-filtered components", () => {
|
|
301
|
+
const bom = {
|
|
302
|
+
bomFormat: "CycloneDX",
|
|
303
|
+
specVersion: "1.5",
|
|
304
|
+
components: [
|
|
305
|
+
{ type: "machine-learning-model", name: "prod", scope: "required" },
|
|
306
|
+
{ type: "machine-learning-model", name: "opt", scope: "optional" },
|
|
307
|
+
{ type: "machine-learning-model", name: "excl", scope: "excluded" },
|
|
308
|
+
{ type: "library", name: "express", scope: "required" },
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
const result = importCycloneDx(bom, { scope: "production" });
|
|
312
|
+
expect(result.summary.totalComponents).toBe(4);
|
|
313
|
+
expect(result.summary.filteredByScope).toBe(2);
|
|
314
|
+
expect(result.summary.aiRelevant).toBe(1);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("version fallback from purl", () => {
|
|
319
|
+
it("uses purl version when component version missing", () => {
|
|
320
|
+
const bom = {
|
|
321
|
+
bomFormat: "CycloneDX",
|
|
322
|
+
specVersion: "1.5",
|
|
323
|
+
components: [{ type: "library", name: "openai", purl: "pkg:pypi/openai@1.30.0" }],
|
|
324
|
+
};
|
|
325
|
+
const result = importCycloneDx(bom);
|
|
326
|
+
expect(result.aibom.components[0].version).toBe("1.30.0");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parsePurl } from "../../src/sbom/purl";
|
|
3
|
+
|
|
4
|
+
describe("parsePurl", () => {
|
|
5
|
+
it("parses a simple pypi purl", () => {
|
|
6
|
+
const result = parsePurl("pkg:pypi/transformers@4.38.0");
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
scheme: "pkg",
|
|
9
|
+
type: "pypi",
|
|
10
|
+
namespace: undefined,
|
|
11
|
+
name: "transformers",
|
|
12
|
+
version: "4.38.0",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses npm purl with namespace", () => {
|
|
17
|
+
const result = parsePurl("pkg:npm/%40langchain/core@0.1.22");
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
scheme: "pkg",
|
|
20
|
+
type: "npm",
|
|
21
|
+
namespace: "@langchain",
|
|
22
|
+
name: "core",
|
|
23
|
+
version: "0.1.22",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("parses npm purl with unencoded @ namespace", () => {
|
|
28
|
+
const result = parsePurl("pkg:npm/@huggingface/transformers@2.0.0");
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
scheme: "pkg",
|
|
31
|
+
type: "npm",
|
|
32
|
+
namespace: "@huggingface",
|
|
33
|
+
name: "transformers",
|
|
34
|
+
version: "2.0.0",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("parses generic purl without version", () => {
|
|
39
|
+
const result = parsePurl("pkg:generic/llama-3.2-1b");
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
scheme: "pkg",
|
|
42
|
+
type: "generic",
|
|
43
|
+
namespace: undefined,
|
|
44
|
+
name: "llama-3.2-1b",
|
|
45
|
+
version: undefined,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("parses purl with qualifiers (ignores them)", () => {
|
|
50
|
+
const result = parsePurl("pkg:npm/openai@1.12.0?vcs_url=github.com/openai");
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
scheme: "pkg",
|
|
53
|
+
type: "npm",
|
|
54
|
+
namespace: undefined,
|
|
55
|
+
name: "openai",
|
|
56
|
+
version: "1.12.0",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("parses purl with subpath (ignores it)", () => {
|
|
61
|
+
const result = parsePurl("pkg:pypi/torch@2.2.0#cuda");
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
scheme: "pkg",
|
|
64
|
+
type: "pypi",
|
|
65
|
+
namespace: undefined,
|
|
66
|
+
name: "torch",
|
|
67
|
+
version: "2.2.0",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("parses maven purl with deep namespace", () => {
|
|
72
|
+
const result = parsePurl("pkg:maven/org.tensorflow/tensorflow-core-api@0.5.0");
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
scheme: "pkg",
|
|
75
|
+
type: "maven",
|
|
76
|
+
namespace: "org.tensorflow",
|
|
77
|
+
name: "tensorflow-core-api",
|
|
78
|
+
version: "0.5.0",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns null for empty string", () => {
|
|
83
|
+
expect(parsePurl("")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns null for non-pkg scheme", () => {
|
|
87
|
+
expect(parsePurl("http://example.com")).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns null for malformed purl", () => {
|
|
91
|
+
expect(parsePurl("pkg:")).toBeNull();
|
|
92
|
+
expect(parsePurl("pkg:/")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns null for malformed percent-encoding", () => {
|
|
96
|
+
expect(parsePurl("pkg:npm/%ZZ/bad-encoding@1.0.0")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns null for truncated percent-encoding", () => {
|
|
100
|
+
expect(parsePurl("pkg:npm/foo%2")).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|