Triaging Static Analysis Findings: A Practical Methodology
How to work through BrassCoders findings without burning out your team: severity tiers, when to fix vs suppress, .brassignore vs inline comments, and how AI enrichment changes the calculus.
A tool that emits 1500 findings and ships you no triage guide is not a tool. It’s homework.
Static analysis has a noise problem. A typical Python project scanned by a full suite of analyzers — Bandit, Semgrep, Pylint, detect-secrets, custom pattern detectors — produces a wall of output. Some of those findings are critical bugs. Some are false positives. Most are somewhere in between. Without a methodology, teams do one of two things: they try to fix everything (burnout, ignored tool) or they ignore everything (silent debt accumulation, tool removed from the pipeline). Both outcomes waste the scan.
This post lays out the methodology I’d recommend to any team running BrassCoders for the first time: how to tier the findings by severity, when to fix versus suppress, what belongs in .brassignore versus an inline comment, and how the Paid plan’s AI enrichment changes the calculus.
The Severity Tiers
BrassCoders rates every finding CRITICAL, HIGH, MEDIUM, or LOW, and pairs each rating with the scanner source that produced it — so you know whether you’re looking at a Bandit security rule, a Semgrep custom pattern, a detect-secrets hit, or a Pylint style warning.
Work the tiers in order. Don’t skip ahead.
CRITICAL: fix before merge. No exceptions. This tier covers SQL injection (B608 from Bandit), shell injection with unsanitized user input (B602), and hardcoded credentials flagged at HIGH confidence by Yelp’s detect-secrets. These are findings that, if wrong, ship you a breach or an RCE. The bar for bypassing them is evidence, not gut feel. One rule on CRITICALs: never suppress inline. If you’re convinced the finding is a false positive, document the reasoning and bring it to the team. If you’re right, you’ll be able to defend it. If you’re wrong, you just stopped a real bug.
HIGH: fix in the same sprint or next sprint. Deserialization of untrusted data, taint paths that are one hop from user input, high-entropy strings that pattern-match to API key formats in paths that look like production config. You don’t skip the sprint for a HIGH. You put it in the backlog with a due date.
MEDIUM: add to backlog, don’t suppress without a reason. This tier is where teams make mistakes. Medium findings are easy to dismiss because they feel speculative — they’re not immediately obviously exploitable. That doesn’t mean they’re safe. Add them to the backlog. Review them on a cadence. Suppress only when you’ve read the finding, understood the context, and can articulate why it doesn’t apply.
LOW: suppress or .brassignore if consistently irrelevant. If every LOW from a particular rule class fires in a path that’s consistently out of scope for your codebase (say, B108 /tmp path warnings in a container that never persists data), that’s a .brassignore candidate. The rule produced low-signal output for your project specifically. Excluding it is honest.
Fix vs. Suppress: The Decision
BrassCoders intentionally does not suppress findings automatically. The suppression decision is yours — but the framework for making it consistently is simple.
Fix when: the finding is in a production path and the underlying pattern is actually dangerous. Security findings in API request handlers. Hardcoded credentials anywhere. Performance anti-patterns in hot loops that will matter under real load. Secrets especially: the right answer is always to rotate the credential and remove it from source, not to annotate the file and move on. A suppression comment on a leaked key does not revoke the key.
Suppress inline when: you have context that makes the finding wrong, and that context isn’t visible to the scanner. Bandit flags B108 whenever it sees /tmp paths. If you’re writing a disposable Lambda function that processes one event and exits, /tmp is fine — the scanner can’t know that. Add the suppression and add the reason. The comment is the evidence trail. # noqa: B608 — raw_sql is a static template, never user input is a different suppression than a bare # noqa: B608. One is a decision; the other is a shrug.
Exclude via .brassignore when: the finding fires across a whole path that shouldn’t be in scope at all. Don’t scatter 40 inline suppressions across a test fixture directory. Exclude the directory.
The line between “suppress” and “fix” isn’t always obvious. When it’s not, fix. The triage cost of fixing a false positive is one commit. The production cost of suppressing a real bug is harder to quantify.
.brassignore for Path-Level Exclusion
A well-tuned .brassignore file typically reduces a fresh BrassCoders scan from 1500+ raw findings to around 300 actionable ones — without touching a single finding in production code.
The syntax is gitignore-style. Put the file at the project root. Patterns match relative to the project root by default.
Paths that almost always belong in .brassignore:
# Test fixtures with intentional bad patterns
tests/fixtures/
tests/data/
# Vendor and generated code
vendor/
node_modules/
**/migrations/
# Build artifacts
build/
dist/
.tox/
# Documentation examples with fake credentials
docs/examples/
The decision rule: if you’d tell a new developer “don’t look at findings in that directory, they don’t apply to us,” put it in .brassignore. If you’d tell them “check those findings but with skepticism,” leave the directory in scope and suppress at the line level.
One thing to watch: .brassignore is not a place to bury paths you’re uncomfortable looking at. If you find yourself excluding an src/ subdirectory because the findings there feel overwhelming, that’s a signal — not a .brassignore candidate.
Inline Suppression for Line-Level Exceptions
BrassCoders uses upstream suppression conventions rather than its own — so your existing tooling workflows continue to work, and the suppressions show up naturally in your editor and CI pipeline.
For Bandit rules, the comment is # noqa: BXXX on the flagged line. For Semgrep rules, it’s # nosemgrep. Both go at the end of the line in question.
The format that holds up in code review:
# Suppress with rule ID + one-line reason
result = run_query(raw_sql) # noqa: B608 — raw_sql is a static template, never user input
# Or for Semgrep
data = yaml.load(stream) # nosemgrep — stream is a controlled internal config file, never user-supplied
The reason clause is the load-bearing part. During code review, a teammate sees the suppression and the reasoning simultaneously. If the reasoning is weak (“this is fine”), they can push back before the code ships. The suppression comment is also the artifact you’d check if a similar vulnerability surfaced in production — it tells you exactly what assumption was made and who made it.
Never suppress a CRITICAL with a one-liner. Fix it.
How the Paid Plan Changes Triage
BrassCoders Paid runs a semantic deduplication and reranking pass on your scan output — using an embedding-based pipeline through the BrassCoders gateway — that clusters related findings and surfaces the highest-signal subset for review.
After .brassignore brings a typical scan to around 300 findings, the Paid plan’s enrichment typically surfaces a focused set of 50-80 with context that makes a single triage session tractable. The enrichment doesn’t remove findings from the record; it reorganizes the presentation to put the most important ones front.
The practical workflow: run brasscoders scan ., open .brass/ai_instructions.yaml in your AI assistant (Claude Code, Cursor, Continue — any assistant with file-read access), and start with a prompt like “triage these and tell me the top 5 to fix this sprint.” The YAML is structured for that use case: each finding carries file path, line number, rule ID, severity, and a short description. The AI can read it without hunting through raw scanner JSON.
The honest version: the OSS core with good .brassignore hygiene is a solid starting point. You can work through 300 findings in a few hours if you apply the triage tiers consistently. The Paid plan compresses that session further and surfaces cross-file clustering that’s hard to spot in a flat list. If your team does security triage weekly, the time saved adds up.
Get BrassCoders running in under two minutes: pip install brasscoders and see the install guide.
Frequently Asked Questions
What's the rule on suppressing CRITICAL findings inline?
Never suppress a CRITICAL finding inline. If the finding is genuinely a false positive, document why in writing and bring it to the team as a decision — it's a product risk if you're wrong. Fix the actual issue or, in rare cases, consult the team before touching it. Suppressing CRITICALs quietly is how real bugs ship.
What's the difference between .brassignore and inline suppression?
They operate at different granularities. A .brassignore entry excludes an entire file path or directory from scanning — right for test fixtures, vendor code, generated migrations. An inline suppression (# noqa: B608 or # nosemgrep on the flagged line) excludes one specific line for one specific rule — right for false positives in production code where the context makes the finding wrong.
Do I need BrassCoders Paid to get useful triage output?
No. The OSS core (free, Apache 2.0) with a well-tuned .brassignore file reduces a typical scan from 1500+ raw findings to around 300 actionable ones. BrassCoders Paid then applies semantic deduplication and reranking to that 300, typically surfacing a focused set of 50-80 findings with context suited to a single triage session.
Can I suppress a finding without a code comment explaining why?
Technically yes, but practically no. An unexplained suppression comment is a time bomb. Six months later nobody knows if the suppression was intentional, a laziness call, or a mistake. The convention is: suppression comment plus one-line justification on the same line. That justification shows up in code review, where a teammate can push back if the reasoning is weak.
What finding types should always go to .brassignore rather than inline?
Anything that fires consistently across a whole path for the wrong reason: test fixtures that contain intentional bad patterns for unit-test coverage, vendor directories you don't own and can't change, auto-generated migration files from ORMs, documentation examples that contain fake credentials by design. Path-level exclusions are more honest than scattering inline suppressions across dozens of files.