Skip to content

Commit f4e6fbe

Browse files
committed
feat: initial capsec audit GitHub Action
0 parents  commit f4e6fbe

2 files changed

Lines changed: 299 additions & 0 deletions

File tree

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# capsec audit GitHub Action
2+
3+
Static capability audit for Rust crates. Detects ambient authority (filesystem, network, environment, process, FFI) calls in your code.
4+
5+
## Usage
6+
7+
```yaml
8+
name: Capability Audit
9+
on: [pull_request]
10+
11+
permissions:
12+
contents: read
13+
security-events: write # Required for SARIF upload
14+
pull-requests: write # Required for PR review comments
15+
16+
jobs:
17+
audit:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: dtolnay/rust-toolchain@stable
22+
- uses: capsec/audit-action@v1
23+
with:
24+
fail-on: high
25+
```
26+
27+
## Inputs
28+
29+
| Input | Default | Description |
30+
|-------|---------|-------------|
31+
| `version` | `latest` | cargo-capsec version to install |
32+
| `fail-on` | `high` | Risk threshold: `low`, `medium`, `high`, `critical` |
33+
| `baseline` | `.capsec-baseline.json` | Path to baseline file (empty to disable) |
34+
| `diff` | `auto` | Only fail on new findings. `auto` enables on PRs. |
35+
| `format` | `sarif` | Output format: `text`, `json`, `sarif` |
36+
| `upload-sarif` | `true` | Upload SARIF to GitHub Code Scanning |
37+
| `comment-on-pr` | `true` | Post inline PR review comments via reviewdog |
38+
| `working-directory` | `.` | Path to Cargo workspace root |
39+
| `token` | `${{ github.token }}` | GitHub token |
40+
| `install-from` | `crates-io` | Install method: `crates-io` or `git` |
41+
| `git-repo` | `https://github.com/bordumb/capsec` | Git URL when `install-from` is `git` |
42+
43+
## Outputs
44+
45+
| Output | Description |
46+
|--------|-------------|
47+
| `sarif-file` | Path to generated SARIF file |
48+
| `finding-count` | Number of findings |
49+
| `exit-code` | `0` = pass, `1` = findings exceed threshold, `2` = runtime error |
50+
51+
## Examples
52+
53+
### Minimal (fail on high-risk findings)
54+
55+
```yaml
56+
- uses: capsec/audit-action@v1
57+
```
58+
59+
### With baseline diffing (only fail on new findings)
60+
61+
```yaml
62+
- uses: capsec/audit-action@v1
63+
with:
64+
fail-on: high
65+
baseline: .capsec-baseline.json
66+
diff: 'true'
67+
```
68+
69+
### Install from git (before crates.io publish)
70+
71+
```yaml
72+
- uses: capsec/audit-action@v1
73+
with:
74+
install-from: git
75+
git-repo: https://github.com/bordumb/capsec
76+
```
77+
78+
### Monorepo with custom working directory
79+
80+
```yaml
81+
- uses: capsec/audit-action@v1
82+
with:
83+
working-directory: ./rust-workspace
84+
```
85+
86+
### SARIF only (no PR comments)
87+
88+
```yaml
89+
- uses: capsec/audit-action@v1
90+
with:
91+
comment-on-pr: 'false'
92+
```
93+
94+
## How it works
95+
96+
1. Installs `cargo-capsec` (from crates.io or git)
97+
2. Runs `cargo capsec audit --format sarif --fail-on <threshold>`
98+
3. Uploads SARIF to GitHub Code Scanning (appears in Security tab)
99+
4. Posts inline review comments on PR diffs via reviewdog
100+
5. Fails the check if new findings exceed the threshold
101+
102+
## Permissions
103+
104+
| Permission | Required for |
105+
|-----------|-------------|
106+
| `security-events: write` | SARIF upload to Code Scanning |
107+
| `pull-requests: write` | Inline PR review comments |
108+
| `contents: read` | Reading source code |
109+
110+
## License
111+
112+
MIT

action.yml

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
name: 'capsec audit'
2+
description: 'Static capability audit for Rust crates — detect ambient authority (filesystem, network, env, process, FFI) in your code'
3+
branding:
4+
icon: 'shield'
5+
color: 'orange'
6+
7+
inputs:
8+
version:
9+
description: 'Version of cargo-capsec to install (e.g. "0.1.0" or "latest")'
10+
default: 'latest'
11+
fail-on:
12+
description: 'Minimum risk level to fail on: low, medium, high, critical'
13+
default: 'high'
14+
baseline:
15+
description: 'Path to baseline file. Set to empty string to disable baseline diffing.'
16+
default: '.capsec-baseline.json'
17+
diff:
18+
description: 'Only fail on new findings vs baseline (auto-enabled on pull_request events)'
19+
default: 'auto'
20+
format:
21+
description: 'Output format: text, json, sarif'
22+
default: 'sarif'
23+
upload-sarif:
24+
description: 'Upload SARIF to GitHub Code Scanning (requires security-events: write)'
25+
default: 'true'
26+
comment-on-pr:
27+
description: 'Post findings as inline PR review comments via reviewdog'
28+
default: 'true'
29+
working-directory:
30+
description: 'Path to Cargo workspace root'
31+
default: '.'
32+
token:
33+
description: 'GitHub token for PR comments and SARIF upload'
34+
default: ${{ github.token }}
35+
reviewdog-version:
36+
description: 'Version of reviewdog to install'
37+
default: 'latest'
38+
install-from:
39+
description: 'Install method: "crates-io" (cargo install) or "git" (from repository)'
40+
default: 'crates-io'
41+
git-repo:
42+
description: 'Git repository URL when install-from is "git"'
43+
default: 'https://github.com/bordumb/capsec'
44+
45+
outputs:
46+
sarif-file:
47+
description: 'Path to generated SARIF file (if format is sarif)'
48+
value: ${{ steps.audit.outputs.sarif-file }}
49+
finding-count:
50+
description: 'Total number of findings'
51+
value: ${{ steps.audit.outputs.finding-count }}
52+
exit-code:
53+
description: 'Exit code from cargo capsec audit (0=pass, 1=findings exceed threshold, 2=error)'
54+
value: ${{ steps.audit.outputs.exit-code }}
55+
56+
runs:
57+
using: 'composite'
58+
steps:
59+
- name: Install Rust toolchain
60+
shell: bash
61+
run: |
62+
if ! command -v cargo &>/dev/null; then
63+
echo "::error::Rust toolchain not found. Add dtolnay/rust-toolchain or actions-rust-lang/setup-rust-toolchain before this action."
64+
exit 1
65+
fi
66+
67+
- name: Install cargo-capsec
68+
shell: bash
69+
run: |
70+
if command -v cargo-capsec &>/dev/null; then
71+
echo "cargo-capsec already installed: $(cargo-capsec capsec audit --help 2>&1 | head -1 || echo 'found')"
72+
exit 0
73+
fi
74+
75+
if [ "${{ inputs.install-from }}" = "git" ]; then
76+
echo "Installing cargo-capsec from git: ${{ inputs.git-repo }}"
77+
cargo install --git "${{ inputs.git-repo }}" cargo-capsec
78+
else
79+
if [ "${{ inputs.version }}" = "latest" ]; then
80+
echo "Installing latest cargo-capsec from crates.io"
81+
cargo install cargo-capsec
82+
else
83+
echo "Installing cargo-capsec v${{ inputs.version }} from crates.io"
84+
cargo install cargo-capsec --version "${{ inputs.version }}"
85+
fi
86+
fi
87+
88+
- name: Run capsec audit
89+
id: audit
90+
shell: bash
91+
working-directory: ${{ inputs.working-directory }}
92+
run: |
93+
SARIF_FILE="${RUNNER_TEMP:-/tmp}/capsec-results.sarif"
94+
AUDIT_ARGS="--format sarif --fail-on ${{ inputs.fail-on }}"
95+
96+
# Determine if we should diff against baseline
97+
USE_DIFF="false"
98+
if [ "${{ inputs.diff }}" = "true" ]; then
99+
USE_DIFF="true"
100+
elif [ "${{ inputs.diff }}" = "auto" ] && [ "${{ github.event_name }}" = "pull_request" ]; then
101+
USE_DIFF="true"
102+
fi
103+
104+
# Handle baseline
105+
BASELINE_PATH="${{ inputs.baseline }}"
106+
if [ "$USE_DIFF" = "true" ] && [ -n "$BASELINE_PATH" ]; then
107+
# Fetch baseline from base branch for accurate diffing
108+
BASE_BASELINE="${RUNNER_TEMP:-/tmp}/capsec-base-baseline.json"
109+
if git show "origin/${{ github.base_ref }}:${BASELINE_PATH}" > "$BASE_BASELINE" 2>/dev/null; then
110+
echo "Using baseline from base branch: ${{ github.base_ref }}"
111+
AUDIT_ARGS="$AUDIT_ARGS --diff --baseline"
112+
else
113+
echo "No baseline found on base branch, running full audit"
114+
fi
115+
fi
116+
117+
echo "Running: cargo capsec audit $AUDIT_ARGS"
118+
set +e
119+
cargo capsec audit $AUDIT_ARGS > "$SARIF_FILE" 2>/dev/null
120+
AUDIT_EXIT=$?
121+
set -e
122+
123+
# Count findings from SARIF
124+
if [ -f "$SARIF_FILE" ] && command -v python3 &>/dev/null; then
125+
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")
126+
else
127+
FINDING_COUNT="unknown"
128+
fi
129+
130+
echo "sarif-file=$SARIF_FILE" >> "$GITHUB_OUTPUT"
131+
echo "finding-count=$FINDING_COUNT" >> "$GITHUB_OUTPUT"
132+
echo "exit-code=$AUDIT_EXIT" >> "$GITHUB_OUTPUT"
133+
134+
if [ "$AUDIT_EXIT" -eq 2 ]; then
135+
echo "::error::cargo capsec audit failed with a runtime error (exit code 2)"
136+
exit 1
137+
fi
138+
139+
echo "Audit complete: $FINDING_COUNT findings (exit code $AUDIT_EXIT)"
140+
141+
- name: Upload SARIF to GitHub Code Scanning
142+
if: inputs.upload-sarif == 'true' && steps.audit.outputs.sarif-file != '' && always()
143+
uses: github/codeql-action/upload-sarif@v3
144+
with:
145+
sarif_file: ${{ steps.audit.outputs.sarif-file }}
146+
category: capsec
147+
continue-on-error: true
148+
149+
- name: Install reviewdog
150+
if: inputs.comment-on-pr == 'true' && github.event_name == 'pull_request'
151+
shell: bash
152+
run: |
153+
curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b "${RUNNER_TEMP:-/tmp}/bin" "${{ inputs.reviewdog-version }}"
154+
echo "${RUNNER_TEMP:-/tmp}/bin" >> "$GITHUB_PATH"
155+
156+
- name: Post PR review comments
157+
if: inputs.comment-on-pr == 'true' && github.event_name == 'pull_request'
158+
shell: bash
159+
env:
160+
REVIEWDOG_GITHUB_API_TOKEN: ${{ inputs.token }}
161+
run: |
162+
SARIF_FILE="${{ steps.audit.outputs.sarif-file }}"
163+
if [ ! -f "$SARIF_FILE" ]; then
164+
echo "No SARIF file found, skipping PR comments"
165+
exit 0
166+
fi
167+
168+
# Convert SARIF to reviewdog format and post
169+
# reviewdog supports SARIF input natively via -f=sarif
170+
cat "$SARIF_FILE" | reviewdog \
171+
-f=sarif \
172+
-name="capsec" \
173+
-reporter=github-pr-review \
174+
-filter-mode=nofilter \
175+
-fail-on-error=false \
176+
-level=warning
177+
continue-on-error: true
178+
179+
- name: Set check result
180+
if: always()
181+
shell: bash
182+
run: |
183+
EXIT_CODE="${{ steps.audit.outputs.exit-code }}"
184+
if [ "$EXIT_CODE" = "1" ]; then
185+
echo "::warning::capsec audit found findings exceeding the '${{ inputs.fail-on }}' threshold"
186+
exit 1
187+
fi

0 commit comments

Comments
 (0)