Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add output-file option, default to random directory output in temp #346

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this interact with output? I had the impression we were moving towards output and way from file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We support --output <format> --file <output-file> in both Syft and Grype. This action doesn't allow multiple outputs, so it should work fine. I don't think there is a concrete plan to remove the --file option, since it's a part of Syft 1.0 already. If you feel strongly this should be using the --output flag, I can update it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take this back, allowing for multiple outputs would be nice, so a user could get both a table printed to the logs and a SARIF report uploaded. I'll update this a bit.


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`,
);
});
});
Loading