@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,517 lines (1,464 loc) • 381 kB
JavaScript
import { Buffer } from "node:buffer";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { platform, tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";
import esmock from "esmock";
import { PackageURL } from "packageurl-js";
import { assert, describe, it, test } from "poku";
import sinon from "sinon";
import { parse } from "ssri";
import { parse as loadYaml } from "yaml";
import { validateRefs } from "../validator/bomValidator.js";
import {
addEvidenceForDotnet,
addEvidenceForImports,
attachIdentityTools,
buildObjectForCocoaPod,
cdxgenAgent,
collectExecutables,
collectSharedLibs,
convertOSQueryResults,
encodeForPurl,
extractToolRefs,
findLicenseId,
findPnpmPackagePath,
getAllFiles,
getCratesMetadata,
getDartMetadata,
getDefaultBomAuditCategories,
getLicenses,
getMvnMetadata,
getPropertyGroupTextNodes,
getPyMetadata,
getRecordedActivities,
guessPypiMatchingVersion,
hasAnyProjectType,
inferJarGroupFromManifest,
isAllowedHttpHost,
isDryRunError,
isPackageManagerAllowed,
isPartialTree,
isValidIriReference,
mapConanPkgRefToPurlStringAndNameAndVersion,
PROJECT_TYPE_ALIASES,
parseBazelActionGraph,
parseBazelBuild,
parseBazelSkyframe,
parseBdistMetadata,
parseBitbucketPipelinesFile,
parseBowerJson,
parseCabalData,
parseCargoAuditableData,
parseCargoData,
parseCargoDependencyData,
parseCargoManifestDependencyData,
parseCargoTomlData,
parseCljDep,
parseCloudBuildData,
parseCmakeDotFile,
parseCmakeLikeFile,
parseCocoaDependency,
parseColliderLockData,
parseComposerJson,
parseComposerLock,
parseConanData,
parseConanLockData,
parseContainerFile,
parseContainerSpecData,
parseCsPkgData,
parseCsPkgLockData,
parseCsProjAssetsData,
parseCsProjData,
parseEdnData,
parseFlakeLock,
parseFlakeNix,
parseGemfileLockData,
parseGemspecData,
parseGitHubWorkflowData,
parseGoListDep,
parseGoModData,
parseGoModGraph,
parseGoModulesTxt,
parseGoModWhy,
parseGopkgData,
parseGosumData,
parseGoVersionData,
parseHelmYamlData,
parseJarManifest,
parseKVDep,
parseLeinDep,
parseLeiningenData,
parseMakeDFile,
parseMavenArgs,
parseMavenTree,
parseMavenTreeJson,
parseMillDependency,
parseMinJs,
parseMixLockData,
parseNodeShrinkwrap,
parseNupkg,
parseNuspecData,
parseOpenapiSpecData,
parsePackageJsonName,
parsePaketLockData,
parsePiplockData,
parsePixiLockFile,
parsePixiTomlFile,
parsePkgJson,
parsePkgLock,
parsePnpmLock,
parsePnpmWorkspace,
parsePodfileLock,
parsePodfileTargets,
parsePom,
parsePomProperties,
parsePrivadoFile,
parsePubLockData,
parsePubYamlData,
parsePyLockData,
parsePyProjectTomlFile,
parsePyRequiresDist,
parseReqEnvMarkers,
parseReqFile,
parseSetupPyFile,
parseSwiftJsonTree,
parseSwiftResolved,
parseYarnLock,
pnpmMetadata,
purlFromUrlString,
readEnvironmentVariable,
readZipEntry,
recordSensitiveFileRead,
recordSymlinkResolution,
resetRecordedActivities,
safeExistsSync,
safeMkdtempSync,
safeRmSync,
safeSpawnSync,
safeUnlinkSync,
safeWriteSync,
setDryRunMode,
shouldRunPredictiveBomAudit,
toGemModuleNames,
trimJarGroupSuffix,
yarnLockToIdentMap,
} from "./utils.js";
const jarMetadataFixturesDir = path.resolve("test", "data", "jar-metadata");
function createMockedProcess(envOverrides = {}) {
const env = {
...process.env,
};
for (const [key, value] of Object.entries(envOverrides)) {
if (value === undefined) {
delete env[key];
} else {
env[key] = value;
}
}
const mockedProcess = Object.create(process);
mockedProcess.argv = [...process.argv];
mockedProcess.env = env;
return mockedProcess;
}
function readJarMetadataFixture(...segments) {
return readFileSync(path.join(jarMetadataFixturesDir, ...segments), {
encoding: "utf-8",
});
}
it("SSRI test", () => {
// gopkg.lock hash
let ss = parse(
"2ca532a6bc655663344004ba102436d29031018eab236247678db1d8978627bf",
);
assert.deepStrictEqual(ss, null);
ss = parse(
"sha256-2ca532a6bc655663344004ba102436d29031018eab236247678db1d8978627bf",
);
assert.deepStrictEqual(
ss.sha256[0].digest,
"2ca532a6bc655663344004ba102436d29031018eab236247678db1d8978627bf",
);
ss = parse(
`sha256-${Buffer.from(
"2ca532a6bc655663344004ba102436d29031018eab236247678db1d8978627bf",
"hex",
).toString("base64")}`,
);
assert.deepStrictEqual(
ss.sha256[0].digest,
"LKUyprxlVmM0QAS6ECQ20pAxAY6rI2JHZ42x2JeGJ78=",
);
ss = parse(
"sha512-Vn0lE2mprXEFPcRoI89xjw1fk1VJiyVbwfaPnVnvCXxEieByioO8Mj6sMwa6ON9PRuqbAjIxaQpkzccu41sYlw==",
);
assert.deepStrictEqual(
ss.sha512[0].digest,
"Vn0lE2mprXEFPcRoI89xjw1fk1VJiyVbwfaPnVnvCXxEieByioO8Mj6sMwa6ON9PRuqbAjIxaQpkzccu41sYlw==",
);
});
it("Parse requires dist string", () => {
assert.deepStrictEqual(
parsePyRequiresDist("lazy-object-proxy (>=1.4.0)"),
{
name: "lazy-object-proxy",
version: "1.4.0",
},
);
assert.deepStrictEqual(parsePyRequiresDist("wrapt (<1.13,>=1.11)"), {
name: "wrapt",
version: "1.13",
});
assert.deepStrictEqual(
parsePyRequiresDist(
'typed-ast (<1.5,>=1.4.0) ; implementation_name == "cpython" and python_version < "3.8"',
),
{ name: "typed-ast", version: "1.5" },
);
assert.deepStrictEqual(parsePyRequiresDist("asgiref (<4,>=3.2.10)"), {
name: "asgiref",
version: "4",
});
assert.deepStrictEqual(parsePyRequiresDist("pytz"), {
name: "pytz",
version: "",
});
assert.deepStrictEqual(parsePyRequiresDist("sqlparse (>=0.2.2)"), {
name: "sqlparse",
version: "0.2.2",
});
assert.deepStrictEqual(
parsePyRequiresDist("argon2-cffi (>=16.1.0) ; extra == 'argon2'"),
{ name: "argon2-cffi", version: "16.1.0" },
);
assert.deepStrictEqual(parsePyRequiresDist("bcrypt ; extra == 'bcrypt'"), {
name: "bcrypt",
version: "",
});
});
it("finds license id from name", () => {
assert.deepStrictEqual(
findLicenseId("Apache License Version 2.0"),
"Apache-2.0",
);
assert.deepStrictEqual(
findLicenseId("GNU General Public License (GPL) version 2.0"),
"GPL-2.0-only",
);
});
it("safeSpawnSync() resets ANSI color state for host pip warnings", () => {
const originalConsoleWarn = console.warn;
const originalContainer = process.env.CDXGEN_IN_CONTAINER;
const originalNoticeCache = globalThis.__cdxgenNoticeCache;
const warnings = [];
delete process.env.CDXGEN_IN_CONTAINER;
delete globalThis.__cdxgenNoticeCache;
console.warn = (message) => {
warnings.push(message);
};
try {
safeSpawnSync("pip-cdxgen-test", ["install"], {});
assert.strictEqual(warnings.length, 1);
assert.ok(
warnings[0].startsWith(
"\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'.",
),
);
assert.ok(warnings[0].endsWith("\x1b[0m"));
assert.ok(!warnings[0].endsWith("\x1b"));
} finally {
console.warn = originalConsoleWarn;
if (originalContainer === undefined) {
delete process.env.CDXGEN_IN_CONTAINER;
} else {
process.env.CDXGEN_IN_CONTAINER = originalContainer;
}
if (originalNoticeCache === undefined) {
delete globalThis.__cdxgenNoticeCache;
} else {
globalThis.__cdxgenNoticeCache = originalNoticeCache;
}
}
});
it("safeSpawnSync() returns a dry-run sentinel result when dry run mode is enabled", () => {
setDryRunMode(true);
resetRecordedActivities();
try {
const result = safeSpawnSync("node", ["--version"], {});
assert.strictEqual(result.status, 1);
assert.ok(isDryRunError(result.error));
const executeActivity = getRecordedActivities().find(
(activity) => activity.kind === "execute",
);
assert.ok(executeActivity);
assert.strictEqual(executeActivity.status, "blocked");
} finally {
setDryRunMode(false);
resetRecordedActivities();
}
});
it("safeSpawnSync() does not classify non-probe -v commands as version checks", () => {
setDryRunMode(true);
resetRecordedActivities();
try {
safeSpawnSync("swift", ["package", "-v", "resolve"], {});
const executeActivity = getRecordedActivities().find(
(activity) => activity.kind === "execute",
);
assert.ok(executeActivity);
assert.strictEqual(executeActivity.probeType, undefined);
assert.ok(!/version check/i.test(executeActivity.reason));
} finally {
setDryRunMode(false);
resetRecordedActivities();
}
});
it("safeSpawnSync() blocks shell metacharacters with shell execution", () => {
const markerFile = path.join(tmpdir(), "cdxgen-safe-spawn-shell-marker");
const originalConsoleWarn = console.warn;
const warnings = [];
rmSync(markerFile, { force: true });
resetRecordedActivities();
console.warn = (message) => {
warnings.push(message);
};
try {
const result = safeSpawnSync("printf", ["blocked", ">", markerFile], {
shell: true,
});
assert.strictEqual(result.status, 1);
assert.ok(result.error);
assert.match(result.error.message, /shell metacharacters/);
assert.strictEqual(existsSync(markerFile), false);
assert.ok(warnings.some((warning) => /Security Alert/.test(warning)));
} finally {
console.warn = originalConsoleWarn;
resetRecordedActivities();
rmSync(markerFile, { force: true });
}
});
it("safeSpawnSync() reads CDXGEN_ALLOWED_COMMANDS once per invocation", () => {
const originalAllowedCommands = process.env.CDXGEN_ALLOWED_COMMANDS;
process.env.CDXGEN_ALLOWED_COMMANDS = "echo-cdxgen-test";
setDryRunMode(true);
resetRecordedActivities();
try {
safeSpawnSync("echo-cdxgen-test", ["value"], {});
const envActivities = getRecordedActivities().filter(
(activity) => activity.target === "process.env:CDXGEN_ALLOWED_COMMANDS",
);
assert.strictEqual(envActivities.length, 1);
assert.strictEqual(envActivities[0].count, 1);
} finally {
if (originalAllowedCommands === undefined) {
delete process.env.CDXGEN_ALLOWED_COMMANDS;
} else {
process.env.CDXGEN_ALLOWED_COMMANDS = originalAllowedCommands;
}
setDryRunMode(false);
resetRecordedActivities();
}
});
it("safeSpawnSync() records stdout and stderr byte sizes in debug mode", async () => {
const mockedProcess = createMockedProcess({
CDXGEN_ALLOWED_COMMANDS: "echo-cdxgen-test",
CDXGEN_DEBUG_MODE: "debug",
CDXGEN_SECURE_MODE: undefined,
NODE_OPTIONS: undefined,
});
const utilsModule = await esmock("./utils.js", {
"node:child_process": {
spawnSync: sinon.stub().returns({
status: 0,
stdout: "hello",
stderr: "warn",
}),
},
"node:process": {
default: mockedProcess,
},
});
utilsModule.resetRecordedActivities();
utilsModule.safeSpawnSync("echo-cdxgen-test", ["value"], {});
const executeActivity = utilsModule
.getRecordedActivities()
.find(
(activity) =>
activity.kind === "execute" &&
activity.target === "echo-cdxgen-test value",
);
assert.ok(executeActivity);
assert.strictEqual(executeActivity.stdoutBytes, 5);
assert.strictEqual(executeActivity.stderrBytes, 4);
utilsModule.resetRecordedActivities();
});
it("safeExtractArchive() records source byte size in debug mode", async () => {
const mockedProcess = createMockedProcess({
CDXGEN_ALLOWED_COMMANDS: undefined,
CDXGEN_DEBUG_MODE: "debug",
CDXGEN_SECURE_MODE: undefined,
NODE_OPTIONS: undefined,
});
const utilsModule = await esmock("./utils.js", {
"node:process": {
default: mockedProcess,
},
});
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
const sourcePath = path.join(tempDir, "archive.zip");
const targetPath = path.join(tempDir, "extracted");
mkdirSync(targetPath, { recursive: true });
writeFileSync(sourcePath, "abc");
utilsModule.resetRecordedActivities();
await utilsModule.safeExtractArchive(
sourcePath,
targetPath,
async () => {
writeFileSync(path.join(targetPath, "a.txt"), "hello");
mkdirSync(path.join(targetPath, "nested"), { recursive: true });
writeFileSync(path.join(targetPath, "nested", "b.txt"), "xy");
},
"unzip",
);
const archiveActivity = utilsModule
.getRecordedActivities()
.find(
(activity) =>
activity.kind === "unzip" &&
activity.target === `${sourcePath} -> ${targetPath}`,
);
assert.ok(archiveActivity);
assert.strictEqual(archiveActivity.status, "completed");
assert.strictEqual(archiveActivity.sourceBytes, 3);
rmSync(tempDir, { recursive: true, force: true });
});
it("safeExtractArchive() records failed extraction activity in debug mode", async () => {
const mockedProcess = createMockedProcess({
CDXGEN_ALLOWED_COMMANDS: undefined,
CDXGEN_DEBUG_MODE: "debug",
CDXGEN_SECURE_MODE: undefined,
NODE_OPTIONS: undefined,
});
const utilsModule = await esmock("./utils.js", {
"node:process": {
default: mockedProcess,
},
});
const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-archive-trace-"));
const sourcePath = path.join(tempDir, "archive.tar");
const targetPath = path.join(tempDir, "extracted");
mkdirSync(targetPath, { recursive: true });
writeFileSync(sourcePath, "abcd");
utilsModule.resetRecordedActivities();
const extractionError = new Error("permission denied");
extractionError.code = "EACCES";
await assert.rejects(
utilsModule.safeExtractArchive(
sourcePath,
targetPath,
async () => {
throw extractionError;
},
"untar",
),
extractionError,
);
const archiveActivity = utilsModule
.getRecordedActivities()
.find(
(activity) =>
activity.kind === "untar" &&
activity.target === `${sourcePath} -> ${targetPath}`,
);
assert.ok(archiveActivity);
assert.strictEqual(archiveActivity.status, "failed");
assert.strictEqual(archiveActivity.errorCode, "EACCES");
assert.strictEqual(archiveActivity.sourceBytes, 4);
rmSync(tempDir, { recursive: true, force: true });
});
it("records dry-run environment variable reads via helper access", () => {
const originalEnvValue = process.env.CDXGEN_TEST_ENV_READ;
process.env.CDXGEN_TEST_ENV_READ = "trace-me";
setDryRunMode(true);
resetRecordedActivities();
try {
readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
readEnvironmentVariable("CDXGEN_TEST_ENV_READ");
const activities = getRecordedActivities().filter(
(activity) => activity.target === "process.env:CDXGEN_TEST_ENV_READ",
);
assert.strictEqual(activities.length, 1);
assert.strictEqual(activities[0].kind, "env");
assert.match(activities[0].reason, /2 times/);
} finally {
if (originalEnvValue === undefined) {
delete process.env.CDXGEN_TEST_ENV_READ;
} else {
process.env.CDXGEN_TEST_ENV_READ = originalEnvValue;
}
setDryRunMode(false);
resetRecordedActivities();
}
});
it("isAllowedHttpHost() honors exact and wildcard host allowlists", () => {
const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
try {
process.env.CDXGEN_ALLOWED_HOSTS = "example.com,*.trusted.test";
assert.strictEqual(isAllowedHttpHost("example.com"), true);
assert.strictEqual(isAllowedHttpHost("api.trusted.test"), true);
assert.strictEqual(isAllowedHttpHost("trusted.test"), false);
assert.strictEqual(isAllowedHttpHost("evil.com"), false);
} finally {
if (originalAllowedHosts === undefined) {
delete process.env.CDXGEN_ALLOWED_HOSTS;
} else {
process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
}
}
});
it("deduplicates sensitive file read activity entries in dry-run mode", () => {
setDryRunMode(true);
resetRecordedActivities();
try {
recordSensitiveFileRead("/tmp/docker/config.json", {
label: "Docker credential file",
});
recordSensitiveFileRead("/tmp/docker/config.json", {
label: "Docker credential file",
});
const activities = getRecordedActivities().filter(
(activity) => activity.target === "/tmp/docker/config.json",
);
assert.strictEqual(activities.length, 1);
assert.strictEqual(activities[0].kind, "read");
assert.match(activities[0].reason, /2 times/);
} finally {
setDryRunMode(false);
resetRecordedActivities();
}
});
it("records classified manifest and config inspections in dry-run mode", () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-inspect-"));
const packageJsonFile = path.join(tmpRoot, "package.json");
const settingsXmlFile = path.join(tmpRoot, "settings.xml");
writeFileSync(packageJsonFile, "{}");
writeFileSync(settingsXmlFile, "<settings />");
setDryRunMode(true);
resetRecordedActivities();
try {
assert.ok(safeExistsSync(packageJsonFile));
assert.ok(safeExistsSync(settingsXmlFile));
const activities = getRecordedActivities().filter((activity) =>
[packageJsonFile, settingsXmlFile].includes(activity.target),
);
assert.strictEqual(activities.length, 2);
assert.deepStrictEqual(
activities.map((activity) => activity.kind),
["inspect", "inspect"],
);
assert.deepStrictEqual(
activities.map((activity) => activity.classification),
["manifest", "config"],
);
} finally {
setDryRunMode(false);
resetRecordedActivities();
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("records recursive file discovery activity in dry-run mode", () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
const packageJsonFile = path.join(tmpRoot, "package.json");
writeFileSync(packageJsonFile, "{}");
setDryRunMode(true);
resetRecordedActivities();
try {
const files = getAllFiles(tmpRoot, "**/package.json");
assert.deepStrictEqual(files, [packageJsonFile]);
const activities = getRecordedActivities().filter((activity) =>
activity.target.includes("**/package.json"),
);
assert.strictEqual(activities.length, 1);
assert.strictEqual(activities[0].kind, "discover");
assert.strictEqual(activities[0].discoveryType, "manifest-discovery");
} finally {
setDryRunMode(false);
resetRecordedActivities();
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("records suspicious discovered paths with shell metacharacters in dry-run mode", () => {
const tmpRoot = mkdtempSync(
path.join(tmpdir(), "cdxgen-dry-run-shell-path-"),
);
const shellIfs = "$" + "{IFS}";
const maliciousDirName =
platform() === "win32"
? "evil&echo%CDXGEN_GITURL_E2E_MARKER%&rem"
: `evil;cd${shellIfs}..;printf${shellIfs}marker>CDXGEN_GITURL_E2E_MARKER;#`;
const maliciousDir = path.join(tmpRoot, maliciousDirName);
const pomFile = path.join(maliciousDir, "pom.xml");
mkdirSync(maliciousDir, { recursive: true });
writeFileSync(pomFile, "<project />");
setDryRunMode(true);
resetRecordedActivities();
try {
assert.deepStrictEqual(getAllFiles(tmpRoot, "**/pom.xml"), [pomFile]);
const suspiciousActivity = getRecordedActivities().find(
(activity) => activity.classification === "suspicious-path",
);
assert.ok(suspiciousActivity);
assert.strictEqual(suspiciousActivity.risk, "shell-metacharacters");
assert.strictEqual(suspiciousActivity.target, pomFile);
assert.match(suspiciousActivity.reason, /shell metacharacters/);
} finally {
setDryRunMode(false);
resetRecordedActivities();
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("records updated discovery activity when a repeated glob match count changes", () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-glob-"));
const packageJsonFile = path.join(tmpRoot, "package.json");
const nestedDir = path.join(tmpRoot, "nested");
const nestedPackageJsonFile = path.join(nestedDir, "package.json");
writeFileSync(packageJsonFile, "{}");
setDryRunMode(true);
resetRecordedActivities();
try {
getAllFiles(tmpRoot, "**/package.json");
mkdirSync(nestedDir, { recursive: true });
writeFileSync(nestedPackageJsonFile, "{}");
getAllFiles(tmpRoot, "**/package.json");
const activities = getRecordedActivities().filter((activity) =>
activity.target.includes("**/package.json"),
);
assert.strictEqual(activities.length, 2);
assert.deepStrictEqual(
activities.map((activity) => activity.matchedCount),
[1, 2],
);
} finally {
setDryRunMode(false);
resetRecordedActivities();
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("handles noIgnore option and ignores docs removal", () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-no-ignore-test-"));
const docsDir = path.join(tmpRoot, "docs");
const nodeModulesDir = path.join(tmpRoot, "node_modules");
const gitDir = path.join(tmpRoot, ".git");
mkdirSync(docsDir, { recursive: true });
mkdirSync(nodeModulesDir, { recursive: true });
mkdirSync(gitDir, { recursive: true });
const testFileDocs = path.join(docsDir, "test.txt");
const testFileNodeModules = path.join(nodeModulesDir, "test.txt");
const testFileGit = path.join(gitDir, "test.txt");
const testFileRoot = path.join(tmpRoot, "test.txt");
writeFileSync(testFileDocs, "docs content");
writeFileSync(testFileNodeModules, "node_modules content");
writeFileSync(testFileGit, "git content");
writeFileSync(testFileRoot, "root content");
try {
// 1. By default, docs is NOT ignored anymore because the block was removed.
// However, .git and node_modules are ignored by default.
const defaultFiles = getAllFiles(tmpRoot, "**/*.txt");
assert.ok(defaultFiles.includes(testFileRoot));
assert.ok(defaultFiles.includes(testFileDocs));
assert.ok(!defaultFiles.includes(testFileNodeModules));
assert.ok(!defaultFiles.includes(testFileGit));
// 2. With noIgnore: true, node_modules and .git are also NOT ignored.
const allFiles = getAllFiles(tmpRoot, "**/*.txt", { noIgnore: true });
assert.ok(allFiles.includes(testFileRoot));
assert.ok(allFiles.includes(testFileDocs));
assert.ok(allFiles.includes(testFileNodeModules));
assert.ok(allFiles.includes(testFileGit));
} finally {
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("dry-run filesystem wrappers do not mutate the filesystem", () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-dry-run-"));
const fileToKeep = path.join(tmpRoot, "keep.txt");
const fileToSkip = path.join(tmpRoot, "skip.txt");
const dirToKeep = path.join(tmpRoot, "keep-dir");
writeFileSync(fileToKeep, "hello");
mkdirSync(dirToKeep, { recursive: true });
setDryRunMode(true);
resetRecordedActivities();
try {
const tempPath = safeMkdtempSync(path.join(tmpRoot, "temp-"));
safeWriteSync(fileToSkip, "world");
safeUnlinkSync(fileToKeep);
safeRmSync(dirToKeep, { recursive: true, force: true });
assert.ok(!existsSync(fileToSkip));
assert.ok(existsSync(fileToKeep));
assert.ok(existsSync(dirToKeep));
assert.ok(!existsSync(tempPath));
const activities = getRecordedActivities();
assert.deepStrictEqual(
activities.map((activity) => activity.kind),
["temp-dir", "write", "cleanup", "cleanup"],
);
assert.ok(activities.every((activity) => activity.status === "blocked"));
} finally {
setDryRunMode(false);
resetRecordedActivities();
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("safeWriteSync() honors explicit fs.write permission for output files", async () => {
const tmpRoot = mkdtempSync(path.join(tmpdir(), "cdxgen-secure-write-"));
const outputFile = path.join(tmpRoot, "bom.json");
const originalDebugMode = process.env.CDXGEN_DEBUG_MODE;
const originalSecureMode = process.env.CDXGEN_SECURE_MODE;
const originalPermissionDescriptor = Object.getOwnPropertyDescriptor(
process,
"permission",
);
const hasPermissionStub = sinon.stub().callsFake((scope, filePath) => {
return (
scope === "fs.read" || (scope === "fs.write" && filePath === outputFile)
);
});
process.env.CDXGEN_DEBUG_MODE = "debug";
process.env.CDXGEN_SECURE_MODE = "true";
Object.defineProperty(process, "permission", {
configurable: true,
value: {
has: hasPermissionStub,
},
});
try {
const {
getRecordedActivities: getRecordedActivitiesMocked,
resetRecordedActivities: resetRecordedActivitiesMocked,
safeWriteSync: safeWriteSyncMocked,
} = await import(
new URL(`./utils.js?secure-write-test=${Date.now()}`, import.meta.url)
);
resetRecordedActivitiesMocked();
safeWriteSyncMocked(outputFile, "{}");
assert.strictEqual(readFileSync(outputFile, "utf-8"), "{}");
assert.strictEqual(getRecordedActivitiesMocked()[0].status, "completed");
} finally {
if (originalDebugMode === undefined) {
delete process.env.CDXGEN_DEBUG_MODE;
} else {
process.env.CDXGEN_DEBUG_MODE = originalDebugMode;
}
if (originalSecureMode === undefined) {
delete process.env.CDXGEN_SECURE_MODE;
} else {
process.env.CDXGEN_SECURE_MODE = originalSecureMode;
}
if (originalPermissionDescriptor) {
Object.defineProperty(
process,
"permission",
originalPermissionDescriptor,
);
} else {
delete process.permission;
}
rmSync(tmpRoot, { force: true, recursive: true });
}
});
it("cdxgenAgent records completed and failed network activity outcomes", async () => {
let setDryRunModeMocked;
try {
const {
cdxgenAgent,
getRecordedActivities: getRecordedActivitiesMocked,
resetRecordedActivities: resetRecordedActivitiesMocked,
setDryRunMode: mockedSetDryRunMode,
} = await esmock("./utils.js", {
got: {
default: {
extend: sinon.stub().callsFake((options) => {
return {
hooks: options.hooks,
};
}),
},
},
});
setDryRunModeMocked = mockedSetDryRunMode;
const afterResponseHook = cdxgenAgent.hooks.afterResponse[0];
const beforeErrorHook = cdxgenAgent.hooks.beforeError[0];
setDryRunModeMocked(true);
resetRecordedActivitiesMocked();
const successUrl = "https://example.com/success";
const successOptions = {
context: {
activityTarget: successUrl,
},
url: new URL(successUrl),
};
afterResponseHook({
request: {
options: successOptions,
},
statusCode: 200,
url: successUrl,
});
assert.strictEqual(getRecordedActivitiesMocked()[0].status, "completed");
assert.strictEqual(getRecordedActivitiesMocked()[0].target, successUrl);
resetRecordedActivitiesMocked();
const failureUrl = "https://example.com/failure";
const failureOptions = {
context: {
activityTarget: failureUrl,
},
url: new URL(failureUrl),
};
const returnedError = beforeErrorHook({
message: "Request failed with status code 500",
options: failureOptions,
});
assert.match(returnedError.message, /status code 500/);
const failureActivities = getRecordedActivitiesMocked();
assert.strictEqual(failureActivities.length, 1);
assert.strictEqual(failureActivities[0].status, "failed");
assert.strictEqual(failureActivities[0].target, failureUrl);
} finally {
if (setDryRunModeMocked) {
setDryRunModeMocked(false);
}
}
});
it("cdxgenAgent reads CDXGEN_ALLOWED_HOSTS once per request", () => {
const originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
try {
process.env.CDXGEN_ALLOWED_HOSTS = "example.com";
const beforeRequestHook =
cdxgenAgent.defaults.options.hooks.beforeRequest[0];
setDryRunMode(true);
resetRecordedActivities();
assert.throws(() =>
beforeRequestHook({
context: {},
url: new URL("https://example.com/resource"),
}),
);
const envActivities = getRecordedActivities().filter(
(activity) => activity.target === "process.env:CDXGEN_ALLOWED_HOSTS",
);
assert.strictEqual(envActivities.length, 1);
assert.strictEqual(envActivities[0].count, 1);
} finally {
if (originalAllowedHosts === undefined) {
delete process.env.CDXGEN_ALLOWED_HOSTS;
} else {
process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
}
setDryRunMode(false);
resetRecordedActivities();
}
});
it("safeSpawnSync() logs container python notices to stdout", () => {
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalContainer = process.env.CDXGEN_IN_CONTAINER;
const originalNoticeCache = globalThis.__cdxgenNoticeCache;
const logs = [];
const warnings = [];
process.env.CDXGEN_IN_CONTAINER = "true";
delete globalThis.__cdxgenNoticeCache;
console.log = (message) => {
logs.push(message);
};
console.warn = (message) => {
warnings.push(message);
};
try {
safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
safeSpawnSync("python-cdxgen-test", ["-c", "pass"], {});
assert.strictEqual(logs.length + warnings.length, 1);
assert.ok(
[...logs, ...warnings].some((message) =>
message.includes("Running python command without '-S' argument."),
),
);
} finally {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
if (originalContainer === undefined) {
delete process.env.CDXGEN_IN_CONTAINER;
} else {
process.env.CDXGEN_IN_CONTAINER = originalContainer;
}
if (originalNoticeCache === undefined) {
delete globalThis.__cdxgenNoticeCache;
} else {
globalThis.__cdxgenNoticeCache = originalNoticeCache;
}
}
});
it("parse maven tree", () => {
assert.deepStrictEqual(parseMavenTree(null), {});
let parsedList = parseMavenTree(
readFileSync("./test/data/sample-mvn-tree.txt", { encoding: "utf-8" }),
);
assert.deepStrictEqual(parsedList.pkgList.length, 61);
assert.deepStrictEqual(parsedList.dependenciesList.length, 61);
assert.deepStrictEqual(parsedList.pkgList[0], {
"bom-ref": "pkg:maven/com.pogeyan.cmis/copper-server@1.15.2?type=war",
group: "com.pogeyan.cmis",
name: "copper-server",
version: "1.15.2",
qualifiers: { type: "war" },
properties: [],
purl: "pkg:maven/com.pogeyan.cmis/copper-server@1.15.2?type=war",
scope: undefined,
});
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/com.pogeyan.cmis/copper-server@1.15.2?type=war",
dependsOn: [
"pkg:maven/com.fasterxml.jackson.core/jackson-core@2.12.0?type=jar",
"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.0?type=jar",
"pkg:maven/com.github.davidb/metrics-influxdb@0.9.3?type=jar",
"pkg:maven/com.pogeyan.cmis/copper-server-api@1.15.2?type=jar",
"pkg:maven/com.pogeyan.cmis/copper-server-impl@1.15.2?type=jar",
"pkg:maven/com.pogeyan.cmis/copper-server-ldap@1.15.2?type=jar",
"pkg:maven/com.pogeyan.cmis/copper-server-mongo@1.15.2?type=jar",
"pkg:maven/com.pogeyan.cmis/copper-server-repo@1.15.2?type=jar",
"pkg:maven/com.typesafe.akka/akka-actor_2.11@2.4.14?type=jar",
"pkg:maven/com.typesafe.akka/akka-cluster_2.11@2.4.14?type=jar",
"pkg:maven/commons-fileupload/commons-fileupload@1.4?type=jar",
"pkg:maven/commons-io/commons-io@2.6?type=jar",
"pkg:maven/io.dropwizard.metrics/metrics-core@3.1.2?type=jar",
"pkg:maven/javax/javaee-web-api@7.0?type=jar",
"pkg:maven/junit/junit@4.12?type=jar",
"pkg:maven/org.apache.chemistry.opencmis/chemistry-opencmis-server-support@1.0.0?type=jar",
"pkg:maven/org.apache.commons/commons-lang3@3.4?type=jar",
"pkg:maven/org.codehaus.jackson/jackson-mapper-asl@1.9.13?type=jar",
"pkg:maven/org.slf4j/slf4j-log4j12@1.7.21?type=jar",
],
});
parsedList = parseMavenTree(
readFileSync("./test/data/mvn-dep-tree-simple.txt", {
encoding: "utf-8",
}),
);
assert.deepStrictEqual(parsedList.pkgList.length, 39);
assert.deepStrictEqual(parsedList.dependenciesList.length, 39);
assert.deepStrictEqual(parsedList.pkgList[0], {
"bom-ref":
"pkg:maven/com.gitlab.security_products.tests/java-maven@1.0-SNAPSHOT?type=jar",
purl: "pkg:maven/com.gitlab.security_products.tests/java-maven@1.0-SNAPSHOT?type=jar",
group: "com.gitlab.security_products.tests",
name: "java-maven",
version: "1.0-SNAPSHOT",
qualifiers: { type: "jar" },
properties: [],
scope: undefined,
});
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/com.gitlab.security_products.tests/java-maven@1.0-SNAPSHOT?type=jar",
dependsOn: [
"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.2?type=jar",
"pkg:maven/com.github.jnr/jffi@1.3.11?classifier=native&type=jar",
"pkg:maven/com.github.jnr/jffi@1.3.11?type=jar",
"pkg:maven/io.netty/netty@3.9.1.Final?type=jar",
"pkg:maven/junit/junit@3.8.1?type=jar",
"pkg:maven/org.apache.geode/geode-core@1.1.1?type=jar",
"pkg:maven/org.apache.maven/maven-artifact@3.3.9?type=jar",
"pkg:maven/org.mozilla/rhino@1.7.10?type=jar",
"pkg:maven/org.powermock/powermock-api-mockito@1.7.3?type=jar",
],
});
parsedList = parseMavenTree(
readFileSync("./test/data/mvn-p2-plugin.txt", {
encoding: "utf-8",
}),
);
assert.deepStrictEqual(parsedList.pkgList.length, 79);
assert.deepStrictEqual(parsedList.pkgList[0], {
"bom-ref":
"pkg:maven/example.group/eclipse-repository@1.0.0-SNAPSHOT?type=eclipse-repository",
purl: "pkg:maven/example.group/eclipse-repository@1.0.0-SNAPSHOT?type=eclipse-repository",
group: "example.group",
name: "eclipse-repository",
version: "1.0.0-SNAPSHOT",
qualifiers: { type: "eclipse-repository" },
scope: undefined,
properties: [],
});
assert.deepStrictEqual(parsedList.pkgList[4], {
"bom-ref":
"pkg:maven/p2.eclipse.plugin/com.ibm.icu@67.1.0.v20200706-1749?type=eclipse-plugin",
purl: "pkg:maven/p2.eclipse.plugin/com.ibm.icu@67.1.0.v20200706-1749?type=eclipse-plugin",
group: "p2.eclipse.plugin",
name: "com.ibm.icu",
version: "67.1.0.v20200706-1749",
qualifiers: { type: "eclipse-plugin" },
scope: "excluded",
properties: [],
});
assert.deepStrictEqual(parsedList.dependenciesList.length, 79);
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/example.group/eclipse-repository@1.0.0-SNAPSHOT?type=eclipse-repository",
dependsOn: [
"pkg:maven/example.group/example-bundle@0.1.0-SNAPSHOT?type=eclipse-plugin",
"pkg:maven/example.group/example-feature-2@0.2.0-SNAPSHOT?type=eclipse-feature",
"pkg:maven/example.group/example-feature@0.1.0-SNAPSHOT?type=eclipse-feature",
"pkg:maven/example.group/org.tycho.demo.rootfiles.win@1.0.0-SNAPSHOT?type=p2-installable-unit",
"pkg:maven/example.group/org.tycho.demo.rootfiles@1.0.0?type=p2-installable-unit",
],
});
parsedList = parseMavenTree(
readFileSync("./test/data/mvn-metrics-tree.txt", {
encoding: "utf-8",
}),
);
assert.deepStrictEqual(parsedList.pkgList.length, 58);
assert.deepStrictEqual(
parsedList.parentComponent["bom-ref"],
"pkg:maven/org.apache.dubbo/dubbo-metrics@3.3.0?type=pom",
);
assert.deepStrictEqual(parsedList.dependenciesList.length, 58);
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/org.apache.dubbo/dubbo-metrics@3.3.0?type=pom",
dependsOn: [
"pkg:maven/org.apache.dubbo/dubbo-test-check@3.3.0?type=jar",
"pkg:maven/org.awaitility/awaitility@4.2.0?type=jar",
"pkg:maven/org.hamcrest/hamcrest@2.2?type=jar",
"pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.9.3?type=jar",
"pkg:maven/org.junit.jupiter/junit-jupiter-params@5.9.3?type=jar",
"pkg:maven/org.mockito/mockito-core@4.11.0?type=jar",
"pkg:maven/org.mockito/mockito-inline@4.11.0?type=jar",
],
});
parsedList = parseMavenTree(
readFileSync("./test/data/mvn-sbstarter-tree.txt", {
encoding: "utf-8",
}),
);
assert.deepStrictEqual(parsedList.pkgList.length, 102);
assert.deepStrictEqual(
parsedList.parentComponent["bom-ref"],
"pkg:maven/org.apache.dubbo/dubbo-spring-boot-starter@3.3.0?type=jar",
);
assert.deepStrictEqual(parsedList.dependenciesList.length, 102);
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/org.apache.dubbo/dubbo-spring-boot-starter@3.3.0?type=jar",
dependsOn: [
"pkg:maven/net.bytebuddy/byte-buddy-agent@1.15.0?type=jar",
"pkg:maven/net.bytebuddy/byte-buddy@1.15.0?type=jar",
"pkg:maven/org.apache.dubbo/dubbo-spring-boot-autoconfigure@3.3.0?type=jar",
"pkg:maven/org.apache.dubbo/dubbo-test-check@3.3.0?type=jar",
"pkg:maven/org.apache.logging.log4j/log4j-slf4j-impl@2.17.2?type=jar",
"pkg:maven/org.awaitility/awaitility@4.2.0?type=jar",
"pkg:maven/org.hamcrest/hamcrest@2.2?type=jar",
"pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.8.2?type=jar",
"pkg:maven/org.junit.jupiter/junit-jupiter-params@5.8.2?type=jar",
"pkg:maven/org.junit.vintage/junit-vintage-engine@5.8.2?type=jar",
"pkg:maven/org.mockito/mockito-core@4.11.0?type=jar",
"pkg:maven/org.mockito/mockito-inline@4.11.0?type=jar",
"pkg:maven/org.springframework.boot/spring-boot-starter@2.7.18?type=jar",
],
});
});
it("parse maven tree optional and repeated dependency edges", () => {
const parsedOptional = parseMavenTree(`example:optional-sample:jar:1.0.0
\\- org.apache.maven:maven-artifact:jar:3.9.9:compile (optional)
\\- org.codehaus.plexus:plexus-utils:jar:3.5.1:compile (optional)
`);
assert.strictEqual(parsedOptional.pkgList.length, 3);
assert.strictEqual(parsedOptional.pkgList[1].scope, "optional");
assert.deepStrictEqual(parsedOptional.pkgList[1].properties, []);
assert.deepStrictEqual(parsedOptional.dependenciesList[0], {
ref: "pkg:maven/example/optional-sample@1.0.0?type=jar",
dependsOn: ["pkg:maven/org.apache.maven/maven-artifact@3.9.9?type=jar"],
});
const parsedDuplicate = parseMavenTree(`example:dup-root:jar:1.0.0
+- g:a:jar:1:compile
| \\- g:c:jar:1:compile
\\- g:b:jar:1:compile
\\- g:c:jar:1:compile
`);
const bDependency = parsedDuplicate.dependenciesList.find(
(dep) => dep.ref === "pkg:maven/g/b@1?type=jar",
);
assert.deepStrictEqual(bDependency.dependsOn, ["pkg:maven/g/c@1?type=jar"]);
});
it("parse maven tree json", () => {
const parsedList = parseMavenTreeJson(
JSON.stringify({
groupId: "example",
artifactId: "json-root",
version: "1.0.0",
type: "jar",
scope: "",
classifier: "",
optional: "false",
children: [
{
groupId: "org.apache.maven",
artifactId: "maven-artifact",
version: "3.9.9",
type: "jar",
scope: "compile",
classifier: "",
optional: "true",
children: [
{
groupId: "org.codehaus.plexus",
artifactId: "plexus-utils",
version: "3.5.1",
type: "jar",
scope: "compile",
classifier: "",
optional: "true",
},
],
},
],
}),
);
assert.strictEqual(parsedList.pkgList.length, 3);
assert.strictEqual(parsedList.pkgList[1].scope, "optional");
assert.deepStrictEqual(parsedList.dependenciesList[0], {
ref: "pkg:maven/example/json-root@1.0.0?type=jar",
dependsOn: ["pkg:maven/org.apache.maven/maven-artifact@3.9.9?type=jar"],
});
assert.deepStrictEqual(parsedList.dependenciesList[1], {
ref: "pkg:maven/org.apache.maven/maven-artifact@3.9.9?type=jar",
dependsOn: ["pkg:maven/org.codehaus.plexus/plexus-utils@3.5.1?type=jar"],
});
assert.deepStrictEqual(parseMavenTreeJson("{not-json"), {});
assert.deepStrictEqual(parseMavenTree("{not-json"), {});
});
it("parse maven args", () => {
assert.deepStrictEqual(
parseMavenArgs(
'--settings "/tmp/path with spaces/settings.xml" -P dev,test -DskipTests',
),
[
"--settings",
"/tmp/path with spaces/settings.xml",
"-P",
"dev,test",
"-DskipTests",
],
);
assert.deepStrictEqual(
parseMavenArgs(String.raw`-s C:\Users\me\settings.xml -Dpath=C:\repo\demo`),
[
"-s",
String.raw`C:\Users\me\settings.xml`,
String.raw`-Dpath=C:\repo\demo`,
],
);
assert.deepStrictEqual(parseMavenArgs(String.raw`-Dname=hello\ world`), [
"-Dname=hello world",
]);
});
// Slow test
/*
it("get maven metadata", async () => {
let data = await utils.getMvnMetadata([
{
group: "com.squareup.okhttp3",
name: "okhttp",
version: "3.8.1",
},
]);
assert.deepStrictEqual(data, [
{
description: "",
group: "com.squareup.okhttp3",
name: "okhttp",
version: "3.8.1",
},
]);
data = await utils.getMvnMetadata([
{
group: "com.fasterxml.jackson.core",
name: "jackson-databind",
version: "2.8.5",
},
{
group: "com.github.jnr",
name: "jnr-posix",
version: "3.0.47",
},
]);
assert.deepStrictEqual(data, [
{
group: "com.fasterxml.jackson.core",
name: "jackson-databind",
version: "2.8.5",
description:
"General data-binding functionality for Jackson: works on core streaming API",
repository: { url: "http://github.com/FasterXML/jackson-databind" },
},
{
group: "com.github.jnr",
name: "jnr-posix",
version: "3.0.47",
license: ["EPL-2.0", "GPL-2.0-only", "LGPL-2.1-only"],
description: "\n Common cross-project/cross-platform POSIX APIs\n ",
repository: { url: "git@github.com:jnr/jnr-posix.git" },
},
]);
});
*/
it("get py metadata", async () => {
const data = await getPyMetadata(
[
{
group: "",
name: "Flask",
version: "1.1.0",
},
],
false,
);
assert.deepStrictEqual(data, [
{
group: "",
name: "Flask",
version: "1.1.0",
},
]);
}, 240000);
it("get py metadata adds distribution external references", async () => {
const agentGetStub = sinon.stub().resolves({
body: {
info: {
author: "",
author_email: "",
classifiers: [],
license: "",
license_expression: "",
name: "requests",
summary: "HTTP client",
version: "2.31.0",
},
releases: {
"2.31.0": [
{
digests: { sha256: "abc123" },
filename: "requests-2.31.0-py3-none-any.whl",
packagetype: "bdist_wheel",
url: "https://files.pythonhosted.org/packages/example/requests-2.31.0-py3-none-any.whl",
},
{
digests: { sha256: "def456" },
filename: "requests-2.31.0.tar.gz",
packagetype: "sdist",
url: "https://files.pythonhosted.org/packages/example/requests-2.31.0.tar.gz",
},
],
},
},
});
const { getPyMetadata: mockedGetPyMetadata } = await esmock("./utils.js", {
got: {
default: {
extend: sinon.stub().returns({ get: agentGetStub }),
},
},
});
const data = await mockedGetPyMetadata(
[
{
externalReferences: [
{
type: "website",
url: "https://example.com/requests",
},
],
group: "",
name: "requests",
version: "2.31.0",
},
],
true,
);
assert.strictEqual(data.length, 1);
assert.ok(
data[0].externalReferences?.some(
(reference) => reference.type === "website",
),
);
assert.ok(
data[0].externalReferences?.some(
(reference) =>
reference.type === "distribution" &&
reference.url.endsWith(".whl") &&
reference.comment === "requests-2.31.0-py3-none-any.whl",
),
);
assert.ok(
data[0].externalReferences?.some(
(reference) =>
reference.type === "distribution" &&
reference.url.endsWith(".tar.gz") &&
reference.comment === "requests-2.31.0.tar.gz",
),
);
});
it("parseGoModData", async () => {
let retMap = await parseGoModData(null);
assert.deepStrictEqual(retMap, {});
const gosumMap = {
"google.golang.org/grpc@v1.21.0":
"sha256-oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=",
"github.com/aws/aws-sdk-go@v1.38.47": "sha256-fake-sha-for-aws-go-sdk=",
"github.com/spf13/cobra@v1.0.0":
"sha256-/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=",
"github.com/spf13/viper@v1.3.0":
"sha256-A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=",
"github.com/stretchr/testify@v1.6.1":
"sha256-6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=",
};
retMap = await parseGoModData(
readFileSync("./test/gomod/go.mod", { encoding: "utf-8" }),
gosumMap,
);
assert.deepStrictEqual(retMap.pkgList.length, 6);
assert.ok(retMap.pkgList);
retMap.pkgList.forEach((d) => {
assert.deepStrictEqual(d.license);
});
retMap = await parseGoModData(
readFileSync("./test/data/go-dvwa.mod", { encoding: "utf-8" }),
{},
);
assert.deepStrictEqual(retMap.parentComponent, {
"bom-ref": "pkg:golang/github.com/sqreen/go-dvwa",
name: "github.com/sqreen/go-dvwa",
purl: "pkg:golang/github.com/sqreen/go-dvwa",
type: "application",
});
assert.deepStrictEqual(retMap.pkgList.length, 19);
assert.deepStrictEqual(retMap.rootList.length, 4);
retMap = await parseGoModData(
readFileSync("./test/data/go-syft.mod", { encoding: "utf-8" }),
{},
);
assert.deepStrictEqual(retMap.parentComponent, {
"bom-ref": "pkg:golang/github.com/anchore/syft",
name: "github.com/anchore/syft",
purl: "pkg:golang/github.com/anchore/syft",
type: "application",
});
assert.deepStrictEqual(retMap.pkgList.length, 239);
assert.deepStrictEqual(retMap.rootList.length, 84);
}, 120000);
it("parseGoSumData", async () => {
let dep_list = await parseGosumData(null);
assert.deepStrictEqual(dep_list, []);
dep_list = await parseGosumData(
readFileSync("./test/gomod/go.sum", { encoding: "utf-8" }),
);
assert.deepStrictEqual(dep_list.length, 4);
assert.deepStrictEqual(dep_list[0], {
group: "",
name: "google.golang.org/grpc",
license: undefined,
version: "v1.21.0",
_integrity: "sha256-oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=",
"bom-ref": "pkg:golang/google.golang.org/grpc@v1.21.0",
purl: "pkg:golang/google.golang.org/grpc@v1.21.0",
});
assert.deepStrictEqual(dep_list[1], {
group: "",
name: "github.com/spf13/cobra",
license: undefined,
version: "v1.0.0",
_integrity: "sha256-/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=",
"bom-ref": "pkg:golang/github.com/spf13/cobra@v1.0.0",
purl: "pkg:golang/github.com/spf13/cobra@v1.0.0",
});
assert.deepStrictEqual(dep_list[2], {
group: "",
name: "github.com/spf13/viper",
license: undefined,
version: "v1.0.2",
_integrity: "sha256-A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=",
"bom-ref": "pkg:golang/github.com/spf13/viper@v1.0.2",
purl: "pkg:golang/github.com/spf13/viper@v1.0.2",
});
assert.deepStrictEqual(dep_list[3], {
group: "",
name: "github.com/stretchr/testify",
license: undefined,
version: "v1.6.1",
_integrity: "sha256-6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=",
"bom-ref": "pkg:golang/github.com/stretchr/testify@v1.6.1",
purl: "pkg:golang/github.com/stretchr/testify@v1.6.1",
});
dep_list.forEach((d) => {
assert.deepStrictEqual(d.license);
});
it(() => {
delete process.env.GO_FETCH_VCS;
});
}, 120000);
describe("go data with vcs", () => {
it(() => {
process.env.GO_FETCH_VCS = "true";
});
it("parseGoSumData with vcs", async () => {
let dep_list = await parseGosumData(null);
assert.deepStrictEqual(dep_list, []);
dep_list = await parseGosumData(
readFileSync("./test/gomod/go.sum", { encoding: "utf-8" }),
);
assert.deepStrictEqual(dep_list.length, 4);
assert.ok(dep_list[0]);
}, 120000);
it("parseGoModData", async () => {
process.env.GO_FETCH_VCS = "false";
let retMap = await parseGoModData(null);
assert.deepStrictEqual(retMap, {});
const gosumMap = {
"google.golang.org/grpc@v1.21.0":
"sha256-oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=",
"github.com/aws/aws-sdk-go@v1.38.47": "sha256-fake-sha-for-aws-go-sdk=",
"github.com/spf13/cobra@v1.0.0":
"sha256-/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=",
"github.com/spf13/viper@v1.3.0":
"sha256-A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=",
"github.com/stretchr/testify@v1.6.1":
"sha256-6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=",
};
retMap = await parseGoModData(
readFileSync("./test/gomod/go.mod", { encoding: "utf-8" }),
gosumMap,
);
assert.deepStrictEqual(retMap.pkgList.length, 6);
// Doesn't reliably work in CI/CD due to rate limiting.
/*
assert.deepStrictEqual(retMap.pkgList, [
{
group: "",
name: "github.com/aws/aws-sdk-go",
version: "v1.38.47",
_integrity: "sha256-fake-sha-for-aws-go-sdk=",
purl: "pkg:golang/github.com/aws/aws-sdk-go@v1.38.47",
"bom-ref": "pkg:golang/github.com/aws/aws-sdk-go@v1.38.47",
externalReferences: [
{
type: "vcs",
url: "https://github.com/aws/aws-sdk-go",
},
],
},
{
grou