Repository navigation
Expand file tree
/
Copy pathscan
More file actions
executable file
·125 lines (114 loc) · 4.31 KB
/
Copy pathscan
File metadata and controls
executable file
·125 lines (114 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env bash
# Runs the scanners of docs/security/METHOD.md over a clean export of origin/main of each checkout.
# Usage: scripts/security/scan <run-dir> <checkout>...
# scripts/security/scan ~/Projects/AI/cli-private/security/runs/2026-11-01 ~/Projects/AI/max-cli ~/Projects/AI/tg-cli
# Writes <run-dir>/SCOPE.md and <run-dir>/raw/<repo>/{osv,semgrep,zizmor,codeql}.json.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
run="$1"; shift
[ "$#" -gt 0 ] || { echo "usage: scan <run-dir> <checkout>..." >&2; exit 2; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$run/raw"
scope="$run/SCOPE.md"
slug() { git -C "$1" remote get-url origin | sed -E 's#^(git@github\.com:|https://github\.com/)##; s#\.git$##'; }
{
echo "# Run $(basename "$run") — scope"
echo
echo "Exported from \`origin/main\` of each repository."
echo
echo "## Commits"
echo
} > "$scope"
names=()
for checkout in "$@"; do
name="$(basename "$checkout")"
names+=("$name")
git -C "$checkout" fetch -q origin main
echo "- $(slug "$checkout") \`$(git -C "$checkout" rev-parse origin/main)\`" >> "$scope"
mkdir -p "$work/$name"
git -C "$checkout" archive origin/main | tar -x -C "$work/$name"
done
{
echo
echo "## Tools"
echo
echo "- osv-scanner $(osv-scanner --version | head -1 | awk '{print $3}')"
echo "- semgrep $(semgrep --version)"
echo "- $(zizmor --version)"
echo "- esbuild 0.25.12 (runtime parse fallback only)"
echo
echo "## GitHub settings"
echo
for checkout in "$@"; do
s="$(slug "$checkout")"
a="$(gh api "repos/$s" --jq '.security_and_analysis | "secret_scanning=\(.secret_scanning.status) push_protection=\(.secret_scanning_push_protection.status) security_updates=\(.dependabot_security_updates.status)"')" || a="unreadable"
c="$(gh api "repos/$s/code-scanning/default-setup" --jq .state)" || c="unreadable"
echo "- $s: $a codeql=$c"
done
} >> "$scope"
# A tool exit code its docs do not list as a result marks that repository failed, so a crash never
# reads as "no findings": osv-scanner 0/1, semgrep 0 (no --error), zizmor 0 or 11-14. The other
# repositories are still scanned, and the run exits 1 at the end.
status=()
failed=()
run_tool() {
local tool="$1" ok="$2" file="$3"; shift 3
local code=0
"$@" > "$file" || code=$?
status+=("$name $tool exit=$code")
if [[ " $ok " != *" $code "* ]]; then
echo "$name: $tool failed with exit $code" >&2
failed+=("$name $tool")
return 1
fi
}
# A checkout runs the source; the published package has only the compiled file, because Node does
# not strip types under node_modules.
semgrep_runner=(node "$here/../../dist/semgrep.js")
if [ -f "$here/../../src/semgrep.ts" ]; then
semgrep_runner=(node --experimental-strip-types "$here/../../src/semgrep.ts")
fi
i=0
for checkout in "$@"; do
name="${names[$i]}"; i=$((i + 1))
out="$run/raw/$name"
mkdir -p "$out"
cd "$work/$name"
if [ -f pnpm-lock.yaml ]; then
run_tool osv-scanner "0 1" "$out/osv.json" osv-scanner scan source -r --format json . || true
fi
if [ -f .semgrepignore ]; then
status+=("$name semgrep ignores $(sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' .semgrepignore | paste -sd ' ' -)")
fi
if run_tool semgrep "0" /dev/null "${semgrep_runner[@]}" "$work/$name" "$out/semgrep.json" "$run/supplement/$name"; then
while IFS= read -r gap; do
status+=("$name semgrep gap $gap")
done < <(jq -r '.coverageGaps // [] | .[] | "\(.rule) \(.path)"' "$out/semgrep.json")
fi
if [ -d .github ]; then
run_tool zizmor "0 11 12 13 14" "$out/zizmor.json" zizmor --offline --format json .github || true
fi
if gh api --paginate --slurp "repos/$(slug "$checkout")/code-scanning/alerts?state=open&per_page=100" > "$out/codeql.json"; then
alerts="$(jq 'flatten | length' "$out/codeql.json")" || alerts="unreadable"
status+=("$name codeql open_alerts=$alerts")
else
echo "$name: reading CodeQL alerts failed" >&2
rm -f "$out/codeql.json"
failed+=("$name codeql")
fi
echo "$name done" >&2
done
# Temp paths differ on every run; strip them so two runs diff cleanly.
find "$run/raw" -name '*.json' -exec sed -i "s|$work/[^/\"]*/||g" {} +
{
echo
echo "## Tool exit codes"
echo
for s in "${status[@]}"; do echo "- $s"; done
} >> "$scope"
if [ "${#failed[@]}" -gt 0 ]; then
printf 'failed: %s\n' "${failed[@]}" >&2
exit 1
fi
echo "$run" >&2