UNPKG

@cyclonedx/cdxgen

Version:

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

1,552 lines (1,505 loc) 371 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, buildObjectForGradleModule, 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, parseGradleDep, parseGradleProjects, parseGradleProperties, parseHelmYamlData, parseJarManifest, parseKVDep, parseLeinDep, parseLeiningenData, parseMakeDFile, parseMavenTree, parseMillDependency, parseMinJs, parseMixLockData, parseNodeShrinkwrap, parseNupkg, parseNuspecData, parseOpenapiSpecData, parsePackageJsonName, parsePaketLockData, parsePiplockData, parsePkgJson, parsePkgLock, parsePnpmLock, parsePnpmWorkspace, parsePodfileLock, parsePodfileTargets, parsePom, parsePomProperties, parsePrivadoFile, parsePubLockData, parsePubYamlData, parsePyLockData, parsePyProjectTomlFile, parsePyRequiresDist, parseReqEnvMarkers, parseReqFile, parseSbtLock, parseSbtTree, parseSetupPyFile, parseSwiftJsonTree, parseSwiftResolved, parseYarnLock, pnpmMetadata, purlFromUrlString, readEnvironmentVariable, readZipEntry, recordSensitiveFileRead, recordSymlinkResolution, resetRecordedActivities, safeExistsSync, safeMkdtempSync, safeRmSync, safeSpawnSync, safeUnlinkSync, safeWriteSync, setDryRunMode, shouldRunPredictiveBomAudit, splitOutputByGradleProjects, 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("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("splits parallel gradle properties output correctly", () => { const parallelGradlePropertiesOutput = readFileSync( "./test/gradle-prop-parallel.out", { encoding: "utf-8" }, ); const relevantTasks = ["properties"]; const propOutputSplitBySubProject = splitOutputByGradleProjects( parallelGradlePropertiesOutput, relevantTasks, ); assert.deepStrictEqual(propOutputSplitBySubProject.size, 4); assert.deepStrictEqual( propOutputSplitBySubProject.has("dependency-diff-check"), true, ); assert.deepStrictEqual( propOutputSplitBySubProject.has(":dependency-diff-check-service"), true, ); assert.deepStrictEqual( propOutputSplitBySubProject.has(":dependency-diff-check-common-core"), true, ); assert.deepStrictEqual( propOutputSplitBySubProject.has(":dependency-diff-check-client-starter"), true, ); const retMap = parseGradleProperties( propOutputSplitBySubProject.get("dependency-diff-check"), ); assert.deepStrictEqual(retMap.rootProject, "dependency-diff-check"); assert.deepStrictEqual(retMap.projects.length, 3); assert.deepStrictEqual(retMap.metadata.group, "com.ajmalab"); assert.deepStrictEqual(retMap.metadata.version, "0.0.1-SNAPSHOT"); }); it("splits parallel gradle dependencies output correctly", async () => { const parallelGradleDepOutput = readFileSync( "./test/gradle-dep-parallel.out", { encoding: "utf-8" }, ); const relevantTasks = ["dependencies"]; const depOutputSplitBySubProject = splitOutputByGradleProjects( parallelGradleDepOutput, relevantTasks, ); assert.deepStrictEqual(depOutputSplitBySubProject.size, 4); assert.deepStrictEqual( depOutputSplitBySubProject.has("dependency-diff-check"), true, ); assert.deepStrictEqual( depOutputSplitBySubProject.has(":dependency-diff-check-service"), true, ); assert.deepStrictEqual( depOutputSplitBySubProject.has(":dependency-diff-check-common-core"), true, ); assert.deepStrictEqual( depOutputSplitBySubProject.has(":dependency-diff-check-client-starter"), true, ); const retMap = await parseGradleDep( depOutputSplitBySubProject.get("dependency-diff-check"), "dependency-diff-check", new Map().set( "dependency-diff-check", await buildObjectForGradleModule("dependency-diff-check", { version: "latest", }), ), ); assert.deepStrictEqual(retMap.pkgList.length, 12); assert.deepStrictEqual(retMap.dependenciesList.length, 13); }); it("splits parallel custom gradle task outputs correctly", async () => { const parallelGradleOutputWithOverridenTask = readFileSync( "./test/gradle-build-env-dep.out", { encoding: "utf-8" }, ); const overridenTasks = ["buildEnvironment"]; const customDepTaskOuputSplitByProject = splitOutputByGradleProjects( parallelGradleOutputWithOverridenTask, overridenTasks, ); assert.deepStrictEqual(customDepTaskOuputSplitByProject.size, 4); assert.deepStrictEqual( customDepTaskOuputSplitByProject.has("dependency-diff-check"), true, ); assert.deepStrictEqual( customDepTaskOuputSplitByProject.has(":dependency-diff-check-service"), true, ); assert.deepStrictEqual( customDepTaskOuputSplitByProject.has(":dependency-diff-check-common-core"), true, ); assert.deepStrictEqual( customDepTaskOuputSplitByProject.has( ":dependency-diff-check-client-starter", ), true, ); const retMap = await parseGradleDep( customDepTaskOuputSplitByProject.get( ":dependency-diff-check-client-starter", ), "dependency-diff-check", new Map().set( "dependency-diff-check", await buildObjectForGradleModule("dependency-diff-check", { version: "latest", }), ), ); assert.deepStrictEqual(retMap.pkgList.length, 22); assert.deepStrictEqual(retMap.dependenciesList.length, 23); }); it("parse gradle dependencies", async () => { const modulesMap = new Map(); modulesMap.set( "test-project", await buildObjectForGradleModule("test-project", { version: "latest", }), ); modulesMap.set( "dependency-diff-check-common-core", await buildObjectForGradleModule("dependency-diff-check-common-core", { version: "latest", }), ); modulesMap.set( "app", await buildObjectForGradleModule("app", { version: "latest", }), ); modulesMap.set( "failing-project", await buildObjectForGradleModule("failing-project", { version: "latest", }), ); assert.deepStrictEqual(await parseGradleDep(null), {}); let parsedList = await parseGradleDep( readFileSync("./test/gradle-dep.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 33); assert.deepStrictEqual(parsedList.dependenciesList.length, 34); assert.deepStrictEqual(parsedList.pkgList[0], { group: "org.ethereum", name: "solcJ-all", qualifiers: { type: "jar", }, version: "0.4.25", "bom-ref": "pkg:maven/org.ethereum/solcJ-all@0.4.25?type=jar", purl: "pkg:maven/org.ethereum/solcJ-all@0.4.25?type=jar", }); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-android-dep.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 104); assert.deepStrictEqual(parsedList.dependenciesList.length, 105); assert.deepStrictEqual(parsedList.pkgList[0], { group: "com.android.support.test", name: "runner", qualifiers: { type: "jar", }, scope: "optional", version: "1.0.2", properties: [ { name: "GradleProfileName", value: "androidTestImplementation", }, ], "bom-ref": "pkg:maven/com.android.support.test/runner@1.0.2?type=jar", purl: "pkg:maven/com.android.support.test/runner@1.0.2?type=jar", }); assert.deepStrictEqual(parsedList.pkgList[103], { group: "androidx.core", name: "core", qualifiers: { type: "jar", }, version: "1.7.0", scope: "optional", properties: [ { name: "GradleProfileName", value: "releaseUnitTestRuntimeClasspath", }, ], "bom-ref": "pkg:maven/androidx.core/core@1.7.0?type=jar", purl: "pkg:maven/androidx.core/core@1.7.0?type=jar", }); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-out1.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 89); assert.deepStrictEqual(parsedList.dependenciesList.length, 90); assert.deepStrictEqual(parsedList.pkgList[0], { group: "org.springframework.boot", name: "spring-boot-starter-web", version: "2.2.0.RELEASE", qualifiers: { type: "jar" }, properties: [ { name: "GradleProfileName", value: "compileClasspath", }, ], "bom-ref": "pkg:maven/org.springframework.boot/spring-boot-starter-web@2.2.0.RELEASE?type=jar", purl: "pkg:maven/org.springframework.boot/spring-boot-starter-web@2.2.0.RELEASE?type=jar", }); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-rich1.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 4); assert.deepStrictEqual(parsedList.pkgList[parsedList.pkgList.length - 1], { group: "ch.qos.logback", name: "logback-core", qualifiers: { type: "jar" }, version: "1.4.5", "bom-ref": "pkg:maven/ch.qos.logback/logback-core@1.4.5?type=jar", purl: "pkg:maven/ch.qos.logback/logback-core@1.4.5?type=jar", }); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-rich2.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 2); assert.deepStrictEqual(parsedList.pkgList, [ { group: "io.appium", name: "java-client", qualifiers: { type: "jar" }, version: "8.1.1", "bom-ref": "pkg:maven/io.appium/java-client@8.1.1?type=jar", purl: "pkg:maven/io.appium/java-client@8.1.1?type=jar", }, { group: "org.seleniumhq.selenium", name: "selenium-support", qualifiers: { type: "jar" }, version: "4.5.0", "bom-ref": "pkg:maven/org.seleniumhq.selenium/selenium-support@4.5.0?type=jar", purl: "pkg:maven/org.seleniumhq.selenium/selenium-support@4.5.0?type=jar", }, ]); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-rich3.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 1); assert.deepStrictEqual(parsedList.pkgList, [ { group: "org.seleniumhq.selenium", name: "selenium-remote-driver", version: "4.5.0", qualifiers: { type: "jar" }, "bom-ref": "pkg:maven/org.seleniumhq.selenium/selenium-remote-driver@4.5.0?type=jar", purl: "pkg:maven/org.seleniumhq.selenium/selenium-remote-driver@4.5.0?type=jar", }, ]); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-rich4.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 1); assert.deepStrictEqual(parsedList.pkgList, [ { group: "org.seleniumhq.selenium", name: "selenium-api", version: "4.5.0", qualifiers: { type: "jar" }, "bom-ref": "pkg:maven/org.seleniumhq.selenium/selenium-api@4.5.0?type=jar", purl: "pkg:maven/org.seleniumhq.selenium/selenium-api@4.5.0?type=jar", }, ]); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-rich5.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 67); assert.deepStrictEqual(parsedList.dependenciesList.length, 68); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-out-249.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 21); assert.deepStrictEqual(parsedList.dependenciesList.length, 22); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-service.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 35); assert.deepStrictEqual(parsedList.dependenciesList.length, 36); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-s.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 28); assert.deepStrictEqual(parsedList.dependenciesList.length, 29); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-core.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 18); assert.deepStrictEqual(parsedList.dependenciesList.length, 19); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-single.out", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 152); assert.deepStrictEqual(parsedList.dependenciesList.length, 153); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 102); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-android-jetify.dep", { encoding: "utf-8", }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 1); assert.deepStrictEqual(parsedList.pkgList, [ { group: "androidx.appcompat", name: "appcompat", version: "1.2.0", qualifiers: { type: "jar" }, "bom-ref": "pkg:maven/androidx.appcompat/appcompat@1.2.0?type=jar", purl: "pkg:maven/androidx.appcompat/appcompat@1.2.0?type=jar", }, ]); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-sm.dep", { encoding: "utf-8" }), "test-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 6); assert.deepStrictEqual(parsedList.dependenciesList.length, 7); parsedList = await parseGradleDep( readFileSync("./test/data/gradle-dependencies-559.txt", { encoding: "utf-8", }), "failing-project", modulesMap, ); assert.deepStrictEqual(parsedList.pkgList.length, 372); }); it("parse gradle projects", () => { assert.deepStrictEqual(parseGradleProjects(null), { projects: [], rootProject: "root", }); let retMap = parseGradleProjects( readFileSync("./test/data/gradle-projects.out", { encoding: "utf-8" }), ); assert.deepStrictEqual(retMap.rootProject, "elasticsearch"); assert.deepStrictEqual(retMap.projects.length, 368); retMap = parseGradleProjects( readFileSync("./test/data/gradle-projects1.out", { encoding: "utf-8" }), ); assert.deepStrictEqual(retMap.rootProject, "elasticsearch"); assert.deepStrictEqual(retMap.projects.length, 409); retMap = parseGradleProjects( readFileSync("./test/data/gradle-projects2.out", { encoding: "utf-8" }), ); assert.deepStrictEqual(retMap.rootProject, "fineract"); assert.deepStrictEqual(retMap.projects.length, 22); retMap = parseGradleProjects( readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" }), ); assert.deepStrictEqual(retMap.rootProject, "root"); assert.deepStrictEqual(retMap.projects, [":app"]); retMap = parseGradleProjects( readFileSync("./test/data/gradle-properties-sm.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap.rootProject, "root"); assert.deepStrictEqual(retMap.projects, [ ":module:dummy:core", ":module:dummy:service", ":module:dummy:starter", ":custom:foo:service", ]); }); it("parse gradle properties", () => { assert.deepStrictEqual(parseGradleProperties(null), { projects: [], rootProject: "root", metadata: { group: "", version: "latest", properties: [], }, }); let retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties.txt", { encoding: "utf-8" }), ); assert.deepStrictEqual(retMap, { rootProject: "dependency-diff-check", projects: [ ":dependency-diff-check-client-starter", ":dependency-diff-check-common-core", ":dependency-diff-check-service", ], metadata: { group: "com.ajmalab", version: "0.0.1-SNAPSHOT", properties: [ { name: "GradleModule", value: "dependency-diff-check", }, { name: "buildFile", value: "/home/almalinux/work/sandbox/dependency-diff-check/build.gradle", }, { name: "projectDir", value: "/home/almalinux/work/sandbox/dependency-diff-check", }, { name: "rootDir", value: "/home/almalinux/work/sandbox/dependency-diff-check", }, ], }, }); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-single.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap, { rootProject: "java-test", projects: [":app"], metadata: { group: "com.ajmalab.demo", version: "latest", properties: [ { name: "GradleModule", value: "java-test", }, { name: "buildFile", value: "/home/almalinux/work/sandbox/java-test/build.gradle", }, { name: "projectDir", value: "/home/almalinux/work/sandbox/java-test", }, { name: "rootDir", value: "/home/almalinux/work/sandbox/java-test" }, ], }, }); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-single2.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap, { rootProject: "java-test", projects: [], metadata: { group: "com.ajmalab.demo", version: "latest", properties: [ { name: "GradleModule", value: "java-test", }, { name: "buildFile", value: "/home/almalinux/work/sandbox/java-test/build.gradle", }, { name: "projectDir", value: "/home/almalinux/work/sandbox/java-test" }, { name: "rootDir", value: "/home/almalinux/work/sandbox/java-test" }, ], }, }); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-elastic.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap.rootProject, "elasticsearch"); assert.deepStrictEqual(retMap.projects.length, 409); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-android.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap.rootProject, "CdxgenAndroidTest"); assert.deepStrictEqual(retMap.projects.length, 2); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-sm.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap.rootProject, "root"); assert.deepStrictEqual(retMap.projects, []); retMap = parseGradleProperties( readFileSync("./test/data/gradle-properties-559.txt", { encoding: "utf-8", }), ); assert.deepStrictEqual(retMap.rootProject, "failing-project"); assert.deepStrictEqual(retMap.projects, []); }); 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-repo