UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,517 lines (1,464 loc) 381 kB
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 (&gt;=1.4.0)"), { name: "lazy-object-proxy", version: "1.4.0", }, ); assert.deepStrictEqual(parsePyRequiresDist("wrapt (&lt;1.13,&gt;=1.11)"), { name: "wrapt", version: "1.13", }); assert.deepStrictEqual( parsePyRequiresDist( 'typed-ast (&lt;1.5,&gt;=1.4.0) ; implementation_name == "cpython" and python_version &lt; "3.8"', ), { name: "typed-ast", version: "1.5" }, ); assert.deepStrictEqual(parsePyRequiresDist("asgiref (&lt;4,&gt;=3.2.10)"), { name: "asgiref", version: "4", }); assert.deepStrictEqual(parsePyRequiresDist("pytz"), { name: "pytz", version: "", }); assert.deepStrictEqual(parsePyRequiresDist("sqlparse (&gt;=0.2.2)"), { name: "sqlparse", version: "0.2.2", }); assert.deepStrictEqual( parsePyRequiresDist("argon2-cffi (&gt;=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