diff --git a/.gitignore b/.gitignore index a9ed078a..4e311d06 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,4 @@ typings/ /grype-db # Action temporary files -results.sarif -vulnerabilities.json -results.json +/results.* diff --git a/README.md b/README.md index ae30ff33..f76df412 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ The inputs `image`, `path`, and `sbom` are mutually exclusive to specify the sou | `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-format` | Set the output parameter after successful action execution. Valid choices are `json`, `sarif`, `cyclonedx-xml`, `cyclonedx-json`, and `table`; where `table` output will also display in the logs. | `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` | @@ -139,10 +139,12 @@ The inputs `image`, `path`, and `sbom` are mutually exclusive to specify the sou ### Action Outputs -| Output Name | Description | Type | -| ----------- | ------------------------------------------------------------ | ------ | -| `sarif` | Path to the SARIF report file, if `output-format` is `sarif` | string | -| `json` | Path to the report file , if `output-format` is `json` | string | +| Output Name | Description | Type | +|------------------|--------------------------------------------------------------------------------|--------| +| `sarif` | Path to the SARIF report file, if `output-format` is `sarif` | string | +| `json` | Path to the report file , if `output-format` is `json` | string | +| `cyclonedx-xml` | Path to the CycloneDX report file, if `output-format` is `cyclonedx` | string | +| `cyclonedx-json` | Path to the CycloneDX JSON report file, if `output-format` is `cyclonedx-json` | string | ### Example Workflows diff --git a/action.yml b/action.yml index 35db289f..06ce7758 100644 --- a/action.yml +++ b/action.yml @@ -18,7 +18,7 @@ inputs: required: false default: "true" output-format: - description: 'Set the output parameter after successful action execution. Valid choices are "json", "sarif", and "table".' + description: 'Set the output parameter after successful action execution. Valid choices are "json", "sarif", "cyclonedx", "cyclonedx-json" and "table".' required: false default: "sarif" output-file: @@ -51,9 +51,13 @@ inputs: required: false outputs: sarif: - description: "Path to a SARIF report file for the image" + description: "Path to a SARIF report file for the scan" json: - description: "Path to a JSON report file for the image" + description: "Path to a JSON report file for the scan" + cyclonedx-xml: + description: "Path to a CycloneDX XML report file for the scan" + cyclonedx-json: + description: "Path to a CycloneDX JSON report file for the scan" runs: using: "node20" main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js index 5c6bce0b..ea246aa1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -329,7 +329,13 @@ async function runScan({ } const SEVERITY_LIST = ["negligible", "low", "medium", "high", "critical"]; - const FORMAT_LIST = ["sarif", "json", "table"]; + const FORMAT_LIST = [ + "sarif", + "json", + "table", + "cyclonedx-xml", + "cyclonedx-json", + ]; let cmdArgs = []; if (core.isDebug()) { diff --git a/index.js b/index.js index 7864c97c..1f6d3400 100644 --- a/index.js +++ b/index.js @@ -315,7 +315,13 @@ async function runScan({ } const SEVERITY_LIST = ["negligible", "low", "medium", "high", "critical"]; - const FORMAT_LIST = ["sarif", "json", "table"]; + const FORMAT_LIST = [ + "sarif", + "json", + "table", + "cyclonedx-xml", + "cyclonedx-json", + ]; let cmdArgs = []; if (core.isDebug()) { diff --git a/tests/action.test.js b/tests/action.test.js index 79d038b5..2bfa97a5 100644 --- a/tests/action.test.js +++ b/tests/action.test.js @@ -90,6 +90,37 @@ describe("Github action", () => { expect(outputs["json"]).toBeFalsy(); }); + it("runs with cyclonedx-xml output", async () => { + const outputs = mockIO({ + image: "", + path: "tests/fixtures/npm-project", + "fail-build": "true", + "output-format": "cyclonedx-xml", + "output-file": "./results.cdx.xml", + "severity-cutoff": "medium", + "add-cpes-if-none": "true", + }); + + await run(); + + expect(outputs["cyclonedx-xml"]).toBe("./results.cdx.xml"); + }); + + it("runs with cyclonedx-json output", async () => { + const outputs = mockIO({ + image: "", + path: "tests/fixtures/npm-project", + "fail-build": "true", + "output-format": "cyclonedx-json", + "severity-cutoff": "medium", + "add-cpes-if-none": "true", + }); + + await run(); + + expect(outputs["cyclonedx-json"]).toBeDefined(); + }); + it("runs with environment variables", async () => { mockIO({ path: "tests/fixtures/npm-project", diff --git a/tests/grype_command.test.js b/tests/grype_command.test.js index 38328212..3da4b5a8 100644 --- a/tests/grype_command.test.js +++ b/tests/grype_command.test.js @@ -30,6 +30,52 @@ describe("Grype command args", () => { ]); }); + it("is invoked with cyclonedx output", async () => { + const args = await mockRun({ + source: "dir:.", + "fail-build": "false", + "output-file": "the-output-file", + "output-format": "cyclonedx-xml", + "severity-cutoff": "high", + version: "0.6.0", + "only-fixed": "false", + "add-cpes-if-none": "false", + "by-cve": "false", + }); + expect(args).toEqual([ + "-o", + "cyclonedx-xml", + "--file", + "the-output-file", + "--fail-on", + "high", + "dir:.", + ]); + }); + + it("is invoked with cyclonedx-json output", async () => { + const args = await mockRun({ + source: "dir:.", + "fail-build": "false", + "output-file": "the-output-file", + "output-format": "cyclonedx-json", + "severity-cutoff": "high", + version: "0.6.0", + "only-fixed": "false", + "add-cpes-if-none": "false", + "by-cve": "false", + }); + expect(args).toEqual([ + "-o", + "cyclonedx-json", + "--file", + "the-output-file", + "--fail-on", + "high", + "dir:.", + ]); + }); + it("is invoked with values", async () => { const args = await mockRun({ image: "asdf",