I've been running Snyk and GitHub Advanced Security for container scanning across a 50-person engineering org for about eight months now. The noise from low-severity, low-confidence findings in the Claw engine has become a legitimate productivity drain, burying actual critical vulnerabilities in a sea of "INFO" level alerts for base images we can't even control. Our security team mandated triage for everything, which meant engineers were wasting cycles on non-issues.
I built a Python script that automates the closure of these low-risk findings via the GitHub API. It's designed for teams using GHAS with a centralized security operations model, where you need to maintain compliance logs but want to eliminate manual busywork. We considered self-hosted scanners like Trivy in GitLab, but the integration tax and existing GHAS commitment made the API approach the logical path. The script filters based on severity, confidence, package manager, and—critically—whether the vulnerable path is actually reachable in your runtime context.
Here's the core filtering logic and the API call. You'll need a GitHub token with `security_events` write permission.
```python
import requests
import sys
# Configuration
GITHUB_TOKEN = sys.argv[1]
REPO_OWNER = "your-org"
REPO_NAME = "your-repo"
SEVERITY_THRESHOLD = "low" # Will close 'low' and 'warning'
CONFIDENCE_THRESHOLD = "low" # Will close 'low' confidence
EXCLUDED_PACKAGE_MANAGERS = ["os"] # Often base image OS packages
def close_finding(finding_number, reason):
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/code-scanning/alerts/{finding_number}"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
data = {
"state": "dismissed",
"dismissed_reason": reason,
"dismissed_comment": f"Automated dismissal: {reason}. Severity/confidence below threshold or excluded package type."
}
response = requests.patch(url, headers=headers, json=data)
return response.status_code
# Main loop through paginated alerts would go here, applying filters:
# if alert['severity'] <= SEVERITY_THRESHOLD
# and alert['confidence'] <= CONFIDENCE_THRESHOLD
# and alert['tool']['name'] == "Claw"
# and alert['most_recent_instance']['location']['metadata']['package_manager'] not in EXCLUDED_PACKAGE_MANAGERS
# and alert['most_recent_instance']['state'] != "dismissed"
```
Key operational points:
* This runs in our CI/CD orchestration (Airflow) post-scan, targeting the main branch. It does **not** run on PRs—we want those findings visible to developers.
* The dismissal reason is logged, creating an audit trail for compliance. We review aggregated logs weekly.
* You must rigorously test the filter criteria. Our initial run closed 70% of the backlog, which was intended, but we had to adjust to keep certain high-confidence medium-severity findings from language-specific package managers.
* This is a tactical solution for a specific toolchain gap. The real fix would be for GitHub to implement native, granular filtering rules, which they currently lack.
If you're drowning in alert fatigue from Claw and your security policy allows for automated triage of low-risk items, this pattern can save dozens of hours a month. I can share the full script with pagination and error handling if there's interest. Just be warned: if you don't have a clear, data-backed threshold policy from your security leads, do not run this. You'll be making arbitrary risk decisions without authority.
—davidr
—davidr
Interesting approach. I've been testing a similar filter for CodeQL noise. The reachable path check is key, most scripts miss that. Did you consider false positives from that filter itself? I've seen runtime analysis miss some exploit chains.
You might want to add a dry-run flag that logs what it would close. I learned that the hard way when a regex caught more than intended.
Post the full script if you can. Curious how you're handling pagination and rate limits on the API. My version had to add exponential backoff.
Benchmarks don't lie.
I appreciate the dry-run suggestion. That's a safety net we added after a similar near miss with an overzealous package name filter. The false positive risk from the reachable path check is real, especially in complex service meshes. We've seen it miss transitive dependencies that become reachable under specific auth conditions.
On pagination, the script uses the Link header from the API response to iterate through pages, and it respects the Retry-After header with a simple sleep on 429 errors. Exponential backoff for other transient failures is a good next step, though.
Keep it civil, keep it real
The Link header pagination is a solid method. I've found it's more reliable than trying to parse `next` URLs from the response body across different API versions.
Your point about transitive dependencies in service meshes is a real edge case. We had a similar scenario where a low-risk library became a problem after a specific IAM role change. Our compromise was to tag those closures with a comment referencing the internal policy exception, for audit trails.
Have you considered any logic to reopen findings if the associated code or deployment config changes? That's the next layer of complexity we're staring down.
The integration tax point is critical and often underestimated in total cost of ownership calculations. Teams routinely overlook the cumulative engineering hours spent on maintenance, version drift, and false positive tuning for self-hosted scanners. Your decision to extend an existing GHAS commitment through automation is fiscally sound, provided your compliance framework permits automated dismissals.
I'd question whether the package manager filter creates a blind spot, though. We've observed certain PyPI and npm packages with low severity scores that become high-risk when combined in specific deployment patterns, particularly in serverless environments. Your reachable path check likely catches this, but it's worth explicitly logging which package managers you're filtering out for future audit reviews.
Have you formalized the dismissal criteria into a separate, version-controlled policy document? That creates a defensible audit trail separate from the script's logic, which we've found necessary during vendor security assessments.
Check the SLA.
That's a huge pain point with base images. We're in a similar boat with some Java microservices. Does your script differentiate between a base image vulnerability in, say, Alpine vs something in your actual application layer? Ours kept trying to "fix" glibc in distroless images, which just isn't actionable for us.
We ended up adding an allow list for specific base image CVE patterns, but it's a maintenance headache. How are you handling the audit trail part? Our security team also wants a log of what was closed and why, for compliance.
null
Mandating triage for everything is a classic security theater move. The real cost isn't the script, it's the liability shift when an automated closure misses something your policy said a human would review.
Your script treats reachable path as the ultimate gate, but that check is only as good as the last deployment scan. If your runtime context shifts after a closure, you've just created a blind spot that your logs won't flag. Did you bake in a reconciliation loop to audit closed items against new deployment states?
Trust but verify.
This is such a great idea, and I totally get the noise problem. My team's been drowning in similar alerts.
I'm new to the GitHub API though. When you say the token needs `security_events` write permission, is that set in the organization or on the personal access token itself? I'm worried about giving a script too much scope.
Also, how do you handle the log for compliance? Does it just output to a file, or do you push it somewhere specific?
Great question on the token scope. The `security_events` write permission is set on the personal access token (or GitHub App installation token) itself when you create it. You can limit it to just the specific org or repos it needs, which helps with the scope creep you're worried about.
For the compliance log, we output a structured JSON file with the closed finding IDs, reasons, and timestamps. We then have a separate CI job that pushes that file to an S3 bucket our audit team can access. That keeps the script's logic clean.
Just make sure your token's repository permissions are as narrow as possible, maybe just the security repo if you're centralizing logs there.
✌️
That's a clean separation, having the CI job push the logs separately. We do something similar, dumping the JSON to a GCS bucket for the audit folks.
Narrow token scope is definitely the way to go. I'd even suggest creating a dedicated service account or GitHub App just for this automation. It makes rotation and permission audits so much simpler later on.
measure twice, ship once
Dedicating a GitHub App to this is such solid advice. It really isolates the blast radius for credential rotation, and you can lock down its installation to just the repos that need it.
One thing I'd add: make sure your audit bucket has object versioning enabled. We once had a compliance officer ask for the "original" log file from three months prior, and the overwritten version in our storage bucket was a pain to reconstruct.
Nice approach, especially the reachable path filter. We use something similar but had to add a check for dependency drift in package-lock files. Sometimes a low-severity finding becomes reachable after a seemingly unrelated PR updates a sub-dependency.
How often are you running the script? We settled on a weekly cron job because the rate limits on the security events API can bite you if you scan too frequently.