<?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: Thi Ho</title>
    <description>The latest articles on DEV Community by Thi Ho (@thihodev).</description>
    <link>https://dev.to/thihodev</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%2F4236111%2F73638a8c-4ea6-464c-a520-5b03dc0def0b.png</url>
      <title>DEV Community: Thi Ho</title>
      <link>https://dev.to/thihodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thihodev"/>
    <language>en</language>
    <item>
      <title>I benchmarked my malware scanner against GuardDog on 15 fresh samples of npm and PyPI malware. Here is every result, including the losses.</title>
      <dc:creator>Thi Ho</dc:creator>
      <pubDate>Tue, 10 Nov 2026 01:22:25 +0000</pubDate>
      <link>https://dev.to/thihodev/i-benchmarked-my-malware-scanner-against-guarddog-on-15-fresh-samples-of-npm-and-pypi-malware-here-5h5d</link>
      <guid>https://dev.to/thihodev/i-benchmarked-my-malware-scanner-against-guarddog-on-15-fresh-samples-of-npm-and-pypi-malware-here-5h5d</guid>
      <description>&lt;p&gt;Every few weeks another npm or PyPI package turns out to steal tokens on install. The usual advice is "check before you install", but few people have a tool that does the checking, and fewer know how well that tool works.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://github.com/openyard-cloud/vetcode" rel="noopener noreferrer"&gt;vetcode&lt;/a&gt;, an open-source scanner for malicious packages, supply-chain attacks and leaked secrets. Before telling anyone to use it, I wanted to know how it compares with the best free tool in this space. That tool is &lt;a href="https://github.com/DataDog/guarddog" rel="noopener noreferrer"&gt;GuardDog&lt;/a&gt; from Datadog. A recent paper (NPMAnalysis) ranked it first among 13 npm malware detectors.&lt;/p&gt;

&lt;p&gt;This post covers how I measured, every result I got, and where vetcode still loses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; Malicious npm packages come from &lt;a href="https://github.com/lxyeternal/NPMAnalysis" rel="noopener noreferrer"&gt;NPMAnalysis&lt;/a&gt;. Malicious PyPI packages come from the &lt;a href="https://github.com/DataDog/malicious-software-packages-dataset" rel="noopener noreferrer"&gt;Datadog malicious-software-packages dataset&lt;/a&gt;. The clean side is real, popular packages from the npm and PyPI registries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh samples only.&lt;/strong&gt; Each run draws a random sample with a new seed. I tune rules on one seed, then confirm on a seed that has never been looked at. Every number below comes from a confirmation seed. A scanner tuned and scored on the same samples will always look good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same packages for both tools.&lt;/strong&gt; Both scanners read exactly the same tarballs. Nothing is installed or executed; both tools only read files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GuardDog gets its best verdict.&lt;/strong&gt; GuardDog has no single yes/no answer, so I scored it four ways: any issue, any threat rule, risk score ≥ 5, and risk score ≥ 7. It is credited with whichever gives the best F1 on each sample. vetcode is scored one way only: "flagged at severity high or above".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;F1&lt;/strong&gt; combines precision and recall into one number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision&lt;/strong&gt; is the share of flagged packages that really are malicious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall&lt;/strong&gt; is the share of malicious packages caught.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False alarms&lt;/strong&gt; is the share of clean packages wrongly flagged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;False alarms matter more than they look. A tool that flags 1 in 5 popular packages gets switched off.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm: eight fresh samples
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;vetcode F1 · P · R · FA&lt;/th&gt;
&lt;th&gt;GuardDog F1 · P · R · FA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;seed 14 (500 + 500)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;93.3&lt;/strong&gt; · 96.4 · 90.4 · 3.4%&lt;/td&gt;
&lt;td&gt;92.8 · 95.7 · 90.0 · 4.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 16 (500 + 500)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;94.4&lt;/strong&gt; · 97.6 · 91.4 · 2.2%&lt;/td&gt;
&lt;td&gt;93.3 · 95.4 · 91.2 · 4.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 18 (150 + 150)&lt;/td&gt;
&lt;td&gt;92.6 · 97.8 · 88.0 · 2.0%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;93.2&lt;/strong&gt; · 94.5 · 92.0 · 5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 20 (500 + 499)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;95.1&lt;/strong&gt; · 98.3 · 92.2 · 1.6%&lt;/td&gt;
&lt;td&gt;93.0 · 96.0 · 90.2 · 3.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 21 (500 + 498)&lt;/td&gt;
&lt;td&gt;94.1 · 97.2 · 91.2 · 2.6%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;94.2&lt;/strong&gt; · 97.4 · 91.2 · 2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 23 (150 + 150)&lt;/td&gt;
&lt;td&gt;93.5 · 96.5 · 90.7 · 3.3%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;94.6&lt;/strong&gt; · 96.5 · 92.7 · 3.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 26 (500 + 499)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;96.1&lt;/strong&gt; · 98.3 · 94.0 · 1.6%&lt;/td&gt;
&lt;td&gt;93.0 · 93.5 · 92.4 · 6.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 27 (150 + 150)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;95.5&lt;/strong&gt; · 98.6 · 92.7 · 1.3%&lt;/td&gt;
&lt;td&gt;93.9 · 95.2 · 92.7 · 4.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vetcode wins five of eight. GuardDog wins three, by 0.1 to 1.1 F1 points. Two of its wins are on the small 300-package samples, where one package moves F1 by about half a point.&lt;/p&gt;

&lt;p&gt;The more consistent difference is false alarms. vetcode flags 1.3–3.4% of clean packages; GuardDog flags 2.4–6.4%. On the latest 1,000-package sample that is 8 clean packages against 32.&lt;/p&gt;

&lt;h2&gt;
  
  
  PyPI: seven fresh samples
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;vetcode F1 · P · R · FA&lt;/th&gt;
&lt;th&gt;GuardDog F1 · P · R · FA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;seed 12 (300 + 216)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;87.4&lt;/strong&gt; · 93.5 · 82.0 · 7.9%&lt;/td&gt;
&lt;td&gt;81.3 · 83.0 · 79.7 · 22.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 15 (300 + 216)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;84.1&lt;/strong&gt; · 93.1 · 76.7 · 7.9%&lt;/td&gt;
&lt;td&gt;81.1 · 82.9 · 79.3 · 22.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 17 (300 + 216)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;86.4&lt;/strong&gt; · 93.4 · 80.3 · 7.9%&lt;/td&gt;
&lt;td&gt;78.3 · 78.5 · 78.0 · 29.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 19 (150 + 150)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;89.0&lt;/strong&gt; · 95.4 · 83.3 · 4.0%&lt;/td&gt;
&lt;td&gt;80.0 · 80.0 · 80.0 · 20.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 22 (300 + 152)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;89.2&lt;/strong&gt; · 96.5 · 83.0 · 5.9%&lt;/td&gt;
&lt;td&gt;85.0 · 87.9 · 82.3 · 22.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 24 (150 + 150)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;89.0&lt;/strong&gt; · 95.4 · 83.3 · 4.0%&lt;/td&gt;
&lt;td&gt;80.0 · 80.0 · 80.0 · 20.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 28 (150 + 150)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;88.6&lt;/strong&gt; · 95.4 · 82.7 · 4.0%&lt;/td&gt;
&lt;td&gt;79.6 · 79.9 · 79.3 · 20.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here the gap is clear. GuardDog's PyPI rules fire on many ordinary packages: 20–30% of popular packages get flagged at its best setting.&lt;/p&gt;

&lt;p&gt;Seed 15 is the one I learned most from. GuardDog caught more malware there (79.3% vs 76.7%). Two kinds of package were behind the gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;beacons hidden inside functions;&lt;/li&gt;
&lt;li&gt;droppers that write an &lt;code&gt;.exe&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added rules for both, and recall went back above GuardDog's on the next fresh sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disguised malware
&lt;/h2&gt;

&lt;p&gt;Malware authors know scanners read source code, so they rewrite it. I wrote a script that rewrites caught samples the same way, without running anything. Then I checked how many each tool still catches. The table shows the share of previously caught samples still caught:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewrite&lt;/th&gt;
&lt;th&gt;vetcode npm&lt;/th&gt;
&lt;th&gt;GuardDog npm&lt;/th&gt;
&lt;th&gt;vetcode PyPI&lt;/th&gt;
&lt;th&gt;GuardDog PyPI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;strings split and joined with &lt;code&gt;+&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;95.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;\xNN&lt;/code&gt; escapes&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;93.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;built from character codes (&lt;code&gt;String.fromCharCode&lt;/code&gt;, &lt;code&gt;chr&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;98.4%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;90.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings written backwards&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;97.6%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;90.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;whole file base64-packed into &lt;code&gt;eval&lt;/code&gt;/&lt;code&gt;exec&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;92.2%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gzip, then base64&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;98.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;code pushed 400 spaces off screen&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;moved into &lt;code&gt;dist/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vetcode decodes these tricks before its rules run, and it labels what it found "(hidden)". It does this statically: joins split strings, decodes escapes, rebuilds character-code strings, reverses reversed ones, and unpacks base64 or zlib payloads up to three layers deep.&lt;/p&gt;

&lt;p&gt;Building this benchmark also found a real bug: one of vetcode's own regexes could hang for minutes on a 500 KB base64 line. Measuring hostile input finds things that clean input never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things the datasets barely contain
&lt;/h2&gt;

&lt;p&gt;Public datasets are full of 2021–2023 typosquats. Newer techniques are rare in them, so I keep a separate set of 15 hand-built cases from recent campaigns. Each one sits next to a clean look-alike that must not be flagged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GlassWorm's invisible Unicode payloads;&lt;/li&gt;
&lt;li&gt;code hidden off-screen in &lt;code&gt;dist/&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;payloads inside images;&lt;/li&gt;
&lt;li&gt;protestware and time bombs;&lt;/li&gt;
&lt;li&gt;homoglyph package names;&lt;/li&gt;
&lt;li&gt;a Rust &lt;code&gt;build.rs&lt;/code&gt; that reads &lt;code&gt;~/.ssh&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a Visual Studio project that runs encoded PowerShell at build time;&lt;/li&gt;
&lt;li&gt;a malicious agent skill in the style of ClawHavoc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;vetcode catches 15 of 15 with no false alarms on the look-alikes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed
&lt;/h2&gt;

&lt;p&gt;On the same 30 PyPI packages (659 MB), GuardDog took 1,104 seconds and vetcode 299. Since then vetcode has become another third faster. It has zero dependencies: Node 18+ and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The datasets favour GuardDog.&lt;/strong&gt; Many packages in them were found by GuardDog in the first place, which helps it on recall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samples are small.&lt;/strong&gt; They range from 300 to 1,000 packages, and one or two F1 points between seeds is normal variation. That is why I publish every seed, not the best one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules alone are not the whole story.&lt;/strong&gt; vetcode is designed to run inside an AI coding agent (Claude Code, Codex, Cursor, Copilot and others). The agent re-reads each finding before reporting it. None of the numbers above include that step; they are the scanner alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent skills are hard for everyone.&lt;/strong&gt; On 204 malicious and 132 clean agent skills, vetcode's rules catch 29.9% with 3.0% false alarms. Cisco's skill-scanner catches 23.5% with 15.9%. Most malicious skills are plain-English instructions, which rules cannot judge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, or check my work
&lt;/h2&gt;

&lt;p&gt;Scan a project, a package you are about to install, or a repo you just cloned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @openyard/vetcode .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or give it to your AI agent as a skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add openyard-cloud/vetcode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also runs as an MCP server and as a GitHub Action that comments on pull requests.&lt;/p&gt;

&lt;p&gt;Every script used here is in &lt;a href="https://github.com/openyard-cloud/vetcode/tree/main/bench" rel="noopener noreferrer"&gt;&lt;code&gt;bench/&lt;/code&gt;&lt;/a&gt;. If you run it and get different numbers, or find a malicious package vetcode misses, please &lt;a href="https://github.com/openyard-cloud/vetcode/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. That is the most useful thing you can send me.&lt;/p&gt;

&lt;p&gt;vetcode is MIT-licensed and part of &lt;a href="https://openyard.cloud" rel="noopener noreferrer"&gt;OpenYard&lt;/a&gt;, a small community of open-source projects.&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
