↓ Skip to main content
Security

Learning OSV, part 5: I scanned my own blog, and the real problem wasn't in the report

Learning OSV - This article is part of a series.
Part 5: This Article
A test project is a controlled experiment; your own repository is the one that talks back. I pointed OSV-Scanner at the source of this blog expecting a dull report. The report was indeed dull. The interesting part was what it could not see.
Anime-style illustration: an excited young experimenter in a lab coat, with goggles pushed up on the forehead, holds a glowing green flask high at a lab bench and says “Let’s try it!”
The mood of the evening. It lasted until the first command.

In parts 2 and 3 I used a project built to be vulnerable. This time the target is the repository behind this site: Hugo, the Blowfish theme as a git submodule, some Python helper scripts and two GitHub Actions workflows. All runs used OSV-Scanner v2.7.0.

Setup, and the first surprise
#

The setup is one binary, checked against its published checksums as in part 2. Then the obvious command, from the root of the repository:

osv-scanner scan source .
No package sources found, --help for usage information.

Exit code 128. A site like this has no package.json of its own, and without -r the scanner looks only at the top folder. The docs define 128 as “no packages found”. Worth remembering: a scan that looked at nothing is not the same as a clean scan.

osv-scanner scan source -r .
Scanned themes/blowfish/go.mod file and found 1 package
Scanned themes/blowfish/package-lock.json file and found 272 packages
Scanned themes/blowfish/scripts/package-lock.json file and found 179 packages

Total 6 packages affected by 8 known vulnerabilities
(1 Critical, 2 High, 4 Medium, 0 Low, 1 Unknown) from 1 ecosystem.

That took under four seconds. Every lockfile it found belongs to the theme.

Reading the 8 findings
#

PackageVersionTagAdvisoriesUsed by
dompurify3.4.16dev3, top CVSS 9.2mermaid, the theme’s diagram library
glob13.0.6, 10.5.0dev, optional1 in each lockfileTheme tooling
minimatch10.2.6, 9.0.9dev, optional1 in each lockfileTheme tooling
zod3.25.76dev1Theme tooling

My first instinct was to wave all of it away, since it says dev. My deploy workflow never runs npm: Hugo builds from files the theme has already committed. So glob, minimatch and zod never run anywhere I control. Those five rows really can wait.

DOMPurify was different, and this is where I started reading rather than sorting.

dev does not mean “does not ship”
#

The theme lists mermaid as a dev dependency, then keeps a built copy of it in assets/lib/mermaid/. That file goes to readers’ browsers on the six posts of mine with Mermaid charts. So I searched inside it:

"DOMPurify");if(t.version="3.4.12"

The bundle carries DOMPurify 3.4.12. The lockfile says 3.4.16. Asking the OSV API about each version:

VersionWhereKnown advisories
3.4.16The lockfile, what the scanner reported3
3.4.12The bundle, what readers actually get5

So the label was wrong for my situation and the count was low. Is it urgent? I don’t think so. All five advisories need options such as IN_PLACE, SAFE_FOR_TEMPLATES or an unusual PARSER_MEDIA_TYPE (example). In the minified bundle those names appear only where DOMPurify reads its own configuration, my theme’s setup passes no custom config, and the only diagrams on the page are ones I wrote. That is my reading of minified code, not proof, so I am keeping it on a short review date rather than dismissing it.

I am already on the latest theme release (v3.9.0), so the fix has to come from upstream. osv-scanner fix confirmed the trap: on a copy of the lockfile it could fix 3 of 6 by moving dompurify to 3.4.17, but that changes the lockfile, not the bundle. The report would go green and readers would get exactly the same file.

The finding that was not in the report
#

Then I asked what the scanner never looked at. One of my slide decks loads its libraries straight from a CDN:

<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>

A <script> tag is not a manifest, so this version never appears in a scan. I asked the API anyway:

curl -s -X POST https://api.osv.dev/v1/query \
  -d '{"package":{"name":"mermaid","ecosystem":"npm"},"version":"11.4.1"}'

Nine known vulnerabilities, in a library I picked myself. Two of them are cross-site scripting. And having just learnt to look inside bundles, I looked inside this one: it carries DOMPurify 3.2.1, which has another 23. The deck also set securityLevel: "loose", which by Mermaid’s definition allows HTML tags in diagram text. Again the input is only my own diagrams, so the real risk is small, but this was the one problem entirely mine to fix, and the scanner reported nothing.

Here is the whole picture as I found it:

What runsDoes the scan see it?Status
Theme toolingYes, 5 rowsNever runs in my pipeline
DOMPurify in the Mermaid bundleYes, with the wrong version and tagLow risk; needs a theme release
mermaid@11.4.1 from a CDNNo9 advisories, plus 23 in its DOMPurify; mine to fix
Python helpers (Pillow 12.1.1, PyYAML)No, no lockfilePillow has 59 advisories; run only on my machine, on images I made
GitHub Actions in the workflowsNo, in this runKept fresh by Dependabot
Hugo 0.167.0NoAPI shows none for the Go module

Making the invisible visible
#

The scanner reads SBOMs, so I can tell it what it cannot find. An SBOM (software bill of materials) is just a list of components. I wrote a small CycloneDX file by hand:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "components": [
    { "type": "library", "name": "mermaid", "version": "11.4.1", "purl": "pkg:npm/mermaid@11.4.1" },
    { "type": "library", "name": "reveal.js", "version": "5.1.0", "purl": "pkg:npm/reveal.js@5.1.0" },
    { "type": "library", "name": "dompurify", "version": "3.4.12", "purl": "pkg:npm/dompurify@3.4.12" }
  ]
}

Named site-libs.cdx.json, it was picked up by -r with no extra flag, and the scan then reported the Mermaid and theme-bundle DOMPurify findings: 14 in all. The catch is obvious: it is a list I maintain by hand, and it is only as true as my memory of updating it.

For Python, a pip freeze into a requirements.txt works the same way; the docs list that format.

Setting it up for this repository
#

Two details here were specific to my repository, and I found both by testing.

Accepted risks need a root-level config. An osv-scanner.toml applies only to its own folder. My findings live in a submodule I don’t own, so a file there is not an option. Passing one explicitly works:

[[IgnoredVulns]]
id = "GHSA-3g3m-2fjr-6pw4"
ignoreUntil = 2027-02-01
reason = "glob: theme tooling only; my pipeline never runs npm"
osv-scanner scan source -r --config=osv-scanner.toml .
GHSA-3g3m-2fjr-6pw4 and 1 alias have been filtered out because: glob: theme tooling only; my pipeline never runs npm

The stock GitHub workflow would see nothing. The reusable workflow has a checkout-submodules input that defaults to false. I tested the effect with a clean clone: no submodule, no lockfiles, exit 128. So my workflow needs it switched on:

name: OSV-Scanner Scheduled Scan
on:
  schedule:
    - cron: "30 12 * * 1"
  push:
    branches: [main]
permissions:
  actions: read
  security-events: write
  contents: read
jobs:
  scan-scheduled:
    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.7.0"
    with:
      checkout-submodules: true
      scan-args: |-
        --recursive
        --config=./osv-scanner.toml
        ./

I have tested the commands on my machine, but this workflow has not run on GitHub yet, so treat it as my plan rather than a proven recipe.

How I fixed the deck
#

It looked like a one-line version bump. It took four attempts, and my checks passed on the ones that were wrong.

AttemptMermaidIts DOMPurifyAdvisoriesWhat happened
Starting point11.4.13.2.19 + 23Worked, but vulnerable
1. Latest version12.3.03.4.170 + 0Every chart rendered, but labels wrapped early and layouts changed
2. Last 11.x, to keep the layouts11.17.23.4.120 + 5Looked right; DOMPurify advisories remained
3. 12.3.0 with the old layout engine12.3.03.4.170 + 0Flowcharts right; the mind map collapsed into a flat line
4. Plus a layout for the mind map12.3.03.4.170 + 0All six charts right

The Mermaid 12 release notes explain attempts 1 and 3: version 12 makes ELK the default layout engine and changes the default look, and you can opt out with layout: dagre. They also say a mind map keeps its own layout “unless a layout is explicitly requested”, and my global setting had requested one.

The final change:

-<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/mermaid@12.3.0/dist/mermaid.min.js"></script>

-    securityLevel: "loose",
+    securityLevel: "strict",
+    layout: "dagre",
+    look: "classic",
-    flowchart: { htmlLabels: true, curve: "basis" }
+    flowchart: { htmlLabels: true, curve: "basis", wrappingWidth: 500 }

The mind map then asks for its own engine, in front matter at the top of its source. That only works at the very start of the text, so the render function also needed a .trim():

---
config:
  layout: cose-bilkent
---
mindmap
  root((SSDLC))

My script reported “six charts rendered, none failed” for all of these attempts, including the flat mind map. Only comparing before and after screenshots, slide by slide, caught the problems. Moving to strict cost nothing: the deck has no clickable diagrams and its <br/> line breaks still work.

Where things stand on 4 November 2026
#

ItemStatusDetail
Deck: Mermaid and its DOMPurifyFixed32 known advisories down to 0; an SBOM scan of the deck’s libraries says “No issues found”
Deck: securityLevelFixedNow strict
DOMPurify 3.4.12 in the theme’s bundleOpen5 advisories; needs a theme release. Not yet raised with the theme
Theme tooling (glob, minimatch, zod)Open, low riskNever runs in my pipeline; ignore file tested, not yet committed
SBOM, config, GitHub workflowNot yet in the repoFirst two tested locally; workflow untested on GitHub
Pillow on my machineOpen59 advisories; used locally, on images I made

Two honest footnotes. The plain scan still says 8, exactly as before: everything I fixed was invisible to it, which is why the SBOM has to come next. And “0 advisories” means none known today. Mermaid 12.3.0 came out the day before this scan, so it has had little time to collect any. That is the argument for a scheduled rescan, not a reason to relax.

What I learnt
#

  1. A scanner reports on manifests, not on what you ship. On a static site those can be very different things.
  2. dev describes how a package is installed, not where its code ends up. Bundlers copy dev dependencies into production all the time.
  3. A clean report needs a question after it: what did it look at? Exit 128 is the honest version of this. A green result with half the code unscanned is not.
  4. fix can turn a report green without changing what users get. Check the artefact, not the lockfile.
  5. Asking “what runs here?” found more than the scan did, but the scan is what made me ask.
  6. Zero advisories is not a finished upgrade. My checks passed on a broken diagram. Read the release notes, then look at the result with your own eyes.

Takeaways
#

  • Use -r, and treat exit 128 as a failure in CI. With submodules, check they are actually checked out.
  • Compare the report with what you really serve: bundles, CDN links, downloaded binaries.
  • List what the scanner cannot find in a small SBOM, and accept that you now have to keep it up to date.
  • Give every accepted risk a reason and an expiry date.
  • After a security bump, compare before and after. “It rendered” is not “it is right”.

References
#

Previous: part 4, where it fits.

Author
Samarthya
Engineer, learner, tinkerer. This is my trophy shelf of everyday work and leisure.
Learning OSV - This article is part of a series.
Part 5: This Article

Related

Learning OSV, part 4: where it fits, and my recommendation

·4 mins
Three parts of trying things. This one is the verdict: what I would actually switch on, in what order, and what I would still look elsewhere for. The loop I would run, and the verdict as a checklist. Quick recap of the proof of concept: part 1 read the database, part 2 scanned a project (5 dependencies, 68 packages, 49 vulnerabilities), and part 3 fixed it (7 of 50 safely, 50 of 50 with a major upgrade).

Learning OSV, part 3: guided remediation with osv-scanner fix

·3 mins
Finding 49 vulnerabilities is easy. Deciding which three lines to change so that most of them go away is the hard part. That is what osv-scanner fix tries to do. in-place is the safe small win; relax fixes everything but chose a major upgrade. In part 2 the scanner found 49 known vulnerabilities in a small Node project. This part tries the guided remediation command on it.

Learning OSV, part 2: scanning a project with OSV-Scanner

·4 mins
I picked four dependencies. The scanner found problems in eleven packages. That gap is the whole reason to scan lockfiles and not just package.json. Five dependencies I chose became 68 packages; 11 are affected, six of them only because Express pins them. Part 1 was about the OSV database. This part runs the scanner against a small project and reads the result.