<?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: Aiden Bolin</title>
    <description>The latest articles on DEV Community by Aiden Bolin (@accuoa).</description>
    <link>https://dev.to/accuoa</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3906607%2Fe6bdb864-8f09-41ec-a9d4-401cc0ef18af.png</url>
      <title>DEV Community: Aiden Bolin</title>
      <link>https://dev.to/accuoa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/accuoa"/>
    <language>en</language>
    <item>
      <title>Vuls vs Trivy vs Grype: when to pick which CVE scanner (from the team that built one more)</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:47:18 +0000</pubDate>
      <link>https://dev.to/accuoa/vuls-vs-trivy-vs-grype-when-to-pick-which-cve-scanner-from-the-team-that-built-one-more-2bjd</link>
      <guid>https://dev.to/accuoa/vuls-vs-trivy-vs-grype-when-to-pick-which-cve-scanner-from-the-team-that-built-one-more-2bjd</guid>
      <description>&lt;h1&gt;
  
  
  Vuls vs Trivy vs Grype: when to pick which CVE scanner
&lt;/h1&gt;

&lt;p&gt;I shipped a CVE patch-ops tool last month. The most common feedback from engineers, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Why not just use Vuls?"&lt;/li&gt;
&lt;li&gt;"Doesn't Trivy already do this?"&lt;/li&gt;
&lt;li&gt;"Isn't Grype better?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are fair. They are all good. Here is the honest comparison I wish someone had handed me before I built mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vuls.io — the original self-hosted host scanner
&lt;/h2&gt;

&lt;p&gt;Vuls is the closest open-source equivalent to a managed patch-ops product. It's mature (started in 2016), in Go, and it does the same fundamental work: pull advisories from the upstream feeds, snapshot your box's package state, match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Vuls if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want everything on-prem / air-gapped — no third party sees your inventory.&lt;/li&gt;
&lt;li&gt;You have at least an afternoon of ops time to wire it up (config server, cron, report exporter, your own alerting).&lt;/li&gt;
&lt;li&gt;You're comfortable writing your own remediation playbooks. Vuls tells you the package + fixed version; what you do with it is up to you.&lt;/li&gt;
&lt;li&gt;You already have a Prometheus/Grafana stack you can plug the JSON output into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Vuls if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a 1-3 person dev shop and ops time is the bottleneck. You'll set it up, it'll run for two weeks, then a cron will silently fail and you'll forget about it for two months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trivy — containers and IaC, host CVEs as a bonus
&lt;/h2&gt;

&lt;p&gt;Trivy from Aqua is the most popular scanner now, but it's container-and-IaC-shaped. It scans images, Dockerfiles, Terraform/CloudFormation, Kubernetes manifests, SBOM files, and yes — also host filesystems via &lt;code&gt;trivy rootfs /&lt;/code&gt;. The latter is a real feature, but it's a sidecar to the container story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Trivy if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your security risk is concentrated in container images and you ship a lot of them.&lt;/li&gt;
&lt;li&gt;You want SBOM generation + license scanning + secret detection + misconfig in one binary.&lt;/li&gt;
&lt;li&gt;You're running Kubernetes and want admission-controller integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Trivy for host CVE management if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your fleet is bare-metal VPSes (no containers) — you're paying for a container model that doesn't fit your shape.&lt;/li&gt;
&lt;li&gt;You want per-host audit URLs that your customers can read, not just a CLI report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Grype — SBOM-first, simple
&lt;/h2&gt;

&lt;p&gt;Grype from Anchore is the cleanest of the three. Generate an SBOM with &lt;code&gt;syft&lt;/code&gt;, pipe it to &lt;code&gt;grype&lt;/code&gt;, get findings. It does exactly that and not much else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Grype if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already producing SBOMs as part of your build pipeline (you should be).&lt;/li&gt;
&lt;li&gt;You want a tool that does one thing well — match SBOM packages against the vulnerability DB.&lt;/li&gt;
&lt;li&gt;You're scripting CI gates and need predictable exit codes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Grype if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the fix-action layer ("here is the apt command to run") — Grype is a finder, not a fixer.&lt;/li&gt;
&lt;li&gt;You need continuous monitoring of a running host, not a snapshot at build time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  StackPatch — the one I built
&lt;/h2&gt;

&lt;p&gt;Built specifically for indie SaaS shops running 1-3 Linux boxes. The bet is that the gap between "free OSS scanners you have to babysit" and "$25-50K/yr Snyk-class products" leaves out tens of thousands of indie founders. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosted by us.&lt;/strong&gt; No &lt;code&gt;apt install vuls&lt;/code&gt;. SSH read-only or a small read-only agent, both of which you can revoke instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action-first.&lt;/strong&gt; Every finding ships with the exact &lt;code&gt;apt install --only-upgrade pkg=fixed-version&lt;/code&gt; one-liner, or the modprobe blacklist syntax for kernel-module CVEs, or the &lt;code&gt;pro attach + apt upgrade&lt;/code&gt; flow for Ubuntu Pro / ESM fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public audit URL per server.&lt;/strong&gt; A read-only URL you can hand to your enterprise prospects: "here is our security posture, timestamped." Replaces the emailed PDF that's stale a week later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie-priced.&lt;/strong&gt; $99 lifetime for the first 50 founders, then $19-49/mo. Snyk's smallest sales-team-required tier is roughly 10x that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick StackPatch if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a solo founder or 1-3 person team on Ubuntu / Debian / Alpine / AlmaLinux / Rocky.&lt;/li&gt;
&lt;li&gt;You want patch ops to be a 5-minute habit, not a 5-hour setup.&lt;/li&gt;
&lt;li&gt;You want a public security URL more than you want CLI integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip StackPatch if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need air-gap / on-prem — use Vuls.&lt;/li&gt;
&lt;li&gt;Your scope is container images, not running hosts — use Trivy.&lt;/li&gt;
&lt;li&gt;You're already producing SBOMs in CI — use Grype.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bare-metal VPS fleet, no security team&lt;/td&gt;
&lt;td&gt;StackPatch (hosted) or Vuls (self-hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container-heavy CI/CD&lt;/td&gt;
&lt;td&gt;Trivy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBOM-driven build pipeline&lt;/td&gt;
&lt;td&gt;Grype&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Air-gapped / no third party allowed&lt;/td&gt;
&lt;td&gt;Vuls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise + budget + compliance team&lt;/td&gt;
&lt;td&gt;Snyk / Tenable / Wiz&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The competitive landscape is honest. I'm not pretending StackPatch beats Vuls on universality or Trivy on container coverage. I'm betting on a different problem shape: indie founders who'd pay $19-99 for a hosted patch-ops tool that hands them a per-server URL and an exact remediation command.&lt;/p&gt;

&lt;p&gt;If you're in that shape, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; takes 30 seconds. If you're in a different shape, one of the other three is the right answer — and I'd rather lose the sale than be the wrong tool for your workload.&lt;/p&gt;

&lt;p&gt;Full comparison breakdowns are on &lt;a href="https://mindsparkstack.com/patch/vs-vuls?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-vuls&lt;/a&gt;, &lt;a href="https://mindsparkstack.com/patch/vs-trivy?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-trivy&lt;/a&gt;, and &lt;a href="https://mindsparkstack.com/patch/vs-grype?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-grype&lt;/a&gt;. Comments open — I'll defend or concede whichever way the argument goes.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>linux</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Matching live CVEs to your actual apt packages in ~800 lines of Python</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:46:42 +0000</pubDate>
      <link>https://dev.to/accuoa/matching-live-cves-to-your-actual-apt-packages-in-800-lines-of-python-119h</link>
      <guid>https://dev.to/accuoa/matching-live-cves-to-your-actual-apt-packages-in-800-lines-of-python-119h</guid>
      <description>&lt;h1&gt;
  
  
  Matching live CVEs to your actual apt packages in ~800 lines of Python
&lt;/h1&gt;

&lt;p&gt;41,000+ CVEs are indexed in the NVD right now. The vast majority do not affect you. The interesting engineering question is: which ones do, and what is the exact one-liner to fix them?&lt;/p&gt;

&lt;p&gt;I spent a weekend building the matcher. Here is the shape of the problem and the shape of the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The matching problem
&lt;/h2&gt;

&lt;p&gt;A CVE record from one of the upstream feeds (Ubuntu USN, Debian Security Tracker, Alpine secdb, OSV.dev for the RHEL family, NVD as a fallback) looks roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USN-7100-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenSSH client vulnerability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cves"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2026-35414"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-2026-35387"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noble"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"binaries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"openssh-client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1:9.6p1-3ubuntu13.5"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-09T00:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server, meanwhile, has a snapshot like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dpkg-query &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'${Package}=${Version}\n'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
openssh-client&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
openssh-server&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
openssh-sftp-server&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match logic: for each advisory, intersect on package name. For every hit, compare the installed version against the fixed version using &lt;code&gt;dpkg --compare-versions&lt;/code&gt;. If &lt;code&gt;installed lt fixed&lt;/code&gt;, you are affected; emit the exact &lt;code&gt;apt install &amp;lt;pkg&amp;gt;=&amp;lt;fixed&amp;gt;&lt;/code&gt; one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual matcher loop
&lt;/h2&gt;

&lt;p&gt;The core is brutally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_affected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wrap dpkg --compare-versions. Returns True if installed &amp;lt; fixed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpkg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--compare-versions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match_advisory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;codename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;releases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binaries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_affected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advisory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;installed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remediation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install --only-upgrade &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that over the full advisory cache (USN + DSA + Alpine secdb + OSV-RPM) against &lt;code&gt;dpkg-query&lt;/code&gt; output, and you get a per-server findings list in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets interesting
&lt;/h2&gt;

&lt;p&gt;The version compare is the easy part. The hard parts are the edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Kernel-module CVEs.&lt;/strong&gt; Some CVEs aren't a package version bump at all. CVE-2026-31431 (the "Copy Fail" algif_aead local-priv-esc, disclosed late April) was patched at the upstream kernel level — but Ubuntu had not shipped a fixed kernel for noble at advisory time. So &lt;code&gt;apt install --only-upgrade linux-image-generic&lt;/code&gt; was a no-op. The correct mitigation was a modprobe blacklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/modprobe.d/cve-2026-31431-copyfail.conf
&lt;/span&gt;&lt;span class="n"&gt;blacklist&lt;/span&gt; &lt;span class="n"&gt;algif_aead&lt;/span&gt;
&lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;algif_aead&lt;/span&gt; /&lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the matcher needs a &lt;code&gt;playbook_class&lt;/code&gt; field on each advisory: &lt;code&gt;apt_upgrade&lt;/code&gt;, &lt;code&gt;kernel_reboot&lt;/code&gt;, &lt;code&gt;modprobe_block&lt;/code&gt;, &lt;code&gt;apt_upgrade_esm&lt;/code&gt; (Ubuntu Pro), &lt;code&gt;oss_only_fix&lt;/code&gt;. The default is &lt;code&gt;apt_upgrade&lt;/code&gt;; the rest are hand-curated for kernel/firmware CVEs because the upstream feeds don't tag them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ubuntu Pro / ESM.&lt;/strong&gt; Many "fixed" Ubuntu CVEs are fixed only in Ubuntu Pro (the ESM channel). Free for personal + small-team use, but the fix won't apply without attaching a Pro token. The matcher emits a different playbook for those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pro attach &amp;lt;token&amp;gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--only-upgrade&lt;/span&gt; &amp;lt;pkg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Multi-arch and source vs binary package confusion.&lt;/strong&gt; &lt;code&gt;libssl3&lt;/code&gt; (binary) and &lt;code&gt;openssl&lt;/code&gt; (source) are both real package names. The advisory binaries map is the source of truth — match on that, not the source name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Apt pinning and snapshot repos.&lt;/strong&gt; If a customer is pinned to an older &lt;code&gt;noble-updates&lt;/code&gt; snapshot, the &lt;code&gt;fixed&lt;/code&gt; version may not be installable on their box. The matcher flags this as a &lt;code&gt;remediation_blocked: pin_conflict&lt;/code&gt; state instead of pretending the fix is one apt command away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed plumbing
&lt;/h2&gt;

&lt;p&gt;The actual fetcher is 9 cron jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-usn-poller.py&lt;/code&gt; — every 30 minutes, polls &lt;code&gt;https://ubuntu.com/security/notices.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-dsa-poller.py&lt;/code&gt; — every 30 minutes, parses &lt;code&gt;https://security-tracker.debian.org/tracker/data/json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-alpine-secdb-poller.py&lt;/code&gt; — every hour, fetches alpine-secdb branch refs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-osv-rpm-poller.py&lt;/code&gt; — every hour, OSV.dev RHEL/AlmaLinux/Rocky ecosystems&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-nvd-poller.py&lt;/code&gt; — every 6 hours, NVD modified feed as a backstop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-matcher.py&lt;/code&gt; — every hour, runs the match loop against the inventory cache&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-index-builder.py&lt;/code&gt; — twice daily, regenerates the per-CVE static pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-alert-dispatcher.py&lt;/code&gt; — every 15 minutes, fans out new findings to email/Telegram/Discord webhooks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-customer-backup.py&lt;/code&gt; — daily, snapshots inventory + findings per server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;State lives as JSONL at &lt;code&gt;/var/lib/stackpatch/{advisories,findings,inventory}/&lt;/code&gt;. Not a database — for fewer than 10,000 servers, JSONL + atomic rename is faster and easier to back up than Postgres. A V2 with FTS will probably move it, but premature SQL is a tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "use it on yourself" rule
&lt;/h2&gt;

&lt;p&gt;The first server we pointed the matcher at was our own production VPS — the one this blog is served from. The matcher found 4 outstanding CVEs we hadn't seen. We patched 3 (the OpenSSH set) within an hour; the 4th is gated on attaching a Pro token, which is correctly flagged. That ratio — "matcher finds real CVEs on its own author's box" — is the only credibility test I trust for a security tool.&lt;/p&gt;

&lt;p&gt;If you run Linux on a VPS and you don't have a continuous CVE-to-action pipeline, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=matcher-deepdive" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; takes about 30 seconds. Or read the &lt;a href="https://mindsparkstack.com/patch?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=matcher-deepdive" rel="noopener noreferrer"&gt;open methodology page&lt;/a&gt;. I'm @aiden_bolin in the comments if you have edge-case advisories the matcher misses — that is exactly the feedback I'm hunting for right now.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>The indie SaaS security stack I run on a $7/mo VPS</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:46:06 +0000</pubDate>
      <link>https://dev.to/accuoa/the-indie-saas-security-stack-i-run-on-a-7mo-vps-1457</link>
      <guid>https://dev.to/accuoa/the-indie-saas-security-stack-i-run-on-a-7mo-vps-1457</guid>
      <description>&lt;h1&gt;
  
  
  The indie SaaS security stack I run on a $7/mo VPS
&lt;/h1&gt;

&lt;p&gt;When you're a 1-3 person dev shop, you can't afford Snyk and you don't have a security team. You also can't afford to get breached. Here's the full stack I actually run on the $7/mo Hostinger VPS that serves my production SaaS.&lt;/p&gt;

&lt;p&gt;This is not a "best practices" article. Every line below corresponds to a config file in production right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. UFW — block everything by default
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow ssh
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five commands. The single largest reduction in attack surface you can make in 30 seconds. Verify with &lt;code&gt;sudo ufw status verbose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SSH keys only, no passwords
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/etc/ssh/sshd_config.d/00-hardening.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;PasswordAuthentication&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PermitRootLogin&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PubkeyAuthentication&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveInterval&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;MaxAuthTries&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo systemctl restart ssh&lt;/code&gt;. Now password brute-force attempts hit a wall on the first packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. fail2ban — auto-ban brute-forcers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;600&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three failed SSH attempts in 10 minutes → IP banned for an hour. Check &lt;code&gt;sudo fail2ban-client status sshd&lt;/code&gt; after a week — you'll see hundreds of banned IPs. Most of the noise just stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. unattended-upgrades — apt patches without you
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unattended-upgrades
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;--priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;low unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Allowed&lt;/span&gt;-&lt;span class="n"&gt;Origins&lt;/span&gt; {
    &lt;span class="s2"&gt;"${distro_id}:${distro_codename}-security"&lt;/span&gt;;
    &lt;span class="s2"&gt;"${distro_id}ESM:${distro_codename}"&lt;/span&gt;;
};
&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Automatic&lt;/span&gt;-&lt;span class="n"&gt;Reboot&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;;
&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Mail&lt;/span&gt; &lt;span class="s2"&gt;"ops@yourdomain.com"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately leave &lt;code&gt;Automatic-Reboot "false"&lt;/code&gt; because a 3am reboot during traffic is its own incident. Instead, see step 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Stripe webhook signature validation
&lt;/h2&gt;

&lt;p&gt;If you take payments, every webhook handler must verify the signature header. Without this, anyone can POST &lt;code&gt;customer.subscription.created&lt;/code&gt; events at your endpoint and grant themselves access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... safe to process&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail closed. If &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; is unset, return 503 — never silently skip the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Kernel patching with an actual reboot strategy
&lt;/h2&gt;

&lt;p&gt;This is the part most indie shops skip. &lt;code&gt;unattended-upgrades&lt;/code&gt; installs the new kernel but won't reboot. The running kernel stays vulnerable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/usr/local/bin/kernel-patch-check.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;installed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'linux-image-*'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^ii/ {print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/linux-image-//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$running&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$installed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Reboot pending: running=&lt;/span&gt;&lt;span class="nv"&gt;$running&lt;/span&gt;&lt;span class="s2"&gt; installed=&lt;/span&gt;&lt;span class="nv"&gt;$installed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Kernel patch ready on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ops@yourdomain.com
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cron it hourly. Get an email the moment a new kernel ships and is installed. Then you reboot during your next low-traffic window — manually, with eyes on the systemd units coming back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The CVE-to-action layer (the missing piece)
&lt;/h2&gt;

&lt;p&gt;Steps 1-6 cover known-class vulnerabilities being delivered via apt. They don't cover the case where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CVE drops for a kernel module (CVE-2026-31431 / "Copy Fail", late April) and no Ubuntu kernel fix is shipped yet — you need a &lt;code&gt;modprobe blacklist&lt;/code&gt; for the affected module, not an apt update.&lt;/li&gt;
&lt;li&gt;A CVE is fixed only in Ubuntu Pro (ESM) and your free Ubuntu install can't see the fix — you need to attach a free Pro token first.&lt;/li&gt;
&lt;li&gt;You want to know within 5 minutes that an advisory landed for your stack, not when the cron mail eventually arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried Snyk's free tier. Hit the 200-vulns/mo limit in under a week. Their smallest sales-team-required tier was ~$25K/yr. I tried Vuls.io — works, but I spent more time tuning it than I would have spent reading the advisories myself.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://mindsparkstack.com/patch?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=indie-saas-stack" rel="noopener noreferrer"&gt;StackPatch&lt;/a&gt; for exactly this shape. SSH read-only or small read-only agent. Continuous match against the upstream feeds (USN, DSA, Alpine secdb, OSV.dev for RHEL family). Per-server public audit URL. Exact remediation command per finding — including the modprobe-blacklist case, not just &lt;code&gt;apt upgrade&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;$99 lifetime for the first 50 founders, then $19-49/mo. The free quickscan is a one-liner you can curl right now without signing up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That'll spit out your kernel + apt package state matched against the live USN feed. No data sent anywhere unless you opt into the waitlist + monitoring at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest pitch
&lt;/h2&gt;

&lt;p&gt;This stack — UFW, SSH hardening, fail2ban, unattended-upgrades, kernel patch checker, webhook signature validation, and a CVE-to-action layer — covers most of what a security team would set up for a small Linux fleet. The first six pieces are free and take about an hour. The seventh piece is the gap most indie shops just live with, because the existing tools are either ops-heavy or enterprise-priced.&lt;/p&gt;

&lt;p&gt;If you have a more complete stack, the comments are open and I will steal from you. If you're missing pieces, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=indie-saas-stack" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; will tell you which ones in 30 seconds.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>security</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>I built a CVE patch-ops tool for indie SaaS shops in a weekend (open scan, honest comparison vs vuls.io)</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Fri, 01 May 2026 14:02:25 +0000</pubDate>
      <link>https://dev.to/accuoa/i-built-a-cve-patch-ops-tool-for-indie-saas-shops-in-a-weekend-open-scan-honest-comparison-vs-3llk</link>
      <guid>https://dev.to/accuoa/i-built-a-cve-patch-ops-tool-for-indie-saas-shops-in-a-weekend-open-scan-honest-comparison-vs-3llk</guid>
      <description>&lt;h1&gt;
  
  
  I built a CVE patch-ops tool for indie SaaS shops in a weekend (open scan, honest comparison vs vuls.io)
&lt;/h1&gt;

&lt;p&gt;Last week Hostinger emailed every customer about CVE-2026-31431 — the "Copy Fail" Linux kernel local-privilege-escalation. A 732-byte Python script gets root via &lt;code&gt;algif_aead&lt;/code&gt; AF_ALG + &lt;code&gt;splice()&lt;/code&gt;. Every kernel between 2017 and the upstream fix in early 2026.&lt;/p&gt;

&lt;p&gt;I run a one-person SaaS on a Hostinger VPS. I read the advisory, dropped a &lt;code&gt;modprobe&lt;/code&gt; blacklist, and was patched in 30 minutes. Then I realized: most indie SaaS founders running their own boxes wouldn't read that email until tomorrow. Some would never read it at all.&lt;/p&gt;

&lt;p&gt;Patch ops for the indie-SaaS tier doesn't exist. vuls.io is great if you have a security engineer with half a day. Enterprise tools are unaffordable. The gap is "I have 1–10 Linux servers, I know I should patch, the workflow is too annoying to do consistently."&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;StackPatch&lt;/strong&gt; — &lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash&lt;/code&gt; for a free anonymous CVE check, $99 lifetime founder seat for hourly monitoring with a public audit URL.&lt;/p&gt;

&lt;p&gt;This post is the build log: architecture, the honest vs-vuls comparison, and the bash one-liner you can run on your VPS in five seconds to see what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free quickscan: 5 seconds, 0 signup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script reads &lt;code&gt;/etc/os-release&lt;/code&gt;, &lt;code&gt;uname -r&lt;/code&gt;, the top 200 packages from &lt;code&gt;dpkg-query&lt;/code&gt;, and POSTs them to a public API. The API runs the live USN feed (Ubuntu) and the Debian Security Tracker (~110K fix-records across bookworm/trixie/bullseye) against your inventory using &lt;code&gt;dpkg --compare-versions&lt;/code&gt;, returns matching CVEs with the exact remediation command.&lt;/p&gt;

&lt;p&gt;The source is served as &lt;code&gt;text/plain&lt;/code&gt; so you can &lt;code&gt;curl https://mindsparkstack.com/scan.sh&lt;/code&gt; and read it before piping to bash. No persistent state server-side beyond a 5-min cache. No identifying info collected (no hostname, no IP, no env vars).&lt;/p&gt;

&lt;p&gt;On a real noble box with &lt;code&gt;openssh-client 1:9.6p1-3ubuntu13.10&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== StackPatch quickscan ===
  distro:   ubuntu
  codename: noble
  kernel:   6.8.0-100-generic
  packages: 187

⚠️  2 active CVE matches on your stack right now (worst: high).
   Run the recommended commands above. To monitor every server hourly...

  [HIGH] CVE-2026-31431  Linux kernel "Copy Fail" — local-priv-esc via algif_aead
        why: Linux kernel local-priv-esc; 732-byte Python script gets root...
        match: Running kernel: 6.8.0-100-generic
        recommend: Apply persistent modprobe blacklist for algif_aead now...

  [HIGH] USN-8222-1  OpenSSH 9.6p1 vulnerabilities
        match: openssh-client: installed 1:9.6p1-3ubuntu13.10 &amp;lt; fixed 1:9.6p1-3ubuntu13.16
        recommend: sudo apt-get install --only-upgrade -y openssh-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the demo. Five seconds, real CVEs, exact commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: file-based, Python stdlib, no DB
&lt;/h2&gt;

&lt;p&gt;The whole backend is JSONL files on disk + cron + a small Next.js layer. There's no Postgres. There's no Kubernetes. The matcher is one Python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode of the matcher join
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;usn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cached_usns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;usn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_packages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_codename&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;dpkg_lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;dpkg --compare-versions&lt;/code&gt; for Debian-policy-correct version comparison.&lt;/strong&gt; Lexicographic compare is wrong for &lt;code&gt;1:9.6p1-3ubuntu13.10&lt;/code&gt; vs &lt;code&gt;1:9.6p1-3ubuntu13.16&lt;/code&gt; (lex says "10" &amp;gt; "16"). Spawning &lt;code&gt;dpkg&lt;/code&gt; once per pair is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-filter by codename.&lt;/strong&gt; USN &lt;code&gt;release_packages&lt;/code&gt; is keyed by &lt;code&gt;noble | jammy | focal | bionic&lt;/code&gt;. Reading the user's &lt;code&gt;/etc/os-release&lt;/code&gt; &lt;code&gt;VERSION_CODENAME&lt;/code&gt; upfront lets the matcher skip 90% of records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the USN window.&lt;/strong&gt; I scan the last 200 USNs (sorted by ID). Older ones are fine to stale; if a 2017 USN matters to your 2024 box, you have bigger problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debian Security Tracker is huge.&lt;/strong&gt; The &lt;code&gt;tracker.json&lt;/code&gt; is 70 MB with 36K fix-records per release. I pre-build per-codename indexes (&lt;code&gt;{package: [{cve, fixed_version, urgency}]}&lt;/code&gt;) once daily so the matcher loads ~12 MB instead of 70 MB per request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inventory + matcher run on three crons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="m"&gt;3&lt;/span&gt; * * * *           &lt;span class="n"&gt;inventory&lt;/span&gt;   (&lt;span class="n"&gt;reads&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;os&lt;/span&gt;-&lt;span class="n"&gt;release&lt;/span&gt;, &lt;span class="n"&gt;uname&lt;/span&gt;, &lt;span class="n"&gt;dpkg&lt;/span&gt;, &lt;span class="n"&gt;docker&lt;/span&gt;, &lt;span class="n"&gt;ports&lt;/span&gt;, &lt;span class="n"&gt;modprobe&lt;/span&gt;)
&lt;span class="m"&gt;23&lt;/span&gt;,&lt;span class="m"&gt;53&lt;/span&gt; * * * *       &lt;span class="n"&gt;USN&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;    (&lt;span class="n"&gt;Ubuntu&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="n"&gt;Notices&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;, &lt;span class="n"&gt;twice&lt;/span&gt; &lt;span class="n"&gt;hourly&lt;/span&gt;)
&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; * * *           &lt;span class="n"&gt;DSA&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;    (&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="n"&gt;Tracker&lt;/span&gt;, &lt;span class="n"&gt;daily&lt;/span&gt; — &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;huge&lt;/span&gt;)
&lt;span class="m"&gt;33&lt;/span&gt; * * * *          &lt;span class="n"&gt;matcher&lt;/span&gt;     (&lt;span class="n"&gt;joins&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; × &lt;span class="n"&gt;USN&lt;/span&gt; × &lt;span class="n"&gt;DSA&lt;/span&gt;, &lt;span class="n"&gt;writes&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="n"&gt;JSONL&lt;/span&gt;)
&lt;span class="m"&gt;40&lt;/span&gt; * * * *          &lt;span class="n"&gt;alerts&lt;/span&gt;      (&lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. The Next.js layer is a thin wrapper that exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/quickscan&lt;/code&gt; — anonymous POST, returns matches in &amp;lt;1s&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/enroll&lt;/code&gt; — paid customer enrolls a server with a token, returns audit URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/inventory&lt;/code&gt; — authenticated inventory POST from the agent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/patch/audit/&amp;lt;slug&amp;gt;&lt;/code&gt; — public posture page per server&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The killer workflow: the public audit URL
&lt;/h2&gt;

&lt;p&gt;This is what I didn't see in any other tool. Every monitored server gets a URL like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://mindsparkstack.com/patch/audit/mss-vps&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It shows current findings, applied mitigations, recent resolutions, package counts, kernel state. Updates hourly. Customers (or your customers' enterprise prospects) can verify your security posture without an NDA, without a stale PDF.&lt;/p&gt;

&lt;p&gt;When a $3K/year customer asks "how do you handle server security updates?" — you send the link. Done.&lt;/p&gt;

&lt;p&gt;This isn't a security scanner. It's a security-response receipt. That distinction is the whole reason the product exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest vs vuls.io
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vuls.io" rel="noopener noreferrer"&gt;vuls.io&lt;/a&gt; is the obvious comparison. It's free, OSS, mature since 2016, 10K+ GitHub stars, supports Ubuntu/Debian/RHEL/CentOS/Amazon Linux/openSUSE/Alpine/FreeBSD/Windows.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://mindsparkstack.com/patch/vs-vuls" rel="noopener noreferrer"&gt;/patch/vs-vuls&lt;/a&gt; as an honest 12-row green/red/grey side-by-side. Honest enough that the page actively recommends vuls.io if you fit those constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a security engineer with half a day to set up &lt;code&gt;go-cve-dictionary&lt;/code&gt; + &lt;code&gt;goval-dictionary&lt;/code&gt; + &lt;code&gt;gost&lt;/code&gt; + &lt;code&gt;cve-search&lt;/code&gt; and rebuild them on cron&lt;/li&gt;
&lt;li&gt;You need FreeBSD / Windows / openSUSE support&lt;/li&gt;
&lt;li&gt;Compliance forbids any package data leaving your network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick StackPatch instead if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a one-person SaaS shop with 1–10 boxes on Ubuntu, Debian, Alpine, AlmaLinux, or Rocky Linux&lt;/li&gt;
&lt;li&gt;You want the answer in 5 minutes, not half a day&lt;/li&gt;
&lt;li&gt;You want the exact &lt;code&gt;apt&lt;/code&gt; / &lt;code&gt;apk&lt;/code&gt; / &lt;code&gt;dnf&lt;/code&gt; / kernel-reboot / modprobe-blacklist one-liner, not just a CVE link&lt;/li&gt;
&lt;li&gt;You want a public audit URL for sales due diligence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a Sunday-afternoon scanner project sounds fun: vuls.io. If you want to be patched and provably so by tomorrow morning: StackPatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What V1 doesn't do
&lt;/h2&gt;

&lt;p&gt;I'm not going to lie about the gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No FreeBSD / Windows / openSUSE yet&lt;/strong&gt; (V1+ covers Ubuntu, Debian, Alpine, AlmaLinux, Rocky Linux — 18 release versions across those 5 distros, 41K unique CVEs cross-indexed from USN + DSA + Alpine secdb + OSV-rpm + NVD)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-apply&lt;/strong&gt; (deliberate — you should review the command, security-product trust is fragile)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi-user RBAC, no SSO, no compliance attestations&lt;/strong&gt; (this is for solo founders, not security teams)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Kubernetes&lt;/strong&gt; (out of scope for V1; you're not running k8s on a $5/mo VPS anyway)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SLA on remediation&lt;/strong&gt; (we tell you the command; you run it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those are dealbreakers, vuls.io or an enterprise tool is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: $99 lifetime for the first 50
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free quickscan:&lt;/strong&gt; anonymous, no signup, no limits. Run it on as many boxes as you like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$99 lifetime founder seat:&lt;/strong&gt; 3 servers, hourly monitoring, real-time email alerts, public audit URL, every V2+ feature included. &lt;strong&gt;50 only&lt;/strong&gt;, then it's a monthly subscription tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no growth-hack reason for the lifetime price. It's a forcing function — a recurring-revenue product is cheaper for me long-term but it lets me delay shipping. Lifetime sales mean I have to keep adding founders, which means I have to keep shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://mindsparkstack.com/scan.sh&lt;/code&gt; (read first, then pipe).&lt;br&gt;
Comparison: &lt;code&gt;https://mindsparkstack.com/patch/vs-vuls&lt;/code&gt;&lt;br&gt;
Live demo audit: &lt;code&gt;https://mindsparkstack.com/patch/audit/mss-vps&lt;/code&gt; (our own VPS, public)&lt;br&gt;
Buy ($99 lifetime, 50 only): &lt;code&gt;https://mindsparkstack.com/patch&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Comments welcome — what's missing, what's wrong, what would make you switch from vuls.io. I read everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Aiden runs MindSparkStack. StackPatch is the patch-ops layer. Source: this is shipped on Hostinger, with Next.js 16 + Python stdlib + cron, running on the same VPS whose audit URL is public above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
