← All guides

Risk acceptance: the exception nobody can explain later

Compliance & governance
On this page

In plain terms

Every security program accepts risk. Not as a failure, as the only way to ship anything. A finding you cannot fix this sprint, a vulnerability in a dependency with no patch, a rule that is wrong about your code: each one ends in somebody deciding not now or not here.

Risk acceptance is that decision, made deliberately and written down. An exception is the record of it.

The distinction that matters is not between accepting risk and not accepting it, but between accepting it on the record and accepting it by making the finding go away.

Why it matters

An auditor, an acquirer's counsel or your own successor will eventually look at a scan that passed and ask a question you must be able to answer:

This finding was reported in March and is not in the June report. What happened to it?

There are three possible answers, and only one of them is comfortable.

  • It was fixed. Fine. The evidence is the diff.
  • Somebody accepted it, here is who and why. Also fine. That is a functioning process.
  • We do not know. That is the answer that costs you, and it is what happens by default.

The third answer is the normal outcome of most tooling, because most tooling offers you an ignore-list and no memory.

Suppress, don't delete

The two look identical in a passing report and are opposites in an audit.

Deleting

a finding, an ignore file, a baseline, a rule switched off, removes it from the output. What reaches the reader is a report with nothing in it, which is indistinguishable from a scan that found nothing, which is indistinguishable from a scan that did not run. Three very different situations, one output.

Suppressing

it keeps the finding in the report, marked, carrying the reason somebody gave. The report says: this was found, this is who decided it was acceptable, this is why. It still passes the gate. It just does not pretend.

The cost of suppressing is that your report is longer. The cost of deleting is that you cannot reconstruct a decision, and the people who ask are the ones you least want to say "we don't know" to.

What a defensible exception carries

What an expiry does that a comment cannot An exception carries a reason, an owner and a date. Until that date the finding is suppressed, still present in the report, and the gate passes. On the day after, it is counted again and the report says the exclusion lapsed rather than letting the finding reappear unexplained. Nobody has to remember. WHAT THE REPORT SAYS, OVER TIME you accept it a reason, a name, a date the date passes nobody kept a calendar Suppressed, and still in the report The gate passes. The finding is marked, carrying your reason, so an auditor can still see what you decided. Counted again, by itself The report says the exclusion lapsed, so it does not reappear unexplained. An entry in an ignore file has no second half: it stays until somebody deletes it, and nobody does.
The expiry is the field nearly every tool omits, and it is the one that turns a decision into something with an end rather than a permanent condition adopted by accident.

Four things, and the absence of any one is what turns an exception into a hole:

It carriesWithout it
1What it applies to, narrowlyan exception scoped to a rule and a path is a decision. One scoped to a whole control is a switch somebody flipped
2A reason a stranger can evaluatenot "false positive" but why it is one. The reader in two years has no context and cannot ask you
3Who accepted ita named person, because accountability that belongs to everybody belongs to nobody
4An expiryrisk accepted on the basis of "no patch exists" stops being acceptable the week one does. An exception with no end date is a permanent decision made under temporary circumstances

The fourth is the one most tools omit and most programs need. An accepted risk that nobody revisits is not a decision; it is a decision's fossil.

Where this differs from a bug

A bug is a thing to fix. An accepted risk is a thing to review. That difference should be visible in your process: the exceptions list is a document somebody reads on a schedule, not a configuration file that grows.

If nobody has ever removed an entry from your exception list, it is not a list of decisions. It is sediment.

How Draugr fits

Draugr's config.exclude is built on this: an excluded finding stays in the report, marked suppressed, carrying its reason.

config:
  exclude:
    - paths: ["test/fixtures/"]
      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

The scan output says how many findings were suppressed, so a passing verdict never hides the fact that a judgement was made. An exclusion past its expiry stops suppressing and says so, 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.

The repository records who accepted it. --evidence prints each exclusion that set findings aside with its file and line and the git history of that line, the last commit to change it, its author and its date:

DECISIONS
  The integration test writes a throwaway key so the secrets control has something real to…
  draugr.saga.yaml:13 · covers 1 finding · private-key · expires 2026-12-31
  last changed in 9a70069be00c by A. Engineer, 2026-10-30

That record is stronger than a name typed into the descriptor, which nothing checks and anyone editing the file can write. The commit is the repository's own record of the change, and it holds to whatever the repository enforces before a change merges, signed commits and required review included. report.json carries the same history for every exclusion, and where a shallow clone holds too little history to be sure, it says the history is unavailable rather than crediting whoever made the clone's oldest commit.

When somebody else made the decision

Not every acceptance is yours. A supplier ships a component and a document saying which of its vulnerabilities do not affect it, and the tempting move is to copy their conclusion into your own exclusions, where it stops being theirs.

That is the one thing worth avoiding. The question at the top of this page is who decided, and a retyped not_affected answers it with your name. Draugr reads a supplier's VEX document directly instead, so the claim keeps its author, its source and the date it was asserted:

components:
  - name: payments-api
    vex:
      - path: vendor/payments-sdk.openvex.json
ACCEPTED
  config.exclude  1 finding suppressed
  VEX             1 finding excused

and, under --evidence, whose decision each one was:

ACCEPTED
  config.exclude  1 finding suppressed
  VEX             1 finding excused · 1 asserted by Example Payments SDK <security@example.com>

The VEX row names the supplier who wrote the statement, and the exclusion's entry under DECISIONS names the commit somebody here made, which is the distinction the whole section turns on.

Accepting a supplier's analysis is still an acceptance, and you are still the one who chose to accept it. What changes is that the record says whose analysis it was, so the question can go to the party who did the work.

None of that makes the decision for you. It makes the decision legible, which is the part an auditor is asking about.