@cdxgen/cdxgen 13.6.0 → 13.7.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 +15 -8
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +256 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +25 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +18 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +52 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +80 -1
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +609 -898
- package/lib/cli/jsBom.js +3 -0
- package/lib/cli/jvmBom.js +365 -40
- package/lib/cli/managedBom.js +19 -3
- package/lib/cli/nativeBom.js +10 -0
- package/lib/core/env.js +26 -0
- package/lib/core/fs.js +126 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/processTree.js +99 -12
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +15 -1
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1082 -6
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-python.js +89 -28
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalasem.js +2 -1
- package/lib/evinser/evinser.js +196 -34
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +112 -1
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +106 -13
- package/lib/inventory/dosai.js +236 -1
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +164 -14
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +132 -38
- package/lib/inventory/evidenceUtils.js +2 -1
- package/lib/inventory/formulationParsers.js +95 -13
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/source.js +65 -6
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/server/server.js +36 -34
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/validator/bomValidator.js +8 -1
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +48 -7
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +9 -9
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +16 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +2 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +30 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +10 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +16 -5
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +248 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +61 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +19 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +22 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +15 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- 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/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-quantum migration risk for cryptographic assets (cdxgen/cdxgen#4592).
|
|
3
|
+
*
|
|
4
|
+
* The facts come from data/pqc-migration.json: the classes of NIST IR 8547
|
|
5
|
+
* (final, October 2026) Tables 2, 4 and 8 and section 4.1.3, plus FIPS 186-5
|
|
6
|
+
* for DSA, each with its members (algorithm families, OIDs and normalised
|
|
7
|
+
* names), the transition the source states and the rationale text. This
|
|
8
|
+
* module classifies a component and builds the risk entry; it adds no fact of
|
|
9
|
+
* its own.
|
|
10
|
+
*
|
|
11
|
+
* Classification of an algorithm asset (cryptoProperties.assetType
|
|
12
|
+
* `algorithm`), first match wins. A hybrid combiner (primitive `combiner`) is
|
|
13
|
+
* not classified: IR 8547 section 3.2.1 approves a composite scheme with one
|
|
14
|
+
* approved input, so the OID of one input does not decide it.
|
|
15
|
+
*
|
|
16
|
+
* 1. `algorithmProperties.algorithmFamily`. A quantum-safe family (SHA-2,
|
|
17
|
+
* SHA-3, AES, ML-KEM, …) is safe, except its 112-bit members (SHA-224,
|
|
18
|
+
* SHA-512/224, SHA3-224), which are recognised by OID or name.
|
|
19
|
+
* 2. `cryptoProperties.oid` (rsaEncryption, id-ecPublicKey, the RSA, ECDSA,
|
|
20
|
+
* EdDSA and DSA signature OIDs, X25519/X448, DH, …).
|
|
21
|
+
* 3. The component name, lower case with only letters and digits kept
|
|
22
|
+
* ("SHA-512/224" is `sha512224`), then resolveAlgorithmFamily(name). A
|
|
23
|
+
* name that resolves to a hash family but names a DSA, RSA or ECDSA
|
|
24
|
+
* signature ("SHA256withDSA", "dsa-with-sha256", "RSA-SHA-512") is that
|
|
25
|
+
* signature algorithm, not its digest.
|
|
26
|
+
*
|
|
27
|
+
* A class whose members have a key family (rsa, ff, ec) is checked against
|
|
28
|
+
* the minimum key size of IR 8547 Tables 2 and 4 (112 bits of classical
|
|
29
|
+
* strength: RSA and finite-field 2048, elliptic curve 224). An algorithm with
|
|
30
|
+
* a known smaller key, and a public or private key asset
|
|
31
|
+
* (`related-crypto-material`) whose size is smaller, is classed
|
|
32
|
+
* `below-minimum-strength` instead. The key size of an algorithm comes from
|
|
33
|
+
* the first of these that gives a plausible key size (at least 512 for rsa
|
|
34
|
+
* and ff, 160 to 571 for ec): a trailing number in its name ("RSA-1024")
|
|
35
|
+
* unless it follows a hash token ("RSASSA-PSS-SHA-512",
|
|
36
|
+
* "sha512WithRSAEncryption-512"), an RFC 7919 group name ("ffdhe2048"), its
|
|
37
|
+
* `parameterSetIdentifier` (a digest size such as "256" on an RSA signature
|
|
38
|
+
* is not a key size) or the bit size in its curve name. A key asset is
|
|
39
|
+
* checked with its own size against the base class of its algorithm, so a
|
|
40
|
+
* 4096-bit key of an algorithm named "RSA-1024" is not reported again.
|
|
41
|
+
*
|
|
42
|
+
* The likelihood and impact levels are the fixed per-class rating the data
|
|
43
|
+
* file records. No confidence is emitted (the rating is not a measurement)
|
|
44
|
+
* and no organisational fact is invented: no owner, status or responses.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { readFileSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
|
|
50
|
+
import { dirNameStr } from "../core/paths.js";
|
|
51
|
+
import { resolveAlgorithmFamily } from "./cryptoAlgorithmFamily.js";
|
|
52
|
+
|
|
53
|
+
/** Prefix of every risk refId this module derives. */
|
|
54
|
+
export const PQC_RISK_REF_ID_PREFIX = "risk-pqc-";
|
|
55
|
+
|
|
56
|
+
const pqcData = JSON.parse(
|
|
57
|
+
readFileSync(join(dirNameStr, "data", "pqc-migration.json"), "utf-8"),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const QUANTUM_SAFE_FAMILIES = new Set(pqcData.quantumSafeFamilies);
|
|
61
|
+
const HASH_FAMILIES = new Set(["SHA-1", "SHA-2", "SHA-3"]);
|
|
62
|
+
const NAMED_GROUP_KEY_SIZES = new Map(
|
|
63
|
+
Object.entries(pqcData.namedGroupKeySizes).filter(
|
|
64
|
+
([name]) => !name.startsWith("$"),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
const UNCLASSIFIED_PRIMITIVES = new Set(pqcData.unclassifiedPrimitives);
|
|
68
|
+
const BELOW_MINIMUM_CLASS = "below-minimum-strength";
|
|
69
|
+
const SYMMETRIC_112_CLASS = "symmetric-112-bit";
|
|
70
|
+
const KEY_MATERIAL_TYPES = new Set(["public-key", "private-key", "key"]);
|
|
71
|
+
const PLAUSIBLE_KEY_SIZES = Object.freeze({
|
|
72
|
+
rsa: [512, 65536],
|
|
73
|
+
ff: [512, 65536],
|
|
74
|
+
ec: [160, 571],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function buildLookup(member) {
|
|
78
|
+
const lookup = new Map();
|
|
79
|
+
for (const [className, definition] of Object.entries(pqcData.classes)) {
|
|
80
|
+
for (const [key, keyFamily] of Object.entries(definition[member] || {})) {
|
|
81
|
+
lookup.set(key, keyFamily ? { className, keyFamily } : { className });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return lookup;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const BY_FAMILY = buildLookup("families");
|
|
88
|
+
const BY_OID = buildLookup("oids");
|
|
89
|
+
const BY_NAME = buildLookup("names");
|
|
90
|
+
|
|
91
|
+
const isDigits = (text) =>
|
|
92
|
+
typeof text === "string" &&
|
|
93
|
+
text.length > 0 &&
|
|
94
|
+
[...text].every((ch) => ch >= "0" && ch <= "9");
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A name in lower case with only ASCII letters and digits kept, the form the
|
|
98
|
+
* `names` tables of data/pqc-migration.json use.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} name Algorithm or key name
|
|
101
|
+
* @returns {string} The compact name, empty for no name
|
|
102
|
+
*/
|
|
103
|
+
export function compactAlgorithmName(name) {
|
|
104
|
+
let compact = "";
|
|
105
|
+
for (const ch of `${name ?? ""}`.toLowerCase()) {
|
|
106
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9")) {
|
|
107
|
+
compact += ch;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return compact;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Is this name segment a digest name ("SHA", "sha512WithRSAEncryption")? */
|
|
114
|
+
function isHashToken(segment) {
|
|
115
|
+
const compact = compactAlgorithmName(segment);
|
|
116
|
+
return compact.startsWith("sha") || compact === "hash";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* "RSA-2048" is `{base: "RSA", size: 2048}`; other names have no size. A
|
|
121
|
+
* trailing number after a hash token is a digest size, so
|
|
122
|
+
* "RSASSA-PSS-SHA-512" has no size.
|
|
123
|
+
*/
|
|
124
|
+
function splitNamedKeySize(name) {
|
|
125
|
+
const text = `${name ?? ""}`;
|
|
126
|
+
const index = Math.max(text.lastIndexOf("-"), text.lastIndexOf("_"));
|
|
127
|
+
const tail = index > 0 ? text.slice(index + 1) : "";
|
|
128
|
+
if (!isDigits(tail) || tail.length > 5) {
|
|
129
|
+
return { base: text };
|
|
130
|
+
}
|
|
131
|
+
const head = text.slice(0, index);
|
|
132
|
+
const previous = head.slice(
|
|
133
|
+
Math.max(
|
|
134
|
+
head.lastIndexOf("-"),
|
|
135
|
+
head.lastIndexOf("_"),
|
|
136
|
+
head.lastIndexOf("/"),
|
|
137
|
+
) + 1,
|
|
138
|
+
);
|
|
139
|
+
if (isHashToken(previous)) {
|
|
140
|
+
return { base: text };
|
|
141
|
+
}
|
|
142
|
+
return { base: head, size: Number(tail) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const SIGNATURE_FAMILY_BY_TOKEN = Object.freeze([
|
|
146
|
+
["ecdsa", "ECDSA"],
|
|
147
|
+
["dsa", "DSA"],
|
|
148
|
+
["rsa", "RSASSA-PKCS1"],
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The signature family of a name that pairs a hash with DSA, RSA or ECDSA:
|
|
153
|
+
* "SHA256withDSA", "dsa-with-sha256", "RSA-SHA-512". ECDSA is checked before
|
|
154
|
+
* DSA, which it contains.
|
|
155
|
+
*/
|
|
156
|
+
function signatureFamilyOfHashName(name) {
|
|
157
|
+
const compact = compactAlgorithmName(name);
|
|
158
|
+
const tokens = `${name ?? ""}`
|
|
159
|
+
.toLowerCase()
|
|
160
|
+
.split(/[-_/ ]/u)
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
const hasHashToken = tokens.some(isHashToken);
|
|
163
|
+
for (const [token, family] of SIGNATURE_FAMILY_BY_TOKEN) {
|
|
164
|
+
if (
|
|
165
|
+
compact.includes(`with${token}`) ||
|
|
166
|
+
compact.startsWith(`${token}with`) ||
|
|
167
|
+
(hasHashToken && tokens.includes(token))
|
|
168
|
+
) {
|
|
169
|
+
return family;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function lookupCompactName(name) {
|
|
176
|
+
const compact = compactAlgorithmName(name);
|
|
177
|
+
return compact ? BY_NAME.get(compact) : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function lookupName(name) {
|
|
181
|
+
const { base } = splitNamedKeySize(name);
|
|
182
|
+
const byName = lookupCompactName(name) ?? lookupCompactName(base);
|
|
183
|
+
if (byName) {
|
|
184
|
+
return byName;
|
|
185
|
+
}
|
|
186
|
+
let family = resolveAlgorithmFamily(name) ?? resolveAlgorithmFamily(base);
|
|
187
|
+
if (!family || HASH_FAMILIES.has(family)) {
|
|
188
|
+
family = signatureFamilyOfHashName(name) ?? family;
|
|
189
|
+
}
|
|
190
|
+
return family ? BY_FAMILY.get(family) : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The first run of digits in a curve name ("secg/secp192r1" is 192). */
|
|
194
|
+
function curveBits(curve) {
|
|
195
|
+
const text = `${curve ?? ""}`;
|
|
196
|
+
let digits = "";
|
|
197
|
+
for (const ch of text.slice(text.lastIndexOf("/") + 1)) {
|
|
198
|
+
if (ch >= "0" && ch <= "9") {
|
|
199
|
+
digits += ch;
|
|
200
|
+
} else if (digits) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return digits && digits.length <= 4 ? Number(digits) : undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function plausibleKeySize(size, keyFamily) {
|
|
208
|
+
const range = PLAUSIBLE_KEY_SIZES[keyFamily];
|
|
209
|
+
return range && size >= range[0] && size <= range[1] ? size : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The key size a name states: a trailing number or an RFC 7919 group. */
|
|
213
|
+
function namedKeySize(name) {
|
|
214
|
+
return (
|
|
215
|
+
splitNamedKeySize(name).size ??
|
|
216
|
+
NAMED_GROUP_KEY_SIZES.get(compactAlgorithmName(name))
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function algorithmKeySize(component, keyFamily) {
|
|
221
|
+
const algorithm = component.cryptoProperties?.algorithmProperties || {};
|
|
222
|
+
const parameterSet = `${algorithm.parameterSetIdentifier ?? ""}`;
|
|
223
|
+
const candidates = [
|
|
224
|
+
namedKeySize(component.name),
|
|
225
|
+
isDigits(parameterSet) ? Number(parameterSet) : undefined,
|
|
226
|
+
keyFamily === "ec"
|
|
227
|
+
? curveBits(algorithm.ellipticCurve ?? algorithm.curve)
|
|
228
|
+
: undefined,
|
|
229
|
+
];
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
const size =
|
|
232
|
+
candidate === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: plausibleKeySize(candidate, keyFamily);
|
|
235
|
+
if (size !== undefined) {
|
|
236
|
+
return size;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Apply the IR 8547 minimum key size to a key-family classification. */
|
|
243
|
+
function withStrengthCheck(match, keySize) {
|
|
244
|
+
const minimumKeySize = pqcData.minimumKeySizes[match.keyFamily];
|
|
245
|
+
if (keySize !== undefined && minimumKeySize && keySize < minimumKeySize) {
|
|
246
|
+
return {
|
|
247
|
+
className: BELOW_MINIMUM_CLASS,
|
|
248
|
+
keyFamily: match.keyFamily,
|
|
249
|
+
keySize,
|
|
250
|
+
minimumKeySize,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { ...match };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The class of an algorithm asset before the minimum key size is applied, or
|
|
258
|
+
* undefined when it is quantum-safe or unclassified.
|
|
259
|
+
*/
|
|
260
|
+
function baseAlgorithmClass(component) {
|
|
261
|
+
const cryptoProperties = component.cryptoProperties;
|
|
262
|
+
const family = cryptoProperties.algorithmProperties?.algorithmFamily;
|
|
263
|
+
if (
|
|
264
|
+
UNCLASSIFIED_PRIMITIVES.has(cryptoProperties.algorithmProperties?.primitive)
|
|
265
|
+
) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (family && QUANTUM_SAFE_FAMILIES.has(family)) {
|
|
269
|
+
const refined =
|
|
270
|
+
BY_OID.get(cryptoProperties.oid) ?? lookupCompactName(component.name);
|
|
271
|
+
return refined?.className === SYMMETRIC_112_CLASS
|
|
272
|
+
? { ...refined }
|
|
273
|
+
: undefined;
|
|
274
|
+
}
|
|
275
|
+
const match =
|
|
276
|
+
(family && BY_FAMILY.get(family)) ||
|
|
277
|
+
BY_OID.get(cryptoProperties.oid) ||
|
|
278
|
+
lookupName(component.name);
|
|
279
|
+
return match ? { ...match } : undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function classifyAlgorithm(component) {
|
|
283
|
+
const match = baseAlgorithmClass(component);
|
|
284
|
+
if (!match?.keyFamily) {
|
|
285
|
+
return match;
|
|
286
|
+
}
|
|
287
|
+
return withStrengthCheck(match, algorithmKeySize(component, match.keyFamily));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function classifyKey(component, algorithmsByRef) {
|
|
291
|
+
const material = component.cryptoProperties.relatedCryptoMaterialProperties;
|
|
292
|
+
if (!KEY_MATERIAL_TYPES.has(material?.type)) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const algorithm = algorithmsByRef.get(material.algorithmRef);
|
|
296
|
+
const match = algorithm?.cryptoProperties
|
|
297
|
+
? baseAlgorithmClass(algorithm)
|
|
298
|
+
: lookupName(component.name);
|
|
299
|
+
const keySize =
|
|
300
|
+
typeof material.size === "number"
|
|
301
|
+
? material.size
|
|
302
|
+
: namedKeySize(component.name);
|
|
303
|
+
if (!match?.keyFamily || keySize === undefined) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const checked = withStrengthCheck(match, keySize);
|
|
307
|
+
return checked.className === BELOW_MINIMUM_CLASS ? checked : undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Classify one cryptographic-asset component. Algorithm assets get their
|
|
312
|
+
* IR 8547 class; key assets are classified only when their size is below the
|
|
313
|
+
* IR 8547 minimum (the algorithm asset carries the quantum risk).
|
|
314
|
+
*
|
|
315
|
+
* @param {object} component A component with cryptoProperties
|
|
316
|
+
* @param {Map<string, object>} [algorithmsByRef] Components by bom-ref, to
|
|
317
|
+
* resolve a key's `algorithmRef`
|
|
318
|
+
* @returns {{className: string, keyFamily?: string, keySize?: number, minimumKeySize?: number}|undefined}
|
|
319
|
+
* The class, or undefined when the asset is quantum-safe or unclassified
|
|
320
|
+
*/
|
|
321
|
+
export function classifyPqcCryptoAsset(component, algorithmsByRef = new Map()) {
|
|
322
|
+
const assetType = component?.cryptoProperties?.assetType;
|
|
323
|
+
if (assetType === "algorithm") {
|
|
324
|
+
return classifyAlgorithm(component);
|
|
325
|
+
}
|
|
326
|
+
if (assetType === "related-crypto-material") {
|
|
327
|
+
return classifyKey(component, algorithmsByRef);
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function flattenComponents(components, into = []) {
|
|
333
|
+
for (const component of Array.isArray(components) ? components : []) {
|
|
334
|
+
if (component && typeof component === "object") {
|
|
335
|
+
into.push(component);
|
|
336
|
+
flattenComponents(component.components, into);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return into;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build the risks.risks[] entries for the quantum-vulnerable and weak
|
|
344
|
+
* cryptographic assets of a 1.x-shaped internal BOM: one per asset with a
|
|
345
|
+
* bom-ref (an asset without one could not be referenced from `affects`), in
|
|
346
|
+
* component order, unique by refId.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} internalBom The internal model
|
|
349
|
+
* @returns {object[]} Risk entries shaped for the draft's
|
|
350
|
+
* #/$defs/cyclonedx-risk-2.0/$defs/risk; empty when nothing is at risk
|
|
351
|
+
*/
|
|
352
|
+
export function collectPqcRisks(internalBom) {
|
|
353
|
+
const components = flattenComponents(internalBom?.components);
|
|
354
|
+
const byRef = new Map();
|
|
355
|
+
for (const component of components) {
|
|
356
|
+
if (typeof component["bom-ref"] === "string") {
|
|
357
|
+
byRef.set(component["bom-ref"], component);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const risks = [];
|
|
361
|
+
const seen = new Set();
|
|
362
|
+
for (const component of components) {
|
|
363
|
+
const bomRef = component["bom-ref"];
|
|
364
|
+
if (typeof bomRef !== "string" || !bomRef.length) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const classification = classifyPqcCryptoAsset(component, byRef);
|
|
368
|
+
const refId = `${PQC_RISK_REF_ID_PREFIX}${bomRef}`;
|
|
369
|
+
if (classification && !seen.has(refId)) {
|
|
370
|
+
seen.add(refId);
|
|
371
|
+
risks.push(buildRiskEntry(component, classification, refId));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return risks;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const fillTemplate = (template, values) =>
|
|
378
|
+
Object.entries(values).reduce(
|
|
379
|
+
(text, [key, value]) => text.split(`{${key}}`).join(`${value}`),
|
|
380
|
+
template,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const PROPERTY_FIELDS = Object.freeze([
|
|
384
|
+
["deprecatedAfter", "cdx:crypto:pqc:deprecatedAfter"],
|
|
385
|
+
["disallowedAfter", "cdx:crypto:pqc:disallowedAfter"],
|
|
386
|
+
["disallowedIn", "cdx:crypto:pqc:disallowedIn"],
|
|
387
|
+
["transition", "cdx:crypto:pqc:transition"],
|
|
388
|
+
["source", "cdx:crypto:pqc:source"],
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
function buildRiskEntry(component, classification, refId) {
|
|
392
|
+
const definition = pqcData.classes[classification.className];
|
|
393
|
+
const label =
|
|
394
|
+
typeof component.name === "string" && component.name.length
|
|
395
|
+
? component.name
|
|
396
|
+
: component.cryptoProperties?.algorithmProperties?.algorithmFamily ||
|
|
397
|
+
"algorithm";
|
|
398
|
+
const properties = [
|
|
399
|
+
{ name: "cdx:crypto:pqc:class", value: classification.className },
|
|
400
|
+
];
|
|
401
|
+
for (const [field, name] of PROPERTY_FIELDS) {
|
|
402
|
+
if (definition[field]) {
|
|
403
|
+
properties.push({ name, value: definition[field] });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
refId,
|
|
408
|
+
name: `${definition.riskName}: ${label}`,
|
|
409
|
+
statement: definition.statement,
|
|
410
|
+
domains: [{ type: "security" }],
|
|
411
|
+
affects: [component["bom-ref"]],
|
|
412
|
+
inherentRisk: {
|
|
413
|
+
likelihood: { level: definition.likelihood },
|
|
414
|
+
impacts: [{ level: definition.impact }],
|
|
415
|
+
rationale: fillTemplate(definition.rationale, {
|
|
416
|
+
algorithm: label,
|
|
417
|
+
size: classification.keySize,
|
|
418
|
+
minimum: classification.minimumKeySize,
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
properties,
|
|
422
|
+
externalReferences: [
|
|
423
|
+
{ type: "documentation", url: definition.url || pqcData.source.url },
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
@@ -3,6 +3,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { safeExistsSync } from "../core/fs.js";
|
|
5
5
|
import { parseToml } from "../core/toml.js";
|
|
6
|
+
import { formulationSourcePath } from "./ecosystemFormulationParsers.js";
|
|
6
7
|
|
|
7
8
|
// Keep this list conservative and high-signal: it is meant to surface common
|
|
8
9
|
// Rust native build helpers for audit triage, not to exhaustively model every
|
|
@@ -53,17 +54,18 @@ function appendProperty(properties, name, value) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
function createFormulationComponent(name, filePath, toolName) {
|
|
57
|
+
function createFormulationComponent(name, filePath, toolName, rootDir) {
|
|
58
|
+
const sourcePath = formulationSourcePath(filePath, rootDir);
|
|
57
59
|
return {
|
|
58
60
|
type: "application",
|
|
59
61
|
name,
|
|
60
62
|
version: "config",
|
|
61
|
-
"bom-ref": `urn:cdxgen:formulation:${toolName}:${
|
|
62
|
-
properties: [{ name: "internal:SrcFile", value:
|
|
63
|
+
"bom-ref": `urn:cdxgen:formulation:${toolName}:${sourcePath}`,
|
|
64
|
+
properties: [{ name: "internal:SrcFile", value: sourcePath }],
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function parseCargoManifest(filePath) {
|
|
68
|
+
function parseCargoManifest(filePath, rootDir) {
|
|
67
69
|
let cargoData;
|
|
68
70
|
try {
|
|
69
71
|
cargoData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
|
|
@@ -75,7 +77,12 @@ function parseCargoManifest(filePath) {
|
|
|
75
77
|
}
|
|
76
78
|
const packageName =
|
|
77
79
|
cargoData?.package?.name || basename(dirname(filePath)) || "cargo-project";
|
|
78
|
-
const component = createFormulationComponent(
|
|
80
|
+
const component = createFormulationComponent(
|
|
81
|
+
packageName,
|
|
82
|
+
filePath,
|
|
83
|
+
"cargo",
|
|
84
|
+
rootDir,
|
|
85
|
+
);
|
|
79
86
|
const properties = component.properties;
|
|
80
87
|
const buildDependencies = cargoData?.["build-dependencies"];
|
|
81
88
|
const devDependencies = cargoData?.["dev-dependencies"];
|
|
@@ -248,7 +255,7 @@ function parseCargoManifest(filePath) {
|
|
|
248
255
|
return component;
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
function parsePyProject(filePath) {
|
|
258
|
+
function parsePyProject(filePath, rootDir) {
|
|
252
259
|
let pyprojectData;
|
|
253
260
|
try {
|
|
254
261
|
pyprojectData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
|
|
@@ -269,6 +276,7 @@ function parsePyProject(filePath) {
|
|
|
269
276
|
"maturin-project",
|
|
270
277
|
filePath,
|
|
271
278
|
"maturin",
|
|
279
|
+
rootDir,
|
|
272
280
|
);
|
|
273
281
|
const properties = component.properties;
|
|
274
282
|
appendProperty(properties, "cdx:rust:buildTool", "maturin");
|
|
@@ -311,23 +319,26 @@ function parsePyProject(filePath) {
|
|
|
311
319
|
* formulation components carrying build-tool, build-script capability, native
|
|
312
320
|
* build dependency, and maturin metadata properties.
|
|
313
321
|
*
|
|
314
|
-
*
|
|
322
|
+
* Components record the manifest path relative to context.rootDir, the
|
|
323
|
+
* scanned project root.
|
|
324
|
+
*
|
|
325
|
+
* @type {{id: string, patterns: string[], parse(files: string[], options?: Object, context?: {rootDir?: string}): {components: Object[]}}}
|
|
315
326
|
*/
|
|
316
327
|
export const rustFormulationParser = {
|
|
317
328
|
id: "rust-build",
|
|
318
329
|
patterns: ["**/Cargo.toml", "**/pyproject.toml"],
|
|
319
|
-
parse(files) {
|
|
330
|
+
parse(files, _options, context = {}) {
|
|
320
331
|
const components = [];
|
|
321
332
|
for (const filePath of files || []) {
|
|
322
333
|
if (filePath.endsWith("Cargo.toml")) {
|
|
323
|
-
const cargoComponent = parseCargoManifest(filePath);
|
|
334
|
+
const cargoComponent = parseCargoManifest(filePath, context.rootDir);
|
|
324
335
|
if (cargoComponent) {
|
|
325
336
|
components.push(cargoComponent);
|
|
326
337
|
}
|
|
327
338
|
continue;
|
|
328
339
|
}
|
|
329
340
|
if (filePath.endsWith("pyproject.toml")) {
|
|
330
|
-
const pyprojectComponent = parsePyProject(filePath);
|
|
341
|
+
const pyprojectComponent = parsePyProject(filePath, context.rootDir);
|
|
331
342
|
if (pyprojectComponent) {
|
|
332
343
|
components.push(pyprojectComponent);
|
|
333
344
|
}
|
package/lib/inventory/source.js
CHANGED
|
@@ -106,9 +106,48 @@ function inferGitOperation(args = []) {
|
|
|
106
106
|
return "command";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
let gitRedirectRefusalReported = false;
|
|
110
|
+
|
|
111
|
+
registerRunReset("source:git-redirect-refusal", () => {
|
|
112
|
+
gitRedirectRefusalReported = false;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Determine whether git must refuse HTTP redirects. A git host allowlist is
|
|
117
|
+
* checked against the URL cdxgen is given, and git would otherwise follow
|
|
118
|
+
* the server's redirect on its first request to a host the allowlist never
|
|
119
|
+
* saw. Secure mode refuses redirects in the HTTP client too.
|
|
120
|
+
*
|
|
121
|
+
* @returns {boolean} true when a git host allowlist or secure mode applies
|
|
122
|
+
*/
|
|
123
|
+
export function gitRedirectsRefused() {
|
|
124
|
+
return isSecureMode || getAllowedHosts().length > 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Determine whether a failed git command stopped at an HTTP redirect.
|
|
129
|
+
*
|
|
130
|
+
* @param {string|undefined} stderr git error output
|
|
131
|
+
* @returns {boolean} true when git reports a 3xx response
|
|
132
|
+
*/
|
|
133
|
+
function isGitRedirectRefusal(stderr) {
|
|
134
|
+
if (typeof stderr !== "string") {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const marker = "The requested URL returned error: 30";
|
|
138
|
+
const index = stderr.indexOf(marker);
|
|
139
|
+
return index !== -1 && "12378".includes(stderr[index + marker.length]);
|
|
140
|
+
}
|
|
141
|
+
|
|
109
142
|
/**
|
|
110
143
|
* Execute git with hardened defaults.
|
|
111
144
|
*
|
|
145
|
+
* Every call runs with command-scope settings that keep the repository from
|
|
146
|
+
* running fsmonitor hooks or being opened as a bare repository. While a git
|
|
147
|
+
* host allowlist or secure mode applies, git also refuses HTTP redirects, so
|
|
148
|
+
* an allowed host cannot send a clone or ls-remote to a host the allowlist
|
|
149
|
+
* does not name.
|
|
150
|
+
*
|
|
112
151
|
* @param {string[]} args git arguments
|
|
113
152
|
* @param {Object} options command options
|
|
114
153
|
* @param {string|undefined} options.cwd working directory
|
|
@@ -120,15 +159,23 @@ function inferGitOperation(args = []) {
|
|
|
120
159
|
export function hardenedGitCommand(args, options = {}) {
|
|
121
160
|
const gitOperation = inferGitOperation(args);
|
|
122
161
|
const gitAllowProtocol = getGitAllowProtocol();
|
|
162
|
+
const refuseRedirects = gitRedirectsRefused();
|
|
163
|
+
const commandConfig = [
|
|
164
|
+
["core.fsmonitor", "false"],
|
|
165
|
+
["safe.bareRepository", "explicit"],
|
|
166
|
+
];
|
|
167
|
+
if (refuseRedirects) {
|
|
168
|
+
commandConfig.push(["http.followRedirects", "false"]);
|
|
169
|
+
}
|
|
123
170
|
const envConfigs = {
|
|
124
|
-
GIT_CONFIG_COUNT:
|
|
125
|
-
GIT_CONFIG_KEY_0: "core.fsmonitor",
|
|
126
|
-
GIT_CONFIG_VALUE_0: "false",
|
|
127
|
-
GIT_CONFIG_KEY_1: "safe.bareRepository",
|
|
128
|
-
GIT_CONFIG_VALUE_1: "explicit",
|
|
171
|
+
GIT_CONFIG_COUNT: String(commandConfig.length),
|
|
129
172
|
GIT_NO_LAZY_FETCH: "1",
|
|
130
173
|
GIT_TERMINAL_PROMPT: "0",
|
|
131
174
|
};
|
|
175
|
+
commandConfig.forEach(([key, value], index) => {
|
|
176
|
+
envConfigs[`GIT_CONFIG_KEY_${index}`] = key;
|
|
177
|
+
envConfigs[`GIT_CONFIG_VALUE_${index}`] = value;
|
|
178
|
+
});
|
|
132
179
|
if (options.noRepository) {
|
|
133
180
|
// ls-remote works without a repository. Pointing GIT_DIR at a path that
|
|
134
181
|
// does not exist stops git from discovering one in the working directory.
|
|
@@ -147,7 +194,7 @@ export function hardenedGitCommand(args, options = {}) {
|
|
|
147
194
|
...envConfigs,
|
|
148
195
|
GIT_ALLOW_PROTOCOL: gitAllowProtocol,
|
|
149
196
|
};
|
|
150
|
-
|
|
197
|
+
const result = safeSpawnSync("git", args, {
|
|
151
198
|
cdxgenActivity: {
|
|
152
199
|
blockedReason: `Dry run mode blocks git ${gitOperation} operations.`,
|
|
153
200
|
gitOperation,
|
|
@@ -160,6 +207,18 @@ export function hardenedGitCommand(args, options = {}) {
|
|
|
160
207
|
cwd: options.cwd,
|
|
161
208
|
env,
|
|
162
209
|
});
|
|
210
|
+
if (
|
|
211
|
+
refuseRedirects &&
|
|
212
|
+
!gitRedirectRefusalReported &&
|
|
213
|
+
result?.status !== 0 &&
|
|
214
|
+
isGitRedirectRefusal(result?.stderr)
|
|
215
|
+
) {
|
|
216
|
+
gitRedirectRefusalReported = true;
|
|
217
|
+
console.warn(
|
|
218
|
+
"A Git server answered with a redirect, which cdxgen does not follow while CDXGEN_GIT_ALLOWED_HOSTS, CDXGEN_SERVER_ALLOWED_HOSTS or secure mode is in effect. Use the repository's current URL instead.",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
function normalizeTagName(tagName) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
normalizeHuggingFaceReference,
|
|
16
16
|
repositoryUrlForHuggingFaceAssetType,
|
|
17
17
|
toHuggingFacePurl,
|
|
18
|
+
toHuggingFaceSupplier,
|
|
18
19
|
} from "./huggingfaceUtils.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -278,7 +279,7 @@ export function repoIdFromFixtureDirectory(filePath) {
|
|
|
278
279
|
*
|
|
279
280
|
* @param {string} modelRef model, dataset, or space reference
|
|
280
281
|
* @param {{ includeDatasetPurl?: boolean }} [options={}] pedigree reference options
|
|
281
|
-
* @returns {{ "bom-ref": string, group: string, name: string, purl?: string, type: string }|undefined} component reference
|
|
282
|
+
* @returns {{ "bom-ref": string, group: string, name: string, purl?: string, supplier?: { name: string, url?: string[] }, type: string }|undefined} component reference
|
|
282
283
|
*/
|
|
283
284
|
export function createHuggingFaceComponentReference(modelRef, _options = {}) {
|
|
284
285
|
const reference = normalizeHuggingFaceReference(modelRef);
|
|
@@ -302,6 +303,9 @@ export function createHuggingFaceComponentReference(modelRef, _options = {}) {
|
|
|
302
303
|
group,
|
|
303
304
|
name,
|
|
304
305
|
purl,
|
|
306
|
+
// The namespace doubles as the producer for references, so pedigree
|
|
307
|
+
// ancestors and Space-linked models carry the producer element too.
|
|
308
|
+
supplier: toHuggingFaceSupplier(group),
|
|
305
309
|
};
|
|
306
310
|
}
|
|
307
311
|
|
|
@@ -329,6 +333,7 @@ export function createHuggingFaceComponentReference(modelRef, _options = {}) {
|
|
|
329
333
|
* properties?: Array<object>,
|
|
330
334
|
* purl?: string,
|
|
331
335
|
* scope?: string,
|
|
336
|
+
* supplier?: { name: string, url?: string[] },
|
|
332
337
|
* tags?: string[],
|
|
333
338
|
* type: "data",
|
|
334
339
|
* },
|
|
@@ -369,6 +374,9 @@ export function createHuggingFaceDatasetReference(dataset, options = {}) {
|
|
|
369
374
|
scope: options.componentScope,
|
|
370
375
|
description: normalizedDataset.description,
|
|
371
376
|
externalReferences,
|
|
377
|
+
// The dataset's namespace is the creator identifier the provenance
|
|
378
|
+
// element asks for, when the reference carries one.
|
|
379
|
+
supplier: toHuggingFaceSupplier(normalizedDataset.group),
|
|
372
380
|
data: [toDatasetComponentData(normalizedDataset)],
|
|
373
381
|
properties: options.componentProperties,
|
|
374
382
|
tags: options.componentTags,
|
|
@@ -195,6 +195,24 @@ export function toHuggingFaceAssetUrl(assetType, repoId) {
|
|
|
195
195
|
return assetPath ? `${HF_BASE_URL}/${assetPath}` : undefined;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Build the supplier entity for a Hugging Face namespace: the namespace as
|
|
200
|
+
* the name and its organisation or user page as the url. The url is left
|
|
201
|
+
* out when the namespace has characters a Hub account name cannot have.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} namespace Hugging Face organisation or user name
|
|
204
|
+
* @returns {{ name: string, url?: string[] }|undefined} supplier entity
|
|
205
|
+
*/
|
|
206
|
+
export function toHuggingFaceSupplier(namespace) {
|
|
207
|
+
const name = typeof namespace === "string" ? namespace.trim() : "";
|
|
208
|
+
if (!name) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
|
|
212
|
+
? { name, url: [`${HF_BASE_URL}/${name}`] }
|
|
213
|
+
: { name };
|
|
214
|
+
}
|
|
215
|
+
|
|
198
216
|
/**
|
|
199
217
|
* Convert a Hugging Face repo reference to a package URL.
|
|
200
218
|
*
|