<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wilson Santos</title>
    <description>The latest articles on DEV Community by Wilson Santos (@wilson-draugr).</description>
    <link>https://dev.to/wilson-draugr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4145879%2F7b1b59ad-111c-426b-a7f7-1eab66b38621.jpg</url>
      <title>DEV Community: Wilson Santos</title>
      <link>https://dev.to/wilson-draugr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wilson-draugr"/>
    <language>en</language>
    <item>
      <title>The ignore file is where security programs go to die</title>
      <dc:creator>Wilson Santos</dc:creator>
      <pubDate>Tue, 10 Nov 2026 04:05:22 +0000</pubDate>
      <link>https://dev.to/wilson-draugr/the-ignore-file-is-where-security-programs-go-to-die-4nnd</link>
      <guid>https://dev.to/wilson-draugr/the-ignore-file-is-where-security-programs-go-to-die-4nnd</guid>
      <description>&lt;p&gt;Open the ignore file of any security tool that has been in a codebase for two years.&lt;/p&gt;

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

&lt;p&gt;None of that happened because the team was careless. It happened because the tool offered exactly&lt;br&gt;
one way to deal with a finding it could not fix today, and that way was to make it disappear.&lt;/p&gt;
&lt;h2&gt;
  
  
  Accepting risk is not the problem
&lt;/h2&gt;

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

&lt;p&gt;The problem is the shape of the mechanism. An ignore entry deletes the finding, and a deleted&lt;br&gt;
finding produces a report that is indistinguishable from three very different situations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuldsg9zje7e2xvncq6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuldsg9zje7e2xvncq6t.png" alt="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." width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A deleted finding does not leave a gap shaped like itself. It leaves a report that reads exactly like one with nothing to report.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Every tool means something different by it
&lt;/h2&gt;

&lt;p&gt;There is no shared idea of what an exclusion is. Three tools a normal pipeline runs suppress three&lt;br&gt;
different kinds of thing, in three files, with three syntaxes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What an entry actually names&lt;/th&gt;
&lt;th&gt;Expiry&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.trivyignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a vulnerability id, everywhere it appears&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exp:2026-01-01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a comment&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.trivyignore.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an id, narrowed by path or package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expired_at:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statement:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.semgrepignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;files and paths&lt;/strong&gt;, gitignore-style. Not findings at all&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;a comment&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.gitleaksignore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;a fingerprint&lt;/strong&gt;, &lt;code&gt;commit:path:rule:line&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;a comment&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

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

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

&lt;p&gt;The question is not &lt;em&gt;"do you have vulnerabilities?"&lt;/em&gt;, since everyone does. It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This was in the March report and not in June's. What happened to it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three possible answers. &lt;em&gt;It was fixed&lt;/em&gt; is fine; the diff proves it. &lt;em&gt;Somebody accepted it, here is&lt;br&gt;
who and why&lt;/em&gt; is fine, and is a functioning process. &lt;em&gt;We don't know&lt;/em&gt; is the one that costs, and it&lt;br&gt;
is what an ignore file produces by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a decision needs to survive
&lt;/h2&gt;

&lt;p&gt;Four things. The absence of any one turns a decision into a hole:&lt;/p&gt;

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

&lt;p&gt;The last is the one nearly every tool omits and nearly every program needs, and it is why an entry&lt;br&gt;
outlives the person who wrote it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Suppress rather than delete
&lt;/h2&gt;

&lt;p&gt;The alternative to an ignore file is not "fix everything" but keeping the finding &lt;strong&gt;in&lt;/strong&gt; the&lt;br&gt;
report, marked, with the decision attached.&lt;/p&gt;

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

&lt;p&gt;If nobody has ever removed an entry from yours, it is sediment rather than a list of decisions.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to check in your own tooling
&lt;/h2&gt;

&lt;p&gt;Three questions, and you can answer them this afternoon:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;If the answer is no&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Does a suppressed finding still appear anywhere?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;your reports cannot distinguish clean from quiet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can an exception carry a reason and an owner in a structured way?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;you have a comment somebody might have written, which nothing can read or check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can it expire, and does anything say so when it does?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;every acceptance is permanent, and you will find out which ones mattered from somebody else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktllte22ttvmqrrpsru0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktllte22ttvmqrrpsru0.png" alt="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." width="799" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test/fixtures/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# narrow scope, not a whole control
&lt;/span&gt;      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"private-key"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;
        &lt;span class="s"&gt;The integration test writes a throwaway key so the secrets control has&lt;/span&gt;
        &lt;span class="s"&gt;something real to find. The material is fake and was never valid anywhere.&lt;/span&gt;
      &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2026-12-31&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffiit2i8cpjgw5icm3xqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffiit2i8cpjgw5icm3xqj.png" alt="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." width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of that decides anything for you. It makes the decision legible, which is the part anyone&lt;br&gt;
asking is after.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://draugr.dev/learn/risk-acceptance-and-exceptions/" rel="noopener noreferrer"&gt;risk acceptance guide&lt;/a&gt; covers the discipline, and&lt;br&gt;
&lt;a href="https://draugr.dev/blog/your-ci-logs-are-not-evidence/" rel="noopener noreferrer"&gt;your CI logs are not evidence&lt;/a&gt; covers what the resulting&lt;br&gt;
report has to carry.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>devsecops</category>
      <category>compliance</category>
    </item>
    <item>
      <title>What to fix first when everything is critical</title>
      <dc:creator>Wilson Santos</dc:creator>
      <pubDate>Thu, 01 Oct 2026 20:31:45 +0000</pubDate>
      <link>https://dev.to/wilson-draugr/what-to-fix-first-when-everything-is-critical-2fh0</link>
      <guid>https://dev.to/wilson-draugr/what-to-fix-first-when-everything-is-critical-2fh0</guid>
      <description>&lt;p&gt;Run any scanner across a real system and you will get the same shape of result, a wall of&lt;br&gt;
findings, most of them stamped "high" or "critical." Those labels come from&lt;br&gt;
&lt;strong&gt;&lt;a href="https://draugr.dev/learn/cve-cvss-and-severity/" rel="noopener noreferrer"&gt;CVSS&lt;/a&gt;&lt;/strong&gt;, the Common Vulnerability Scoring System, a 0 to 10 scale&lt;br&gt;
published with most vulnerabilities. The word is a band on that scale, and the bands are defined by&lt;br&gt;
FIRST in the &lt;a href="https://www.first.org/cvss/v3.1/specification-document" rel="noopener noreferrer"&gt;CVSS v3.1 specification&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The label&lt;/th&gt;
&lt;th&gt;The score&lt;/th&gt;
&lt;th&gt;What it took to earn it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Critical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.0 to 10.0&lt;/td&gt;
&lt;td&gt;reachable over a network, by somebody with no account, needing nothing from a user, and it takes confidentiality, integrity and availability together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.0 to 8.9&lt;/td&gt;
&lt;td&gt;most of the above, with one condition attached. An account is needed, or the attack is not straightforward, or one of the three impacts is spared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.0 to 6.9&lt;/td&gt;
&lt;td&gt;several conditions at once, or a partial impact. The bulk of what a scanner returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Low&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.1 to 3.9&lt;/td&gt;
&lt;td&gt;local access, or heavy prerequisites, or an impact that is real and narrow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a useful thing to know and it is not a statement about you. The specification is explicit&lt;br&gt;
about it: a base score reflects severity &lt;em&gt;"assuming the reasonable worst case impact across&lt;br&gt;
deployed environments"&lt;/em&gt;. So &lt;code&gt;AV:N/PR:N&lt;/code&gt;, the pair that makes a 9.8, is a claim that &lt;em&gt;somebody's&lt;/em&gt;&lt;br&gt;
deployment exposes this to the internet without authentication. It is not a claim that yours does.&lt;/p&gt;

&lt;p&gt;So the score did its job: it told you how severe each issue is in the abstract. What it did not tell&lt;br&gt;
you is the only thing you need on Monday morning: &lt;strong&gt;which one do I fix first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Severity is not priority. And the gap between them is where security programs quietly fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why severity can't rank your work
&lt;/h2&gt;

&lt;p&gt;CVSS scores a vulnerability in isolation, as if every place it appears were identical. But&lt;br&gt;
the same CVE is not the same risk everywhere:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The same CVE, sitting on&lt;/th&gt;
&lt;th&gt;What it actually is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;an internet-facing service with no auth&lt;/td&gt;
&lt;td&gt;a live attack path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an internal tool three network hops away, behind a policy&lt;/td&gt;
&lt;td&gt;a someday-maybe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the component that takes the platform with it when it falls over&lt;/td&gt;
&lt;td&gt;an emergency, whatever the score says&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Feed all of that into one severity number and you get the two failure modes every team knows:&lt;br&gt;
either you chase &lt;em&gt;everything&lt;/em&gt; marked critical and burn out, or you stop believing the labels&lt;br&gt;
and route around the gate. Both end in the same place, with the urgent thing sitting in a&lt;br&gt;
backlog next to a hundred things that look exactly as scary.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two questions a scanner can't answer
&lt;/h2&gt;

&lt;p&gt;To rank findings you need two pieces of context that no scanner can compute, because they&lt;br&gt;
aren't properties of the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How exposed is it?&lt;/strong&gt; Reachable from the internet, or namespace-scoped behind a network policy? This is &lt;strong&gt;likelihood&lt;/strong&gt;, meaning how reachable the weakness is at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much would it cost?&lt;/strong&gt; A platform-wide outage, or a dev tool nobody would notice for a week? This is &lt;strong&gt;impact&lt;/strong&gt;, meaning what it costs when it goes wrong.&lt;/p&gt;

&lt;p&gt;Real risk is the product of the two, layered on top of raw severity. A scanner sees none of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  But your app description does
&lt;/h2&gt;

&lt;p&gt;You already know this context. You know which services face the world and&lt;br&gt;
which are buried. You know which ones are load-bearing. That knowledge is stable, small, and&lt;br&gt;
exactly the kind of thing &lt;a href="https://draugr.dev/blog/describe-your-app-not-your-scanners/" rel="noopener noreferrer"&gt;Draugr already captures&lt;/a&gt;&lt;br&gt;
in the app descriptor.&lt;/p&gt;

&lt;p&gt;So it becomes two more attributes on a component you already describe, its &lt;strong&gt;exposure&lt;/strong&gt; and its&lt;br&gt;
&lt;strong&gt;business criticality&lt;/strong&gt;, and prioritization falls out of the description you wrote once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress-gateway&lt;/span&gt;
    &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;          &lt;span class="c1"&gt;# reachable from the internet
&lt;/span&gt;    &lt;span class="na"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;     &lt;span class="c1"&gt;# failure = platform-wide outage
&lt;/span&gt;  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-dashboard&lt;/span&gt;
    &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restricted&lt;/span&gt;      &lt;span class="c1"&gt;# internal, network-scoped
&lt;/span&gt;    &lt;span class="na"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supporting&lt;/span&gt;   &lt;span class="c1"&gt;# nobody pages at 3am for this
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhweb4r6ahx4ngu7xtnl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhweb4r6ahx4ngu7xtnl1.png" alt="One CVE of identical severity resolves to priority P1 on a public, business-critical gateway and P3 on a restricted, supporting dashboard." width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same finding, same severity, two answers, because the components differ.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the &lt;em&gt;same&lt;/em&gt; CVE resolves two different ways, so on the gateway it is fix-now and top of the list.&lt;br&gt;
On the dashboard it is track-and-move-on. Severity was identical and &lt;strong&gt;priority was not&lt;/strong&gt;, because&lt;br&gt;
&lt;a href="https://draugr.dev/learn/vulnerability-prioritization/" rel="noopener noreferrer"&gt;priority&lt;/a&gt; combines the finding's severity with the&lt;br&gt;
component's exposure and criticality into a single, ordered answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  The third input, whether anyone is exploiting it
&lt;/h2&gt;

&lt;p&gt;Exposure and criticality describe your side. There's one more signal, and it comes from the&lt;br&gt;
world: &lt;strong&gt;exploitability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A CVE on &lt;a href="https://draugr.dev/learn/epss-and-kev/" rel="noopener noreferrer"&gt;CISA's KEV catalog&lt;/a&gt; is &lt;em&gt;known to be exploited in the wild&lt;/em&gt;, not&lt;br&gt;
theoretically exploitable, and used against real systems. That escalates a finding whatever&lt;br&gt;
its CVSS score says, because a modest score being exploited today outranks a 9.8 nobody has ever&lt;br&gt;
weaponized. A high &lt;a href="https://draugr.dev/learn/epss-and-kev/" rel="noopener noreferrer"&gt;EPSS&lt;/a&gt; probability, the likelihood a CVE gets exploited&lt;br&gt;
in the next thirty days, raises a finding's severity one level on the same logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl2tjwu445rm02jp9zqf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl2tjwu445rm02jp9zqf.png" alt="Two findings reorder when exploitability is taken into account: a medium being exploited today outranks a critical nobody has weaponized." width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The severity did not change and neither did the code. What changed is that one of them is being used, which is a fact about the world rather than about your repository, and it is the input almost nobody feeds in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both are opt-in and read from feeds you provide, so the ranking stays reproducible, because the same&lt;br&gt;
inputs give the same order, and you can point at where each escalation came from.&lt;/p&gt;
&lt;h2&gt;
  
  
  An order is still a list, until it names the work
&lt;/h2&gt;

&lt;p&gt;Ranking gets the right thing to the top and shortens nothing: four hundred findings ranked are&lt;br&gt;
still four hundred rows, and the first twelve are often one library. So the last step groups them&lt;br&gt;
by the thing that fixes them, into one list for each kind of work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;$ draugr scan . --view actions --top 4

WHAT TO DO  49 actions clear 1238 findings

Update an image you &lt;span class="k"&gt;do&lt;/span&gt; not build  1 action clears 605 findings
  P1  Update python:3.8-slim
      component api · control images · 605 findings · upstream · CVE-2025-15467 +604

Bump a dependency  10 actions clear 38 findings
  P1  Upgrade jquery 1.8.3 → 1.12.2
      component storefront · control sca · 4 findings · web/package-lock.json:12 · and 1 more
  P1  Upgrade jquery 1.12.2 → 3.5.0  major · beyond policy minor
      component storefront · control sca · 5 findings · web/package-lock.json:12 · and 1 more
  P1  Upgrade jquery 1.8.3 → 3.5.0  major · beyond policy patch
      component api · control sca · 7 findings · …/js/jquery.min.js vendored · CVE-2020-11023 +6
  P1  Upgrade Jinja2 2.10 → 2.10.1
      component api · control sca · 1 finding · app/requirements.txt:5 · CVE-2019-10906
  … and 6 actions not listed

Change the code  14 actions clear 23 findings
  P1  Run the &lt;span class="nb"&gt;command&lt;/span&gt; as an argument list, without a shell
      Found user-controlled data used &lt;span class="k"&gt;in&lt;/span&gt; a system call
      Semgrep: python.lang.security.dangerous-system-call.dangerous-system-call · Bandit: B605
      component api · control sast · 2 findings · app/app.py:16
  P1  Run the &lt;span class="nb"&gt;command&lt;/span&gt; as an argument list, without a shell
      subprocess call with &lt;span class="nv"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;True identified, security issue
      Bandit: B602 · and 3 more
      component api · control sast · 4 findings · app/app.py:23
  P1  Turn off debug mode outside development
      Also: Bind a specific host unless the server must be reachable from outside
      Semgrep: python.flask.security.audit.debug-enabled.debug-enabled · Bandit: B201 · and 2 more
      component api · control sast · 4 findings · app/app.py:35
  P1  Remove &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;, or check no outside data reaches it
      Found user data &lt;span class="k"&gt;in&lt;/span&gt; a call to &lt;span class="s1"&gt;'eval'&lt;/span&gt;
      Semgrep: python.django.security.injection.code.user-eval.user-eval
      component api · control sast · 1 finding · app/app.py:29
  … and 10 actions not listed
  ⋯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the demo project, where 49 actions clear 1,238 findings. Each kind of work has its own&lt;br&gt;
list, so whoever does one kind reads one list. The first is an image the team does not build:&lt;br&gt;
&lt;code&gt;upstream&lt;/code&gt; comes from the descriptor, which declares &lt;code&gt;python:3.8-slim&lt;/code&gt; &lt;code&gt;builtBy: upstream&lt;/code&gt;, so the&lt;br&gt;
action is to take a newer image rather than to upgrade a package nobody there can reach, and that&lt;br&gt;
one move clears 605 findings. Then come the upgrades, the lines of code to change, the&lt;br&gt;
infrastructure settings and the credential to rotate. The jquery rows are one library in two&lt;br&gt;
components, each with its own owner, and &lt;code&gt;vendored&lt;/code&gt; marks a copy committed to the tree, which&lt;br&gt;
bumping the lockfile leaves as it was.&lt;/p&gt;

&lt;p&gt;A row is a &lt;strong&gt;remediation&lt;/strong&gt;, not a finding. Six vulnerabilities in one library are one upgrade; the&lt;br&gt;
same misconfiguration in three Dockerfiles is one habit. Listing those separately makes the&lt;br&gt;
repetitive work crowd out everything else, which is how a ranked list still ends up unread.&lt;/p&gt;

&lt;p&gt;Within each list, actions are ordered by &lt;strong&gt;the worst thing each one clears&lt;/strong&gt;, and by how many only&lt;br&gt;
at the end. An action clearing a single P1 outranks one clearing forty P4s, because volume is not a&lt;br&gt;
reason to do something first and a P1 is not something to trade away for a bigger number. Inside a&lt;br&gt;
band, an action clearing a flaw on KEV comes first, then the higher severity, and only then the&lt;br&gt;
count. So in the code list, the shell call built from user input clears two critical findings and&lt;br&gt;
sits above the &lt;code&gt;shell=True&lt;/code&gt; call that clears four high ones.&lt;br&gt;
&lt;a href="https://draugr.dev/learn/what-to-fix-first/" rel="noopener noreferrer"&gt;What to fix first&lt;/a&gt; walks through every key, the upgrade policy behind&lt;br&gt;
&lt;code&gt;beyond policy&lt;/code&gt;, and where the report records each one.&lt;/p&gt;

&lt;p&gt;Grouping is opt-in while that annotation spreads: &lt;code&gt;--view actions&lt;/code&gt; turns it on, and the default&lt;br&gt;
still lists one finding per row. The report files always carry findings separately either way, so&lt;br&gt;
an auditor reading the SARIF sees one record per finding whichever way the console was asked to&lt;br&gt;
show them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that changes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For a developer
&lt;/h3&gt;

&lt;p&gt;The output stops being a 400-row dashboard and becomes a short list: &lt;em&gt;these three, in this order, today.&lt;/em&gt; Clear action, not triage archeology.&lt;/p&gt;

&lt;h3&gt;
  
  
  For the business
&lt;/h3&gt;

&lt;p&gt;Consistent and defensible, because the same rules apply to every service. "What is our real exposure right now?" has an answer, and the urgent gets a faster response because it is no longer buried under look-alikes.&lt;/p&gt;

&lt;h3&gt;
  
  
  For your coding assistant
&lt;/h3&gt;

&lt;p&gt;The difference between a list and an order. Point one &lt;a href="https://draugr.dev/blog/your-assistant-is-already-answering/" rel="noopener noreferrer"&gt;at Draugr&lt;/a&gt; and it answers from the same ranking your pipeline uses. A scanner can hand it a pile of findings; only your description says which one to fix.&lt;/p&gt;

&lt;p&gt;This is what Draugr does: a gate that tells you the few things that matter, ranked by your own&lt;br&gt;
context. Describe your app, and&lt;br&gt;
the description doesn't just decide which scanners run. It decides what you fix first.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>appsec</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
