From fedbbb67a4c9625cf593f6eb379d118a92527b75 Mon Sep 17 00:00:00 2001 From: Rhys Koedijk Date: Mon, 30 Dec 2024 22:18:30 +1300 Subject: [PATCH] Support for generating multiple spdx manifests from a single task run, using glob patterns --- task/index.ts | 91 ++++++--- task/package-lock.json | 383 ++++++++++++++++++++++++++++++++--- task/package.json | 3 +- task/task.json | 55 +++-- task/utils/globs.ts | 21 ++ task/utils/sbomToolRunner.ts | 257 +++++++++++++---------- 6 files changed, 618 insertions(+), 192 deletions(-) create mode 100644 task/utils/globs.ts diff --git a/task/index.ts b/task/index.ts index adb711f..2719eb4 100644 --- a/task/index.ts +++ b/task/index.ts @@ -1,46 +1,91 @@ -import { getBoolInput, getInput, getInputRequired, setResult, TaskResult } from 'azure-pipelines-task-lib/task'; +import fs from 'fs'; +import path from 'path'; + +import { + error, + getBoolInput, + getInput, + getInputRequired, + getVariable, + setResult, + TaskResult, +} from 'azure-pipelines-task-lib/task'; + import { getGithubAccessToken } from './utils/azureDevOps/getGithubAccessToken'; +import { getFilesMatchingPathGlobs, resolvePathGlobs } from './utils/globs'; import { SbomToolRunner } from './utils/sbomToolRunner'; async function run() { + let taskResult: TaskResult = TaskResult.Succeeded; + let lastError: any; try { const sbomTool = new SbomToolRunner(getInput('version', false)); const sbomCommand = getInput('command', true); switch (sbomCommand) { + // Generate SBOM manifest(s) case 'generate': - await sbomTool.generateAsync({ - buildSourcePath: getInputRequired('buildSourcePath'), - buildArtifactPath: getInputRequired('buildArtifactPath'), - buildFileList: getInput('buildFileList', false), - buildDockerImagesToScan: getInput('buildDockerImagesToScan', false), - manifestOutputPath: getInput('manifestOutputPath', false), - enableManifestSpreadsheetGeneration: getBoolInput('enableManifestSpreadsheetGeneration', false), - enableManifestGraphGeneration: getBoolInput('enableManifestGraphGeneration', false), - enablePackageMetadataParsing: getBoolInput('enablePackageMetadataParsing', false), - fetchLicenseInformation: getBoolInput('fetchLicenseInformation', false), - fetchSecurityAdvisories: getBoolInput('fetchSecurityAdvisories', false), - gitHubAccessToken: getGithubAccessToken(), - packageName: getInputRequired('packageName'), - packageVersion: getInputRequired('packageVersion'), - packageSupplier: getInputRequired('packageSupplier'), - packageNamespaceUriBase: getInput('packageNamespaceUriBase', false), - packageNamespaceUriUniquePart: getInput('packageNamespaceUriUniquePart', false), - additionalComponentDetectorArgs: getInput('additionalComponentDetectorArgs', false), - externalDocumentReferenceListFile: getInput('externalDocumentReferenceListFile', false), - }); + const packageSourcePaths = resolvePathGlobs(getInputRequired('buildSourcePath')).map((p) => + fs.lstatSync(p).isDirectory() ? p : path.dirname(p), + ); + for (var i = 0; i < packageSourcePaths.length; i++) { + const packageName = path.basename(packageSourcePaths[i]); + const packageSourcePath = packageSourcePaths[i]; + const packageArtifactPaths = resolvePathGlobs(getInputRequired('buildArtifactPath'), packageSourcePath).map( + (p) => (fs.lstatSync(p).isDirectory() ? p : path.dirname(p)), + ); + try { + await sbomTool.generateAsync({ + buildSourcePath: packageSourcePath, + buildArtifactPath: packageArtifactPaths[0] || packageSourcePath, + buildFileList: getFilesMatchingPathGlobs(packageArtifactPaths, packageSourcePath), + buildDockerImagesToScan: getInput('buildDockerImagesToScan', false), + manifestOutputPath: getInput('manifestOutputPath', false), + manifestFileNamePrefix: + getInput('manifestOutputPath', false) && packageSourcePaths.length > 1 ? `${packageName}.` : undefined, + enableManifestSpreadsheetGeneration: getBoolInput('enableManifestSpreadsheetGeneration', false), + enableManifestGraphGeneration: getBoolInput('enableManifestGraphGeneration', false), + enablePackageMetadataParsing: getBoolInput('enablePackageMetadataParsing', false), + fetchLicenseInformation: getBoolInput('fetchLicenseInformation', false), + fetchSecurityAdvisories: getBoolInput('fetchSecurityAdvisories', false), + gitHubAccessToken: getGithubAccessToken(), + packageName: + (packageSourcePaths.length == 1 + ? getInput('packageName', false) || getVariable('Build.Repository.Name') + : packageName) || packageName, + packageVersion: getInput('packageVersion', false) || getVariable('Build.BuildNumber') || '0.0.0', + packageSupplier: getInput('packageSupplier', false) || getVariable('System.CollectionId') || 'Unknown', + packageNamespaceUriBase: getInput('packageNamespaceUriBase', false), + packageNamespaceUriUniquePart: getInput('packageNamespaceUriUniquePart', false), + additionalComponentDetectorArgs: getInput('additionalComponentDetectorArgs', false), + externalDocumentReferenceListFile: getInput('externalDocumentReferenceListFile', false), + }); + } catch (e) { + lastError = e; + console.debug(e); // Dump the stack trace, helps with debugging + error(`SBOM generation failed for '${packageSourcePath}'. ${e}`); + taskResult = TaskResult.SucceededWithIssues; + } + } break; + + // Validate SBOM manifest(s) case 'validate': throw new Error('Not implemented'); + + // Redact SBOM manifest(s) case 'redact': throw new Error('Not implemented'); + + // Unknown command default: throw new Error(`Invalid command: ${sbomCommand}`); } - setResult(TaskResult.Succeeded, 'Success'); + setResult(taskResult, lastError?.message || '', true); } catch (e: any) { - setResult(TaskResult.Failed, e?.message); console.debug(e); // Dump the stack trace, helps with debugging + error(`SBOM task failed. ${e}`); + setResult(TaskResult.Failed, e?.message, true); } } diff --git a/task/package-lock.json b/task/package-lock.json index 80332ea..5c56591 100644 --- a/task/package-lock.json +++ b/task/package-lock.json @@ -4,10 +4,10 @@ "requires": true, "packages": { "": { - "name": "task", "dependencies": { "@vizdom/vizdom-ts-node": "^0.1.18", - "azure-pipelines-task-lib": "^4.17.3" + "azure-pipelines-task-lib": "^4.17.3", + "glob": "^11.0.0" }, "devDependencies": { "copy-webpack-plugin": "^12.0.2", @@ -26,6 +26,22 @@ "node": ">=14.17.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -455,11 +471,21 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -631,7 +657,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -642,8 +667,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/colorette": { "version": "2.0.20", @@ -690,7 +714,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -716,12 +739,22 @@ } } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/electron-to-chromium": { "version": "1.5.64", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", "dev": true }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -931,6 +964,21 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -945,20 +993,22 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -982,15 +1032,26 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globby": { @@ -1125,6 +1186,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1161,8 +1230,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -1173,6 +1241,20 @@ "node": ">=0.10.0" } }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -1229,6 +1311,14 @@ "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1287,6 +1377,14 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1368,6 +1466,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1389,7 +1492,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -1399,6 +1501,21 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", @@ -1663,7 +1780,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1675,7 +1791,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -1696,6 +1811,48 @@ "node": ">=4" } }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shelljs/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -1727,6 +1884,94 @@ "source-map": "^0.6.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2155,7 +2400,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2172,6 +2416,87 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/task/package.json b/task/package.json index 4fb8c61..5beb125 100644 --- a/task/package.json +++ b/task/package.json @@ -2,7 +2,8 @@ "private": true, "dependencies": { "@vizdom/vizdom-ts-node": "^0.1.18", - "azure-pipelines-task-lib": "^4.17.3" + "azure-pipelines-task-lib": "^4.17.3", + "glob": "^11.0.0" }, "devDependencies": { "copy-webpack-plugin": "^12.0.2", diff --git a/task/task.json b/task/task.json index 34c4c50..e141d36 100644 --- a/task/task.json +++ b/task/task.json @@ -44,34 +44,25 @@ }, { "name": "buildSourcePath", - "type": "string", - "label": "Build Source Path", + "type": "multiLine", + "label": "Build Source Paths", "defaultValue": "$(Build.SourcesDirectory)", "required": true, - "helpMarkDown": "The directory containing build source files to be scanned for package dependencies. Defaults to the source repository root.", + "helpMarkDown": "The list of source code directories (one per line) to be scanned for package dependencies. [Glob patterns](https://globster.xyz/) are supported. Each directory will generates its own set of SBOM manifest files. Defaults to the build repository root directory, which will result in a single SBOM for the entire repository.", "visibleRule": "command=generate" }, { "name": "buildArtifactPath", - "type": "string", - "label": "Build Artifact Path", + "type": "multiLine", + "label": "Build Artifact Paths", "defaultValue": "$(Build.ArtifactStagingDirectory)", "required": true, - "helpMarkDown": "The directory containing (published) build artifact files for which the SBOM file will describe. Defaults to the build artifact staging directory.", - "visibleRule": "command=generate" - }, - { - "name": "buildFileList", - "type": "multiLine", - "label": "Build File List", - "defaultValue": "", - "required": false, - "helpMarkDown": "The list of file paths (one per line) for which the SBOM file will describe. If specified, this overrides `buildArtifactPath`, only files listed here will be included in the generated SBOM.", + "helpMarkDown": "The list of directories or files (one per line) to be included as build artifact files in the SBOM. [Glob patterns](https://globster.xyz/) are supported. Relative paths will be resolved from the source code directory. Defaults to the build artifact staging directory.", "visibleRule": "command=generate" }, { "name": "buildDockerImagesToScan", - "type": "string", + "type": "multiLine", "label": "Build Docker Images To Scan", "defaultValue": "", "required": false, @@ -84,7 +75,7 @@ "label": "Manifest Output Path", "defaultValue": "", "required": false, - "helpMarkDown": "The directory where the generated SBOM files will be placed. A subdirectory named '_manifest' will be created at this location, where all generated SBOMs will be placed. Defaults to `buildArtifactPath`.", + "helpMarkDown": "The directory where the generated SBOM manifest files will be written. A subdirectory named '_manifest' will be created at this location. Defaults to `buildArtifactPath`.", "visibleRule": "command=generate" }, { @@ -93,7 +84,7 @@ "label": "Enable Manifest XLSX Spreadsheet Generation", "defaultValue": false, "required": false, - "helpMarkDown": "If set to `true`, a XLSX spreadsheet representation of the SBOM content will be generated in the manifest directory.", + "helpMarkDown": "If set to `true`, a XLSX spreadsheet representation of the SBOM contents will be generated in the manifest directory.", "visibleRule": "command=generate" }, { @@ -102,7 +93,7 @@ "label": "Enable Manifest SVG Graph Generation", "defaultValue": false, "required": false, - "helpMarkDown": "If set to `true`, a SVG graph diagram of the SBOM content will be generated in the manifest directory.", + "helpMarkDown": "If set to `true`, a SVG graph diagram of the SBOM contents will be generated in the manifest directory.", "visibleRule": "command=generate" }, { @@ -111,7 +102,7 @@ "label": "Enable Package Metadata Parsing", "defaultValue": false, "required": false, - "helpMarkDown": "If set to `true`, we will attempt to parse license and supplier info from the packages metadata file (RubyGems, NuGet, Maven, Npm).", + "helpMarkDown": "If set to `true`, we will attempt to parse license and supplier info from the packages metadata file.", "visibleRule": "command=generate" }, { @@ -120,7 +111,7 @@ "label": "Fetch License Information", "defaultValue": false, "required": false, - "helpMarkDown": "If set to true, we will attempt to fetch license information of packages detected in the SBOM from [ClearlyDefined](https://clearlydefined.io/).", + "helpMarkDown": "If set to true, we will attempt to fetch license information for all packages using [ClearlyDefined](https://clearlydefined.io/).", "visibleRule": "command=generate" }, { @@ -129,7 +120,7 @@ "label": "Fetch Security Advisory Information", "defaultValue": false, "required": false, - "helpMarkDown": "If set to true, we will attempt to fetch security advisory information of packages detected in the SBOM from the [GitHub Advisory Database](https://github.com/advisories). If enabled, one of `gitHubAccessToken` or `gitHubServiceConnection` must be configured to access the GHSA database. If any security advisories are found, the SPDX document version will be upgraded to 2.3, the minimum version that supports security advisory references.", + "helpMarkDown": "If set to true, we will attempt to fetch security advisory information for all packages using the [GitHub Advisory Database](https://github.com/advisories). If enabled, one of `gitHubAccessToken` or `gitHubServiceConnection` must be configured to access the GHSA database. If any security advisories are found, the SPDX document version will be upgraded to 2.3, which is the minimum version that supports security advisory references.", "visibleRule": "command=generate" }, { @@ -155,9 +146,9 @@ "name": "packageName", "type": "string", "label": "Package Name", - "defaultValue": "$(System.TeamProject)", - "required": true, - "helpMarkDown": "The name of the package this SBOM represents. Defaults to the parent project name. If this is empty, we will try to infer this name from the build that generated this package.", + "defaultValue": "$(Build.Repository.Name)", + "required": false, + "helpMarkDown": "The name of the package that this SBOM will describe. Defaults to the build repository name if `buildSourcePath` is a single directory; Defaults to the parent directory name when `buildSourcePath` is a glob pattern.", "visibleRule": "command=generate" }, { @@ -166,8 +157,8 @@ "type": "string", "label": "Package Version", "defaultValue": "$(Build.BuildNumber)", - "required": true, - "helpMarkDown": "The version of the package this SBOM represents. Default to the pipeline build number. If this is empty, we will try to infer the version from the build that generated this package.", + "required": false, + "helpMarkDown": "The version of the package that this SBOM will describe. Defaults to the build pipeline version number. ", "visibleRule": "command=generate" }, { @@ -176,8 +167,8 @@ "type": "string", "label": "Package Supplier", "defaultValue": "$(System.CollectionId)", - "required": true, - "helpMarkDown": "The supplier of the package that this SBOM represents. Defaults to the parent collection GUID.", + "required": false, + "helpMarkDown": "The supplier name of the package that this SBOM will describe. Defaults to the parent collection GUID.", "visibleRule": "command=generate" }, { @@ -187,7 +178,7 @@ "label": "Package Namespace URI Base", "defaultValue": "", "required": false, - "helpMarkDown": "The base path of the SBOM namespace URI. Defaults to `https://{packageSupplier}.com`. For example, a URI base of `https://companyName.com/teamName` will create the (unique) namespace `https://companyName.com/teamName/{packageName}/{packageVersion}/{newGuid}`.", + "helpMarkDown": "The base path of the SPDX namespace URI that this SBOM will describe. Defaults to `https://{packageSupplier}.com`. For example, a base path of `https://companyName.com/teamName` will create the (unique) namespace URI `https://companyName.com/teamName/{packageName}/{packageVersion}/{newGuid}`.", "visibleRule": "command=generate" }, { @@ -197,7 +188,7 @@ "label": "Package Namespace URI Unique Part", "defaultValue": "", "required": false, - "helpMarkDown": "A unique valid URI part that will be appended to the SBOM namespace URI. If specified, this value should be globally unique.", + "helpMarkDown": "A unique valid URI part that will be appended to the SPDX namespace URI. If specified, this value should be globally unique.", "visibleRule": "command=generate" }, { @@ -217,7 +208,7 @@ "label": "External Document Reference List", "defaultValue": "", "required": false, - "helpMarkDown": "A list of external SBOMs (one per line) that will be included as external document references in the SBOM. Referenced files must be in SPDX 2.2 format.", + "helpMarkDown": "A list of external SBOMs (one per line) that will be included as external document references in the SBOM. Referenced files must be in SPDX 2.X format.", "visibleRule": "command=generate" } ], diff --git a/task/utils/globs.ts b/task/utils/globs.ts new file mode 100644 index 0000000..fe21140 --- /dev/null +++ b/task/utils/globs.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import path from 'path'; + +import { globSync } from 'glob'; + +export function resolvePathGlobs(paths: string | string[], cwd?: string): string[] { + const patterns = Array.isArray(paths) + ? paths + : paths + .split(/\r?\n|;/) + .map((p) => p.trim()) + .filter((p) => p); + return globSync(patterns, { cwd: cwd, absolute: true, realpath: true }); +} + +export function getFilesMatchingPathGlobs(paths: string | string[], cwd?: string): string[] { + const resolvedPaths = resolvePathGlobs(paths, cwd); + return resolvedPaths.flatMap((p) => + fs.lstatSync(p).isDirectory() ? getFilesMatchingPathGlobs(p + path.sep + '*') : [p], + ); +} diff --git a/task/utils/sbomToolRunner.ts b/task/utils/sbomToolRunner.ts index 0e35767..c55493c 100644 --- a/task/utils/sbomToolRunner.ts +++ b/task/utils/sbomToolRunner.ts @@ -1,10 +1,11 @@ -import { addAttachment, getVariable, tool, which } from 'azure-pipelines-task-lib/task'; import { existsSync as fileExistsSync } from 'fs'; import * as fs from 'fs/promises'; import { tmpdir } from 'node:os'; import * as path from 'path'; -import { section } from './azureDevOps/formatCommands'; +import { addAttachment, getVariable, tool, which } from 'azure-pipelines-task-lib/task'; + +import { endgroup, group, section } from './azureDevOps/formatCommands'; import { IDocument } from '../../shared/models/spdx/2.3/IDocument'; @@ -19,10 +20,11 @@ const MANIFEST_VERSION = '2.2'; export interface SbomGenerateArgs { buildSourcePath: string; - buildArtifactPath: string; - buildFileList?: string; + buildArtifactPath?: string; + buildFileList?: string[]; buildDockerImagesToScan?: string; manifestOutputPath?: string; + manifestFileNamePrefix?: string; enableManifestSpreadsheetGeneration?: boolean; enableManifestGraphGeneration?: boolean; enablePackageMetadataParsing?: boolean; @@ -39,16 +41,18 @@ export interface SbomGenerateArgs { } export class SbomToolRunner { - private toolsDirectory: string; - private toolArchitecture: string; - private toolVersion?: string; + private agentOperatingSystem: string; + private agentArchitecture: string; + private agentToolsDirectory: string; private debug: boolean; + private version?: string; constructor(version?: string) { - this.toolsDirectory = getVariable('Agent.ToolsDirectory') || __dirname; - this.toolArchitecture = getVariable('Agent.OSArchitecture') || 'x64'; - this.toolVersion = version; + this.agentOperatingSystem = getVariable('Agent.OS') || 'Linux'; + this.agentArchitecture = getVariable('Agent.OSArchitecture') || 'x64'; + this.agentToolsDirectory = getVariable('Agent.ToolsDirectory') || __dirname; this.debug = getVariable('System.Debug')?.toLocaleLowerCase() == 'true'; + this.version = version; } // Run `sbom-tool generate` command @@ -57,106 +61,145 @@ export class SbomToolRunner { // Find the sbom-tool path, or install it if missing const sbomToolPath = await this.getToolPathAsync(); - // Build sbom-tool arguments - // See: https://github.com/microsoft/sbom-tool/blob/main/docs/sbom-tool-arguments.md - let sbomToolArguments = ['generate']; - sbomToolArguments.push('-bc', args.buildSourcePath); - sbomToolArguments.push('-b', args.buildArtifactPath); - if (args.buildFileList) { - sbomToolArguments.push('-bl', await createTemporaryFileAsync('build-file-list', args.buildFileList)); - } - if (args.buildDockerImagesToScan) { - sbomToolArguments.push('-di', args.buildDockerImagesToScan); - } - if (args.manifestOutputPath) { - sbomToolArguments.push('-m', args.manifestOutputPath); - } - sbomToolArguments.push('-D', 'true'); - if (args.enablePackageMetadataParsing) { - sbomToolArguments.push('-pm', 'true'); - } - if (args.fetchLicenseInformation) { - sbomToolArguments.push('-li', 'true'); - } - sbomToolArguments.push('-pn', args.packageName); - sbomToolArguments.push('-pv', args.packageVersion); - sbomToolArguments.push('-ps', args.packageSupplier); - if (args.packageNamespaceUriBase) { - sbomToolArguments.push('-nsb', args.packageNamespaceUriBase); - } else { - // No base namespace provided, so generate one from the supplier name - // To get a valid URI hostname, replace spaces with dashes, strip all other special characters, convert to lowercase - const supplierHostname = args.packageSupplier - .replace(/\s+/g, '-') - .replace(/[^a-zA-Z0-9 ]/g, '') - .toLowerCase(); - sbomToolArguments.push('-nsb', `https://${supplierHostname}.com`); - } - if (args.packageNamespaceUriUniquePart) { - sbomToolArguments.push('-nsu', args.packageNamespaceUriUniquePart); - } - if (args.additionalComponentDetectorArgs) { - sbomToolArguments.push('-cd', args.additionalComponentDetectorArgs); - } - if (args.externalDocumentReferenceListFile) { - sbomToolArguments.push( - '-er', - await createTemporaryFileAsync('external-doc-refs-list', args.externalDocumentReferenceListFile), + group(`SBOM generate '${args.packageName}' in ${args.buildSourcePath}`); + try { + // Sanity check + if (!args.buildArtifactPath && !args.buildFileList) { + throw new Error('Either `buildArtifactPath` or `buildFileList` must be provided'); + } + + // Build sbom-tool arguments + // See: https://github.com/microsoft/sbom-tool/blob/main/docs/sbom-tool-arguments.md + let sbomToolArguments = ['generate']; + sbomToolArguments.push('-bc', args.buildSourcePath); + if (args.buildArtifactPath) { + sbomToolArguments.push('-b', args.buildArtifactPath); + } + if (args.buildFileList) { + sbomToolArguments.push('-bl', await createTemporaryFileAsync('build-file-list', args.buildFileList.join('\n'))); + } + if (args.buildDockerImagesToScan) { + sbomToolArguments.push('-di', args.buildDockerImagesToScan); + } + if (args.manifestOutputPath) { + sbomToolArguments.push('-m', args.manifestOutputPath); + } + sbomToolArguments.push('-D', 'true'); + if (args.enablePackageMetadataParsing) { + sbomToolArguments.push('-pm', 'true'); + } + if (args.fetchLicenseInformation) { + sbomToolArguments.push('-li', 'true'); + } + sbomToolArguments.push('-pn', args.packageName); + sbomToolArguments.push('-pv', args.packageVersion); + sbomToolArguments.push('-ps', args.packageSupplier); + if (args.packageNamespaceUriBase) { + sbomToolArguments.push('-nsb', args.packageNamespaceUriBase); + } else { + // No base namespace provided, so generate one from the supplier name + // To get a valid URI hostname, replace spaces with dashes, strip all other special characters, convert to lowercase + const supplierHostname = args.packageSupplier + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9 ]/g, '') + .toLowerCase(); + sbomToolArguments.push('-nsb', `https://${supplierHostname}.com`); + } + if (args.packageNamespaceUriUniquePart) { + sbomToolArguments.push('-nsu', args.packageNamespaceUriUniquePart); + } + if (args.additionalComponentDetectorArgs) { + sbomToolArguments.push('-cd', args.additionalComponentDetectorArgs); + } + if (args.externalDocumentReferenceListFile) { + sbomToolArguments.push( + '-er', + await createTemporaryFileAsync('external-doc-refs-list', args.externalDocumentReferenceListFile), + ); + } + sbomToolArguments.push('-V', this.debug ? 'Debug' : 'Information'); + + // Run sbom-tool + section(`Running 'sbom-tool generate'`); + const sbomTool = tool(sbomToolPath).arg(sbomToolArguments); + const sbomToolResultCode = await sbomTool.execAsync({ + failOnStdErr: false, + ignoreReturnCode: true, + }); + if (sbomToolResultCode != 0) { + throw new Error(`SBOM Tool failed with exit code ${sbomToolResultCode}`); + } + + const manifestOutputPath = path.join( + args.manifestOutputPath || args.buildArtifactPath || __dirname, + MANIFEST_DIR_NAME, + `${MANIFEST_FORMAT}_${MANIFEST_VERSION}`, ); - } - sbomToolArguments.push('-V', this.debug ? 'Debug' : 'Information'); - - // Run sbom-tool - section(`Running 'sbom-tool generate'`); - const sbomTool = tool(sbomToolPath).arg(sbomToolArguments); - const sbomToolResultCode = await sbomTool.execAsync({ - failOnStdErr: false, - ignoreReturnCode: true, - }); - if (sbomToolResultCode != 0) { - throw new Error(`SBOM Tool failed with exit code ${sbomToolResultCode}`); - } - const spdxPath = path.join( - args.manifestOutputPath || args.buildArtifactPath || __dirname, - MANIFEST_DIR_NAME, - `${MANIFEST_FORMAT}_${MANIFEST_VERSION}`, - `manifest.${MANIFEST_FORMAT}.json`, - ); - if (!fileExistsSync(spdxPath)) { - throw new Error(`SBOM Tool did not generate SPDX file: '${spdxPath}'`); - } - // Check packages for security advisories - if (args.fetchSecurityAdvisories && args.gitHubAccessToken) { - section('Checking package security advisories'); - await addSpdxPackageSecurityAdvisoryExternalRefsAsync(spdxPath, args.gitHubAccessToken); - } + // Rename all SPDX files in the manifest directory if a file name prefix is provided + if (args.manifestFileNamePrefix) { + const manifestFiles = await fs.readdir(manifestOutputPath); + if (manifestFiles) { + for (const manifestFileName of manifestFiles) { + if (manifestFileName.startsWith('manifest')) { + const newManifestFileName = args.manifestFileNamePrefix + manifestFileName; + await fs.rename( + path.join(manifestOutputPath, manifestFileName), + path.join(manifestOutputPath, newManifestFileName), + ); + } + } + } + } - // Attach the SPDX file to the build timeline; This is used by the SBOM report tab. - addAttachment(`${MANIFEST_FORMAT}.json`, path.basename(spdxPath), spdxPath); - const spdxContent = await fs.readFile(spdxPath, 'utf8'); - const spdx = JSON.parse(spdxContent) as IDocument; + // Check for the generated SPDX json file + const spdxJsonPath = path.join( + manifestOutputPath, + `${args.manifestFileNamePrefix || ''}manifest.${MANIFEST_FORMAT}.json`, + ); + if (!fileExistsSync(spdxJsonPath)) { + throw new Error(`SBOM Tool did not generate SPDX file: '${spdxJsonPath}'`); + } - // Generate a XLSX spreadsheet of the SPDX file, if configured - if (args.enableManifestSpreadsheetGeneration && spdxContent) { - section(`Generating XLSX spreadsheet`); - const xlsx = await convertSpdxToXlsxAsync(spdx); - if (xlsx) { - const xlsxPath = path.format({ ...path.parse(spdxPath), base: '', ext: '.xlsx' }); - await fs.writeFile(xlsxPath, xlsx); + // Add security advisories to the SPDX file, if configured + if (args.fetchSecurityAdvisories && args.gitHubAccessToken) { + section('Checking packages for security advisories'); + await addSpdxPackageSecurityAdvisoryExternalRefsAsync(spdxJsonPath, args.gitHubAccessToken); + } + + // Attach the SPDX file to the build timeline so we can view it in the SBOM build result tab. + const spdxJsonContent = await fs.readFile(spdxJsonPath, 'utf8'); + addAttachment(`${MANIFEST_FORMAT}.json`, path.basename(spdxJsonPath), spdxJsonPath); + + // Regenerate the SHA-256 hash of the SPDX file, in case it was modified + section('Generating SHA-256 hash file'); + // TODO: writeSha265HashFileAsync(spdxJsonPath); + + // Generate a XLSX spreadsheet of the SPDX file, if configured + if (args.enableManifestSpreadsheetGeneration && spdxJsonContent) { + section(`Generating XLSX spreadsheet`); + const xlsx = await convertSpdxToXlsxAsync(JSON.parse(spdxJsonContent) as IDocument); + if (xlsx) { + const xlsxPath = path.format({ ...path.parse(spdxJsonPath), base: '', ext: '.xlsx' }); + await fs.writeFile(xlsxPath, xlsx); + // TODO: writeSha265HashFileAsync(xlsxPath); + } } - } - // Generate a SVG graph diagram of the SPDX file - if (args.enableManifestGraphGeneration && spdxContent) { - section(`Generating SVG graph diagram`); - const svg = await convertSpdxToSvgAsync(spdx); - if (svg) { - const svgPath = path.format({ ...path.parse(spdxPath), base: '', ext: '.svg' }); - await fs.writeFile(svgPath, svg); - // TODO: Remove this attachment once web browser SPDX to SVG generation is implemented - addAttachment(`${MANIFEST_FORMAT}.svg`, path.basename(svgPath), svgPath); + // Generate a SVG graph diagram of the SPDX file + if (args.enableManifestGraphGeneration && spdxJsonContent) { + section(`Generating SVG graph diagram`); + const svg = await convertSpdxToSvgAsync(JSON.parse(spdxJsonContent) as IDocument); + if (svg) { + const svgPath = path.format({ ...path.parse(spdxJsonPath), base: '', ext: '.svg' }); + await fs.writeFile(svgPath, svg); + // TODO: writeSha265HashFileAsync(svgPath); + // TODO: Remove this attachment once web browser SPDX to SVG generation is implemented + addAttachment(`${MANIFEST_FORMAT}.svg`, path.basename(svgPath), svgPath); + } } + } finally { + endgroup(); } } @@ -172,16 +215,16 @@ export class SbomToolRunner { console.info('SBOM Tool install was not found, attempting to install now...'); section("Installing 'sbom-tool'"); - switch (getVariable('Agent.OS')) { + switch (this.agentOperatingSystem) { case 'Darwin': case 'Linux': - toolPath = await installToolLinuxAsync(this.toolsDirectory, this.toolArchitecture, this.toolVersion); + toolPath = await installToolLinuxAsync(this.agentToolsDirectory, this.agentArchitecture, this.version); break; case 'Windows_NT': - toolPath = await installToolWindowsAsync(this.toolsDirectory, this.toolArchitecture, this.toolVersion); + toolPath = await installToolWindowsAsync(this.agentToolsDirectory, this.agentArchitecture, this.version); break; default: - throw new Error(`Unable to install SBOM Tool, unsupported agent OS '${getVariable('Agent.OS')}'`); + throw new Error(`Unable to install SBOM Tool, unsupported agent OS '${this.agentOperatingSystem}'`); } return toolPath || which('sbom-tool', true);