-
-
Notifications
You must be signed in to change notification settings - Fork 0
156 lines (136 loc) · 6.62 KB
/
Copy pathscan-and-report.yml
File metadata and controls
156 lines (136 loc) · 6.62 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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# SPDX-License-Identifier: MPL-2.0
name: Scan and Report to VeriSimDB
on:
workflow_call:
inputs:
repo_path:
description: 'Path to scan (default: .)'
default: '.'
type: string
fail_on_severity:
description: >-
Fail the job when a non-suppressed finding at or above this severity
is present. One of: none | low | medium | high | critical.
Defaults to 'none', which preserves the historical behaviour exactly:
`panic-attack assail` has no non-zero exit path of its own (no
process::exit, no ExitCode, no --fail-on anywhere in src/main.rs), so
without this input a Security Scan reports success regardless of what
it finds. Existing callers are unaffected until they opt in.
default: 'none'
type: string
secrets:
VERISIMDB_PAT:
description: 'PAT with repo scope for cross-repo dispatch to verisimdb-data'
required: false
workflow_dispatch:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install Rust
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
- name: Install panic-attack
run: |
cargo install --git https://github.com/hyperpolymath/panic-attack --branch main
- name: Run scan
id: scan
run: |
panic-attack assail ${{ inputs.repo_path }} --output scan-result.json
echo "scan_complete=true" >> $GITHUB_OUTPUT
- name: Enforce severity gate
if: steps.scan.outputs.scan_complete == 'true' && inputs.fail_on_severity != 'none'
env:
FAIL_ON: ${{ inputs.fail_on_severity }}
run: |
set -euo pipefail
# Normalise and validate up front. An unrecognised value must abort,
# never silently degrade to "threshold 0" -- a typo'd threshold that
# quietly passes everything is precisely the failure this gate exists
# to remove.
case "$(printf '%s' "$FAIL_ON" | tr '[:upper:]' '[:lower:]')" in
low) THRESHOLD=1 ;;
medium) THRESHOLD=2 ;;
high) THRESHOLD=3 ;;
critical) THRESHOLD=4 ;;
*)
echo "::error::fail_on_severity must be one of none|low|medium|high|critical (got '${FAIL_ON}')"
exit 1
;;
esac
# Count only findings the scanner did NOT suppress. `suppressed` is
# set by the user-classification registry (audits/*.a2ml) and by
# test-context detection, and is omitted rather than set false, so
# test for `!= true`.
OFFENDING=$(jq --argjson t "$THRESHOLD" '
def rank: {"Low":1,"Medium":2,"High":3,"Critical":4}[.] // 0;
[ .weak_points[]?
| select(.suppressed != true)
| select((.severity | rank) >= $t) ]
' scan-result.json)
COUNT=$(printf '%s' "$OFFENDING" | jq 'length')
if [ "$COUNT" -gt 0 ]; then
echo "::error::${COUNT} unsuppressed finding(s) at or above '${FAIL_ON}'"
printf '%s' "$OFFENDING" \
| jq -r '.[] | " [\(.severity)] \(.category) \(.file // .location // "?")"'
echo
echo "To accept one of these, add a (file, category) entry with an audit"
echo "rationale to the repo's assail classification registry -- not to this"
echo "workflow. Suppression belongs next to the evidence, in review."
exit 1
fi
echo "OK: no unsuppressed findings at or above '${FAIL_ON}'"
- name: Send to verisimdb-data
if: steps.scan.outputs.scan_complete == 'true'
env:
DISPATCH_TOKEN: ${{ secrets.VERISIMDB_PAT }}
run: |
# VERISIMDB_PAT is declared optional. Cross-repo dispatch is
# impossible with the workflow's own GITHUB_TOKEN (it is scoped
# to the calling repo), so without the PAT we skip rather than
# fail every consumer's Security Scan.
if [ -z "${DISPATCH_TOKEN}" ]; then
echo "::notice::VERISIMDB_PAT not configured; scan passed, skipping cross-repo dispatch to verisimdb-data"
exit 0
fi
REPO_NAME=$(basename $(pwd))
# Build the payload in a file: inlining the scan JSON on argv
# overflows the kernel arg-length limit on large repos (curl
# exits 126 "Argument list too long").
jq -c --arg repo_name "$REPO_NAME" \
'{event_type: "scan_result", client_payload: {repo_name: $repo_name, scan_data: .}}' \
scan-result.json > dispatch-payload.json
# repository_dispatch caps client_payload at ~64KB. When the full
# report is too big, dispatch a counts-only summary instead of
# letting the API reject the whole run with a 422.
if [ "$(wc -c < dispatch-payload.json)" -gt 60000 ]; then
echo "Full report $(wc -c < scan-result.json) bytes exceeds dispatch cap; sending summary"
jq -c --arg repo_name "$REPO_NAME" \
'{event_type: "scan_result", client_payload: {repo_name: $repo_name, scan_data: {
schema_version: .schema_version,
program_path: .program_path,
language: .language,
statistics: .statistics,
suppressed_count: (.suppressed_count // 0),
weak_point_count: (.weak_points | length),
weak_points_by_severity: (.weak_points | map(.severity | tostring) | group_by(.) | map({key: .[0], value: length}) | from_entries),
truncated: true
}}}' scan-result.json > dispatch-payload.json
fi
# Capture the HTTP status so a rejected dispatch says *why*
# instead of a bare curl exit 22.
HTTP_STATUS=$(curl -s -o dispatch-response.json -w "%{http_code}" -X POST \
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/hyperpolymath/verisimdb-data/dispatches" \
--data @dispatch-payload.json)
if [ "${HTTP_STATUS}" -ge 300 ]; then
echo "::error::verisimdb-data dispatch failed with HTTP ${HTTP_STATUS}: $(cat dispatch-response.json)"
exit 1
fi
echo "Dispatched scan results for ${REPO_NAME} to verisimdb-data (HTTP ${HTTP_STATUS})"