ci: replace commit scan with PR security scan (#224)
This commit is contained in:
1 parent
30c516de15
commit
446f60c887
2 files changed
+125
-224
No files matched your search
@@ -1,224 +0,0 @@
|
||||
name: PR Commit Security Scan
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
name: Scan PR commits
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
concurrency:
|
||||
group: brin-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Scan commits with Brin
|
||||
id: scan
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
BLOCK_BELOW: "30"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
total=0
|
||||
blocking=0
|
||||
review=0
|
||||
inconclusive=0
|
||||
rows=""
|
||||
|
||||
while IFS=$'\t' read -r sha title; do
|
||||
total=$((total + 1))
|
||||
short_sha="${sha:0:7}"
|
||||
commit_url="https://github.com/${REPO}/commit/${sha}"
|
||||
|
||||
response="$(
|
||||
curl -sfL --max-time 300 \
|
||||
"https://api.brin.sh/commit/${REPO}@${sha}?details=true&mode=full&tolerance=conservative" \
|
||||
|| echo '{}'
|
||||
)"
|
||||
|
||||
pending="$(jq -r '.pending_deep_scan // false' <<<"$response")"
|
||||
score="$(jq -r '.score // empty' <<<"$response")"
|
||||
verdict="$(jq -r '.verdict // "unknown"' <<<"$response")"
|
||||
|
||||
if [ -z "$score" ] || [ "$pending" = "true" ]; then
|
||||
inconclusive=$((inconclusive + 1))
|
||||
rows="${rows}| [\`${short_sha}\`](${commit_url}) | inconclusive | unknown | follow up | Scan did not complete in time |\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
detail="$(
|
||||
jq -r '(.threats // []) | map("\(.type): \(.detail)") | .[0] // "No threat detail provided"' \
|
||||
<<<"$response"
|
||||
)"
|
||||
detail="${detail//$'\n'/ }"
|
||||
|
||||
if [ "$verdict" = "dangerous" ] || [ "$score" -lt "$BLOCK_BELOW" ]; then
|
||||
blocking=$((blocking + 1))
|
||||
rows="${rows}| [\`${short_sha}\`](${commit_url}) | ${score} | ${verdict} | block | ${detail} |\n"
|
||||
elif [ "$verdict" = "suspicious" ]; then
|
||||
review=$((review + 1))
|
||||
rows="${rows}| [\`${short_sha}\`](${commit_url}) | ${score} | ${verdict} | review | ${detail} |\n"
|
||||
fi
|
||||
done < <(
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}/commits?per_page=100" \
|
||||
--paginate \
|
||||
--jq '.[] | [.sha, (.commit.message | split("\n")[0])] | @tsv'
|
||||
)
|
||||
|
||||
has_findings=false
|
||||
if [ "$blocking" -gt 0 ] || [ "$review" -gt 0 ] || [ "$inconclusive" -gt 0 ]; then
|
||||
has_findings=true
|
||||
fi
|
||||
|
||||
overall="clean"
|
||||
if [ "$blocking" -gt 0 ]; then
|
||||
overall="blocking"
|
||||
elif [ "$review" -gt 0 ]; then
|
||||
overall="review"
|
||||
elif [ "$inconclusive" -gt 0 ]; then
|
||||
overall="inconclusive"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "total=${total}"
|
||||
echo "blocking=${blocking}"
|
||||
echo "review=${review}"
|
||||
echo "inconclusive=${inconclusive}"
|
||||
echo "has_findings=${has_findings}"
|
||||
echo "overall=${overall}"
|
||||
if [ "$blocking" -gt 0 ]; then
|
||||
echo "should_fail=true"
|
||||
else
|
||||
echo "should_fail=false"
|
||||
fi
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$has_findings" = "true" ]; then
|
||||
{
|
||||
echo "rows<<BRIN_EOF"
|
||||
printf "%b" "$rows"
|
||||
echo "BRIN_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "## Brin PR Commit Scan"
|
||||
echo
|
||||
echo "- Overall: ${overall}"
|
||||
echo "- Commits scanned: ${total}"
|
||||
echo "- Blocking findings: ${blocking}"
|
||||
echo "- Review findings: ${review}"
|
||||
echo "- Inconclusive scans: ${inconclusive}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Create or update PR comment
|
||||
if: steps.scan.outputs.has_findings == 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TOTAL: ${{ steps.scan.outputs.total }}
|
||||
BLOCKING: ${{ steps.scan.outputs.blocking }}
|
||||
REVIEW: ${{ steps.scan.outputs.review }}
|
||||
INCONCLUSIVE: ${{ steps.scan.outputs.inconclusive }}
|
||||
OVERALL: ${{ steps.scan.outputs.overall }}
|
||||
ROWS: ${{ steps.scan.outputs.rows }}
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- brin-pr-commit-scan -->";
|
||||
|
||||
let headline = "No issues found.";
|
||||
if (process.env.OVERALL === "blocking") {
|
||||
headline = "This PR contains commit-level findings that should block merge.";
|
||||
} else if (process.env.OVERALL === "review") {
|
||||
headline = "This PR contains commit-level findings that should be reviewed.";
|
||||
} else if (process.env.OVERALL === "inconclusive") {
|
||||
headline = "Some commit scans were inconclusive and may need a rerun.";
|
||||
}
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
"### Brin PR Commit Scan",
|
||||
"",
|
||||
headline,
|
||||
"",
|
||||
`- Commits scanned: ${process.env.TOTAL}`,
|
||||
`- Blocking findings: ${process.env.BLOCKING}`,
|
||||
`- Review findings: ${process.env.REVIEW}`,
|
||||
`- Inconclusive scans: ${process.env.INCONCLUSIVE}`,
|
||||
"",
|
||||
"| Commit | Score | Verdict | Action | Detail |",
|
||||
"|--------|-------|---------|--------|--------|",
|
||||
process.env.ROWS ?? "",
|
||||
"",
|
||||
"<sub>Scanned by [Brin](https://brin.sh)</sub>",
|
||||
].join("\n");
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find((c) => c.body?.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Delete old comment when PR is clean
|
||||
if: steps.scan.outputs.has_findings == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- brin-pr-commit-scan -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find((c) => c.body?.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if blocking findings exist
|
||||
if: steps.scan.outputs.should_fail == 'true'
|
||||
run: |
|
||||
echo "::error::Brin found one or more dangerous commits or commits scoring below 30"
|
||||
exit 1
|
||||
@@ -0,0 +1,125 @@
|
||||
name: PR Security Scan
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
name: Scan PR
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: brin-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Scan PR with Brin
|
||||
id: scan
|
||||
shell: bash
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
RESPONSE=$(curl -sfL --max-time 300 \
|
||||
"https://api.brin.sh/pr/${REPO}/${PR_NUMBER}?details=true&mode=full&tolerance=conservative" \
|
||||
|| echo '{}')
|
||||
|
||||
SCORE=$(jq -r '.score // empty' <<<"$RESPONSE")
|
||||
VERDICT=$(jq -r '.verdict // "unknown"' <<<"$RESPONSE")
|
||||
PENDING=$(jq -r '.pending_deep_scan // false' <<<"$RESPONSE")
|
||||
|
||||
if [ -z "$SCORE" ] || [ "$PENDING" = "true" ]; then
|
||||
echo "status=inconclusive" >> "$GITHUB_OUTPUT"
|
||||
echo "should_fail=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "score=${SCORE}" >> "$GITHUB_OUTPUT"
|
||||
echo "verdict=${VERDICT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$VERDICT" = "dangerous" ] || [ "$SCORE" -lt 30 ]; then
|
||||
echo "status=blocking" >> "$GITHUB_OUTPUT"
|
||||
echo "should_fail=true" >> "$GITHUB_OUTPUT"
|
||||
elif [ "$VERDICT" = "suspicious" ]; then
|
||||
echo "status=review" >> "$GITHUB_OUTPUT"
|
||||
echo "should_fail=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=clean" >> "$GITHUB_OUTPUT"
|
||||
echo "should_fail=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
THREATS=$(jq -r '(.threats // [])[] | "- \(.type): \(.detail)"' <<<"$RESPONSE")
|
||||
{
|
||||
echo "threats<<BRIN_EOF"
|
||||
echo "$THREATS"
|
||||
echo "BRIN_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Comment on flagged PR
|
||||
if: steps.scan.outputs.status == 'blocking' || steps.scan.outputs.status == 'review'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SCORE: ${{ steps.scan.outputs.score }}
|
||||
VERDICT: ${{ steps.scan.outputs.verdict }}
|
||||
STATUS: ${{ steps.scan.outputs.status }}
|
||||
THREATS: ${{ steps.scan.outputs.threats }}
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- brin-pr-scan -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
|
||||
const headline = process.env.STATUS === "blocking"
|
||||
? "This PR has findings that should block merge."
|
||||
: "This PR has findings that should be reviewed.";
|
||||
|
||||
let body = `${marker}\n### Brin PR Security Scan\n\n`;
|
||||
body += `${headline}\n\n`;
|
||||
body += `- **Score:** ${process.env.SCORE}/100\n`;
|
||||
body += `- **Verdict:** ${process.env.VERDICT}\n\n`;
|
||||
if (process.env.THREATS) {
|
||||
body += `**Findings:**\n${process.env.THREATS}\n\n`;
|
||||
}
|
||||
body += `<sub>Analyzed by [Brin](https://brin.sh)</sub>`;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number, per_page: 100,
|
||||
});
|
||||
const existing = comments.find((c) => c.body?.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
}
|
||||
|
||||
- name: Delete old comment when clean
|
||||
if: steps.scan.outputs.status == 'clean'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- brin-pr-scan -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find((c) => c.body?.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
|
||||
}
|
||||
|
||||
- name: Fail if blocking
|
||||
if: steps.scan.outputs.should_fail == 'true'
|
||||
run: |
|
||||
echo "::error::Brin flagged this PR as dangerous or scoring below 30"
|
||||
exit 1
|
||||
Reference in new issue
Block a user