@cdxgen/cdxgen 13.2.0 → 13.4.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 +33 -14
- package/bin/cdxgen.js +310 -138
- package/bin/evinse.js +22 -0
- package/bin/repl.js +86 -0
- package/bin/sign.js +119 -15
- package/bin/tracebom.js +69 -5
- package/bin/validate.js +107 -19
- package/bin/verify.js +172 -63
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +85 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +156 -58
- package/lib/cli/jsBom.js +99 -53
- package/lib/cli/jvmBom.js +708 -147
- package/lib/cli/managedBom.js +415 -224
- package/lib/cli/nativeBom.js +264 -44
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +22 -15
- package/lib/core/fs.js +176 -7
- package/lib/core/httpClient.js +159 -24
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +274 -108
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +76 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +399 -51
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1983 -793
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +356 -37
- package/lib/ecosystems/jsEvidence.js +104 -44
- package/lib/ecosystems/kosi.js +31 -7
- package/lib/ecosystems/npmutils.js +174 -9
- package/lib/ecosystems/parsers-dotnet.js +616 -159
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +371 -70
- package/lib/ecosystems/parsers-js.js +263 -76
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +929 -216
- package/lib/ecosystems/parsers-python.js +167 -24
- package/lib/ecosystems/parsers-rust.js +28 -22
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +1492 -182
- package/lib/ecosystems/rusi.js +54 -10
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +1008 -191
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +1101 -222
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +703 -43
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +798 -57
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +90 -6
- package/lib/inventory/evidenceUtils.js +166 -113
- package/lib/inventory/fetchBatch.js +690 -55
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +45 -27
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +295 -25
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +467 -46
- package/lib/managers/oci.js +63 -3
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +466 -0
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +83 -16
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +64 -11
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +2 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +23 -23
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- 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.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +70 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +57 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +54 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +16 -5
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- 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/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +164 -3
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +132 -15
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +182 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +219 -5
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +121 -9
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +59 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +17 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +77 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +18 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
package/lib/inventory/source.js
CHANGED
|
@@ -7,7 +7,6 @@ import { Purl } from "@cdxgen/cdx-purl";
|
|
|
7
7
|
import { coerce, diff, prerelease } from "semver";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
cdxgenAgent,
|
|
11
10
|
createDryRunError,
|
|
12
11
|
DEBUG_MODE,
|
|
13
12
|
hasDangerousUnicode,
|
|
@@ -24,12 +23,14 @@ import {
|
|
|
24
23
|
} from "../core/fs.js";
|
|
25
24
|
import { thoughtLog } from "../core/logger.js";
|
|
26
25
|
import { isValidDriveRoot, isWin } from "../core/paths.js";
|
|
26
|
+
import { registerRunReset } from "../core/runState.js";
|
|
27
27
|
import {
|
|
28
|
+
gatedGet,
|
|
28
29
|
prefetchEnabled,
|
|
29
30
|
prefetchedResponse,
|
|
30
31
|
prefetchJson,
|
|
31
|
-
withHostRateLimit,
|
|
32
32
|
} from "./fetchBatch.js";
|
|
33
|
+
import { canonicalPurlFromLooseString } from "./purl.js";
|
|
33
34
|
|
|
34
35
|
/** Raised when a caller omits a registry helper that the purl type requires. */
|
|
35
36
|
class MissingRegistryHelperError extends Error {}
|
|
@@ -954,6 +955,36 @@ function packageNameForLookup(purlObj) {
|
|
|
954
955
|
return `${namespace}/${purlObj.name}`;
|
|
955
956
|
}
|
|
956
957
|
|
|
958
|
+
/**
|
|
959
|
+
* Read a purl supplied as a scan source.
|
|
960
|
+
*
|
|
961
|
+
* People type purls the way their package manager prints versions and URLs:
|
|
962
|
+
* `pkg:cargo/wasi@0.11.0+wasi-snapshot-preview1` or
|
|
963
|
+
* `pkg:npm/foo?vcs_url=git+https://...`. cdx-purl requires those `+` to be
|
|
964
|
+
* escaped, so a purl that does not parse strictly is split on its separators
|
|
965
|
+
* and rebuilt with every component escaped once. The rebuilt purl is then
|
|
966
|
+
* parsed strictly, so validation always inspects cdx-purl's reading of it.
|
|
967
|
+
*
|
|
968
|
+
* @param {string} purlString Package URL as supplied
|
|
969
|
+
* @returns {Purl|undefined} Parsed purl, or undefined when none can be recovered
|
|
970
|
+
*/
|
|
971
|
+
function parsePurlSourceInput(purlString) {
|
|
972
|
+
try {
|
|
973
|
+
return Purl.parse(purlString);
|
|
974
|
+
} catch {
|
|
975
|
+
// Fall through to the structural rebuild.
|
|
976
|
+
}
|
|
977
|
+
const canonical = canonicalPurlFromLooseString(purlString);
|
|
978
|
+
if (!canonical) {
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
return Purl.parse(canonical);
|
|
983
|
+
} catch {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
957
988
|
/**
|
|
958
989
|
* Validate package URL source input and return an error object when invalid.
|
|
959
990
|
*
|
|
@@ -964,10 +995,8 @@ export function validatePurlSource(purlString) {
|
|
|
964
995
|
if (!maybePurlSource(purlString)) {
|
|
965
996
|
return null;
|
|
966
997
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
purlObj = Purl.parse(purlString);
|
|
970
|
-
} catch (_err) {
|
|
998
|
+
const purlObj = parsePurlSourceInput(purlString);
|
|
999
|
+
if (!purlObj) {
|
|
971
1000
|
return {
|
|
972
1001
|
status: 400,
|
|
973
1002
|
error: "Invalid purl source",
|
|
@@ -1136,6 +1165,8 @@ export function resetGitUrlSourcePrefetch() {
|
|
|
1136
1165
|
gitUrlSourcePrefetch = new Map();
|
|
1137
1166
|
}
|
|
1138
1167
|
|
|
1168
|
+
registerRunReset("source:registry-documents", resetGitUrlSourcePrefetch);
|
|
1169
|
+
|
|
1139
1170
|
/**
|
|
1140
1171
|
* Prefetch the registry documents a list of purls will be resolved from.
|
|
1141
1172
|
*
|
|
@@ -1189,12 +1220,10 @@ async function fetchRegistryDocument(lookup) {
|
|
|
1189
1220
|
// whose policy is one request per second however the request was reached.
|
|
1190
1221
|
return (
|
|
1191
1222
|
prefetchedResponse(gitUrlSourcePrefetch, lookup.url) ||
|
|
1192
|
-
(await
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
}),
|
|
1197
|
-
))
|
|
1223
|
+
(await gatedGet(lookup.url, {
|
|
1224
|
+
...(lookup.accept ? { headers: { Accept: lookup.accept } } : {}),
|
|
1225
|
+
responseType: "json",
|
|
1226
|
+
}))
|
|
1198
1227
|
);
|
|
1199
1228
|
}
|
|
1200
1229
|
|
|
@@ -1209,24 +1238,13 @@ function parsePurlForSourceResolution(purlString) {
|
|
|
1209
1238
|
if (!maybePurlSource(purlString) || validatePurlSource(purlString)) {
|
|
1210
1239
|
return undefined;
|
|
1211
1240
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
// the qualifier section before parsing so real-world purls resolve.
|
|
1217
|
-
const qIdx = purlString.indexOf("?");
|
|
1218
|
-
const safePurlString =
|
|
1219
|
-
qIdx >= 0
|
|
1220
|
-
? purlString.slice(0, qIdx + 1) +
|
|
1221
|
-
purlString.slice(qIdx + 1).replaceAll("+", "%2B")
|
|
1222
|
-
: purlString;
|
|
1223
|
-
purlObj = Purl.parse(safePurlString);
|
|
1224
|
-
} catch (err) {
|
|
1241
|
+
// Purls in the wild leave `+` unescaped in versions and in qualifier values
|
|
1242
|
+
// such as `vcs_url=git+https://`; the shared reader rebuilds them.
|
|
1243
|
+
const purlObj = parsePurlSourceInput(purlString);
|
|
1244
|
+
if (!purlObj) {
|
|
1225
1245
|
if (DEBUG_MODE) {
|
|
1226
|
-
const errorMessage = err?.message || String(err);
|
|
1227
1246
|
thoughtLog("Unable to resolve repository URL for purl.", {
|
|
1228
1247
|
purlString,
|
|
1229
|
-
errorMessage,
|
|
1230
1248
|
});
|
|
1231
1249
|
}
|
|
1232
1250
|
return undefined;
|
package/lib/inventory/spdx.js
CHANGED
|
@@ -343,12 +343,113 @@ export function findLicenseId(name) {
|
|
|
343
343
|
: name;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Identify a license from the title and opening of its full text. A license
|
|
348
|
+
* text quotes other licenses (the GNU GPL's text names the GNU Lesser GPL),
|
|
349
|
+
* so the substring scan of `guessLicenseId` alone can name the wrong one.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} content License file contents
|
|
352
|
+
* @returns {string|undefined} SPDX license id or expression
|
|
353
|
+
*/
|
|
354
|
+
export function licenseIdFromText(content) {
|
|
355
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const head = content.slice(0, 4000).replace(/\s+/g, " ").trim().toUpperCase();
|
|
359
|
+
const version = (prefix) => {
|
|
360
|
+
const at = head.indexOf(prefix);
|
|
361
|
+
if (at < 0) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const tail = head.slice(at + prefix.length, at + prefix.length + 60);
|
|
365
|
+
const match = tail.match(/VERSION (\d(?:\.\d)?)/);
|
|
366
|
+
return match?.[1];
|
|
367
|
+
};
|
|
368
|
+
// the first lines name the license; quotations of others come later, so
|
|
369
|
+
// of the GNU licenses the one named first is the one the text is
|
|
370
|
+
const title = head.slice(0, 200);
|
|
371
|
+
const gnu = [
|
|
372
|
+
["GNU AFFERO GENERAL PUBLIC LICENSE", { 3: "AGPL-3.0-only" }],
|
|
373
|
+
[
|
|
374
|
+
"GNU LESSER GENERAL PUBLIC LICENSE",
|
|
375
|
+
{ 3: "LGPL-3.0-only", 2.1: "LGPL-2.1-only", 2: "LGPL-2.0-only" },
|
|
376
|
+
],
|
|
377
|
+
[
|
|
378
|
+
"GNU LIBRARY GENERAL PUBLIC LICENSE",
|
|
379
|
+
{ 2.1: "LGPL-2.1-only", 2: "LGPL-2.0-only" },
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
"GNU GENERAL PUBLIC LICENSE",
|
|
383
|
+
{ 3: "GPL-3.0-only", 2: "GPL-2.0-only", 1: "GPL-1.0-only" },
|
|
384
|
+
],
|
|
385
|
+
]
|
|
386
|
+
.map(([name, ids]) => ({ name, ids, at: title.indexOf(name) }))
|
|
387
|
+
.filter((t) => t.at >= 0)
|
|
388
|
+
.sort((a, b) => a.at - b.at)[0];
|
|
389
|
+
if (gnu) {
|
|
390
|
+
return gnu.ids[version(gnu.name)];
|
|
391
|
+
}
|
|
392
|
+
if (head.includes("APACHE LICENSE") && head.includes("VERSION 2.0")) {
|
|
393
|
+
return head.includes("LLVM EXCEPTION")
|
|
394
|
+
? "Apache-2.0 WITH LLVM-exception"
|
|
395
|
+
: "Apache-2.0";
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
title.includes("MOZILLA PUBLIC LICENSE") &&
|
|
399
|
+
head.includes("VERSION 2.0")
|
|
400
|
+
) {
|
|
401
|
+
return "MPL-2.0";
|
|
402
|
+
}
|
|
403
|
+
if (title.includes("BOOST SOFTWARE LICENSE - VERSION 1.0")) {
|
|
404
|
+
return "BSL-1.0";
|
|
405
|
+
}
|
|
406
|
+
if (
|
|
407
|
+
head.includes(
|
|
408
|
+
"THIS IS FREE AND UNENCUMBERED SOFTWARE RELEASED INTO THE PUBLIC DOMAIN",
|
|
409
|
+
)
|
|
410
|
+
) {
|
|
411
|
+
return "Unlicense";
|
|
412
|
+
}
|
|
413
|
+
if (head.includes("PERMISSION IS HEREBY GRANTED, FREE OF CHARGE")) {
|
|
414
|
+
return "MIT";
|
|
415
|
+
}
|
|
416
|
+
if (
|
|
417
|
+
head.includes(
|
|
418
|
+
"PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED",
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
return "ISC";
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
head.includes("THIS SOFTWARE IS PROVIDED 'AS-IS'") &&
|
|
425
|
+
head.includes(
|
|
426
|
+
"PERMISSION IS GRANTED TO ANYONE TO USE THIS SOFTWARE FOR ANY PURPOSE",
|
|
427
|
+
)
|
|
428
|
+
) {
|
|
429
|
+
return "Zlib";
|
|
430
|
+
}
|
|
431
|
+
if (head.includes("REDISTRIBUTION AND USE IN SOURCE AND BINARY FORMS")) {
|
|
432
|
+
if (head.includes("ALL ADVERTISING MATERIALS MENTIONING")) {
|
|
433
|
+
return "BSD-4-Clause";
|
|
434
|
+
}
|
|
435
|
+
return head.includes("NEITHER THE NAME") ||
|
|
436
|
+
head.includes("THE NAME OF THE AUTHOR MAY NOT BE USED")
|
|
437
|
+
? "BSD-3-Clause"
|
|
438
|
+
: "BSD-2-Clause";
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
346
443
|
/**
|
|
347
444
|
* Method to guess the spdx license id from license contents
|
|
348
445
|
*
|
|
349
446
|
* @param {string} content License file contents
|
|
350
447
|
*/
|
|
351
448
|
export function guessLicenseId(content) {
|
|
449
|
+
const fromText = licenseIdFromText(content);
|
|
450
|
+
if (fromText) {
|
|
451
|
+
return fromText;
|
|
452
|
+
}
|
|
352
453
|
content = content.replace(/\n/g, " ");
|
|
353
454
|
for (const l of licenseMapping) {
|
|
354
455
|
for (const j in l.names) {
|
|
@@ -20,6 +20,17 @@ try {
|
|
|
20
20
|
SaferExec = undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether the @cdxgen/safer-exec tracing runtime could be loaded. When false,
|
|
25
|
+
* dynamic tracing cannot run at all, and every trace would otherwise exit 0
|
|
26
|
+
* with a valid but empty BOM, hiding the broken installation (#4378).
|
|
27
|
+
*
|
|
28
|
+
* @returns {boolean} True when the tracing runtime is available
|
|
29
|
+
*/
|
|
30
|
+
export function isSaferExecAvailable() {
|
|
31
|
+
return Boolean(SaferExec);
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
35
|
const __dirname = dirname(__filename);
|
|
25
36
|
|
|
@@ -66,6 +77,7 @@ export function parseCommand(cmdStr) {
|
|
|
66
77
|
export function resolveSaferExecBinary() {
|
|
67
78
|
const currentPlatform = platform();
|
|
68
79
|
const currentArch = arch();
|
|
80
|
+
const isWindows = currentPlatform === "win32";
|
|
69
81
|
let pkgName = "";
|
|
70
82
|
|
|
71
83
|
if (currentPlatform === "darwin") {
|
|
@@ -80,6 +92,14 @@ export function resolveSaferExecBinary() {
|
|
|
80
92
|
} else if (currentArch === "arm64") {
|
|
81
93
|
pkgName = "@cdxgen/safer-exec-linux-arm64";
|
|
82
94
|
}
|
|
95
|
+
} else if (isWindows) {
|
|
96
|
+
// The Windows packages ship the MXC executor (wxc-exec.exe) next to the
|
|
97
|
+
// engine, which is where the engine looks for it.
|
|
98
|
+
if (currentArch === "x64") {
|
|
99
|
+
pkgName = "@cdxgen/safer-exec-windows-amd64";
|
|
100
|
+
} else if (currentArch === "arm64") {
|
|
101
|
+
pkgName = "@cdxgen/safer-exec-windows-arm64";
|
|
102
|
+
}
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
if (!pkgName) {
|
|
@@ -105,8 +125,15 @@ export function resolveSaferExecBinary() {
|
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
for (const modulesDir of searchDirs) {
|
|
108
|
-
// Direct structure under node_modules
|
|
109
|
-
|
|
128
|
+
// Direct structure under node_modules. The Go runtime inside the
|
|
129
|
+
// platform package is bin/safer-exec-rt(.exe); "safer-exec" is the JS
|
|
130
|
+
// dispatcher's npm bin name, not a file on disk.
|
|
131
|
+
const directPath = join(
|
|
132
|
+
modulesDir,
|
|
133
|
+
pkgName,
|
|
134
|
+
"bin",
|
|
135
|
+
isWindows ? "safer-exec-rt.exe" : "safer-exec-rt",
|
|
136
|
+
);
|
|
110
137
|
let realDirectPath;
|
|
111
138
|
try {
|
|
112
139
|
realDirectPath = realpathSync(directPath);
|
|
@@ -114,10 +141,12 @@ export function resolveSaferExecBinary() {
|
|
|
114
141
|
realDirectPath = directPath;
|
|
115
142
|
}
|
|
116
143
|
if (existsSync(realDirectPath)) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
if (!isWindows) {
|
|
145
|
+
try {
|
|
146
|
+
chmodSync(realDirectPath, 0o755);
|
|
147
|
+
} catch (_err) {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
121
150
|
}
|
|
122
151
|
return realDirectPath;
|
|
123
152
|
}
|
|
@@ -157,7 +186,19 @@ export function resolveSaferExecBinary() {
|
|
|
157
186
|
* @param {boolean} [options.traceExec] - Log every child process spawned
|
|
158
187
|
* @param {string[]} [options.allowExec] - Executables the command is allowed to run
|
|
159
188
|
* @param {string[]} [options.blockExec] - Executables to block from running
|
|
160
|
-
* @
|
|
189
|
+
* @param {boolean} [options.proxyEgress] - Route egress through safer-exec's hostname-pinning proxy (network stays enabled; only allowHosts are reachable)
|
|
190
|
+
* @param {boolean} [options.allowLoopback] - Allow loopback connections
|
|
191
|
+
* @param {boolean} [options.dryRun] - Deny every filesystem and network side effect and collect a dry-run report
|
|
192
|
+
* @param {string} [options.policy] - Named safer-exec ecosystem policy (npm, pypi, maven, ...)
|
|
193
|
+
* @param {string} [options.policyFile] - Path to a safer-exec JSON policy file
|
|
194
|
+
* @param {boolean} [options.blockInterpreters] - Block interpreters with sandbox or task-port exemptions (macOS)
|
|
195
|
+
* @param {boolean} [options.denyPersistenceWrites] - Deny writes to auto-execution and persistence locations
|
|
196
|
+
* @param {boolean} [options.privateTmp] - Private tmpfs /tmp and /var/tmp (Linux)
|
|
197
|
+
* @param {string} [options.protectHome] - Home isolation: read-only, tmpfs, or off (Linux)
|
|
198
|
+
* @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, proxy egress decisions, and the dry-run summary
|
|
199
|
+
* @throws {Error} When the @cdxgen/safer-exec tracing runtime is not installed
|
|
200
|
+
* (for example after pruning it from a standalone payload), since the traced
|
|
201
|
+
* command could never run and the BOM would silently be empty
|
|
161
202
|
*/
|
|
162
203
|
export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
163
204
|
const emptyResult = { libPaths: [], httpAccessEntries: [] };
|
|
@@ -174,11 +215,23 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
174
215
|
);
|
|
175
216
|
|
|
176
217
|
if (!SaferExec) {
|
|
177
|
-
|
|
218
|
+
throw new Error(
|
|
219
|
+
"Dynamic tracing requires the @cdxgen/safer-exec package, which could not be loaded from this installation. Re-install with optional dependencies enabled, or use the official tracebom standalone binary.",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const exec = new SaferExec();
|
|
224
|
+
// Policies are applied first so explicit options below take precedence, and
|
|
225
|
+
// outside the try so an unknown policy or unreadable policy file fails the
|
|
226
|
+
// run instead of producing an empty BOM.
|
|
227
|
+
if (options.policy) {
|
|
228
|
+
exec.applyPolicy(options.policy);
|
|
229
|
+
}
|
|
230
|
+
if (options.policyFile) {
|
|
231
|
+
exec.applyPolicyFile(options.policyFile);
|
|
178
232
|
}
|
|
179
233
|
|
|
180
234
|
try {
|
|
181
|
-
const exec = new SaferExec();
|
|
182
235
|
if (workingDir) {
|
|
183
236
|
exec.workingDir(workingDir);
|
|
184
237
|
}
|
|
@@ -192,9 +245,49 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
192
245
|
exec.traceLibraries().suppressLibLoadStderr(true).enableAudit();
|
|
193
246
|
|
|
194
247
|
// Apply sandbox options
|
|
195
|
-
|
|
248
|
+
// The egress proxy runs on the host side and must be reachable, so it
|
|
249
|
+
// needs the network enabled (an isolated namespace would bypass it).
|
|
250
|
+
if (
|
|
251
|
+
options.disableNetwork !== false &&
|
|
252
|
+
!options.traceHTTPURLs &&
|
|
253
|
+
!options.proxyEgress
|
|
254
|
+
) {
|
|
196
255
|
exec.disableNetwork();
|
|
197
256
|
}
|
|
257
|
+
if (options.proxyEgress) {
|
|
258
|
+
if (!options.allowHosts?.length && !options.allowUrls?.length) {
|
|
259
|
+
console.warn(
|
|
260
|
+
"[cdxgen trace] egress proxy enabled without --allow-host: every outbound connection will be denied.",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
exec.proxyEgress();
|
|
264
|
+
}
|
|
265
|
+
if (options.allowLoopback) {
|
|
266
|
+
exec.allowLoopback();
|
|
267
|
+
}
|
|
268
|
+
if (options.dryRun) {
|
|
269
|
+
exec.enableDryRun();
|
|
270
|
+
}
|
|
271
|
+
if (platform() === "win32" && options.blockInterpreters) {
|
|
272
|
+
// safer-exec reports --private-tmp and --protect-home on Windows when a
|
|
273
|
+
// grant defeats them, but --block-interpreters is macOS-only and stays
|
|
274
|
+
// silent (cdxgen/safer-exec#4).
|
|
275
|
+
console.warn(
|
|
276
|
+
"[cdxgen trace] --block-interpreters has no effect on Windows.",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (options.blockInterpreters) {
|
|
280
|
+
exec.blockInterpreters();
|
|
281
|
+
}
|
|
282
|
+
if (options.denyPersistenceWrites) {
|
|
283
|
+
exec.denyPersistenceWrites();
|
|
284
|
+
}
|
|
285
|
+
if (options.privateTmp) {
|
|
286
|
+
exec.privateTmp();
|
|
287
|
+
}
|
|
288
|
+
if (options.protectHome && options.protectHome !== "off") {
|
|
289
|
+
exec.protectHome(options.protectHome);
|
|
290
|
+
}
|
|
198
291
|
if (options.readPaths?.length) {
|
|
199
292
|
exec.readPaths(options.readPaths);
|
|
200
293
|
}
|
|
@@ -306,25 +399,86 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
306
399
|
}
|
|
307
400
|
|
|
308
401
|
const result = await exec.run(cmd, args);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
402
|
+
const stderr = result?.stderr || "";
|
|
403
|
+
const { warnings, error: engineError } = engineMessages(stderr);
|
|
404
|
+
// Windows records DLL loads through an ETW session, which only an
|
|
405
|
+
// elevated process (or a Performance Log Users member) may start.
|
|
406
|
+
const etwDenied =
|
|
407
|
+
platform() === "win32" &&
|
|
408
|
+
warnings.some(
|
|
409
|
+
(line) =>
|
|
410
|
+
line.includes("DLL tracing:") &&
|
|
411
|
+
line.includes("administrator rights"),
|
|
412
|
+
);
|
|
413
|
+
// run() captures the engine's stderr, so a control the sandbox could not
|
|
414
|
+
// apply would otherwise read like one it did.
|
|
415
|
+
for (const line of warnings) {
|
|
416
|
+
if (etwDenied && line.includes("DLL tracing:")) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
console.warn(line);
|
|
420
|
+
}
|
|
421
|
+
if (etwDenied) {
|
|
422
|
+
console.warn(
|
|
423
|
+
"[cdxgen trace] library tracing on Windows needs an elevated prompt (or membership in the Performance Log Users group); the BOM has no library components. Egress services from --proxy-egress are still recorded.",
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (result && result.exitCode !== 0) {
|
|
427
|
+
if (engineError) {
|
|
428
|
+
console.error(engineError);
|
|
429
|
+
if (options.strict) {
|
|
430
|
+
// Under --strict the sandbox refused the run; an empty BOM would
|
|
431
|
+
// read as a command that loaded nothing.
|
|
432
|
+
throw Object.assign(
|
|
433
|
+
new Error(`Tracing refused under --strict: ${engineError}`),
|
|
434
|
+
{ strictRefusal: true },
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
} else if (stderr.includes("[safer-exec] Error:")) {
|
|
438
|
+
console.error("Tracing launcher execution failed:", stderr.trim());
|
|
439
|
+
} else {
|
|
440
|
+
// A failing command usually means a partial trace (for example a
|
|
441
|
+
// sandbox denial), so the BOM may be incomplete.
|
|
442
|
+
console.warn(
|
|
443
|
+
`[cdxgen trace] traced command exited with code ${result.exitCode}; the BOM may be incomplete.`,
|
|
314
444
|
);
|
|
315
445
|
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
446
|
+
}
|
|
447
|
+
if (options.traceHTTPURLs) {
|
|
448
|
+
// eBPF URL tracing needs CAP_BPF/CAP_PERFMON. Surface why it was not
|
|
449
|
+
// available whatever the command's exit code, since the BOM otherwise
|
|
450
|
+
// silently lacks services.
|
|
451
|
+
const httpLines = stderr
|
|
452
|
+
.split("\n")
|
|
453
|
+
.filter(
|
|
454
|
+
(line) =>
|
|
455
|
+
line.includes("http-trace") ||
|
|
456
|
+
line.includes("httptrace") ||
|
|
457
|
+
line.includes("SSL/TLS libraries"),
|
|
458
|
+
);
|
|
319
459
|
if (
|
|
320
|
-
|
|
321
|
-
stderr.includes("httptrace") ||
|
|
322
|
-
stderr.includes("SSL/TLS libraries")
|
|
460
|
+
httpLines.some((line) => /warning|not supported|failed/i.test(line))
|
|
323
461
|
) {
|
|
324
|
-
console.warn(
|
|
462
|
+
console.warn(
|
|
463
|
+
"[cdxgen trace] HTTP URL tracing unavailable or degraded (it needs root or CAP_BPF and CAP_PERFMON):",
|
|
464
|
+
httpLines.join("\n"),
|
|
465
|
+
);
|
|
325
466
|
}
|
|
326
467
|
}
|
|
327
468
|
|
|
469
|
+
// Without an event source (strace on Linux) a dry run still denies every
|
|
470
|
+
// side effect but reports nothing, which must not read as "no activity".
|
|
471
|
+
let dryRunSummary = result?.dryRun?.summary;
|
|
472
|
+
if (
|
|
473
|
+
options.dryRun &&
|
|
474
|
+
result?.stderr?.includes("dry-run cannot capture attempted operations")
|
|
475
|
+
) {
|
|
476
|
+
console.warn(
|
|
477
|
+
"[cdxgen trace] dry run could not capture attempted operations on this host (install strace or relax ptrace_scope); the BOM records no counts.",
|
|
478
|
+
);
|
|
479
|
+
dryRunSummary = { captured: false };
|
|
480
|
+
}
|
|
481
|
+
|
|
328
482
|
// Read and parse CBOM file if temp/explicit path was used
|
|
329
483
|
let cryptoComponents = [];
|
|
330
484
|
const cbomPathToRead = options.cbom || tempCbomPath;
|
|
@@ -377,6 +531,8 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
377
531
|
httpAccessEntries: [...collectedUrls, ...urls],
|
|
378
532
|
cryptoEntries: [...collectedCrypto, ...cryptoLogs],
|
|
379
533
|
cryptoComponents,
|
|
534
|
+
egressEntries: parseEgressEntries(result.auditLog),
|
|
535
|
+
dryRunSummary,
|
|
380
536
|
};
|
|
381
537
|
}
|
|
382
538
|
|
|
@@ -385,13 +541,119 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
385
541
|
httpAccessEntries: collectedUrls,
|
|
386
542
|
cryptoEntries: collectedCrypto,
|
|
387
543
|
cryptoComponents,
|
|
544
|
+
egressEntries: [],
|
|
545
|
+
dryRunSummary,
|
|
388
546
|
};
|
|
389
547
|
} catch (err) {
|
|
548
|
+
if (err?.strictRefusal) {
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
390
551
|
console.error("Tracing command execution failed:", err);
|
|
391
552
|
}
|
|
392
553
|
return emptyResult;
|
|
393
554
|
}
|
|
394
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Picks the safer-exec engine's own lines out of the stderr it shares with the
|
|
558
|
+
* traced command. Warnings start with "safer-exec: warning:"; when the engine
|
|
559
|
+
* stops a run, its error is the last other "safer-exec: " line it prints.
|
|
560
|
+
*
|
|
561
|
+
* @param {string} stderr Captured stderr of a safer-exec run
|
|
562
|
+
* @returns {{warnings: string[], error: string|undefined}} Unique engine warnings, and the engine's error if any
|
|
563
|
+
*/
|
|
564
|
+
export function engineMessages(stderr) {
|
|
565
|
+
const lines = String(stderr || "")
|
|
566
|
+
.split(/\r?\n/)
|
|
567
|
+
.filter((line) => line.startsWith("safer-exec: "));
|
|
568
|
+
const warnings = [
|
|
569
|
+
...new Set(lines.filter((line) => line.startsWith("safer-exec: warning:"))),
|
|
570
|
+
];
|
|
571
|
+
const others = lines.filter(
|
|
572
|
+
(line) => !line.startsWith("safer-exec: warning:"),
|
|
573
|
+
);
|
|
574
|
+
return { warnings, error: others.at(-1) };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Returns the sorted, de-duplicated parameter names of a query string.
|
|
579
|
+
*
|
|
580
|
+
* @param {string} query - Raw query string, with or without a leading "?"
|
|
581
|
+
* @returns {string} Comma-separated parameter names, or "" when there are none
|
|
582
|
+
*/
|
|
583
|
+
function queryParameterNames(query) {
|
|
584
|
+
if (!query) {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
const names = new Set();
|
|
588
|
+
for (const pair of String(query).replace(/^\?/, "").split("&")) {
|
|
589
|
+
const eqIdx = pair.indexOf("=");
|
|
590
|
+
const name = (eqIdx >= 0 ? pair.substring(0, eqIdx) : pair).trim();
|
|
591
|
+
if (name) {
|
|
592
|
+
names.add(name);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return Array.from(names).sort().join(",");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Extracts the egress proxy's decisions from a safer-exec audit log.
|
|
600
|
+
* "proxy-connect" targets are "host:port"; "proxy-violation" targets are
|
|
601
|
+
* "CONNECT host:port" or "METHOD http://host[:port]/path". Only the authority
|
|
602
|
+
* is kept, never a path or query.
|
|
603
|
+
*
|
|
604
|
+
* @param {Object[]} auditLog - safer-exec audit entries
|
|
605
|
+
* @returns {{host: string, port: number, protocol: string, decision: string}[]} Unique egress decisions
|
|
606
|
+
*/
|
|
607
|
+
export function parseEgressEntries(auditLog) {
|
|
608
|
+
const seen = new Set();
|
|
609
|
+
const entries = [];
|
|
610
|
+
for (const e of auditLog || []) {
|
|
611
|
+
if (e?.type !== "proxy-connect" && e?.type !== "proxy-violation") {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const target = String(e.target || "");
|
|
615
|
+
let authority = target;
|
|
616
|
+
let protocol = "https";
|
|
617
|
+
if (e.type === "proxy-connect") {
|
|
618
|
+
protocol = e.details === "http" ? "http" : "https";
|
|
619
|
+
} else {
|
|
620
|
+
const spaceIdx = target.indexOf(" ");
|
|
621
|
+
authority = spaceIdx >= 0 ? target.substring(spaceIdx + 1) : target;
|
|
622
|
+
const schemeIdx = authority.indexOf("://");
|
|
623
|
+
if (schemeIdx >= 0) {
|
|
624
|
+
protocol = authority.substring(0, schemeIdx);
|
|
625
|
+
authority = authority.substring(schemeIdx + 3);
|
|
626
|
+
const slashIdx = authority.indexOf("/");
|
|
627
|
+
if (slashIdx >= 0) {
|
|
628
|
+
authority = authority.substring(0, slashIdx);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
let host = authority;
|
|
633
|
+
let port = protocol === "http" ? 80 : 443;
|
|
634
|
+
const colonIdx = authority.lastIndexOf(":");
|
|
635
|
+
if (colonIdx > 0 && !authority.endsWith("]")) {
|
|
636
|
+
const parsedPort = Number.parseInt(authority.substring(colonIdx + 1), 10);
|
|
637
|
+
if (!Number.isNaN(parsedPort)) {
|
|
638
|
+
port = parsedPort;
|
|
639
|
+
host = authority.substring(0, colonIdx);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
host = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
643
|
+
if (!host) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const decision = e.type === "proxy-connect" ? "allowed" : "denied";
|
|
647
|
+
const key = `${decision}|${protocol}|${host}|${port}`;
|
|
648
|
+
if (seen.has(key)) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
seen.add(key);
|
|
652
|
+
entries.push({ host, port, protocol, decision });
|
|
653
|
+
}
|
|
654
|
+
return entries;
|
|
655
|
+
}
|
|
656
|
+
|
|
395
657
|
/**
|
|
396
658
|
* Groups HTTP access entries into a CycloneDX services-ready map.
|
|
397
659
|
* Each unique (host, port, protocol) combination becomes a service.
|
|
@@ -409,7 +671,9 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
|
|
|
409
671
|
properties: [],
|
|
410
672
|
};
|
|
411
673
|
}
|
|
412
|
-
const
|
|
674
|
+
const scheme = entry.protocol === "http" ? "http" : "https";
|
|
675
|
+
const defaultPort = scheme === "http" ? 80 : 443;
|
|
676
|
+
const endpoint = `${scheme}://${entry.host}${entry.port && entry.port !== defaultPort ? `:${entry.port}` : ""}${entry.path || "/"}`;
|
|
413
677
|
servicesMap[serviceName].endpoints.add(endpoint);
|
|
414
678
|
if (entry.method) {
|
|
415
679
|
const methodProp = {
|
|
@@ -424,8 +688,14 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
|
|
|
424
688
|
servicesMap[serviceName].properties.push(methodProp);
|
|
425
689
|
}
|
|
426
690
|
}
|
|
427
|
-
|
|
428
|
-
|
|
691
|
+
// Query values can carry tokens and signatures, so only the parameter
|
|
692
|
+
// names are recorded.
|
|
693
|
+
const queryParams = queryParameterNames(entry.query);
|
|
694
|
+
if (queryParams) {
|
|
695
|
+
const queryProp = {
|
|
696
|
+
name: "cdx:dynamic:httpQueryParams",
|
|
697
|
+
value: queryParams,
|
|
698
|
+
};
|
|
429
699
|
if (
|
|
430
700
|
!servicesMap[serviceName].properties.some(
|
|
431
701
|
(p) => p.name === queryProp.name && p.value === queryProp.value,
|