Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

The ignore file is where security programs go to die

Open the ignore file of any security tool that has been in a codebase for two years. You will find entries nobody can explain. A rule id with no comment. A path that no longer exists. A wildcard somebody added during an

The ignore file is where security programs go to die

Open the ignore file of any security tool that has been in a codebase for two years.

You will find entries nobody can explain. A rule id with no comment. A path that no longer exists.
A wildcard somebody added during an incident. Possibly a whole control switched off, dated by
git blame to a Friday.

None of that happened because the team was careless. It happened because the tool offered exactly
one way to deal with a finding it could not fix today, and that way was to make it disappear.

Accepting risk is not the problem

Every security program accepts risk. A vulnerability with no patch, a rule that is wrong about your
code, a finding in a test fixture that is supposed to look like a secret. Refusing to accept any
risk means never shipping, so the question was never whether. Only whether it is written down.

The problem is the shape of the mechanism. An ignore entry deletes the finding, and a deleted
finding produces a report that is indistinguishable from three very different situations:

Three different situations all produce the same clean line in a report: nothing was found, something was found and excused, or the check never ran.

A deleted finding does not leave a gap shaped like itself. It leaves a report that reads exactly like one with nothing to report.

Every tool means something different by it

There is no shared idea of what an exclusion is. Three tools a normal pipeline runs suppress three
different kinds of thing, in three files, with three syntaxes:

What an entry actually names Expiry Reason Owner
.trivyignore a vulnerability id, everywhere it appears exp:2026-01-01 a comment no
.trivyignore.yaml an id, narrowed by path or package expired_at: statement: no
.semgrepignore files and paths, gitignore-style. Not findings at all none a comment no
.gitleaksignore a fingerprint, commit:path:rule:line none a comment no

Trivy's YAML form is the closest any of them gets, with somewhere to write the reason and a date it
lapses. It is also one tool's file, experimental enough that it is not read unless you name it on
the command line, and it has nowhere to record who decided. The other two cannot express the
decision at all: a Semgrep exclusion is a path glob, so "we accepted this finding" and "do not look
in this directory" are written identically, and a Gitleaks entry is a hash of a finding at a commit,
which stops matching when the file moves.

So a team running four scanners is not maintaining an exceptions policy so much as four lists, in
four formats, that mean four different things and cannot be read together.

And the worst of it is not in a file at all. # nosemgrep and #trivy:ignore put the decision in
the source, on the line it excuses, where no review of accepted risk will ever find it. At best the
scanner notes the excluded finding in a machine format nobody reads, and a #trivy:ignore in an
IaC file leaves no record anywhere. You would have to grep the codebase to know your own policy.

The question you will be asked

The question is not "do you have vulnerabilities?", since everyone does. It is:

This was in the March report and not in June's. What happened to it?

Three possible answers. It was fixed is fine; the diff proves it. Somebody accepted it, here is
who and why
is fine, and is a functioning process. We don't know is the one that costs, and it
is what an ignore file produces by default.

What a decision needs to survive

Four things. The absence of any one turns a decision into a hole:

What it carries What its absence leaves behind
Narrow scope, a rule and a path an exception tied to a whole control is a switch somebody flipped, and it silently covers findings nobody has ever seen
A reason a stranger can evaluate, not "false positive" a reader in two years has no context and cannot ask you, so they re-derive it badly or leave it alone forever
A named owner accountability that belongs to everybody belongs to nobody. A name also tells the next person who to ask, which is often all they need
An expiry risk accepted because no patch exists stops being acceptable the week one ships. Without a date a temporary decision becomes permanent by inaction

The last is the one nearly every tool omits and nearly every program needs, and it is why an entry
outlives the person who wrote it.

Suppress rather than delete

The alternative to an ignore file is not "fix everything" but keeping the finding in the
report, marked, with the decision attached.

The report gets longer. That is the entire cost, and it buys something specific: a passing verdict
that does not pretend, and a list of accepted risks that somebody can review. Because that
is the other half. An exceptions list is a document to be read on a schedule, not a configuration
file that grows.

If nobody has ever removed an entry from yours, it is sediment rather than a list of decisions.

What to check in your own tooling

Three questions, and you can answer them this afternoon:

Ask If the answer is no
Does a suppressed finding still appear anywhere? your reports cannot distinguish clean from quiet
Can an exception carry a reason and an owner in a structured way? you have a comment somebody might have written, which nothing can read or check
Can it expire, and does anything say so when it does? every acceptance is permanent, and you will find out which ones mattered from somebody else

The same decision, excusing a throwaway key in test/fixtures, written in four tools' formats, each recording part of it, converging on one config.exclude entry that records the scope, the reason and when it lapses, with the scan naming who last changed it from its git history.

One decision, the throwaway key in test/fixtures, in each format that can express it and as one config.exclude entry, the only one whose scan says who accepted it.

Draugr's answer is config.exclude: one syntax for every scanner, written in the file that
already describes your scope, so excusing a Trivy finding and a Gitleaks one are the same edit. It
writes down three of the four things above, and the git history of its line records the fourth:

config:
  exclude:
    - paths: ["test/fixtures/"]        # narrow scope, not a whole control
      rules: ["private-key"]
      reason: >-
        The integration test writes a throwaway key so the secrets control has
        something real to find. The material is fake and was never valid anywhere.
      expires: 2026-12-31

An excluded finding stays in the report marked suppressed, carrying its reason and the file and
line of the exclusion. --evidence prints the git history of that line, the last commit to change
it with its author and date, and report.json carries the same for every exclusion. That record is
stronger than a name typed into the entry, which nothing checks and anyone editing the file can
write. It holds to whatever the repository enforces before a change merges, signed commits and
required review included.

The scan says how many findings were suppressed, an exclusion past its expires date stops
suppressing and reports that it lapsed, and one that matched nothing is reported too, because a
rule doing nothing is usually a typo, a rule id that moved, or a finding somebody already fixed and
forgot to stop excusing.

There is also nothing to learn per tool. A new scanner arrives with its own ignore format and its
own semantics, and none of that reaches you, because the exclusion was never expressed in the
scanner's terms. It was expressed in yours, against a rule id and a path, in the descriptor that
already says what your software is.

The exclusions somebody already wrote in a scanner's own terms are kept as well. A # nosemgrep, a
#nosec or a gitleaks:allow beside the line, a .trivyignore or .gitleaksignore line and a
Grype ignore: rule each leave the finding in the report, suppressed and still ranked
into a priority band, P1 to P4, from its severity and the
component's declared exposure and criticality. They are counted under scanner exclusions,
separately from config.exclude, because neither kind is a decision recorded in the descriptor.
Each one says where it was written: origin: tool for a directive in the scanned file, which sends
you to whoever committed the line,
and origin: scanner for the scanner's own configuration, named by file, which sends you to
whoever owns it. An exclusion the scanner records nowhere, such as a #trivy:ignore comment
in an IaC file, cannot be reported by anything, while one written in config.exclude
keeps the finding and its reason in every case.

The same five exclusions grouped by where they were written, which decides who to ask: three directives in the code, one line in the scanner's configuration, one rule in the descriptor.

Every exclusion in one repository, grouped by where it was written, which decides who to ask about it. Only the one in the descriptor says who decided and until when.

None of that decides anything for you. It makes the decision legible, which is the part anyone
asking is after.

The risk acceptance guide covers the discipline, and
your CI logs are not evidence covers what the resulting
report has to carry.

This article was created with the help of AI.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.