Skip to content

Commit 1adaaf9

Browse files
authored
Merge pull request #59 from homanp/feature/pr-commit-security-scan
chore: add PR commit security scan workflow
2 parents 78b5726 + d36c813 commit 1adaaf9

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
name: PR Commit Security Scan
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize, ready_for_review]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
issues: write
11+
12+
jobs:
13+
scan:
14+
name: Scan PR commits
15+
runs-on: ubuntu-24.04
16+
timeout-minutes: 20
17+
concurrency:
18+
group: brin-pr-${{ github.event.pull_request.number }}
19+
cancel-in-progress: true
20+
21+
steps:
22+
- name: Scan commits with Brin
23+
id: scan
24+
shell: bash
25+
env:
26+
GH_TOKEN: ${{ github.token }}
27+
REPO: ${{ github.repository }}
28+
PR_NUMBER: ${{ github.event.pull_request.number }}
29+
BLOCK_BELOW: "30"
30+
run: |
31+
set -euo pipefail
32+
33+
total=0
34+
blocking=0
35+
review=0
36+
inconclusive=0
37+
rows=""
38+
39+
while IFS=$'\t' read -r sha title; do
40+
total=$((total + 1))
41+
short_sha="${sha:0:7}"
42+
commit_url="https://github.com/${REPO}/commit/${sha}"
43+
44+
response="$(
45+
curl -sfL --max-time 110 \
46+
"https://api.brin.sh/commit/${REPO}@${sha}?details=true&mode=full&tolerance=conservative" \
47+
|| echo '{}'
48+
)"
49+
50+
pending="$(jq -r '.pending_deep_scan // false' <<<"$response")"
51+
score="$(jq -r '.score // empty' <<<"$response")"
52+
verdict="$(jq -r '.verdict // "unknown"' <<<"$response")"
53+
54+
if [ -z "$score" ] || [ "$pending" = "true" ]; then
55+
inconclusive=$((inconclusive + 1))
56+
rows="${rows}| [\`${short_sha}\`](${commit_url}) | inconclusive | unknown | follow up | Scan did not complete in time |\n"
57+
continue
58+
fi
59+
60+
detail="$(
61+
jq -r '(.threats // []) | map("\(.type): \(.detail)") | .[0] // "No threat detail provided"' \
62+
<<<"$response"
63+
)"
64+
detail="${detail//$'\n'/ }"
65+
66+
if [ "$verdict" = "dangerous" ] || [ "$score" -lt "$BLOCK_BELOW" ]; then
67+
blocking=$((blocking + 1))
68+
rows="${rows}| [\`${short_sha}\`](${commit_url}) | ${score} | ${verdict} | block | ${detail} |\n"
69+
elif [ "$verdict" = "suspicious" ]; then
70+
review=$((review + 1))
71+
rows="${rows}| [\`${short_sha}\`](${commit_url}) | ${score} | ${verdict} | review | ${detail} |\n"
72+
fi
73+
done < <(
74+
gh api "repos/${REPO}/pulls/${PR_NUMBER}/commits?per_page=100" \
75+
--paginate \
76+
--jq '.[] | [.sha, (.commit.message | split("\n")[0])] | @tsv'
77+
)
78+
79+
has_findings=false
80+
if [ "$blocking" -gt 0 ] || [ "$review" -gt 0 ] || [ "$inconclusive" -gt 0 ]; then
81+
has_findings=true
82+
fi
83+
84+
overall="clean"
85+
if [ "$blocking" -gt 0 ]; then
86+
overall="blocking"
87+
elif [ "$review" -gt 0 ]; then
88+
overall="review"
89+
elif [ "$inconclusive" -gt 0 ]; then
90+
overall="inconclusive"
91+
fi
92+
93+
{
94+
echo "total=${total}"
95+
echo "blocking=${blocking}"
96+
echo "review=${review}"
97+
echo "inconclusive=${inconclusive}"
98+
echo "has_findings=${has_findings}"
99+
echo "overall=${overall}"
100+
if [ "$blocking" -gt 0 ]; then
101+
echo "should_fail=true"
102+
else
103+
echo "should_fail=false"
104+
fi
105+
} >> "$GITHUB_OUTPUT"
106+
107+
if [ "$has_findings" = "true" ]; then
108+
{
109+
echo "rows<<BRIN_EOF"
110+
printf "%b" "$rows"
111+
echo "BRIN_EOF"
112+
} >> "$GITHUB_OUTPUT"
113+
fi
114+
115+
{
116+
echo "## Brin PR Commit Scan"
117+
echo
118+
echo "- Overall: ${overall}"
119+
echo "- Commits scanned: ${total}"
120+
echo "- Blocking findings: ${blocking}"
121+
echo "- Review findings: ${review}"
122+
echo "- Inconclusive scans: ${inconclusive}"
123+
} >> "$GITHUB_STEP_SUMMARY"
124+
125+
- name: Create or update PR comment
126+
if: steps.scan.outputs.has_findings == 'true'
127+
uses: actions/github-script@v7
128+
env:
129+
TOTAL: ${{ steps.scan.outputs.total }}
130+
BLOCKING: ${{ steps.scan.outputs.blocking }}
131+
REVIEW: ${{ steps.scan.outputs.review }}
132+
INCONCLUSIVE: ${{ steps.scan.outputs.inconclusive }}
133+
OVERALL: ${{ steps.scan.outputs.overall }}
134+
ROWS: ${{ steps.scan.outputs.rows }}
135+
with:
136+
script: |
137+
const marker = "<!-- brin-pr-commit-scan -->";
138+
139+
let headline = "No issues found.";
140+
if (process.env.OVERALL === "blocking") {
141+
headline = "This PR contains commit-level findings that should block merge.";
142+
} else if (process.env.OVERALL === "review") {
143+
headline = "This PR contains commit-level findings that should be reviewed.";
144+
} else if (process.env.OVERALL === "inconclusive") {
145+
headline = "Some commit scans were inconclusive and may need a rerun.";
146+
}
147+
148+
const body = [
149+
marker,
150+
"### Brin PR Commit Scan",
151+
"",
152+
headline,
153+
"",
154+
`- Commits scanned: ${process.env.TOTAL}`,
155+
`- Blocking findings: ${process.env.BLOCKING}`,
156+
`- Review findings: ${process.env.REVIEW}`,
157+
`- Inconclusive scans: ${process.env.INCONCLUSIVE}`,
158+
"",
159+
"| Commit | Score | Verdict | Action | Detail |",
160+
"|--------|-------|---------|--------|--------|",
161+
process.env.ROWS,
162+
"",
163+
"<sub>Scanned by [Brin](https://brin.sh)</sub>",
164+
].join("\n");
165+
166+
const { owner, repo } = context.repo;
167+
const issue_number = context.payload.pull_request.number;
168+
169+
const comments = await github.paginate(github.rest.issues.listComments, {
170+
owner,
171+
repo,
172+
issue_number,
173+
per_page: 100,
174+
});
175+
176+
const existing = comments.find((c) => c.body?.includes(marker));
177+
178+
if (existing) {
179+
await github.rest.issues.updateComment({
180+
owner,
181+
repo,
182+
comment_id: existing.id,
183+
body,
184+
});
185+
} else {
186+
await github.rest.issues.createComment({
187+
owner,
188+
repo,
189+
issue_number,
190+
body,
191+
});
192+
}
193+
194+
- name: Delete old comment when PR is clean
195+
if: steps.scan.outputs.has_findings == 'false'
196+
uses: actions/github-script@v7
197+
with:
198+
script: |
199+
const marker = "<!-- brin-pr-commit-scan -->";
200+
const { owner, repo } = context.repo;
201+
const issue_number = context.payload.pull_request.number;
202+
203+
const comments = await github.paginate(github.rest.issues.listComments, {
204+
owner,
205+
repo,
206+
issue_number,
207+
per_page: 100,
208+
});
209+
210+
const existing = comments.find((c) => c.body?.includes(marker));
211+
212+
if (existing) {
213+
await github.rest.issues.deleteComment({
214+
owner,
215+
repo,
216+
comment_id: existing.id,
217+
});
218+
}
219+
220+
- name: Fail if blocking findings exist
221+
if: steps.scan.outputs.should_fail == 'true'
222+
run: |
223+
echo "::error::Brin found one or more dangerous commits or commits scoring below 30"
224+
exit 1

0 commit comments

Comments
 (0)