<?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: kotakanbe</title>
    <description>The latest articles on DEV Community by kotakanbe (@kotakanbe).</description>
    <link>https://dev.to/kotakanbe</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%2F44746%2F423615e1-2660-4dfc-9278-1db8a449d5ec.jpg</url>
      <title>DEV Community: kotakanbe</title>
      <link>https://dev.to/kotakanbe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kotakanbe"/>
    <language>en</language>
    <item>
      <title>A maintained package can still ship malware. What a health check can and can't see.</title>
      <dc:creator>kotakanbe</dc:creator>
      <pubDate>Mon, 26 Oct 2026 02:39:49 +0000</pubDate>
      <link>https://dev.to/kotakanbe/a-maintained-package-can-still-ship-malware-what-a-health-check-can-and-cant-see-4k5l</link>
      <guid>https://dev.to/kotakanbe/a-maintained-package-can-still-ship-malware-what-a-health-check-can-and-cant-see-4k5l</guid>
      <description>&lt;p&gt;On September 8, 2025, the maintainer of &lt;code&gt;chalk&lt;/code&gt; and &lt;code&gt;debug&lt;/code&gt; was phished, and malicious versions of those and other widely used npm packages were published from that account. The maintainer disclosed it within hours, and clean versions were out the same afternoon. &lt;code&gt;chalk&lt;/code&gt; had shipped 5.6.0 three weeks earlier. By any maintenance measure, it was a maintained package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;uzomuzo&lt;/a&gt;, the dependency health checker I maintain, judges dependencies by exactly that kind of measure. Its job is to find dependencies with maintenance or lifecycle concerns. Until v0.6.0, &lt;code&gt;uzomuzo scan pkg:npm/chalk@5.6.1&lt;/code&gt; returned &lt;code&gt;✅ ok … Active&lt;/code&gt;. A hijacked release of a maintained package is the case that kind of check can't see.&lt;/p&gt;

&lt;p&gt;This post covers the two things we added to narrow that gap, the numbers behind each decision, and what still gets through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Malicious versions&lt;/strong&gt;: read OSV's malicious-package records and stop the versions they name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before a record exists&lt;/strong&gt;: for 7 days, mark a release that comes after a year of silence.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Stopping versions that OSV says are malicious
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ossf/malicious-packages" rel="noopener noreferrer"&gt;OpenSSF malicious-packages&lt;/a&gt; project publishes records with IDs starting with &lt;code&gt;MAL-&lt;/code&gt;, served through &lt;a href="https://osv.dev" rel="noopener noreferrer"&gt;OSV&lt;/a&gt;. For a hijack, a record usually names the specific versions that were malicious; for a package that was malicious from the start, it covers every version.&lt;/p&gt;

&lt;p&gt;Here's what uzomuzo v0.10.0 shows (output excerpt, October 26, 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uzomuzo scan pkg:npm/chalk@5.6.1 pkg:npm/chalk@5.6.2 pkg:cargo/rustdecimal@1.23.1
STATUS     PURL                          LIFECYCLE                   BUILD
🔴 replace  pkg:npm/chalk@5.6.1           Active ☠ MAL-2025-46969     —
✅ ok       pkg:npm/chalk@5.6.2           Active                      Moderate 4.4
🔴 replace  pkg:cargo/rustdecimal@1.23.1  Review Needed ☠ MAL-2022-1  —
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design choices are visible here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lifecycle label doesn't change.&lt;/strong&gt; &lt;code&gt;chalk&lt;/code&gt; is still &lt;code&gt;Active&lt;/code&gt;, because it is. Whether a project is maintained and whether one of its versions is malicious are separate facts, so the malicious record sits next to the label (&lt;code&gt;☠ MAL-…&lt;/code&gt;) instead of replacing it. Other tools that read the label keep working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The match is by version.&lt;/strong&gt; Chalk's record names &lt;code&gt;5.6.1&lt;/code&gt;, so &lt;code&gt;5.6.2&lt;/code&gt;, the clean release that followed, isn't affected by it. A flag on the whole package would push people away from the fix. (&lt;code&gt;rustdecimal&lt;/code&gt;'s record covers every version, because that package was malicious from the start.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any CI gate catches it.&lt;/strong&gt; If &lt;code&gt;--fail-on&lt;/code&gt; names anything at all, a matched malicious version fails the build. A pipeline that only ever wrote &lt;code&gt;--fail-on eol-confirmed&lt;/code&gt; will stop on &lt;code&gt;chalk@5.6.1&lt;/code&gt; too:&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;uzomuzo scan pkg:npm/chalk@5.6.1 &lt;span class="nt"&gt;--fail-on&lt;/span&gt; eol-confirmed &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two conditions apply. Without &lt;code&gt;--fail-on&lt;/code&gt;, findings alone don't make uzomuzo exit with an error (command errors still do). And if the OSV lookup fails (no network, for example), uzomuzo warns that the check is incomplete; it doesn't fail the build on that alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  No waiting period
&lt;/h3&gt;

&lt;p&gt;uzomuzo already waits 14 days before acting on a RustSec "unmaintained" advisory, because those advisories are sometimes withdrawn and filed again. For malicious-package records there is no waiting period. Chalk's record was published about four hours after the malicious version (13:13 → 17:11 UTC, the same day). Waiting a few days before trusting such a record would let the version through during exactly the days it matters.&lt;/p&gt;

&lt;p&gt;That has a cost, and we accepted it. In June 2024, &lt;code&gt;MAL-2024-2929&lt;/code&gt; named &lt;code&gt;react@1.0.0&lt;/code&gt; and &lt;code&gt;react@35.0.0&lt;/code&gt; as malicious. Those versions don't exist on npm, and the record was withdrawn about six days later as "False positive caused by problematic ingestion". Nobody's build could have broken on that one. But if a record like it named a version people actually use, our rule would fail every gate on that version until the record was withdrawn. The flag clears on the next run after a withdrawal; it doesn't undo a build that already failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it misses
&lt;/h3&gt;

&lt;p&gt;uzomuzo only acts on records that say, in a field a machine can read, that the code is malicious: a &lt;code&gt;MAL-&lt;/code&gt; ID, or a GitHub advisory tagged CWE-506 ("embedded malicious code"). &lt;code&gt;ctx&lt;/code&gt; 0.2.6 on PyPI was hijacked in 2022. Its advisories list the affected versions and describe the takeover in prose, but none carries that classification. We considered reading the prose and decided against it: in a CI gate, misreading free text is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Before a record exists
&lt;/h2&gt;

&lt;p&gt;A record needs time to appear. We looked at npm records in the OpenSSF data (as of October 8, 2026) that name only &lt;em&gt;some&lt;/em&gt; versions of a package, kept packages that had existed for at least a year with at least three normal releases before the malicious one, and treated those as takeovers. That left &lt;strong&gt;1,556 packages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the 1,515 of them with a usable timestamp, the median time from the first malicious publish to the earliest timestamp in its record was about &lt;strong&gt;6 hours&lt;/strong&gt;. 89% were within a day, and 98% within 7 days. That timestamp approximates when the malware was found, not when OSV started serving the record.&lt;/p&gt;

&lt;p&gt;Those figures are flattered by a few huge events. Most of the 1,556 packages belong to bulk account compromises, where one stolen account published to every package it owned and records came quickly (1,418 packages, 99% within 7 days). Outside those waves the tail is longer: of 97 packages, 86% had a record within 7 days, and 8% took more than 30 days.&lt;/p&gt;

&lt;p&gt;So for hours, sometimes for weeks, a check that only reads records says nothing. Can anything be said in that window?&lt;/p&gt;

&lt;h3&gt;
  
  
  What these packages looked like before the attack
&lt;/h3&gt;

&lt;p&gt;In &lt;strong&gt;737 of the 1,556 packages (47%)&lt;/strong&gt;, the malicious version was the first release in more than a year.&lt;/p&gt;

&lt;p&gt;That number needs three qualifications.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's mostly a few events.&lt;/strong&gt; 724 of the 737 come from 13 bulk account compromises — the Shai-Hulud waves, the September 2025 chalk/debug phishing, and others. Outside those waves, only 13 of 103 packages had been quiet for a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't show that attackers prefer quiet packages.&lt;/strong&gt; On the dates of those waves, between about a quarter and a half (27–53%) of the same packages that hadn't been hit yet had also been quiet for a year. That fits bulk attacks publishing from accounts that already held many quiet packages; it says nothing about which packages attackers prefer across npm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It compares different units.&lt;/strong&gt; 47% is per hijacked package. Among the normal releases of the same packages, about 0.5% came after a year of silence (counting every release), or 6.8% if you look only at the last normal release before the attack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the numbers support is narrow: a release after a year of silence is uncommon among normal releases and common among these malicious ones. That makes it a reason to look at a release, not evidence of compromise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The marker
&lt;/h3&gt;

&lt;p&gt;So for npm packages, uzomuzo now marks a stable release that comes after at least a year without releases. Here's a benign release that triggered it. This is an excerpt from a v0.8.0 scan captured on October 15, 2026; the marker expired on October 18, so running the command today won't show it.&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;uzomuzo scan pkg:npm/dateformat@5.0.4
STATUS     PURL                      LIFECYCLE               BUILD
✅ ok       pkg:npm/dateformat@5.0.4  Active ⚠ 1695d silence  Moderate 5.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detailed view explains it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠ Release after silence: This version came out 2026-10-11,
                         after 1695 days with no release.
                         Takeovers of npm packages that
                         had gone quiet looked like this,
                         and so does a maintainer who
                         ships again after a break. No OSV
                         malicious-package record names it
                         yet. Before using it, check who
                         published it (npm view
                         &amp;lt;package&amp;gt;@&amp;lt;version&amp;gt; _npmUser).
                         This marker clears on 2026-10-18.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verdict stays &lt;code&gt;ok&lt;/code&gt;. We did what the message says: &lt;code&gt;npm view dateformat@5.0.4 _npmUser&lt;/code&gt; shows the same account that published 5.0.3 four years earlier, and 5.0.5 followed on October 24. That's what a project coming back after a break looks like.&lt;/p&gt;

&lt;p&gt;The publisher check is a starting point, not proof. In a takeover the account name stays the same, and a legitimate handover changes it. Look at the release itself and at what the maintainers have said, too.&lt;/p&gt;

&lt;p&gt;The marker covers the first release after the silence, plus anything published within a day of it, since a follow-up the next day comes from the same silence. &lt;code&gt;dateformat@5.0.3&lt;/code&gt;, the release before the silence, has no marker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why 7 days
&lt;/h3&gt;

&lt;p&gt;The marker is a bridge until a record can take over. Once an OSV record names the version, the first mechanism stops it, whatever its age.&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%2Fo1w3h4quc72w8imqkekm.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%2Fo1w3h4quc72w8imqkekm.png" alt="Timeline of September 8, 2025: chalk@5.6.1 and ansi-styles@6.2.2 were published maliciously within a minute. chalk had released 22 days earlier, so nothing flagged it until its MAL record about 4 hours later. ansi-styles had been silent for 1,061 days, so the release-after-silence marker would have covered it from the first minute until the record took over."&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two packages from the same attack, with today's rules applied to the published timestamps. The marker only helps the one that had been quiet.&lt;/em&gt; So the question is: for takeovers that started with a release after a year of silence, how long did the record take?&lt;/p&gt;

&lt;p&gt;The percentages above count packages, so a bulk wave of hundreds of packages counts hundreds of times. For choosing a marker period the right unit is the incident, so we counted again: incidents whose first malicious version came after a year of silence, a bulk wave counted once, timed from the first malicious publish to when the record entered the OpenSSF data. 27 incidents qualified: 20 that touched one release line (such as 5.x), and 7 that touched two or more at once. An incident qualified if at least one of its packages matched, and the earliest publish and the earliest record can belong to different packages. So these counts describe timing per incident, not a record for every affected version.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Marker kept for&lt;/th&gt;
&lt;th&gt;One-line incidents with a record by then (of 20)&lt;/th&gt;
&lt;th&gt;Multi-line incidents with a record by then (of 7)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 days&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;365 days&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One one-line incident has no import time; it stays in the count but never counts as recorded. The other three that took longer than 7 days took about 12.6, 57 and 310 days.&lt;/p&gt;

&lt;p&gt;Going from 7 to 14 days would have added one incident. A year would have added three, and kept every ordinary return from a break flagged for a year. So 7 days is a policy whose window held a record for 16 of the 20 one-line incidents and all 7 multi-line ones we've seen so far. It isn't proof that 7 days will be enough next time, and when the marker expires, that says nothing about whether the release is safe.&lt;/p&gt;

&lt;p&gt;This incident count is a separate selection from the package counts above. It was made outside the uzomuzo repository: the design record publishes the results and limitations, but not the full selection.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it doesn't fail CI by default
&lt;/h3&gt;

&lt;p&gt;On the lockfiles of VS Code and the npm CLI, 186 of about 6,100 and 146 of about 2,600 stable releases in the year to October 2026 would have carried the marker. A team that adopts and scans every release within its first week could have run into all of them. None of them is known to be malicious.&lt;/p&gt;

&lt;p&gt;So by default the marker only shows up in the output. To fail CI on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uzomuzo scan &lt;span class="nt"&gt;--sbom&lt;/span&gt; bom.json &lt;span class="nt"&gt;--show-transitive&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; dormant-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One exception: when a quiet package returns with releases on two or more lines at once (as &lt;code&gt;node-ipc&lt;/code&gt; did in May 2026, with three malicious versions in 55 seconds), an &lt;code&gt;Active&lt;/code&gt; or &lt;code&gt;Legacy-Safe&lt;/code&gt; label also becomes &lt;code&gt;Review Needed&lt;/code&gt; for those 7 days, so an existing &lt;code&gt;--fail-on review-needed&lt;/code&gt; gate fails too.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it can't catch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attacks on recently active packages.&lt;/strong&gt; &lt;code&gt;chalk&lt;/code&gt; itself had released three weeks earlier, so there was no silence to mark. A release cooldown can delay adopting such a version, and a matching record identifies it once one exists; neither guarantees it's caught in time. (Several other packages published from the same account that day &lt;em&gt;had&lt;/em&gt; been quiet for years, and would have carried the marker.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patient attackers.&lt;/strong&gt; An attacker who ends the silence with a harmless release, then publishes the malicious one more than a day later, walks past the marker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Takeovers that never get a record.&lt;/strong&gt; Some older incidents have only a GitHub advisory tagged as malicious code, which uzomuzo also reads. Some have nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Turn on a release cooldown too
&lt;/h3&gt;

&lt;p&gt;The marker can't help with the attack this post opened with. What would have helped is simply not taking a brand-new release. &lt;code&gt;chalk@5.6.1&lt;/code&gt; was gone from npm within about 2 hours 40 minutes, so a team that waited even one day before adopting new releases would never have installed it.&lt;/p&gt;

&lt;p&gt;So, separately from uzomuzo, we recommend a release cooldown in whatever updates your dependencies. It doesn't detect anything; it just gives the ecosystem time to notice.&lt;/p&gt;

&lt;p&gt;Dependabot (&lt;code&gt;.github/dependabot.yml&lt;/code&gt;):&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly"&lt;/span&gt;
    &lt;span class="na"&gt;cooldown&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Renovate (&lt;code&gt;renovate.json&lt;/code&gt;):&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;"minimumReleaseAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 days"&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;pnpm (&lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, in minutes; pnpm 11 waits one day by default):&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;minimumReleaseAge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4320&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats. Dependabot's cooldown applies to version updates, not security updates, so a fix for a known vulnerability still arrives right away; that's usually what you want. And a cooldown is a delay, not a guarantee: event-stream's malicious version went unnoticed for about 72 days.&lt;/p&gt;

&lt;p&gt;The two work together. A cooldown holds back every new release for a few days. The marker picks out the small share of releases that end a long silence and asks you to look at them. The MAL record stops the version once someone has identified it.&lt;/p&gt;

&lt;p&gt;In v0.10.0 we also added a quieter notice for any selected version that is 7 days old or less, in any ecosystem, when uzomuzo can find its publish time. It's always in the JSON output, and appears in the detailed view unless the release-after-silence marker or a malicious record already covers the version. It doesn't change the verdict or fail CI. It tells you that you're early, before records have had time to arrive.&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;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@v0.10.0   &lt;span class="c"&gt;# or download a release binary&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# optional; adds commit history and repository signals&lt;/span&gt;

&lt;span class="c"&gt;# One package&lt;/span&gt;
uzomuzo scan pkg:npm/chalk@5.6.1

&lt;span class="c"&gt;# A whole project, via a CycloneDX SBOM&lt;/span&gt;
trivy fs &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; cyclonedx | uzomuzo scan &lt;span class="nt"&gt;--sbom&lt;/span&gt; - &lt;span class="nt"&gt;--show-transitive&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; malicious
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Include &lt;code&gt;--show-transitive&lt;/code&gt;. Without it, uzomuzo skips the dependencies your SBOM marks as transitive, and &lt;code&gt;chalk&lt;/code&gt; and &lt;code&gt;debug&lt;/code&gt; usually arrive as transitive ones. What gets checked also depends on what your SBOM contains.&lt;/p&gt;

&lt;p&gt;The malicious-version check needs v0.6.0 or later, and the release-after-silence marker v0.7.0 or later. The design records behind this post are &lt;a href="https://github.com/future-architect/uzomuzo-oss/blob/v0.10.0/docs/adr/0027-osv-malicious-advisories.md" rel="noopener noreferrer"&gt;ADR-0027&lt;/a&gt; and &lt;a href="https://github.com/future-architect/uzomuzo-oss/blob/v0.10.0/docs/adr/0028-dormant-release-marker.md" rel="noopener noreferrer"&gt;ADR-0028&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Earlier posts on uzomuzo: &lt;a href="https://dev.to/kotakanbe/your-dependencies-are-48-unmaintained-and-sca-tools-cant-see-it-2h7h"&gt;"Your dependencies are 48% unmaintained — and SCA tools can't see it"&lt;/a&gt; and &lt;a href="https://dev.to/kotakanbe/your-ci-runs-other-peoples-code-is-anyone-still-maintaining-it-19c5"&gt;"Your CI runs other people's code. Is anyone still maintaining it?"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kota Kanbe — Creator of &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt;, an open-source vulnerability scanner, and maintainer of uzomuzo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>npm</category>
      <category>supplychain</category>
    </item>
    <item>
      <title>We cut our scanner binary by 68% — and removing packages one by one wasn't how</title>
      <dc:creator>kotakanbe</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:57:06 +0000</pubDate>
      <link>https://dev.to/kotakanbe/we-cut-our-scanner-binary-by-68-and-removing-packages-one-by-one-wasnt-how-4d98</link>
      <guid>https://dev.to/kotakanbe/we-cut-our-scanner-binary-by-68-and-removing-packages-one-by-one-wasnt-how-4d98</guid>
      <description>&lt;p&gt;Finding unmaintained dependencies is the easy part. Removing them is where it gets hard.&lt;/p&gt;

&lt;p&gt;This is the story of how we slimmed down &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt;, the open-source vulnerability scanner I created — what didn't work, what did, and the tooling we built after doing it all by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy win: 15 minutes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mitchellh/go-homedir" rel="noopener noreferrer"&gt;&lt;code&gt;mitchellh/go-homedir&lt;/code&gt;&lt;/a&gt; has been archived for years. Vuls used it in a couple of lines in two source files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import "github.com/mitchellh/go-homedir"
- dir, err := homedir.Dir()
&lt;/span&gt;&lt;span class="gi"&gt;+ dir, err := os.UserHomeDir()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen minutes, start to finish. The standard library has done this since Go 1.12.&lt;/p&gt;

&lt;p&gt;Trivy used the same package, so we sent them &lt;a href="https://github.com/aquasecurity/trivy/pull/10484" rel="noopener noreferrer"&gt;a similar fix&lt;/a&gt;. The Trivy maintainers reviewed and merged it within three weeks.&lt;/p&gt;

&lt;p&gt;We felt good. Let's keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where almost nothing happened
&lt;/h2&gt;

&lt;p&gt;We kept going. Over two weeks we replaced 9 direct dependencies with standard-library code or a few lines of our own. &lt;code&gt;go.mod&lt;/code&gt; went from 60 direct dependencies to 51.&lt;/p&gt;

&lt;p&gt;Then we looked at what had actually left the build. &lt;code&gt;go.sum&lt;/code&gt; went from 1,220 lines to about 1,210. Only 5 modules disappeared completely. Most of the packages we had stopped calling were still required by other modules, so they stayed in the dependency list anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a dependency takes one line. Removing one can take real work and barely change what you ship.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just like a real diet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked: removing a whole layer
&lt;/h2&gt;

&lt;p&gt;So we stopped picking off packages one by one and looked at which layer they came in through.&lt;/p&gt;

&lt;p&gt;Vuls uses Trivy's parsers to read lockfiles (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;, and so on). We called them through &lt;strong&gt;fanal&lt;/strong&gt;, Trivy's analysis framework. fanal is built for everything Trivy scans — container images, disk images, filesystems, infrastructure-as-code. That breadth is exactly what Trivy needs. Our scanner only needed the part that reads lockfiles, but the way we had wired fanal in registered 25+ analyzers through blank imports, and with them the infrastructure-as-code and misconfiguration packages that Trivy needs and we never called.&lt;/p&gt;

&lt;p&gt;We changed Vuls to &lt;strong&gt;call Trivy's lockfile parsers directly&lt;/strong&gt;. Much less code compiled in, and — as we checked below — the same results on almost every project we tested. (Vuls still depends on Trivy — we use its parsers, just not through fanal.)&lt;/p&gt;

&lt;p&gt;The result (&lt;a href="https://github.com/future-architect/vuls/pull/2476" rel="noopener noreferrer"&gt;vuls PR #2476&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;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scanner binary&lt;/td&gt;
&lt;td&gt;106.6 MB&lt;/td&gt;
&lt;td&gt;34.1 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-68%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanner build time&lt;/td&gt;
&lt;td&gt;7.8 s&lt;/td&gt;
&lt;td&gt;3.2 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-59%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How we measured: scanner-only builds (&lt;code&gt;-tags scanner&lt;/code&gt;) with our release build settings, &lt;code&gt;CGO_ENABLED=0 GOEXPERIMENT=jsonv2 go build -a -trimpath -ldflags "-s -w"&lt;/code&gt;. The build times are with a warm build cache.&lt;/p&gt;

&lt;p&gt;To be precise about what shrank: this is the scanner binary. The full vuls binary, which also includes the vulnerability detection and reporting code, went from 139.1 MB to 138.5 MB after this change and a follow-up one. That code still uses Trivy's vulnerability database layer, so most of the weight stays.&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%2Fp0qaipapw8w0y8mgr4zj.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%2Fp0qaipapw8w0y8mgr4zj.png" alt="Left: after vuls removed its direct import of go-homedir, nine other modules, including trivy and go-getter, still required it, so go.sum stayed at 1,220 lines. Right: when vuls stopped going through fanal and called Trivy's lockfile parsers directly, what only fanal had pulled in left the build: 2,345 packages became 737, and the scanner binary went from 106.6 MB to 34.1 MB." width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Packages in the scanner build counted with &lt;code&gt;go list -tags scanner -deps ./cmd/scanner&lt;/code&gt; before and after PR #2476.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't pick leaves one by one. Find the branch they hang from.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How we tested it
&lt;/h2&gt;

&lt;p&gt;The change rewrote how the whole scanner package reads lockfiles. Here's how we checked it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Golden tests.&lt;/strong&gt; JSON output for 35 lockfile types, compared for an exact match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: A/B comparison on real projects.&lt;/strong&gt; We collected real lockfiles from 129 open-source projects across 13 ecosystems. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old code + lockfile → result A
new code + lockfile → result B
diff(A, B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocks. No hand-written expected values. Just: "does the new code produce the same results as the old code?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;127 of 129 were identical.&lt;/strong&gt; The other 2 differed only in the pnpm &lt;code&gt;dev&lt;/code&gt; flag, which our old wrapper code had been losing along the way. Calling Trivy's pnpm parser directly gave us the parser's correct output.&lt;/p&gt;

&lt;p&gt;Building the comparison set taught us something too. On the first pass, many fixtures produced &lt;em&gt;zero&lt;/em&gt; packages — &lt;code&gt;requirements.txt&lt;/code&gt; files with no pinned versions, a nearly empty &lt;code&gt;yarn.lock&lt;/code&gt;, a root &lt;code&gt;pom.xml&lt;/code&gt; with only &lt;code&gt;&amp;lt;modules&amp;gt;&lt;/code&gt;. "Identical" on an empty result proves nothing, so we replaced those fixtures with ones that actually exercised the parsers.&lt;/p&gt;

&lt;p&gt;This lives in vuls as &lt;code&gt;make compare-lockfile&lt;/code&gt;. It didn't catch everything: code review and testing on Windows still found a case the comparison had missed (&lt;code&gt;.exe&lt;/code&gt; files weren't detected as executables), which we fixed before merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool we built afterwards: &lt;code&gt;uzomuzo diet&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We did all of the above by hand, in March 2026. Looking back, the expensive part wasn't writing code. It was deciding &lt;strong&gt;where to start&lt;/strong&gt; — and our first choice was wrong.&lt;/p&gt;

&lt;p&gt;So in April we added a &lt;code&gt;diet&lt;/code&gt; command to &lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;uzomuzo&lt;/a&gt;, our dependency lifecycle scanner. It ranks your direct dependencies as candidates for removal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dependency graph&lt;/strong&gt; — from a CycloneDX SBOM: what would disappear along with each direct dependency?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source coupling&lt;/strong&gt; — tree-sitter reads your code: how many files import it, how many call sites?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health&lt;/strong&gt; — maintenance status (Active, Stalled, Archived, ...), from the same signals &lt;code&gt;uzomuzo scan&lt;/code&gt; uses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — high impact, high risk, and easy to remove rises to the top&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;scan&lt;/code&gt; and &lt;code&gt;diet&lt;/code&gt; are rule-based. No LLM is involved. Their results reflect the dependency and maintenance data available when you run them.&lt;/p&gt;

&lt;p&gt;One caveat up front: &lt;strong&gt;&lt;code&gt;diet&lt;/code&gt; is experimental.&lt;/strong&gt; An audit against hand-checked expected values found cases where a dependency that is in use is reported as &lt;code&gt;unused&lt;/code&gt; or &lt;code&gt;trivial&lt;/code&gt; — for example when it's imported from a skipped directory or in an import style the analyzer doesn't recognize yet. The CLI prints a notice saying so after every run, and the &lt;a href="https://github.com/future-architect/uzomuzo-oss/blob/main/docs/diet.md#accuracy" rel="noopener noreferrer"&gt;Accuracy section of the docs&lt;/a&gt; lists what to check. Treat its output as a list of places to look, not a list of things to delete.&lt;/p&gt;

&lt;p&gt;Here's an excerpt from a run on &lt;a href="https://github.com/hashicorp/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; (commit &lt;code&gt;9ec49e0&lt;/code&gt;, October 13, 2026; SBOM generated with &lt;code&gt;cyclonedx-gomod&lt;/code&gt;; run on October 26, 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uzomuzo diet --sbom terraform-bom.json --source ./terraform

RANK  SCORE  EFFORT    PURL                          REMOVES  IMPORTS  CALLS  STATUS
3     0.13   trivial   github.com/mitchellh/gox            1        1      1  Archived
5     0.10   hard      github.com/mitchellh/colorstring    0       22     79  Archived
6     0.10   moderate  github.com/mitchellh/go-homedir     0        9     11  Archived
 ...
72    0.00   hard      github.com/hashicorp/hcl/v2         0      352   6764  Active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Edited for width: four of the 77 direct dependencies, PURLs shortened to module paths, and the &lt;code&gt;REMAINS&lt;/code&gt; column omitted. Ranks 1, 2 and 4 are not shown: 1 and 4 are Terraform's own nested &lt;code&gt;internal/backend/remote-state/*&lt;/code&gt; modules, which the SBOM lists as direct dependencies, and 2 is an active OpenTelemetry package. The full run also reported zero "quick wins".)&lt;/p&gt;

&lt;p&gt;The ranking is a starting point for investigation, not a verdict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gox&lt;/code&gt; is archived and has one import and one call site, so it gets a &lt;code&gt;trivial&lt;/code&gt; effort label. That's exactly the kind of label to verify: check that one call site, what it does, and what replaces it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go-homedir&lt;/code&gt; is the same package we removed from vuls in 15 minutes. Here it's imported in 9 files, so it's rated moderate. Removing the direct use wouldn't drop it from Terraform's dependency list either — another module still requires it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hcl/v2&lt;/code&gt; is at rank 72: 352 importing files, 6,764 call sites, and actively maintained. &lt;strong&gt;Don't start here.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works for Go, Python, JavaScript/TypeScript, and Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last mile: Claude Code skills
&lt;/h2&gt;

&lt;p&gt;Ranking is automated. Actually removing a dependency — tracing what data flows through it, writing the replacement, getting the change accepted — still takes judgment. uzomuzo ships three &lt;a href="https://github.com/future-architect/uzomuzo-oss/tree/main/claude-skills" rel="noopener noreferrer"&gt;Claude Code skills&lt;/a&gt; for that part:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/diet-assess-risk&lt;/code&gt; (how risky is it to keep?), &lt;code&gt;/diet-evaluate-removal&lt;/code&gt; (is it worth the effort?), and &lt;code&gt;/diet-remove&lt;/code&gt; (draft the change or the upstream report). They use an LLM, so review what they propose before you use it.&lt;/p&gt;

&lt;p&gt;The rules in &lt;code&gt;/diet-remove&lt;/code&gt; come from our own mistakes. When we found that Next.js still depended on &lt;code&gt;@vercel/kv&lt;/code&gt;, which Vercel has deprecated on npm in favor of Upstash Redis, we tried three channels by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR&lt;/strong&gt; → we closed it ourselves after CI failed. It's a large pnpm-workspaces monorepo, and we couldn't reproduce its CI locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue&lt;/strong&gt; → the repository's bot closed it within seconds, pointing to the bug report template. The issue tracker is for bugs with a reproduction, and ours wasn't one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussion&lt;/strong&gt; → the channel the repository's templates point to. &lt;a href="https://github.com/vercel/next.js/discussions/92479" rel="noopener noreferrer"&gt;It's still open&lt;/a&gt; with no reply yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the skill now defaults to drafting an issue rather than a PR for projects you don't own, checks whether the repository accepts blank issues, and falls back to a Discussion when it doesn't. Those rules are written from that manual experience; we haven't yet published an end-to-end run of the skill itself. And getting the channel right doesn't guarantee a response. Of our four upstream reports in April, two have been fixed and two are still waiting. Whether to fix something is the maintainers' call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start your diet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@latest
&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/future-architect/uzomuzo-oss/cmd/uzomuzo-diet@latest   &lt;span class="c"&gt;# needs a C compiler&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# read-only is enough; gives uzomuzo repository signals such as archived status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output in this post was produced with v0.10.0.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;uzomuzo scan --file go.mod&lt;/code&gt; (or pipe in an SBOM for other ecosystems). See what's unmaintained.&lt;/li&gt;
&lt;li&gt;Generate a CycloneDX SBOM — for example &lt;code&gt;trivy fs . --format cyclonedx -o bom.json&lt;/code&gt;, or for Go, &lt;code&gt;cyclonedx-gomod mod -json -output bom.json&lt;/code&gt; — and run &lt;code&gt;uzomuzo diet --sbom bom.json --source .&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pick a dependency whose use and replacement you understand. Remove it, then measure: binary size, dependency list, build time. If nothing moved, look for the layer it came in through.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And don't give up on your diet.&lt;/p&gt;




&lt;p&gt;Earlier posts: &lt;a href="https://dev.to/kotakanbe/your-dependencies-are-48-unmaintained-and-sca-tools-cant-see-it-2h7h"&gt;"Your dependencies are 48% unmaintained — and SCA tools can't see it"&lt;/a&gt; and &lt;a href="https://dev.to/kotakanbe/your-ci-runs-other-peoples-code-is-anyone-still-maintaining-it-19c5"&gt;"Your CI runs other people's code. Is anyone still maintaining it?"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source and docs: &lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;github.com/future-architect/uzomuzo-oss&lt;/a&gt; (Apache 2.0)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kota Kanbe — Creator of &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt;, an open-source vulnerability scanner.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your CI runs other people's code. Is anyone still maintaining it?</title>
      <dc:creator>kotakanbe</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:54:49 +0000</pubDate>
      <link>https://dev.to/kotakanbe/your-ci-runs-other-peoples-code-is-anyone-still-maintaining-it-19c5</link>
      <guid>https://dev.to/kotakanbe/your-ci-runs-other-peoples-code-is-anyone-still-maintaining-it-19c5</guid>
      <description>&lt;p&gt;Every third-party &lt;code&gt;uses:&lt;/code&gt; line in a GitHub Actions workflow runs someone else's code in your job. Depending on the workflow, that code can reach your &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, the secrets passed to that step, and your checked-out source.&lt;/p&gt;

&lt;p&gt;In March 2025, the widely used &lt;code&gt;tj-actions/changed-files&lt;/code&gt; Action was compromised: its tags were pointed at a malicious commit that printed CI secrets into workflow logs (&lt;a href="https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-github-action-cve-2025-30066" rel="noopener noreferrer"&gt;CISA alert, CVE-2025-30066&lt;/a&gt;). That project was actively maintained — a maintenance check would not have prevented it — and a fixed release followed.&lt;/p&gt;

&lt;p&gt;That last part is the point of this post. When something goes wrong in an Action that is still maintained, a fix comes out and you upgrade. When the Action is no longer maintained, there is no next release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question workflow scanners don't ask
&lt;/h2&gt;

&lt;p&gt;There are good tools for securing your workflows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenSSF Scorecard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your repo's practices: dangerous workflow patterns, token permissions, pinned dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;zizmor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your workflow files: injection risks, &lt;code&gt;pull_request_target&lt;/code&gt; misuse, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;actionlint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your workflow syntax and expressions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three look at &lt;strong&gt;your&lt;/strong&gt; workflow. Scorecard does have a &lt;em&gt;Maintained&lt;/em&gt; check, and it gives an archived repository the lowest score — but only for the repository you run it on. Few teams run Scorecard against each of the 30-odd Actions their workflows reference.&lt;/p&gt;

&lt;p&gt;Pinning Actions by commit SHA is good practice. It stops a tag from being silently repointed. But it doesn't tell you whether anyone will ship a fix for the code you pinned.&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%2Fneh10l6g0294pmg1cul3.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%2Fneh10l6g0294pmg1cul3.png" alt="Three layers of code run in your job: your workflow file, the Actions its uses: lines reference, and Actions called inside composite Actions. Scorecard, zizmor and actionlint read the first layer. uzomuzo with --include-actions checks the repository behind each referenced Action, such as the archived tibdex/github-app-token in Grafana's release-build.yml; --show-transitive adds the third layer. You can fix the second layer by changing a uses: line; the third needs the parent Action's maintainer." width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;uzomuzo&lt;/a&gt; does that inventory for you: it reads your workflows, lists every Action they reference, and reports the maintenance status of each. It complements the tools above — it doesn't replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found in Grafana (April 2026)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/grafana/grafana" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; is one of the most popular open-source projects on GitHub. In April 2026 we scanned the Actions referenced in its workflows. One of them was archived: &lt;a href="https://github.com/tibdex/github-app-token" rel="noopener noreferrer"&gt;&lt;code&gt;tibdex/github-app-token&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repository was archived in July 2025 (its last push is July 7, 2025). Its README says the Action is deprecated and points to GitHub's official &lt;a href="https://github.com/actions/create-github-app-token" rel="noopener noreferrer"&gt;&lt;code&gt;actions/create-github-app-token&lt;/code&gt;&lt;/a&gt; — the maintainer did exactly the right thing.&lt;/li&gt;
&lt;li&gt;Grafana used it in &lt;strong&gt;14 places across 13 workflow files&lt;/strong&gt;, including &lt;code&gt;release-build.yml&lt;/code&gt; and &lt;code&gt;release-npm.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It declares &lt;code&gt;runs.using: node20&lt;/code&gt;. GitHub &lt;a href="https://github.blog/changelog/2026-09-23-node-20-is-no-longer-available-in-github-actions" rel="noopener noreferrer"&gt;removed Node 20 from Actions runners on September 23, 2026&lt;/a&gt;, and an archived Action can't be updated to Node 24. Anyone still using it after that date has a broken workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An Action runs in your job, with access to its workspace, not in an isolated sandbox. Depending on the workflow, it can reach &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; (with whatever permissions the job grants), the secrets passed to it, and the checked-out source. Release jobs often hold sensitive credentials. That doesn't mean anything was exposed here — it means an archived Action in a release job deserves a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened next
&lt;/h2&gt;

&lt;p&gt;We filed &lt;a href="https://github.com/grafana/grafana/issues/121911" rel="noopener noreferrer"&gt;an issue&lt;/a&gt; on April 5, 2026.&lt;/p&gt;

&lt;p&gt;Grafana had already begun moving to &lt;code&gt;actions/create-github-app-token&lt;/code&gt; in one workflow in October 2025. A community member opened &lt;a href="https://github.com/grafana/grafana/pull/122122" rel="noopener noreferrer"&gt;a PR&lt;/a&gt; for the remaining files three days after our issue. A maintainer closed it, explaining that the team had a new recommended approach using their own internal workflow. We don't know whether our issue influenced their timing.&lt;/p&gt;

&lt;p&gt;On September 15, 2026, the issue was closed as completed: &lt;em&gt;"This has been done, we no longer use that action :)"&lt;/em&gt; — a week before Node 20 was removed.&lt;/p&gt;

&lt;p&gt;When we scanned Grafana's workflows again on October 26, 2026, &lt;code&gt;tibdex/github-app-token&lt;/code&gt; no longer appeared, and none of the 32 Actions was archived or got a &lt;code&gt;replace&lt;/code&gt; verdict.&lt;/p&gt;

&lt;p&gt;Reporting doesn't always lead to a fix. Of the four upstream reports we filed in April, two have been resolved and two have had no response yet. Whether and how to fix something is the maintainers' call. What a scanner can do is make sure the question gets asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scan your own Actions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download a binary from &lt;a href="https://github.com/future-architect/uzomuzo-oss/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;. You need v0.8.0 or later for &lt;code&gt;--fail-on archived&lt;/code&gt; and the &lt;code&gt;· archived&lt;/code&gt; marker. The examples below assume &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is set: scanning a repository's Actions reads workflow files through the GitHub API, and the token also gives uzomuzo the repository signals (archived, commit history) it needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check a repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# a read-only token is enough&lt;/span&gt;

&lt;span class="c"&gt;# Actions referenced directly in your workflows&lt;/span&gt;
uzomuzo scan https://github.com/your-org/your-repo &lt;span class="nt"&gt;--include-actions&lt;/span&gt;

&lt;span class="c"&gt;# Also include Actions used inside composite Actions&lt;/span&gt;
uzomuzo scan https://github.com/your-org/your-repo &lt;span class="nt"&gt;--include-actions&lt;/span&gt; &lt;span class="nt"&gt;--show-transitive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A composite Action can itself &lt;code&gt;uses:&lt;/code&gt; other Actions. Those run in your job too. The difference is who can fix them: you can swap a direct Action today, but a transitive one needs the parent Action's maintainer to act.&lt;/p&gt;

&lt;p&gt;You can also scan a single Action or workflow file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uzomuzo scan https://github.com/tibdex/github-app-token
uzomuzo scan &lt;span class="nt"&gt;--file&lt;/span&gt; .github/workflows/ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What an archived Action looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uzomuzo scan https://github.com/tibdex/github-app-token https://github.com/actions/github-script
STATUS      PURL                                        LIFECYCLE           BUILD
🔴 replace  https://github.com/tibdex/github-app-token  Stalled · archived  —
⚠️ caution  https://github.com/actions/github-script    Stalled             —
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both Actions are &lt;code&gt;Stalled&lt;/code&gt;: neither has had much recent activity. Only one is archived, and that's the one marked &lt;code&gt;· archived&lt;/code&gt; and given a &lt;code&gt;replace&lt;/code&gt; verdict.&lt;/p&gt;

&lt;p&gt;The lifecycle label stays &lt;code&gt;Stalled&lt;/code&gt; rather than &lt;code&gt;EOL-Confirmed&lt;/code&gt; on purpose. uzomuzo uses &lt;code&gt;EOL-Confirmed&lt;/code&gt; only for an explicit end-of-life signal, such as an npm deprecation or a Packagist "abandoned" flag. Archiving alone isn't one: some archived repositories are packages that moved to a monorepo and keep shipping releases from there. So the archive is shown as a separate fact next to the label instead. (tibdex's README does say "This action is deprecated", but uzomuzo deliberately doesn't treat README prose as a signal — free text is easy to misread.)&lt;/p&gt;

&lt;p&gt;If you read the earlier post in this series: its table showed archived repositories as &lt;code&gt;EOL-Confirmed&lt;/code&gt;. Current versions show them as above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate your CI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uzomuzo scan https://github.com/your-org/your-repo &lt;span class="nt"&gt;--include-actions&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; archived
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--fail-on archived&lt;/code&gt; fails the build when any referenced Action's repository is archived — and, like every &lt;code&gt;--fail-on&lt;/code&gt; value, when a version is known to be malicious. It doesn't fire on Actions that are merely quiet. That matters: &lt;code&gt;--fail-on stalled&lt;/code&gt; would also catch archived Actions, but in our October scan of Grafana it would have failed on four Actions that are simply less active, including &lt;code&gt;actions/github-script&lt;/code&gt;. &lt;code&gt;--fail-on eol-confirmed&lt;/code&gt; alone wouldn't fail on an archived Action at all.&lt;/p&gt;

&lt;p&gt;Start with a report-only run, look at what comes back, then turn the gate on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run it on a schedule
&lt;/h3&gt;

&lt;p&gt;The uzomuzo repository has a &lt;a href="https://github.com/future-architect/uzomuzo-oss/blob/main/.github/workflows/dependency-scan.yml" rel="noopener noreferrer"&gt;monthly workflow&lt;/a&gt; that generates an SBOM with &lt;code&gt;aquasecurity/trivy-action&lt;/code&gt;, runs &lt;code&gt;uzomuzo scan --sbom&lt;/code&gt;, and opens an issue with the results — SCA and lifecycle checks side by side. That workflow covers package dependencies, not Actions. To cover Actions as well, add a step that runs &lt;code&gt;uzomuzo scan https://github.com/$GITHUB_REPOSITORY --include-actions --fail-on archived&lt;/code&gt; with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers, two questions
&lt;/h2&gt;

&lt;p&gt;Scorecard, zizmor, and actionlint answer: &lt;strong&gt;"Is my workflow configured safely?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;uzomuzo answers: &lt;strong&gt;"Is the code my workflow runs still maintained?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A safely configured workflow can still depend on an Action that will never get another release. That isn't a known vulnerability. It's something to review before it becomes a broken build — or worse.&lt;/p&gt;




&lt;p&gt;Earlier post: &lt;a href="https://dev.to/kotakanbe/your-dependencies-are-48-unmaintained-and-sca-tools-cant-see-it-2h7h"&gt;"Your dependencies are 48% unmaintained — and SCA tools can't see it"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source and docs: &lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;github.com/future-architect/uzomuzo-oss&lt;/a&gt; (Apache 2.0)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kota Kanbe — Creator of &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt;, an open-source vulnerability scanner.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your dependencies are 48% unmaintained — and SCA tools can't see it</title>
      <dc:creator>kotakanbe</dc:creator>
      <pubDate>Thu, 16 Apr 2026 22:16:44 +0000</pubDate>
      <link>https://dev.to/kotakanbe/your-dependencies-are-48-unmaintained-and-sca-tools-cant-see-it-2h7h</link>
      <guid>https://dev.to/kotakanbe/your-dependencies-are-48-unmaintained-and-sca-tools-cant-see-it-2h7h</guid>
      <description>&lt;p&gt;I just presented this at &lt;a href="https://www.first.org/conference/vulncon26/program#pThe-CVE-Blind-Spot-Defeating-Hidden-EOLs-and-Repo-Jacking-with-Engineering-Triage-Code-Diet" rel="noopener noreferrer"&gt;VulnCon 2026&lt;/a&gt; (&lt;a href="https://github.com/future-architect/uzomuzo-oss/blob/main/docs/presentations/vulncon2026.pdf" rel="noopener noreferrer"&gt;slides&lt;/a&gt;). Here's the tool and the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The blind spot
&lt;/h2&gt;

&lt;p&gt;Your vulnerability scanner is excellent at finding CVEs. Trivy, Snyk, Grype — they do their job well.&lt;/p&gt;

&lt;p&gt;But there's a category of risk they &lt;strong&gt;cannot see&lt;/strong&gt;: packages that are no longer maintained.&lt;/p&gt;

&lt;p&gt;No maintainer means no security patches. No bug fixes. No one watching. And because no one is looking — &lt;strong&gt;no CVEs get filed&lt;/strong&gt;. Your scanner reports zero vulnerabilities, and you assume it's safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's not safe. It's invisible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our team at FutureVuls analyzed 16,627 packages across 74 organizations running in production (published in &lt;a href="https://www.nikkei.com/article/DGXZQOUE066RT0W6A300C2000000/" rel="noopener noreferrer"&gt;Nikkei&lt;/a&gt; — Japan's largest business newspaper — March 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;%&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢 Active&lt;/td&gt;
&lt;td&gt;40.6%&lt;/td&gt;
&lt;td&gt;Someone is working on it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔵 Legacy-Safe&lt;/td&gt;
&lt;td&gt;10.9%&lt;/td&gt;
&lt;td&gt;Dormant but stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟡 &lt;strong&gt;Stalled&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Activity declining&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴 &lt;strong&gt;EOL&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;End of life&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;48.5% of production dependencies have lifecycle risk.&lt;/strong&gt; Most of it is invisible.&lt;/p&gt;

&lt;p&gt;(These figures come from the FutureVuls survey and its own classification rules. uzomuzo, introduced below, uses similar but not identical rules, so its numbers on the same data would differ.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this look like in practice?
&lt;/h2&gt;

&lt;p&gt;I scanned &lt;a href="https://github.com/hashicorp/vault" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; — the tool you trust with your secrets. 209 dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uzomuzo scan &lt;span class="nt"&gt;--file&lt;/span&gt; vault-go.mod
&lt;span class="go"&gt;
STATUS      PURL                                    LIFECYCLE
✅ ok        cloud.google.com/go/storage@v1.56.1     Active
✅ ok        Azure/azure-sdk-for-go/sdk/azcore       Active
🔴 replace   aws/aws-sdk-go@v1.55.8                  EOL-Confirmed
🔴 replace   mitchellh/copystructure@v1.2.0          EOL-Confirmed
     ... (209 deps total)
── Summary ──────────────────────────────────
│ 209 deps | ✅ 186 ok | ⚠️ 11 caution | 🔴 11 replace
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;11 packages are EOL.&lt;/strong&gt; In a secrets management tool. Most have &lt;strong&gt;zero CVEs&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scary one: &lt;code&gt;copystructure&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Mitchell Hashimoto — Vault's creator — left HashiCorp in 2023. He publicly announced: &lt;a href="https://gist.github.com/mitchellh/90029601268e59a29e64e55bab1c5bdc" rel="noopener noreferrer"&gt;"I very rarely write Go anymore."&lt;/a&gt; He archived 15 libraries at once, including &lt;code&gt;copystructure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unlike &lt;code&gt;mapstructure&lt;/code&gt; (which has a &lt;a href="https://github.com/go-viper/mapstructure" rel="noopener noreferrer"&gt;community fork&lt;/a&gt;), &lt;strong&gt;copystructure has no successor&lt;/strong&gt;. Nobody picked it up.&lt;/p&gt;

&lt;p&gt;Where is it used? &lt;code&gt;vault/acl.go&lt;/code&gt; — &lt;strong&gt;the access control layer&lt;/strong&gt;. It deep-copies the deny rules for each security check.&lt;/p&gt;

&lt;p&gt;If this package were compromised — deny rules silently disappear. No error. No log. No crash. Your scanner will never find this because there is no CVE to find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The risk is not a bug. It's being abandoned.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  From detection to analysis — with an LLM
&lt;/h3&gt;

&lt;p&gt;Finding the EOL package is step one. But &lt;em&gt;what exactly happens if it's compromised?&lt;/em&gt; I used an LLM to trace the data flow and build the attack scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mitchellh/copystructure — Risk: CRITICAL
Lifecycle: Archived. Maintainer left HashiCorp in 2023. CVEs: 0

Data flow:
  IN:  ACL DeniedParameters, MFA methods, ClientToken
  OUT: Deep-copied structs for per-request isolation

Attack scenario:
  1. acl.go: deny rules silently disappear
  2. request.go: ClientToken shared across requests
  3. policy.go: MFA bypass via shared ControlGroup
  → NO errors. NO logs. NO crashes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verdict: CRITICAL.&lt;/strong&gt; Action: self-implement with ~100 lines of reflect-based code, replacing the ~500-line archived dependency.&lt;/p&gt;

&lt;p&gt;This analysis — tracing what data flows through a package, constructing attack scenarios, assessing severity — takes weeks to do manually across 209 dependencies. With uzomuzo + LLM, it takes minutes. I automated this as the &lt;code&gt;/diet-assess-risk&lt;/code&gt; skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built a tool to find this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;&lt;strong&gt;uzomuzo&lt;/strong&gt;&lt;/a&gt; — an open-source tool that detects unmaintained packages that SCA tools miss. The name comes from a Buddhist concept: &lt;em&gt;uzōmuzō&lt;/em&gt; (有象無象) — "the visible and the invisible." That's exactly what your dependency tree is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes it different
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Seven lifecycle stages, not binary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Other tools give you "maintained or not." uzomuzo gives you a spectrum:&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;Signal&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Registry EOL? (npm deprecated, PyPI inactive)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes →&lt;/strong&gt; 🔴 EOL-Confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Archived / Disabled repository?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes →&lt;/strong&gt; 🟡 Stalled, marked &lt;code&gt;· archived&lt;/code&gt; or &lt;code&gt;· disabled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;EOL announced?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes →&lt;/strong&gt; 🟠 EOL-Scheduled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;No recent human commits + HIGH/CRITICAL advisory?&lt;/td&gt;
&lt;td&gt;⚫ EOL-Effective&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;No recent human commits + LOW/MEDIUM advisory?&lt;/td&gt;
&lt;td&gt;🟡 Stalled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;No recent human commits + no advisory?&lt;/td&gt;
&lt;td&gt;🔵 Legacy-Safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Recent human commits + recent publish or VCS-direct?&lt;/td&gt;
&lt;td&gt;🟢 Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Recent human commits + no publish + low Scorecard?&lt;/td&gt;
&lt;td&gt;🟡 Stalled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each check is evaluated &lt;strong&gt;top to bottom&lt;/strong&gt; — first match wins. "Stalled" and "dead" require different responses. Stalled — you watch. EOL — you replace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multiple signal sources.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It combines data from deps.dev (Scorecard, releases, advisories), GitHub API (archive status, commit history), and registry heuristics (PyPI classifiers, npm deprecated flag, Packagist abandoned).&lt;/p&gt;

&lt;p&gt;The judgment is ecosystem-aware. Go delivers via git — commits &lt;em&gt;are&lt;/em&gt; releases. npm requires a registry publish. Same commits, different verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Two-axis evaluation: lifecycle × build integrity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Lifecycle alone isn't enough. A package can be actively maintained but have no signed releases, no reproducible builds, no provenance. uzomuzo evaluates both axes — so you can distinguish "healthy and well-built" from "active but risky."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Not just detection — all the way to removal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the big one. Every other tool stops at detection. Scorecard gives you a score. Trivy gives you a CVE. Then you're on your own.&lt;/p&gt;

&lt;p&gt;uzomuzo goes from &lt;strong&gt;detect → prioritize → remove&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uzomuzo scan&lt;/code&gt; — find EOL packages (every CI build)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uzomuzo diet&lt;/code&gt; — rank by removal priority: graph impact × health risk × coupling effort&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/diet-*&lt;/code&gt; Claude Code skills — assess risk, evaluate removal, execute safely&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it now (30 seconds)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download a binary from &lt;a href="https://github.com/future-architect/uzomuzo-oss/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scan your project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Go project&lt;/span&gt;
uzomuzo scan &lt;span class="nt"&gt;--file&lt;/span&gt; go.mod

&lt;span class="c"&gt;# Any language (via SBOM)&lt;/span&gt;
trivy fs &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; cyclonedx | uzomuzo scan &lt;span class="nt"&gt;--sbom&lt;/span&gt; -

&lt;span class="c"&gt;# GitHub Actions&lt;/span&gt;
uzomuzo scan &lt;span class="nt"&gt;--file&lt;/span&gt; .github/workflows/ci.yml

&lt;span class="c"&gt;# Single package&lt;/span&gt;
uzomuzo scan pkg:npm/express@4.18.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CI gate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fail the build if any EOL dependency is found&lt;/span&gt;
trivy fs &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; cyclonedx &lt;span class="se"&gt;\&lt;/span&gt;
  | uzomuzo scan &lt;span class="nt"&gt;--sbom&lt;/span&gt; - &lt;span class="nt"&gt;--fail-on&lt;/span&gt; eol-confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One pipeline command.&lt;/p&gt;

&lt;p&gt;By default, an SBOM scan skips the dependencies your SBOM marks as transitive, since direct dependencies are the ones you can replace yourself. To check transitive dependencies too, including malicious versions that arrive through them, add &lt;code&gt;--show-transitive&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  I used it on my own project
&lt;/h2&gt;

&lt;p&gt;I ran uzomuzo on my own project — &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt;, an open-source vulnerability scanner with 391 dependencies. Then I removed what I found.&lt;/p&gt;

&lt;p&gt;Scanner binary size: &lt;a href="https://github.com/future-architect/vuls/pull/2476" rel="noopener noreferrer"&gt;106.6 MB → 34.1 MB (&lt;strong&gt;-68%&lt;/strong&gt;)&lt;/a&gt;. I reported my findings upstream — Grafana's maintainer &lt;a href="https://github.com/grafana/grafana/pull/122122" rel="noopener noreferrer"&gt;closed the community PR&lt;/a&gt; and committed to replacing the archived Action internally using their own &lt;a href="https://github.com/grafana/shared-workflows" rel="noopener noreferrer"&gt;shared-workflows&lt;/a&gt;, and finished the migration in September 2026.&lt;/p&gt;

&lt;p&gt;The full story of how I did it — in a follow-up post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot is bigger than you think
&lt;/h2&gt;

&lt;p&gt;I cross-checked SCA results against lifecycle status on real-world projects. The overlap was almost zero — most EOL packages had no CVEs at all. They are completely invisible to SCA tools.&lt;/p&gt;

&lt;p&gt;"No CVE" doesn't mean "no vulnerability." It often means "no one is looking."&lt;/p&gt;

&lt;p&gt;Full methodology and data — in a follow-up post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;30 seconds:&lt;/strong&gt; &lt;code&gt;uzomuzo scan --file go.mod&lt;/code&gt; or &lt;code&gt;uzomuzo scan https://github.com/your/repo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 minutes:&lt;/strong&gt; Add &lt;code&gt;--fail-on eol-confirmed&lt;/code&gt; to your CI pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly:&lt;/strong&gt; Run &lt;code&gt;uzomuzo diet&lt;/code&gt; for a prioritized removal plan&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will be surprised what you find.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Updated October 26, 2026: corrected the survey size (74 organizations, 16,627 packages), clarified that the 68% reduction is for the scanner-only build, removed a dependency count we could not trace to a primary record, and updated the lifecycle table: current versions label an archived repository &lt;code&gt;Stalled&lt;/code&gt; and mark it &lt;code&gt;· archived&lt;/code&gt;, rather than &lt;code&gt;EOL-Confirmed&lt;/code&gt;; and noted that SBOM scans skip transitive dependencies unless you add &lt;code&gt;--show-transitive&lt;/code&gt;. The scan outputs above were captured in April 2026 with an earlier version.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;a href="https://github.com/future-architect/uzomuzo-oss" rel="noopener noreferrer"&gt;github.com/future-architect/uzomuzo-oss&lt;/a&gt; — Apache 2.0&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kota Kanbe — Creator of &lt;a href="https://github.com/future-architect/vuls" rel="noopener noreferrer"&gt;vuls&lt;/a&gt; (12K+ ⭐), presented at VulnCon 2026&lt;/em&gt;&lt;/p&gt;

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