PR Scanning Strategy¶
PR scanning gives developers security feedback before code reaches the main branch. This guide covers how to design a PR scanning strategy that balances security with developer velocity.
Goals¶
A good PR scanning strategy:
- Catches real issues before they reach production
- Doesn't block developers on false positives or low-severity noise
- Provides fast feedback — ideally under 2 minutes
- Gives actionable guidance — not just "you have a vulnerability"
Scanner selection for PRs¶
PR scans should be fast. Choose scanners accordingly:
| Scanner | PR scan time | Recommended for PRs? |
|---|---|---|
| Gitleaks | 5-15 seconds | Yes — always include |
| Grype | 10-30 seconds | Yes — catches new vulnerable deps |
| Semgrep | 30s - 3 minutes | Yes, if speed is acceptable |
| Trivy (container) | 30s - 2 minutes | Only if Dockerfile is changed |
Start with secrets + SCA
Gitleaks + Grype gives you fast, high-signal feedback. Add Semgrep once your team is comfortable with the workflow.
Gate strategy¶
Level 1 — Inform only (no blocking)¶
Post a comment with findings but don't block the PR:
package mayo.pr_scan
import rego.v1
default result := "pass"
default block := false
message := sprintf("Found %d new finding(s)", [
input.scan_results.total_new
]) if {
input.scan_results.total_new > 0
}
Best for: Teams new to PR scanning. Builds awareness without friction.
Level 2 — Block on critical only¶
Block PRs that introduce critical vulnerabilities:
package mayo.pr_scan
import rego.v1
result := "fail" if {
input.scan_results.by_severity.critical > 0
}
default result := "pass"
Best for: Most teams. Catches the worst issues without excessive blocking.
Level 3 — Block on critical + high¶
package mayo.pr_scan
import rego.v1
result := "fail" if {
input.scan_results.by_severity.critical + input.scan_results.by_severity.high > 0
}
default result := "pass"
Best for: Teams with mature triage policies and low false-positive rates.
Level 4 — Block on any new finding¶
package mayo.pr_scan
import rego.v1
result := "fail" if {
input.scan_results.total_new > 0
}
default result := "pass"
Warning
This is very strict. Only use this for critical repositories with well-tuned scanners. High false-positive rates will frustrate developers.
Graduated rollout¶
Roll out PR scanning in phases:
| Phase | Duration | Gate level | Repositories |
|---|---|---|---|
| 1 | Week 1-2 | Inform only | 3-5 pilot repos |
| 2 | Week 3-4 | Block on critical | Pilot repos |
| 3 | Month 2 | Block on critical | All active repos |
| 4 | Month 3+ | Block on critical+high | High-risk repos |
Comment styles¶
Summary comment¶
A single comment at the top of the PR:
## Mayo ASPM Scan Results
| Severity | New | Fixed |
|----------|-----|-------|
| Critical | 0 | 0 |
| High | 1 | 0 |
| Medium | 3 | 1 |
**1 blocking finding** — [View details](https://mayoaspm.com/...)
Inline annotations¶
Comments on specific lines in the PR diff:
⚠️ SQL Injection (high)
semgrep: java.lang.security.audit.sqli.tainted-sql-string
This query uses unsanitized user input. Use parameterized queries instead.
Both¶
Use summary + inline for the best developer experience.
Handling edge cases¶
Draft PRs¶
Skip scanning on draft PRs to save resources:
Dependabot / Renovate PRs¶
Auto-generated dependency update PRs should still be scanned but may warrant different rules:
result := "fail" if {
input.pull_request.author in ["dependabot[bot]", "renovate[bot]"]
input.scan_results.by_severity.critical > 0
}
Branch-specific rules¶
Stricter rules for PRs targeting main:
result := "fail" if {
input.pull_request.base_branch == "main"
input.scan_results.by_severity.critical + input.scan_results.by_severity.high > 0
}
result := "fail" if {
input.pull_request.base_branch != "main"
input.scan_results.by_severity.critical > 0
}
default result := "pass"
Performance tips¶
- Use differential scanning — Mayo ASPM only scans the PR diff, not the entire repo.
- Limit scanner count — 2-3 scanners for PRs is usually enough.
- Set scan timeouts — if a scan takes > 5 minutes, it may indicate an issue.
- Cache results — re-pushes to the same PR only scan changed files.
Metrics¶
Track PR scanning effectiveness:
| Metric | Target | Description |
|---|---|---|
| PR scan time | < 2 minutes | Time from PR event to check run result |
| Block rate | < 5% of PRs | PRs blocked by scan failures |
| False block rate | < 1% of PRs | PRs blocked incorrectly |
| Developer override rate | < 2% | PRs merged despite scan failure |
Next steps¶
- PR scan policies — policy reference
- GitHub integration — setup check runs
- PR scanning use case — end-to-end walkthrough