Skip to content

Commit 3141be0

Browse files
smurchingclaude
andcommitted
Add supply chain audit script and Trivy incident report
Adds scripts/supply-chain-audit.sh — a reusable script that checks for compromised packages/tools, known IOCs, and unpinned GitHub Actions (the root attack vector in the March 2026 Trivy supply chain incident). Also includes supply-chain-audit-report.md (full structured assessment) and supply-chain-audit-results.md (latest script output). Current status: no exposure to the Trivy incident; all Actions are tag/branch-pinned rather than SHA-pinned (74 findings requiring remediation). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5dc3abd commit 3141be0

3 files changed

Lines changed: 525 additions & 0 deletions

File tree

scripts/supply-chain-audit.sh

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env bash
2+
# supply-chain-audit.sh
3+
# Audits this repo for supply chain attack indicators.
4+
# Usage: ./scripts/supply-chain-audit.sh [--packages "pkg1 pkg2"] [--iocs "domain1 domain2"]
5+
#
6+
# Checks performed:
7+
# 1. Compromised packages/tools in current code and git history
8+
# 2. Known IOC domains/files
9+
# 3. GitHub Actions pinning (tags vs commit SHAs)
10+
11+
set -euo pipefail
12+
13+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14+
RESULTS_FILE="$REPO_ROOT/supply-chain-audit-results.md"
15+
16+
# ── Defaults (extend via flags) ────────────────────────────────────────────────
17+
SUSPECT_PACKAGES=(
18+
"trivy"
19+
"aquasecurity"
20+
"emilgroup"
21+
)
22+
IOCS=(
23+
"scan.aquasecurtiy.org" # typosquatted Trivy exfil domain
24+
"tpcp-docs" # Trivy exfil fallback repo
25+
"sysmon.py" # Trivy C2 loader path
26+
)
27+
28+
# ── Argument parsing ───────────────────────────────────────────────────────────
29+
while [[ $# -gt 0 ]]; do
30+
case "$1" in
31+
--packages)
32+
IFS=' ' read -ra EXTRA_PACKAGES <<< "$2"
33+
SUSPECT_PACKAGES+=("${EXTRA_PACKAGES[@]}")
34+
shift 2 ;;
35+
--iocs)
36+
IFS=' ' read -ra EXTRA_IOCS <<< "$2"
37+
IOCS+=("${EXTRA_IOCS[@]}")
38+
shift 2 ;;
39+
*) echo "Unknown flag: $1"; exit 1 ;;
40+
esac
41+
done
42+
43+
# ── Helpers ────────────────────────────────────────────────────────────────────
44+
PASS=""
45+
FAIL=""
46+
WARN="⚠️"
47+
48+
findings=0
49+
warnings=0
50+
51+
emit() { echo "$1" | tee -a "$RESULTS_FILE"; }
52+
section() { emit ""; emit "## $1"; emit ""; }
53+
result() {
54+
local icon="$1" label="$2" detail="$3"
55+
emit "$icon **$label** — $detail"
56+
[[ "$icon" == "$FAIL" ]] && (( findings++ )) || true
57+
[[ "$icon" == "$WARN" ]] && (( warnings++ )) || true
58+
}
59+
60+
# ── Start report ───────────────────────────────────────────────────────────────
61+
cd "$REPO_ROOT"
62+
: > "$RESULTS_FILE" # truncate
63+
64+
emit "# Supply Chain Audit Results"
65+
emit ""
66+
emit "**Date:** $(date -u '+%Y-%m-%d %H:%M UTC')"
67+
emit "**Repo:** $(git remote get-url origin 2>/dev/null || echo '(local)')"
68+
emit "**Branch:** $(git rev-parse --abbrev-ref HEAD)"
69+
emit "**Commit:** $(git rev-parse HEAD)"
70+
71+
# ══════════════════════════════════════════════════════════════════════════════
72+
section "1. Suspect Packages / Tools"
73+
emit "Searching current files and full git history for: \`${SUSPECT_PACKAGES[*]}\`"
74+
emit ""
75+
76+
for pkg in "${SUSPECT_PACKAGES[@]}"; do
77+
# Current files (case-insensitive, skip node_modules/.venv/.git and this script's own output files)
78+
current_hits=$(grep -ril "$pkg" \
79+
--exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv \
80+
--exclude="supply-chain-audit*.md" \
81+
--exclude="supply-chain-audit.sh" \
82+
"$REPO_ROOT" 2>/dev/null || true)
83+
84+
# Git history (commits predating this audit script)
85+
history_hits=$(git log --all --oneline -S "$pkg" \
86+
-- ':!scripts/supply-chain-audit.sh' ':!supply-chain-audit*.md' \
87+
2>/dev/null || true)
88+
89+
if [[ -z "$current_hits" && -z "$history_hits" ]]; then
90+
result "$PASS" "$pkg" "not found in current files or git history"
91+
else
92+
if [[ -n "$current_hits" ]]; then
93+
result "$FAIL" "$pkg" "found in current files:"
94+
while IFS= read -r f; do
95+
emit " - \`${f#$REPO_ROOT/}\`"
96+
done <<< "$current_hits"
97+
fi
98+
if [[ -n "$history_hits" ]]; then
99+
result "$FAIL" "$pkg" "found in git history:"
100+
while IFS= read -r line; do
101+
emit " - $line"
102+
done <<< "$history_hits"
103+
fi
104+
fi
105+
done
106+
107+
# ══════════════════════════════════════════════════════════════════════════════
108+
section "2. Indicators of Compromise (IOCs)"
109+
emit "Searching for known malicious domains, filenames, and artifacts."
110+
emit ""
111+
112+
for ioc in "${IOCS[@]}"; do
113+
hits=$(grep -ril "$ioc" \
114+
--exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv \
115+
--exclude="supply-chain-audit*.md" \
116+
--exclude="supply-chain-audit.sh" \
117+
"$REPO_ROOT" 2>/dev/null || true)
118+
git_hits=$(git log --all --oneline -S "$ioc" \
119+
-- ':!scripts/supply-chain-audit.sh' ':!supply-chain-audit*.md' \
120+
2>/dev/null || true)
121+
122+
if [[ -z "$hits" && -z "$git_hits" ]]; then
123+
result "$PASS" "$ioc" "not found"
124+
else
125+
result "$FAIL" "$ioc" "FOUND — potential indicator of compromise"
126+
[[ -n "$hits" ]] && while IFS= read -r f; do emit " - \`${f#$REPO_ROOT/}\`"; done <<< "$hits"
127+
[[ -n "$git_hits" ]] && while IFS= read -r line; do emit " - (history) $line"; done <<< "$git_hits"
128+
fi
129+
done
130+
131+
# ══════════════════════════════════════════════════════════════════════════════
132+
section "3. GitHub Actions Pinning"
133+
emit "Checking whether Actions are pinned to commit SHAs (safe) or mutable tags/branches (at risk)."
134+
emit ""
135+
136+
workflow_files=$(find "$REPO_ROOT/.github/workflows" -name "*.yml" -o -name "*.yaml" 2>/dev/null | sort)
137+
138+
if [[ -z "$workflow_files" ]]; then
139+
result "$PASS" "No workflow files found" "nothing to check"
140+
else
141+
tag_pinned=()
142+
sha_pinned=()
143+
branch_pinned=()
144+
145+
while IFS= read -r wf; do
146+
wf_name="${wf#$REPO_ROOT/}"
147+
# Extract all `uses:` lines
148+
while IFS= read -r uses_line; do
149+
action=$(echo "$uses_line" | sed 's/.*uses:[[:space:]]*//' | tr -d '"'"'" | xargs)
150+
[[ -z "$action" ]] && continue
151+
152+
# Classify by ref
153+
if echo "$action" | grep -qE '@[0-9a-f]{40}$'; then
154+
sha_pinned+=("$wf_name | $action")
155+
elif echo "$action" | grep -qE '@(v[0-9]|[0-9]+\.[0-9]+)'; then
156+
tag_pinned+=("$wf_name | $action")
157+
elif echo "$action" | grep -qE '@(master|main|release/)'; then
158+
branch_pinned+=("$wf_name | $action")
159+
elif echo "$action" | grep -qE '@'; then
160+
tag_pinned+=("$wf_name | $action") # unknown ref — treat as unsafe
161+
fi
162+
done < <(grep -E '^\s+uses:' "$wf")
163+
done <<< "$workflow_files"
164+
165+
if [[ ${#sha_pinned[@]} -gt 0 ]]; then
166+
emit "**SHA-pinned (safe):**"
167+
for entry in "${sha_pinned[@]}"; do result "$PASS" "${entry##* | }" "in \`${entry%% |*}\`"; done
168+
emit ""
169+
fi
170+
171+
if [[ ${#tag_pinned[@]} -gt 0 ]]; then
172+
emit "**Tag-pinned (at risk — tags are mutable):**"
173+
for entry in "${tag_pinned[@]}"; do result "$FAIL" "${entry##* | }" "in \`${entry%% |*}\`"; done
174+
emit ""
175+
fi
176+
177+
if [[ ${#branch_pinned[@]} -gt 0 ]]; then
178+
emit "**Branch-pinned (high risk — branch HEAD changes constantly):**"
179+
for entry in "${branch_pinned[@]}"; do result "$FAIL" "${entry##* | }" "in \`${entry%% |*}\`"; done
180+
emit ""
181+
fi
182+
fi
183+
184+
# ══════════════════════════════════════════════════════════════════════════════
185+
section "Summary"
186+
total_checks=$(( findings + warnings ))
187+
if [[ $findings -eq 0 && $warnings -eq 0 ]]; then
188+
emit "$PASS **No issues found.** Repository appears clean."
189+
else
190+
emit "$FAIL **$findings finding(s), $warnings warning(s) require attention.**"
191+
emit ""
192+
emit "Review the sections above and:"
193+
emit "- Investigate any package or IOC findings immediately"
194+
emit "- Pin all tag/branch-referenced Actions to full commit SHAs"
195+
fi
196+
197+
emit ""
198+
emit "---"
199+
emit "_Generated by \`scripts/supply-chain-audit.sh\`_"
200+
201+
echo ""
202+
echo "Report written to: $RESULTS_FILE"
203+
echo "Findings: $findings | Warnings: $warnings"
204+
[[ $findings -gt 0 ]] && exit 1 || exit 0

0 commit comments

Comments
 (0)