Skip to content

Commit 2e13328

Browse files
feat: add OpenAnt security scanning pipeline
Adds a GitHub Actions workflow that fetches OpenAnt from santandersecurityresearch/OpenAnt, builds it, and runs a two-stage vulnerability scan (Stage 1 detection + Stage 2 attacker simulation) on every PR and push to master. Results are posted as a PR comment, uploaded to the Security tab as SARIF, and stored as workflow artefacts for 30 days. The check fails if any confirmed vulnerabilities are found.
1 parent 5249cc8 commit 2e13328

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

.github/workflows/openant-scan.yml

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
name: OpenAnt Security Scan
2+
3+
# Runs on every pull request and on pushes to main/master.
4+
# Fetches OpenAnt from santandersecurityresearch, builds it, scans this
5+
# repository, posts a summary comment on PRs, uploads SARIF to the
6+
# Security tab, and fails the check if confirmed vulnerabilities are found.
7+
8+
on:
9+
pull_request:
10+
branches: [main, master]
11+
push:
12+
branches: [main, master]
13+
14+
permissions:
15+
contents: read
16+
pull-requests: write # post PR comment
17+
security-events: write # upload SARIF
18+
19+
jobs:
20+
scan:
21+
name: Vulnerability scan
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 60
24+
25+
steps:
26+
# ------------------------------------------------------------------ Checkout target repo
27+
- name: Checkout
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
fetch-depth: 0
31+
persist-credentials: false
32+
33+
# ------------------------------------------------------------------ Fetch OpenAnt
34+
- name: Checkout OpenAnt
35+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
36+
with:
37+
repository: santandersecurityresearch/OpenAnt
38+
ref: feat/github-actions-pipeline
39+
path: _openant
40+
41+
# ------------------------------------------------------------------ Go
42+
- name: Set up Go
43+
uses: actions/setup-go@v5
44+
with:
45+
go-version-file: _openant/apps/openant-cli/go.mod
46+
cache-dependency-path: _openant/apps/openant-cli/go.sum
47+
48+
# ------------------------------------------------------------------ Python
49+
- name: Set up Python
50+
uses: actions/setup-python@v5
51+
with:
52+
python-version: '3.11'
53+
cache: pip
54+
cache-dependency-path: _openant/libs/openant-core/requirements.txt
55+
56+
- name: Install Python dependencies
57+
run: pip install --quiet -r _openant/libs/openant-core/requirements.txt
58+
59+
# ------------------------------------------------------------------ Build openant
60+
- name: Build openant CLI
61+
run: |
62+
cd _openant/apps/openant-cli
63+
go build -o bin/openant .
64+
echo "$(pwd)/bin" >> "$GITHUB_PATH"
65+
66+
- name: Verify openant
67+
run: openant version
68+
69+
# ------------------------------------------------------------------ Scan
70+
- name: Run security scan
71+
id: scan
72+
env:
73+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
74+
run: |
75+
mkdir -p /tmp/openant-results
76+
77+
# Point openant at the checked-out source (GITHUB_WORKSPACE is the
78+
# vulpy root; _openant is a subdirectory we exclude via --language).
79+
openant scan "$GITHUB_WORKSPACE" \
80+
--language python \
81+
--output /tmp/openant-results \
82+
--level reachable \
83+
--verify \
84+
--enhance-mode single-shot \
85+
--model sonnet \
86+
--limit 50 \
87+
--no-report \
88+
--workers 8 \
89+
--json \
90+
2>/tmp/openant-stderr.log \
91+
| tee /tmp/openant-stdout.json || true
92+
93+
# ------------------------------------------------------------------ SARIF
94+
- name: Convert findings to SARIF
95+
if: always()
96+
run: |
97+
PIPELINE_OUTPUT="/tmp/openant-results/pipeline_output.json"
98+
if [ -f "$PIPELINE_OUTPUT" ]; then
99+
python _openant/tools/sarif_convert.py "$PIPELINE_OUTPUT" \
100+
-o /tmp/openant-results/results.sarif
101+
else
102+
# Minimal valid empty SARIF so the upload step does not fail.
103+
cat > /tmp/openant-results/results.sarif <<'SARIF'
104+
{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","version":"2.1.0","runs":[{"tool":{"driver":{"name":"OpenAnt","version":"1.0.0","rules":[]}},"results":[]}]}
105+
SARIF
106+
fi
107+
108+
- name: Upload SARIF to GitHub Security tab
109+
uses: github/codeql-action/upload-sarif@v3
110+
if: always()
111+
with:
112+
sarif_file: /tmp/openant-results/results.sarif
113+
category: openant
114+
115+
# ------------------------------------------------------------------ PR comment
116+
- name: Post scan summary as PR comment
117+
if: always() && github.event_name == 'pull_request'
118+
env:
119+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120+
run: |
121+
STDOUT="/tmp/openant-stdout.json"
122+
PIPELINE_OUTPUT="/tmp/openant-results/pipeline_output.json"
123+
124+
VULNERABLE=0; BYPASSABLE=0; INCONCLUSIVE=0; TOTAL=0; COST="N/A"
125+
if [ -f "$STDOUT" ]; then
126+
VULNERABLE=$(jq -r '.data.metrics.vulnerable // 0' "$STDOUT" 2>/dev/null || echo 0)
127+
BYPASSABLE=$(jq -r '.data.metrics.bypassable // 0' "$STDOUT" 2>/dev/null || echo 0)
128+
INCONCLUSIVE=$(jq -r '.data.metrics.inconclusive // 0' "$STDOUT" 2>/dev/null || echo 0)
129+
TOTAL=$(jq -r '.data.units_count // 0' "$STDOUT" 2>/dev/null || echo 0)
130+
RAW_COST=$(jq -r '.data.usage.total_cost_usd // 0' "$STDOUT" 2>/dev/null || echo 0)
131+
COST=$(printf '$%.2f' "$RAW_COST" 2>/dev/null || echo "N/A")
132+
fi
133+
134+
if [ "$VULNERABLE" -gt 0 ] || [ "$BYPASSABLE" -gt 0 ]; then
135+
BADGE="🔴 **Vulnerabilities confirmed**"
136+
elif [ "$INCONCLUSIVE" -gt 0 ]; then
137+
BADGE="🟡 **Needs manual review**"
138+
else
139+
BADGE="🟢 **No confirmed vulnerabilities**"
140+
fi
141+
142+
FINDING_LINES=""
143+
if [ -f "$PIPELINE_OUTPUT" ]; then
144+
FINDING_LINES=$(jq -r '
145+
.findings[:10][]
146+
| "| `\(.location.file)` | \(.location.function) | \(.stage1_verdict | ascii_upcase) | \(.name) |"
147+
' "$PIPELINE_OUTPUT" 2>/dev/null || echo "")
148+
fi
149+
150+
FINDING_TABLE=""
151+
if [ -n "$FINDING_LINES" ]; then
152+
FINDING_TABLE="### Findings
153+
154+
| File | Function | Verdict | Type |
155+
|------|----------|---------|------|
156+
${FINDING_LINES}"
157+
fi
158+
159+
cat > /tmp/pr-comment.md <<EOF
160+
## OpenAnt Security Scan
161+
162+
${BADGE}
163+
164+
| Metric | Value |
165+
|--------|-------|
166+
| Units scanned | ${TOTAL} |
167+
| Confirmed vulnerable | ${VULNERABLE} |
168+
| Bypassable controls | ${BYPASSABLE} |
169+
| Inconclusive | ${INCONCLUSIVE} |
170+
| LLM cost | ${COST} |
171+
172+
${FINDING_TABLE}
173+
174+
> Stage 1 detection + Stage 2 attacker simulation via [OpenAnt](https://github.com/santandersecurityresearch/OpenAnt).
175+
> See the **Security > Code scanning** tab for annotated findings.
176+
177+
<sub>commit \`${{ github.sha }}\`</sub>
178+
EOF
179+
180+
gh pr comment "${{ github.event.pull_request.number }}" \
181+
--repo "${{ github.repository }}" \
182+
--body-file /tmp/pr-comment.md \
183+
--edit-last \
184+
|| \
185+
gh pr comment "${{ github.event.pull_request.number }}" \
186+
--repo "${{ github.repository }}" \
187+
--body-file /tmp/pr-comment.md
188+
189+
# ------------------------------------------------------------------ Artifacts
190+
- name: Upload scan artifacts
191+
uses: actions/upload-artifact@v4
192+
if: always()
193+
with:
194+
name: openant-results-${{ github.sha }}
195+
path: |
196+
/tmp/openant-results/pipeline_output.json
197+
/tmp/openant-results/results_verified.json
198+
/tmp/openant-results/results.sarif
199+
/tmp/openant-stdout.json
200+
/tmp/openant-stderr.log
201+
if-no-files-found: warn
202+
retention-days: 30
203+
204+
# ------------------------------------------------------------------ Fail gate
205+
- name: Fail if confirmed vulnerabilities found
206+
run: |
207+
PIPELINE_OUTPUT="/tmp/openant-results/pipeline_output.json"
208+
209+
if [ ! -f "$PIPELINE_OUTPUT" ]; then
210+
echo "::warning::pipeline_output.json not found — scan may have errored. Check artifacts for stderr log."
211+
exit 0
212+
fi
213+
214+
VULNERABLE=$(jq '[.findings[] | select(.stage1_verdict == "vulnerable")] | length' \
215+
"$PIPELINE_OUTPUT" 2>/dev/null || echo 0)
216+
BYPASSABLE=$(jq '[.findings[] | select(.stage1_verdict == "bypassable")] | length' \
217+
"$PIPELINE_OUTPUT" 2>/dev/null || echo 0)
218+
219+
echo "Confirmed vulnerable : ${VULNERABLE}"
220+
echo "Bypassable controls : ${BYPASSABLE}"
221+
222+
if [ "$VULNERABLE" -gt 0 ]; then
223+
echo "::error::${VULNERABLE} confirmed $([ "$VULNERABLE" -eq 1 ] && echo vulnerability || echo vulnerabilities) found. Review the Security tab or PR comment."
224+
exit 1
225+
fi
226+
227+
if [ "$BYPASSABLE" -gt 0 ]; then
228+
echo "::warning::${BYPASSABLE} finding$([ "$BYPASSABLE" -eq 1 ] && echo '' || echo s) with bypassable controls — review before merging."
229+
fi

0 commit comments

Comments
 (0)