← All articles

We pointed Draugr at itself: what happens when a security scanner runs its own gate

July 12, 2026 · Wilson Santos · updated July 15, 2026

The first time we ran Draugr against Draugr the verdict came back red, on our own code, which was the right answer.

A security tool that can't survive its own gate has no business gating anyone else's. So we pointed it at ourselves early and wired it into CI on every commit. A red verdict on day one isn't embarrassing; it's the tool doing its job. What you do with it next is the part that matters.

Describe Draugr, scan Draugr#

The setup is the same descriptor any user writes, checked into the repo as .draugr/self.saga.yaml:

project: draugr
release:
  version: dev
config:
  controllers:
    sca:        # dependency vulnerabilities
      enabled: true
    secrets:    # leaked credentials
      enabled: true
    sast:       # bugs in our own code
      enabled: true
    iac:        # infrastructure misconfig
      enabled: true
components:
  - name: draugr
    repositories:
      - url: .

CI runs this on every change, and, deliberately, with the latest signed release of Draugr, not a build of the current branch. That's how our users run it, so it's how we should run it too. If we ever shipped a regression, the self-scan would be running the released binary that has it.

What it found#

Three of the four controls came back clean: no vulnerable dependencies, no leaked secrets, and no infrastructure to misconfigure. Then sast, Semgrep over our own Go, lit up: three error-level findings and three warnings.

Which is exactly what you want dogfooding to do. Not confirm you're perfect, hand you a pile of findings and make you decide what each one means.

Triage is the actual work#

None of the three errors were bugs. But saying so is a claim you have to defend:

  • A "racy append to a slice" in the engine. Real-looking. We do append to shared slices from concurrent goroutines elsewhere. But this append runs before any goroutine starts, and every concurrent one is mutex-guarded, a false positive.
  • Two "dangerous exec" findings, in the scanner runner and the tool adapter. Executing external tools is literally Draugr's job. The mitigation isn't "don't exec", it's how: no shell (exec.CommandContext, never sh -c), and the argv is built from typed config, not user strings. Accepted by design.

Semgrep wasn't wrong. It surfaced exactly the lines a reviewer should look at. The judgment, real bug, false positive, or accepted risk, is ours to make, and to record.

Suppress out loud#

So we suppressed them, under two rules that keep it honest:

Rule-scoped, never blanket. Each suppression names the specific rule, so the line stays covered for every other rule:

// Executing the configured tool is the point; no shell, argv from typed config.
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...) //nolint:gosec // nosem: ...dangerous-exec-command

With a reason, in the diff. The justification lives next to the code and in git blame. If that line ever does turn dangerous, the suppression is a comment someone has to consciously reconsider, not an invisible hole.

There was a wrinkle worth sharing. Semgrep's nosem doesn't delete a finding; it marks it suppressed in the SARIF output (a suppressions field, per the spec). Draugr was still counting those. So we fixed the reader to honor it: any result a scanner reports as suppressed is dropped, not tallied. That's not a Semgrep special case. It's how SARIF says suppression should work, and now every tool we run gets it for free.

The verdict#

After triage, the self-scan is green, not because Draugr has nothing to find in itself, but because every finding was looked at and either fixed or justified. That's the standard we hold our own code to, and it's the same one the tool enforces for everyone else.

Dogfooding didn't prove Draugr is flawless. It proved something more useful: that when the tool hands you a verdict, there's a real, auditable path from "here's a finding" to "here's why it does or doesn't matter." That path is the whole product.