@cdxgen/cdxgen 13.4.0 → 13.5.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 +7 -7
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +25 -5
- package/bin/evinse.js +5 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +283 -1
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -0
- package/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +28 -3
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +73 -4
- package/lib/cli/jvmBom.js +126 -20
- package/lib/cli/managedBom.js +711 -71
- package/lib/cli/nativeBom.js +225 -28
- package/lib/core/env.js +30 -8
- package/lib/core/fs.js +28 -19
- package/lib/core/httpClient.js +74 -45
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +30 -20
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +229 -44
- package/lib/ecosystems/parsers-python.js +11 -10
- package/lib/ecosystems/rubyutils.js +6 -3
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +2 -1
- package/lib/ecosystems/scalasem.js +3 -1
- package/lib/evinser/evinser.js +126 -18
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +33 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +6 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +500 -58
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +168 -5
- package/lib/inventory/evidenceUtils.js +436 -23
- package/lib/inventory/formulationParsers.js +3 -2
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +89 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +487 -0
- package/lib/inventory/phpHostProbe.js +641 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/source.js +207 -3
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +529 -183
- package/lib/managers/docker.js +166 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +34 -2
- package/lib/server/server.js +186 -1
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +58 -1
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/pregen.js +117 -5
- package/lib/validator/bomValidator.js +26 -5
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +26 -7
- package/lib/validator/generated/validate-1.7.mjs +31 -12
- package/lib/validator/generated/validate-2.0.mjs +30 -11
- package/package.json +17 -17
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +9 -2
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +20 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +18 -2
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +10 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +4 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +25 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +14 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +145 -6
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +40 -2
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +6 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +28 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +10 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +13 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +14 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { platform } from "node:os";
|
|
2
|
-
import { extname } from "node:path";
|
|
2
|
+
import { basename, dirname, extname } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { build } from "@cdxgen/cdx-purl";
|
|
5
5
|
|
|
6
6
|
import { safeExistsSync } from "../core/fs.js";
|
|
7
7
|
import { thoughtLog } from "../core/logger.js";
|
|
8
|
+
import { sanitizeBomUrl } from "../core/propertySanitizer.js";
|
|
9
|
+
import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
|
|
8
10
|
import {
|
|
9
11
|
addDosaiReachabilityProperties,
|
|
10
12
|
readDosaiMethodsReport,
|
|
@@ -20,16 +22,26 @@ import {
|
|
|
20
22
|
} from "./dosaiParsers.js";
|
|
21
23
|
import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
|
|
22
24
|
import { createLolbasProperties } from "./lolbas.js";
|
|
25
|
+
import { isOsPackagePurlType } from "./osPackagePurl.js";
|
|
23
26
|
import {
|
|
24
27
|
createOsQueryFallbackBomRef,
|
|
28
|
+
createOsQueryPackageIdentity,
|
|
25
29
|
createOsQueryPurl,
|
|
26
30
|
deriveOsQueryDescription,
|
|
27
31
|
deriveOsQueryName,
|
|
28
32
|
deriveOsQueryPublisher,
|
|
29
33
|
deriveOsQueryVersion,
|
|
34
|
+
OS_PACKAGE_LICENSE_COLUMNS,
|
|
35
|
+
osPackagePropertyName,
|
|
30
36
|
sanitizeOsQueryIdentity,
|
|
31
37
|
shouldCreateOsQueryPurl,
|
|
32
38
|
} from "./osqueryTransform.js";
|
|
39
|
+
import {
|
|
40
|
+
adjustLicenseInformation,
|
|
41
|
+
findLicenseId,
|
|
42
|
+
isSpdxLicenseExpression,
|
|
43
|
+
spdxLicenses,
|
|
44
|
+
} from "./spdx.js";
|
|
33
45
|
|
|
34
46
|
/**
|
|
35
47
|
* Build a CycloneDX occurrence evidence object from a location string.
|
|
@@ -540,20 +552,55 @@ export function convertOSQueryResults(
|
|
|
540
552
|
enhance = false,
|
|
541
553
|
osPackageListers = undefined,
|
|
542
554
|
) {
|
|
555
|
+
if (queryCategory === "ai_agent_skills") {
|
|
556
|
+
return convertAgentSkillRows(results);
|
|
557
|
+
}
|
|
543
558
|
const pkgList = [];
|
|
559
|
+
// deb, rpm, apk, pacman, Homebrew, MacPorts, snap and flatpak rows describe a
|
|
560
|
+
// package, not a file or an app: the package manager names them, and nothing
|
|
561
|
+
// in such a row is a path inside the package.
|
|
562
|
+
const isOsPackage = isOsPackagePurlType(queryObj.purlType);
|
|
563
|
+
const isLocatedPackage =
|
|
564
|
+
LOCATED_PACKAGE_PURL_TYPES.has(queryObj.purlType) ||
|
|
565
|
+
LOCATED_PACKAGE_CATEGORIES.has(queryCategory);
|
|
544
566
|
if (results?.length) {
|
|
545
567
|
for (const res of results) {
|
|
546
|
-
|
|
568
|
+
let version = deriveOsQueryVersion(res);
|
|
547
569
|
let name = deriveOsQueryName(res, results.length === 1, queryObj.name);
|
|
548
570
|
if (queryObj.purlType === "chrome-extension") {
|
|
549
|
-
|
|
571
|
+
// osquery computes identifier from the manifest key, which an
|
|
572
|
+
// unpacked extension may lack; the profile still knows its ID.
|
|
573
|
+
name = (
|
|
574
|
+
res.identifier ||
|
|
575
|
+
res.referenced_identifier ||
|
|
576
|
+
res.extension_id ||
|
|
577
|
+
name ||
|
|
578
|
+
""
|
|
579
|
+
).toLowerCase();
|
|
580
|
+
}
|
|
581
|
+
let npmScope;
|
|
582
|
+
if (queryObj.purlType === "npm") {
|
|
583
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(`${name || ""}`);
|
|
584
|
+
if (scoped) {
|
|
585
|
+
[, npmScope, name] = scoped;
|
|
586
|
+
}
|
|
550
587
|
}
|
|
551
588
|
let group = "";
|
|
552
|
-
const subpath =
|
|
589
|
+
const subpath =
|
|
590
|
+
isOsPackage || isLocatedPackage
|
|
591
|
+
? undefined
|
|
592
|
+
: res.path || res.admindir || res.source;
|
|
553
593
|
const publisher = deriveOsQueryPublisher(res);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
594
|
+
if (queryObj.purlType === "vscode-extension") {
|
|
595
|
+
// osquery names a VS Code extension by its ID, <publisher>.<name>,
|
|
596
|
+
// which the vscode-extension type splits into namespace and name. Its
|
|
597
|
+
// publisher column is the publisher's display name.
|
|
598
|
+
const extensionId = /^([^.]+)\.(.+)$/u.exec(`${name || ""}`);
|
|
599
|
+
if (extensionId) {
|
|
600
|
+
[, group, name] = extensionId;
|
|
601
|
+
} else if (publisher) {
|
|
602
|
+
group = publisher.toLowerCase();
|
|
603
|
+
}
|
|
557
604
|
}
|
|
558
605
|
let scope;
|
|
559
606
|
const compScope = res.priority;
|
|
@@ -568,25 +615,53 @@ export function convertOSQueryResults(
|
|
|
568
615
|
qualifiers = {
|
|
569
616
|
tag_id: res.identifying_number.replace("{", "").replace("}", ""),
|
|
570
617
|
};
|
|
618
|
+
} else if (queryCategory === "firefox_addons" && res.identifier) {
|
|
619
|
+
// The profile an add-on was found in is not part of its purl, so the
|
|
620
|
+
// add-on ID is what tells apart two add-ons that share a name.
|
|
621
|
+
qualifiers = { tag_id: firefoxAddonTagId(res.identifier) };
|
|
571
622
|
}
|
|
572
623
|
if (name) {
|
|
573
624
|
name = sanitizeOsQueryIdentity(name);
|
|
574
|
-
|
|
625
|
+
// sanitizeOsQueryIdentity drops a leading @, which an npm scope keeps.
|
|
626
|
+
group = npmScope
|
|
627
|
+
? `@${sanitizeOsQueryIdentity(npmScope)}`
|
|
628
|
+
: sanitizeOsQueryIdentity(group);
|
|
575
629
|
const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
630
|
+
let purl;
|
|
631
|
+
if (isOsPackage) {
|
|
632
|
+
// The version of such a row is part of its identity: an rpm's is its
|
|
633
|
+
// version and release, behind its epoch.
|
|
634
|
+
const identity = createOsQueryPackageIdentity(
|
|
635
|
+
queryObj.purlType,
|
|
636
|
+
res,
|
|
637
|
+
name,
|
|
638
|
+
);
|
|
639
|
+
version = identity.version;
|
|
640
|
+
purl = shouldCreateOsQueryPurl(queryObj.componentType)
|
|
641
|
+
? identity.purl
|
|
642
|
+
: undefined;
|
|
643
|
+
} else if (shouldCreateOsQueryPurl(queryObj.componentType)) {
|
|
644
|
+
purl = createOsQueryPurl(
|
|
645
|
+
queryObj.purlType,
|
|
646
|
+
group,
|
|
647
|
+
name,
|
|
648
|
+
version,
|
|
649
|
+
qualifiers,
|
|
650
|
+
subpath,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
586
653
|
const props = [{ name: "cdx:osquery:category", value: queryCategory }];
|
|
587
654
|
props.push(...createLolbasProperties(queryCategory, res));
|
|
588
655
|
if (platform() === "linux") {
|
|
589
|
-
|
|
656
|
+
// The GTFOBins matcher reads a description for command names, and
|
|
657
|
+
// the free text of a package's description matches by chance: zlib's
|
|
658
|
+
// "the deflate compression method found in gzip" is no use of gzip.
|
|
659
|
+
props.push(
|
|
660
|
+
...createGtfoBinsPropertiesFromRow(
|
|
661
|
+
queryCategory,
|
|
662
|
+
isOsPackage ? { ...res, description: undefined } : res,
|
|
663
|
+
),
|
|
664
|
+
);
|
|
590
665
|
}
|
|
591
666
|
let providesList;
|
|
592
667
|
if (enhance) {
|
|
@@ -620,10 +695,12 @@ export function convertOSQueryResults(
|
|
|
620
695
|
version: version || "",
|
|
621
696
|
description,
|
|
622
697
|
publisher,
|
|
698
|
+
// Without a purl, the bom-ref is built from the row, and the path of
|
|
699
|
+
// a located package is not part of what it is.
|
|
623
700
|
"bom-ref": createOsQueryBomRef(
|
|
624
701
|
queryCategory,
|
|
625
702
|
queryObj.componentType,
|
|
626
|
-
res,
|
|
703
|
+
isLocatedPackage ? { ...res, path: undefined } : res,
|
|
627
704
|
name,
|
|
628
705
|
version,
|
|
629
706
|
purl,
|
|
@@ -644,25 +721,361 @@ export function convertOSQueryResults(
|
|
|
644
721
|
if (cryptoProperties) {
|
|
645
722
|
apkg.cryptoProperties = cryptoProperties;
|
|
646
723
|
}
|
|
724
|
+
const licenseColumns = isOsPackage
|
|
725
|
+
? OS_PACKAGE_LICENSE_COLUMNS[queryObj.purlType]
|
|
726
|
+
: undefined;
|
|
727
|
+
if (licenseColumns) {
|
|
728
|
+
const licenses = osPackageLicenses(
|
|
729
|
+
res[licenseColumns.license],
|
|
730
|
+
licenseColumns.separator,
|
|
731
|
+
);
|
|
732
|
+
if (licenses.length) {
|
|
733
|
+
apkg.licenses = licenses;
|
|
734
|
+
}
|
|
735
|
+
const website = licenseColumns.website
|
|
736
|
+
? osPackageWebsite(res[licenseColumns.website])
|
|
737
|
+
: undefined;
|
|
738
|
+
if (website) {
|
|
739
|
+
apkg.externalReferences = [{ type: "website", url: website }];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
647
742
|
for (const k of Object.keys(res).filter((p) => {
|
|
648
743
|
if (["version", "description", "publisher"].includes(p)) {
|
|
649
744
|
return false;
|
|
650
745
|
}
|
|
651
746
|
return !(queryObj.purlType !== "chrome-extension" && p === "name");
|
|
652
747
|
})) {
|
|
653
|
-
|
|
748
|
+
const propertyName = isOsPackage
|
|
749
|
+
? osPackagePropertyName(queryObj.purlType, k)
|
|
750
|
+
: k;
|
|
751
|
+
// (none) is what rpm's database says for a tag a record lacks, such
|
|
752
|
+
// as the source rpm of a key record.
|
|
753
|
+
if (
|
|
754
|
+
propertyName &&
|
|
755
|
+
res[k] &&
|
|
756
|
+
res[k] !== "null" &&
|
|
757
|
+
!(isOsPackage && res[k] === "(none)")
|
|
758
|
+
) {
|
|
654
759
|
props.push({
|
|
655
|
-
name:
|
|
760
|
+
name: propertyName,
|
|
656
761
|
value: res[k],
|
|
657
762
|
});
|
|
658
763
|
}
|
|
659
764
|
}
|
|
765
|
+
if (isLocatedPackage) {
|
|
766
|
+
// osquery writes a JSON null as "null", which is the path Firefox
|
|
767
|
+
// gives its default theme.
|
|
768
|
+
const occurrence = createOccurrenceEvidence(
|
|
769
|
+
[res.path, res.directory].find(
|
|
770
|
+
(value) => value && value !== "null",
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
if (occurrence) {
|
|
774
|
+
apkg.evidence = { occurrences: [occurrence] };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
660
777
|
apkg.properties = props;
|
|
661
778
|
pkgList.push(apkg);
|
|
662
779
|
}
|
|
663
780
|
}
|
|
664
781
|
}
|
|
665
|
-
return pkgList;
|
|
782
|
+
return isLocatedPackage ? mergeLocatedPackages(pkgList) : pkgList;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// npm and pypi rows name a package found at a path on the host, Chromium and
|
|
786
|
+
// VS Code extension rows an extension found in a browser profile or an editor's
|
|
787
|
+
// extensions folder, and a firefox_addons row an add-on found in a Firefox
|
|
788
|
+
// profile. Where it was found is not part of what it is, so the path is an
|
|
789
|
+
// occurrence rather than a subpath: a home directory stays out of the purl, and
|
|
790
|
+
// one package installed in two places is one component with two occurrences.
|
|
791
|
+
const LOCATED_PACKAGE_PURL_TYPES = new Set([
|
|
792
|
+
"npm",
|
|
793
|
+
"pypi",
|
|
794
|
+
"chrome-extension",
|
|
795
|
+
"vscode-extension",
|
|
796
|
+
]);
|
|
797
|
+
const LOCATED_PACKAGE_CATEGORIES = new Set(["firefox_addons"]);
|
|
798
|
+
|
|
799
|
+
const FIREFOX_ADDON_GUID =
|
|
800
|
+
/^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/iu;
|
|
801
|
+
|
|
802
|
+
// A Firefox add-on ID is an email-like string or a GUID in braces. A swid
|
|
803
|
+
// tag_id that is a GUID must be in lower case, and is written without braces.
|
|
804
|
+
function firefoxAddonTagId(identifier) {
|
|
805
|
+
const id = `${identifier}`.trim();
|
|
806
|
+
const guid = FIREFOX_ADDON_GUID.exec(id);
|
|
807
|
+
return guid ? guid[1].toLowerCase() : id;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function mergeLocatedPackages(components) {
|
|
811
|
+
const byRef = new Map();
|
|
812
|
+
for (const component of components) {
|
|
813
|
+
const existing = byRef.get(component["bom-ref"]);
|
|
814
|
+
if (!existing) {
|
|
815
|
+
byRef.set(component["bom-ref"], component);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const occurrences = existing.evidence?.occurrences || [];
|
|
819
|
+
for (const occurrence of component.evidence?.occurrences || []) {
|
|
820
|
+
if (
|
|
821
|
+
!occurrences.some((known) => known.location === occurrence.location)
|
|
822
|
+
) {
|
|
823
|
+
occurrences.push(occurrence);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (occurrences.length) {
|
|
827
|
+
existing.evidence = { ...existing.evidence, occurrences };
|
|
828
|
+
}
|
|
829
|
+
for (const property of component.properties) {
|
|
830
|
+
if (
|
|
831
|
+
!existing.properties.some(
|
|
832
|
+
(known) =>
|
|
833
|
+
known.name === property.name && known.value === property.value,
|
|
834
|
+
)
|
|
835
|
+
) {
|
|
836
|
+
existing.properties.push(property);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const component of byRef.values()) {
|
|
841
|
+
component.evidence?.occurrences?.sort((a, b) =>
|
|
842
|
+
a.location.localeCompare(b.location),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
return [...byRef.values()];
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// The licence an OS package declares. An apk `L:` field is a list of SPDX ids
|
|
849
|
+
// separated by spaces on older packages and an SPDX expression on newer ones;
|
|
850
|
+
// pacman keeps one licence per entry, each an SPDX id or expression, which
|
|
851
|
+
// osquery joins with commas; a snap or a flatpak declares one expression. Only
|
|
852
|
+
// an explicit operator or a parenthesis makes a text an expression: the SPDX
|
|
853
|
+
// parser's own check takes the `or` inside `LGPL-2.0-or-later` for an operator,
|
|
854
|
+
// and would turn a plain list into one expression. A token is an id only when
|
|
855
|
+
// the SPDX list has what it maps to; any other is kept by name rather than
|
|
856
|
+
// dropped.
|
|
857
|
+
const MAX_OS_PACKAGE_LICENSES = 64;
|
|
858
|
+
const SPDX_OPERATORS = new Set(["AND", "OR", "WITH"]);
|
|
859
|
+
|
|
860
|
+
function isExplicitSpdxExpression(text) {
|
|
861
|
+
// No SPDX expression holds a comma, but the expression parser lets one
|
|
862
|
+
// through, which would take a whole pacman licence list for one expression.
|
|
863
|
+
return (
|
|
864
|
+
!text.includes(",") &&
|
|
865
|
+
(text.split(/\s+/u).some((token) => SPDX_OPERATORS.has(token)) ||
|
|
866
|
+
text.includes("(")) &&
|
|
867
|
+
isSpdxLicenseExpression(text)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function osPackageLicenses(text, separator) {
|
|
872
|
+
const license = `${text ?? ""}`.split(/\s+/u).filter(Boolean).join(" ");
|
|
873
|
+
if (!license) {
|
|
874
|
+
return [];
|
|
875
|
+
}
|
|
876
|
+
if (isExplicitSpdxExpression(license)) {
|
|
877
|
+
return [{ expression: license }];
|
|
878
|
+
}
|
|
879
|
+
const entries = [];
|
|
880
|
+
const items = separator ? license.split(separator) : [license];
|
|
881
|
+
for (const item of items.slice(0, MAX_OS_PACKAGE_LICENSES)) {
|
|
882
|
+
const entry = item.trim();
|
|
883
|
+
if (!entry || SPDX_OPERATORS.has(entry)) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (isExplicitSpdxExpression(entry)) {
|
|
887
|
+
entries.push({ expression: entry });
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const id = findLicenseId(entry);
|
|
891
|
+
entries.push(
|
|
892
|
+
spdxLicenses.includes(id)
|
|
893
|
+
? { license: { id } }
|
|
894
|
+
: { license: { name: entry } },
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
// A list that holds an expression must become one expression, since a
|
|
898
|
+
// component's licenses are either licences or a single expression. Every
|
|
899
|
+
// entry applies, so they are joined with AND.
|
|
900
|
+
if (entries.length > 1 && entries.some((entry) => entry.expression)) {
|
|
901
|
+
const parts = entries.map((entry) =>
|
|
902
|
+
entry.expression ? `(${entry.expression})` : entry.license.id,
|
|
903
|
+
);
|
|
904
|
+
const expression = parts.join(" AND ");
|
|
905
|
+
if (parts.every(Boolean) && isSpdxLicenseExpression(expression)) {
|
|
906
|
+
return [{ expression }];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return adjustLicenseInformation(entries);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// The home page of a package. A URL a package carries is untrusted text: only
|
|
913
|
+
// an http(s) URL is kept, without credentials, query or fragment.
|
|
914
|
+
function osPackageWebsite(value) {
|
|
915
|
+
const cleaned = sanitizeBomUrl(`${value ?? ""}`.trim());
|
|
916
|
+
try {
|
|
917
|
+
const { protocol } = new URL(cleaned);
|
|
918
|
+
return protocol === "http:" || protocol === "https:" ? cleaned : undefined;
|
|
919
|
+
} catch {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Convert the packages of an apk installed database into components.
|
|
926
|
+
*
|
|
927
|
+
* The packages are named as the container scan names an Alpine, Wolfi or
|
|
928
|
+
* Chainguard package, and grouped with the other OS package categories under
|
|
929
|
+
* `cdx:osquery:category` = `apk_packages`. Beyond what an osquery package row
|
|
930
|
+
* gives, an apk record knows its licence and home page, which become
|
|
931
|
+
* `licenses` and a `website` external reference.
|
|
932
|
+
*
|
|
933
|
+
* @param {Object[]} packages Packages from readApkInstalledDb()
|
|
934
|
+
* @returns {Object[]} Components
|
|
935
|
+
*/
|
|
936
|
+
export function convertApkPackages(packages) {
|
|
937
|
+
const components = [];
|
|
938
|
+
for (const pkg of packages || []) {
|
|
939
|
+
const row = { name: pkg.name, version: pkg.version };
|
|
940
|
+
for (const column of ["arch", "origin", "maintainer"]) {
|
|
941
|
+
if (pkg[column]) {
|
|
942
|
+
row[column] = pkg[column];
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
const [component] = convertOSQueryResults(
|
|
946
|
+
"apk_packages",
|
|
947
|
+
{ purlType: "apk" },
|
|
948
|
+
[row],
|
|
949
|
+
);
|
|
950
|
+
if (!component) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
// The description is set here and not in the row. The GTFOBins matcher reads
|
|
954
|
+
// a row's description for command names, which a deb or rpm row has no
|
|
955
|
+
// column for, and the free text of an apk description matches by chance:
|
|
956
|
+
// "Alpine base dir structure" is no use of the alpine mail client.
|
|
957
|
+
if (pkg.description) {
|
|
958
|
+
component.description = pkg.description;
|
|
959
|
+
}
|
|
960
|
+
const licenses = osPackageLicenses(pkg.license, /\s+/u);
|
|
961
|
+
if (licenses.length) {
|
|
962
|
+
component.licenses = licenses;
|
|
963
|
+
}
|
|
964
|
+
const website = osPackageWebsite(pkg.url);
|
|
965
|
+
if (website) {
|
|
966
|
+
component.externalReferences = [{ type: "website", url: website }];
|
|
967
|
+
}
|
|
968
|
+
components.push(component);
|
|
969
|
+
}
|
|
970
|
+
return components;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// The frontmatter and osquery columns of a skill row that become properties,
|
|
974
|
+
// named as the ai-skill project scan names them where it has a name.
|
|
975
|
+
const AGENT_SKILL_PROPERTY_NAMES = [
|
|
976
|
+
["name", "cdx:skill:name"],
|
|
977
|
+
["description", "cdx:skill:description"],
|
|
978
|
+
["license", "cdx:skill:license"],
|
|
979
|
+
["compatibility", "cdx:skill:compatibility"],
|
|
980
|
+
["agent", "cdx:skill:agent"],
|
|
981
|
+
["scope", "cdx:skill:scope"],
|
|
982
|
+
["allowed_tools", "cdx:skill:allowedTools"],
|
|
983
|
+
["resource_count", "cdx:skill:resourceCount"],
|
|
984
|
+
["script_count", "cdx:skill:scriptCount"],
|
|
985
|
+
["username", "username"],
|
|
986
|
+
];
|
|
987
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/u;
|
|
988
|
+
|
|
989
|
+
// A skill's licence is free text ("Complete terms in LICENSE.txt"), so it
|
|
990
|
+
// becomes `licenses` only when it is an SPDX id or expression.
|
|
991
|
+
function agentSkillLicenses(text) {
|
|
992
|
+
const license = `${text ?? ""}`.trim();
|
|
993
|
+
if (!license) {
|
|
994
|
+
return [];
|
|
995
|
+
}
|
|
996
|
+
if (isExplicitSpdxExpression(license)) {
|
|
997
|
+
return [{ expression: license }];
|
|
998
|
+
}
|
|
999
|
+
const id = findLicenseId(license);
|
|
1000
|
+
return spdxLicenses.includes(id) ? [{ license: { id } }] : [];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Convert the rows of the `ai_agent_skills` query into components.
|
|
1005
|
+
*
|
|
1006
|
+
* A skill installed on the host is what the `ai-skill` project scan finds in a
|
|
1007
|
+
* repository, so it is a `file` component with the same `cdx:file:kind` and
|
|
1008
|
+
* `cdx:skill:*` properties, and no purl: a skill has no package identity. Its
|
|
1009
|
+
* bom-ref is the SHA-256 of its SKILL.md rather than the path, which is under a
|
|
1010
|
+
* home directory, and where it was found is an occurrence. The same skill
|
|
1011
|
+
* found under several roots, such as a plugin marketplace checkout and the
|
|
1012
|
+
* plugin cache, is one component with one occurrence for each.
|
|
1013
|
+
*
|
|
1014
|
+
* @param {Object[]} results Rows of `ai_agent_skills` joined with `hash`
|
|
1015
|
+
* @returns {Object[]} Components
|
|
1016
|
+
*/
|
|
1017
|
+
export function convertAgentSkillRows(results) {
|
|
1018
|
+
const components = [];
|
|
1019
|
+
for (const row of results || []) {
|
|
1020
|
+
const skillFile = `${row.path ?? ""}`;
|
|
1021
|
+
const directory = `${row.directory ?? ""}` || dirname(skillFile);
|
|
1022
|
+
// A SKILL.md whose frontmatter osquery cannot read has no name; the
|
|
1023
|
+
// project scan names such a skill after its directory too.
|
|
1024
|
+
const name = sanitizeOsQueryIdentity(row.name || basename(directory));
|
|
1025
|
+
if (!skillFile || !name) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const version = row.version || "";
|
|
1029
|
+
const sha256 = SHA256_HEX.test(`${row.sha256 ?? ""}`)
|
|
1030
|
+
? row.sha256
|
|
1031
|
+
: undefined;
|
|
1032
|
+
const properties = [
|
|
1033
|
+
{ name: "cdx:osquery:category", value: "ai_agent_skills" },
|
|
1034
|
+
{ name: "cdx:file:kind", value: "skill-file" },
|
|
1035
|
+
{ name: "cdx:agent:inventorySource", value: "osquery" },
|
|
1036
|
+
];
|
|
1037
|
+
for (const [column, propertyName] of AGENT_SKILL_PROPERTY_NAMES) {
|
|
1038
|
+
const value = `${row[column] ?? ""}`.trim();
|
|
1039
|
+
if (value && value !== "null") {
|
|
1040
|
+
properties.push({ name: propertyName, value });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (Number(row.script_count) > 0) {
|
|
1044
|
+
properties.push({ name: "cdx:skill:hasScripts", value: "true" });
|
|
1045
|
+
}
|
|
1046
|
+
properties.push({
|
|
1047
|
+
name: "cdx:skill:specValid",
|
|
1048
|
+
value: isAgentSkillSpecValid(row) ? "true" : "false",
|
|
1049
|
+
});
|
|
1050
|
+
const component = {
|
|
1051
|
+
name,
|
|
1052
|
+
version,
|
|
1053
|
+
description: row.description || "",
|
|
1054
|
+
type: "file",
|
|
1055
|
+
"bom-ref": createOsQueryFallbackBomRef(
|
|
1056
|
+
"ai_agent_skills",
|
|
1057
|
+
"file",
|
|
1058
|
+
name,
|
|
1059
|
+
version,
|
|
1060
|
+
sha256 ? "sha256" : "scope",
|
|
1061
|
+
sha256 || `${row.agent || ""}/${row.scope || ""}`,
|
|
1062
|
+
),
|
|
1063
|
+
properties,
|
|
1064
|
+
};
|
|
1065
|
+
if (sha256) {
|
|
1066
|
+
component.hashes = [{ alg: "SHA-256", content: sha256 }];
|
|
1067
|
+
}
|
|
1068
|
+
const licenses = agentSkillLicenses(row.license);
|
|
1069
|
+
if (licenses.length) {
|
|
1070
|
+
component.licenses = licenses;
|
|
1071
|
+
}
|
|
1072
|
+
const occurrence = createOccurrenceEvidence(skillFile);
|
|
1073
|
+
if (occurrence) {
|
|
1074
|
+
component.evidence = { occurrences: [occurrence] };
|
|
1075
|
+
}
|
|
1076
|
+
components.push(component);
|
|
1077
|
+
}
|
|
1078
|
+
return mergeLocatedPackages(components);
|
|
666
1079
|
}
|
|
667
1080
|
function createOsQueryCryptoProperties(queryCategory, res, version) {
|
|
668
1081
|
switch (queryCategory) {
|
|
@@ -285,8 +285,9 @@ export function addFormulationSection(filePath, options, context = {}) {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
// Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby
|
|
289
|
-
|
|
288
|
+
// Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby,
|
|
289
|
+
// PHP, Composer)
|
|
290
|
+
const infoComponents = collectEnvInfo(projectPath, options);
|
|
290
291
|
if (infoComponents?.length) {
|
|
291
292
|
components = components.concat(infoComponents);
|
|
292
293
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Bounded Go buildinfo reader for executable file components: recovers the
|
|
2
|
+
// toolchain version a binary was built with, so a BOM can answer "which Go
|
|
3
|
+
// programs predate the CVE-2026-111943 distrust list". Only the header and
|
|
4
|
+
// the inline version string are read; pointer-based (pre-1.18) layouts are
|
|
5
|
+
// reported as such and never followed.
|
|
6
|
+
//
|
|
7
|
+
// Layout: go/src/debug/buildinfo/buildinfo.go at tag go1.27.2. The 14-byte
|
|
8
|
+
// magic `\xff Go buildinf:` is 16-byte aligned; the next two bytes are the
|
|
9
|
+
// pointer size and flags. When flags&2 is set the version string follows as a
|
|
10
|
+
// uvarint length plus bytes. The magic can appear anywhere in the file's
|
|
11
|
+
// initial segment, so the search scans aligned offsets up to a cap.
|
|
12
|
+
|
|
13
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const BUILD_INFO_MAGIC = Buffer.from([
|
|
16
|
+
0xff, 0x20, 0x47, 0x6f, 0x20, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x66,
|
|
17
|
+
0x3a,
|
|
18
|
+
]);
|
|
19
|
+
// The buildinfo section follows text and rodata, often late in the file
|
|
20
|
+
// (37 MB into a 40 MB gh binary), so the cap must cover large binaries.
|
|
21
|
+
const MAX_SEARCH_BYTES = 512 * 1024 * 1024;
|
|
22
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
23
|
+
const MAX_VERSION_BYTES = 128;
|
|
24
|
+
export const PRE_1_18_VERSION = "unknown-pre-1.18";
|
|
25
|
+
|
|
26
|
+
function readUvarint(buffer, offset) {
|
|
27
|
+
let value = 0;
|
|
28
|
+
let shift = 0;
|
|
29
|
+
for (let i = 0; i < 10 && offset + i < buffer.length; i++) {
|
|
30
|
+
const byte = buffer[offset + i];
|
|
31
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
32
|
+
if (!(byte & 0x80)) {
|
|
33
|
+
return { value, next: offset + i + 1 };
|
|
34
|
+
}
|
|
35
|
+
shift += 7;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseHeaderAt(buffer, offset, limit) {
|
|
41
|
+
if (offset + 32 > limit) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < BUILD_INFO_MAGIC.length; i++) {
|
|
45
|
+
if (buffer[offset + i] !== BUILD_INFO_MAGIC[i]) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const pointerSize = buffer[offset + 14];
|
|
50
|
+
const flags = buffer[offset + 15];
|
|
51
|
+
if (pointerSize !== 4 && pointerSize !== 8) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (!(flags & 0x02)) {
|
|
55
|
+
// Version lives behind pointers (pre-1.18 layout): not followed.
|
|
56
|
+
return PRE_1_18_VERSION;
|
|
57
|
+
}
|
|
58
|
+
// The inline strings start 16 bytes past the magic header (magic 14 +
|
|
59
|
+
// pointerSize 1 + flags 1, then a reserved 16-byte slot), as a uvarint
|
|
60
|
+
// length followed by the bytes.
|
|
61
|
+
const varint = readUvarint(buffer, offset + 32);
|
|
62
|
+
if (
|
|
63
|
+
!varint ||
|
|
64
|
+
varint.value === 0 ||
|
|
65
|
+
varint.value > MAX_VERSION_BYTES ||
|
|
66
|
+
varint.next + varint.value > limit
|
|
67
|
+
) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const version = buffer.toString(
|
|
71
|
+
"latin1",
|
|
72
|
+
varint.next,
|
|
73
|
+
varint.next + varint.value,
|
|
74
|
+
);
|
|
75
|
+
if (!/^go[0-9]/.test(version)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return version;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the Go toolchain version from an executable's buildinfo header.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} filePath absolute path to a possible Go binary
|
|
85
|
+
* @returns {string|undefined} `go1.x.y`, `unknown-pre-1.18` for old layouts,
|
|
86
|
+
* or undefined when the file carries no Go buildinfo
|
|
87
|
+
*/
|
|
88
|
+
export function readGoBuildVersion(filePath) {
|
|
89
|
+
if (typeof filePath !== "string" || !filePath) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
let fd;
|
|
93
|
+
try {
|
|
94
|
+
fd = openSync(filePath, "r");
|
|
95
|
+
const size = fstatSync(fd).size;
|
|
96
|
+
if (size < 32) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const searchLimit = Math.min(size, MAX_SEARCH_BYTES);
|
|
100
|
+
// Each chunk examines the aligned offsets that start inside its 64 KiB
|
|
101
|
+
// window, reading a fixed overlap past it so a header near the window
|
|
102
|
+
// end is parsed whole; the next chunk starts at the next window.
|
|
103
|
+
const OVERLAP_BYTES = 256;
|
|
104
|
+
const chunk = Buffer.alloc(READ_CHUNK_BYTES + OVERLAP_BYTES);
|
|
105
|
+
for (
|
|
106
|
+
let position = 0;
|
|
107
|
+
position < searchLimit;
|
|
108
|
+
position += READ_CHUNK_BYTES
|
|
109
|
+
) {
|
|
110
|
+
const want = Math.min(READ_CHUNK_BYTES + OVERLAP_BYTES, size - position);
|
|
111
|
+
const read = readSync(fd, chunk, 0, want, position);
|
|
112
|
+
if (read < 32) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
// position is a multiple of 16, so chunk offsets stay on the file's
|
|
116
|
+
// 16-byte grid.
|
|
117
|
+
for (
|
|
118
|
+
let offset = 0;
|
|
119
|
+
offset < READ_CHUNK_BYTES && offset + 32 <= read;
|
|
120
|
+
offset += 16
|
|
121
|
+
) {
|
|
122
|
+
const version = parseHeaderAt(chunk, offset, read);
|
|
123
|
+
if (version) {
|
|
124
|
+
return version;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
} finally {
|
|
132
|
+
if (fd !== undefined) {
|
|
133
|
+
try {
|
|
134
|
+
closeSync(fd);
|
|
135
|
+
} catch {
|
|
136
|
+
// already closed
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|