@cyclonedx/cdxgen 12.3.2 → 12.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 +70 -22
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +238 -116
- package/bin/convert.js +28 -13
- package/bin/hbom.js +490 -0
- package/bin/repl.js +580 -29
- package/bin/validate.js +34 -4
- package/bin/verify.js +40 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +171 -15
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +76 -5
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +36 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +209 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +647 -127
- package/lib/cli/index.poku.js +1905 -187
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +1444 -38
- package/lib/helpers/analyzer.poku.js +409 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +336 -23
- package/lib/helpers/display.poku.js +179 -43
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/protobom.js +156 -45
- package/lib/helpers/protobom.poku.js +140 -5
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +2454 -198
- package/lib/helpers/utils.poku.js +1798 -74
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2306 -350
- package/lib/managers/binary.poku.js +1700 -1
- package/lib/managers/docker.js +441 -95
- package/lib/managers/docker.poku.js +1479 -14
- package/lib/server/server.js +2 -24
- package/lib/server/server.poku.js +36 -1
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +2967 -990
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +192 -1
- package/lib/stages/postgen/postgen.poku.js +321 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/package.json +24 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +3 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +74 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -1
- 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/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/managers/binary.js
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
|
-
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { arch as _arch, platform as _platform, homedir } from "node:os";
|
|
3
1
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "node:
|
|
2
|
+
lstatSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
statSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { platform as _platform, homedir } from "node:os";
|
|
9
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
11
10
|
import process from "node:process";
|
|
12
11
|
|
|
13
12
|
import { PackageURL } from "packageurl-js";
|
|
14
13
|
|
|
15
14
|
import { createContainerRiskProperties } from "../helpers/containerRisk.js";
|
|
16
15
|
import { createGtfoBinsProperties } from "../helpers/gtfobins.js";
|
|
16
|
+
import {
|
|
17
|
+
resolveCdxgenPlugins,
|
|
18
|
+
resolvePluginBinary,
|
|
19
|
+
setPluginsPathEnv,
|
|
20
|
+
} from "../helpers/plugins.js";
|
|
17
21
|
import {
|
|
18
22
|
adjustLicenseInformation,
|
|
23
|
+
attachIdentityTools,
|
|
19
24
|
collectExecutables,
|
|
20
25
|
collectSharedLibs,
|
|
21
26
|
DEBUG_MODE,
|
|
22
|
-
dirNameStr,
|
|
23
27
|
extractPathEnv,
|
|
28
|
+
extractToolRefs,
|
|
24
29
|
findLicenseId,
|
|
25
30
|
getTmpDir,
|
|
31
|
+
hasDangerousUnicode,
|
|
26
32
|
isDryRun,
|
|
27
33
|
isSpdxLicenseExpression,
|
|
34
|
+
isValidDriveRoot,
|
|
28
35
|
multiChecksumFile,
|
|
29
36
|
recordActivity,
|
|
30
|
-
|
|
37
|
+
recordSymlinkResolution,
|
|
31
38
|
safeExistsSync,
|
|
32
39
|
safeMkdirSync,
|
|
33
40
|
safeMkdtempSync,
|
|
@@ -36,241 +43,398 @@ import {
|
|
|
36
43
|
} from "../helpers/utils.js";
|
|
37
44
|
import { getDirs } from "./containerutils.js";
|
|
38
45
|
|
|
39
|
-
const dirName = dirNameStr;
|
|
40
46
|
const isWin = _platform() === "win32";
|
|
41
47
|
const OS_PURL_TYPES = ["deb", "apk", "rpm", "alpm", "qpkg"];
|
|
48
|
+
const pluginRuntime = setPluginsPathEnv(resolveCdxgenPlugins());
|
|
49
|
+
const platform = pluginRuntime.platform;
|
|
50
|
+
const CDXGEN_PLUGINS_DIR = pluginRuntime.pluginsDir;
|
|
51
|
+
const PLUGIN_MANIFEST_FILE = pluginRuntime.pluginManifestFile;
|
|
52
|
+
let pluginManifest;
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Is there a non-empty local node_modules directory
|
|
92
|
-
if (
|
|
93
|
-
!CDXGEN_PLUGINS_DIR &&
|
|
94
|
-
safeExistsSync(
|
|
95
|
-
join(
|
|
96
|
-
dirName,
|
|
97
|
-
"node_modules",
|
|
98
|
-
"@cdxgen",
|
|
99
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
100
|
-
"plugins",
|
|
101
|
-
),
|
|
102
|
-
) &&
|
|
103
|
-
safeExistsSync(
|
|
104
|
-
join(
|
|
105
|
-
dirName,
|
|
106
|
-
"node_modules",
|
|
107
|
-
"@cdxgen",
|
|
108
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
109
|
-
"plugins",
|
|
110
|
-
"trivy",
|
|
111
|
-
),
|
|
112
|
-
)
|
|
113
|
-
) {
|
|
114
|
-
CDXGEN_PLUGINS_DIR = join(
|
|
115
|
-
dirName,
|
|
116
|
-
"node_modules",
|
|
117
|
-
"@cdxgen",
|
|
118
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
119
|
-
"plugins",
|
|
54
|
+
const MAX_PLUGIN_MANIFEST_BYTES = 1024 * 1024;
|
|
55
|
+
const MAX_PLUGIN_MANIFEST_PLUGINS = 32;
|
|
56
|
+
const MAX_PLUGIN_COMPONENT_HASHES = 16;
|
|
57
|
+
const MAX_PLUGIN_COMPONENT_REFERENCES = 32;
|
|
58
|
+
const MAX_PLUGIN_COMPONENT_PROPERTIES = 128;
|
|
59
|
+
const MAX_PLUGIN_COMPONENT_LICENSES = 8;
|
|
60
|
+
const MAX_PLUGIN_STRING_LENGTH = 4096;
|
|
61
|
+
const MAX_PLUGIN_LONG_STRING_LENGTH = 16384;
|
|
62
|
+
|
|
63
|
+
function sanitizeManifestString(value, maxLength = MAX_PLUGIN_STRING_LENGTH) {
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const trimmedValue = value.trim();
|
|
68
|
+
if (!trimmedValue || trimmedValue.length > maxLength) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return trimmedValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sanitizeManifestObjectList(values, limit, mapper) {
|
|
75
|
+
if (!Array.isArray(values) || !values.length) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const mappedValues = values
|
|
79
|
+
.slice(0, limit)
|
|
80
|
+
.map((value) => mapper(value))
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
return mappedValues.length ? mappedValues : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sanitizeManifestHash(hash) {
|
|
86
|
+
const alg = sanitizeManifestString(hash?.alg, 64);
|
|
87
|
+
const content = sanitizeManifestString(hash?.content, 512);
|
|
88
|
+
if (!alg || !content) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return { alg, content };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sanitizeManifestProperty(property) {
|
|
95
|
+
const name = sanitizeManifestString(property?.name, 256);
|
|
96
|
+
const value = sanitizeManifestString(
|
|
97
|
+
property?.value,
|
|
98
|
+
MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
120
99
|
);
|
|
121
|
-
if (
|
|
122
|
-
|
|
100
|
+
if (!name || !value) {
|
|
101
|
+
return undefined;
|
|
123
102
|
}
|
|
103
|
+
return { name, value };
|
|
124
104
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
105
|
+
|
|
106
|
+
function sanitizeManifestExternalReference(reference) {
|
|
107
|
+
const type = sanitizeManifestString(reference?.type, 64);
|
|
108
|
+
const url = sanitizeManifestString(
|
|
109
|
+
reference?.url,
|
|
110
|
+
MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
111
|
+
);
|
|
112
|
+
if (!type || !url) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const sanitizedReference = { type, url };
|
|
116
|
+
const comment = sanitizeManifestString(reference?.comment, 512);
|
|
117
|
+
if (comment) {
|
|
118
|
+
sanitizedReference.comment = comment;
|
|
119
|
+
}
|
|
120
|
+
return sanitizedReference;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizeManifestLicense(licenseEntry) {
|
|
124
|
+
const licenseId = sanitizeManifestString(licenseEntry?.license?.id, 128);
|
|
125
|
+
const licenseName = sanitizeManifestString(licenseEntry?.license?.name, 256);
|
|
126
|
+
const licenseUrl = sanitizeManifestString(
|
|
127
|
+
licenseEntry?.license?.url,
|
|
128
|
+
MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
129
|
+
);
|
|
130
|
+
if (!licenseId && !licenseName) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const license = {};
|
|
134
|
+
if (licenseId) {
|
|
135
|
+
license.id = licenseId;
|
|
136
|
+
}
|
|
137
|
+
if (licenseName) {
|
|
138
|
+
license.name = licenseName;
|
|
139
|
+
}
|
|
140
|
+
if (licenseUrl) {
|
|
141
|
+
license.url = licenseUrl;
|
|
142
|
+
}
|
|
143
|
+
return { license };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sanitizeManifestComponent(component, fallbackName) {
|
|
147
|
+
if (!component || typeof component !== "object") {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const sanitizedComponent = {};
|
|
151
|
+
const stringFields = {
|
|
152
|
+
group: 256,
|
|
153
|
+
name: 256,
|
|
154
|
+
version: 256,
|
|
155
|
+
description: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
156
|
+
publisher: 256,
|
|
157
|
+
purl: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
158
|
+
"bom-ref": MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
159
|
+
type: 64,
|
|
160
|
+
};
|
|
161
|
+
for (const [field, maxLength] of Object.entries(stringFields)) {
|
|
162
|
+
const sanitizedValue = sanitizeManifestString(component[field], maxLength);
|
|
163
|
+
if (sanitizedValue) {
|
|
164
|
+
sanitizedComponent[field] = sanitizedValue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!sanitizedComponent.name && fallbackName) {
|
|
168
|
+
sanitizedComponent.name = fallbackName;
|
|
169
|
+
}
|
|
170
|
+
if (!sanitizedComponent.name || !sanitizedComponent["bom-ref"]) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const hashes = sanitizeManifestObjectList(
|
|
174
|
+
component.hashes,
|
|
175
|
+
MAX_PLUGIN_COMPONENT_HASHES,
|
|
176
|
+
sanitizeManifestHash,
|
|
177
|
+
);
|
|
178
|
+
const externalReferences = sanitizeManifestObjectList(
|
|
179
|
+
component.externalReferences,
|
|
180
|
+
MAX_PLUGIN_COMPONENT_REFERENCES,
|
|
181
|
+
sanitizeManifestExternalReference,
|
|
182
|
+
);
|
|
183
|
+
const properties = sanitizeManifestObjectList(
|
|
184
|
+
component.properties,
|
|
185
|
+
MAX_PLUGIN_COMPONENT_PROPERTIES,
|
|
186
|
+
sanitizeManifestProperty,
|
|
187
|
+
);
|
|
188
|
+
const licenses = sanitizeManifestObjectList(
|
|
189
|
+
component.licenses,
|
|
190
|
+
MAX_PLUGIN_COMPONENT_LICENSES,
|
|
191
|
+
sanitizeManifestLicense,
|
|
192
|
+
);
|
|
193
|
+
if (hashes) {
|
|
194
|
+
sanitizedComponent.hashes = hashes;
|
|
195
|
+
}
|
|
196
|
+
if (externalReferences) {
|
|
197
|
+
sanitizedComponent.externalReferences = externalReferences;
|
|
198
|
+
}
|
|
199
|
+
if (properties) {
|
|
200
|
+
sanitizedComponent.properties = properties;
|
|
201
|
+
}
|
|
202
|
+
if (licenses) {
|
|
203
|
+
sanitizedComponent.licenses = licenses;
|
|
204
|
+
}
|
|
205
|
+
return sanitizedComponent;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sanitizePluginManifest(manifest) {
|
|
209
|
+
if (!manifest || typeof manifest !== "object") {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const sanitizedManifest = {
|
|
213
|
+
plugins: [],
|
|
214
|
+
};
|
|
215
|
+
const generatedAt = sanitizeManifestString(manifest.generatedAt, 128);
|
|
216
|
+
if (generatedAt) {
|
|
217
|
+
sanitizedManifest.generatedAt = generatedAt;
|
|
218
|
+
}
|
|
219
|
+
if (manifest.package && typeof manifest.package === "object") {
|
|
220
|
+
const sanitizedPackage = {};
|
|
221
|
+
for (const [field, maxLength] of Object.entries({
|
|
222
|
+
name: 256,
|
|
223
|
+
version: 256,
|
|
224
|
+
repository: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
225
|
+
homepage: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
226
|
+
})) {
|
|
227
|
+
const sanitizedValue = sanitizeManifestString(
|
|
228
|
+
manifest.package[field],
|
|
229
|
+
maxLength,
|
|
131
230
|
);
|
|
231
|
+
if (sanitizedValue) {
|
|
232
|
+
sanitizedPackage[field] = sanitizedValue;
|
|
233
|
+
}
|
|
132
234
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
235
|
+
if (Object.keys(sanitizedPackage).length) {
|
|
236
|
+
sanitizedManifest.package = sanitizedPackage;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const pluginEntry of Array.isArray(manifest.plugins)
|
|
240
|
+
? manifest.plugins.slice(0, MAX_PLUGIN_MANIFEST_PLUGINS)
|
|
241
|
+
: []) {
|
|
242
|
+
const name = sanitizeManifestString(pluginEntry?.name, 128);
|
|
243
|
+
const component = sanitizeManifestComponent(pluginEntry?.component, name);
|
|
244
|
+
if (!name || !component) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const sanitizedPlugin = { name, component };
|
|
248
|
+
for (const [field, maxLength] of Object.entries({
|
|
249
|
+
version: 256,
|
|
250
|
+
binaryPath: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
251
|
+
sbomFile: MAX_PLUGIN_LONG_STRING_LENGTH,
|
|
252
|
+
sha256: 256,
|
|
253
|
+
})) {
|
|
254
|
+
const sanitizedValue = sanitizeManifestString(
|
|
255
|
+
pluginEntry?.[field],
|
|
256
|
+
maxLength,
|
|
257
|
+
);
|
|
258
|
+
if (sanitizedValue) {
|
|
259
|
+
sanitizedPlugin[field] = sanitizedValue;
|
|
138
260
|
}
|
|
139
261
|
}
|
|
262
|
+
sanitizedManifest.plugins.push(sanitizedPlugin);
|
|
140
263
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"plugins",
|
|
148
|
-
);
|
|
149
|
-
extraNMBinPath = join(
|
|
150
|
-
globalNodePath,
|
|
151
|
-
"..",
|
|
152
|
-
".pnpm",
|
|
153
|
-
"node_modules",
|
|
154
|
-
".bin",
|
|
155
|
-
);
|
|
264
|
+
return sanitizedManifest.plugins.length ? sanitizedManifest : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function loadPluginManifest() {
|
|
268
|
+
if (pluginManifest !== undefined) {
|
|
269
|
+
return pluginManifest;
|
|
156
270
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const tmpA = dirName.split(join("node_modules", ".pnpm"));
|
|
161
|
-
altGlobalPlugins = join(
|
|
162
|
-
tmpA[0],
|
|
163
|
-
"node_modules",
|
|
164
|
-
".pnpm",
|
|
165
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
166
|
-
"node_modules",
|
|
167
|
-
"@cdxgen",
|
|
168
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
169
|
-
"plugins",
|
|
170
|
-
);
|
|
171
|
-
if (safeExistsSync(join(tmpA[0], "node_modules", ".bin"))) {
|
|
172
|
-
extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
|
|
173
|
-
}
|
|
174
|
-
} else if (dirName.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
|
|
175
|
-
// pnpm dlx
|
|
176
|
-
const tmpA = dirName.split(".pnpm");
|
|
177
|
-
altGlobalPlugins = join(
|
|
178
|
-
tmpA[0],
|
|
179
|
-
".pnpm",
|
|
180
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
181
|
-
"node_modules",
|
|
182
|
-
"@cdxgen",
|
|
183
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
184
|
-
"plugins",
|
|
185
|
-
);
|
|
186
|
-
if (safeExistsSync(join(tmpA[0], ".bin"))) {
|
|
187
|
-
extraNMBinPath = join(tmpA[0], ".bin");
|
|
188
|
-
}
|
|
189
|
-
} else if (dirName.includes(join("caxa", "applications"))) {
|
|
190
|
-
// sae binaries
|
|
191
|
-
altGlobalPlugins = join(
|
|
192
|
-
dirName,
|
|
193
|
-
"node_modules",
|
|
194
|
-
"pnpm",
|
|
195
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
196
|
-
"node_modules",
|
|
197
|
-
"@cdxgen",
|
|
198
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
199
|
-
"plugins",
|
|
200
|
-
);
|
|
201
|
-
extraNMBinPath = join(dirName, "node_modules", ".bin");
|
|
271
|
+
if (!PLUGIN_MANIFEST_FILE) {
|
|
272
|
+
pluginManifest = null;
|
|
273
|
+
return pluginManifest;
|
|
202
274
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
275
|
+
try {
|
|
276
|
+
const manifestRealPath = realpathSync(PLUGIN_MANIFEST_FILE);
|
|
277
|
+
const manifestDirectory = dirname(manifestRealPath);
|
|
278
|
+
const expectedManifestDirectory = realpathSync(CDXGEN_PLUGINS_DIR);
|
|
279
|
+
const manifestStats = statSync(manifestRealPath);
|
|
280
|
+
if (
|
|
281
|
+
basename(manifestRealPath) !== "plugins-manifest.json" ||
|
|
282
|
+
manifestDirectory !== expectedManifestDirectory ||
|
|
283
|
+
!manifestStats.isFile() ||
|
|
284
|
+
manifestStats.size > MAX_PLUGIN_MANIFEST_BYTES
|
|
285
|
+
) {
|
|
286
|
+
pluginManifest = null;
|
|
287
|
+
return pluginManifest;
|
|
214
288
|
}
|
|
289
|
+
pluginManifest = sanitizePluginManifest(
|
|
290
|
+
JSON.parse(readFileSync(manifestRealPath, { encoding: "utf-8" })),
|
|
291
|
+
);
|
|
292
|
+
} catch (_err) {
|
|
293
|
+
pluginManifest = null;
|
|
215
294
|
}
|
|
295
|
+
return pluginManifest;
|
|
216
296
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
297
|
+
|
|
298
|
+
function cloneSerializable(value) {
|
|
299
|
+
if (!value || typeof value !== "object") {
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
return JSON.parse(JSON.stringify(value));
|
|
220
303
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
304
|
+
|
|
305
|
+
function getPluginManifestEntry(toolName) {
|
|
306
|
+
const manifest = loadPluginManifest();
|
|
307
|
+
if (!manifest?.plugins?.length) {
|
|
308
|
+
return undefined;
|
|
226
309
|
}
|
|
227
|
-
|
|
310
|
+
return manifest.plugins.find((entry) => entry?.name === toolName);
|
|
228
311
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
312
|
+
|
|
313
|
+
function mergeToolComponent(manifestComponent, existingComponent) {
|
|
314
|
+
if (!manifestComponent) {
|
|
315
|
+
return cloneSerializable(existingComponent);
|
|
316
|
+
}
|
|
317
|
+
if (!existingComponent) {
|
|
318
|
+
return cloneSerializable(manifestComponent);
|
|
319
|
+
}
|
|
320
|
+
const merged = {
|
|
321
|
+
...cloneSerializable(manifestComponent),
|
|
322
|
+
...cloneSerializable(existingComponent),
|
|
323
|
+
};
|
|
324
|
+
for (const key of [
|
|
325
|
+
"group",
|
|
326
|
+
"name",
|
|
327
|
+
"version",
|
|
328
|
+
"description",
|
|
329
|
+
"publisher",
|
|
330
|
+
"purl",
|
|
331
|
+
"bom-ref",
|
|
332
|
+
"type",
|
|
333
|
+
]) {
|
|
334
|
+
if (manifestComponent?.[key]) {
|
|
335
|
+
merged[key] = manifestComponent[key];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
merged.hashes = manifestComponent?.hashes?.length
|
|
339
|
+
? manifestComponent.hashes
|
|
340
|
+
: existingComponent?.hashes;
|
|
341
|
+
merged.externalReferences = uniqueExternalReferences(
|
|
342
|
+
(manifestComponent?.externalReferences || []).concat(
|
|
343
|
+
existingComponent?.externalReferences || [],
|
|
344
|
+
),
|
|
235
345
|
);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
CDXGEN_PLUGINS_DIR,
|
|
241
|
-
"cargo-auditable",
|
|
242
|
-
`cargo-auditable-cdxgen-${platform}-${arch}${extn}`,
|
|
346
|
+
merged.properties = uniqueProperties(
|
|
347
|
+
(manifestComponent?.properties || []).concat(
|
|
348
|
+
existingComponent?.properties || [],
|
|
349
|
+
),
|
|
243
350
|
);
|
|
351
|
+
merged.licenses =
|
|
352
|
+
manifestComponent?.licenses?.length || !existingComponent?.licenses?.length
|
|
353
|
+
? manifestComponent?.licenses
|
|
354
|
+
: existingComponent.licenses;
|
|
355
|
+
merged.evidence = manifestComponent?.evidence || existingComponent?.evidence;
|
|
356
|
+
return merged;
|
|
244
357
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
358
|
+
|
|
359
|
+
function uniqueExternalReferences(references) {
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
const uniqueValues = [];
|
|
362
|
+
for (const reference of references || []) {
|
|
363
|
+
if (!reference?.url || !reference?.type) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const key = `${reference.type}\u0000${reference.url}`;
|
|
367
|
+
if (seen.has(key)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
seen.add(key);
|
|
371
|
+
uniqueValues.push(reference);
|
|
255
372
|
}
|
|
373
|
+
return uniqueValues;
|
|
256
374
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
375
|
+
|
|
376
|
+
export function getPluginToolComponents(toolNames = []) {
|
|
377
|
+
const components = [];
|
|
378
|
+
const seenRefs = new Set();
|
|
379
|
+
for (const toolName of uniqueSortedStrings(toolNames)) {
|
|
380
|
+
const component = cloneSerializable(
|
|
381
|
+
getPluginManifestEntry(toolName)?.component,
|
|
382
|
+
);
|
|
383
|
+
if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
seenRefs.add(component["bom-ref"]);
|
|
387
|
+
components.push(component);
|
|
388
|
+
}
|
|
389
|
+
return components;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function enrichToolComponents(existingTools = [], toolNames = []) {
|
|
393
|
+
const manifestTools = getPluginToolComponents(toolNames);
|
|
394
|
+
if (!existingTools?.length) {
|
|
395
|
+
return manifestTools;
|
|
396
|
+
}
|
|
397
|
+
const toolMap = new Map();
|
|
398
|
+
for (const tool of existingTools) {
|
|
399
|
+
if (!tool) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
toolMap.set(tool["bom-ref"] || tool.name || JSON.stringify(tool), tool);
|
|
403
|
+
}
|
|
404
|
+
for (const manifestTool of manifestTools) {
|
|
405
|
+
const matchKey = Array.from(toolMap.keys()).find((key) => {
|
|
406
|
+
const existing = toolMap.get(key);
|
|
407
|
+
return (
|
|
408
|
+
existing?.["bom-ref"] === manifestTool["bom-ref"] ||
|
|
409
|
+
existing?.name === manifestTool.name
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
if (matchKey) {
|
|
413
|
+
toolMap.set(
|
|
414
|
+
matchKey,
|
|
415
|
+
mergeToolComponent(manifestTool, toolMap.get(matchKey)),
|
|
416
|
+
);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
toolMap.set(manifestTool["bom-ref"], manifestTool);
|
|
420
|
+
}
|
|
421
|
+
return Array.from(toolMap.values());
|
|
264
422
|
}
|
|
265
423
|
|
|
424
|
+
const TRIVY_BIN = resolvePluginBinary("trivy", pluginRuntime);
|
|
425
|
+
const CARGO_AUDITABLE_BIN = resolvePluginBinary(
|
|
426
|
+
"cargo-auditable",
|
|
427
|
+
pluginRuntime,
|
|
428
|
+
);
|
|
429
|
+
const OSQUERY_BIN = resolvePluginBinary("osquery", pluginRuntime);
|
|
430
|
+
const DOSAI_BIN = resolvePluginBinary("dosai", pluginRuntime);
|
|
431
|
+
const TRUSTINSPECTOR_BIN = resolvePluginBinary("trustinspector", pluginRuntime);
|
|
432
|
+
|
|
266
433
|
// Blint bin
|
|
267
434
|
const BLINT_BIN = process.env.BLINT_CMD || "blint";
|
|
268
435
|
|
|
269
436
|
// sourcekitten
|
|
270
|
-
|
|
271
|
-
if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
|
|
272
|
-
SOURCEKITTEN_BIN = join(CDXGEN_PLUGINS_DIR, "sourcekitten", "sourcekitten");
|
|
273
|
-
}
|
|
437
|
+
const SOURCEKITTEN_BIN = resolvePluginBinary("sourcekitten", pluginRuntime);
|
|
274
438
|
|
|
275
439
|
// Keep this list updated every year
|
|
276
440
|
const OS_DISTRO_ALIAS = {
|
|
@@ -464,14 +628,21 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
464
628
|
dependenciesList: [],
|
|
465
629
|
executables: [],
|
|
466
630
|
osPackages: [],
|
|
631
|
+
osPackageFiles: [],
|
|
467
632
|
sharedLibs: [],
|
|
633
|
+
services: [],
|
|
634
|
+
tools: [],
|
|
468
635
|
};
|
|
469
636
|
}
|
|
470
637
|
const pkgList = [];
|
|
638
|
+
const osPackageEntries = [];
|
|
471
639
|
const dependenciesList = [];
|
|
472
640
|
const allTypes = new Set();
|
|
473
641
|
const bundledSdks = new Set();
|
|
474
642
|
const bundledRuntimes = new Set();
|
|
643
|
+
let osPackageFiles = [];
|
|
644
|
+
let services = [];
|
|
645
|
+
let tools = [];
|
|
475
646
|
let binPaths = extractPathEnv(imageConfig?.Env);
|
|
476
647
|
if (!binPaths?.length) {
|
|
477
648
|
const rootBinPaths = getDirs(src, "{sbin,bin}", true, false);
|
|
@@ -511,25 +682,13 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
511
682
|
const bomJsonFile = join(tempDir, "trivy-bom.json");
|
|
512
683
|
const args = [
|
|
513
684
|
imageType,
|
|
514
|
-
"--skip-db-update",
|
|
515
|
-
"--skip-java-db-update",
|
|
516
|
-
"--offline-scan",
|
|
517
|
-
"--disable-telemetry",
|
|
518
|
-
"--skip-version-check",
|
|
519
|
-
"--skip-files",
|
|
520
|
-
"**/*.jar,**/*.war,**/*.par,**/*.ear",
|
|
521
|
-
"--no-progress",
|
|
522
|
-
"--exit-code",
|
|
523
|
-
"0",
|
|
524
|
-
"--format",
|
|
525
|
-
"cyclonedx",
|
|
526
685
|
"--cache-dir",
|
|
527
686
|
trivyCacheDir,
|
|
528
687
|
"--output",
|
|
529
688
|
bomJsonFile,
|
|
530
689
|
];
|
|
531
|
-
if (
|
|
532
|
-
args.push("
|
|
690
|
+
if (DEBUG_MODE) {
|
|
691
|
+
args.push("--debug");
|
|
533
692
|
}
|
|
534
693
|
args.push(src);
|
|
535
694
|
if (DEBUG_MODE) {
|
|
@@ -633,6 +792,17 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
633
792
|
}
|
|
634
793
|
}
|
|
635
794
|
const tmpDependencies = {};
|
|
795
|
+
tools = enrichToolComponents(
|
|
796
|
+
(Array.isArray(tmpBom?.metadata?.tools)
|
|
797
|
+
? tmpBom.metadata.tools
|
|
798
|
+
: tmpBom?.metadata?.tools?.components || []
|
|
799
|
+
).filter((tool) => tool?.["bom-ref"] && tool?.name !== "cdxgen"),
|
|
800
|
+
["trivy"].concat(imageType === "rootfs" ? ["trustinspector"] : []),
|
|
801
|
+
);
|
|
802
|
+
const toolRefs = extractToolRefs(
|
|
803
|
+
{ components: tools },
|
|
804
|
+
(tool) => tool?.name !== "cdxgen",
|
|
805
|
+
);
|
|
636
806
|
(tmpBom.dependencies || []).forEach((d) => {
|
|
637
807
|
tmpDependencies[d.ref] = d.dependsOn;
|
|
638
808
|
});
|
|
@@ -781,6 +951,7 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
781
951
|
console.log(err);
|
|
782
952
|
}
|
|
783
953
|
}
|
|
954
|
+
attachIdentityTools(comp, toolRefs);
|
|
784
955
|
// Fix licenses
|
|
785
956
|
if (
|
|
786
957
|
comp.licenses &&
|
|
@@ -824,30 +995,12 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
824
995
|
}
|
|
825
996
|
}
|
|
826
997
|
const compProperties = comp.properties;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// Property name: aquasecurity:trivy:SrcName
|
|
834
|
-
if (aprop.name.endsWith("SrcName")) {
|
|
835
|
-
srcName = aprop.value;
|
|
836
|
-
}
|
|
837
|
-
// Property name: aquasecurity:trivy:SrcVersion
|
|
838
|
-
if (aprop.name.endsWith("SrcVersion")) {
|
|
839
|
-
srcVersion = aprop.value;
|
|
840
|
-
}
|
|
841
|
-
// Property name: aquasecurity:trivy:SrcRelease
|
|
842
|
-
if (aprop.name.endsWith("SrcRelease")) {
|
|
843
|
-
srcRelease = aprop.value;
|
|
844
|
-
}
|
|
845
|
-
// Property name: aquasecurity:trivy:SrcEpoch
|
|
846
|
-
if (aprop.name.endsWith("SrcEpoch")) {
|
|
847
|
-
epoch = aprop.value;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
998
|
+
const trivyMetadata = extractTrivyOsPackageMetadata(compProperties);
|
|
999
|
+
const fallbackIdentityProperties = promoteTrivyOsPackageIdentity(
|
|
1000
|
+
comp,
|
|
1001
|
+
trivyMetadata,
|
|
1002
|
+
);
|
|
1003
|
+
let { srcName, srcVersion, srcRelease, epoch } = trivyMetadata;
|
|
851
1004
|
// See issue #2067
|
|
852
1005
|
if (srcVersion && srcRelease) {
|
|
853
1006
|
srcVersion = `${srcVersion}-${srcRelease}`;
|
|
@@ -855,7 +1008,18 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
855
1008
|
if (epoch) {
|
|
856
1009
|
srcVersion = `${epoch}:${srcVersion}`;
|
|
857
1010
|
}
|
|
858
|
-
|
|
1011
|
+
if (
|
|
1012
|
+
trivyMetadata.retainedProperties.length ||
|
|
1013
|
+
fallbackIdentityProperties.length
|
|
1014
|
+
) {
|
|
1015
|
+
comp.properties = uniqueProperties(
|
|
1016
|
+
trivyMetadata.retainedProperties.concat(
|
|
1017
|
+
fallbackIdentityProperties,
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
} else {
|
|
1021
|
+
delete comp.properties;
|
|
1022
|
+
}
|
|
859
1023
|
// Bug fix: We can get bom-ref like this: pkg:rpm/sles/libstdc%2B%2B6@14.2.0+git10526-150000.1.6.1?arch=x86_64&distro=sles-15.5
|
|
860
1024
|
if (
|
|
861
1025
|
comp["bom-ref"] &&
|
|
@@ -865,6 +1029,17 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
865
1029
|
comp["bom-ref"] = decodeURIComponent(comp.purl);
|
|
866
1030
|
}
|
|
867
1031
|
pkgList.push(comp);
|
|
1032
|
+
if (trivyMetadata.installedFiles.length) {
|
|
1033
|
+
osPackageEntries.push({
|
|
1034
|
+
capabilities: trivyMetadata.capabilities,
|
|
1035
|
+
commandPaths: trivyMetadata.installedCommandPaths,
|
|
1036
|
+
commands: trivyMetadata.installedCommands,
|
|
1037
|
+
files: trivyMetadata.installedFiles,
|
|
1038
|
+
packageName: comp.name,
|
|
1039
|
+
packageRef: comp["bom-ref"],
|
|
1040
|
+
packageVersion: comp.version,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
868
1043
|
detectSdksRuntimes(comp, bundledSdks, bundledRuntimes);
|
|
869
1044
|
const compDeps = retrieveDependencies(
|
|
870
1045
|
tmpDependencies,
|
|
@@ -916,6 +1091,8 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
916
1091
|
).toString();
|
|
917
1092
|
}
|
|
918
1093
|
newComp["bom-ref"] = decodeURIComponent(newComp.purl);
|
|
1094
|
+
delete newComp.properties;
|
|
1095
|
+
attachIdentityTools(newComp, toolRefs);
|
|
919
1096
|
pkgList.push(newComp);
|
|
920
1097
|
detectSdksRuntimes(newComp, bundledSdks, bundledRuntimes);
|
|
921
1098
|
}
|
|
@@ -924,11 +1101,33 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
924
1101
|
}
|
|
925
1102
|
}
|
|
926
1103
|
}
|
|
1104
|
+
const rootfsRepositoryInventory = await collectRootfsRepositoryInventory(src);
|
|
1105
|
+
if (rootfsRepositoryInventory.components.length) {
|
|
1106
|
+
pkgList.push(...rootfsRepositoryInventory.components);
|
|
1107
|
+
}
|
|
1108
|
+
if (rootfsRepositoryInventory.dependenciesList.length) {
|
|
1109
|
+
dependenciesList.push(...rootfsRepositoryInventory.dependenciesList);
|
|
1110
|
+
}
|
|
1111
|
+
const {
|
|
1112
|
+
components: ownedFileComponents,
|
|
1113
|
+
dependenciesList: ownedFileDependencies,
|
|
1114
|
+
ownedFilePaths,
|
|
1115
|
+
services: ownedServices,
|
|
1116
|
+
} = await createOSPackageFileComponents(src, osPackageEntries);
|
|
1117
|
+
if (ownedFileComponents.length) {
|
|
1118
|
+
osPackageFiles = ownedFileComponents;
|
|
1119
|
+
}
|
|
1120
|
+
if (ownedFileDependencies.length) {
|
|
1121
|
+
dependenciesList.push(...ownedFileDependencies);
|
|
1122
|
+
}
|
|
1123
|
+
if (ownedServices.length) {
|
|
1124
|
+
services = ownedServices;
|
|
1125
|
+
}
|
|
927
1126
|
let executables = [];
|
|
928
1127
|
if (binPaths?.length) {
|
|
929
1128
|
executables = await fileComponents(
|
|
930
1129
|
src,
|
|
931
|
-
collectExecutables(src, binPaths),
|
|
1130
|
+
collectExecutables(src, binPaths, ownedFilePaths),
|
|
932
1131
|
"executable",
|
|
933
1132
|
);
|
|
934
1133
|
}
|
|
@@ -956,11 +1155,13 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
956
1155
|
defaultLibPaths,
|
|
957
1156
|
"/etc/ld.so.conf",
|
|
958
1157
|
"/etc/ld.so.conf.d/*.conf",
|
|
1158
|
+
ownedFilePaths,
|
|
959
1159
|
),
|
|
960
1160
|
"shared_library",
|
|
961
1161
|
);
|
|
962
1162
|
return {
|
|
963
1163
|
osPackages: pkgList,
|
|
1164
|
+
osPackageFiles,
|
|
964
1165
|
dependenciesList,
|
|
965
1166
|
allTypes: Array.from(allTypes).sort(),
|
|
966
1167
|
bundledSdks: Array.from(bundledSdks).sort(),
|
|
@@ -968,112 +1169,1857 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
968
1169
|
binPaths,
|
|
969
1170
|
executables,
|
|
970
1171
|
sharedLibs,
|
|
1172
|
+
services,
|
|
1173
|
+
tools,
|
|
971
1174
|
};
|
|
972
1175
|
}
|
|
973
1176
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1177
|
+
function extractTrivyOsPackageMetadata(compProperties) {
|
|
1178
|
+
const metadata = {
|
|
1179
|
+
capabilities: [],
|
|
1180
|
+
installedCommandPaths: [],
|
|
1181
|
+
installedCommands: [],
|
|
1182
|
+
installedFiles: [],
|
|
1183
|
+
packageMaintainer: undefined,
|
|
1184
|
+
packageVendor: undefined,
|
|
1185
|
+
retainedProperties: [],
|
|
1186
|
+
srcName: undefined,
|
|
1187
|
+
srcRelease: undefined,
|
|
1188
|
+
srcVersion: undefined,
|
|
1189
|
+
epoch: undefined,
|
|
1190
|
+
};
|
|
1191
|
+
if (!Array.isArray(compProperties) || !compProperties.length) {
|
|
1192
|
+
return metadata;
|
|
978
1193
|
}
|
|
979
|
-
|
|
980
|
-
|
|
1194
|
+
for (const aprop of compProperties) {
|
|
1195
|
+
if (!aprop?.name) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (aprop.name.endsWith("SrcName")) {
|
|
1199
|
+
metadata.srcName = aprop.value;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (aprop.name.endsWith("SrcVersion")) {
|
|
1203
|
+
metadata.srcVersion = aprop.value;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (aprop.name.endsWith("SrcRelease")) {
|
|
1207
|
+
metadata.srcRelease = aprop.value;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (aprop.name.endsWith("SrcEpoch")) {
|
|
1211
|
+
metadata.epoch = aprop.value;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (aprop.name.endsWith("PackageMaintainer")) {
|
|
1215
|
+
metadata.packageMaintainer = aprop.value;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (aprop.name.endsWith("PackageVendor")) {
|
|
1219
|
+
metadata.packageVendor = aprop.value;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (aprop.name.endsWith("InstalledFile")) {
|
|
1223
|
+
metadata.installedFiles.push(aprop.value);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (aprop.name.endsWith("InstalledCommandPath")) {
|
|
1227
|
+
metadata.installedCommandPaths.push(aprop.value);
|
|
1228
|
+
metadata.retainedProperties.push(aprop);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (aprop.name.endsWith("InstalledCommand")) {
|
|
1232
|
+
metadata.installedCommands.push(aprop.value);
|
|
1233
|
+
metadata.retainedProperties.push(aprop);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (aprop.name.endsWith("Capability")) {
|
|
1237
|
+
metadata.capabilities.push(aprop.value);
|
|
1238
|
+
metadata.retainedProperties.push(aprop);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (
|
|
1242
|
+
aprop.name.endsWith("CapabilityCount") ||
|
|
1243
|
+
aprop.name.endsWith("InstalledFileCount") ||
|
|
1244
|
+
aprop.name.endsWith("InstalledCommandCount")
|
|
1245
|
+
) {
|
|
1246
|
+
metadata.retainedProperties.push(aprop);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
metadata.retainedProperties.push(aprop);
|
|
981
1250
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
)
|
|
987
|
-
|
|
1251
|
+
metadata.capabilities = uniqueSortedStrings(metadata.capabilities);
|
|
1252
|
+
metadata.installedCommandPaths = uniqueSortedStrings(
|
|
1253
|
+
metadata.installedCommandPaths,
|
|
1254
|
+
);
|
|
1255
|
+
metadata.installedCommands = uniqueSortedStrings(metadata.installedCommands);
|
|
1256
|
+
metadata.installedFiles = uniqueSortedStrings(metadata.installedFiles);
|
|
1257
|
+
metadata.retainedProperties = uniqueProperties(metadata.retainedProperties);
|
|
1258
|
+
return metadata;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function getOrganizationalEntityName(entity) {
|
|
1262
|
+
if (!entity) {
|
|
1263
|
+
return undefined;
|
|
988
1264
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1265
|
+
if (typeof entity === "string") {
|
|
1266
|
+
return entity.trim() || undefined;
|
|
1267
|
+
}
|
|
1268
|
+
if (typeof entity === "object" && typeof entity.name === "string") {
|
|
1269
|
+
return entity.name.trim() || undefined;
|
|
992
1270
|
}
|
|
1271
|
+
return undefined;
|
|
993
1272
|
}
|
|
994
1273
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (compPurl.qualifiers.distro_name) {
|
|
1011
|
-
tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
|
|
1012
|
-
}
|
|
1013
|
-
if (compPurl.qualifiers.distro) {
|
|
1014
|
-
tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
if (tmpPurl.qualifiers) {
|
|
1019
|
-
if (
|
|
1020
|
-
tmpPurl.qualifiers.epoch &&
|
|
1021
|
-
!tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
|
|
1022
|
-
) {
|
|
1023
|
-
tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
// Prevents purls ending with ?
|
|
1027
|
-
if (!Object.keys(tmpPurl.qualifiers).length) {
|
|
1028
|
-
tmpPurl.qualifiers = undefined;
|
|
1029
|
-
}
|
|
1030
|
-
dependsOn.add(decodeURIComponent(tmpPurl.toString()));
|
|
1031
|
-
} catch (_e) {
|
|
1032
|
-
// ignore
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
|
|
1036
|
-
} catch (_e) {
|
|
1037
|
-
// ignore
|
|
1274
|
+
function sameOrganizationalEntity(entity, expectedName) {
|
|
1275
|
+
const currentName = getOrganizationalEntityName(entity);
|
|
1276
|
+
return Boolean(
|
|
1277
|
+
currentName &&
|
|
1278
|
+
expectedName &&
|
|
1279
|
+
currentName.localeCompare(expectedName, undefined, {
|
|
1280
|
+
sensitivity: "accent",
|
|
1281
|
+
}) === 0,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function mergeOrganizationalEntityField(component, fieldName, entityName) {
|
|
1286
|
+
const normalizedName = `${entityName || ""}`.trim();
|
|
1287
|
+
if (!normalizedName) {
|
|
1288
|
+
return { applied: false, represented: false };
|
|
1038
1289
|
}
|
|
1039
|
-
|
|
1040
|
-
};
|
|
1290
|
+
if (!component?.[fieldName]) {
|
|
1291
|
+
component[fieldName] = { name: normalizedName };
|
|
1292
|
+
return { applied: true, represented: true };
|
|
1293
|
+
}
|
|
1294
|
+
if (sameOrganizationalEntity(component[fieldName], normalizedName)) {
|
|
1295
|
+
return { applied: false, represented: true };
|
|
1296
|
+
}
|
|
1297
|
+
return { applied: false, represented: false };
|
|
1298
|
+
}
|
|
1041
1299
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
kind: "osquery",
|
|
1046
|
-
reason:
|
|
1047
|
-
"Dry run mode blocks osquery execution and reports the query instead.",
|
|
1048
|
-
status: "blocked",
|
|
1049
|
-
target: query,
|
|
1050
|
-
});
|
|
1300
|
+
function parseOrganizationalContact(value) {
|
|
1301
|
+
const normalizedValue = `${value || ""}`.trim();
|
|
1302
|
+
if (!normalizedValue) {
|
|
1051
1303
|
return undefined;
|
|
1052
1304
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1305
|
+
const match = normalizedValue.match(/^([^<>]+?)\s*<([^<>\s]+@[^<>\s]+)>$/);
|
|
1306
|
+
if (match) {
|
|
1307
|
+
return {
|
|
1308
|
+
name: match[1].trim(),
|
|
1309
|
+
email: match[2].trim(),
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
return { name: normalizedValue };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function sameOrganizationalContact(left, right) {
|
|
1316
|
+
if (!left || !right) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
const leftName = `${left.name || ""}`.trim();
|
|
1320
|
+
const rightName = `${right.name || ""}`.trim();
|
|
1321
|
+
const leftEmail = `${left.email || ""}`.trim().toLowerCase();
|
|
1322
|
+
const rightEmail = `${right.email || ""}`.trim().toLowerCase();
|
|
1323
|
+
return leftName === rightName && leftEmail === rightEmail;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function mergeAuthorsFromMaintainer(component, maintainerValue) {
|
|
1327
|
+
const authorContact = parseOrganizationalContact(maintainerValue);
|
|
1328
|
+
if (!authorContact?.name) {
|
|
1329
|
+
return { applied: false, represented: false };
|
|
1330
|
+
}
|
|
1331
|
+
if (!Array.isArray(component?.authors) || !component.authors.length) {
|
|
1332
|
+
component.authors = [authorContact];
|
|
1333
|
+
return { applied: true, represented: true };
|
|
1334
|
+
}
|
|
1335
|
+
if (
|
|
1336
|
+
component.authors.some((author) =>
|
|
1337
|
+
sameOrganizationalContact(author, authorContact),
|
|
1338
|
+
)
|
|
1339
|
+
) {
|
|
1340
|
+
return { applied: false, represented: true };
|
|
1341
|
+
}
|
|
1342
|
+
return { applied: false, represented: false };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function promoteTrivyOsPackageIdentity(component, trivyMetadata) {
|
|
1346
|
+
const fallbackProperties = [];
|
|
1347
|
+
const vendorValue = `${trivyMetadata?.packageVendor || ""}`.trim();
|
|
1348
|
+
const supplierName = getOrganizationalEntityName(component?.supplier);
|
|
1349
|
+
const maintainerValue = `${
|
|
1350
|
+
trivyMetadata?.packageMaintainer || supplierName || ""
|
|
1351
|
+
}`.trim();
|
|
1352
|
+
|
|
1353
|
+
const maintainerAuthorResult = mergeAuthorsFromMaintainer(
|
|
1354
|
+
component,
|
|
1355
|
+
maintainerValue,
|
|
1356
|
+
);
|
|
1357
|
+
const maintainerSupplierResult = mergeOrganizationalEntityField(
|
|
1358
|
+
component,
|
|
1359
|
+
"supplier",
|
|
1360
|
+
maintainerValue,
|
|
1361
|
+
);
|
|
1362
|
+
if (
|
|
1363
|
+
trivyMetadata?.packageMaintainer &&
|
|
1364
|
+
!maintainerAuthorResult.represented &&
|
|
1365
|
+
!maintainerSupplierResult.represented
|
|
1366
|
+
) {
|
|
1367
|
+
fallbackProperties.push({
|
|
1368
|
+
name: "PackageMaintainer",
|
|
1369
|
+
value: trivyMetadata.packageMaintainer,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const vendorSupplierResult = mergeOrganizationalEntityField(
|
|
1374
|
+
component,
|
|
1375
|
+
"supplier",
|
|
1376
|
+
vendorValue,
|
|
1377
|
+
);
|
|
1378
|
+
const vendorManufacturerResult = mergeOrganizationalEntityField(
|
|
1379
|
+
component,
|
|
1380
|
+
"manufacturer",
|
|
1381
|
+
vendorValue,
|
|
1382
|
+
);
|
|
1383
|
+
if (
|
|
1384
|
+
vendorValue &&
|
|
1385
|
+
!vendorSupplierResult.represented &&
|
|
1386
|
+
!vendorManufacturerResult.represented
|
|
1387
|
+
) {
|
|
1388
|
+
fallbackProperties.push({
|
|
1389
|
+
name: "PackageVendor",
|
|
1390
|
+
value: vendorValue,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
return fallbackProperties;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
async function collectRootfsRepositoryInventory(basePath) {
|
|
1397
|
+
let { components: trustedKeyComponents, refsByPath } =
|
|
1398
|
+
await collectTrustedKeyComponents(basePath);
|
|
1399
|
+
trustedKeyComponents = applyTrustMaterialEnhancements(
|
|
1400
|
+
trustedKeyComponents,
|
|
1401
|
+
collectTrustInspectorRootfsInventory(basePath),
|
|
1402
|
+
);
|
|
1403
|
+
refsByPath = new Map(
|
|
1404
|
+
trustedKeyComponents
|
|
1405
|
+
.map((component) => {
|
|
1406
|
+
const srcFile = (component.properties || []).find(
|
|
1407
|
+
(property) => property.name === "SrcFile",
|
|
1408
|
+
)?.value;
|
|
1409
|
+
return srcFile
|
|
1410
|
+
? [normalizeContainerPath(srcFile), component["bom-ref"]]
|
|
1411
|
+
: undefined;
|
|
1412
|
+
})
|
|
1413
|
+
.filter(Boolean),
|
|
1414
|
+
);
|
|
1415
|
+
const repositoryEntries = uniqueRepositoryEntries(
|
|
1416
|
+
parseAptRepositorySources(basePath).concat(
|
|
1417
|
+
parseYumRepositorySources(basePath),
|
|
1418
|
+
),
|
|
1419
|
+
);
|
|
1420
|
+
const components = [...trustedKeyComponents];
|
|
1421
|
+
const dependenciesList = [];
|
|
1422
|
+
const seenComponentRefs = new Set(components.map((comp) => comp["bom-ref"]));
|
|
1423
|
+
for (const entry of repositoryEntries) {
|
|
1424
|
+
const component = createRepositorySourceComponent(entry);
|
|
1425
|
+
if (seenComponentRefs.has(component["bom-ref"])) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
seenComponentRefs.add(component["bom-ref"]);
|
|
1429
|
+
components.push(component);
|
|
1430
|
+
const dependsOn = uniqueSortedStrings(
|
|
1431
|
+
(entry.keyReferences || [])
|
|
1432
|
+
.map((keyRef) => normalizeLocalRepositoryReference(keyRef))
|
|
1433
|
+
.map((keyRef) => refsByPath.get(keyRef))
|
|
1434
|
+
.filter(Boolean),
|
|
1435
|
+
);
|
|
1436
|
+
if (dependsOn.length) {
|
|
1437
|
+
dependenciesList.push({
|
|
1438
|
+
ref: component["bom-ref"],
|
|
1439
|
+
dependsOn,
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return { components, dependenciesList };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function collectTrustedKeyComponents(basePath) {
|
|
1447
|
+
const refsByPath = new Map();
|
|
1448
|
+
const components = [];
|
|
1449
|
+
for (const normalizedPath of collectTrustedKeyPaths(basePath)) {
|
|
1450
|
+
const component = await createTrustedKeyComponent(basePath, normalizedPath);
|
|
1451
|
+
if (!component?.["bom-ref"]) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
refsByPath.set(normalizedPath, component["bom-ref"]);
|
|
1455
|
+
components.push(component);
|
|
1456
|
+
}
|
|
1457
|
+
return { components, refsByPath };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function collectTrustedKeyPaths(basePath) {
|
|
1461
|
+
const results = new Set();
|
|
1462
|
+
for (const candidate of [
|
|
1463
|
+
"/etc/apt/trusted.gpg",
|
|
1464
|
+
"/etc/apt/trusted.gpg.d",
|
|
1465
|
+
"/usr/share/keyrings",
|
|
1466
|
+
"/etc/pki/rpm-gpg",
|
|
1467
|
+
"/usr/share/distribution-gpg-keys",
|
|
1468
|
+
"/etc/apk/keys",
|
|
1469
|
+
]) {
|
|
1470
|
+
const normalizedCandidate = normalizeContainerPath(candidate);
|
|
1471
|
+
const absoluteCandidate = join(
|
|
1472
|
+
basePath,
|
|
1473
|
+
normalizedCandidate.replace(/^\/+/, ""),
|
|
1474
|
+
);
|
|
1475
|
+
if (!safeExistsSync(absoluteCandidate)) {
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
const stats = statSync(absoluteCandidate, { throwIfNoEntry: false });
|
|
1479
|
+
if (!stats) {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (stats.isDirectory()) {
|
|
1483
|
+
for (const filePath of walkRootfsFiles(basePath, normalizedCandidate)) {
|
|
1484
|
+
if (isTrustedKeyPath(filePath)) {
|
|
1485
|
+
results.add(filePath);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (isTrustedKeyPath(normalizedCandidate)) {
|
|
1491
|
+
results.add(normalizedCandidate);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return Array.from(results).sort();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function walkRootfsFiles(basePath, normalizedDir) {
|
|
1498
|
+
const results = [];
|
|
1499
|
+
const absoluteDir = join(basePath, normalizedDir.replace(/^\/+/, ""));
|
|
1500
|
+
if (!safeExistsSync(absoluteDir)) {
|
|
1501
|
+
return results;
|
|
1502
|
+
}
|
|
1503
|
+
for (const entry of readdirSync(absoluteDir, { withFileTypes: true })) {
|
|
1504
|
+
const normalizedPath = normalizeContainerPath(
|
|
1505
|
+
`${normalizedDir.replace(/\/+$/, "")}/${entry.name}`,
|
|
1506
|
+
);
|
|
1507
|
+
if (entry.isDirectory()) {
|
|
1508
|
+
results.push(...walkRootfsFiles(basePath, normalizedPath));
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (entry.isFile()) {
|
|
1512
|
+
results.push(normalizedPath);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return results;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function isTrustedKeyPath(normalizedPath) {
|
|
1519
|
+
const lowerPath = normalizeContainerPath(normalizedPath)?.toLowerCase();
|
|
1520
|
+
if (!lowerPath) {
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
return (
|
|
1524
|
+
lowerPath === "/etc/apt/trusted.gpg" ||
|
|
1525
|
+
lowerPath.includes("/trusted.gpg.d/") ||
|
|
1526
|
+
lowerPath.includes("/keyrings/") ||
|
|
1527
|
+
lowerPath.includes("/rpm-gpg/") ||
|
|
1528
|
+
lowerPath.includes("/distribution-gpg-keys/") ||
|
|
1529
|
+
lowerPath.includes("/apk/keys/")
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async function createTrustedKeyComponent(basePath, normalizedPath) {
|
|
1534
|
+
const absolutePath = join(basePath, normalizedPath.replace(/^\/+/, ""));
|
|
1535
|
+
const stats = statSync(absolutePath, { throwIfNoEntry: false });
|
|
1536
|
+
if (!stats || stats.isDirectory()) {
|
|
1537
|
+
return undefined;
|
|
1538
|
+
}
|
|
1539
|
+
let hashValues = {};
|
|
1540
|
+
try {
|
|
1541
|
+
hashValues = await multiChecksumFile(["sha1", "sha256"], absolutePath);
|
|
1542
|
+
} catch (_err) {
|
|
1543
|
+
// ignore
|
|
1544
|
+
}
|
|
1545
|
+
const version = hashValues.sha256 || hashValues.sha1 || `${stats.mtimeMs}`;
|
|
1546
|
+
const hashes = [];
|
|
1547
|
+
if (hashValues.sha1) {
|
|
1548
|
+
hashes.push({ alg: "SHA-1", content: hashValues.sha1 });
|
|
1549
|
+
}
|
|
1550
|
+
if (hashValues.sha256) {
|
|
1551
|
+
hashes.push({ alg: "SHA-256", content: hashValues.sha256 });
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${hashValues.sha256 ? `sha256:${hashValues.sha256}` : version}`,
|
|
1555
|
+
name: basename(normalizedPath),
|
|
1556
|
+
type: "cryptographic-asset",
|
|
1557
|
+
version,
|
|
1558
|
+
hashes,
|
|
1559
|
+
cryptoProperties: {
|
|
1560
|
+
assetType: "related-crypto-material",
|
|
1561
|
+
relatedCryptoMaterialProperties: {
|
|
1562
|
+
type: "public-key",
|
|
1563
|
+
id: hashValues.sha256 || hashValues.sha1 || normalizedPath,
|
|
1564
|
+
state: "active",
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
properties: uniqueProperties([
|
|
1568
|
+
{ name: "SrcFile", value: normalizedPath },
|
|
1569
|
+
{
|
|
1570
|
+
name: "cdx:crypto:trustDomain",
|
|
1571
|
+
value: deriveTrustedKeyDomain(normalizedPath),
|
|
1572
|
+
},
|
|
1573
|
+
{ name: "cdx:crypto:keyPath", value: normalizedPath },
|
|
1574
|
+
{
|
|
1575
|
+
name: "cdx:crypto:fileExtension",
|
|
1576
|
+
value: extname(normalizedPath).replace(/^\./, "") || "gpg",
|
|
1577
|
+
},
|
|
1578
|
+
]),
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function deriveTrustedKeyDomain(normalizedPath) {
|
|
1583
|
+
const lowerPath = normalizeContainerPath(normalizedPath)?.toLowerCase() || "";
|
|
1584
|
+
if (lowerPath.includes("/apt/") || lowerPath.includes("/keyrings/")) {
|
|
1585
|
+
return "apt";
|
|
1586
|
+
}
|
|
1587
|
+
if (
|
|
1588
|
+
lowerPath.includes("/rpm-gpg/") ||
|
|
1589
|
+
lowerPath.includes("/distribution-gpg-keys/")
|
|
1590
|
+
) {
|
|
1591
|
+
return "rpm";
|
|
1592
|
+
}
|
|
1593
|
+
if (lowerPath.includes("/apk/keys/")) {
|
|
1594
|
+
return "apk";
|
|
1595
|
+
}
|
|
1596
|
+
return "generic";
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function trustInspectorToolRefs() {
|
|
1600
|
+
return extractToolRefs({
|
|
1601
|
+
components: getPluginToolComponents(["trustinspector"]),
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function executeTrustInspector(args, activity) {
|
|
1606
|
+
if (isDryRun) {
|
|
1607
|
+
recordActivity({
|
|
1608
|
+
kind: "trustinspector",
|
|
1609
|
+
reason:
|
|
1610
|
+
"Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
|
|
1611
|
+
status: "blocked",
|
|
1612
|
+
...activity,
|
|
1613
|
+
});
|
|
1614
|
+
return undefined;
|
|
1615
|
+
}
|
|
1616
|
+
if (!TRUSTINSPECTOR_BIN) {
|
|
1617
|
+
return undefined;
|
|
1618
|
+
}
|
|
1619
|
+
if (DEBUG_MODE) {
|
|
1620
|
+
console.log("Executing", TRUSTINSPECTOR_BIN, args.join(" "));
|
|
1621
|
+
}
|
|
1622
|
+
const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args);
|
|
1623
|
+
if (result?.status !== 0 || result?.error) {
|
|
1624
|
+
if (DEBUG_MODE && (result?.stdout || result?.stderr)) {
|
|
1625
|
+
console.error(result.stdout, result.stderr);
|
|
1626
|
+
}
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
if (!result?.stdout) {
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
try {
|
|
1633
|
+
return JSON.parse(result.stdout);
|
|
1634
|
+
} catch (_err) {
|
|
1635
|
+
return undefined;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function normalizeTrustInspectorTargetPath(basePath) {
|
|
1640
|
+
if (typeof basePath !== "string") {
|
|
1641
|
+
return undefined;
|
|
1642
|
+
}
|
|
1643
|
+
const trimmedPath = basePath.trim();
|
|
1644
|
+
if (
|
|
1645
|
+
!trimmedPath ||
|
|
1646
|
+
hasDangerousUnicode(trimmedPath) ||
|
|
1647
|
+
/[\r\n]/.test(trimmedPath)
|
|
1648
|
+
) {
|
|
1649
|
+
return undefined;
|
|
1650
|
+
}
|
|
1651
|
+
const resolvedPath = resolve(trimmedPath);
|
|
1652
|
+
if (
|
|
1653
|
+
!resolvedPath ||
|
|
1654
|
+
hasDangerousUnicode(resolvedPath) ||
|
|
1655
|
+
/[\r\n]/.test(resolvedPath)
|
|
1656
|
+
) {
|
|
1657
|
+
return undefined;
|
|
1658
|
+
}
|
|
1659
|
+
if (
|
|
1660
|
+
(isWin &&
|
|
1661
|
+
!(
|
|
1662
|
+
resolvedPath.startsWith("\\\\") ||
|
|
1663
|
+
isValidDriveRoot(resolvedPath.slice(0, 3))
|
|
1664
|
+
)) ||
|
|
1665
|
+
(!isWin && !resolvedPath.startsWith("/")) ||
|
|
1666
|
+
!safeExistsSync(resolvedPath)
|
|
1667
|
+
) {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
const targetStats = statSync(resolvedPath, { throwIfNoEntry: false });
|
|
1671
|
+
if (!targetStats?.isDirectory()) {
|
|
1672
|
+
return undefined;
|
|
1673
|
+
}
|
|
1674
|
+
let canonicalPath;
|
|
1675
|
+
try {
|
|
1676
|
+
canonicalPath = realpathSync(resolvedPath);
|
|
1677
|
+
} catch (_err) {
|
|
1678
|
+
return undefined;
|
|
1679
|
+
}
|
|
1680
|
+
if (
|
|
1681
|
+
!canonicalPath ||
|
|
1682
|
+
hasDangerousUnicode(canonicalPath) ||
|
|
1683
|
+
/[\r\n]/.test(canonicalPath)
|
|
1684
|
+
) {
|
|
1685
|
+
return undefined;
|
|
1686
|
+
}
|
|
1687
|
+
return resolvedPath;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function trustMaterialHashes(material) {
|
|
1691
|
+
const hashes = [];
|
|
1692
|
+
if (material?.sha1) {
|
|
1693
|
+
hashes.push({ alg: "SHA-1", content: material.sha1 });
|
|
1694
|
+
}
|
|
1695
|
+
if (material?.sha256) {
|
|
1696
|
+
hashes.push({ alg: "SHA-256", content: material.sha256 });
|
|
1697
|
+
}
|
|
1698
|
+
return hashes;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function trustMaterialState(material) {
|
|
1702
|
+
const expiresAt = material?.expiresAt
|
|
1703
|
+
? Date.parse(material.expiresAt)
|
|
1704
|
+
: Number.NaN;
|
|
1705
|
+
if (!Number.isNaN(expiresAt) && expiresAt < Date.now()) {
|
|
1706
|
+
return "expired";
|
|
1707
|
+
}
|
|
1708
|
+
return "active";
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function createTrustMaterialComponent(material) {
|
|
1712
|
+
const normalizedPath = normalizeContainerPath(material?.path);
|
|
1713
|
+
if (!normalizedPath || !material?.kind) {
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
const hashes = trustMaterialHashes(material);
|
|
1717
|
+
const sharedProperties = uniqueProperties([
|
|
1718
|
+
{ name: "SrcFile", value: normalizedPath },
|
|
1719
|
+
...(material?.properties || []),
|
|
1720
|
+
...(material?.fingerprint
|
|
1721
|
+
? [{ name: "cdx:crypto:fingerprint", value: material.fingerprint }]
|
|
1722
|
+
: []),
|
|
1723
|
+
...(material?.algorithm
|
|
1724
|
+
? [{ name: "cdx:crypto:algorithm", value: material.algorithm }]
|
|
1725
|
+
: []),
|
|
1726
|
+
...(material?.keyStrength
|
|
1727
|
+
? [{ name: "cdx:crypto:keyStrength", value: `${material.keyStrength}` }]
|
|
1728
|
+
: []),
|
|
1729
|
+
...(material?.createdAt
|
|
1730
|
+
? [{ name: "cdx:crypto:createdAt", value: material.createdAt }]
|
|
1731
|
+
: []),
|
|
1732
|
+
...(material?.expiresAt
|
|
1733
|
+
? [{ name: "cdx:crypto:expiresAt", value: material.expiresAt }]
|
|
1734
|
+
: []),
|
|
1735
|
+
]);
|
|
1736
|
+
let component;
|
|
1737
|
+
if (material.kind === "certificate") {
|
|
1738
|
+
component = {
|
|
1739
|
+
"bom-ref": `crypto/certificate/${encodeURIComponent(material.name || normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.serial || material.expiresAt || "unknown"}`,
|
|
1740
|
+
name: material.name || basename(normalizedPath),
|
|
1741
|
+
type: "cryptographic-asset",
|
|
1742
|
+
version:
|
|
1743
|
+
material.sha256 ||
|
|
1744
|
+
material.serial ||
|
|
1745
|
+
material.expiresAt ||
|
|
1746
|
+
"configured",
|
|
1747
|
+
hashes,
|
|
1748
|
+
description: material.subject || normalizedPath,
|
|
1749
|
+
cryptoProperties: {
|
|
1750
|
+
assetType: "certificate",
|
|
1751
|
+
algorithmProperties: {
|
|
1752
|
+
executionEnvironment: "unknown",
|
|
1753
|
+
implementationPlatform: "unknown",
|
|
1754
|
+
},
|
|
1755
|
+
certificateProperties: {
|
|
1756
|
+
serialNumber: material.serial || undefined,
|
|
1757
|
+
subjectName: material.subject || undefined,
|
|
1758
|
+
issuerName: material.issuer || undefined,
|
|
1759
|
+
notValidBefore: material.createdAt || undefined,
|
|
1760
|
+
notValidAfter: material.expiresAt || undefined,
|
|
1761
|
+
certificateFormat: material.format || "X.509",
|
|
1762
|
+
certificateFileExtension: material.fileExtension || undefined,
|
|
1763
|
+
fingerprint: material.fingerprint
|
|
1764
|
+
? { alg: "SHA-256", content: material.fingerprint }
|
|
1765
|
+
: undefined,
|
|
1766
|
+
},
|
|
1767
|
+
},
|
|
1768
|
+
properties: uniqueProperties(
|
|
1769
|
+
sharedProperties.concat(
|
|
1770
|
+
material.trustDomain
|
|
1771
|
+
? [{ name: "cdx:crypto:trustDomain", value: material.trustDomain }]
|
|
1772
|
+
: [],
|
|
1773
|
+
),
|
|
1774
|
+
),
|
|
1775
|
+
};
|
|
1776
|
+
} else {
|
|
1777
|
+
component = {
|
|
1778
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
|
|
1779
|
+
name: material.name || basename(normalizedPath),
|
|
1780
|
+
type: "cryptographic-asset",
|
|
1781
|
+
version: material.sha256 || material.keyId || normalizedPath,
|
|
1782
|
+
hashes,
|
|
1783
|
+
cryptoProperties: {
|
|
1784
|
+
assetType: "related-crypto-material",
|
|
1785
|
+
relatedCryptoMaterialProperties: {
|
|
1786
|
+
type: "public-key",
|
|
1787
|
+
id:
|
|
1788
|
+
material.keyId ||
|
|
1789
|
+
material.fingerprint ||
|
|
1790
|
+
material.sha256 ||
|
|
1791
|
+
normalizedPath,
|
|
1792
|
+
state: trustMaterialState(material),
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
properties: uniqueProperties(
|
|
1796
|
+
sharedProperties.concat([
|
|
1797
|
+
{
|
|
1798
|
+
name: "cdx:crypto:trustDomain",
|
|
1799
|
+
value:
|
|
1800
|
+
material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
|
|
1801
|
+
},
|
|
1802
|
+
{ name: "cdx:crypto:keyPath", value: normalizedPath },
|
|
1803
|
+
{
|
|
1804
|
+
name: "cdx:crypto:fileExtension",
|
|
1805
|
+
value:
|
|
1806
|
+
material.fileExtension ||
|
|
1807
|
+
extname(normalizedPath).replace(/^\./, "") ||
|
|
1808
|
+
"gpg",
|
|
1809
|
+
},
|
|
1810
|
+
...(material?.keyId
|
|
1811
|
+
? [{ name: "cdx:crypto:keyId", value: material.keyId }]
|
|
1812
|
+
: []),
|
|
1813
|
+
...(material?.userIds || []).map((value) => ({
|
|
1814
|
+
name: "cdx:crypto:userId",
|
|
1815
|
+
value,
|
|
1816
|
+
})),
|
|
1817
|
+
]),
|
|
1818
|
+
),
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
attachIdentityTools(component, trustInspectorToolRefs());
|
|
1822
|
+
return component;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function enhanceComponentFromTrustMaterial(component, material) {
|
|
1826
|
+
if (!component || !material) {
|
|
1827
|
+
return component;
|
|
1828
|
+
}
|
|
1829
|
+
component.hashes = component.hashes?.length
|
|
1830
|
+
? component.hashes
|
|
1831
|
+
: trustMaterialHashes(material);
|
|
1832
|
+
component.properties = uniqueProperties(
|
|
1833
|
+
(component.properties || []).concat(
|
|
1834
|
+
createTrustMaterialComponent(material)?.properties || [],
|
|
1835
|
+
),
|
|
1836
|
+
);
|
|
1837
|
+
if (!component.cryptoProperties) {
|
|
1838
|
+
component.cryptoProperties =
|
|
1839
|
+
createTrustMaterialComponent(material)?.cryptoProperties;
|
|
1840
|
+
}
|
|
1841
|
+
attachIdentityTools(component, trustInspectorToolRefs());
|
|
1842
|
+
return component;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function applyTrustMaterialEnhancements(components, materials) {
|
|
1846
|
+
if (!materials?.length) {
|
|
1847
|
+
return components || [];
|
|
1848
|
+
}
|
|
1849
|
+
const componentList = [...(components || [])];
|
|
1850
|
+
const bySrcFile = new Map();
|
|
1851
|
+
const seenRefs = new Set();
|
|
1852
|
+
for (const component of componentList) {
|
|
1853
|
+
if (component?.["bom-ref"]) {
|
|
1854
|
+
seenRefs.add(component["bom-ref"]);
|
|
1855
|
+
}
|
|
1856
|
+
const srcFile = (component?.properties || []).find(
|
|
1857
|
+
(property) => property.name === "SrcFile",
|
|
1858
|
+
)?.value;
|
|
1859
|
+
if (srcFile) {
|
|
1860
|
+
bySrcFile.set(normalizeContainerPath(srcFile), component);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
for (const material of materials) {
|
|
1864
|
+
const srcFile = normalizeContainerPath(material?.path);
|
|
1865
|
+
const existing = bySrcFile.get(srcFile);
|
|
1866
|
+
if (
|
|
1867
|
+
existing &&
|
|
1868
|
+
existing.type === "cryptographic-asset" &&
|
|
1869
|
+
material?.kind === "public-key"
|
|
1870
|
+
) {
|
|
1871
|
+
enhanceComponentFromTrustMaterial(existing, material);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const component = createTrustMaterialComponent(material);
|
|
1875
|
+
if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
seenRefs.add(component["bom-ref"]);
|
|
1879
|
+
componentList.push(component);
|
|
1880
|
+
if (srcFile) {
|
|
1881
|
+
bySrcFile.set(srcFile, component);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return componentList;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function collectTrustInspectorRootfsInventory(basePath) {
|
|
1888
|
+
const targetPath = normalizeTrustInspectorTargetPath(basePath);
|
|
1889
|
+
if (!targetPath) {
|
|
1890
|
+
return [];
|
|
1891
|
+
}
|
|
1892
|
+
const trustData = executeTrustInspector(["rootfs", targetPath], {
|
|
1893
|
+
target: targetPath,
|
|
1894
|
+
});
|
|
1895
|
+
return trustData?.materials || [];
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function parseAptRepositorySources(basePath) {
|
|
1899
|
+
const sourceFiles = [
|
|
1900
|
+
"/etc/apt/sources.list",
|
|
1901
|
+
...walkRootfsFiles(basePath, "/etc/apt/sources.list.d").filter(
|
|
1902
|
+
(filePath) => filePath.endsWith(".list") || filePath.endsWith(".sources"),
|
|
1903
|
+
),
|
|
1904
|
+
].filter((filePath, index, values) => values.indexOf(filePath) === index);
|
|
1905
|
+
const entries = [];
|
|
1906
|
+
for (const sourceFile of sourceFiles) {
|
|
1907
|
+
const data = readRootfsTextFile(basePath, sourceFile);
|
|
1908
|
+
if (!data) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (sourceFile.endsWith(".sources")) {
|
|
1912
|
+
entries.push(...parseDeb822AptRepositorySources(data, sourceFile));
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
entries.push(...parseLegacyAptRepositorySources(data, sourceFile));
|
|
1916
|
+
}
|
|
1917
|
+
return entries;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function parseLegacyAptRepositorySources(data, sourceFile) {
|
|
1921
|
+
const entries = [];
|
|
1922
|
+
for (const rawLine of data.split(/\r?\n/)) {
|
|
1923
|
+
const line = rawLine.split("#")[0].trim();
|
|
1924
|
+
if (!line || (!line.startsWith("deb ") && !line.startsWith("deb-src "))) {
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const match = line.match(
|
|
1928
|
+
/^(deb(?:-src)?)\s+(?:\[(?<options>[^\]]+)\]\s+)?(?<uri>\S+)\s+(?<suite>\S+)(?:\s+(?<components>.+))?$/,
|
|
1929
|
+
);
|
|
1930
|
+
if (!match?.groups?.uri) {
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
const options = parseRepositoryOptionString(match.groups.options);
|
|
1934
|
+
entries.push({
|
|
1935
|
+
name: deriveRepositoryDisplayName(match.groups.uri, sourceFile),
|
|
1936
|
+
path: sourceFile,
|
|
1937
|
+
repoType: isPpaRepository(match.groups.uri) ? "ppa-source" : "apt-source",
|
|
1938
|
+
release: match.groups.suite,
|
|
1939
|
+
url: match.groups.uri,
|
|
1940
|
+
description: line,
|
|
1941
|
+
enabled: true,
|
|
1942
|
+
keyReferences: extractRepositoryKeyReferences(options["signed-by"]),
|
|
1943
|
+
properties: uniqueProperties([
|
|
1944
|
+
{ name: "SrcFile", value: sourceFile },
|
|
1945
|
+
{ name: "cdx:os:repo:kind", value: match[1] },
|
|
1946
|
+
{ name: "cdx:os:repo:url", value: match.groups.uri },
|
|
1947
|
+
{ name: "cdx:os:repo:release", value: match.groups.suite },
|
|
1948
|
+
...(match.groups.components
|
|
1949
|
+
? [
|
|
1950
|
+
{
|
|
1951
|
+
name: "cdx:os:repo:components",
|
|
1952
|
+
value: match.groups.components,
|
|
1953
|
+
},
|
|
1954
|
+
]
|
|
1955
|
+
: []),
|
|
1956
|
+
...(options.arch
|
|
1957
|
+
? [{ name: "cdx:os:repo:architectures", value: options.arch }]
|
|
1958
|
+
: []),
|
|
1959
|
+
...(options["signed-by"]
|
|
1960
|
+
? [{ name: "cdx:os:repo:signedBy", value: options["signed-by"] }]
|
|
1961
|
+
: []),
|
|
1962
|
+
]),
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return entries;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function parseDeb822AptRepositorySources(data, sourceFile) {
|
|
1969
|
+
const entries = [];
|
|
1970
|
+
for (const stanza of data.split(/\n\s*\n/)) {
|
|
1971
|
+
const fields = parseDeb822Fields(stanza);
|
|
1972
|
+
const uris = splitRepositoryField(fields.uris);
|
|
1973
|
+
const suites = splitRepositoryField(fields.suites || fields.suite);
|
|
1974
|
+
if (!uris.length) {
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
for (const uri of uris) {
|
|
1978
|
+
entries.push({
|
|
1979
|
+
name: deriveRepositoryDisplayName(uri, sourceFile),
|
|
1980
|
+
path: sourceFile,
|
|
1981
|
+
repoType: isPpaRepository(uri) ? "ppa-source" : "apt-source",
|
|
1982
|
+
release: suites.join(",") || "configured",
|
|
1983
|
+
url: uri,
|
|
1984
|
+
description: stanza.trim(),
|
|
1985
|
+
enabled: !["no", "false", "0"].includes(
|
|
1986
|
+
`${fields.enabled || "yes"}`.toLowerCase(),
|
|
1987
|
+
),
|
|
1988
|
+
keyReferences: extractRepositoryKeyReferences(fields["signed-by"]),
|
|
1989
|
+
properties: uniqueProperties([
|
|
1990
|
+
{ name: "SrcFile", value: sourceFile },
|
|
1991
|
+
{ name: "cdx:os:repo:kind", value: fields.types || "deb" },
|
|
1992
|
+
{ name: "cdx:os:repo:url", value: uri },
|
|
1993
|
+
...(suites.length
|
|
1994
|
+
? [{ name: "cdx:os:repo:release", value: suites.join(",") }]
|
|
1995
|
+
: []),
|
|
1996
|
+
...(fields.components
|
|
1997
|
+
? [{ name: "cdx:os:repo:components", value: fields.components }]
|
|
1998
|
+
: []),
|
|
1999
|
+
...(fields.architectures
|
|
2000
|
+
? [
|
|
2001
|
+
{
|
|
2002
|
+
name: "cdx:os:repo:architectures",
|
|
2003
|
+
value: fields.architectures,
|
|
2004
|
+
},
|
|
2005
|
+
]
|
|
2006
|
+
: []),
|
|
2007
|
+
...(fields["signed-by"]
|
|
2008
|
+
? [
|
|
2009
|
+
{
|
|
2010
|
+
name: "cdx:os:repo:signedBy",
|
|
2011
|
+
value: fields["signed-by"],
|
|
2012
|
+
},
|
|
2013
|
+
]
|
|
2014
|
+
: []),
|
|
2015
|
+
]),
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return entries;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function parseYumRepositorySources(basePath) {
|
|
2023
|
+
const entries = [];
|
|
2024
|
+
for (const repoFile of walkRootfsFiles(basePath, "/etc/yum.repos.d").filter(
|
|
2025
|
+
(f) => f.endsWith(".repo"),
|
|
2026
|
+
)) {
|
|
2027
|
+
const data = readRootfsTextFile(basePath, repoFile);
|
|
2028
|
+
if (!data) {
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
let currentSection;
|
|
2032
|
+
let currentConfig = {};
|
|
2033
|
+
const flushCurrentSection = () => {
|
|
2034
|
+
if (!currentSection) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
const url =
|
|
2038
|
+
currentConfig.baseurl ||
|
|
2039
|
+
currentConfig.mirrorlist ||
|
|
2040
|
+
currentConfig.metalink;
|
|
2041
|
+
if (!url) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
entries.push({
|
|
2045
|
+
name: currentSection,
|
|
2046
|
+
path: repoFile,
|
|
2047
|
+
repoType: "yum-source",
|
|
2048
|
+
release:
|
|
2049
|
+
`${currentConfig.enabled || "1"}` === "1" ? "enabled" : "disabled",
|
|
2050
|
+
url,
|
|
2051
|
+
description: `${repoFile}#${currentSection}`,
|
|
2052
|
+
enabled: `${currentConfig.enabled || "1"}` === "1",
|
|
2053
|
+
keyReferences: extractRepositoryKeyReferences(currentConfig.gpgkey),
|
|
2054
|
+
properties: uniqueProperties([
|
|
2055
|
+
{ name: "SrcFile", value: repoFile },
|
|
2056
|
+
{ name: "cdx:os:repo:url", value: url },
|
|
2057
|
+
...(currentConfig.baseurl
|
|
2058
|
+
? [{ name: "cdx:os:repo:baseurl", value: currentConfig.baseurl }]
|
|
2059
|
+
: []),
|
|
2060
|
+
...(currentConfig.mirrorlist
|
|
2061
|
+
? [
|
|
2062
|
+
{
|
|
2063
|
+
name: "cdx:os:repo:mirrorlist",
|
|
2064
|
+
value: currentConfig.mirrorlist,
|
|
2065
|
+
},
|
|
2066
|
+
]
|
|
2067
|
+
: []),
|
|
2068
|
+
...(currentConfig.metalink
|
|
2069
|
+
? [{ name: "cdx:os:repo:metalink", value: currentConfig.metalink }]
|
|
2070
|
+
: []),
|
|
2071
|
+
{
|
|
2072
|
+
name: "cdx:os:repo:enabled",
|
|
2073
|
+
value: `${currentConfig.enabled || "1"}`,
|
|
2074
|
+
},
|
|
2075
|
+
...(currentConfig.gpgcheck
|
|
2076
|
+
? [{ name: "cdx:os:repo:gpgcheck", value: currentConfig.gpgcheck }]
|
|
2077
|
+
: []),
|
|
2078
|
+
...(currentConfig.gpgkey
|
|
2079
|
+
? [{ name: "cdx:os:repo:gpgkey", value: currentConfig.gpgkey }]
|
|
2080
|
+
: []),
|
|
2081
|
+
]),
|
|
2082
|
+
});
|
|
2083
|
+
};
|
|
2084
|
+
for (const rawLine of data.split(/\r?\n/)) {
|
|
2085
|
+
const line = rawLine.trim();
|
|
2086
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) {
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
2090
|
+
flushCurrentSection();
|
|
2091
|
+
currentSection = line.slice(1, -1).trim();
|
|
2092
|
+
currentConfig = {};
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
const equalsIndex = line.indexOf("=");
|
|
2096
|
+
if (equalsIndex === -1) {
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
currentConfig[line.slice(0, equalsIndex).trim().toLowerCase()] = line
|
|
2100
|
+
.slice(equalsIndex + 1)
|
|
2101
|
+
.trim();
|
|
2102
|
+
}
|
|
2103
|
+
flushCurrentSection();
|
|
2104
|
+
}
|
|
2105
|
+
return entries;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function createRepositorySourceComponent(entry) {
|
|
2109
|
+
const version = entry.release || "configured";
|
|
2110
|
+
const purl = new PackageURL(
|
|
2111
|
+
"generic",
|
|
2112
|
+
"os-repository",
|
|
2113
|
+
entry.name,
|
|
2114
|
+
version,
|
|
2115
|
+
{
|
|
2116
|
+
path: entry.path,
|
|
2117
|
+
repo_type: entry.repoType,
|
|
2118
|
+
},
|
|
2119
|
+
undefined,
|
|
2120
|
+
).toString();
|
|
2121
|
+
return {
|
|
2122
|
+
"bom-ref": decodeURIComponent(purl),
|
|
2123
|
+
purl,
|
|
2124
|
+
name: entry.name,
|
|
2125
|
+
type: "data",
|
|
2126
|
+
version,
|
|
2127
|
+
description: entry.description || entry.url,
|
|
2128
|
+
properties: uniqueProperties(
|
|
2129
|
+
[
|
|
2130
|
+
{ name: "SrcFile", value: entry.path },
|
|
2131
|
+
{ name: "cdx:os:repo:type", value: entry.repoType },
|
|
2132
|
+
{ name: "cdx:os:repo:url", value: entry.url },
|
|
2133
|
+
{
|
|
2134
|
+
name: "cdx:os:repo:enabled",
|
|
2135
|
+
value: entry.enabled === false ? "false" : "true",
|
|
2136
|
+
},
|
|
2137
|
+
].concat(entry.properties || []),
|
|
2138
|
+
),
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function uniqueRepositoryEntries(entries) {
|
|
2143
|
+
const seen = new Set();
|
|
2144
|
+
const results = [];
|
|
2145
|
+
for (const entry of entries || []) {
|
|
2146
|
+
if (!entry?.name || !entry?.path || !entry?.url) {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
const key = `${entry.repoType}\u0000${entry.path}\u0000${entry.url}\u0000${entry.release || ""}`;
|
|
2150
|
+
if (seen.has(key)) {
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
seen.add(key);
|
|
2154
|
+
results.push(entry);
|
|
2155
|
+
}
|
|
2156
|
+
return results;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function deriveRepositoryDisplayName(url, sourceFile) {
|
|
2160
|
+
try {
|
|
2161
|
+
const parsedUrl = new URL(url);
|
|
2162
|
+
const repoPath = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
2163
|
+
return `${parsedUrl.hostname}${repoPath}`;
|
|
2164
|
+
} catch (_err) {
|
|
2165
|
+
return basename(sourceFile);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function isPpaRepository(url) {
|
|
2170
|
+
try {
|
|
2171
|
+
const hostname = new URL(`${url || ""}`).hostname.toLowerCase();
|
|
2172
|
+
return (
|
|
2173
|
+
hostname === "ppa.launchpadcontent.net" ||
|
|
2174
|
+
hostname === "ppa.launchpad.net"
|
|
2175
|
+
);
|
|
2176
|
+
} catch (_err) {
|
|
2177
|
+
return false;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
function parseRepositoryOptionString(optionString) {
|
|
2182
|
+
const options = {};
|
|
2183
|
+
for (const token of `${optionString || ""}`.split(/\s+/).filter(Boolean)) {
|
|
2184
|
+
const [key, ...valueParts] = token.split("=");
|
|
2185
|
+
if (!key || !valueParts.length) {
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
options[key.toLowerCase()] = valueParts.join("=");
|
|
2189
|
+
}
|
|
2190
|
+
return options;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function parseDeb822Fields(stanza) {
|
|
2194
|
+
const fields = {};
|
|
2195
|
+
let currentKey;
|
|
2196
|
+
for (const rawLine of stanza.split(/\r?\n/)) {
|
|
2197
|
+
if (!rawLine.trim()) {
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (/^[ \t]/.test(rawLine) && currentKey) {
|
|
2201
|
+
fields[currentKey] = `${fields[currentKey]} ${rawLine.trim()}`.trim();
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const separatorIndex = rawLine.indexOf(":");
|
|
2205
|
+
if (separatorIndex === -1) {
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
currentKey = rawLine.slice(0, separatorIndex).trim().toLowerCase();
|
|
2209
|
+
fields[currentKey] = rawLine.slice(separatorIndex + 1).trim();
|
|
2210
|
+
}
|
|
2211
|
+
return fields;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function splitRepositoryField(value) {
|
|
2215
|
+
return `${value || ""}`.split(/\s+/).filter(Boolean);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function extractRepositoryKeyReferences(value) {
|
|
2219
|
+
return uniqueSortedStrings(
|
|
2220
|
+
`${value || ""}`
|
|
2221
|
+
.split(/[\s,]+/)
|
|
2222
|
+
.map((part) => normalizeLocalRepositoryReference(part))
|
|
2223
|
+
.filter(Boolean),
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function normalizeLocalRepositoryReference(value) {
|
|
2228
|
+
if (!value) {
|
|
2229
|
+
return undefined;
|
|
2230
|
+
}
|
|
2231
|
+
const trimmedValue = `${value}`.trim();
|
|
2232
|
+
if (!trimmedValue || trimmedValue.includes("BEGIN PGP PUBLIC KEY BLOCK")) {
|
|
2233
|
+
return undefined;
|
|
2234
|
+
}
|
|
2235
|
+
if (trimmedValue.startsWith("file://")) {
|
|
2236
|
+
return normalizeContainerPath(trimmedValue.slice("file://".length));
|
|
2237
|
+
}
|
|
2238
|
+
if (trimmedValue.startsWith("/")) {
|
|
2239
|
+
return normalizeContainerPath(trimmedValue);
|
|
2240
|
+
}
|
|
2241
|
+
return undefined;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
function readRootfsTextFile(basePath, normalizedPath) {
|
|
2245
|
+
const absolutePath = join(basePath, normalizedPath.replace(/^\/+/, ""));
|
|
2246
|
+
if (!safeExistsSync(absolutePath)) {
|
|
2247
|
+
return undefined;
|
|
2248
|
+
}
|
|
2249
|
+
try {
|
|
2250
|
+
return readFileSync(absolutePath, "utf-8");
|
|
2251
|
+
} catch (_err) {
|
|
2252
|
+
return undefined;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
async function createOSPackageFileComponents(basePath, osPackageEntries) {
|
|
2257
|
+
const components = [];
|
|
2258
|
+
const dependenciesList = [];
|
|
2259
|
+
const ownedFilePaths = new Set();
|
|
2260
|
+
const services = [];
|
|
2261
|
+
const componentByPath = new Map();
|
|
2262
|
+
for (const packageEntry of osPackageEntries) {
|
|
2263
|
+
const commandPathSet = new Set(packageEntry.commandPaths || []);
|
|
2264
|
+
const providedRefs = new Set();
|
|
2265
|
+
for (const filePath of packageEntry.files || []) {
|
|
2266
|
+
if (!filePath) {
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
ownedFilePaths.add(filePath);
|
|
2270
|
+
let fileComponent = componentByPath.get(filePath);
|
|
2271
|
+
if (!fileComponent) {
|
|
2272
|
+
fileComponent = await createOSPackageFileComponent(
|
|
2273
|
+
basePath,
|
|
2274
|
+
filePath,
|
|
2275
|
+
commandPathSet,
|
|
2276
|
+
);
|
|
2277
|
+
if (!fileComponent) {
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
componentByPath.set(filePath, fileComponent);
|
|
2281
|
+
components.push(fileComponent);
|
|
2282
|
+
}
|
|
2283
|
+
providedRefs.add(fileComponent["bom-ref"]);
|
|
2284
|
+
}
|
|
2285
|
+
const serviceResults = await createOSPackageServices(
|
|
2286
|
+
basePath,
|
|
2287
|
+
packageEntry,
|
|
2288
|
+
componentByPath,
|
|
2289
|
+
);
|
|
2290
|
+
for (const service of serviceResults.services) {
|
|
2291
|
+
services.push(service);
|
|
2292
|
+
providedRefs.add(service["bom-ref"]);
|
|
2293
|
+
}
|
|
2294
|
+
if (serviceResults.dependenciesList.length) {
|
|
2295
|
+
dependenciesList.push(...serviceResults.dependenciesList);
|
|
2296
|
+
}
|
|
2297
|
+
if (providedRefs.size) {
|
|
2298
|
+
dependenciesList.push({
|
|
2299
|
+
ref: packageEntry.packageRef,
|
|
2300
|
+
provides: Array.from(providedRefs).sort(),
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return {
|
|
2305
|
+
components,
|
|
2306
|
+
dependenciesList,
|
|
2307
|
+
ownedFilePaths: Array.from(ownedFilePaths).sort(),
|
|
2308
|
+
services: dedupeServices(services),
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
async function createOSPackageFileComponent(
|
|
2313
|
+
basePath,
|
|
2314
|
+
filePath,
|
|
2315
|
+
commandPathSet,
|
|
2316
|
+
) {
|
|
2317
|
+
const normalizedFilePath = normalizeContainerPath(filePath);
|
|
2318
|
+
if (!normalizedFilePath) {
|
|
2319
|
+
return undefined;
|
|
2320
|
+
}
|
|
2321
|
+
let hashes;
|
|
2322
|
+
try {
|
|
2323
|
+
const hashValues = await multiChecksumFile(
|
|
2324
|
+
["md5", "sha1"],
|
|
2325
|
+
join(basePath, normalizedFilePath.replace(/^\/+/, "")),
|
|
2326
|
+
);
|
|
2327
|
+
hashes = [
|
|
2328
|
+
{ alg: "MD5", content: hashValues.md5 },
|
|
2329
|
+
{ alg: "SHA-1", content: hashValues.sha1 },
|
|
2330
|
+
];
|
|
2331
|
+
} catch (_e) {
|
|
2332
|
+
// ignore
|
|
2333
|
+
}
|
|
2334
|
+
const fileName = basename(normalizedFilePath);
|
|
2335
|
+
const stats = statSync(
|
|
2336
|
+
join(basePath, normalizedFilePath.replace(/^\/+/, "")),
|
|
2337
|
+
{
|
|
2338
|
+
throwIfNoEntry: false,
|
|
2339
|
+
},
|
|
2340
|
+
);
|
|
2341
|
+
if (!stats || stats.isDirectory()) {
|
|
2342
|
+
return undefined;
|
|
2343
|
+
}
|
|
2344
|
+
let linkedName;
|
|
2345
|
+
try {
|
|
2346
|
+
const resolvedPath = realpathSync(
|
|
2347
|
+
join(basePath, normalizedFilePath.replace(/^\/+/, "")),
|
|
2348
|
+
);
|
|
2349
|
+
const linkStats = lstatSync(
|
|
2350
|
+
join(basePath, normalizedFilePath.replace(/^\/+/, "")),
|
|
2351
|
+
);
|
|
2352
|
+
if (linkStats?.isSymbolicLink()) {
|
|
2353
|
+
linkedName = basename(resolvedPath);
|
|
2354
|
+
recordSymlinkResolution(
|
|
2355
|
+
join(basePath, normalizedFilePath.replace(/^\/+/, "")),
|
|
2356
|
+
resolvedPath,
|
|
2357
|
+
{
|
|
2358
|
+
basePath,
|
|
2359
|
+
metadata: {
|
|
2360
|
+
resolutionKind: "container-os-package-file",
|
|
2361
|
+
},
|
|
2362
|
+
},
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
} catch (_e) {
|
|
2366
|
+
// ignore
|
|
2367
|
+
}
|
|
2368
|
+
const fileType = determineOwnedFileType(
|
|
2369
|
+
normalizedFilePath,
|
|
2370
|
+
stats,
|
|
2371
|
+
commandPathSet,
|
|
2372
|
+
);
|
|
2373
|
+
const properties = [{ name: "SrcFile", value: normalizedFilePath }];
|
|
2374
|
+
if (fileType === "executable") {
|
|
2375
|
+
properties.push({ name: "internal:is_executable", value: "true" });
|
|
2376
|
+
} else if (fileType === "shared_library") {
|
|
2377
|
+
properties.push({ name: "internal:is_shared_library", value: "true" });
|
|
2378
|
+
} else {
|
|
2379
|
+
properties.push({ name: "internal:is_file", value: "true" });
|
|
2380
|
+
}
|
|
2381
|
+
properties.push(...createContainerRiskProperties(fileName, linkedName));
|
|
2382
|
+
properties.push(...createGtfoBinsProperties(fileName, linkedName));
|
|
2383
|
+
const purl = `pkg:generic/${encodeURIComponent(fileName)}?path=${encodeURIComponent(normalizedFilePath)}`;
|
|
2384
|
+
return {
|
|
2385
|
+
name: fileName,
|
|
2386
|
+
type: "file",
|
|
2387
|
+
purl,
|
|
2388
|
+
"bom-ref": purl,
|
|
2389
|
+
hashes,
|
|
2390
|
+
properties,
|
|
2391
|
+
evidence: {
|
|
2392
|
+
identity: [
|
|
2393
|
+
{
|
|
2394
|
+
field: "purl",
|
|
2395
|
+
confidence: 0,
|
|
2396
|
+
methods: [
|
|
2397
|
+
{
|
|
2398
|
+
technique: "filename",
|
|
2399
|
+
confidence: 0,
|
|
2400
|
+
value: normalizedFilePath,
|
|
2401
|
+
},
|
|
2402
|
+
],
|
|
2403
|
+
concludedValue: normalizedFilePath,
|
|
2404
|
+
},
|
|
2405
|
+
],
|
|
2406
|
+
},
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
async function createOSPackageServices(
|
|
2411
|
+
basePath,
|
|
2412
|
+
packageEntry,
|
|
2413
|
+
componentByPath,
|
|
2414
|
+
) {
|
|
2415
|
+
const services = [];
|
|
2416
|
+
const dependenciesList = [];
|
|
2417
|
+
for (const filePath of packageEntry.files || []) {
|
|
2418
|
+
const serviceDescriptor = parseOwnedServiceFile(basePath, filePath);
|
|
2419
|
+
if (!serviceDescriptor) {
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
const service = buildOwnedServiceComponent(packageEntry, serviceDescriptor);
|
|
2423
|
+
services.push(service);
|
|
2424
|
+
const dependsOn = new Set([packageEntry.packageRef]);
|
|
2425
|
+
const fileComponent = componentByPath.get(filePath);
|
|
2426
|
+
if (fileComponent?.["bom-ref"]) {
|
|
2427
|
+
dependsOn.add(fileComponent["bom-ref"]);
|
|
2428
|
+
}
|
|
2429
|
+
for (const execPath of serviceDescriptor.execPaths) {
|
|
2430
|
+
const execComponent = componentByPath.get(execPath);
|
|
2431
|
+
if (execComponent?.["bom-ref"]) {
|
|
2432
|
+
dependsOn.add(execComponent["bom-ref"]);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
dependenciesList.push({
|
|
2436
|
+
ref: service["bom-ref"],
|
|
2437
|
+
dependsOn: Array.from(dependsOn).sort(),
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return { services, dependenciesList };
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function parseOwnedServiceFile(basePath, filePath) {
|
|
2444
|
+
const normalizedFilePath = normalizeContainerPath(filePath);
|
|
2445
|
+
if (!normalizedFilePath) {
|
|
2446
|
+
return undefined;
|
|
2447
|
+
}
|
|
2448
|
+
const fileContentPath = join(
|
|
2449
|
+
basePath,
|
|
2450
|
+
normalizedFilePath.replace(/^\/+/, ""),
|
|
2451
|
+
);
|
|
2452
|
+
if (!safeExistsSync(fileContentPath)) {
|
|
2453
|
+
return undefined;
|
|
2454
|
+
}
|
|
2455
|
+
if (isSystemdServiceFile(normalizedFilePath)) {
|
|
2456
|
+
const unitData = readFileSync(fileContentPath, "utf-8");
|
|
2457
|
+
const unitMetadata = parseSystemdUnitFile(unitData);
|
|
2458
|
+
return {
|
|
2459
|
+
description: unitMetadata.description,
|
|
2460
|
+
execPaths: unitMetadata.execPaths,
|
|
2461
|
+
filePath: normalizedFilePath,
|
|
2462
|
+
manager: "systemd",
|
|
2463
|
+
name: basename(normalizedFilePath).replace(/\.[^.]+$/, ""),
|
|
2464
|
+
properties: [
|
|
2465
|
+
{ name: "cdx:service:manager", value: "systemd" },
|
|
2466
|
+
{
|
|
2467
|
+
name: "cdx:service:unitType",
|
|
2468
|
+
value: basename(normalizedFilePath).split(".").pop() || "service",
|
|
2469
|
+
},
|
|
2470
|
+
...unitMetadata.properties,
|
|
2471
|
+
],
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
if (isInitServiceFile(normalizedFilePath)) {
|
|
2475
|
+
const initData = readFileSync(fileContentPath, "utf-8");
|
|
2476
|
+
const initMetadata = parseInitScriptMetadata(initData);
|
|
2477
|
+
return {
|
|
2478
|
+
description: initMetadata.description,
|
|
2479
|
+
execPaths: initMetadata.execPaths,
|
|
2480
|
+
filePath: normalizedFilePath,
|
|
2481
|
+
manager: "sysvinit",
|
|
2482
|
+
name: initMetadata.name || basename(normalizedFilePath),
|
|
2483
|
+
properties: [
|
|
2484
|
+
{ name: "cdx:service:manager", value: "sysvinit" },
|
|
2485
|
+
...initMetadata.properties,
|
|
2486
|
+
],
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
return undefined;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function buildOwnedServiceComponent(packageEntry, serviceDescriptor) {
|
|
2493
|
+
const serviceName = sanitizeServiceName(serviceDescriptor.name);
|
|
2494
|
+
const manager = serviceDescriptor.manager;
|
|
2495
|
+
return {
|
|
2496
|
+
"bom-ref": `urn:service:${manager}:${sanitizeServiceRefToken(packageEntry.packageRef)}:${sanitizeServiceRefToken(serviceName)}`,
|
|
2497
|
+
name: serviceName,
|
|
2498
|
+
version: packageEntry.packageVersion || "latest",
|
|
2499
|
+
group: packageEntry.packageName || manager,
|
|
2500
|
+
description: serviceDescriptor.description,
|
|
2501
|
+
properties: uniqueProperties(
|
|
2502
|
+
[
|
|
2503
|
+
{ name: "SrcFile", value: serviceDescriptor.filePath },
|
|
2504
|
+
{ name: "cdx:service:packageRef", value: packageEntry.packageRef },
|
|
2505
|
+
{ name: "cdx:service:packageName", value: packageEntry.packageName },
|
|
2506
|
+
].concat(serviceDescriptor.properties || []),
|
|
2507
|
+
),
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function parseSystemdUnitFile(unitData) {
|
|
2512
|
+
const properties = [];
|
|
2513
|
+
const execPaths = new Set();
|
|
2514
|
+
let description;
|
|
2515
|
+
let currentSection = "";
|
|
2516
|
+
for (const rawLine of unitData.split(/\r?\n/)) {
|
|
2517
|
+
const line = rawLine.trim();
|
|
2518
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) {
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
2522
|
+
currentSection = line.slice(1, -1);
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const equalsIndex = line.indexOf("=");
|
|
2526
|
+
if (equalsIndex === -1) {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const key = line.slice(0, equalsIndex).trim();
|
|
2530
|
+
const value = line.slice(equalsIndex + 1).trim();
|
|
2531
|
+
if (!value) {
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (currentSection === "Unit" && key === "Description") {
|
|
2535
|
+
description = value;
|
|
2536
|
+
}
|
|
2537
|
+
if (currentSection === "Service") {
|
|
2538
|
+
if (key.startsWith("Exec")) {
|
|
2539
|
+
properties.push({ name: `cdx:service:${key}`, value });
|
|
2540
|
+
const execPath = extractExecPath(value);
|
|
2541
|
+
if (execPath) {
|
|
2542
|
+
execPaths.add(execPath);
|
|
2543
|
+
}
|
|
2544
|
+
} else if (
|
|
2545
|
+
["Type", "User", "Group", "WorkingDirectory", "Restart"].includes(key)
|
|
2546
|
+
) {
|
|
2547
|
+
properties.push({ name: `cdx:service:${key}`, value });
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (
|
|
2551
|
+
currentSection === "Install" &&
|
|
2552
|
+
["WantedBy", "RequiredBy", "Alias"].includes(key)
|
|
2553
|
+
) {
|
|
2554
|
+
properties.push({ name: `cdx:service:${key}`, value });
|
|
2555
|
+
}
|
|
2556
|
+
if (
|
|
2557
|
+
currentSection === "Unit" &&
|
|
2558
|
+
["After", "Requires", "Wants"].includes(key)
|
|
2559
|
+
) {
|
|
2560
|
+
properties.push({ name: `cdx:service:${key}`, value });
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
return {
|
|
2564
|
+
description,
|
|
2565
|
+
execPaths: Array.from(execPaths).sort(),
|
|
2566
|
+
properties: uniqueProperties(properties),
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function parseInitScriptMetadata(initData) {
|
|
2571
|
+
const properties = [];
|
|
2572
|
+
const execPaths = new Set();
|
|
2573
|
+
let description;
|
|
2574
|
+
let name;
|
|
2575
|
+
for (const rawLine of initData.split(/\r?\n/)) {
|
|
2576
|
+
const line = rawLine.trim();
|
|
2577
|
+
if (!line.startsWith("#")) {
|
|
2578
|
+
const execPath = extractExecPath(line);
|
|
2579
|
+
if (execPath) {
|
|
2580
|
+
execPaths.add(execPath);
|
|
2581
|
+
}
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
const normalized = line.replace(/^#+\s*/, "");
|
|
2585
|
+
if (normalized.startsWith("Provides:")) {
|
|
2586
|
+
const providedName = normalized
|
|
2587
|
+
.replace(/^Provides:\s*/, "")
|
|
2588
|
+
.split(/\s+/)[0];
|
|
2589
|
+
if (providedName) {
|
|
2590
|
+
name = providedName;
|
|
2591
|
+
properties.push({ name: "cdx:service:Provides", value: providedName });
|
|
2592
|
+
}
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
if (normalized.startsWith("Short-Description:")) {
|
|
2596
|
+
description = normalized.replace(/^Short-Description:\s*/, "");
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
if (normalized.startsWith("Description:")) {
|
|
2600
|
+
description = normalized.replace(/^Description:\s*/, "");
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (normalized.startsWith("Required-Start:")) {
|
|
2604
|
+
properties.push({
|
|
2605
|
+
name: "cdx:service:RequiredStart",
|
|
2606
|
+
value: normalized.replace(/^Required-Start:\s*/, ""),
|
|
2607
|
+
});
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (normalized.startsWith("Default-Start:")) {
|
|
2611
|
+
properties.push({
|
|
2612
|
+
name: "cdx:service:DefaultStart",
|
|
2613
|
+
value: normalized.replace(/^Default-Start:\s*/, ""),
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return {
|
|
2618
|
+
description,
|
|
2619
|
+
execPaths: Array.from(execPaths).sort(),
|
|
2620
|
+
name,
|
|
2621
|
+
properties: uniqueProperties(properties),
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function extractExecPath(commandLine) {
|
|
2626
|
+
if (!commandLine) {
|
|
2627
|
+
return undefined;
|
|
2628
|
+
}
|
|
2629
|
+
const sanitized = commandLine.replace(/^[\-@:+!|]+/, "").trim();
|
|
2630
|
+
const match = sanitized.match(/^("[^"]+"|'[^']+'|\S+)/);
|
|
2631
|
+
if (!match?.[1]) {
|
|
2632
|
+
return undefined;
|
|
2633
|
+
}
|
|
2634
|
+
const candidate = match[1].replace(/^['"]|['"]$/g, "");
|
|
2635
|
+
return candidate.startsWith("/")
|
|
2636
|
+
? normalizeContainerPath(candidate)
|
|
2637
|
+
: undefined;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function determineOwnedFileType(filePath, stats, commandPathSet) {
|
|
2641
|
+
if (!stats) {
|
|
2642
|
+
return "file";
|
|
2643
|
+
}
|
|
2644
|
+
if (isSharedLibraryPath(filePath)) {
|
|
2645
|
+
return "shared_library";
|
|
2646
|
+
}
|
|
2647
|
+
if (commandPathSet?.has(filePath)) {
|
|
2648
|
+
return "executable";
|
|
2649
|
+
}
|
|
2650
|
+
if (stats.mode & 0o111) {
|
|
2651
|
+
return "executable";
|
|
2652
|
+
}
|
|
2653
|
+
return "file";
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function isSharedLibraryPath(filePath) {
|
|
2657
|
+
return /(?:^|\/)[^/]+\.(?:so(?:\.[^/]+)?|a|lib|dll)$/i.test(filePath);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function isSystemdServiceFile(filePath) {
|
|
2661
|
+
return /\/(?:etc|lib|usr\/lib)\/systemd\/system\/.+\.(?:service|socket|timer|mount|path|target|slice|automount)$/i.test(
|
|
2662
|
+
filePath,
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function isInitServiceFile(filePath) {
|
|
2667
|
+
return /\/(?:etc\/init\.d|etc\/rc\.d\/init\.d)\/.+/i.test(filePath);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function sanitizeServiceName(value) {
|
|
2671
|
+
return String(value || "service").trim() || "service";
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function sanitizeServiceRefToken(value) {
|
|
2675
|
+
return (
|
|
2676
|
+
String(value || "service")
|
|
2677
|
+
.toLowerCase()
|
|
2678
|
+
.replace(/[^a-z0-9._:-]+/g, "-")
|
|
2679
|
+
.replace(/^-+|-+$/g, "") || "service"
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function uniqueProperties(properties) {
|
|
2684
|
+
const seen = new Set();
|
|
2685
|
+
const uniqueValues = [];
|
|
2686
|
+
for (const property of properties || []) {
|
|
2687
|
+
if (!property?.name || !property?.value) {
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
const key = `${property.name}\u0000${property.value}`;
|
|
2691
|
+
if (seen.has(key)) {
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
seen.add(key);
|
|
2695
|
+
uniqueValues.push(property);
|
|
2696
|
+
}
|
|
2697
|
+
return uniqueValues;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function uniqueSortedStrings(values) {
|
|
2701
|
+
return Array.from(new Set((values || []).filter(Boolean))).sort();
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
function dedupeServices(services) {
|
|
2705
|
+
const serviceMap = new Map();
|
|
2706
|
+
for (const service of services || []) {
|
|
2707
|
+
if (!service?.["bom-ref"]) {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
if (!serviceMap.has(service["bom-ref"])) {
|
|
2711
|
+
serviceMap.set(service["bom-ref"], {
|
|
2712
|
+
...service,
|
|
2713
|
+
properties: uniqueProperties(service.properties),
|
|
2714
|
+
});
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
const existing = serviceMap.get(service["bom-ref"]);
|
|
2718
|
+
existing.properties = uniqueProperties(
|
|
2719
|
+
(existing.properties || []).concat(service.properties || []),
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
return Array.from(serviceMap.values());
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function normalizeContainerPath(filePath) {
|
|
2726
|
+
if (!filePath) {
|
|
2727
|
+
return undefined;
|
|
2728
|
+
}
|
|
2729
|
+
const normalized = filePath.replace(/\\/g, "/").replace(/^\/+/, "/");
|
|
2730
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// Detect common sdks and runtimes from the name
|
|
2734
|
+
function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
|
|
2735
|
+
if (!comp?.name) {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (/dotnet[6-9]?-sdk/.test(comp.name)) {
|
|
2739
|
+
bundledSdks.add(comp.name);
|
|
2740
|
+
}
|
|
2741
|
+
if (
|
|
2742
|
+
/dotnet[6-9]?-runtime/.test(comp.name) ||
|
|
2743
|
+
comp.name.includes("aspnet-runtime") ||
|
|
2744
|
+
/aspnetcore[6-9]?-runtime/.test(comp.name)
|
|
2745
|
+
) {
|
|
2746
|
+
bundledRuntimes.add(comp.name);
|
|
2747
|
+
}
|
|
2748
|
+
// TODO: Need to test this for a range of base images
|
|
2749
|
+
if (COMMON_RUNTIMES.includes(comp.name)) {
|
|
2750
|
+
bundledRuntimes.add(comp.name);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
|
|
2755
|
+
try {
|
|
2756
|
+
const tmpDependsOn =
|
|
2757
|
+
tmpDependencies[origBomRef] || tmpDependencies[comp["bom-ref"]] || [];
|
|
2758
|
+
const dependsOn = new Set();
|
|
2759
|
+
tmpDependsOn.forEach((d) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const compPurl = PackageURL.fromString(comp.purl);
|
|
2762
|
+
const tmpPurl = PackageURL.fromString(d.replace("none", compPurl.type));
|
|
2763
|
+
tmpPurl.type = compPurl.type;
|
|
2764
|
+
// FIXME: Check if this hack is still needed with the latest trivy
|
|
2765
|
+
if (OS_PURL_TYPES.includes(compPurl.type)) {
|
|
2766
|
+
tmpPurl.namespace = compPurl.namespace;
|
|
2767
|
+
tmpPurl.qualifiers = tmpPurl.qualifiers || {};
|
|
2768
|
+
if (compPurl.qualifiers) {
|
|
2769
|
+
if (compPurl.qualifiers.distro_name) {
|
|
2770
|
+
tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
|
|
2771
|
+
}
|
|
2772
|
+
if (compPurl.qualifiers.distro) {
|
|
2773
|
+
tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (tmpPurl.qualifiers) {
|
|
2778
|
+
if (
|
|
2779
|
+
tmpPurl.qualifiers.epoch &&
|
|
2780
|
+
!tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
|
|
2781
|
+
) {
|
|
2782
|
+
tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
// Prevents purls ending with ?
|
|
2786
|
+
if (!Object.keys(tmpPurl.qualifiers).length) {
|
|
2787
|
+
tmpPurl.qualifiers = undefined;
|
|
2788
|
+
}
|
|
2789
|
+
dependsOn.add(decodeURIComponent(tmpPurl.toString()));
|
|
2790
|
+
} catch (_e) {
|
|
2791
|
+
// ignore
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
|
|
2795
|
+
} catch (_e) {
|
|
2796
|
+
// ignore
|
|
2797
|
+
}
|
|
2798
|
+
return undefined;
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
function isHostInspectionPath(value) {
|
|
2802
|
+
if (!value) {
|
|
2803
|
+
return false;
|
|
2804
|
+
}
|
|
2805
|
+
if (platform === "darwin") {
|
|
2806
|
+
return value.startsWith("/");
|
|
2807
|
+
}
|
|
2808
|
+
if (platform === "windows") {
|
|
2809
|
+
return /^[a-z]:\\/i.test(value) || /^\\\\/.test(value);
|
|
2810
|
+
}
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function isDarwinSystemHostPath(value) {
|
|
2815
|
+
if (typeof value !== "string") {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
return (
|
|
2819
|
+
value.startsWith("/bin/") ||
|
|
2820
|
+
value.startsWith("/sbin/") ||
|
|
2821
|
+
value.startsWith("/System/") ||
|
|
2822
|
+
value.startsWith("/usr/bin/") ||
|
|
2823
|
+
value.startsWith("/usr/libexec/") ||
|
|
2824
|
+
value.startsWith("/usr/sbin/") ||
|
|
2825
|
+
value.startsWith("/Library/Apple/System/") ||
|
|
2826
|
+
value.startsWith("/System/Volumes/Preboot/Cryptexes/")
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
function shouldInspectComponentHostPath(value) {
|
|
2831
|
+
if (!isHostInspectionPath(value)) {
|
|
2832
|
+
return false;
|
|
2833
|
+
}
|
|
2834
|
+
if (platform === "darwin") {
|
|
2835
|
+
return !(
|
|
2836
|
+
value.toLowerCase().endsWith(".plist") || isDarwinSystemHostPath(value)
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
return true;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
function shouldInspectComponentTrust(component) {
|
|
2843
|
+
if (platform !== "darwin") {
|
|
2844
|
+
return true;
|
|
2845
|
+
}
|
|
2846
|
+
const queryCategory = `${
|
|
2847
|
+
(component?.properties || []).find(
|
|
2848
|
+
(property) => property?.name === "cdx:osquery:category",
|
|
2849
|
+
)?.value || ""
|
|
2850
|
+
}`.trim();
|
|
2851
|
+
if (!queryCategory) {
|
|
2852
|
+
return true;
|
|
2853
|
+
}
|
|
2854
|
+
return new Set(["launchd_services", "startup_items", "running_apps"]).has(
|
|
2855
|
+
queryCategory,
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function extractComponentHostPaths(component) {
|
|
2860
|
+
if (!shouldInspectComponentTrust(component)) {
|
|
2861
|
+
return [];
|
|
2862
|
+
}
|
|
2863
|
+
const pathPropertyNames = new Set([
|
|
2864
|
+
"path",
|
|
2865
|
+
"bundle_path",
|
|
2866
|
+
"bundle_executable",
|
|
2867
|
+
"executable",
|
|
2868
|
+
"program",
|
|
2869
|
+
"image_path",
|
|
2870
|
+
"binary_path",
|
|
2871
|
+
"action_path",
|
|
2872
|
+
]);
|
|
2873
|
+
const paths = [];
|
|
2874
|
+
for (const property of component?.properties || []) {
|
|
2875
|
+
const propertyName = `${property?.name || ""}`.toLowerCase();
|
|
2876
|
+
if (!pathPropertyNames.has(propertyName)) {
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
const normalizedValue = `${property?.value || ""}`.trim();
|
|
2880
|
+
if (shouldInspectComponentHostPath(normalizedValue)) {
|
|
2881
|
+
paths.push(normalizedValue);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return uniqueSortedStrings(paths);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function createHostTrustFindingComponent(finding) {
|
|
2888
|
+
if (!finding?.kind || !finding?.name) {
|
|
2889
|
+
return undefined;
|
|
2890
|
+
}
|
|
2891
|
+
const purl = new PackageURL(
|
|
2892
|
+
"generic",
|
|
2893
|
+
"host-trust",
|
|
2894
|
+
finding.name,
|
|
2895
|
+
finding.version || "observed",
|
|
2896
|
+
{
|
|
2897
|
+
kind: finding.kind,
|
|
2898
|
+
...(finding.path ? { path: finding.path } : {}),
|
|
2899
|
+
},
|
|
2900
|
+
undefined,
|
|
2901
|
+
).toString();
|
|
2902
|
+
const component = {
|
|
2903
|
+
"bom-ref": decodeURIComponent(purl),
|
|
2904
|
+
name: finding.name,
|
|
2905
|
+
version: finding.version || "observed",
|
|
2906
|
+
description: finding.description,
|
|
2907
|
+
purl,
|
|
2908
|
+
type: "data",
|
|
2909
|
+
properties: uniqueProperties(
|
|
2910
|
+
(finding.properties || []).concat([
|
|
2911
|
+
{ name: "cdx:trustinspector:kind", value: finding.kind },
|
|
2912
|
+
...(finding.path ? [{ name: "SrcFile", value: finding.path }] : []),
|
|
2913
|
+
]),
|
|
2914
|
+
),
|
|
2915
|
+
};
|
|
2916
|
+
if (finding.sha256) {
|
|
2917
|
+
component.hashes = [{ alg: "SHA-256", content: finding.sha256 }];
|
|
2918
|
+
}
|
|
2919
|
+
attachIdentityTools(component, trustInspectorToolRefs());
|
|
2920
|
+
return component;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
export function enrichOSComponentsWithTrustData(components = []) {
|
|
2924
|
+
if (!["darwin", "windows"].includes(platform) || !TRUSTINSPECTOR_BIN) {
|
|
2925
|
+
return { components, tools: [] };
|
|
2926
|
+
}
|
|
2927
|
+
const mergedComponents = [...components];
|
|
2928
|
+
const pathInspectionCandidates = uniqueSortedStrings(
|
|
2929
|
+
components.flatMap((component) => extractComponentHostPaths(component)),
|
|
2930
|
+
);
|
|
2931
|
+
if (pathInspectionCandidates.length) {
|
|
2932
|
+
const inspectionMap = new Map();
|
|
2933
|
+
const batchSize = 200;
|
|
2934
|
+
for (
|
|
2935
|
+
let offset = 0;
|
|
2936
|
+
offset < pathInspectionCandidates.length;
|
|
2937
|
+
offset += batchSize
|
|
2938
|
+
) {
|
|
2939
|
+
const inspectionBatch = pathInspectionCandidates.slice(
|
|
2940
|
+
offset,
|
|
2941
|
+
offset + batchSize,
|
|
2942
|
+
);
|
|
2943
|
+
const inspectionData = executeTrustInspector(
|
|
2944
|
+
["paths", ...inspectionBatch],
|
|
2945
|
+
{
|
|
2946
|
+
target: `${inspectionBatch.length} path(s)`,
|
|
2947
|
+
},
|
|
2948
|
+
);
|
|
2949
|
+
for (const inspection of inspectionData?.inspections || []) {
|
|
2950
|
+
inspectionMap.set(
|
|
2951
|
+
inspection.path,
|
|
2952
|
+
uniqueProperties(inspection.properties || []),
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
if (inspectionMap.size) {
|
|
2957
|
+
for (const component of mergedComponents) {
|
|
2958
|
+
const matchingProperties = extractComponentHostPaths(component)
|
|
2959
|
+
.map((candidatePath) => inspectionMap.get(candidatePath))
|
|
2960
|
+
.filter(Boolean)
|
|
2961
|
+
.flat();
|
|
2962
|
+
if (!matchingProperties.length) {
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
component.properties = uniqueProperties(
|
|
2966
|
+
(component.properties || []).concat(matchingProperties),
|
|
2967
|
+
);
|
|
2968
|
+
attachIdentityTools(component, trustInspectorToolRefs());
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const hostData = executeTrustInspector(["host"], { target: platform });
|
|
2973
|
+
const hostComponents = (hostData?.hostFindings || [])
|
|
2974
|
+
.map((finding) => createHostTrustFindingComponent(finding))
|
|
2975
|
+
.filter(Boolean);
|
|
2976
|
+
if (hostComponents.length) {
|
|
2977
|
+
mergedComponents.push(...hostComponents);
|
|
2978
|
+
}
|
|
2979
|
+
return {
|
|
2980
|
+
components: mergedComponents,
|
|
2981
|
+
tools:
|
|
2982
|
+
pathInspectionCandidates.length || hostComponents.length
|
|
2983
|
+
? getPluginToolComponents(["trustinspector"])
|
|
2984
|
+
: [],
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
export function executeOsQuery(query) {
|
|
2989
|
+
if (isDryRun) {
|
|
2990
|
+
recordActivity({
|
|
2991
|
+
kind: "osquery",
|
|
2992
|
+
reason:
|
|
2993
|
+
"Dry run mode blocks osquery execution and reports the query instead.",
|
|
2994
|
+
status: "blocked",
|
|
2995
|
+
target: query,
|
|
2996
|
+
});
|
|
2997
|
+
return undefined;
|
|
2998
|
+
}
|
|
2999
|
+
if (OSQUERY_BIN) {
|
|
3000
|
+
if (!query.endsWith(";")) {
|
|
3001
|
+
query = `${query};`;
|
|
3002
|
+
}
|
|
3003
|
+
const args = ["--S", "--disable_database", "--json", query];
|
|
3004
|
+
// On darwin, we need to disable the safety check and run cdxgen with sudo
|
|
3005
|
+
// https://github.com/osquery/osquery/issues/1382
|
|
3006
|
+
if (platform === "darwin") {
|
|
3007
|
+
args.push("--allow_unsafe");
|
|
3008
|
+
args.push("--disable_logging");
|
|
3009
|
+
args.push("--disable_events");
|
|
3010
|
+
}
|
|
3011
|
+
if (DEBUG_MODE) {
|
|
3012
|
+
console.log("Executing", OSQUERY_BIN, args.join(" "));
|
|
3013
|
+
}
|
|
3014
|
+
const result = safeSpawnSync(OSQUERY_BIN, args);
|
|
3015
|
+
if (result.status !== 0 || result.error) {
|
|
3016
|
+
if (
|
|
3017
|
+
DEBUG_MODE &&
|
|
3018
|
+
result.stderr &&
|
|
3019
|
+
!result.stderr.includes("no such table")
|
|
3020
|
+
) {
|
|
3021
|
+
console.error(result.stdout, result.stderr);
|
|
3022
|
+
}
|
|
1077
3023
|
}
|
|
1078
3024
|
if (result) {
|
|
1079
3025
|
const stdout = result.stdout;
|
|
@@ -1215,6 +3161,16 @@ async function fileComponents(basePath, fileList, fileType) {
|
|
|
1215
3161
|
const linkStats = lstatSync(join(basePath, f.replace(/^\/+/, "")));
|
|
1216
3162
|
if (linkStats?.isSymbolicLink()) {
|
|
1217
3163
|
linkedName = basename(resolvedPath);
|
|
3164
|
+
recordSymlinkResolution(
|
|
3165
|
+
join(basePath, f.replace(/^\/+/, "")),
|
|
3166
|
+
resolvedPath,
|
|
3167
|
+
{
|
|
3168
|
+
basePath,
|
|
3169
|
+
metadata: {
|
|
3170
|
+
resolutionKind: "container-binary",
|
|
3171
|
+
},
|
|
3172
|
+
},
|
|
3173
|
+
);
|
|
1218
3174
|
}
|
|
1219
3175
|
} catch (_e) {
|
|
1220
3176
|
// ignore
|
|
@@ -1225,7 +3181,7 @@ async function fileComponents(basePath, fileList, fileType) {
|
|
|
1225
3181
|
let isSetgid;
|
|
1226
3182
|
let isSticky;
|
|
1227
3183
|
try {
|
|
1228
|
-
const stats = statSync(f);
|
|
3184
|
+
const stats = statSync(join(basePath, f.replace(/^\/+/, "")));
|
|
1229
3185
|
const mode = stats.mode;
|
|
1230
3186
|
isExecutable = !!(mode & 0o111);
|
|
1231
3187
|
isSetuid = !!(mode & 0o4000);
|