Skip to content

Commit

Permalink
Grype SBOM scan capability (#193)
Browse files Browse the repository at this point in the history
* push syft multi format

* test for loop

* test multi format syft

* check for report output

* fix increment operator

* fix sbom_format data type

* remove sbom_format default

* test sbom_format.size()

* ts for loop

* testing loop

* fix equal operator ==

* testing loop

* check args

* ts list

* ts ARGS

* remove size()

* loop troubleshoot

* Test using ArrayList

* comment out loops

* print loop index

* test with string concatenation

* rm println

* test toString

* test arraylist

* test

* test

* test ARG building

* testing

* test w/o tostring

* test

* move sbom_format to LinkedHashMap for extensions

* check sbom_format

* test

* fix lib conf

* revert to ArrayList for sbom_format var

* fixed lib config

* rm ! from index identifier

* fix format

* test

* ts stdout

* comment out erring cmd

* missed end "

* rm extra text

* test --output

* check ARGS

* test

* add space for multi outputs

* add formatter var

* ts formatter

* add space

* cmd formatting

* archival ts

* test archival

* trim trailing comma

* escape the $

* test if statements

* fixes

* add exception handling

* test exception

* skip archival if failed

* echo exception

* test

* test err

* test echo err

* test error

* exception test

* test throw err

* test error

* test

* test error

* add shouldFail bool

* Syft Unit test changes

* push syft Unit tests

* update syft docs

* make PR suggested changes

* troubleshoot json report archival

* make artifacts empty string.

* test regex

* escape $ sign

* test regex

* test regex in line

* regex in-line test

* test syft var from grype

* fix scan sbom var

* fix sbom scan var

* test

* test sbom_scan var

* wont work as expected test

* test regex

* fixed sbom var

* test filetype

* test file match

* rm String baseDir

* test findFiles

* fix sbom var

* test filePath

* testing findFiles

* move findFiles to after unstash

* exlude spdx json

* test excludes

* testing excludes in pipeline

* find json

* testing findFiles

* find files exclude

* test size

* test with img props

* replace raw_results_file var with asterisk

* test replaceALll

* test

* test img output

* test reportBase

* fixed reportBase var

* test

* move syftSbom def

* Add message for SBOM scanning

* test

* grype unit tests for SBOM scanning

* push grype unit test troubleshooting

* testing each method on findFiles

* test findFiles closure

* maps maps maps

* test println

* testing findFiles

* testing

* test findfiles

* ts

* testing

* Push unit test troubleshooting

* ts unit test

* working unit tests

* test results script

* test line 39, remove Unknown vuln

* change >> to > to stop appending

* put quotes back around 0 on ln39

* test

* updated unit tests

* format and trailing whitespace

* fence ln 43

* remove ``` from ln 43

* fix readme

* readme finessing
  • Loading branch information
mackeyaj authored Nov 15, 2022
1 parent e61301d commit 902eb65
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 25 deletions.
28 changes: 20 additions & 8 deletions libraries/grype/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,32 @@ Uses the [Grype CLI](https://github.com/anchore/grype) to scan container images

## Configuration

| Library Configuration | Description | Type | Default Value | Options |
|-----------------------|----------------------------------------------------------|--------|---------------|---------------------------------------------------|
| `grype_container` | The container image to execute the scan within | String | grype:0.38.0 | |
| `report_format` | The output format of the generated report | String | json | `json`, `table`, `cyclonedx`, `template` |
| `fail_on_severity` | The severity level threshold that will fail the pipeline | String | high | `none`, `negligible`, `low`, `medium`, `high`, `critical` |
| `grype_config` | A custom path to a grype configuration file | String | `null` | |
| Library Configuration | Description | Type | Default Value | Options |
|-----------------------|----------------------------------------------------------|---------|---------------|-----------------------------------------------------------|
| `grype_container` | The container image to execute the scan within | String | grype:0.38.0 | |
| `report_format` | The output format of the generated report | String | json | `json`, `table`, `cyclonedx`, `template` |
| `fail_on_severity` | The severity level threshold that will fail the pipeline | String | high | `none`, `negligible`, `low`, `medium`, `high`, `critical` |
| `grype_config` | A custom path to a grype configuration file | String | `null` | |
| `scan_sbom` | Boolean to turn on SBOM scanning | Boolean | false | true, false |

``` groovy title='pipeline_config.groovy'
libraries {
grype {
grype_container = "grype:0.38.0"
report_format = "json"
fail_on_severity = "high"
grype_config = "Path/to/Grype.yaml"
scan_sbom = false
}
}
```

## Grype Configuration File

If `grype_config` isn't provided, the default locations for an application are `.grype.yaml`, `.grype/config.yaml`.

!!! note "Learn More About Grype Configuration"

Read [the grype docs](https://github.com/anchore/grype#configuration) to learn more about the Grype configuration file
Read [the grype docs](https://github.com/anchore/grype#configuration) to learn more about the Grype configuration file

## Dependencies

Expand Down
1 change: 1 addition & 0 deletions libraries/grype/library_config.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ fields{
report_format = ["json", "table", "cyclonedx", "template"]
fail_on_severity = ["none", "negligible", "low", "medium", "high", "critical"]
grype_config = String
scan_sbom = Boolean
}
}
21 changes: 20 additions & 1 deletion libraries/grype/steps/container_image_scan.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ void call() {
String outputFormat = config?.report_format ?: 'json'
String severityThreshold = config?.fail_on_severity ?: 'high'
String grypeConfig = config?.grype_config
Boolean scanSbom = config?.scan_sbom ?: false
ArrayList syftSbom = []
String resultsFileFormat = ".txt"
String ARGS = ""
// is flipped to True if an image scan fails
Expand Down Expand Up @@ -66,6 +68,16 @@ void call() {

def images = get_images_to_build()
images.each { img ->
if (scanSbom) {
String reportBase = "${img.repo}-${img.tag}".replaceAll("/","-")
syftSbom = findFiles(glob: "${reportBase}-*-json.json", excludes: "${reportBase}-*-*dx-json.json")
if (syftSbom.size() == 0) {
syftSbom = findFiles(glob: "${reportBase}-*-cyclonedx*")
if (syftSbom.size() == 0) {
syftSbom = findFiles(glob: "${reportBase}-*-spdx*")
}
}
}
// Use $img.repo to help name our results uniquely. Checks to see if a forward slash exists and splits the string at that location.
String rawResultsFile, transformedResultsFile
if (img.repo.contains("/")) {
Expand All @@ -80,7 +92,14 @@ void call() {

// perform the grype scan
try {
sh "grype ${img.registry}/${img.repo}:${img.tag} ${ARGS} >> ${rawResultsFile}"
if (scanSbom && syftSbom) {
echo "Scanning provided SBOM artifact"
sh "grype sbom:${syftSbom[0]} ${ARGS} > ${rawResultsFile}"
}
else {
echo "An SBOM artifact was not provided. Scanning registry image."
sh "grype ${img.registry}/${img.repo}:${img.tag} ${ARGS} > ${rawResultsFile}"
}
}
// Catch the error on quality gate failure
catch(Exception err) {
Expand Down
82 changes: 66 additions & 16 deletions libraries/grype/test/ContainerImageScanSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
explicitlyMockPipelineStep("get_images_to_build")
getPipelineMock("sh")([script: 'echo $HOME', returnStdout: true]) >> "/home"
getPipelineMock("sh")([script: 'echo $XDG_CONFIG_HOME', returnStdout: true]) >> "/xdg"

getPipelineMock("get_images_to_build")() >> {
def images = []
images << [registry: "test_registry", repo: "image1_repo", context: "image1", tag: "4321dcba"]
Expand Down Expand Up @@ -61,7 +60,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
ContainerImageScan()
then:
1 * getPipelineMock("echo")("Grype file explicitly specified in pipeline_config.groovy")
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/testPath\/grype.yaml >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/testPath\/grype.yaml > .*/})
}

def "Grype config is found at current dir .grype.yaml" () {
Expand All @@ -76,7 +75,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
1 * getPipelineMock("fileExists")(".grype.yaml") >> true
1 * getPipelineMock("echo")("Found .grype.yaml")
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config .grype.yaml >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config .grype.yaml > .*/})
}

def "Grype config is found at .grype/config.yaml" () {
Expand All @@ -91,7 +90,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
1 * getPipelineMock("fileExists")(".grype/config.yaml") >> true
1 * getPipelineMock("echo")("Found .grype/config.yaml")
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config .grype\/config.yaml >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config .grype\/config.yaml > .*/})
}

def "Grype config is found at user Home path/.grype.yaml" () {
Expand All @@ -106,7 +105,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
1 * getPipelineMock("fileExists")("/home/.grype.yaml") >> true
1 * getPipelineMock("echo")("Found ~/.grype.yaml")
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/home\/.grype.yaml >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/home\/.grype.yaml > .*/})
}

def "Grype config found at <XDG_CONFIG_HOME>/grype/config.yaml" () {
Expand All @@ -121,16 +120,16 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
1 * getPipelineMock("fileExists")("/xdg/grype/config.yaml") >> true
1 * getPipelineMock("echo")("Found <XDG_CONFIG_HOME>/grype/config.yaml")
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/xdg\/grype\/config.yaml >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* --config \/xdg\/grype\/config.yaml > .*/})
}

def "Check each image is scanned as expected when no extra config is present" () {
when:
ContainerImageScan()
then:
1 * getPipelineMock("sh")("grype test_registry/image1_repo:4321dcba -o json --fail-on high >> image1_repo-grype-scan-results.json")
1 * getPipelineMock("sh")("grype test_registry/image2_repo:4321dcbb -o json --fail-on high >> image2_repo-grype-scan-results.json")
1 * getPipelineMock("sh")("grype test_registry/image3_repo/qwerty:4321dcbc -o json --fail-on high >> qwerty-grype-scan-results.json")
1 * getPipelineMock("sh")("grype test_registry/image1_repo:4321dcba -o json --fail-on high > image1_repo-grype-scan-results.json")
1 * getPipelineMock("sh")("grype test_registry/image2_repo:4321dcbb -o json --fail-on high > image2_repo-grype-scan-results.json")
1 * getPipelineMock("sh")("grype test_registry/image3_repo/qwerty:4321dcbc -o json --fail-on high > qwerty-grype-scan-results.json")
}

def "Test json format and negligible severity" () {
Expand All @@ -139,7 +138,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
when:
ContainerImageScan()
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o json --fail-on negligible >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o json --fail-on negligible > .*/})
}

def "Test table format and low severity" () {
Expand All @@ -148,7 +147,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
when:
ContainerImageScan()
then:
(1.._ ) * getPipelineMock("sh")({it =~ /^grype .* -o table --fail-on low >> .*/})
(1.._ ) * getPipelineMock("sh")({it =~ /^grype .* -o table --fail-on low > .*/})
}

def "Test cyclonedx format and medium severity" () {
Expand All @@ -157,7 +156,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
when:
ContainerImageScan()
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o cyclonedx --fail-on medium >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o cyclonedx --fail-on medium > .*/})
}

def "Test table format and high severity" () {
Expand All @@ -166,7 +165,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
when:
ContainerImageScan()
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o table --fail-on high >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o table --fail-on high > .*/})
}

def "Test cyclonedx format and critical severity" () {
Expand All @@ -175,7 +174,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
when:
ContainerImageScan()
then:
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o cyclonedx --fail-on critical >> .*/})
(1.._) * getPipelineMock("sh")({it =~ /^grype .* -o cyclonedx --fail-on critical > .*/})
}

def "Test Archive artifacts works as expected for json format and not null grype config" () {
Expand All @@ -197,7 +196,7 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
def "Test that error handling works as expected" () {
given:
explicitlyMockPipelineStep("Exception")//("Failed: java.lang.Exception: test")
getPipelineMock("sh")("grype test_registry/image1_repo:4321dcba -o json --fail-on high >> image1_repo-grype-scan-results.json") >> {throw new Exception("test")}
getPipelineMock("sh")("grype test_registry/image1_repo:4321dcba -o json --fail-on high > image1_repo-grype-scan-results.json") >> {throw new Exception("test")}
when:
ContainerImageScan()
then:
Expand All @@ -206,7 +205,58 @@ public class ContainerImageScanSpec extends JTEPipelineSpecification {
1 * getPipelineMock("stash")("workspace")
1 * getPipelineMock("error")(_)
}
}

def "Test scanning syft JSON SBOM artifact" () {
given:
ContainerImageScan.getBinding().setVariable("config", [scan_sbom: true])
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-json.json', excludes:'image1_repo-4321dcba-*-*dx-json.json']) >> ['image1_repo-4321dcba-test-json.json']
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-json.json', excludes:'image2_repo-4321dcbb-*-*dx-json.json']) >> ['image2_repo-4321dcbb-test-json.json']
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-json.json', excludes:'image3_repo-qwerty-4321dcbc-*-*dx-json.json']) >> ['image3_repo-qwerty-4321dcbc-json.json']
explicitlyMockPipelineVariable("syftSbom")

when:
ContainerImageScan()

then:
(1..3) * getPipelineMock("sh")({it =~ /^grype sbom:image.*/})
}

def "Test scanning syft Cyclonedx SBOM artifact" () {
given:
ContainerImageScan.getBinding().setVariable("config", [scan_sbom: true])
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-json.json', excludes:'image1_repo-4321dcba-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-json.json', excludes:'image2_repo-4321dcbb-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-json.json', excludes:'image3_repo-qwerty-4321dcbc-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-cyclonedx*']) >> ['image1_repo-4321dcba-test-cyclonedx-xml.xml']
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-cyclonedx*']) >> ['image2_repo-4321dcbb-test-cyclonedx-json.json']
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-cyclonedx*']) >> ['image3_repo-qwerty-4321dcbc-cyclonedx-json.json']
explicitlyMockPipelineVariable("syftSbom")

when:
ContainerImageScan()

then:
(1..3) * getPipelineMock("sh")({it =~ /^grype sbom:.*cyclonedx*/})
}

def "Test scanning syft SPDX SBOM artifact" () {
given:
ContainerImageScan.getBinding().setVariable("config", [scan_sbom: true])
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-json.json', excludes:'image1_repo-4321dcba-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-json.json', excludes:'image2_repo-4321dcbb-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-json.json', excludes:'image3_repo-qwerty-4321dcbc-*-*dx-json.json']) >> []
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-cyclonedx*']) >> []
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-cyclonedx*']) >> []
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-cyclonedx*']) >> []
getPipelineMock("findFiles")([glob:'image1_repo-4321dcba-*-spdx*']) >> ['image1_repo-4321dcba-test-spdx-json.json']
getPipelineMock("findFiles")([glob:'image2_repo-4321dcbb-*-spdx*']) >> ['image2_repo-4321dcbb-test-spdx-tag-value.txt']
getPipelineMock("findFiles")([glob:'image3_repo-qwerty-4321dcbc-*-spdx*']) >> ['image3_repo-qwerty-4321dcbc-spdx-json.json']
explicitlyMockPipelineVariable("syftSbom")

when:
ContainerImageScan()

then:
(1..3) * getPipelineMock("sh")({it =~ /^grype sbom:.*spdx*/})
}
}

0 comments on commit 902eb65

Please sign in to comment.