Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/scripts/trivy/trivy-scan-images.sh
2492 views
1
#!/bin/bash
2
# Copyright (c) 2025 Gitpod GmbH. All rights reserved.
3
# Licensed under the GNU Affero General Public License (AGPL).
4
# See License.AGPL.txt in the project root for license information.
5
6
set -euo pipefail
7
8
# Check if VERSION and FAIL_ON are provided
9
if [[ $# -lt 2 ]]; then
10
echo "Usage: $0 VERSION FAIL_ON [TRIVY_ARGS...]"
11
echo " VERSION: The version to scan (e.g., main-gha.32006)"
12
echo " FAIL_ON: Severity threshold to fail on (empty, HIGH, or CRITICAL)"
13
echo " TRIVY_ARGS: Additional arguments to pass to Trivy"
14
echo "Example: $0 main-gha.32006 HIGH"
15
exit 1
16
fi
17
18
INSTALLER_IMAGE_BASE_REPO="${INSTALLER_IMAGE_BASE_REPO:-eu.gcr.io/gitpod-dev-artifact}"
19
20
# Extract VERSION and FAIL_ON from arguments and remove them from args list
21
VERSION="$1"
22
FAIL_ON="$2"
23
shift 2
24
25
# Validate FAIL_ON value
26
if [[ -n "$FAIL_ON" ]] && [[ "$FAIL_ON" != "HIGH" ]] && [[ "$FAIL_ON" != "CRITICAL" ]]; then
27
echo "Error: FAIL_ON must be either empty, 'HIGH', or 'CRITICAL'"
28
exit 1
29
fi
30
31
32
if ! command -v jq &> /dev/null; then
33
echo "jq not found. Please install jq to continue."
34
exit 1
35
fi
36
37
# Set up working directory
38
SCAN_DIR=$(mktemp -d -t trivy-scan-XXXXXX)
39
echo "Working directory: $SCAN_DIR"
40
41
# Directory where this script is located
42
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43
INSTALLER_CONFIG_PATH="$SCRIPT_DIR/scan-installer-config.yaml"
44
TRIVYIGNORE_PATH="$SCRIPT_DIR/trivyignore.yaml"
45
46
# Ensure Trivy is installed
47
TRIVY_CMD="trivy"
48
if ! command -v "$TRIVY_CMD" &> /dev/null; then
49
echo "Trivy not found. Installing..."
50
mkdir -p "$SCAN_DIR/bin"
51
TRIVY_CMD="$SCAN_DIR/bin/trivy"
52
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b "$SCAN_DIR/bin"
53
fi
54
55
OCI_TOOL_CMD="oci-tool"
56
OCI_TOOL_VERSION="0.2.0"
57
if ! command -v "$OCI_TOOL_CMD" &> /dev/null; then
58
mkdir -p "$SCAN_DIR/bin"
59
OCI_TOOL_CMD="$SCAN_DIR/bin/oci-tool"
60
curl -fsSL https://github.com/csweichel/oci-tool/releases/download/v${OCI_TOOL_VERSION}/oci-tool_${OCI_TOOL_VERSION}_linux_amd64.tar.gz | tar xz -C "$(dirname "$OCI_TOOL_CMD")" && chmod +x "$OCI_TOOL_CMD"
61
fi
62
63
echo "=== Gathering list of all images for $VERSION"
64
65
# Extract installer binary from installer image
66
INSTALLER_IMAGE="$INSTALLER_IMAGE_BASE_REPO/build/installer:${VERSION}"
67
INSTALLER="$SCAN_DIR/installer"
68
"$OCI_TOOL_CMD" fetch file -o "$INSTALLER" --platform=linux-amd64 "${INSTALLER_IMAGE}" app/installer
69
echo ""
70
chmod +x "$INSTALLER"
71
72
# Run the installer docker image to get the list of images
73
"$INSTALLER" mirror list -c "$INSTALLER_CONFIG_PATH" > "$SCAN_DIR/mirror.json"
74
75
# Extract original image references
76
jq -r '.[].original' "$SCAN_DIR/mirror.json" > "$SCAN_DIR/images.txt"
77
78
# Remove empty lines
79
sed -i '/^\s*$/d' "$SCAN_DIR/images.txt"
80
81
# Filter out specific image patterns
82
echo "=== Filtered out images:"
83
TOTAL_BEFORE=$(wc -l < "$SCAN_DIR/images.txt")
84
85
# Apply all filters at once using extended regex
86
grep -v -E "/build/ide/|/gitpod/workspace-|/library/mysql|/library/redis|/cloudsql-docker/gce-proxy" "$SCAN_DIR/images.txt" > "$SCAN_DIR/filtered_images.txt"
87
88
TOTAL_AFTER=$(wc -l < "$SCAN_DIR/filtered_images.txt")
89
FILTERED=$((TOTAL_BEFORE - TOTAL_AFTER))
90
91
echo " Total filtered: $FILTERED"
92
93
# Use filtered list for scanning
94
mv "$SCAN_DIR/filtered_images.txt" "$SCAN_DIR/images.txt"
95
96
# Count total images
97
TOTAL_IMAGES=$(wc -l < "$SCAN_DIR/images.txt")
98
echo "=== Found $TOTAL_IMAGES images to scan"
99
100
# Create results directory
101
RESULT_FILE="$SCAN_DIR/result.jsonl"
102
103
# Scan all images with Trivy
104
COUNTER=0
105
FAILED=0
106
while IFS= read -r IMAGE_REF; do
107
((COUNTER=COUNTER+1))
108
109
echo "= Scanning $IMAGE_REF [$COUNTER / $TOTAL_IMAGES]"
110
111
# Run Trivy on the image
112
scan_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
113
set +e
114
trivy_output=$("$TRIVY_CMD" image "$IMAGE_REF" --ignorefile "$TRIVYIGNORE_PATH" --scanners vuln --format json "$@" | jq -c)
115
scan_status=$?
116
117
# Create a JSON object for the current scan
118
if [ $scan_status -eq 0 ]; then
119
# Check if trivy_output is valid JSON
120
if echo "$trivy_output" | jq empty > /dev/null 2>&1; then
121
# Direct approach - create the combined JSON object using jq directly
122
jq -c --arg image "$IMAGE_REF" --arg scan_time "$scan_time" \
123
'. + {image: $image, scan_time: $scan_time}' <<< "$trivy_output" | jq >> "$RESULT_FILE"
124
else
125
# If trivy output is not valid JSON, treat as error
126
echo "Warning: Trivy returned invalid JSON for $IMAGE_REF"
127
jq -n --arg image "$IMAGE_REF" \
128
--arg scan_time "$scan_time" \
129
--arg error "Invalid JSON output from Trivy" \
130
--arg details "$trivy_output" \
131
'{image: $image, scan_time: $scan_time, error: $error, error_details: $details}' | jq >> "$RESULT_FILE"
132
((FAILED=FAILED+1))
133
fi
134
135
else
136
# For error cases, create a simple JSON object
137
jq -n --arg image "$IMAGE_REF" \
138
--arg scan_time "$scan_time" \
139
--arg error "Trivy scan failed" \
140
--arg details "$trivy_output" \
141
'{image: $image, scan_time: $scan_time, error: $error, error_details: $details}' >> "$RESULT_FILE"
142
((FAILED=FAILED+1))
143
fi
144
set -e
145
146
echo ""
147
done < "$SCAN_DIR/images.txt"
148
149
# Generate summary report
150
echo "=== Scan Summary ==="
151
echo "Scan directory: $SCAN_DIR"
152
echo "Results file: $RESULT_FILE"
153
echo "Total ignored images: $FILTERED"
154
echo "Total scanned images: $TOTAL_IMAGES"
155
echo "Failed scans: $FAILED"
156
echo "Triviy binary: $TRIVY_CMD"
157
echo "Triviy version: $($TRIVY_CMD version)"
158
echo ""
159
160
# Count vulnerabilities by severity
161
echo "=== Vulnerability Summary ==="
162
CRITICAL="$(jq -r 'if .Results != null then [.Results[].Vulnerabilities // [] | .[] | select(.Severity == "CRITICAL")] | length else 0 end' "$RESULT_FILE" 2>/dev/null | awk '{sum+=$1} END {print sum}')"
163
HIGH="$(jq -r 'if .Results != null then [.Results[].Vulnerabilities // [] | .[] | select(.Severity == "HIGH")] | length else 0 end' "$RESULT_FILE" 2>/dev/null | awk '{sum+=$1} END {print sum}')"
164
echo "CRITICAL: $CRITICAL"
165
echo "HIGH: $HIGH"
166
echo ""
167
168
echo "=== Scan completed ==="
169
if [[ $FAILED -gt 0 ]]; then
170
echo "ERROR: $FAILED scans failed"
171
exit 1
172
fi
173
174
# Check if we should fail based on vulnerability counts
175
if [[ "$FAIL_ON" == "CRITICAL" ]] && [[ $CRITICAL -gt 0 ]]; then
176
echo "FAIL: Found $CRITICAL CRITICAL vulnerabilities, and FAIL_ON=CRITICAL was specified"
177
exit 1
178
elif [[ "$FAIL_ON" == "HIGH" ]] && [[ $((CRITICAL + HIGH)) -gt 0 ]]; then
179
echo "FAIL: Found $CRITICAL CRITICAL and $HIGH HIGH vulnerabilities, and FAIL_ON=HIGH was specified"
180
exit 1
181
fi
182
183
echo "0 $FAIL_ON or higher vulnerabilities found."
184
exit 0
185
186