@cdxgen/cdxgen 13.0.1 → 13.1.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 +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +125 -4
- package/bin/evinse.js +1 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -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/cli/bomAssembly.js +20 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- 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/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +206 -18
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- 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 +227 -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 +115 -2
- 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/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- 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/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 +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- 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.map +1 -1
- package/types/lib/cli/nativeBom.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 +22 -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/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- 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/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- 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/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -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/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- 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/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 +57 -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 +2 -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
|
@@ -331,6 +331,53 @@ export function get_python_command_from_env(env) {
|
|
|
331
331
|
return fallbackCmd;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Is this import satisfied by a module that lives in the project itself?
|
|
336
|
+
*
|
|
337
|
+
* `atom parsedeps` reports every non-stdlib import it sees, including imports of the
|
|
338
|
+
* project's own packages. Attributing those to a distribution invents dependencies:
|
|
339
|
+
* `data/pypi-pkg-aliases.json` maps the module `app` to `zope2`, `test` to `pytabix`,
|
|
340
|
+
* `models` to `asposestorage` and `src` to `auto-mix-prep`, so any project with a
|
|
341
|
+
* local package of that name acquires an obscure component it never depended on -
|
|
342
|
+
* along with every namespace and tag that component carries downstream. Filtering by
|
|
343
|
+
* name would need a denylist of every plausible first-party name; asking the
|
|
344
|
+
* filesystem whether the module is ours answers it exactly.
|
|
345
|
+
*
|
|
346
|
+
* Checks the project root and a `src/` layout for a module file, a package directory,
|
|
347
|
+
* and a PEP 420 namespace package directory.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} src Path to the project being analysed
|
|
350
|
+
* @param {string} name Imported top-level module name
|
|
351
|
+
* @returns {boolean} `true` if the project supplies this module itself
|
|
352
|
+
*/
|
|
353
|
+
export function isFirstPartyModule(src, name) {
|
|
354
|
+
if (!src || !name || name.includes("/") || name.includes("\\")) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
for (const root of [src, join(src, "src")]) {
|
|
358
|
+
if (
|
|
359
|
+
safeExistsSync(join(root, `${name}.py`)) ||
|
|
360
|
+
safeExistsSync(join(root, name, "__init__.py")) ||
|
|
361
|
+
safeExistsSync(join(root, name, "__main__.py"))
|
|
362
|
+
) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
// PEP 420 namespace package: a directory of the right name holding python
|
|
366
|
+
// sources but no __init__.py.
|
|
367
|
+
const candidate = join(root, name);
|
|
368
|
+
if (safeExistsSync(candidate)) {
|
|
369
|
+
try {
|
|
370
|
+
if (readdirSync(candidate).some((f) => f.endsWith(".py"))) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
// not a readable directory - treat as not first-party
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
334
381
|
/**
|
|
335
382
|
* Method to find python modules by parsing the imports and then checking with PyPI to obtain the latest version
|
|
336
383
|
*
|
|
@@ -362,7 +409,8 @@ export async function getPyModules(src, epkgList, options) {
|
|
|
362
409
|
(x) =>
|
|
363
410
|
!pyDefaultModules.has(x.name.toLowerCase()) &&
|
|
364
411
|
!x.name.startsWith("_") &&
|
|
365
|
-
!x.name.startsWith(".")
|
|
412
|
+
!x.name.startsWith(".") &&
|
|
413
|
+
!isFirstPartyModule(src, x.name),
|
|
366
414
|
);
|
|
367
415
|
let pkgList = modList.map((p) => {
|
|
368
416
|
const apkg = {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
prefetchEnabled,
|
|
20
20
|
prefetchedResponse,
|
|
21
21
|
prefetchJson,
|
|
22
|
+
recordPolicyDegradationFromError,
|
|
22
23
|
} from "../inventory/fetchBatch.js";
|
|
23
24
|
|
|
24
25
|
// FIXME. This has to get removed, once we improve the module detection one-liner.
|
|
@@ -780,6 +781,7 @@ export async function getRubyGemsMetadata(pkgList) {
|
|
|
780
781
|
continue;
|
|
781
782
|
}
|
|
782
783
|
} catch (err) {
|
|
784
|
+
recordPolicyDegradationFromError(err);
|
|
783
785
|
if (DEBUG_MODE) {
|
|
784
786
|
console.error(name, err);
|
|
785
787
|
}
|
|
@@ -832,6 +834,7 @@ async function enrichGemFromVersionEndpoint(
|
|
|
832
834
|
}
|
|
833
835
|
applyGemMetadata(p, body);
|
|
834
836
|
} catch (err) {
|
|
837
|
+
recordPolicyDegradationFromError(err);
|
|
835
838
|
if (DEBUG_MODE) {
|
|
836
839
|
console.error(p, err);
|
|
837
840
|
}
|
|
@@ -1241,6 +1244,16 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
|
|
|
1241
1244
|
pkg.externalReferences.push({ type: refType, url: value, comment: key });
|
|
1242
1245
|
}
|
|
1243
1246
|
}
|
|
1247
|
+
// `s.homepage = "..."` is the project website. Keep it as an external
|
|
1248
|
+
// reference; the transient `homepage` key is a plain string that never
|
|
1249
|
+
// reaches the BOM because component conversion expects the `{ url }` shape.
|
|
1250
|
+
if (pkg.homepage) {
|
|
1251
|
+
pkg.externalReferences = pkg.externalReferences || [];
|
|
1252
|
+
if (!pkg.externalReferences.some((ref) => ref.url === pkg.homepage)) {
|
|
1253
|
+
pkg.externalReferences.push({ type: "website", url: pkg.homepage });
|
|
1254
|
+
}
|
|
1255
|
+
delete pkg.homepage;
|
|
1256
|
+
}
|
|
1244
1257
|
if (pkg.name) {
|
|
1245
1258
|
const purlString = toGemPurl(pkg.name, pkg.version, gemPlatform);
|
|
1246
1259
|
pkg.purl = purlString;
|
package/lib/evinser/evinser.js
CHANGED
|
@@ -42,12 +42,15 @@ import {
|
|
|
42
42
|
import { collectMvnDependencies } from "../inventory/deps.js";
|
|
43
43
|
import { mergeServices } from "../inventory/depsUtils.js";
|
|
44
44
|
import {
|
|
45
|
+
collectDosaiAiComponents,
|
|
45
46
|
collectDosaiDataFlowFrames,
|
|
46
47
|
collectDosaiPurlEvidence,
|
|
48
|
+
collectDosaiServiceComponents,
|
|
47
49
|
collectDosaiServicesFromMethods,
|
|
48
50
|
createDosaiDataFlowSlice,
|
|
49
51
|
createDosaiMethodsSlice,
|
|
50
52
|
isDosaiDotnetLanguage,
|
|
53
|
+
normalizeDosaiServiceMap,
|
|
51
54
|
persistDosaiSemanticsReport,
|
|
52
55
|
} from "../inventory/dosai.js";
|
|
53
56
|
import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
|
|
@@ -662,6 +665,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
662
665
|
const bomJson = JSON.parse(fs.readFileSync(bomFile, "utf8"));
|
|
663
666
|
const components = bomJson.components || [];
|
|
664
667
|
let cryptoComponents = [];
|
|
668
|
+
let aiComponents = [];
|
|
665
669
|
let cryptoGeneratePurls = {};
|
|
666
670
|
let openapiSpecFile;
|
|
667
671
|
// Load any existing purl-location information from the sbom.
|
|
@@ -750,6 +754,13 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
750
754
|
purlLocationMap[purl].add(location);
|
|
751
755
|
}
|
|
752
756
|
}
|
|
757
|
+
// Rich Services[] inventory first (bom-refs, trust zones, data); endpoint
|
|
758
|
+
// derivation fills any services the providers did not emit.
|
|
759
|
+
// AI components must be carried out of analyzeProject explicitly. Mutating the local
|
|
760
|
+
// `components` array here had no effect: the return below omits it and createEvinseFile
|
|
761
|
+
// re-reads the BOM from disk, so the whole AI-BOM contribution was silently discarded.
|
|
762
|
+
collectDosaiAiComponents(usageSlice, aiComponents);
|
|
763
|
+
servicesMap = collectDosaiServiceComponents(usageSlice, servicesMap);
|
|
753
764
|
servicesMap = collectDosaiServicesFromMethods(usageSlice, servicesMap);
|
|
754
765
|
userDefinedTypesMap = {};
|
|
755
766
|
}
|
|
@@ -802,6 +813,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
802
813
|
tempDirOwned: retMap?.tempDirOwned,
|
|
803
814
|
userDefinedTypesMap,
|
|
804
815
|
cryptoComponents,
|
|
816
|
+
aiComponents,
|
|
805
817
|
cryptoGeneratePurls,
|
|
806
818
|
};
|
|
807
819
|
}
|
|
@@ -832,6 +844,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
832
844
|
const retMap = collectReachableFrames(language, reachablesSlice);
|
|
833
845
|
dataFlowFrames = retMap.dataFlowFrames;
|
|
834
846
|
cryptoComponents = retMap.cryptoComponents;
|
|
847
|
+
aiComponents = retMap.aiComponents || [];
|
|
835
848
|
cryptoGeneratePurls = retMap.cryptoGeneratePurls;
|
|
836
849
|
}
|
|
837
850
|
// Reuse existing usages slices
|
|
@@ -960,6 +973,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
960
973
|
tempDirOwned: retMap?.tempDirOwned,
|
|
961
974
|
userDefinedTypesMap,
|
|
962
975
|
cryptoComponents,
|
|
976
|
+
aiComponents,
|
|
963
977
|
cryptoGeneratePurls,
|
|
964
978
|
openapiSpecFile,
|
|
965
979
|
};
|
|
@@ -1804,7 +1818,7 @@ export function extractEndpoints(language, code) {
|
|
|
1804
1818
|
* @param {Object} options Command line options
|
|
1805
1819
|
* @returns
|
|
1806
1820
|
*/
|
|
1807
|
-
export function createEvinseFile(sliceArtefacts, options) {
|
|
1821
|
+
export async function createEvinseFile(sliceArtefacts, options) {
|
|
1808
1822
|
const {
|
|
1809
1823
|
tempDir,
|
|
1810
1824
|
tempDirOwned,
|
|
@@ -1817,6 +1831,7 @@ export function createEvinseFile(sliceArtefacts, options) {
|
|
|
1817
1831
|
componentPropertiesMap = {},
|
|
1818
1832
|
metadataProperties = [],
|
|
1819
1833
|
cryptoComponents,
|
|
1834
|
+
aiComponents,
|
|
1820
1835
|
cryptoGeneratePurls,
|
|
1821
1836
|
} = sliceArtefacts;
|
|
1822
1837
|
const bomFile = options.input;
|
|
@@ -1873,16 +1888,12 @@ export function createEvinseFile(sliceArtefacts, options) {
|
|
|
1873
1888
|
}
|
|
1874
1889
|
} // for
|
|
1875
1890
|
if (servicesMap && Object.keys(servicesMap).length) {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
"x-trust-boundary": servicesMap[serviceName].xTrustBoundary,
|
|
1883
|
-
properties: servicesMap[serviceName].properties,
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1891
|
+
// normalizeDosaiServiceMap is the single mapping from the internal service
|
|
1892
|
+
// definitions to CycloneDX services[]. The OpenAPI/Java/Python detectors set
|
|
1893
|
+
// the camelCase `xTrustBoundary` while dosai's provider inventory (schema
|
|
1894
|
+
// 4.0.0) carries stable bom-refs, trust zones, data classifications, and
|
|
1895
|
+
// evidence occurrences; both shapes flow through the same normalizer.
|
|
1896
|
+
const services = normalizeDosaiServiceMap(servicesMap);
|
|
1886
1897
|
// Add to existing services while preserving CycloneDX uniqueItems validity
|
|
1887
1898
|
bomJson.services = mergeServices(bomJson.services || [], services);
|
|
1888
1899
|
servicesPresent = true;
|
|
@@ -1891,6 +1902,10 @@ export function createEvinseFile(sliceArtefacts, options) {
|
|
|
1891
1902
|
if (cryptoComponents?.length) {
|
|
1892
1903
|
bomJson.components = bomJson.components.concat(cryptoComponents);
|
|
1893
1904
|
}
|
|
1905
|
+
// Add the AI components (machine-learning-model, dataset) discovered by dosai
|
|
1906
|
+
if (aiComponents?.length) {
|
|
1907
|
+
bomJson.components = bomJson.components.concat(aiComponents);
|
|
1908
|
+
}
|
|
1894
1909
|
// Fix the dependencies section with provides information
|
|
1895
1910
|
if (
|
|
1896
1911
|
cryptoGeneratePurls &&
|
|
@@ -1943,7 +1958,10 @@ export function createEvinseFile(sliceArtefacts, options) {
|
|
|
1943
1958
|
// Redo post-processing with evinse data. `executeOsQuery` has to be injected
|
|
1944
1959
|
// because postgen no longer imports it directly (stages is below managers);
|
|
1945
1960
|
// without it the crypto formulation path receives `undefined` and throws.
|
|
1946
|
-
const bomNSData = postProcess(
|
|
1961
|
+
const bomNSData = await postProcess(
|
|
1962
|
+
{ bomJson },
|
|
1963
|
+
{ ...options, executeOsQuery },
|
|
1964
|
+
);
|
|
1947
1965
|
safeWriteSync(
|
|
1948
1966
|
evinseOutFile,
|
|
1949
1967
|
JSON.stringify(bomNSData.bomJson, null, options.jsonPretty ? 2 : null),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
import { hasDangerousUnicode } from "../../core/activity.js";
|
|
4
5
|
|
|
@@ -44,26 +45,23 @@ export function getDependencyTrackBomUrl(serverUrl) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
* Build the
|
|
48
|
+
* Build the form fields for a Dependency-Track BOM submission.
|
|
49
|
+
*
|
|
50
|
+
* Field names follow the `multipart/form-data` variant of `/api/v1/bom`, which differs
|
|
51
|
+
* from the JSON variant: the JSON body calls the flag `isLatestProjectVersion` and takes
|
|
52
|
+
* `projectTags` as an array of objects, while the form takes `isLatest` and a
|
|
53
|
+
* comma-separated tag list.
|
|
48
54
|
*
|
|
49
55
|
* @param {Object} args CLI/server arguments
|
|
50
|
-
* @
|
|
51
|
-
* @returns {Object | undefined} payload object if project coordinates are valid
|
|
56
|
+
* @returns {Object | undefined} field map if the project coordinates are valid
|
|
52
57
|
*/
|
|
53
|
-
export function buildDependencyTrackBomPayload(args
|
|
54
|
-
let encodedBomContents = Buffer.from(JSON.stringify(bomContents)).toString(
|
|
55
|
-
"base64",
|
|
56
|
-
);
|
|
57
|
-
if (encodedBomContents.startsWith("77u/")) {
|
|
58
|
-
encodedBomContents = encodedBomContents.substring(4);
|
|
59
|
-
}
|
|
58
|
+
export function buildDependencyTrackBomPayload(args) {
|
|
60
59
|
const autoCreate =
|
|
61
60
|
typeof args.autoCreate === "boolean"
|
|
62
61
|
? args.autoCreate
|
|
63
62
|
: args.autoCreate !== "false";
|
|
64
63
|
const bomPayload = {
|
|
65
64
|
autoCreate: String(autoCreate),
|
|
66
|
-
bom: encodedBomContents,
|
|
67
65
|
};
|
|
68
66
|
if (
|
|
69
67
|
typeof args.projectId !== "undefined" ||
|
|
@@ -103,15 +101,84 @@ export function buildDependencyTrackBomPayload(args, bomContents) {
|
|
|
103
101
|
args.isLatest === "true" ||
|
|
104
102
|
args.isLatest === "false"
|
|
105
103
|
) {
|
|
106
|
-
bomPayload.isLatest =
|
|
104
|
+
bomPayload.isLatest = String(
|
|
107
105
|
typeof args.isLatest === "boolean"
|
|
108
106
|
? args.isLatest
|
|
109
|
-
: args.isLatest === "true"
|
|
107
|
+
: args.isLatest === "true",
|
|
108
|
+
);
|
|
110
109
|
}
|
|
111
110
|
if (typeof args.projectTag !== "undefined") {
|
|
111
|
+
// The server splits this field on commas, so a tag cannot itself contain one.
|
|
112
112
|
bomPayload.projectTags = (
|
|
113
113
|
Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
|
|
114
|
-
).
|
|
114
|
+
).join(",");
|
|
115
115
|
}
|
|
116
116
|
return bomPayload;
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Encode text fields and a single file part as a `multipart/form-data` body.
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} fields Text fields keyed by field name
|
|
123
|
+
* @param {Object} file File part
|
|
124
|
+
* @param {string} file.name Field name of the file part
|
|
125
|
+
* @param {string} file.filename File name reported to the server
|
|
126
|
+
* @param {string} file.contentType Content type of the file part
|
|
127
|
+
* @param {Buffer} file.content File content
|
|
128
|
+
* @returns {{ body: Buffer, contentType: string }} encoded body and its content type
|
|
129
|
+
*/
|
|
130
|
+
export function encodeMultipartFormData(fields, file) {
|
|
131
|
+
// A random boundary keeps the delimiter from colliding with the BOM content.
|
|
132
|
+
const boundary = `--------------------------cdxgen${randomBytes(16).toString("hex")}`;
|
|
133
|
+
const chunks = [];
|
|
134
|
+
const pushPart = (header, content) => {
|
|
135
|
+
chunks.push(
|
|
136
|
+
Buffer.from(`--${boundary}\r\n${header}\r\n\r\n`),
|
|
137
|
+
Buffer.isBuffer(content) ? content : Buffer.from(String(content)),
|
|
138
|
+
Buffer.from("\r\n"),
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
142
|
+
pushPart(`Content-Disposition: form-data; name="${name}"`, value);
|
|
143
|
+
}
|
|
144
|
+
pushPart(
|
|
145
|
+
`Content-Disposition: form-data; name="${file.name}"; filename="${file.filename}"\r\nContent-Type: ${file.contentType}`,
|
|
146
|
+
file.content,
|
|
147
|
+
);
|
|
148
|
+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
|
149
|
+
return {
|
|
150
|
+
body: Buffer.concat(chunks),
|
|
151
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build the `multipart/form-data` request for Dependency-Track BOM submission.
|
|
157
|
+
*
|
|
158
|
+
* The JSON variant of the endpoint carries the BOM as a base64 string, which Jackson
|
|
159
|
+
* refuses to read beyond 20,000,000 characters - roughly a 15 MB BOM, which container
|
|
160
|
+
* image scans exceed routinely. The form variant carries the BOM as a file part and has
|
|
161
|
+
* no such limit, and is what the server recommends for large uploads.
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} args CLI/server arguments
|
|
164
|
+
* @param {Object | string} bomContents BOM Json
|
|
165
|
+
* @returns {{ body: Buffer, contentType: string } | undefined} request body and content type if the arguments are valid
|
|
166
|
+
*/
|
|
167
|
+
export function buildDependencyTrackBomRequest(args, bomContents) {
|
|
168
|
+
if (!bomContents) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const fields = buildDependencyTrackBomPayload(args);
|
|
172
|
+
if (!fields) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const bomString =
|
|
176
|
+
typeof bomContents === "string" ? bomContents : JSON.stringify(bomContents);
|
|
177
|
+
return encodeMultipartFormData(fields, {
|
|
178
|
+
name: "bom",
|
|
179
|
+
filename: "bom.json",
|
|
180
|
+
contentType: "application/vnd.cyclonedx+json",
|
|
181
|
+
// The server rejects a BOM that still carries a byte order mark.
|
|
182
|
+
content: Buffer.from(bomString.replace(/^\ufeff/, "")),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -23,6 +23,15 @@ export const GOLEM_AUDIT_CATEGORIES = Object.freeze([
|
|
|
23
23
|
"golem-compliance",
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Frozen list of audit categories that never activate on their own. These
|
|
28
|
+
* rule packs serve a specific feature (`--introspect` for build-fidelity) and
|
|
29
|
+
* stay inactive unless the caller explicitly requests the category, so a
|
|
30
|
+
* default `--bom-audit` run - which evaluates every loaded rule - cannot pick
|
|
31
|
+
* them up.
|
|
32
|
+
*/
|
|
33
|
+
export const OPT_IN_BOM_AUDIT_CATEGORIES = Object.freeze(["build-fidelity"]);
|
|
34
|
+
|
|
26
35
|
/** Frozen list of AI-bill-of-materials audit categories. */
|
|
27
36
|
export const AI_BOM_AUDIT_CATEGORIES = Object.freeze([
|
|
28
37
|
"ai-governance",
|
|
@@ -222,6 +222,28 @@ function serviceIdentityKey(service) {
|
|
|
222
222
|
return `${service?.group || ""}:${service?.name || ""}:${service?.version || ""}`.toLowerCase();
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Union two services[].data[] lists, keyed by flow + classification + name so that repeated
|
|
227
|
+
* classifications of the same payload do not accumulate duplicates.
|
|
228
|
+
*
|
|
229
|
+
* @param {Array} existingData Data entries already on the service
|
|
230
|
+
* @param {Array} newData Data entries being merged in
|
|
231
|
+
* @returns {Array} Merged data entries
|
|
232
|
+
*/
|
|
233
|
+
function mergeServiceData(existingData = [], newData = []) {
|
|
234
|
+
const seen = new Map();
|
|
235
|
+
for (const entry of [...existingData, ...newData]) {
|
|
236
|
+
if (!entry?.flow || !entry?.classification) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const key = `${entry.flow}|${entry.classification}|${entry.name || ""}`;
|
|
240
|
+
if (!seen.has(key)) {
|
|
241
|
+
seen.set(key, entry);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return Array.from(seen.values());
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
function mergeServiceProperties(existingProps = [], newProps = []) {
|
|
226
248
|
const merged = [...existingProps];
|
|
227
249
|
for (const newProp of newProps) {
|
|
@@ -249,6 +271,38 @@ function normalizeServiceEndpoints(endpoints) {
|
|
|
249
271
|
return [];
|
|
250
272
|
}
|
|
251
273
|
|
|
274
|
+
function evidenceOccurrenceKey(occurrence) {
|
|
275
|
+
const location = occurrence?.location || {};
|
|
276
|
+
return `${location.path || ""}#${location.line ?? ""}#${location.column ?? ""}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Union two service evidence objects by their occurrences so a collision keeps
|
|
281
|
+
* every call site instead of whichever side arrived first.
|
|
282
|
+
*
|
|
283
|
+
* @param {Object} [existingEvidence] Evidence already on the service
|
|
284
|
+
* @param {Object} [newEvidence] Evidence being merged in
|
|
285
|
+
* @returns {Object} Evidence covering both inputs
|
|
286
|
+
*/
|
|
287
|
+
function mergeServiceEvidence(existingEvidence, newEvidence) {
|
|
288
|
+
if (!existingEvidence) {
|
|
289
|
+
return newEvidence;
|
|
290
|
+
}
|
|
291
|
+
const occurrences = [...(existingEvidence.occurrences || [])];
|
|
292
|
+
const seen = new Set(occurrences.map(evidenceOccurrenceKey));
|
|
293
|
+
for (const occurrence of newEvidence?.occurrences || []) {
|
|
294
|
+
const key = evidenceOccurrenceKey(occurrence);
|
|
295
|
+
if (!seen.has(key)) {
|
|
296
|
+
seen.add(key);
|
|
297
|
+
occurrences.push(occurrence);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!occurrences.length) {
|
|
301
|
+
return existingEvidence;
|
|
302
|
+
}
|
|
303
|
+
return { ...existingEvidence, occurrences };
|
|
304
|
+
}
|
|
305
|
+
|
|
252
306
|
/**
|
|
253
307
|
* Merge CycloneDX services using bom-ref or group/name/version identity.
|
|
254
308
|
*
|
|
@@ -313,6 +367,24 @@ export function mergeServices(services, newServices) {
|
|
|
313
367
|
existing.properties || [],
|
|
314
368
|
service.properties || [],
|
|
315
369
|
);
|
|
370
|
+
// Carry the richer fields through a collision too. Without these, a dosai service that collided
|
|
371
|
+
// with one derived from endpoints lost precisely what schema 4.0.0 adds: its bom-ref, its data
|
|
372
|
+
// classifications, and its evidence.
|
|
373
|
+
existing["bom-ref"] = existing["bom-ref"] || service["bom-ref"];
|
|
374
|
+
if (Array.isArray(service.data) && service.data.length) {
|
|
375
|
+
existing.data = mergeServiceData(existing.data || [], service.data);
|
|
376
|
+
}
|
|
377
|
+
if (service.evidence) {
|
|
378
|
+
existing.evidence = mergeServiceEvidence(
|
|
379
|
+
existing.evidence,
|
|
380
|
+
service.evidence,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(service.tags) && service.tags.length) {
|
|
384
|
+
existing.tags = Array.from(
|
|
385
|
+
new Set([...(existing.tags || []), ...service.tags]),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
316
388
|
if (Array.isArray(service.services) && service.services.length) {
|
|
317
389
|
existing.services = mergeServices(
|
|
318
390
|
existing.services || [],
|