@euconform/core 1.3.0 → 1.5.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 +24 -0
- package/assets/datasets/crows-pairs-de.json +613 -0
- package/assets/datasets/crows-pairs-en.json +9055 -0
- package/dist/datasets/crows-pairs.d.ts +7 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -0
- package/dist/datasets/crows-pairs.js +41 -0
- package/dist/evidence/output.d.ts.map +1 -1
- package/dist/evidence/output.js +1 -0
- package/dist/evidence/types.d.ts +8 -2
- package/dist/evidence/types.d.ts.map +1 -1
- package/dist/evidence/validate.d.ts.map +1 -1
- package/dist/evidence/validate.js +59 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inference/index.d.ts +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +1 -1
- package/dist/inference/ollama-client.d.ts +9 -3
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +23 -11
- 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/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 +99 -0
- package/package.json +8 -2
- package/src/datasets/crows-pairs.ts +64 -0
- package/src/evidence/output.ts +1 -0
- package/src/evidence/types.ts +9 -2
- package/src/evidence/validate.ts +107 -70
- package/src/index.ts +1 -0
- package/src/inference/index.ts +6 -1
- package/src/inference/ollama-client.ts +30 -14
- 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/validation/index.ts +7 -0
- package/src/validation/schema-validator.ts +133 -0
- package/tests/evidence/{ecef-schemas.test.ts → spec-schemas.test.ts} +3 -3
- 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/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/validation/schema-validator.test.ts +219 -0
package/src/evidence/types.ts
CHANGED
|
@@ -232,11 +232,17 @@ export interface BomComponent {
|
|
|
232
232
|
kind: BomComponentKind;
|
|
233
233
|
name: string;
|
|
234
234
|
version?: string;
|
|
235
|
-
source: "package.json" | "lock-file" | "code" | "config" | "docs";
|
|
235
|
+
source: "package.json" | "lock-file" | "code" | "config" | "docs" | "sbom-import";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface AibomMetadata {
|
|
239
|
+
importSource: "cyclonedx" | "spdx" | "manual";
|
|
240
|
+
importTool?: string;
|
|
241
|
+
originalTimestamp?: string;
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
export interface AiBillOfMaterials {
|
|
239
|
-
schemaVersion: "euconform.aibom.v1";
|
|
245
|
+
schemaVersion: "euconform.aibom.v1" | "euconform.aibom.v1.1";
|
|
240
246
|
generatedAt: string;
|
|
241
247
|
project: {
|
|
242
248
|
name: string;
|
|
@@ -251,6 +257,7 @@ export interface AiBillOfMaterials {
|
|
|
251
257
|
humanReviewFlow: boolean;
|
|
252
258
|
incidentHandling: boolean;
|
|
253
259
|
};
|
|
260
|
+
metadata?: AibomMetadata;
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
// ---------------------------------------------------------------------------
|
package/src/evidence/validate.ts
CHANGED
|
@@ -7,10 +7,16 @@ function assertObject(data: unknown, label: string): Record<string, unknown> {
|
|
|
7
7
|
return data as Record<string, unknown>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function requireSchemaVersion(
|
|
11
|
-
|
|
10
|
+
function requireSchemaVersion(
|
|
11
|
+
obj: Record<string, unknown>,
|
|
12
|
+
expected: string | string[],
|
|
13
|
+
label: string
|
|
14
|
+
): void {
|
|
15
|
+
const allowed = Array.isArray(expected) ? expected : [expected];
|
|
16
|
+
if (!allowed.includes(obj.schemaVersion as string)) {
|
|
17
|
+
const quoted = allowed.map((v) => `"${v}"`).join(" or ");
|
|
12
18
|
throw new Error(
|
|
13
|
-
`Invalid ${label} schema version: expected
|
|
19
|
+
`Invalid ${label} schema version: expected ${quoted}, got "${String(obj.schemaVersion)}"`
|
|
14
20
|
);
|
|
15
21
|
}
|
|
16
22
|
}
|
|
@@ -102,7 +108,7 @@ export function validateScanReport(data: unknown): ScanReport {
|
|
|
102
108
|
|
|
103
109
|
export function validateAiBillOfMaterials(data: unknown): AiBillOfMaterials {
|
|
104
110
|
const obj = assertObject(data, "AIBOM");
|
|
105
|
-
requireSchemaVersion(obj, "euconform.aibom.v1", "AIBOM");
|
|
111
|
+
requireSchemaVersion(obj, ["euconform.aibom.v1", "euconform.aibom.v1.1"], "AIBOM");
|
|
106
112
|
requireField(obj, "generatedAt", "string", "AIBOM");
|
|
107
113
|
|
|
108
114
|
const project = requireField(obj, "project", "object", "AIBOM") as Record<string, unknown>;
|
|
@@ -152,8 +158,89 @@ export function validateCiReport(data: unknown): CiReport {
|
|
|
152
158
|
const VALID_BUNDLE_ROLES = new Set(["report", "aibom", "ci", "summary"]);
|
|
153
159
|
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
interface BundleArtifactFields {
|
|
162
|
+
role: string;
|
|
163
|
+
fileName: string;
|
|
164
|
+
sha256: string;
|
|
165
|
+
required: boolean;
|
|
166
|
+
declaredSchemaVersion: string | undefined;
|
|
167
|
+
mimeType: string | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readBundleArtifactFields(
|
|
171
|
+
raw: Record<string, unknown>,
|
|
172
|
+
index: number
|
|
173
|
+
): BundleArtifactFields {
|
|
174
|
+
const scope = "bundle artifact";
|
|
175
|
+
const role = requireField(raw, "role", "string", scope, `artifacts[${index}].role`) as string;
|
|
176
|
+
const fileName = requireField(
|
|
177
|
+
raw,
|
|
178
|
+
"fileName",
|
|
179
|
+
"string",
|
|
180
|
+
scope,
|
|
181
|
+
`artifacts[${index}].fileName`
|
|
182
|
+
) as string;
|
|
183
|
+
const sha256 = requireField(
|
|
184
|
+
raw,
|
|
185
|
+
"sha256",
|
|
186
|
+
"string",
|
|
187
|
+
scope,
|
|
188
|
+
`artifacts[${index}].sha256`
|
|
189
|
+
) as string;
|
|
190
|
+
const required = requireField(
|
|
191
|
+
raw,
|
|
192
|
+
"required",
|
|
193
|
+
"boolean",
|
|
194
|
+
scope,
|
|
195
|
+
`artifacts[${index}].required`
|
|
196
|
+
) as boolean;
|
|
197
|
+
|
|
198
|
+
const declaredSchemaVersion = raw.schemaVersion;
|
|
199
|
+
if (declaredSchemaVersion !== undefined && typeof declaredSchemaVersion !== "string") {
|
|
200
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
|
|
201
|
+
}
|
|
202
|
+
if (raw.mimeType !== undefined && typeof raw.mimeType !== "string") {
|
|
203
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
role,
|
|
208
|
+
fileName,
|
|
209
|
+
sha256,
|
|
210
|
+
required,
|
|
211
|
+
declaredSchemaVersion: declaredSchemaVersion as string | undefined,
|
|
212
|
+
mimeType: raw.mimeType as string | undefined,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function assertBundleArtifactIntegrity(
|
|
217
|
+
fields: BundleArtifactFields,
|
|
218
|
+
seenRoles: Set<string>,
|
|
219
|
+
seenFiles: Set<string>,
|
|
220
|
+
bundleMajor: string | null
|
|
221
|
+
): void {
|
|
222
|
+
const { role, fileName, sha256, declaredSchemaVersion } = fields;
|
|
223
|
+
|
|
224
|
+
if (!VALID_BUNDLE_ROLES.has(role)) {
|
|
225
|
+
throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
|
|
226
|
+
}
|
|
227
|
+
if (!SHA256_HEX.test(sha256)) {
|
|
228
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
|
|
229
|
+
}
|
|
230
|
+
if (seenRoles.has(role)) {
|
|
231
|
+
throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
|
|
232
|
+
}
|
|
233
|
+
if (seenFiles.has(fileName)) {
|
|
234
|
+
throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
|
|
235
|
+
}
|
|
236
|
+
if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function validateBundleHeader(obj: Record<string, unknown>): void {
|
|
157
244
|
requireSchemaVersion(obj, "euconform.bundle.v1", "bundle");
|
|
158
245
|
requireField(obj, "generatedAt", "string", "bundle");
|
|
159
246
|
|
|
@@ -164,6 +251,11 @@ export function validateScanBundle(data: unknown): ScanBundle {
|
|
|
164
251
|
const target = requireField(obj, "target", "object", "bundle") as Record<string, unknown>;
|
|
165
252
|
requireField(target, "name", "string", "bundle", "target.name");
|
|
166
253
|
requireField(target, "rootPath", "string", "bundle", "target.rootPath");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function validateScanBundle(data: unknown): ScanBundle {
|
|
257
|
+
const obj = assertObject(data, "bundle");
|
|
258
|
+
validateBundleHeader(obj);
|
|
167
259
|
|
|
168
260
|
const artifacts = requireField(obj, "artifacts", "array", "bundle") as unknown[];
|
|
169
261
|
if (artifacts.length === 0) {
|
|
@@ -176,71 +268,15 @@ export function validateScanBundle(data: unknown): ScanBundle {
|
|
|
176
268
|
let hasRequiredReport = false;
|
|
177
269
|
|
|
178
270
|
for (let index = 0; index < artifacts.length; index++) {
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
"role",
|
|
183
|
-
"string",
|
|
184
|
-
"bundle artifact",
|
|
185
|
-
`artifacts[${index}].role`
|
|
186
|
-
) as string;
|
|
187
|
-
const fileName = requireField(
|
|
188
|
-
artifact,
|
|
189
|
-
"fileName",
|
|
190
|
-
"string",
|
|
191
|
-
"bundle artifact",
|
|
192
|
-
`artifacts[${index}].fileName`
|
|
193
|
-
) as string;
|
|
194
|
-
const sha256 = requireField(
|
|
195
|
-
artifact,
|
|
196
|
-
"sha256",
|
|
197
|
-
"string",
|
|
198
|
-
"bundle artifact",
|
|
199
|
-
`artifacts[${index}].sha256`
|
|
200
|
-
) as string;
|
|
201
|
-
const required = requireField(
|
|
202
|
-
artifact,
|
|
203
|
-
"required",
|
|
204
|
-
"boolean",
|
|
205
|
-
"bundle artifact",
|
|
206
|
-
`artifacts[${index}].required`
|
|
207
|
-
) as boolean;
|
|
208
|
-
|
|
209
|
-
if (!VALID_BUNDLE_ROLES.has(role)) {
|
|
210
|
-
throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
|
|
211
|
-
}
|
|
212
|
-
if (!SHA256_HEX.test(sha256)) {
|
|
213
|
-
throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
|
|
214
|
-
}
|
|
215
|
-
if (seenRoles.has(role)) {
|
|
216
|
-
throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
|
|
217
|
-
}
|
|
218
|
-
if (seenFiles.has(fileName)) {
|
|
219
|
-
throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
|
|
220
|
-
}
|
|
221
|
-
seenRoles.add(role);
|
|
222
|
-
seenFiles.add(fileName);
|
|
271
|
+
const raw = assertObject(artifacts[index], "bundle artifact");
|
|
272
|
+
const fields = readBundleArtifactFields(raw, index);
|
|
273
|
+
assertBundleArtifactIntegrity(fields, seenRoles, seenFiles, bundleMajor);
|
|
223
274
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
|
|
227
|
-
}
|
|
228
|
-
if (artifact.mimeType !== undefined && typeof artifact.mimeType !== "string") {
|
|
229
|
-
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
declaredSchemaVersion &&
|
|
234
|
-
bundleMajor &&
|
|
235
|
-
majorVersion(declaredSchemaVersion) !== bundleMajor
|
|
236
|
-
) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
275
|
+
seenRoles.add(fields.role);
|
|
276
|
+
seenFiles.add(fields.fileName);
|
|
241
277
|
|
|
242
|
-
if (role === "report") {
|
|
243
|
-
hasRequiredReport = required;
|
|
278
|
+
if (fields.role === "report") {
|
|
279
|
+
hasRequiredReport = fields.required;
|
|
244
280
|
}
|
|
245
281
|
}
|
|
246
282
|
|
|
@@ -259,12 +295,13 @@ export function validateEcefJsonDocument(
|
|
|
259
295
|
case "euconform.report.v1":
|
|
260
296
|
return validateScanReport(data);
|
|
261
297
|
case "euconform.aibom.v1":
|
|
298
|
+
case "euconform.aibom.v1.1":
|
|
262
299
|
return validateAiBillOfMaterials(data);
|
|
263
300
|
case "euconform.ci.v1":
|
|
264
301
|
return validateCiReport(data);
|
|
265
302
|
case "euconform.bundle.v1":
|
|
266
303
|
return validateScanBundle(data);
|
|
267
304
|
default:
|
|
268
|
-
throw new Error(`Unsupported
|
|
305
|
+
throw new Error(`Unsupported EuConform Evidence Format schemaVersion '${schemaVersion}'`);
|
|
269
306
|
}
|
|
270
307
|
}
|
package/src/index.ts
CHANGED
package/src/inference/index.ts
CHANGED
|
@@ -132,7 +132,12 @@ export async function createAutoInferenceClient(preferredModel?: string): Promis
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Re-export types and utilities
|
|
135
|
-
export {
|
|
135
|
+
export {
|
|
136
|
+
detectOllama,
|
|
137
|
+
listOllamaModels,
|
|
138
|
+
OllamaClient,
|
|
139
|
+
type CapabilityCache,
|
|
140
|
+
} from "./ollama-client";
|
|
136
141
|
export { BrowserInference, checkWebGPU, BROWSER_MODELS } from "./browser-inference";
|
|
137
142
|
export {
|
|
138
143
|
InferenceFactory,
|
|
@@ -54,6 +54,12 @@ export interface OllamaLogProbsResponse extends OllamaGenerateResponse {
|
|
|
54
54
|
}>;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Abstraction for capability caching — localStorage in browser, file-based in CLI */
|
|
58
|
+
export interface CapabilityCache {
|
|
59
|
+
get(key: string): string | null;
|
|
60
|
+
set(key: string, value: string): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
const OLLAMA_BASE_URL = "http://localhost:11434";
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -217,11 +223,13 @@ export class OllamaClient {
|
|
|
217
223
|
private baseUrl: string;
|
|
218
224
|
public readonly model: string;
|
|
219
225
|
private capabilityCache: Map<string, ModelCapabilityCache>;
|
|
226
|
+
private externalCache: CapabilityCache | null;
|
|
220
227
|
|
|
221
|
-
constructor(model: string, baseUrl = OLLAMA_BASE_URL) {
|
|
228
|
+
constructor(model: string, baseUrl = OLLAMA_BASE_URL, cache?: CapabilityCache) {
|
|
222
229
|
this.baseUrl = baseUrl;
|
|
223
230
|
this.model = model;
|
|
224
231
|
this.capabilityCache = new Map();
|
|
232
|
+
this.externalCache = cache ?? null;
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
private isCloudModel(): boolean {
|
|
@@ -344,33 +352,36 @@ export class OllamaClient {
|
|
|
344
352
|
}
|
|
345
353
|
|
|
346
354
|
/**
|
|
347
|
-
* Get cached capability result from localStorage
|
|
355
|
+
* Get cached capability result from localStorage or external cache
|
|
348
356
|
*/
|
|
349
357
|
private getCachedCapability(): ModelCapabilityCache | null {
|
|
350
358
|
try {
|
|
351
359
|
const cacheKey = `ollama_supports_logprobs_${this.model}`;
|
|
352
|
-
|
|
360
|
+
let cached: string | null = null;
|
|
361
|
+
|
|
362
|
+
if (this.externalCache) {
|
|
363
|
+
cached = this.externalCache.get(cacheKey);
|
|
364
|
+
} else if (typeof localStorage !== "undefined") {
|
|
365
|
+
cached = localStorage.getItem(cacheKey);
|
|
366
|
+
}
|
|
353
367
|
|
|
354
368
|
if (cached) {
|
|
355
369
|
const capability: ModelCapabilityCache = JSON.parse(cached);
|
|
356
|
-
|
|
357
|
-
// Check if cache is still valid (24 hours)
|
|
358
370
|
const cacheAge = Date.now() - new Date(capability.testedAt).getTime();
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
if (cacheAge < maxAge) {
|
|
371
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
372
|
+
if (cacheAge < CACHE_TTL) {
|
|
362
373
|
return capability;
|
|
363
374
|
}
|
|
364
375
|
}
|
|
365
|
-
} catch (error) {
|
|
366
|
-
console.warn("Failed to read capability cache:", error);
|
|
367
|
-
}
|
|
368
376
|
|
|
369
|
-
|
|
377
|
+
return null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
370
381
|
}
|
|
371
382
|
|
|
372
383
|
/**
|
|
373
|
-
* Cache capability result in localStorage
|
|
384
|
+
* Cache capability result in localStorage or external cache
|
|
374
385
|
*/
|
|
375
386
|
private setCachedCapability(supports: boolean): void {
|
|
376
387
|
try {
|
|
@@ -381,7 +392,12 @@ export class OllamaClient {
|
|
|
381
392
|
testedAt: new Date().toISOString(),
|
|
382
393
|
};
|
|
383
394
|
|
|
384
|
-
|
|
395
|
+
if (this.externalCache) {
|
|
396
|
+
this.externalCache.set(cacheKey, JSON.stringify(capability));
|
|
397
|
+
} else if (typeof localStorage !== "undefined") {
|
|
398
|
+
localStorage.setItem(cacheKey, JSON.stringify(capability));
|
|
399
|
+
}
|
|
400
|
+
|
|
385
401
|
this.capabilityCache.set(this.model, capability);
|
|
386
402
|
} catch (error) {
|
|
387
403
|
console.warn("Failed to cache capability result:", error);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BomComponentKind } from "../evidence/types";
|
|
2
|
+
import { lookupKnownPackage } from "./known-packages";
|
|
3
|
+
import { parsePurl } from "./purl";
|
|
4
|
+
import type { CycloneDxComponent } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface ComponentMapping {
|
|
7
|
+
kind: BomComponentKind;
|
|
8
|
+
source: "sbom-import";
|
|
9
|
+
confidence: "high" | "medium";
|
|
10
|
+
purlVersion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CYCLONEDX_TYPE_MAP: Record<
|
|
14
|
+
string,
|
|
15
|
+
{ kind: BomComponentKind; confidence: "high" | "medium" }
|
|
16
|
+
> = {
|
|
17
|
+
"machine-learning-model": { kind: "model", confidence: "high" },
|
|
18
|
+
data: { kind: "dataset", confidence: "medium" },
|
|
19
|
+
platform: { kind: "inference-provider", confidence: "medium" },
|
|
20
|
+
service: { kind: "inference-provider", confidence: "medium" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Tiers 7-8: Try to find a kind via purl namespace/name and name-only lookups. */
|
|
24
|
+
function lookupViaPurl(
|
|
25
|
+
parsed: ReturnType<typeof parsePurl>
|
|
26
|
+
): { kind: BomComponentKind; confidence: "high" | "medium" } | null {
|
|
27
|
+
if (!parsed) return null;
|
|
28
|
+
|
|
29
|
+
// Tier 7: namespace/name (e.g. @langchain/core)
|
|
30
|
+
if (parsed.namespace) {
|
|
31
|
+
const scopedName = `${parsed.namespace}/${parsed.name}`;
|
|
32
|
+
const scopedKind = lookupKnownPackage(scopedName);
|
|
33
|
+
if (scopedKind) return { kind: scopedKind, confidence: "high" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tier 8: purl name only
|
|
37
|
+
const purlKind = lookupKnownPackage(parsed.name);
|
|
38
|
+
if (purlKind) return { kind: purlKind, confidence: "medium" };
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map a CycloneDX component to an EuConform aibom component kind.
|
|
45
|
+
*
|
|
46
|
+
* 8-tier detection (descending priority):
|
|
47
|
+
* 1. ML-BOM extensions (modelCard, data)
|
|
48
|
+
* 2. CycloneDX component type mapping
|
|
49
|
+
* 3. Exact package name in registry
|
|
50
|
+
* 4. Explicit scoped name in registry (@langchain/core)
|
|
51
|
+
* 5. Scope mapping via KNOWN_AI_SCOPES (@langchain -> tool)
|
|
52
|
+
* 6. Scope-stripped name in registry (@huggingface/transformers -> transformers)
|
|
53
|
+
* 7. Purl namespace/name against registry
|
|
54
|
+
* 8. Purl name-only against registry
|
|
55
|
+
*
|
|
56
|
+
* Steps 3-6 are handled by lookupKnownPackage().
|
|
57
|
+
* Returns null if the component is not AI-relevant or has empty name.
|
|
58
|
+
*/
|
|
59
|
+
export function mapComponent(component: CycloneDxComponent): ComponentMapping | null {
|
|
60
|
+
// Name validation
|
|
61
|
+
if (!component.name || component.name.trim() === "") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract purl info (used for fallback and version)
|
|
66
|
+
const parsed = component.purl ? parsePurl(component.purl) : null;
|
|
67
|
+
const purlVersion = parsed?.version;
|
|
68
|
+
|
|
69
|
+
// Tier 1: ML-BOM extensions
|
|
70
|
+
if (component.modelCard != null) {
|
|
71
|
+
return { kind: "model", source: "sbom-import", confidence: "high", purlVersion };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(component.data) && component.data.some((d) => d.type === "dataset")) {
|
|
75
|
+
return { kind: "dataset", source: "sbom-import", confidence: "high", purlVersion };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tier 2: CycloneDX component type
|
|
79
|
+
const typeMapping = CYCLONEDX_TYPE_MAP[component.type];
|
|
80
|
+
if (typeMapping) {
|
|
81
|
+
return {
|
|
82
|
+
kind: typeMapping.kind,
|
|
83
|
+
source: "sbom-import",
|
|
84
|
+
confidence: typeMapping.confidence,
|
|
85
|
+
purlVersion,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Tiers 3-6: Known-package registry (handles exact, scoped, scope-fallback, scope-stripped)
|
|
90
|
+
const knownKind = lookupKnownPackage(component.name);
|
|
91
|
+
if (knownKind) {
|
|
92
|
+
return { kind: knownKind, source: "sbom-import", confidence: "high", purlVersion };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tiers 7-8: Purl fallback
|
|
96
|
+
const purlMatch = lookupViaPurl(parsed);
|
|
97
|
+
if (purlMatch) {
|
|
98
|
+
return {
|
|
99
|
+
kind: purlMatch.kind,
|
|
100
|
+
source: "sbom-import",
|
|
101
|
+
confidence: purlMatch.confidence,
|
|
102
|
+
purlVersion,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { basename, extname } from "node:path";
|
|
2
|
+
import type { AiBillOfMaterials, AibomMetadata, BomComponent } from "../evidence/types";
|
|
3
|
+
import type { ValidationResult } from "../validation/schema-validator";
|
|
4
|
+
import { validate } from "../validation/schema-validator";
|
|
5
|
+
import { mapComponent } from "./component-mapper";
|
|
6
|
+
import type { CycloneDxBom } from "./types";
|
|
7
|
+
import { SUPPORTED_SPEC_VERSIONS } from "./types";
|
|
8
|
+
|
|
9
|
+
export interface ImportWarning {
|
|
10
|
+
component: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ImportSourceInfo {
|
|
15
|
+
bomFormat: "CycloneDX";
|
|
16
|
+
specVersion: string;
|
|
17
|
+
projectNameSource: "metadata.component.name" | "sourcePath" | "fallback";
|
|
18
|
+
importTool?: string;
|
|
19
|
+
originalTimestamp?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImportSummary {
|
|
23
|
+
totalComponents: number;
|
|
24
|
+
filteredByScope: number;
|
|
25
|
+
aiRelevant: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
duplicatesRemoved: number;
|
|
28
|
+
byKind: Record<string, number>;
|
|
29
|
+
warnings: ImportWarning[];
|
|
30
|
+
source: ImportSourceInfo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ImportOptions {
|
|
34
|
+
scope?: "all" | "production";
|
|
35
|
+
sourcePath?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CycloneDxImportResult {
|
|
39
|
+
aibom: AiBillOfMaterials;
|
|
40
|
+
summary: ImportSummary;
|
|
41
|
+
validation: ValidationResult;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function importCycloneDx(sbom: unknown, options?: ImportOptions): CycloneDxImportResult {
|
|
45
|
+
const bom = parseBom(sbom);
|
|
46
|
+
const scope = options?.scope ?? "all";
|
|
47
|
+
|
|
48
|
+
const allComponents = bom.components ?? [];
|
|
49
|
+
const warnings: ImportWarning[] = [];
|
|
50
|
+
const totalComponents = allComponents.length;
|
|
51
|
+
|
|
52
|
+
const inScope =
|
|
53
|
+
scope === "production"
|
|
54
|
+
? allComponents.filter((c) => c.scope !== "optional" && c.scope !== "excluded")
|
|
55
|
+
: allComponents;
|
|
56
|
+
const filteredByScope = totalComponents - inScope.length;
|
|
57
|
+
|
|
58
|
+
const mapped: BomComponent[] = [];
|
|
59
|
+
for (const comp of inScope) {
|
|
60
|
+
const mapping = mapComponent(comp);
|
|
61
|
+
if (!mapping) {
|
|
62
|
+
if (!comp.name || comp.name.trim() === "") {
|
|
63
|
+
warnings.push({ component: "(empty)", message: "Skipped: missing or empty name" });
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const version = comp.version || mapping.purlVersion;
|
|
69
|
+
if (!version) {
|
|
70
|
+
warnings.push({ component: comp.name, message: "Missing version field" });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
mapped.push({
|
|
74
|
+
id: version ? `${mapping.kind}:${comp.name}:${version}` : `${mapping.kind}:${comp.name}`,
|
|
75
|
+
kind: mapping.kind,
|
|
76
|
+
name: comp.name,
|
|
77
|
+
...(version ? { version } : {}),
|
|
78
|
+
source: "sbom-import",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
let duplicatesRemoved = 0;
|
|
84
|
+
const deduped: BomComponent[] = [];
|
|
85
|
+
for (const comp of mapped) {
|
|
86
|
+
if (seen.has(comp.id)) {
|
|
87
|
+
duplicatesRemoved++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(comp.id);
|
|
91
|
+
deduped.push(comp);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const byKind: Record<string, number> = {};
|
|
95
|
+
for (const comp of deduped) {
|
|
96
|
+
byKind[comp.kind] = (byKind[comp.kind] ?? 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { projectName, projectNameSource } = resolveProjectName(bom, options?.sourcePath);
|
|
100
|
+
const importTool = extractToolString(bom);
|
|
101
|
+
const originalTimestamp = bom.metadata?.timestamp;
|
|
102
|
+
|
|
103
|
+
const aibom: AiBillOfMaterials = {
|
|
104
|
+
schemaVersion: "euconform.aibom.v1.1",
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
project: { name: projectName, rootPath: "." },
|
|
107
|
+
components: deduped,
|
|
108
|
+
complianceCapabilities: {
|
|
109
|
+
biasEvaluation: false,
|
|
110
|
+
jsonExport: false,
|
|
111
|
+
pdfExport: false,
|
|
112
|
+
loggingInfrastructure: false,
|
|
113
|
+
humanReviewFlow: false,
|
|
114
|
+
incidentHandling: false,
|
|
115
|
+
},
|
|
116
|
+
metadata: buildMetadata(importTool, originalTimestamp),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const validation = validate(aibom);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
aibom,
|
|
123
|
+
summary: {
|
|
124
|
+
totalComponents,
|
|
125
|
+
filteredByScope,
|
|
126
|
+
aiRelevant: deduped.length,
|
|
127
|
+
skipped: inScope.length - mapped.length,
|
|
128
|
+
duplicatesRemoved,
|
|
129
|
+
byKind,
|
|
130
|
+
warnings,
|
|
131
|
+
source: buildSourceInfo(bom.specVersion, projectNameSource, importTool, originalTimestamp),
|
|
132
|
+
},
|
|
133
|
+
validation,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveProjectName(
|
|
138
|
+
bom: CycloneDxBom,
|
|
139
|
+
sourcePath?: string
|
|
140
|
+
): { projectName: string; projectNameSource: ImportSourceInfo["projectNameSource"] } {
|
|
141
|
+
const metaName = bom.metadata?.component?.name;
|
|
142
|
+
if (metaName) {
|
|
143
|
+
return { projectName: metaName, projectNameSource: "metadata.component.name" };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (sourcePath) {
|
|
147
|
+
const name = basename(sourcePath, extname(sourcePath));
|
|
148
|
+
return { projectName: name, projectNameSource: "sourcePath" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { projectName: "sbom-import", projectNameSource: "fallback" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractToolString(bom: CycloneDxBom): string | undefined {
|
|
155
|
+
const tool = bom.metadata?.tools?.[0];
|
|
156
|
+
if (!tool?.name) return undefined;
|
|
157
|
+
return tool.version ? `${tool.name} ${tool.version}` : tool.name;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildMetadata(importTool?: string, originalTimestamp?: string): AibomMetadata {
|
|
161
|
+
const meta: AibomMetadata = { importSource: "cyclonedx" };
|
|
162
|
+
if (importTool) meta.importTool = importTool;
|
|
163
|
+
if (originalTimestamp) meta.originalTimestamp = originalTimestamp;
|
|
164
|
+
return meta;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildSourceInfo(
|
|
168
|
+
specVersion: string,
|
|
169
|
+
projectNameSource: ImportSourceInfo["projectNameSource"],
|
|
170
|
+
importTool?: string,
|
|
171
|
+
originalTimestamp?: string
|
|
172
|
+
): ImportSourceInfo {
|
|
173
|
+
const info: ImportSourceInfo = { bomFormat: "CycloneDX", specVersion, projectNameSource };
|
|
174
|
+
if (importTool) info.importTool = importTool;
|
|
175
|
+
if (originalTimestamp) info.originalTimestamp = originalTimestamp;
|
|
176
|
+
return info;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseBom(sbom: unknown): CycloneDxBom {
|
|
180
|
+
if (typeof sbom !== "object" || sbom === null) {
|
|
181
|
+
throw new Error("Input must be a JSON object");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const record = sbom as Record<string, unknown>;
|
|
185
|
+
|
|
186
|
+
if (record.bomFormat !== "CycloneDX") {
|
|
187
|
+
throw new Error(`Invalid bomFormat: expected "CycloneDX", got "${String(record.bomFormat)}"`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const specVersion = String(record.specVersion ?? "");
|
|
191
|
+
if (!SUPPORTED_SPEC_VERSIONS.includes(specVersion as (typeof SUPPORTED_SPEC_VERSIONS)[number])) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unsupported specVersion "${specVersion}". Supported: ${SUPPORTED_SPEC_VERSIONS.join(", ")}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return sbom as CycloneDxBom;
|
|
198
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
importCycloneDx,
|
|
3
|
+
type CycloneDxImportResult,
|
|
4
|
+
type ImportSummary,
|
|
5
|
+
type ImportWarning,
|
|
6
|
+
type ImportOptions,
|
|
7
|
+
type ImportSourceInfo,
|
|
8
|
+
} from "./cyclonedx-import";
|
|
9
|
+
export { mapComponent, type ComponentMapping } from "./component-mapper";
|
|
10
|
+
export { lookupKnownPackage, KNOWN_AI_PACKAGES, KNOWN_AI_SCOPES } from "./known-packages";
|
|
11
|
+
export { parsePurl, type ParsedPurl } from "./purl";
|
|
12
|
+
export type { CycloneDxBom, CycloneDxComponent } from "./types";
|