-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
271 lines (244 loc) · 10.2 KB
/
action.yml
File metadata and controls
271 lines (244 loc) · 10.2 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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
name: 'capsec audit'
description: 'Static capability audit for Rust crates — detect ambient authority (filesystem, network, env, process, FFI) in your code'
branding:
icon: 'shield'
color: 'orange'
inputs:
version:
description: 'Version of cargo-capsec to install (e.g. "0.1.0" or "latest")'
default: 'latest'
fail-on:
description: 'Minimum risk level to fail on: low, medium, high, critical'
default: 'high'
baseline:
description: 'Path to baseline file. Set to empty string to disable baseline diffing.'
default: '.capsec-baseline.json'
diff:
description: 'Only fail on new findings vs baseline (auto-enabled on pull_request events)'
default: 'auto'
format:
description: 'Output format: text, json, sarif'
default: 'sarif'
upload-sarif:
description: 'Upload SARIF to GitHub Code Scanning (requires security-events: write)'
default: 'true'
comment-on-pr:
description: 'Post findings as inline PR review comments via reviewdog'
default: 'true'
working-directory:
description: 'Path to Cargo workspace root'
default: '.'
token:
description: 'GitHub token for PR comments and SARIF upload'
default: ${{ github.token }}
reviewdog-version:
description: 'Version of reviewdog to install'
default: 'latest'
only:
description: 'Only scan these crates (comma-separated). Maps to --only flag.'
default: ''
skip:
description: 'Skip these crates (comma-separated). Maps to --skip flag.'
default: ''
sarif-category:
description: 'SARIF category for Code Scanning (must be unique if action is called multiple times in one job)'
default: 'capsec-audit'
install-from:
description: 'Install method: "crates-io" (cargo install) or "git" (from repository)'
default: 'crates-io'
git-repo:
description: 'Git repository URL when install-from is "git"'
default: 'https://github.com/auths-dev/capsec'
outputs:
sarif-file:
description: 'Path to generated SARIF file (if format is sarif)'
value: ${{ steps.audit.outputs.sarif-file }}
finding-count:
description: 'Total number of findings'
value: ${{ steps.audit.outputs.finding-count }}
exit-code:
description: 'Exit code from cargo capsec audit (0=pass, 1=findings exceed threshold, 2=error)'
value: ${{ steps.audit.outputs.exit-code }}
runs:
using: 'composite'
steps:
- name: Install Rust toolchain
shell: bash
run: |
if ! command -v cargo &>/dev/null; then
echo "::error::Rust toolchain not found. Add dtolnay/rust-toolchain or actions-rust-lang/setup-rust-toolchain before this action."
exit 1
fi
- name: Install cargo-capsec
shell: bash
env:
INPUT_INSTALL_FROM: ${{ inputs.install-from }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_GIT_REPO: ${{ inputs.git-repo }}
run: |
if command -v cargo-capsec &>/dev/null; then
echo "cargo-capsec already installed"
exit 0
fi
case "$INPUT_INSTALL_FROM" in
git)
echo "Installing cargo-capsec from git: $INPUT_GIT_REPO"
cargo install --git "$INPUT_GIT_REPO" cargo-capsec
;;
crates-io)
if [ "$INPUT_VERSION" = "latest" ]; then
echo "Installing latest cargo-capsec from crates.io"
cargo install cargo-capsec
else
echo "Installing cargo-capsec v$INPUT_VERSION from crates.io"
cargo install cargo-capsec --version "$INPUT_VERSION"
fi
;;
*)
echo "::error::Invalid install-from value: $INPUT_INSTALL_FROM. Must be 'crates-io' or 'git'."
exit 1
;;
esac
- name: Run capsec audit
id: audit
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
INPUT_FAIL_ON: ${{ inputs.fail-on }}
INPUT_ONLY: ${{ inputs.only }}
INPUT_SKIP: ${{ inputs.skip }}
INPUT_DIFF: ${{ inputs.diff }}
INPUT_BASELINE: ${{ inputs.baseline }}
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
run: |
set -uo pipefail
# Validate fail-on
case "$INPUT_FAIL_ON" in
low|medium|high|critical) ;;
*) echo "::error::Invalid fail-on value: $INPUT_FAIL_ON. Must be low, medium, high, or critical."; exit 1 ;;
esac
# Unique SARIF file per invocation to prevent overwrite on dual calls
SARIF_FILE="${RUNNER_TEMP:-/tmp}/capsec-results-$$.sarif"
STDERR_LOG="${RUNNER_TEMP:-/tmp}/capsec-stderr-$$.log"
# Build args as array for safe quoting
AUDIT_ARGS=(--format sarif --fail-on "$INPUT_FAIL_ON")
if [ -n "$INPUT_ONLY" ]; then
AUDIT_ARGS+=(--only "$INPUT_ONLY")
fi
if [ -n "$INPUT_SKIP" ]; then
AUDIT_ARGS+=(--skip "$INPUT_SKIP")
fi
# Determine if we should diff against baseline
USE_DIFF="false"
if [ "$INPUT_DIFF" = "true" ]; then
USE_DIFF="true"
elif [ "$INPUT_DIFF" = "auto" ] && [ "$EVENT_NAME" = "pull_request" ]; then
USE_DIFF="true"
fi
# Handle baseline
HAVE_BASELINE="false"
if [ "$USE_DIFF" = "true" ] && [ -n "$INPUT_BASELINE" ]; then
# Copy baseline to a temp location so we don't modify the working tree
EFFECTIVE_BASELINE="${RUNNER_TEMP:-/tmp}/capsec-effective-baseline-$$.json"
if [ -n "$BASE_REF" ] && git show "origin/${BASE_REF}:${INPUT_BASELINE}" > "$EFFECTIVE_BASELINE" 2>/dev/null; then
echo "Using baseline from base branch: $BASE_REF"
cp "$EFFECTIVE_BASELINE" "$INPUT_BASELINE"
AUDIT_ARGS+=(--diff)
HAVE_BASELINE="true"
elif [ -f "$INPUT_BASELINE" ]; then
echo "Using baseline from current branch (not found on base branch)"
AUDIT_ARGS+=(--diff)
HAVE_BASELINE="true"
else
echo "No baseline found — skipping diff, running informational audit"
echo "::notice::No capsec baseline found. First run will be informational only. Commit a .capsec-baseline.json to enable regression detection."
fi
fi
echo "Running: cargo capsec audit ${AUDIT_ARGS[*]}"
set +e
cargo capsec audit "${AUDIT_ARGS[@]}" > "$SARIF_FILE" 2>"$STDERR_LOG"
AUDIT_EXIT=$?
set -e
# Count findings from SARIF
if [ -f "$SARIF_FILE" ] && command -v jq &>/dev/null; then
FINDING_COUNT=$(jq '.runs[0].results | length' "$SARIF_FILE" 2>/dev/null || echo "unknown")
elif [ -f "$SARIF_FILE" ] && command -v python3 &>/dev/null; then
FINDING_COUNT=$(python3 -c "import json,sys; d=json.load(open('$SARIF_FILE')); print(len(d.get('runs',[{}])[0].get('results',[])))" 2>/dev/null || echo "unknown")
else
FINDING_COUNT="unknown"
fi
# Set all outputs once
FINAL_EXIT="$AUDIT_EXIT"
if [ "$AUDIT_EXIT" -eq 2 ]; then
echo "::group::capsec audit stderr"
cat "$STDERR_LOG" >&2
echo "::endgroup::"
echo "::error::cargo capsec audit failed with a runtime error (exit code 2)"
fi
# If diff was requested but no baseline exists, treat as informational (don't fail)
if [ "$USE_DIFF" = "true" ] && [ "$HAVE_BASELINE" = "false" ] && [ "$AUDIT_EXIT" -eq 1 ]; then
echo "::notice::Audit found $FINDING_COUNT findings but no baseline exists for diffing. Not failing — commit a baseline to enable regression detection."
FINAL_EXIT=0
fi
echo "sarif-file=$SARIF_FILE" >> "$GITHUB_OUTPUT"
echo "finding-count=$FINDING_COUNT" >> "$GITHUB_OUTPUT"
echo "exit-code=$FINAL_EXIT" >> "$GITHUB_OUTPUT"
echo "have-baseline=$HAVE_BASELINE" >> "$GITHUB_OUTPUT"
echo "Audit complete: $FINDING_COUNT findings (exit code $FINAL_EXIT)"
# Exit 2 (runtime error) fails this step; exit 1 (findings) is handled by Set check result
if [ "$AUDIT_EXIT" -eq 2 ]; then
exit 1
fi
- name: Upload SARIF to GitHub Code Scanning
if: inputs.upload-sarif == 'true' && steps.audit.outputs.sarif-file != '' && !cancelled()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.audit.outputs.sarif-file }}
category: ${{ inputs.sarif-category }}
continue-on-error: true
- name: Install reviewdog
if: inputs.comment-on-pr == 'true' && github.event_name == 'pull_request' && !cancelled()
shell: bash
run: |
if command -v reviewdog &>/dev/null; then
echo "reviewdog already installed"
exit 0
fi
curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b "${RUNNER_TEMP:-/tmp}/bin" "${{ inputs.reviewdog-version }}"
echo "${RUNNER_TEMP:-/tmp}/bin" >> "$GITHUB_PATH"
- name: Post PR review comments
if: inputs.comment-on-pr == 'true' && github.event_name == 'pull_request' && !cancelled()
shell: bash
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ inputs.token }}
run: |
SARIF_FILE="${{ steps.audit.outputs.sarif-file }}"
if [ ! -f "$SARIF_FILE" ]; then
echo "No SARIF file found, skipping PR comments"
exit 0
fi
# When diffing with a baseline and audit passed (exit 0), skip reviewdog
# to avoid annotating all baseline findings as PR comments
if [ "${{ steps.audit.outputs.have-baseline }}" = "true" ] && [ "${{ steps.audit.outputs.exit-code }}" = "0" ]; then
echo "Diff mode: no new findings — skipping PR annotations"
exit 0
fi
# Only annotate findings on lines changed in this PR
cat "$SARIF_FILE" | reviewdog \
-f=sarif \
-name="capsec" \
-reporter=github-pr-review \
-filter-mode=diff_context \
-fail-on-error=false \
-level=warning
continue-on-error: true
- name: Set check result
if: '!cancelled()'
shell: bash
run: |
EXIT_CODE="${{ steps.audit.outputs.exit-code }}"
if [ "$EXIT_CODE" = "1" ]; then
echo "::warning::capsec audit found findings exceeding the '${{ inputs.fail-on }}' threshold"
exit 1
fi