@cdxgen/cdxgen 13.0.1 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/managers/binary.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
recordActivity,
|
|
19
19
|
recordSymlinkResolution,
|
|
20
20
|
} from "../core/activity.js";
|
|
21
|
+
import { recordDegradation } from "../core/buildLedger.js";
|
|
21
22
|
import { extractPathEnv } from "../core/env.js";
|
|
22
23
|
import {
|
|
23
24
|
getTmpDir,
|
|
@@ -779,6 +780,16 @@ export async function getOSPackages(src, imageConfig, options = {}) {
|
|
|
779
780
|
.filter(Boolean),
|
|
780
781
|
),
|
|
781
782
|
).sort();
|
|
783
|
+
if (binPaths.length) {
|
|
784
|
+
recordDegradation("binary.paths.inferred", {
|
|
785
|
+
ecosystem: "generic",
|
|
786
|
+
tool: "trivy",
|
|
787
|
+
impact: "none",
|
|
788
|
+
path: src,
|
|
789
|
+
detail:
|
|
790
|
+
"The image config carried no PATH, so executable directories were inferred by scanning the filesystem.",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
782
793
|
if (DEBUG_MODE && binPaths.length) {
|
|
783
794
|
console.log(
|
|
784
795
|
`Falling back to inferred binary paths for ${src}: ${binPaths.join(", ")}`,
|
|
@@ -887,9 +898,14 @@ export async function getOSPackages(src, imageConfig, options = {}) {
|
|
|
887
898
|
}
|
|
888
899
|
}
|
|
889
900
|
break;
|
|
901
|
+
case "alpaquita":
|
|
902
|
+
purl_type = "apk";
|
|
903
|
+
break;
|
|
890
904
|
default:
|
|
891
905
|
if (distro_id_like.includes("debian")) {
|
|
892
906
|
purl_type = "deb";
|
|
907
|
+
} else if (distro_id_like.includes("alpine")) {
|
|
908
|
+
purl_type = "apk";
|
|
893
909
|
} else if (
|
|
894
910
|
distro_id_like.includes("rhel") ||
|
|
895
911
|
distro_id_like.includes("centos") ||
|
|
@@ -3296,6 +3312,14 @@ export function getDotnetSlices(src, slicesFile) {
|
|
|
3296
3312
|
"You must install or update .NET to run this application",
|
|
3297
3313
|
)
|
|
3298
3314
|
) {
|
|
3315
|
+
recordDegradation("dotnet.sdk.missing", {
|
|
3316
|
+
ecosystem: "csharp",
|
|
3317
|
+
tool: "dotnet",
|
|
3318
|
+
impact: "components",
|
|
3319
|
+
command: `dosai ${args.join(" ")}`,
|
|
3320
|
+
detail:
|
|
3321
|
+
"dosai reported that no .NET SDK is installed, so dotnet-related evidence for this image is incomplete.",
|
|
3322
|
+
});
|
|
3299
3323
|
console.log(
|
|
3300
3324
|
"Dotnet SDK is not installed. Please use the cdxgen dotnet container images to generate slices for this project.",
|
|
3301
3325
|
);
|
package/lib/managers/docker.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
recordDecisionActivity,
|
|
22
22
|
recordSensitiveFileRead,
|
|
23
23
|
} from "../core/activity.js";
|
|
24
|
+
import { deferFailOnError } from "../core/deferredExit.js";
|
|
24
25
|
import { extractPathEnv } from "../core/env.js";
|
|
25
26
|
import {
|
|
26
27
|
getAllFiles,
|
|
@@ -1407,7 +1408,11 @@ export const extractTar = async (fullImageName, dir, options) => {
|
|
|
1407
1408
|
} else if (DEBUG_MODE) {
|
|
1408
1409
|
console.log(err.code, "is not handled yet in extractTar method.");
|
|
1409
1410
|
}
|
|
1410
|
-
options
|
|
1411
|
+
deferFailOnError(options, {
|
|
1412
|
+
ecosystem: "container",
|
|
1413
|
+
tool: "docker",
|
|
1414
|
+
detail: "extracting the image archive failed",
|
|
1415
|
+
});
|
|
1411
1416
|
return false;
|
|
1412
1417
|
}
|
|
1413
1418
|
};
|
|
@@ -1598,10 +1603,18 @@ export const exportArchive = async (fullImageName, options = {}) => {
|
|
|
1598
1603
|
return exportData;
|
|
1599
1604
|
}
|
|
1600
1605
|
console.log(`Unable to extract image archive to ${tempDir}`);
|
|
1601
|
-
options
|
|
1606
|
+
deferFailOnError(options, {
|
|
1607
|
+
ecosystem: "container",
|
|
1608
|
+
tool: "docker",
|
|
1609
|
+
detail: "the image archive could not be extracted",
|
|
1610
|
+
});
|
|
1602
1611
|
} catch (_err) {
|
|
1603
1612
|
// ignore
|
|
1604
|
-
options
|
|
1613
|
+
deferFailOnError(options, {
|
|
1614
|
+
ecosystem: "container",
|
|
1615
|
+
tool: "docker",
|
|
1616
|
+
detail: "the image archive could not be extracted",
|
|
1617
|
+
});
|
|
1605
1618
|
}
|
|
1606
1619
|
return undefined;
|
|
1607
1620
|
};
|
|
@@ -1686,7 +1699,11 @@ export const extractFromManifest = async (
|
|
|
1686
1699
|
}
|
|
1687
1700
|
} else {
|
|
1688
1701
|
console.log(err);
|
|
1689
|
-
options
|
|
1702
|
+
deferFailOnError(options, {
|
|
1703
|
+
ecosystem: "container",
|
|
1704
|
+
tool: "docker",
|
|
1705
|
+
detail: "reading the image manifest failed",
|
|
1706
|
+
});
|
|
1690
1707
|
}
|
|
1691
1708
|
}
|
|
1692
1709
|
}
|
|
@@ -1713,7 +1730,11 @@ export const extractFromManifest = async (
|
|
|
1713
1730
|
? join(allLayersExplodedDir, lastLayerConfig.config.WorkingDir)
|
|
1714
1731
|
: "";
|
|
1715
1732
|
} catch (_err) {
|
|
1716
|
-
options
|
|
1733
|
+
deferFailOnError(options, {
|
|
1734
|
+
ecosystem: "container",
|
|
1735
|
+
tool: "docker",
|
|
1736
|
+
detail: "reading the image manifest failed",
|
|
1737
|
+
});
|
|
1717
1738
|
}
|
|
1718
1739
|
}
|
|
1719
1740
|
}
|
|
@@ -237,9 +237,14 @@ export function toHuggingFacePurl(repoId, version, repositoryUrl) {
|
|
|
237
237
|
* Normalize a direct Hugging Face URL or purl into a repo reference.
|
|
238
238
|
*
|
|
239
239
|
* @param {string} value direct URL, API URL, or purl
|
|
240
|
+
* @param {object} [parseOptions] parsing options
|
|
241
|
+
* @param {boolean} [parseOptions.explicitOnly=false] resolve only unambiguous
|
|
242
|
+
* references (`pkg:huggingface/...` purls and huggingface.co URLs) so that
|
|
243
|
+
* ambiguous `owner/repo`-shaped values keep their filesystem-path meaning
|
|
244
|
+
* until an AI inventory type is explicitly selected
|
|
240
245
|
* @returns {{ assetType: string, repoId: string, version?: string }|undefined} normalized reference
|
|
241
246
|
*/
|
|
242
|
-
export function normalizeHuggingFaceReference(value) {
|
|
247
|
+
export function normalizeHuggingFaceReference(value, parseOptions = {}) {
|
|
243
248
|
if (!value) {
|
|
244
249
|
return undefined;
|
|
245
250
|
}
|
|
@@ -276,9 +281,11 @@ export function normalizeHuggingFaceReference(value) {
|
|
|
276
281
|
// Fall through to the remaining parsers.
|
|
277
282
|
}
|
|
278
283
|
}
|
|
279
|
-
const directTypedMatch =
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
const directTypedMatch = parseOptions.explicitOnly
|
|
285
|
+
? undefined
|
|
286
|
+
: normalizedValue.match(
|
|
287
|
+
/^(models|datasets|spaces)\/([^/\s]+\/[^/\s]+)(?:\/revision\/([^/\s?#]+))?$/u,
|
|
288
|
+
);
|
|
282
289
|
if (directTypedMatch) {
|
|
283
290
|
return {
|
|
284
291
|
assetType:
|
|
@@ -296,9 +303,10 @@ export function normalizeHuggingFaceReference(value) {
|
|
|
296
303
|
const looksLikeFilesystemPath =
|
|
297
304
|
/^(?:\/|\.{1,2}(?:\/|$)|~\/|[a-z]:[\\/])/iu.test(normalizedValue) ||
|
|
298
305
|
normalizedValue.includes("\\");
|
|
299
|
-
const directRepoId =
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
const directRepoId =
|
|
307
|
+
looksLikeFilesystemPath || parseOptions.explicitOnly
|
|
308
|
+
? undefined
|
|
309
|
+
: sanitizeHuggingFaceRepoId(normalizedValue);
|
|
302
310
|
if (directRepoId) {
|
|
303
311
|
return { assetType: "model", repoId: directRepoId };
|
|
304
312
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// An ONNX model is a protobuf-encoded ModelProto. Only the header fields are
|
|
4
|
+
// read here; the graph payload is skipped by offset, so inspection stays
|
|
5
|
+
// cheap for multi-gigabyte files and never loads tensors.
|
|
6
|
+
//
|
|
7
|
+
// ModelProto fields (https://onnx.ai/onnx/repo-docs/Model.html):
|
|
8
|
+
// 1 ir_version (varint), 2 producer_name (string),
|
|
9
|
+
// 3 producer_version (string), 4 domain (string),
|
|
10
|
+
// 5 model_version (varint), 6 doc_string (string),
|
|
11
|
+
// 7 graph (message, skipped), 8 opset_import (repeated message).
|
|
12
|
+
// Each opset_import entry is an OperatorSetIdProto:
|
|
13
|
+
// 1 domain (string), 2 version (varint).
|
|
14
|
+
|
|
15
|
+
/** Fields are walked from the front; a hostile file cannot force a scan
|
|
16
|
+
* deeper than this many top-level entries. */
|
|
17
|
+
const MAX_TOP_LEVEL_FIELDS = 128;
|
|
18
|
+
/** Largest single field payload that will actually be read into memory. */
|
|
19
|
+
const MAX_FIELD_BYTES = 4096;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read the identifying header fields of an ONNX model file.
|
|
23
|
+
*
|
|
24
|
+
* Only `ir_version`, `producer_name`, `producer_version`, `model_version`,
|
|
25
|
+
* and the `opset_import` entries are extracted. The graph and every other
|
|
26
|
+
* payload are skipped by offset without being read, so no tensor data is
|
|
27
|
+
* ever loaded and no model code is executed.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} filePath Path to the `.onnx` file
|
|
30
|
+
* @returns {{
|
|
31
|
+
* irVersion: number,
|
|
32
|
+
* producerName: string,
|
|
33
|
+
* producerVersion: string,
|
|
34
|
+
* modelVersion: number,
|
|
35
|
+
* opsets: Array<{domain: string, version: number}>,
|
|
36
|
+
* } | undefined} Header summary, or undefined when it cannot be read
|
|
37
|
+
*/
|
|
38
|
+
export function readOnnxHeader(filePath) {
|
|
39
|
+
let fd;
|
|
40
|
+
try {
|
|
41
|
+
const stats = statSync(filePath);
|
|
42
|
+
if (!stats.isFile() || stats.size < 2) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
fd = openSync(filePath, "r");
|
|
46
|
+
// The whole file is not buffered; fields are fetched by offset. A small
|
|
47
|
+
// sliding buffer is re-used for varints and short strings.
|
|
48
|
+
const reader = new OffsetReader(fd, stats.size);
|
|
49
|
+
const result = {
|
|
50
|
+
irVersion: 0,
|
|
51
|
+
producerName: "",
|
|
52
|
+
producerVersion: "",
|
|
53
|
+
modelVersion: 0,
|
|
54
|
+
opsets: [],
|
|
55
|
+
};
|
|
56
|
+
let fieldsRead = 0;
|
|
57
|
+
for (let i = 0; i < MAX_TOP_LEVEL_FIELDS; i += 1) {
|
|
58
|
+
const tag = reader.readVarint();
|
|
59
|
+
if (tag === undefined) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const fieldNumber = Number(tag >> 3n);
|
|
63
|
+
const wireType = Number(tag & 7n);
|
|
64
|
+
if (fieldNumber === 0) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (wireType === 0) {
|
|
68
|
+
const value = reader.readVarint();
|
|
69
|
+
if (value === undefined) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
fieldsRead += 1;
|
|
73
|
+
if (fieldNumber === 1) {
|
|
74
|
+
result.irVersion = Number(value);
|
|
75
|
+
} else if (fieldNumber === 5) {
|
|
76
|
+
result.modelVersion = Number(value);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (wireType !== 2) {
|
|
81
|
+
// Group and fixed-width wire types do not appear in ModelProto at the
|
|
82
|
+
// top level; stop rather than guess their length.
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const length = reader.readVarint();
|
|
86
|
+
if (length === undefined) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const byteLength = Number(length);
|
|
90
|
+
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (fieldNumber === 8 && byteLength <= MAX_FIELD_BYTES) {
|
|
94
|
+
const payload = reader.readBytes(byteLength);
|
|
95
|
+
if (!payload) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const opset = parseOperatorSetEntry(payload);
|
|
99
|
+
if (opset) {
|
|
100
|
+
result.opsets.push(opset);
|
|
101
|
+
}
|
|
102
|
+
fieldsRead += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
(fieldNumber === 2 || fieldNumber === 3) &&
|
|
107
|
+
byteLength <= MAX_FIELD_BYTES
|
|
108
|
+
) {
|
|
109
|
+
const payload = reader.readBytes(byteLength);
|
|
110
|
+
if (!payload) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (fieldNumber === 2) {
|
|
114
|
+
result.producerName = payload.toString("utf-8");
|
|
115
|
+
} else {
|
|
116
|
+
result.producerVersion = payload.toString("utf-8");
|
|
117
|
+
}
|
|
118
|
+
fieldsRead += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Skip anything else (notably the graph) by offset.
|
|
122
|
+
if (!reader.skip(byteLength)) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
fieldsRead += 1;
|
|
126
|
+
}
|
|
127
|
+
// A real ModelProto always carries at least an ir_version; zero consumed
|
|
128
|
+
// fields means the file is not an ONNX model at all.
|
|
129
|
+
if (fieldsRead === 0) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
} finally {
|
|
136
|
+
if (fd !== undefined) {
|
|
137
|
+
closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reader that fetches bytes from a file descriptor by absolute offset.
|
|
144
|
+
*
|
|
145
|
+
* @param {number} fd Open file descriptor
|
|
146
|
+
* @param {number} fileSize Total file size
|
|
147
|
+
*/
|
|
148
|
+
class OffsetReader {
|
|
149
|
+
constructor(fd, fileSize) {
|
|
150
|
+
this.fd = fd;
|
|
151
|
+
this.fileSize = fileSize;
|
|
152
|
+
this.offset = 0;
|
|
153
|
+
this.buffer = Buffer.alloc(16);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read a protobuf varint at the cursor.
|
|
158
|
+
*
|
|
159
|
+
* @returns {bigint|undefined} Value, or undefined at end of input
|
|
160
|
+
*/
|
|
161
|
+
readVarint() {
|
|
162
|
+
let result = 0n;
|
|
163
|
+
let shift = 0n;
|
|
164
|
+
for (let i = 0; i < 10; i += 1) {
|
|
165
|
+
if (!this.fillBuffer(1)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
const byte = this.buffer[0];
|
|
169
|
+
this.offset += 1;
|
|
170
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
171
|
+
if ((byte & 0x80) === 0) {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
shift += 7n;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read a length-delimited payload at the cursor.
|
|
181
|
+
*
|
|
182
|
+
* @param {number} length Payload length
|
|
183
|
+
* @returns {Buffer|undefined} Payload, or undefined when unavailable
|
|
184
|
+
*/
|
|
185
|
+
readBytes(length) {
|
|
186
|
+
const payload = Buffer.alloc(length);
|
|
187
|
+
const bytesRead = readSync(this.fd, payload, 0, length, this.offset);
|
|
188
|
+
if (bytesRead !== length) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
this.offset += length;
|
|
192
|
+
return payload;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Advance the cursor without reading the payload.
|
|
197
|
+
*
|
|
198
|
+
* @param {number} length Bytes to skip
|
|
199
|
+
* @returns {boolean} true when the skip stayed within the file
|
|
200
|
+
*/
|
|
201
|
+
skip(length) {
|
|
202
|
+
const next = this.offset + length;
|
|
203
|
+
if (next > this.fileSize) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
this.offset = next;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Ensure up to `length` bytes are available in the scratch buffer.
|
|
212
|
+
*
|
|
213
|
+
* @param {number} length Bytes needed
|
|
214
|
+
* @returns {boolean} true when the read succeeded
|
|
215
|
+
*/
|
|
216
|
+
fillBuffer(length) {
|
|
217
|
+
if (this.offset + length > this.fileSize) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const bytesRead = readSync(this.fd, this.buffer, 0, length, this.offset);
|
|
221
|
+
return bytesRead === length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse one OperatorSetIdProto payload.
|
|
227
|
+
*
|
|
228
|
+
* @param {Buffer} payload Field payload
|
|
229
|
+
* @returns {{domain: string, version: number}|undefined} Parsed entry
|
|
230
|
+
*/
|
|
231
|
+
function parseOperatorSetEntry(payload) {
|
|
232
|
+
let pos = 0;
|
|
233
|
+
let domain = "";
|
|
234
|
+
let version = 0;
|
|
235
|
+
while (pos < payload.length) {
|
|
236
|
+
const tag = readVarintFrom(payload, pos);
|
|
237
|
+
if (!tag) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
pos = tag.next;
|
|
241
|
+
const fieldNumber = Number(tag.value >> 3n);
|
|
242
|
+
const wireType = Number(tag.value & 7n);
|
|
243
|
+
if (wireType === 0) {
|
|
244
|
+
const value = readVarintFrom(payload, pos);
|
|
245
|
+
if (!value) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
pos = value.next;
|
|
249
|
+
if (fieldNumber === 2) {
|
|
250
|
+
version = Number(value.value);
|
|
251
|
+
}
|
|
252
|
+
} else if (wireType === 2) {
|
|
253
|
+
const length = readVarintFrom(payload, pos);
|
|
254
|
+
if (!length) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
pos = length.next;
|
|
258
|
+
const byteLength = Number(length.value);
|
|
259
|
+
if (pos + byteLength > payload.length) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (fieldNumber === 1) {
|
|
263
|
+
domain = payload.toString("utf-8", pos, pos + byteLength);
|
|
264
|
+
}
|
|
265
|
+
pos += byteLength;
|
|
266
|
+
} else {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { domain, version };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read a varint from a buffer at a position.
|
|
275
|
+
*
|
|
276
|
+
* @param {Buffer} buffer Source buffer
|
|
277
|
+
* @param {number} pos Start position
|
|
278
|
+
* @returns {{value: bigint, next: number}|undefined} Result, or undefined
|
|
279
|
+
*/
|
|
280
|
+
function readVarintFrom(buffer, pos) {
|
|
281
|
+
let value = 0n;
|
|
282
|
+
let shift = 0n;
|
|
283
|
+
let cursor = pos;
|
|
284
|
+
for (let i = 0; i < 10 && cursor < buffer.length; i += 1) {
|
|
285
|
+
const byte = buffer[cursor];
|
|
286
|
+
cursor += 1;
|
|
287
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
288
|
+
if ((byte & 0x80) === 0) {
|
|
289
|
+
return { value, next: cursor };
|
|
290
|
+
}
|
|
291
|
+
shift += 7n;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// A safetensors file begins with an 8-byte little-endian unsigned integer
|
|
4
|
+
// giving the byte length of a JSON header, followed by that JSON header, and
|
|
5
|
+
// then the raw tensor buffers. Only the header is parsed here: tensor data is
|
|
6
|
+
// never read, so inspection stays cheap and side-effect free.
|
|
7
|
+
const HEADER_LENGTH_BYTES = 8;
|
|
8
|
+
// A header longer than this almost certainly indicates a non-safetensors file
|
|
9
|
+
// or a corrupt/hostile input; refuse to allocate for it.
|
|
10
|
+
const MAX_HEADER_BYTES = 100 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read and parse the JSON header of a safetensors model file.
|
|
14
|
+
*
|
|
15
|
+
* Only the leading header is read; tensor payloads are ignored. The returned
|
|
16
|
+
* object separates the optional `__metadata__` map from the per-tensor
|
|
17
|
+
* descriptors.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} filePath Path to a `.safetensors` file
|
|
20
|
+
* @returns {{
|
|
21
|
+
* metadata: Object,
|
|
22
|
+
* tensorCount: number,
|
|
23
|
+
* dtypes: string[],
|
|
24
|
+
* totalParameters: number,
|
|
25
|
+
* } | undefined} Parsed header summary, or undefined when it cannot be read
|
|
26
|
+
*/
|
|
27
|
+
export function readSafetensorsHeader(filePath) {
|
|
28
|
+
let fd;
|
|
29
|
+
try {
|
|
30
|
+
const stats = statSync(filePath);
|
|
31
|
+
if (stats.size < HEADER_LENGTH_BYTES) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
fd = openSync(filePath, "r");
|
|
35
|
+
const lengthBuffer = Buffer.alloc(HEADER_LENGTH_BYTES);
|
|
36
|
+
readSync(fd, lengthBuffer, 0, HEADER_LENGTH_BYTES, 0);
|
|
37
|
+
const headerLength = Number(lengthBuffer.readBigUInt64LE(0));
|
|
38
|
+
if (
|
|
39
|
+
!Number.isSafeInteger(headerLength) ||
|
|
40
|
+
headerLength <= 0 ||
|
|
41
|
+
headerLength > MAX_HEADER_BYTES ||
|
|
42
|
+
headerLength + HEADER_LENGTH_BYTES > stats.size
|
|
43
|
+
) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const headerBuffer = Buffer.alloc(headerLength);
|
|
47
|
+
readSync(fd, headerBuffer, 0, headerLength, HEADER_LENGTH_BYTES);
|
|
48
|
+
const header = JSON.parse(headerBuffer.toString("utf-8"));
|
|
49
|
+
if (!header || typeof header !== "object") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return summarizeHeader(header);
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
} finally {
|
|
56
|
+
if (fd !== undefined) {
|
|
57
|
+
try {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore close failures
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Summarize a parsed safetensors header into metadata and tensor statistics.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} header Parsed safetensors JSON header
|
|
70
|
+
* @returns {{
|
|
71
|
+
* metadata: Object,
|
|
72
|
+
* tensorCount: number,
|
|
73
|
+
* dtypes: string[],
|
|
74
|
+
* totalParameters: number,
|
|
75
|
+
* }} Header summary
|
|
76
|
+
*/
|
|
77
|
+
function summarizeHeader(header) {
|
|
78
|
+
const metadata =
|
|
79
|
+
header.__metadata__ && typeof header.__metadata__ === "object"
|
|
80
|
+
? header.__metadata__
|
|
81
|
+
: {};
|
|
82
|
+
const dtypes = new Set();
|
|
83
|
+
let tensorCount = 0;
|
|
84
|
+
let totalParameters = 0;
|
|
85
|
+
for (const [key, descriptor] of Object.entries(header)) {
|
|
86
|
+
if (
|
|
87
|
+
key === "__metadata__" ||
|
|
88
|
+
!descriptor ||
|
|
89
|
+
typeof descriptor !== "object"
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
tensorCount++;
|
|
94
|
+
if (typeof descriptor.dtype === "string") {
|
|
95
|
+
dtypes.add(descriptor.dtype);
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(descriptor.shape) && descriptor.shape.length) {
|
|
98
|
+
const elements = descriptor.shape.reduce(
|
|
99
|
+
(product, dimension) =>
|
|
100
|
+
Number.isFinite(dimension) ? product * dimension : product,
|
|
101
|
+
1,
|
|
102
|
+
);
|
|
103
|
+
if (Number.isFinite(elements)) {
|
|
104
|
+
totalParameters += elements;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
metadata,
|
|
110
|
+
tensorCount,
|
|
111
|
+
dtypes: Array.from(dtypes).sort(),
|
|
112
|
+
totalParameters,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/lib/server/server.js
CHANGED
|
@@ -520,7 +520,7 @@ const start = (options) => {
|
|
|
520
520
|
ensureAiProvenanceProperties(bomNSData.bomJson, reqOptions);
|
|
521
521
|
await ensureAiOversightProperties(bomNSData.bomJson, reqOptions);
|
|
522
522
|
}
|
|
523
|
-
bomNSData = postProcess(
|
|
523
|
+
bomNSData = await postProcess(
|
|
524
524
|
bomNSData,
|
|
525
525
|
{ ...reqOptions, executeOsQuery },
|
|
526
526
|
srcDir,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared assembly of CycloneDX document-level annotations.
|
|
3
|
+
*
|
|
4
|
+
* Two postgen features attach findings to the BOM document as annotations
|
|
5
|
+
* whose `subjects` is the BOM serial number: the BOM audit's rule findings and
|
|
6
|
+
* the build introspection's remediations. Both describe the document rather
|
|
7
|
+
* than a component, both are annotated by the cdxgen tool component from
|
|
8
|
+
* metadata, and both carry the document's own timestamp because an annotation
|
|
9
|
+
* describes the document it is attached to. The structured facts travel as
|
|
10
|
+
* `name`/`value` pairs rendered into the annotation text, one `:evidence:<key>`
|
|
11
|
+
* entry per evidence item.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { DEBUG_MODE } from "../../core/activity.js";
|
|
15
|
+
import { getTimestamp } from "../../core/fs.js";
|
|
16
|
+
import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find the cdxgen tool component that annotates the document.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} bomJson CycloneDX BOM
|
|
22
|
+
* @returns {Object[]} The cdxgen tool components, empty when metadata carries none.
|
|
23
|
+
*/
|
|
24
|
+
function findCdxgenAnnotator(bomJson) {
|
|
25
|
+
return (
|
|
26
|
+
bomJson?.metadata?.tools?.components?.filter((c) => c.name === "cdxgen") ||
|
|
27
|
+
[]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Flatten an evidence object into `:evidence:<key>` property entries. Nested
|
|
33
|
+
* values are serialized as JSON so every property value stays a string.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} propertyNamePrefix Property namespace, e.g. "cdx:audit".
|
|
36
|
+
* @param {Object} evidence Arbitrary key/value evidence facts.
|
|
37
|
+
* @returns {{name: string, value: string}[]} Property entries.
|
|
38
|
+
*/
|
|
39
|
+
export function evidenceProperties(propertyNamePrefix, evidence) {
|
|
40
|
+
const properties = [];
|
|
41
|
+
if (evidence && typeof evidence === "object") {
|
|
42
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
43
|
+
const propValue =
|
|
44
|
+
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
45
|
+
properties.push({
|
|
46
|
+
name: `${propertyNamePrefix}:evidence:${key}`,
|
|
47
|
+
value: propValue,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return properties;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build one document-level annotation. Returns undefined when the BOM carries
|
|
56
|
+
* no cdxgen tool component to annotate with; the caller decides whether that
|
|
57
|
+
* is worth a warning.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} params Annotation inputs.
|
|
60
|
+
* @param {Object} params.bomJson CycloneDX BOM the annotation describes.
|
|
61
|
+
* @param {string} params.message Leading annotation sentence.
|
|
62
|
+
* @param {{name: string, value: string}[]} params.properties Structured facts rendered into the annotation text.
|
|
63
|
+
* @returns {Object|undefined} CycloneDX annotation, or undefined without an annotator.
|
|
64
|
+
*/
|
|
65
|
+
export function buildDocumentAnnotation({ bomJson, message, properties }) {
|
|
66
|
+
const cdxgenAnnotator = findCdxgenAnnotator(bomJson);
|
|
67
|
+
if (!cdxgenAnnotator.length) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
subjects: [bomJson.serialNumber],
|
|
72
|
+
annotator: {
|
|
73
|
+
component: cdxgenAnnotator[0],
|
|
74
|
+
},
|
|
75
|
+
// An annotation describes the document it is attached to, so it carries
|
|
76
|
+
// that document's timestamp rather than the moment the annotation was
|
|
77
|
+
// built.
|
|
78
|
+
timestamp: bomJson.metadata?.timestamp || getTimestamp(),
|
|
79
|
+
text: buildAnnotationText(message, properties),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Warn that a document could not be annotated because it carries no cdxgen
|
|
85
|
+
* tool component. Suppressed outside debug mode, where the missing annotator
|
|
86
|
+
* is an expected shape for foreign BOMs.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} feature Feature that wanted to annotate.
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
91
|
+
export function warnMissingAnnotator(feature) {
|
|
92
|
+
if (DEBUG_MODE) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`Cannot create ${feature} annotations: cdxgen tool component not found in metadata`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|