@cyclonedx/cdxgen 12.3.3 → 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 +64 -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 +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- 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 +14 -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 +506 -88
- package/lib/cli/index.poku.js +1352 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -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/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- 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/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/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 +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +2 -27
- 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 +1366 -31
- 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 +23 -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/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/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/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/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 +45 -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.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- 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,35 +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,
|
|
19
23
|
attachIdentityTools,
|
|
20
24
|
collectExecutables,
|
|
21
25
|
collectSharedLibs,
|
|
22
26
|
DEBUG_MODE,
|
|
23
|
-
dirNameStr,
|
|
24
27
|
extractPathEnv,
|
|
25
28
|
extractToolRefs,
|
|
26
29
|
findLicenseId,
|
|
27
30
|
getTmpDir,
|
|
31
|
+
hasDangerousUnicode,
|
|
28
32
|
isDryRun,
|
|
29
33
|
isSpdxLicenseExpression,
|
|
34
|
+
isValidDriveRoot,
|
|
30
35
|
multiChecksumFile,
|
|
31
36
|
recordActivity,
|
|
32
|
-
|
|
37
|
+
recordSymlinkResolution,
|
|
33
38
|
safeExistsSync,
|
|
34
39
|
safeMkdirSync,
|
|
35
40
|
safeMkdtempSync,
|
|
@@ -38,241 +43,398 @@ import {
|
|
|
38
43
|
} from "../helpers/utils.js";
|
|
39
44
|
import { getDirs } from "./containerutils.js";
|
|
40
45
|
|
|
41
|
-
const dirName = dirNameStr;
|
|
42
46
|
const isWin = _platform() === "win32";
|
|
43
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;
|
|
44
53
|
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Is there a non-empty local node_modules directory
|
|
94
|
-
if (
|
|
95
|
-
!CDXGEN_PLUGINS_DIR &&
|
|
96
|
-
safeExistsSync(
|
|
97
|
-
join(
|
|
98
|
-
dirName,
|
|
99
|
-
"node_modules",
|
|
100
|
-
"@cdxgen",
|
|
101
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
102
|
-
"plugins",
|
|
103
|
-
),
|
|
104
|
-
) &&
|
|
105
|
-
safeExistsSync(
|
|
106
|
-
join(
|
|
107
|
-
dirName,
|
|
108
|
-
"node_modules",
|
|
109
|
-
"@cdxgen",
|
|
110
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
111
|
-
"plugins",
|
|
112
|
-
"trivy",
|
|
113
|
-
),
|
|
114
|
-
)
|
|
115
|
-
) {
|
|
116
|
-
CDXGEN_PLUGINS_DIR = join(
|
|
117
|
-
dirName,
|
|
118
|
-
"node_modules",
|
|
119
|
-
"@cdxgen",
|
|
120
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
121
|
-
"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,
|
|
122
99
|
);
|
|
123
|
-
if (
|
|
124
|
-
|
|
100
|
+
if (!name || !value) {
|
|
101
|
+
return undefined;
|
|
125
102
|
}
|
|
103
|
+
return { name, value };
|
|
126
104
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,
|
|
133
230
|
);
|
|
231
|
+
if (sanitizedValue) {
|
|
232
|
+
sanitizedPackage[field] = sanitizedValue;
|
|
233
|
+
}
|
|
134
234
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
140
260
|
}
|
|
141
261
|
}
|
|
262
|
+
sanitizedManifest.plugins.push(sanitizedPlugin);
|
|
142
263
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"plugins",
|
|
150
|
-
);
|
|
151
|
-
extraNMBinPath = join(
|
|
152
|
-
globalNodePath,
|
|
153
|
-
"..",
|
|
154
|
-
".pnpm",
|
|
155
|
-
"node_modules",
|
|
156
|
-
".bin",
|
|
157
|
-
);
|
|
264
|
+
return sanitizedManifest.plugins.length ? sanitizedManifest : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function loadPluginManifest() {
|
|
268
|
+
if (pluginManifest !== undefined) {
|
|
269
|
+
return pluginManifest;
|
|
158
270
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const tmpA = dirName.split(join("node_modules", ".pnpm"));
|
|
163
|
-
altGlobalPlugins = join(
|
|
164
|
-
tmpA[0],
|
|
165
|
-
"node_modules",
|
|
166
|
-
".pnpm",
|
|
167
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
168
|
-
"node_modules",
|
|
169
|
-
"@cdxgen",
|
|
170
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
171
|
-
"plugins",
|
|
172
|
-
);
|
|
173
|
-
if (safeExistsSync(join(tmpA[0], "node_modules", ".bin"))) {
|
|
174
|
-
extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
|
|
175
|
-
}
|
|
176
|
-
} else if (dirName.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
|
|
177
|
-
// pnpm dlx
|
|
178
|
-
const tmpA = dirName.split(".pnpm");
|
|
179
|
-
altGlobalPlugins = join(
|
|
180
|
-
tmpA[0],
|
|
181
|
-
".pnpm",
|
|
182
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
183
|
-
"node_modules",
|
|
184
|
-
"@cdxgen",
|
|
185
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
186
|
-
"plugins",
|
|
187
|
-
);
|
|
188
|
-
if (safeExistsSync(join(tmpA[0], ".bin"))) {
|
|
189
|
-
extraNMBinPath = join(tmpA[0], ".bin");
|
|
190
|
-
}
|
|
191
|
-
} else if (dirName.includes(join("caxa", "applications"))) {
|
|
192
|
-
// sae binaries
|
|
193
|
-
altGlobalPlugins = join(
|
|
194
|
-
dirName,
|
|
195
|
-
"node_modules",
|
|
196
|
-
"pnpm",
|
|
197
|
-
`@cdxgen+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
|
|
198
|
-
"node_modules",
|
|
199
|
-
"@cdxgen",
|
|
200
|
-
`cdxgen-plugins-bin${pluginsBinSuffix}`,
|
|
201
|
-
"plugins",
|
|
202
|
-
);
|
|
203
|
-
extraNMBinPath = join(dirName, "node_modules", ".bin");
|
|
271
|
+
if (!PLUGIN_MANIFEST_FILE) {
|
|
272
|
+
pluginManifest = null;
|
|
273
|
+
return pluginManifest;
|
|
204
274
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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;
|
|
216
288
|
}
|
|
289
|
+
pluginManifest = sanitizePluginManifest(
|
|
290
|
+
JSON.parse(readFileSync(manifestRealPath, { encoding: "utf-8" })),
|
|
291
|
+
);
|
|
292
|
+
} catch (_err) {
|
|
293
|
+
pluginManifest = null;
|
|
217
294
|
}
|
|
295
|
+
return pluginManifest;
|
|
218
296
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
|
|
298
|
+
function cloneSerializable(value) {
|
|
299
|
+
if (!value || typeof value !== "object") {
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
return JSON.parse(JSON.stringify(value));
|
|
222
303
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
304
|
+
|
|
305
|
+
function getPluginManifestEntry(toolName) {
|
|
306
|
+
const manifest = loadPluginManifest();
|
|
307
|
+
if (!manifest?.plugins?.length) {
|
|
308
|
+
return undefined;
|
|
228
309
|
}
|
|
229
|
-
|
|
310
|
+
return manifest.plugins.find((entry) => entry?.name === toolName);
|
|
230
311
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
),
|
|
237
345
|
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
CDXGEN_PLUGINS_DIR,
|
|
243
|
-
"cargo-auditable",
|
|
244
|
-
`cargo-auditable-cdxgen-${platform}-${arch}${extn}`,
|
|
346
|
+
merged.properties = uniqueProperties(
|
|
347
|
+
(manifestComponent?.properties || []).concat(
|
|
348
|
+
existingComponent?.properties || [],
|
|
349
|
+
),
|
|
245
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;
|
|
246
357
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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);
|
|
257
372
|
}
|
|
373
|
+
return uniqueValues;
|
|
258
374
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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());
|
|
266
422
|
}
|
|
267
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
|
+
|
|
268
433
|
// Blint bin
|
|
269
434
|
const BLINT_BIN = process.env.BLINT_CMD || "blint";
|
|
270
435
|
|
|
271
436
|
// sourcekitten
|
|
272
|
-
|
|
273
|
-
if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
|
|
274
|
-
SOURCEKITTEN_BIN = join(CDXGEN_PLUGINS_DIR, "sourcekitten", "sourcekitten");
|
|
275
|
-
}
|
|
437
|
+
const SOURCEKITTEN_BIN = resolvePluginBinary("sourcekitten", pluginRuntime);
|
|
276
438
|
|
|
277
439
|
// Keep this list updated every year
|
|
278
440
|
const OS_DISTRO_ALIAS = {
|
|
@@ -466,15 +628,20 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
466
628
|
dependenciesList: [],
|
|
467
629
|
executables: [],
|
|
468
630
|
osPackages: [],
|
|
631
|
+
osPackageFiles: [],
|
|
469
632
|
sharedLibs: [],
|
|
633
|
+
services: [],
|
|
470
634
|
tools: [],
|
|
471
635
|
};
|
|
472
636
|
}
|
|
473
637
|
const pkgList = [];
|
|
638
|
+
const osPackageEntries = [];
|
|
474
639
|
const dependenciesList = [];
|
|
475
640
|
const allTypes = new Set();
|
|
476
641
|
const bundledSdks = new Set();
|
|
477
642
|
const bundledRuntimes = new Set();
|
|
643
|
+
let osPackageFiles = [];
|
|
644
|
+
let services = [];
|
|
478
645
|
let tools = [];
|
|
479
646
|
let binPaths = extractPathEnv(imageConfig?.Env);
|
|
480
647
|
if (!binPaths?.length) {
|
|
@@ -515,25 +682,13 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
515
682
|
const bomJsonFile = join(tempDir, "trivy-bom.json");
|
|
516
683
|
const args = [
|
|
517
684
|
imageType,
|
|
518
|
-
"--skip-db-update",
|
|
519
|
-
"--skip-java-db-update",
|
|
520
|
-
"--offline-scan",
|
|
521
|
-
"--disable-telemetry",
|
|
522
|
-
"--skip-version-check",
|
|
523
|
-
"--skip-files",
|
|
524
|
-
"**/*.jar,**/*.war,**/*.par,**/*.ear",
|
|
525
|
-
"--no-progress",
|
|
526
|
-
"--exit-code",
|
|
527
|
-
"0",
|
|
528
|
-
"--format",
|
|
529
|
-
"cyclonedx",
|
|
530
685
|
"--cache-dir",
|
|
531
686
|
trivyCacheDir,
|
|
532
687
|
"--output",
|
|
533
688
|
bomJsonFile,
|
|
534
689
|
];
|
|
535
|
-
if (
|
|
536
|
-
args.push("
|
|
690
|
+
if (DEBUG_MODE) {
|
|
691
|
+
args.push("--debug");
|
|
537
692
|
}
|
|
538
693
|
args.push(src);
|
|
539
694
|
if (DEBUG_MODE) {
|
|
@@ -637,15 +792,17 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
637
792
|
}
|
|
638
793
|
}
|
|
639
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
|
+
);
|
|
640
802
|
const toolRefs = extractToolRefs(
|
|
641
|
-
|
|
803
|
+
{ components: tools },
|
|
642
804
|
(tool) => tool?.name !== "cdxgen",
|
|
643
805
|
);
|
|
644
|
-
tools = (
|
|
645
|
-
Array.isArray(tmpBom?.metadata?.tools)
|
|
646
|
-
? tmpBom.metadata.tools
|
|
647
|
-
: tmpBom?.metadata?.tools?.components || []
|
|
648
|
-
).filter((tool) => tool?.["bom-ref"] && tool?.name !== "cdxgen");
|
|
649
806
|
(tmpBom.dependencies || []).forEach((d) => {
|
|
650
807
|
tmpDependencies[d.ref] = d.dependsOn;
|
|
651
808
|
});
|
|
@@ -838,30 +995,12 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
838
995
|
}
|
|
839
996
|
}
|
|
840
997
|
const compProperties = comp.properties;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
// Property name: aquasecurity:trivy:SrcName
|
|
848
|
-
if (aprop.name.endsWith("SrcName")) {
|
|
849
|
-
srcName = aprop.value;
|
|
850
|
-
}
|
|
851
|
-
// Property name: aquasecurity:trivy:SrcVersion
|
|
852
|
-
if (aprop.name.endsWith("SrcVersion")) {
|
|
853
|
-
srcVersion = aprop.value;
|
|
854
|
-
}
|
|
855
|
-
// Property name: aquasecurity:trivy:SrcRelease
|
|
856
|
-
if (aprop.name.endsWith("SrcRelease")) {
|
|
857
|
-
srcRelease = aprop.value;
|
|
858
|
-
}
|
|
859
|
-
// Property name: aquasecurity:trivy:SrcEpoch
|
|
860
|
-
if (aprop.name.endsWith("SrcEpoch")) {
|
|
861
|
-
epoch = aprop.value;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
998
|
+
const trivyMetadata = extractTrivyOsPackageMetadata(compProperties);
|
|
999
|
+
const fallbackIdentityProperties = promoteTrivyOsPackageIdentity(
|
|
1000
|
+
comp,
|
|
1001
|
+
trivyMetadata,
|
|
1002
|
+
);
|
|
1003
|
+
let { srcName, srcVersion, srcRelease, epoch } = trivyMetadata;
|
|
865
1004
|
// See issue #2067
|
|
866
1005
|
if (srcVersion && srcRelease) {
|
|
867
1006
|
srcVersion = `${srcVersion}-${srcRelease}`;
|
|
@@ -869,7 +1008,18 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
869
1008
|
if (epoch) {
|
|
870
1009
|
srcVersion = `${epoch}:${srcVersion}`;
|
|
871
1010
|
}
|
|
872
|
-
|
|
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
|
+
}
|
|
873
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
|
|
874
1024
|
if (
|
|
875
1025
|
comp["bom-ref"] &&
|
|
@@ -879,6 +1029,17 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
879
1029
|
comp["bom-ref"] = decodeURIComponent(comp.purl);
|
|
880
1030
|
}
|
|
881
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
|
+
}
|
|
882
1043
|
detectSdksRuntimes(comp, bundledSdks, bundledRuntimes);
|
|
883
1044
|
const compDeps = retrieveDependencies(
|
|
884
1045
|
tmpDependencies,
|
|
@@ -930,6 +1091,7 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
930
1091
|
).toString();
|
|
931
1092
|
}
|
|
932
1093
|
newComp["bom-ref"] = decodeURIComponent(newComp.purl);
|
|
1094
|
+
delete newComp.properties;
|
|
933
1095
|
attachIdentityTools(newComp, toolRefs);
|
|
934
1096
|
pkgList.push(newComp);
|
|
935
1097
|
detectSdksRuntimes(newComp, bundledSdks, bundledRuntimes);
|
|
@@ -939,11 +1101,33 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
939
1101
|
}
|
|
940
1102
|
}
|
|
941
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
|
+
}
|
|
942
1126
|
let executables = [];
|
|
943
1127
|
if (binPaths?.length) {
|
|
944
1128
|
executables = await fileComponents(
|
|
945
1129
|
src,
|
|
946
|
-
collectExecutables(src, binPaths),
|
|
1130
|
+
collectExecutables(src, binPaths, ownedFilePaths),
|
|
947
1131
|
"executable",
|
|
948
1132
|
);
|
|
949
1133
|
}
|
|
@@ -971,11 +1155,13 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
971
1155
|
defaultLibPaths,
|
|
972
1156
|
"/etc/ld.so.conf",
|
|
973
1157
|
"/etc/ld.so.conf.d/*.conf",
|
|
1158
|
+
ownedFilePaths,
|
|
974
1159
|
),
|
|
975
1160
|
"shared_library",
|
|
976
1161
|
);
|
|
977
1162
|
return {
|
|
978
1163
|
osPackages: pkgList,
|
|
1164
|
+
osPackageFiles,
|
|
979
1165
|
dependenciesList,
|
|
980
1166
|
allTypes: Array.from(allTypes).sort(),
|
|
981
1167
|
bundledSdks: Array.from(bundledSdks).sort(),
|
|
@@ -983,110 +1169,1854 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
983
1169
|
binPaths,
|
|
984
1170
|
executables,
|
|
985
1171
|
sharedLibs,
|
|
1172
|
+
services,
|
|
986
1173
|
tools,
|
|
987
1174
|
};
|
|
988
1175
|
}
|
|
989
1176
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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;
|
|
994
1193
|
}
|
|
995
|
-
|
|
996
|
-
|
|
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);
|
|
997
1250
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
)
|
|
1003
|
-
|
|
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;
|
|
1004
1264
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
bundledRuntimes.add(comp.name);
|
|
1265
|
+
if (typeof entity === "string") {
|
|
1266
|
+
return entity.trim() || undefined;
|
|
1008
1267
|
}
|
|
1268
|
+
if (typeof entity === "object" && typeof entity.name === "string") {
|
|
1269
|
+
return entity.name.trim() || undefined;
|
|
1270
|
+
}
|
|
1271
|
+
return undefined;
|
|
1009
1272
|
}
|
|
1010
1273
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (compPurl.qualifiers.distro_name) {
|
|
1027
|
-
tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
|
|
1028
|
-
}
|
|
1029
|
-
if (compPurl.qualifiers.distro) {
|
|
1030
|
-
tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if (tmpPurl.qualifiers) {
|
|
1035
|
-
if (
|
|
1036
|
-
tmpPurl.qualifiers.epoch &&
|
|
1037
|
-
!tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
|
|
1038
|
-
) {
|
|
1039
|
-
tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
// Prevents purls ending with ?
|
|
1043
|
-
if (!Object.keys(tmpPurl.qualifiers).length) {
|
|
1044
|
-
tmpPurl.qualifiers = undefined;
|
|
1045
|
-
}
|
|
1046
|
-
dependsOn.add(decodeURIComponent(tmpPurl.toString()));
|
|
1047
|
-
} catch (_e) {
|
|
1048
|
-
// ignore
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
|
|
1052
|
-
} catch (_e) {
|
|
1053
|
-
// 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 };
|
|
1054
1289
|
}
|
|
1055
|
-
|
|
1056
|
-
};
|
|
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
|
+
}
|
|
1057
1299
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
kind: "osquery",
|
|
1062
|
-
reason:
|
|
1063
|
-
"Dry run mode blocks osquery execution and reports the query instead.",
|
|
1064
|
-
status: "blocked",
|
|
1065
|
-
target: query,
|
|
1066
|
-
});
|
|
1300
|
+
function parseOrganizationalContact(value) {
|
|
1301
|
+
const normalizedValue = `${value || ""}`.trim();
|
|
1302
|
+
if (!normalizedValue) {
|
|
1067
1303
|
return undefined;
|
|
1068
1304
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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")
|
|
1090
3020
|
) {
|
|
1091
3021
|
console.error(result.stdout, result.stderr);
|
|
1092
3022
|
}
|
|
@@ -1231,6 +3161,16 @@ async function fileComponents(basePath, fileList, fileType) {
|
|
|
1231
3161
|
const linkStats = lstatSync(join(basePath, f.replace(/^\/+/, "")));
|
|
1232
3162
|
if (linkStats?.isSymbolicLink()) {
|
|
1233
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
|
+
);
|
|
1234
3174
|
}
|
|
1235
3175
|
} catch (_e) {
|
|
1236
3176
|
// ignore
|
|
@@ -1241,7 +3181,7 @@ async function fileComponents(basePath, fileList, fileType) {
|
|
|
1241
3181
|
let isSetgid;
|
|
1242
3182
|
let isSticky;
|
|
1243
3183
|
try {
|
|
1244
|
-
const stats = statSync(f);
|
|
3184
|
+
const stats = statSync(join(basePath, f.replace(/^\/+/, "")));
|
|
1245
3185
|
const mode = stats.mode;
|
|
1246
3186
|
isExecutable = !!(mode & 0o111);
|
|
1247
3187
|
isSetuid = !!(mode & 0o4000);
|