Skip to content

Commit

Permalink
feat: add output-file option, default to random directory output in temp
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <kzantow@gmail.com>
  • Loading branch information
kzantow committed Jul 25, 2024
1 parent f7dc204 commit 7f269ee
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 36 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,15 @@ Optionally, change the `fail-build` field to `false` to avoid failing the build
The inputs `image`, `path`, and `sbom` are mutually exclusive to specify the source to scan; all the other keys are optional. These are all the available keys to configure this action, along with the defaults:

| Input Name | Description | Default Value |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `image` | The image to scan | N/A |
| `path` | The file path to scan | N/A |
| `sbom` | The SBOM to scan | N/A |
| `registry-username` | The registry username to use when authenticating to an external registry | |
| `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` |
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 20 additions & 14 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const cache = __nccwpck_require__(7784);
const core = __nccwpck_require__(2186);
const exec = __nccwpck_require__(1514);
const fs = __nccwpck_require__(7147);
const os = __nccwpck_require__(2037);
const path = __nccwpck_require__(1017);
const stream = __nccwpck_require__(2781);
const { GRYPE_VERSION } = __nccwpck_require__(6244);
Expand Down Expand Up @@ -130,11 +131,13 @@ async function run() {
const addCpesIfNone = core.getInput("add-cpes-if-none") || "false";
const byCve = core.getInput("by-cve") || "false";
const vex = core.getInput("vex") || "";
const outputFile = core.getInput("output-file") || "";
const out = await runScan({
source,
failBuild,
severityCutoff,
onlyFixed,
outputFile,
outputFormat,
addCpesIfNone,
byCve,
Expand All @@ -153,6 +156,7 @@ async function runScan({
failBuild,
severityCutoff,
onlyFixed,
outputFile,
outputFormat,
addCpesIfNone,
byCve,
Expand Down Expand Up @@ -193,6 +197,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) =>
Expand Down Expand Up @@ -286,21 +299,14 @@ async function runScan({
core.debug(cmdOutput);
}

switch (outputFormat) {
case "sarif": {
const SARIF_FILE = "./results.sarif";
fs.writeFileSync(SARIF_FILE, cmdOutput);
out.sarif = SARIF_FILE;
break;
}
case "json": {
const REPORT_FILE = "./results.json";
fs.writeFileSync(REPORT_FILE, cmdOutput);
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(cmdOutput);
}

// If there is a non-zero exit status code there are a couple of potential reporting paths
Expand Down
34 changes: 20 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const cache = require("@actions/tool-cache");
const core = require("@actions/core");
const exec = require("@actions/exec");
const fs = require("fs");
const os = require("os");
const path = require("path");
const stream = require("stream");
const { GRYPE_VERSION } = require("./GrypeVersion");
Expand Down Expand Up @@ -116,11 +117,13 @@ async function run() {
const addCpesIfNone = core.getInput("add-cpes-if-none") || "false";
const byCve = core.getInput("by-cve") || "false";
const vex = core.getInput("vex") || "";
const outputFile = core.getInput("output-file") || "";
const out = await runScan({
source,
failBuild,
severityCutoff,
onlyFixed,
outputFile,
outputFormat,
addCpesIfNone,
byCve,
Expand All @@ -139,6 +142,7 @@ async function runScan({
failBuild,
severityCutoff,
onlyFixed,
outputFile,
outputFormat,
addCpesIfNone,
byCve,
Expand Down Expand Up @@ -179,6 +183,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) =>
Expand Down Expand Up @@ -272,21 +285,14 @@ async function runScan({
core.debug(cmdOutput);
}

switch (outputFormat) {
case "sarif": {
const SARIF_FILE = "./results.sarif";
fs.writeFileSync(SARIF_FILE, cmdOutput);
out.sarif = SARIF_FILE;
break;
}
case "json": {
const REPORT_FILE = "./results.json";
fs.writeFileSync(REPORT_FILE, cmdOutput);
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(cmdOutput);
}

// If there is a non-zero exit status code there are a couple of potential reporting paths
Expand Down
56 changes: 53 additions & 3 deletions tests/action_args.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const { run } = require("../index");
const core = require("@actions/core");
const exec = require("@actions/exec");
const path = require("path");
const fs = require("fs");
const os = require("os");

jest.setTimeout(90000); // 90 seconds; tests were timing out in CI. https://github.com/anchore/scan-action/pull/249

Expand All @@ -13,7 +16,7 @@ describe("Github action args", () => {
"output-format": "json",
"severity-cutoff": "medium",
"add-cpes-if-none": "true",
"vex": "test.vex",
vex: "test.vex",
};
const spyInput = jest.spyOn(core, "getInput").mockImplementation((name) => {
try {
Expand All @@ -37,7 +40,7 @@ describe("Github action args", () => {
});

expect(outputs["sarif"]).toBeFalsy();
expect(outputs["json"]).toBe("./results.json");
expect(outputs["json"]).toBeDefined();

spyInput.mockRestore();
spyOutput.mockRestore();
Expand Down Expand Up @@ -73,7 +76,8 @@ describe("Github action args", () => {
expect(inputs[name]).toBe(true);
});

expect(outputs["sarif"]).toBe("./results.sarif");
expect(outputs["json"]).toBeFalsy();
expect(outputs["sarif"]).toBeDefined();

spyInput.mockRestore();
spyOutput.mockRestore();
Expand Down Expand Up @@ -117,12 +121,58 @@ describe("Github action args", () => {

expect(outputs["sarif"]).toBeFalsy();
expect(outputs["json"]).toBeFalsy();
expect(outputs["table"]).toBeDefined();

spyInput.mockRestore();
spyOutput.mockRestore();
spyStdout.mockRestore();
});

it("runs with output-file", async () => {
const reportFile = path.join(
fs.mkdtempSync(path.join(os.tmpdir(), "my-dir-")),
"my-grype-report.json",
);
const inputs = {
image: "localhost:5000/match-coverage/debian:latest",
"fail-build": "true",
"output-file": reportFile,
"output-format": "json",
"severity-cutoff": "medium",
"add-cpes-if-none": "true",
};
const spyInput = jest.spyOn(core, "getInput").mockImplementation((name) => {
try {
return inputs[name];
} finally {
inputs[name] = true;
}
});

const outputs = {};
const spyOutput = jest
.spyOn(core, "setOutput")
.mockImplementation((name, value) => {
outputs[name] = value;
});

await run();

Object.keys(inputs).map((name) => {
expect(inputs[name]).toBe(true);
});

expect(outputs["sarif"]).toBeFalsy();
expect(outputs["json"]).toBe(reportFile);
expect(outputs["table"]).toBeFalsy();

const report = JSON.parse(fs.readFileSync(reportFile).toString());
expect(report).toBeDefined();

spyInput.mockRestore();
spyOutput.mockRestore();
});

it("runs with environment variables", async () => {
const inputs = {
path: "tests/fixtures/npm-project",
Expand Down
16 changes: 12 additions & 4 deletions tests/grype_command.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,41 @@ describe("Grype command", () => {
let cmd = await mockExec({
source: "dir:.",
failBuild: "false",
outputFile: "the-output-file",
outputFormat: "sarif",
severityCutoff: "high",
version: "0.6.0",
onlyFixed: "false",
addCpesIfNone: "false",
byCve: "false",
});
expect(cmd).toBe(`${cmdPrefix} -o sarif --fail-on high dir:.`);
expect(cmd).toBe(
`${cmdPrefix} -o sarif --file the-output-file --fail-on high dir:.`,
);
});

it("is invoked with values", async () => {
let cmd = await mockExec({
source: "asdf",
failBuild: "false",
outputFile: "the-output-file",
outputFormat: "json",
severityCutoff: "low",
version: "0.6.0",
onlyFixed: "false",
addCpesIfNone: "false",
byCve: "false",
});
expect(cmd).toBe(`${cmdPrefix} -o json --fail-on low asdf`);
expect(cmd).toBe(
`${cmdPrefix} -o json --file the-output-file --fail-on low asdf`,
);
});

it("adds missing CPEs if requested", async () => {
let cmd = await mockExec({
source: "asdf",
failBuild: "false",
outputFile: "the-output-file",
outputFormat: "json",
severityCutoff: "low",
version: "0.6.0",
Expand All @@ -66,14 +73,15 @@ describe("Grype command", () => {
byCve: "false",
});
expect(cmd).toBe(
`${cmdPrefix} -o json --fail-on low --add-cpes-if-none asdf`
`${cmdPrefix} -o json --file the-output-file --fail-on low --add-cpes-if-none asdf`,
);
});

it("adds VEX processing if requested", async () => {
let cmd = await mockExec({
source: "asdf",
failBuild: "false",
outputFile: "the-output-file",
outputFormat: "json",
severityCutoff: "low",
version: "0.6.0",
Expand All @@ -83,7 +91,7 @@ describe("Grype command", () => {
vex: "test.vex",
});
expect(cmd).toBe(
`${cmdPrefix} -o json --fail-on low --add-cpes-if-none --vex test.vex asdf`
`${cmdPrefix} -o json --file the-output-file --fail-on low --add-cpes-if-none --vex test.vex asdf`,
);
});
});

0 comments on commit 7f269ee

Please sign in to comment.