DEV Community

CopperSunDev
CopperSunDev

Posted on • Originally published at coppersun.dev

Tuning .brassignore: Suppressing a False Positive in Three Steps

BrassCoders scans a file-deduplication script and returns a CRITICAL finding: MD5 used for security. The pattern match is correct — hashlib.md5() without usedforsecurity=False triggers Bandit B324 regardless of context. The problem is that content fingerprinting for deduplication is not a security use of MD5. Here's how to tell BrassCoders that.

The Starting Point: What the Scan Shows

BrassCoders's published N=15 AI-code-findings corpus includes file_dedupe.py, generated from the prompt "Write a script that finds duplicate files in a directory tree by hashing their contents." The model produced a working deduplication script using hashlib.md5():

import os
import hashlib

def find_duplicates(root):
    seen = {}
    duplicates = []
    for dirpath, _, filenames in os.walk(root):
        for name in filenames:
            path = os.path.join(dirpath, name)
            with open(path, "rb") as f:
                digest = hashlib.md5(f.read()).hexdigest()
            if digest in seen:
                duplicates.append((path, seen[digest]))
            else:
                seen[digest] = path
    return duplicates
Enter fullscreen mode Exit fullscreen mode

Running brasscoders --offline scan /path/to/project on a project containing this file produces the following at line 19:

severity: critical
file_path: file_dedupe.py
title: Use of weak MD5 hash for security. Consider usedforsecurity=False
detected_by: bandit
also_detected_by: AstGrepScanner
line_number: 19
code_snippet: |
    with open(path, "rb") as f:
        digest = hashlib.md5(f.read()).hexdigest()
    if digest in seen:
Enter fullscreen mode Exit fullscreen mode

Total findings for the project: 57. Three of those findings are on file_dedupe.py — the B324 CRITICAL, the AstGrepScanner confirmation, and one additional pattern overlap. All three are on the MD5 call.

Why This Finding Is a False Positive in Context

BrassCoders's Bandit scanner fires B324 on any hashlib.md5() call that doesn't explicitly declare usedforsecurity=False. The rule doesn't inspect how the digest is used after the call — whether it's checked against a stored credential, used to sign a token, or compared to another file's fingerprint for deduplication.

Two uses of MD5 are structurally identical in source:

# Authentication use (real vulnerability — MD5 is broken for this)
stored_hash = hashlib.md5(password.encode()).hexdigest()
if stored_hash == db_user.password_hash:  # attacker can find collisions

# Content fingerprinting (not a security use — collision attacks are irrelevant)
digest = hashlib.md5(f.read()).hexdigest()
if digest in seen:  # duplicate detection
Enter fullscreen mode Exit fullscreen mode

MD5 collision attacks matter when an attacker can substitute a different input that produces the same hash — replacing a document with a forged one that has the same signature, or finding a password that matches a stored hash. For deduplication, you're asking "are these bytes identical?" A collision that produces the same hash for different content is irrelevant — the code would incorrectly report two different files as duplicates, not allow unauthorized access.

The scanner correctly reports the pattern. The context — if digest in seen for duplicate detection — makes it a false positive for security purposes.

Two Ways to Clear the Finding

BrassCoders surfaces the finding so you can decide: fix the code or suppress it. Both clear the finding from scan output.

Option 1 — Code fix (preferred when you own the file):

Python 3.9 added the usedforsecurity parameter to explicitly declare that a hash function is not being used for security purposes:

digest = hashlib.md5(f.read(), usedforsecurity=False).hexdigest()
Enter fullscreen mode Exit fullscreen mode

With this flag, Bandit B324 no longer fires. The intent is documented in the source itself — any future reader, scanner, or AI reviewer sees the declaration. This is the right call when you control the file.

Option 2 — .brassignore glob (when you can't change the file):

If file_dedupe.py is a benchmark corpus file, vendored code, or a third-party script you're scanning but not modifying, add a glob rule to .brassignore at the project root:

# file_dedupe.py uses MD5 for content deduplication only — not a security use.
# usedforsecurity=False would be the code fix; suppressed here because this
# is a benchmark corpus reference file, not production code we own.
file_dedupe.py
Enter fullscreen mode Exit fullscreen mode

Bare filenames in .brassignore match that file at any depth in the project tree. No anchoring needed.

The Re-Scan: From 57 to 54

After adding file_dedupe.py to .brassignore, the re-scan shows:

🙈 .brassignore: dropped 3 findings (57 → 54)
Enter fullscreen mode Exit fullscreen mode

All three findings tied to file_dedupe.py are gone. The remaining 54 findings are from other files in the project — real bugs including SQL injection, command injection via shell=True, and unsafe subprocess calls that warrant review. The MD5 pattern no longer appears in the triage queue.

BrassCoders reads .brassignore before the noise-reduction pass. Suppressed findings never enter the ranking pipeline. For the Paid plan, they also never reach the enrichment step — suppressed findings don't consume enrichment tokens.

The CLI message is the confirmation to check for. If the message doesn't appear, either the .brassignore file wasn't found at the project root or the glob rule didn't match the finding's file path. The --offline flag goes before scan in the command:

brasscoders --offline scan /path/to/project
Enter fullscreen mode Exit fullscreen mode

When to Suppress vs When to Fix

BrassCoders's .brassignore is the right tool for findings in code you don't own or can't change. The decision is straightforward:

Fix the code when you own it. The usedforsecurity=False parameter is the explicit declaration that removes the finding and documents intent. A future reader — or another scanner — sees the decision in the source without needing to cross-reference a suppression file.

Use .brassignore when you can't change the file. Vendored libraries, auto-generated code, third-party integrations, and benchmark corpus reference files are all cases where you've verified the finding is a false positive but can't or shouldn't modify the source. The glob rule suppresses the finding without touching the file.

For patterns that appear across many files — a privacy rule, a Semgrep check ID — .brassignore supports type rules via the :rule_id syntax. Semgrep findings store their check ID as rule_id in metadata; the type rule :brass.python.taint.sql-injection suppresses BrassCoders's SQL injection taint rule project-wide. Bandit findings store the test ID as bandit_test_id rather than rule_id, so per-file glob rules are the supported suppression path for Bandit.

The fuller catalog of .brassignore patterns for Django projects, FastAPI projects, and test fixture directories is at /blog/tuning-brasscoders-brassignore/. The distinction between .brassignore and .gitignore — two independent files with different jobs — is at /blog/what-is-the-difference-between-brassignore-and-gitignore/.

pip install brasscoders
brasscoders --offline scan /path/to/project
# Review findings. For confirmed false positives: fix the code or add to .brassignore.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)