|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | set -eo pipefail |
3 | 3 |
|
4 | | -# Check correct usage |
5 | | -if [[ ! $2 ]]; then |
6 | | - echo "Usage: scan-images.sh <os-distribution> <image-tag>" |
| 4 | +# Disable telemetry and version check: |
| 5 | +# https://github.com/aquasecurity/trivy/discussions/8945 |
| 6 | +export TRIVY_DISABLE_TELEMETRY=true |
| 7 | +export TRIVY_SKIP_VERSION_CHECK=true |
| 8 | + |
| 9 | +# Global variables |
| 10 | +scan_common_args=" \ |
| 11 | + --exit-code 1 \ |
| 12 | + --scanners vuln \ |
| 13 | + --format json \ |
| 14 | + --severity HIGH,CRITICAL \ |
| 15 | + --ignore-unfixed \ |
| 16 | + --db-repository ghcr.io/aquasecurity/trivy-db:2 \ |
| 17 | + --db-repository public.ecr.aws/aquasecurity/trivy-db \ |
| 18 | + --java-db-repository ghcr.io/aquasecurity/trivy-java-db:1 \ |
| 19 | + --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db " |
| 20 | + |
| 21 | +# Print usage instructions and error with wrong inputs |
| 22 | +usage() { |
| 23 | + echo "Usage: scan-images.sh <os-distribution> <image-tag> [--sbom]" |
7 | 24 | exit 2 |
8 | | -fi |
9 | | - |
10 | | -set -u |
11 | | - |
12 | | -# Check that trivy is installed |
13 | | -if ! trivy --version; then |
14 | | - echo 'Please install trivy: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.68.2' |
15 | | -fi |
16 | | - |
17 | | -# Clear any previous outputs |
18 | | -rm -rf image-scan-output |
19 | | - |
20 | | -# Make a fresh output directory |
21 | | -mkdir -p image-scan-output |
22 | | - |
23 | | -# Get built container images |
24 | | -images=$(docker image ls \ |
25 | | - --filter "reference=ark.stackhpc.com/stackhpc-dev/*:$2*" \ |
26 | | - --format "{{.Repository}}:{{.Tag}}") |
27 | | - |
28 | | -# Save list of images to file |
29 | | -echo "$images" > "$1-scanned-container-images.txt" |
30 | | - |
31 | | -# Ensure output files exist |
32 | | -touch image-scan-output/clean-images.txt image-scan-output/dirty-images.txt image-scan-output/critical-images.txt |
33 | | - |
34 | | -# If Trivy detects no vulnerabilities, add the image name to clean-images.txt. |
35 | | -# If there are vulnerabilities detected, add it to dirty-images.txt and |
36 | | -# generate a csv summary |
37 | | -# If the image contains at least one critical vulnerabilities, add it to |
38 | | -# critical-images.txt |
39 | | -for image in $images; do |
40 | | - filename=$(basename $image | sed 's/:/\./g') |
41 | | - imagename=$(echo $filename | cut -d "." -f 1 | sed 's/-/_/g') |
42 | | - global_vulnerabilities=$(yq .global_allowed_vulnerabilities[] src/kayobe-config/etc/kayobe/trivy/allowed-vulnerabilities.yml) |
43 | | - image_vulnerabilities=$(yq .$imagename'_allowed_vulnerabilities[]' src/kayobe-config/etc/kayobe/trivy/allowed-vulnerabilities.yml) |
| 25 | +} |
| 26 | + |
| 27 | +# Check dependencies are installed, print installation instructions otherwise |
| 28 | +check_deps_installed() { |
| 29 | + if ! trivy --version > /dev/null; then |
| 30 | + echo 'Please install trivy: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin v0.68.2' |
| 31 | + exit 1 |
| 32 | + fi |
| 33 | + if ! yq --version > /dev/null; then |
| 34 | + echo 'Please install yq: sudo dnf/apt install yq' |
| 35 | + exit 1 |
| 36 | + fi |
| 37 | +} |
| 38 | + |
| 39 | +# Prepare output files |
| 40 | +file_prep() { |
| 41 | + rm -rf image-scan-output |
| 42 | + mkdir -p image-scan-output |
| 43 | + touch image-scan-output/clean-images.txt image-scan-output/high-images.txt image-scan-output/critical-images.txt |
| 44 | +} |
| 45 | + |
| 46 | +# Gather image lists |
| 47 | +get_images() { |
| 48 | + local output_file="$1-scanned-container-images.txt" |
| 49 | + |
| 50 | + docker image ls \ |
| 51 | + --filter "reference=ark.stackhpc.com/stackhpc-dev/*:$2*" \ |
| 52 | + --format "{{.Repository}}:{{.Tag}}" \ |
| 53 | + > "$output_file" |
| 54 | + |
| 55 | + cat "$output_file" |
| 56 | +} |
| 57 | + |
| 58 | +# Generate ignored vulnerabilities file |
| 59 | +generate_trivy_ignore() { |
| 60 | + local imagename=$1 |
| 61 | + local global_vulnerabilities |
| 62 | + global_vulnerabilities=$(yq .global_allowed_vulnerabilities[] src/kayobe-config/etc/kayobe/trivy/allowed-vulnerabilities.yml 2> /dev/null) |
| 63 | + local image_vulnerabilities |
| 64 | + image_vulnerabilities=$(yq ."$imagename"'_allowed_vulnerabilities[]' src/kayobe-config/etc/kayobe/trivy/allowed-vulnerabilities.yml 2> /dev/null) |
| 65 | + |
44 | 66 | touch .trivyignore |
45 | 67 | for vulnerability in $global_vulnerabilities; do |
46 | | - echo $vulnerability >> .trivyignore |
| 68 | + echo "$vulnerability" >> .trivyignore |
47 | 69 | done |
48 | 70 | for vulnerability in $image_vulnerabilities; do |
49 | | - echo $vulnerability >> .trivyignore |
| 71 | + echo "$vulnerability" >> .trivyignore |
50 | 72 | done |
51 | | - if $(trivy image \ |
52 | | - --quiet \ |
53 | | - --exit-code 1 \ |
54 | | - --scanners vuln \ |
55 | | - --format json \ |
56 | | - --severity HIGH,CRITICAL \ |
57 | | - --output image-scan-output/${filename}.json \ |
58 | | - --ignore-unfixed \ |
59 | | - --db-repository ghcr.io/aquasecurity/trivy-db:2 \ |
60 | | - --db-repository public.ecr.aws/aquasecurity/trivy-db \ |
61 | | - --java-db-repository ghcr.io/aquasecurity/trivy-java-db:1 \ |
62 | | - --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db \ |
63 | | - $image); then |
64 | | - # Clean up the output file for any images with no vulnerabilities |
65 | | - rm -f image-scan-output/${filename}.json |
66 | | - |
67 | | - # Add the image to the clean list |
| 73 | +} |
| 74 | + |
| 75 | +# Put results into CSV |
| 76 | +generate_summary_csv() { |
| 77 | + local scan="$1" |
| 78 | + local summary="$2" |
| 79 | + |
| 80 | + echo '"PkgName","PkgPath","PkgID","VulnerabilityID","FixedVersion","PrimaryURL","Severity"' > "$summary" |
| 81 | + |
| 82 | + jq -r '.Results[] |
| 83 | + | select(.Vulnerabilities) |
| 84 | + | .Vulnerabilities |
| 85 | + | map(select(.PkgName | test("kernel") | not )) |
| 86 | + | group_by(.VulnerabilityID) |
| 87 | + | map( |
| 88 | + [ |
| 89 | + (map(.PkgName) | unique | join(";")), |
| 90 | + (map(.PkgPath | select( . != null )) | join(";")), |
| 91 | + .[0].PkgID, |
| 92 | + .[0].VulnerabilityID, |
| 93 | + .[0].FixedVersion, |
| 94 | + .[0].PrimaryURL, |
| 95 | + .[0].Severity |
| 96 | + ] |
| 97 | + ) |
| 98 | + | .[] |
| 99 | + | @csv' "$scan" >> "$summary" |
| 100 | +} |
| 101 | + |
| 102 | +# Categorise images based on severity |
| 103 | +categorise_image() { |
| 104 | + local summary="$1" |
| 105 | + local image="$2" |
| 106 | + |
| 107 | + if [ "$(grep "CRITICAL" "$summary" -c)" -gt 0 ]; then |
| 108 | + echo "${image}" >> image-scan-output/critical-images.txt |
| 109 | + else |
| 110 | + echo "${image}" >> image-scan-output/high-images.txt |
| 111 | + fi |
| 112 | +} |
| 113 | + |
| 114 | +# Generate SBOM, return correct scan command for SBOM |
| 115 | +generate_sbom() { |
| 116 | + local sbom="$1" |
| 117 | + local scan="$2" |
| 118 | + local image="$3" |
| 119 | + trivy image \ |
| 120 | + --debug \ |
| 121 | + --format spdx-json \ |
| 122 | + --output "$sbom" \ |
| 123 | + "$image" &> "$sbom.log" |
| 124 | + if [ ! -e "$sbom" ]; then |
| 125 | + ( |
| 126 | + echo "ERROR: trivy image didn't produce the sbom file $sbom for $image" 1>&2 |
| 127 | + echo "==== trivy log ====" |
| 128 | + cat "$sbom.log" |
| 129 | + ) 1>&2 |
| 130 | + exit 1 |
| 131 | + elif grep -q FATAL "$sbom.log"; then |
| 132 | + ( |
| 133 | + echo "ERROR: trivy image encountered a fatal error producing $sbom for $image" |
| 134 | + echo "==== trivy log ====" |
| 135 | + cat "$sbom.log" |
| 136 | + echo "==== sbom.json ====" |
| 137 | + cat "$sbom" |
| 138 | + ) 1>&2 |
| 139 | + exit 1 |
| 140 | + else |
| 141 | + echo "trivy sbom $scan_common_args --output $scan $sbom" |
| 142 | + fi |
| 143 | +} |
| 144 | + |
| 145 | +# Scan images, generate SBOMs if requested |
| 146 | +scan_image() { |
| 147 | + local image=$1 |
| 148 | + local filename |
| 149 | + filename=$(basename "$image" | sed 's/:/\./g') |
| 150 | + local imagename |
| 151 | + imagename=$(echo "$filename" | cut -d "." -f 1 | sed 's/-/_/g') |
| 152 | + local sbom="image-scan-output/${imagename}/${filename}-sbom.json" |
| 153 | + local scan="image-scan-output/${imagename}/${filename}-scan.json" |
| 154 | + local summary="image-scan-output/${imagename}/${filename}-summary.csv" |
| 155 | + |
| 156 | + mkdir -p "image-scan-output/$imagename" |
| 157 | + generate_trivy_ignore "$imagename" |
| 158 | + |
| 159 | + # If SBOM is required, generate it first and scan the results, otherwise we |
| 160 | + # scan the image directly. |
| 161 | + if $generate_sbom; then |
| 162 | + echo "Generating SBOM for $imagename" |
| 163 | + scan_command="$(generate_sbom "$sbom" "$scan" "$image")" |
| 164 | + else |
| 165 | + scan_command="trivy image $scan_common_args --output $scan $image" |
| 166 | + fi |
| 167 | + |
| 168 | + # Run scan against image or SBOM, format output. If no results, delete files. |
| 169 | + echo "Scanning $imagename for vulnerabilities" |
| 170 | + if $scan_command >& "$scan.log"; then |
| 171 | + rm -f "$scan" |
68 | 172 | echo "${image}" >> image-scan-output/clean-images.txt |
| 173 | + elif [ ! -f "$scan" ]; then |
| 174 | + ( |
| 175 | + echo "ERROR: trivy scan encountered an error producing $scan" |
| 176 | + echo "Command: $scan_command" |
| 177 | + echo "==== trivy log ====" |
| 178 | + cat "$scan.log" |
| 179 | + if $generate_sbom; then |
| 180 | + echo "==== sbom.json ====" |
| 181 | + cat "$sbom" |
| 182 | + fi |
| 183 | + ) 1>&2 |
| 184 | + exit 1 |
69 | 185 | else |
| 186 | + generate_summary_csv "$scan" "$summary" |
| 187 | + categorise_image "$summary" "$image" |
| 188 | + fi |
| 189 | +} |
| 190 | + |
| 191 | +# Main function |
| 192 | +main() { |
| 193 | + if [[ ! $2 ]]; then |
| 194 | + usage |
| 195 | + fi |
70 | 196 |
|
71 | | - # Write a header for the summary CSV |
72 | | - echo '"PkgName","PkgPath","PkgID","VulnerabilityID","FixedVersion","PrimaryURL","Severity"' > image-scan-output/${filename}.summary.csv |
73 | | - |
74 | | - # Write the summary CSV data |
75 | | - jq -r '.Results[] |
76 | | - | select(.Vulnerabilities) |
77 | | - | .Vulnerabilities |
78 | | - # Ignore packages with "kernel" in the PkgName |
79 | | - | map(select(.PkgName | test("kernel") | not )) |
80 | | - | group_by(.VulnerabilityID) |
81 | | - | map( |
82 | | - [ |
83 | | - (map(.PkgName) | unique | join(";")), |
84 | | - (map(.PkgPath | select( . != null )) | join(";")), |
85 | | - .[0].PkgID, |
86 | | - .[0].VulnerabilityID, |
87 | | - .[0].FixedVersion, |
88 | | - .[0].PrimaryURL, |
89 | | - .[0].Severity |
90 | | - ] |
91 | | - ) |
92 | | - | .[] |
93 | | - | @csv' image-scan-output/${filename}.json >> image-scan-output/${filename}.summary.csv |
94 | | - |
95 | | - if [ $(grep "CRITICAL" image-scan-output/${filename}.summary.csv -c) -gt 0 ]; then |
96 | | - # If the image contains critical vulnerabilities, add the image to critical list |
97 | | - echo "${image}" >> image-scan-output/critical-images.txt |
98 | | - else |
99 | | - # Otherwise, add the image to the dirty list |
100 | | - echo "${image}" >> image-scan-output/dirty-images.txt |
101 | | - fi |
| 197 | + generate_sbom=false |
| 198 | + if [[ "$3" == "--sbom" ]]; then |
| 199 | + generate_sbom=true |
102 | 200 | fi |
103 | | - rm .trivyignore |
104 | | -done |
| 201 | + |
| 202 | + set -u |
| 203 | + |
| 204 | + check_deps_installed |
| 205 | + file_prep |
| 206 | + |
| 207 | + images=$(get_images "$1" "$2") |
| 208 | + for image in $images; do |
| 209 | + scan_image "$image" |
| 210 | + done |
| 211 | +} |
| 212 | + |
| 213 | +main "$@" |
0 commit comments