@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.
Files changed (69) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/assets/datasets/crows-pairs-de.json +613 -0
  4. package/assets/datasets/crows-pairs-en.json +9055 -0
  5. package/dist/datasets/crows-pairs.d.ts +7 -0
  6. package/dist/datasets/crows-pairs.d.ts.map +1 -0
  7. package/dist/datasets/crows-pairs.js +41 -0
  8. package/dist/evidence/output.d.ts.map +1 -1
  9. package/dist/evidence/output.js +1 -0
  10. package/dist/evidence/types.d.ts +8 -2
  11. package/dist/evidence/types.d.ts.map +1 -1
  12. package/dist/evidence/validate.d.ts.map +1 -1
  13. package/dist/evidence/validate.js +59 -39
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/inference/index.d.ts +1 -1
  17. package/dist/inference/index.d.ts.map +1 -1
  18. package/dist/inference/index.js +1 -1
  19. package/dist/inference/ollama-client.d.ts +9 -3
  20. package/dist/inference/ollama-client.d.ts.map +1 -1
  21. package/dist/inference/ollama-client.js +23 -11
  22. package/dist/sbom/component-mapper.d.ts +26 -0
  23. package/dist/sbom/component-mapper.d.ts.map +1 -0
  24. package/dist/sbom/component-mapper.js +83 -0
  25. package/dist/sbom/cyclonedx-import.d.ts +34 -0
  26. package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
  27. package/dist/sbom/cyclonedx-import.js +131 -0
  28. package/dist/sbom/index.d.ts +6 -0
  29. package/dist/sbom/index.d.ts.map +1 -0
  30. package/dist/sbom/index.js +4 -0
  31. package/dist/sbom/known-packages.d.ts +13 -0
  32. package/dist/sbom/known-packages.d.ts.map +1 -0
  33. package/dist/sbom/known-packages.js +129 -0
  34. package/dist/sbom/purl.d.ts +14 -0
  35. package/dist/sbom/purl.d.ts.map +1 -0
  36. package/dist/sbom/purl.js +56 -0
  37. package/dist/sbom/types.d.ts +39 -0
  38. package/dist/sbom/types.d.ts.map +1 -0
  39. package/dist/sbom/types.js +5 -0
  40. package/dist/validation/index.d.ts +2 -0
  41. package/dist/validation/index.d.ts.map +1 -0
  42. package/dist/validation/index.js +1 -0
  43. package/dist/validation/schema-validator.d.ts +18 -0
  44. package/dist/validation/schema-validator.d.ts.map +1 -0
  45. package/dist/validation/schema-validator.js +99 -0
  46. package/package.json +8 -2
  47. package/src/datasets/crows-pairs.ts +64 -0
  48. package/src/evidence/output.ts +1 -0
  49. package/src/evidence/types.ts +9 -2
  50. package/src/evidence/validate.ts +107 -70
  51. package/src/index.ts +1 -0
  52. package/src/inference/index.ts +6 -1
  53. package/src/inference/ollama-client.ts +30 -14
  54. package/src/sbom/component-mapper.ts +107 -0
  55. package/src/sbom/cyclonedx-import.ts +198 -0
  56. package/src/sbom/index.ts +12 -0
  57. package/src/sbom/known-packages.ts +142 -0
  58. package/src/sbom/purl.ts +66 -0
  59. package/src/sbom/types.ts +34 -0
  60. package/src/validation/index.ts +7 -0
  61. package/src/validation/schema-validator.ts +133 -0
  62. package/tests/evidence/{ecef-schemas.test.ts → spec-schemas.test.ts} +3 -3
  63. package/tests/fixtures/cyclonedx-minimal.json +13 -0
  64. package/tests/fixtures/cyclonedx-mixed.json +17 -0
  65. package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
  66. package/tests/sbom/component-mapper.test.ts +258 -0
  67. package/tests/sbom/cyclonedx-import.test.ts +329 -0
  68. package/tests/sbom/purl.test.ts +102 -0
  69. package/tests/validation/schema-validator.test.ts +219 -0
@@ -0,0 +1,131 @@
1
+ import { basename, extname } from "node:path";
2
+ import { validate } from "../validation/schema-validator";
3
+ import { mapComponent } from "./component-mapper";
4
+ import { SUPPORTED_SPEC_VERSIONS } from "./types";
5
+ export function importCycloneDx(sbom, options) {
6
+ const bom = parseBom(sbom);
7
+ const scope = options?.scope ?? "all";
8
+ const allComponents = bom.components ?? [];
9
+ const warnings = [];
10
+ const totalComponents = allComponents.length;
11
+ const inScope = scope === "production"
12
+ ? allComponents.filter((c) => c.scope !== "optional" && c.scope !== "excluded")
13
+ : allComponents;
14
+ const filteredByScope = totalComponents - inScope.length;
15
+ const mapped = [];
16
+ for (const comp of inScope) {
17
+ const mapping = mapComponent(comp);
18
+ if (!mapping) {
19
+ if (!comp.name || comp.name.trim() === "") {
20
+ warnings.push({ component: "(empty)", message: "Skipped: missing or empty name" });
21
+ }
22
+ continue;
23
+ }
24
+ const version = comp.version || mapping.purlVersion;
25
+ if (!version) {
26
+ warnings.push({ component: comp.name, message: "Missing version field" });
27
+ }
28
+ mapped.push({
29
+ id: version ? `${mapping.kind}:${comp.name}:${version}` : `${mapping.kind}:${comp.name}`,
30
+ kind: mapping.kind,
31
+ name: comp.name,
32
+ ...(version ? { version } : {}),
33
+ source: "sbom-import",
34
+ });
35
+ }
36
+ const seen = new Set();
37
+ let duplicatesRemoved = 0;
38
+ const deduped = [];
39
+ for (const comp of mapped) {
40
+ if (seen.has(comp.id)) {
41
+ duplicatesRemoved++;
42
+ continue;
43
+ }
44
+ seen.add(comp.id);
45
+ deduped.push(comp);
46
+ }
47
+ const byKind = {};
48
+ for (const comp of deduped) {
49
+ byKind[comp.kind] = (byKind[comp.kind] ?? 0) + 1;
50
+ }
51
+ const { projectName, projectNameSource } = resolveProjectName(bom, options?.sourcePath);
52
+ const importTool = extractToolString(bom);
53
+ const originalTimestamp = bom.metadata?.timestamp;
54
+ const aibom = {
55
+ schemaVersion: "euconform.aibom.v1.1",
56
+ generatedAt: new Date().toISOString(),
57
+ project: { name: projectName, rootPath: "." },
58
+ components: deduped,
59
+ complianceCapabilities: {
60
+ biasEvaluation: false,
61
+ jsonExport: false,
62
+ pdfExport: false,
63
+ loggingInfrastructure: false,
64
+ humanReviewFlow: false,
65
+ incidentHandling: false,
66
+ },
67
+ metadata: buildMetadata(importTool, originalTimestamp),
68
+ };
69
+ const validation = validate(aibom);
70
+ return {
71
+ aibom,
72
+ summary: {
73
+ totalComponents,
74
+ filteredByScope,
75
+ aiRelevant: deduped.length,
76
+ skipped: inScope.length - mapped.length,
77
+ duplicatesRemoved,
78
+ byKind,
79
+ warnings,
80
+ source: buildSourceInfo(bom.specVersion, projectNameSource, importTool, originalTimestamp),
81
+ },
82
+ validation,
83
+ };
84
+ }
85
+ function resolveProjectName(bom, sourcePath) {
86
+ const metaName = bom.metadata?.component?.name;
87
+ if (metaName) {
88
+ return { projectName: metaName, projectNameSource: "metadata.component.name" };
89
+ }
90
+ if (sourcePath) {
91
+ const name = basename(sourcePath, extname(sourcePath));
92
+ return { projectName: name, projectNameSource: "sourcePath" };
93
+ }
94
+ return { projectName: "sbom-import", projectNameSource: "fallback" };
95
+ }
96
+ function extractToolString(bom) {
97
+ const tool = bom.metadata?.tools?.[0];
98
+ if (!tool?.name)
99
+ return undefined;
100
+ return tool.version ? `${tool.name} ${tool.version}` : tool.name;
101
+ }
102
+ function buildMetadata(importTool, originalTimestamp) {
103
+ const meta = { importSource: "cyclonedx" };
104
+ if (importTool)
105
+ meta.importTool = importTool;
106
+ if (originalTimestamp)
107
+ meta.originalTimestamp = originalTimestamp;
108
+ return meta;
109
+ }
110
+ function buildSourceInfo(specVersion, projectNameSource, importTool, originalTimestamp) {
111
+ const info = { bomFormat: "CycloneDX", specVersion, projectNameSource };
112
+ if (importTool)
113
+ info.importTool = importTool;
114
+ if (originalTimestamp)
115
+ info.originalTimestamp = originalTimestamp;
116
+ return info;
117
+ }
118
+ function parseBom(sbom) {
119
+ if (typeof sbom !== "object" || sbom === null) {
120
+ throw new Error("Input must be a JSON object");
121
+ }
122
+ const record = sbom;
123
+ if (record.bomFormat !== "CycloneDX") {
124
+ throw new Error(`Invalid bomFormat: expected "CycloneDX", got "${String(record.bomFormat)}"`);
125
+ }
126
+ const specVersion = String(record.specVersion ?? "");
127
+ if (!SUPPORTED_SPEC_VERSIONS.includes(specVersion)) {
128
+ throw new Error(`Unsupported specVersion "${specVersion}". Supported: ${SUPPORTED_SPEC_VERSIONS.join(", ")}`);
129
+ }
130
+ return sbom;
131
+ }
@@ -0,0 +1,6 @@
1
+ export { importCycloneDx, type CycloneDxImportResult, type ImportSummary, type ImportWarning, type ImportOptions, type ImportSourceInfo, } from "./cyclonedx-import";
2
+ export { mapComponent, type ComponentMapping } from "./component-mapper";
3
+ export { lookupKnownPackage, KNOWN_AI_PACKAGES, KNOWN_AI_SCOPES } from "./known-packages";
4
+ export { parsePurl, type ParsedPurl } from "./purl";
5
+ export type { CycloneDxBom, CycloneDxComponent } from "./types";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sbom/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpD,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { importCycloneDx, } from "./cyclonedx-import";
2
+ export { mapComponent } from "./component-mapper";
3
+ export { lookupKnownPackage, KNOWN_AI_PACKAGES, KNOWN_AI_SCOPES } from "./known-packages";
4
+ export { parsePurl } from "./purl";
@@ -0,0 +1,13 @@
1
+ import type { BomComponentKind } from "../evidence/types";
2
+ export declare const KNOWN_AI_PACKAGES: Record<string, BomComponentKind>;
3
+ export declare const KNOWN_AI_SCOPES: Record<string, BomComponentKind>;
4
+ /**
5
+ * Look up a package name in the known AI packages registry.
6
+ * For scoped packages, tries:
7
+ * 1. Exact match (e.g. @langchain/core)
8
+ * 2. Scope match via KNOWN_AI_SCOPES (e.g. @langchain -> tool)
9
+ * 3. Strip scope, check bare name (e.g. @huggingface/transformers -> transformers)
10
+ * Returns the BomComponentKind if found, null otherwise.
11
+ */
12
+ export declare function lookupKnownPackage(name: string): BomComponentKind | null;
13
+ //# sourceMappingURL=known-packages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-packages.d.ts","sourceRoot":"","sources":["../../src/sbom/known-packages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAiG9D,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAO5D,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAuBxE"}
@@ -0,0 +1,129 @@
1
+ export const KNOWN_AI_PACKAGES = {
2
+ // Models / model runtimes
3
+ "llama.cpp": "model",
4
+ "llama-cpp-python": "model",
5
+ "whisper.cpp": "model",
6
+ ggml: "model",
7
+ ctransformers: "model",
8
+ // AI Frameworks
9
+ transformers: "ai-framework",
10
+ torch: "ai-framework",
11
+ pytorch: "ai-framework",
12
+ tensorflow: "ai-framework",
13
+ "tensorflow-gpu": "ai-framework",
14
+ jax: "ai-framework",
15
+ keras: "ai-framework",
16
+ "pytorch-lightning": "ai-framework",
17
+ "scikit-learn": "ai-framework",
18
+ sklearn: "ai-framework",
19
+ xgboost: "ai-framework",
20
+ lightgbm: "ai-framework",
21
+ spacy: "ai-framework",
22
+ "huggingface-hub": "ai-framework",
23
+ diffusers: "ai-framework",
24
+ accelerate: "ai-framework",
25
+ peft: "ai-framework",
26
+ trl: "ai-framework",
27
+ onnxruntime: "ai-framework",
28
+ "onnxruntime-gpu": "ai-framework",
29
+ mlflow: "ai-framework",
30
+ // Embeddings
31
+ "sentence-transformers": "embedding",
32
+ fastembed: "embedding",
33
+ "openai-embeddings": "embedding",
34
+ "cohere-embed": "embedding",
35
+ // Inference Providers
36
+ openai: "inference-provider",
37
+ anthropic: "inference-provider",
38
+ ollama: "inference-provider",
39
+ vllm: "inference-provider",
40
+ "together-ai": "inference-provider",
41
+ replicate: "inference-provider",
42
+ groq: "inference-provider",
43
+ mistralai: "inference-provider",
44
+ cohere: "inference-provider",
45
+ "google-generativeai": "inference-provider",
46
+ ai21: "inference-provider",
47
+ // Vector Stores
48
+ pinecone: "vector-store",
49
+ "pinecone-client": "vector-store",
50
+ weaviate: "vector-store",
51
+ "weaviate-client": "vector-store",
52
+ qdrant: "vector-store",
53
+ "qdrant-client": "vector-store",
54
+ milvus: "vector-store",
55
+ pymilvus: "vector-store",
56
+ chromadb: "vector-store",
57
+ "chromadb-client": "vector-store",
58
+ pgvector: "vector-store",
59
+ faiss: "vector-store",
60
+ "faiss-cpu": "vector-store",
61
+ "faiss-gpu": "vector-store",
62
+ // Datasets
63
+ datasets: "dataset",
64
+ // Tools / Orchestration
65
+ langchain: "tool",
66
+ "langchain-core": "tool",
67
+ "langchain-community": "tool",
68
+ llamaindex: "tool",
69
+ "llama-index": "tool",
70
+ "semantic-kernel": "tool",
71
+ autogen: "tool",
72
+ crewai: "tool",
73
+ dspy: "tool",
74
+ "haystack-ai": "tool",
75
+ guidance: "tool",
76
+ guardrails: "tool",
77
+ "guardrails-ai": "tool",
78
+ lmql: "tool",
79
+ outlines: "tool",
80
+ // Scoped packages (npm)
81
+ "@langchain/core": "tool",
82
+ "@langchain/openai": "tool",
83
+ "@langchain/community": "tool",
84
+ "@langchain/anthropic": "tool",
85
+ "@huggingface/transformers": "ai-framework",
86
+ "@huggingface/inference": "inference-provider",
87
+ "@google/generative-ai": "inference-provider",
88
+ "@anthropic-ai/sdk": "inference-provider",
89
+ "@pinecone-database/pinecone": "vector-store",
90
+ "@qdrant/js-client-rest": "vector-store",
91
+ };
92
+ export const KNOWN_AI_SCOPES = {
93
+ "@langchain": "tool",
94
+ "@huggingface": "ai-framework",
95
+ "@tensorflow": "ai-framework",
96
+ "@anthropic-ai": "inference-provider",
97
+ "@pinecone-database": "vector-store",
98
+ "@qdrant": "vector-store",
99
+ };
100
+ /**
101
+ * Look up a package name in the known AI packages registry.
102
+ * For scoped packages, tries:
103
+ * 1. Exact match (e.g. @langchain/core)
104
+ * 2. Scope match via KNOWN_AI_SCOPES (e.g. @langchain -> tool)
105
+ * 3. Strip scope, check bare name (e.g. @huggingface/transformers -> transformers)
106
+ * Returns the BomComponentKind if found, null otherwise.
107
+ */
108
+ export function lookupKnownPackage(name) {
109
+ const lower = name.toLowerCase();
110
+ // 1. Exact match
111
+ const exact = KNOWN_AI_PACKAGES[lower];
112
+ if (exact)
113
+ return exact;
114
+ // Scoped package handling
115
+ if (lower.startsWith("@") && lower.includes("/")) {
116
+ const scopeEnd = lower.indexOf("/");
117
+ const scope = lower.slice(0, scopeEnd);
118
+ const bareName = lower.slice(scopeEnd + 1);
119
+ // 2. Known AI scope
120
+ const scopeKind = KNOWN_AI_SCOPES[scope];
121
+ if (scopeKind)
122
+ return scopeKind;
123
+ // 3. Strip scope, check bare name
124
+ const bareKind = KNOWN_AI_PACKAGES[bareName];
125
+ if (bareKind)
126
+ return bareKind;
127
+ }
128
+ return null;
129
+ }
@@ -0,0 +1,14 @@
1
+ export interface ParsedPurl {
2
+ scheme: "pkg";
3
+ type: string;
4
+ namespace?: string;
5
+ name: string;
6
+ version?: string;
7
+ }
8
+ /**
9
+ * Parse a Package URL (purl) string into its components.
10
+ * Follows the purl spec: pkg:type/namespace/name@version?qualifiers#subpath
11
+ * Returns null for invalid or non-pkg URLs.
12
+ */
13
+ export declare function parsePurl(purl: string): ParsedPurl | null;
14
+ //# sourceMappingURL=purl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purl.d.ts","sourceRoot":"","sources":["../../src/sbom/purl.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,KAAK,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAoDzD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Parse a Package URL (purl) string into its components.
3
+ * Follows the purl spec: pkg:type/namespace/name@version?qualifiers#subpath
4
+ * Returns null for invalid or non-pkg URLs.
5
+ */
6
+ export function parsePurl(purl) {
7
+ if (!purl || !purl.startsWith("pkg:"))
8
+ return null;
9
+ // Strip qualifiers and subpath
10
+ let rest = purl.slice(4); // remove "pkg:"
11
+ const hashIdx = rest.indexOf("#");
12
+ if (hashIdx !== -1)
13
+ rest = rest.slice(0, hashIdx);
14
+ const queryIdx = rest.indexOf("?");
15
+ if (queryIdx !== -1)
16
+ rest = rest.slice(0, queryIdx);
17
+ // Split type from the rest: type/...
18
+ const slashIdx = rest.indexOf("/");
19
+ if (slashIdx <= 0)
20
+ return null;
21
+ const type = rest.slice(0, slashIdx);
22
+ let remainder = rest.slice(slashIdx + 1);
23
+ try {
24
+ remainder = decodeURIComponent(remainder);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ // Extract version
30
+ let version;
31
+ const atIdx = remainder.lastIndexOf("@");
32
+ if (atIdx !== -1) {
33
+ version = remainder.slice(atIdx + 1);
34
+ remainder = remainder.slice(0, atIdx);
35
+ }
36
+ // Split namespace/name (last segment is name, rest is namespace)
37
+ const lastSlash = remainder.lastIndexOf("/");
38
+ let namespace;
39
+ let name;
40
+ if (lastSlash !== -1) {
41
+ namespace = remainder.slice(0, lastSlash);
42
+ name = remainder.slice(lastSlash + 1);
43
+ }
44
+ else {
45
+ name = remainder;
46
+ }
47
+ if (!name)
48
+ return null;
49
+ return {
50
+ scheme: "pkg",
51
+ type,
52
+ namespace: namespace || undefined,
53
+ name,
54
+ version: version || undefined,
55
+ };
56
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Minimal CycloneDX types covering fields relevant to AI component detection.
3
+ * Supports CycloneDX JSON format versions 1.4, 1.5, 1.6.
4
+ */
5
+ export interface CycloneDxBom {
6
+ bomFormat: "CycloneDX";
7
+ specVersion: string;
8
+ version?: number;
9
+ metadata?: CycloneDxMetadata;
10
+ components?: CycloneDxComponent[];
11
+ }
12
+ export interface CycloneDxMetadata {
13
+ timestamp?: string;
14
+ tools?: Array<{
15
+ name?: string;
16
+ version?: string;
17
+ }>;
18
+ component?: CycloneDxComponent;
19
+ }
20
+ export interface CycloneDxComponent {
21
+ type: string;
22
+ name: string;
23
+ version?: string;
24
+ purl?: string;
25
+ group?: string;
26
+ description?: string;
27
+ scope?: "required" | "optional" | "excluded";
28
+ properties?: Array<{
29
+ name: string;
30
+ value: string;
31
+ }>;
32
+ /** CycloneDX ML-BOM extensions (1.5+) */
33
+ modelCard?: unknown;
34
+ data?: Array<{
35
+ type?: string;
36
+ }>;
37
+ }
38
+ export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.4", "1.5", "1.6"];
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sbom/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;IAC7C,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,yCAAyC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjC;AAED,eAAO,MAAM,uBAAuB,gCAAiC,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Minimal CycloneDX types covering fields relevant to AI component detection.
3
+ * Supports CycloneDX JSON format versions 1.4, 1.5, 1.6.
4
+ */
5
+ export const SUPPORTED_SPEC_VERSIONS = ["1.4", "1.5", "1.6"];
@@ -0,0 +1,2 @@
1
+ export { validate, type ValidationResult, type ValidationError, type SchemaType, type ValidateOptions, } from "./schema-validator";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ export { validate, } from "./schema-validator";
@@ -0,0 +1,18 @@
1
+ export interface ValidationError {
2
+ path: string;
3
+ message: string;
4
+ keyword: string;
5
+ expected?: unknown;
6
+ received?: unknown;
7
+ }
8
+ export type SchemaType = "report.v1" | "aibom.v1" | "aibom.v1.1" | "ci.v1" | "bundle.v1";
9
+ export interface ValidationResult {
10
+ valid: boolean;
11
+ schemaType: SchemaType;
12
+ errors: ValidationError[];
13
+ }
14
+ export interface ValidateOptions {
15
+ strict?: boolean;
16
+ }
17
+ export declare function validate(data: unknown, _options?: ValidateOptions): ValidationResult;
18
+ //# sourceMappingURL=schema-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/validation/schema-validator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,WAAW,CAAC;AAEzF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAmCD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAyEpF"}
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import addFormats from "ajv-formats";
5
+ import Ajv2020 from "ajv/dist/2020";
6
+ // Resolve schema directory relative to this file at runtime.
7
+ // When bundled into packages/cli/dist, schemas are copied to dist/schemas/.
8
+ // When running from source at packages/core/src/validation/, schemas are at ../../../../docs/spec/schemas.
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const _bundledSchemasDir = resolve(__dirname, "schemas");
12
+ const schemasDir = existsSync(_bundledSchemasDir)
13
+ ? _bundledSchemasDir
14
+ : resolve(__dirname, "../../../../docs/spec/schemas");
15
+ function loadSchema(filename) {
16
+ const content = readFileSync(resolve(schemasDir, filename), "utf-8");
17
+ return JSON.parse(content);
18
+ }
19
+ const SCHEMA_MAP = {
20
+ "euconform.report.v1": { schemaFile: "report-v1.schema.json", type: "report.v1" },
21
+ "euconform.aibom.v1": { schemaFile: "aibom-v1.schema.json", type: "aibom.v1" },
22
+ "euconform.aibom.v1.1": { schemaFile: "aibom-v1.1.schema.json", type: "aibom.v1.1" },
23
+ "euconform.ci.v1": { schemaFile: "ci-v1.schema.json", type: "ci.v1" },
24
+ "euconform.bundle.v1": { schemaFile: "bundle-v1.schema.json", type: "bundle.v1" },
25
+ };
26
+ let ajvInstance = null;
27
+ function getAjv() {
28
+ if (!ajvInstance) {
29
+ ajvInstance = new Ajv2020({ allErrors: true, strict: false });
30
+ addFormats(ajvInstance);
31
+ }
32
+ return ajvInstance;
33
+ }
34
+ export function validate(data, _options) {
35
+ if (typeof data !== "object" || data === null) {
36
+ return {
37
+ valid: false,
38
+ schemaType: "report.v1",
39
+ errors: [
40
+ {
41
+ path: "",
42
+ message: "Input must be a JSON object",
43
+ keyword: "type",
44
+ expected: "object",
45
+ received: typeof data,
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ const record = data;
51
+ const schemaVersion = record.schemaVersion;
52
+ if (typeof schemaVersion !== "string") {
53
+ return {
54
+ valid: false,
55
+ schemaType: "report.v1",
56
+ errors: [
57
+ {
58
+ path: "",
59
+ message: `Missing or invalid schemaVersion field. Expected one of: ${Object.keys(SCHEMA_MAP).join(", ")}`,
60
+ keyword: "required",
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ const entry = SCHEMA_MAP[schemaVersion];
66
+ if (!entry) {
67
+ return {
68
+ valid: false,
69
+ schemaType: "report.v1",
70
+ errors: [
71
+ {
72
+ path: "/schemaVersion",
73
+ message: `Unknown schemaVersion "${schemaVersion}". Expected one of: ${Object.keys(SCHEMA_MAP).join(", ")}`,
74
+ keyword: "const",
75
+ expected: Object.keys(SCHEMA_MAP),
76
+ received: schemaVersion,
77
+ },
78
+ ],
79
+ };
80
+ }
81
+ const ajv = getAjv();
82
+ let validateFn = ajv.getSchema(schemaVersion);
83
+ if (!validateFn) {
84
+ const schema = loadSchema(entry.schemaFile);
85
+ validateFn = ajv.compile({ ...schema, $id: schemaVersion });
86
+ }
87
+ const valid = validateFn(data);
88
+ if (valid) {
89
+ return { valid: true, schemaType: entry.type, errors: [] };
90
+ }
91
+ const errors = (validateFn.errors ?? []).map((err) => ({
92
+ path: err.instancePath ?? "",
93
+ message: err.message ?? "Validation error",
94
+ keyword: err.keyword ?? "unknown",
95
+ expected: err.params,
96
+ received: undefined,
97
+ }));
98
+ return { valid: false, schemaType: entry.type, errors };
99
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@euconform/core",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "license": "MIT OR EUPL-1.2",
5
5
  "author": "Benedikt Hiepler",
6
6
  "description": "Core engine for EU AI Act compliance, risk classification, and fairness metrics.",
7
7
  "type": "module",
8
+ "sideEffects": false,
8
9
  "keywords": [
9
10
  "eu-ai-act",
10
11
  "compliance",
@@ -28,7 +29,10 @@
28
29
  "./fairness-metrics": "./src/fairness-metrics.ts",
29
30
  "./types": "./src/types.ts",
30
31
  "./scanner": "./src/scanner/index.ts",
31
- "./evidence": "./src/evidence/index.ts"
32
+ "./evidence": "./src/evidence/index.ts",
33
+ "./datasets": "./src/datasets/crows-pairs.ts",
34
+ "./validation": "./src/validation/index.ts",
35
+ "./sbom": "./src/sbom/index.ts"
32
36
  },
33
37
  "devDependencies": {
34
38
  "@types/node": "^22.15.30",
@@ -39,6 +43,8 @@
39
43
  },
40
44
  "dependencies": {
41
45
  "@xenova/transformers": "^2.17.2",
46
+ "ajv": "^8.18.0",
47
+ "ajv-formats": "^3.0.1",
42
48
  "ignore": "^7.0.3"
43
49
  },
44
50
  "scripts": {
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { CrowsPairsEntry } from "../types";
5
+
6
+ interface CrowsPairsDataset {
7
+ metadata: {
8
+ name: string;
9
+ description: string;
10
+ version: string;
11
+ license: string;
12
+ source: string;
13
+ };
14
+ pairs: Array<{
15
+ id: number;
16
+ stereotype: string;
17
+ antiStereotype: string;
18
+ biasType: string;
19
+ }>;
20
+ }
21
+
22
+ function findDatasetPath(language: "en" | "de"): string {
23
+ const fileName = `crows-pairs-${language}.json`;
24
+
25
+ // Strategy 1: Relative to this source file (works in unbundled tsc builds)
26
+ const currentDir = dirname(fileURLToPath(import.meta.url));
27
+ const fromSource = resolve(currentDir, `../../assets/datasets/${fileName}`);
28
+ if (existsSync(fromSource)) return fromSource;
29
+
30
+ // Strategy 2: Relative to process.cwd() (works in monorepo context)
31
+ const fromCwd = resolve(process.cwd(), `packages/core/assets/datasets/${fileName}`);
32
+ if (existsSync(fromCwd)) return fromCwd;
33
+
34
+ // Strategy 3: Walk up from the entry script to find the monorepo root
35
+ if (process.argv[1]) {
36
+ let dir = dirname(resolve(process.argv[1]));
37
+ for (let i = 0; i < 5; i++) {
38
+ const candidate = resolve(dir, `packages/core/assets/datasets/${fileName}`);
39
+ if (existsSync(candidate)) return candidate;
40
+ dir = dirname(dir);
41
+ }
42
+ }
43
+
44
+ throw new Error(
45
+ `CrowS-Pairs dataset (${language}) not found. Ensure packages/core/assets/datasets/ exists.`
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Load CrowS-Pairs dataset from bundled assets.
51
+ * Converts the structured JSON format to CrowsPairsEntry[] for the bias engine.
52
+ */
53
+ export function loadCrowsPairsDataset(language: "en" | "de"): CrowsPairsEntry[] {
54
+ const filePath = findDatasetPath(language);
55
+ const raw = readFileSync(filePath, "utf-8");
56
+ const dataset: CrowsPairsDataset = JSON.parse(raw);
57
+
58
+ return dataset.pairs.map((pair) => ({
59
+ id: pair.id,
60
+ sent_more: pair.stereotype,
61
+ sent_less: pair.antiStereotype,
62
+ bias_type: pair.biasType,
63
+ }));
64
+ }
@@ -81,6 +81,7 @@ const SOURCE_PRIORITY: Record<BomComponent["source"], number> = {
81
81
  config: 4,
82
82
  code: 3,
83
83
  "lock-file": 2,
84
+ "sbom-import": 2,
84
85
  docs: 1,
85
86
  };
86
87