From 028cd8fdf9cefce0538c3b1e37839c637ba50546 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 11 Dec 2024 13:26:28 -0500 Subject: [PATCH] feat: add output-file option, default to random directory output in temp (#346) Signed-off-by: Keith Zantow --- README.md | 1 + action.yml | 3 +++ dist/index.js | 35 ++++++++++++++++++++--------------- index.js | 35 ++++++++++++++++++++--------------- tests/action.test.js | 2 ++ tests/grype_command.test.js | 31 +++++++++++++++++++++++++++++-- 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 735900de..ae30ff33 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ The inputs `image`, `path`, and `sbom` are mutually exclusive to specify the sou | `registry-password` | The registry password to use when authenticating to an external registry | | | `fail-build` | Fail the build if a vulnerability is found with a higher severity. That severity defaults to `medium` and can be set with `severity-cutoff`. | `true` | | `output-format` | Set the output parameter after successful action execution. Valid choices are `json`, `sarif`, and `table`, where `table` output will print to the console instead of generating a file. | `sarif` | +| `output-file` | File to output the Grype scan results to. Defaults to a file in the system temp directory, available in the action outputs | | | `severity-cutoff` | Optionally specify the minimum vulnerability severity to trigger a failure. Valid choices are "negligible", "low", "medium", "high" and "critical". Any vulnerability with a severity less than this value will lead to a "warning" result. Default is "medium". | `medium` | | `only-fixed` | Specify whether to only report vulnerabilities that have a fix available. | `false` | | `add-cpes-if-none` | Specify whether to autogenerate missing CPEs. | `false` | diff --git a/action.yml b/action.yml index 3e938915..35db289f 100644 --- a/action.yml +++ b/action.yml @@ -21,6 +21,9 @@ inputs: description: 'Set the output parameter after successful action execution. Valid choices are "json", "sarif", and "table".' required: false default: "sarif" + output-file: + description: 'The file to output the grype scan results to' + required: false severity-cutoff: description: 'Optionally specify the minimum vulnerability severity to trigger an "error" level ACS result. Valid choices are "negligible", "low", "medium", "high" and "critical". Any vulnerability with a severity less than this value will lead to a "warning" result. Default is "medium".' required: false diff --git a/dist/index.js b/dist/index.js index a76b1c1b..6edfd62e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -148,11 +148,13 @@ async function run() { const byCve = core.getInput("by-cve") || "false"; const vex = core.getInput("vex") || ""; const cacheDb = core.getInput("cache-db") || "false"; + const outputFile = core.getInput("output-file") || ""; const out = await runScan({ source, failBuild, severityCutoff, onlyFixed, + outputFile, outputFormat, addCpesIfNone, byCve, @@ -299,6 +301,7 @@ async function runScan({ failBuild, severityCutoff, onlyFixed, + outputFile, outputFormat, addCpesIfNone, byCve, @@ -341,6 +344,15 @@ async function runScan({ cmdArgs.push("-o", outputFormat); + // always output to a file, this is read later to print table output + if (!outputFile) { + outputFile = path.join( + fs.mkdtempSync(path.join(os.tmpdir(), "grype-")), + "output", + ); + } + cmdArgs.push("--file", outputFile); + if ( !SEVERITY_LIST.some( (item) => @@ -403,23 +415,16 @@ async function runScan({ } cmdArgs.push(source); - const { stdout, exitCode } = await runCommand(grypeCommand, cmdArgs, env); + const { exitCode } = await runCommand(grypeCommand, cmdArgs, env); - switch (outputFormat) { - case "sarif": { - const SARIF_FILE = "./results.sarif"; - fs.writeFileSync(SARIF_FILE, stdout); - out.sarif = SARIF_FILE; - break; - } - case "json": { - const REPORT_FILE = "./results.json"; - fs.writeFileSync(REPORT_FILE, stdout); - out.json = REPORT_FILE; - break; + out[outputFormat] = outputFile; + if (outputFormat === "table") { + try { + const report = fs.readFileSync(outputFile); + core.info(report.toString()); + } catch (e) { + core.warning(`error writing table output contents: ${e}`); } - default: // e.g. table - core.info(stdout); } // If there is a non-zero exit status code there are a couple of potential reporting paths diff --git a/index.js b/index.js index e98db026..7864c97c 100644 --- a/index.js +++ b/index.js @@ -134,11 +134,13 @@ async function run() { const byCve = core.getInput("by-cve") || "false"; const vex = core.getInput("vex") || ""; const cacheDb = core.getInput("cache-db") || "false"; + const outputFile = core.getInput("output-file") || ""; const out = await runScan({ source, failBuild, severityCutoff, onlyFixed, + outputFile, outputFormat, addCpesIfNone, byCve, @@ -285,6 +287,7 @@ async function runScan({ failBuild, severityCutoff, onlyFixed, + outputFile, outputFormat, addCpesIfNone, byCve, @@ -327,6 +330,15 @@ async function runScan({ cmdArgs.push("-o", outputFormat); + // always output to a file, this is read later to print table output + if (!outputFile) { + outputFile = path.join( + fs.mkdtempSync(path.join(os.tmpdir(), "grype-")), + "output", + ); + } + cmdArgs.push("--file", outputFile); + if ( !SEVERITY_LIST.some( (item) => @@ -389,23 +401,16 @@ async function runScan({ } cmdArgs.push(source); - const { stdout, exitCode } = await runCommand(grypeCommand, cmdArgs, env); + const { exitCode } = await runCommand(grypeCommand, cmdArgs, env); - switch (outputFormat) { - case "sarif": { - const SARIF_FILE = "./results.sarif"; - fs.writeFileSync(SARIF_FILE, stdout); - out.sarif = SARIF_FILE; - break; - } - case "json": { - const REPORT_FILE = "./results.json"; - fs.writeFileSync(REPORT_FILE, stdout); - out.json = REPORT_FILE; - break; + out[outputFormat] = outputFile; + if (outputFormat === "table") { + try { + const report = fs.readFileSync(outputFile); + core.info(report.toString()); + } catch (e) { + core.warning(`error writing table output contents: ${e}`); } - default: // e.g. table - core.info(stdout); } // If there is a non-zero exit status code there are a couple of potential reporting paths diff --git a/tests/action.test.js b/tests/action.test.js index dfbeac05..79d038b5 100644 --- a/tests/action.test.js +++ b/tests/action.test.js @@ -47,6 +47,7 @@ describe("Github action", () => { image: "", path: "tests/fixtures/npm-project", "fail-build": "true", + "output-file": "./results.json", "output-format": "json", "severity-cutoff": "medium", "add-cpes-if-none": "true", @@ -63,6 +64,7 @@ describe("Github action", () => { image: "", path: "tests/fixtures/npm-project", "fail-build": "true", + "output-file": "./results.sarif", "output-format": "sarif", "severity-cutoff": "medium", "add-cpes-if-none": "true", diff --git a/tests/grype_command.test.js b/tests/grype_command.test.js index 51fd55c4..38328212 100644 --- a/tests/grype_command.test.js +++ b/tests/grype_command.test.js @@ -11,6 +11,7 @@ describe("Grype command args", () => { const args = await mockRun({ source: "dir:.", "fail-build": "false", + "output-file": "the-output-file", "output-format": "sarif", "severity-cutoff": "high", version: "0.6.0", @@ -18,13 +19,22 @@ describe("Grype command args", () => { "add-cpes-if-none": "false", "by-cve": "false", }); - expect(args).toEqual(["-o", "sarif", "--fail-on", "high", "dir:."]); + expect(args).toEqual([ + "-o", + "sarif", + "--file", + "the-output-file", + "--fail-on", + "high", + "dir:.", + ]); }); it("is invoked with values", async () => { const args = await mockRun({ image: "asdf", "fail-build": "false", + "output-file": "the-output-file", "output-format": "json", "severity-cutoff": "low", version: "0.6.0", @@ -32,13 +42,22 @@ describe("Grype command args", () => { "add-cpes-if-none": "false", "by-cve": "false", }); - expect(args).toEqual(["-o", "json", "--fail-on", "low", "asdf"]); + expect(args).toEqual([ + "-o", + "json", + "--file", + "the-output-file", + "--fail-on", + "low", + "asdf", + ]); }); it("adds missing CPEs if requested", async () => { const args = await mockRun({ image: "asdf", "fail-build": "false", + "output-file": "the-output-file", "output-format": "json", "severity-cutoff": "low", version: "0.6.0", @@ -49,6 +68,8 @@ describe("Grype command args", () => { expect(args).toEqual([ "-o", "json", + "--file", + "the-output-file", "--fail-on", "low", "--add-cpes-if-none", @@ -60,6 +81,7 @@ describe("Grype command args", () => { const args = await mockRun({ image: "asdf", "fail-build": "false", + "output-file": "the-output-file", "output-format": "json", "severity-cutoff": "low", version: "0.6.0", @@ -71,6 +93,8 @@ describe("Grype command args", () => { expect(args).toEqual([ "-o", "json", + "--file", + "the-output-file", "--fail-on", "low", "--add-cpes-if-none", @@ -84,6 +108,7 @@ describe("Grype command args", () => { const args = await mockRun({ path: "asdf", "fail-build": "false", + "output-file": "the-output-file", "output-format": "table", "severity-cutoff": "low", "by-cve": "true", @@ -91,6 +116,8 @@ describe("Grype command args", () => { expect(args).toEqual([ "-o", "table", + "--file", + "the-output-file", "--fail-on", "low", "--by-cve",