Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/assets/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ td code{color:var(--accent)}
background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--accent);
border-radius:8px;padding:17px 19px;display:flex;flex-direction:column;gap:7px;
}
.callout h3{color:var(--accent)}
.callout h2,.callout h3{color:var(--accent)}
.callout p{font-size:.95rem}

/* footer lives in the components block below */
Expand Down Expand Up @@ -485,7 +485,7 @@ td code{color:var(--accent)}
.rel-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px}
.rel-v{
font-family:var(--disp);font-size:1.45rem;font-weight:800;letter-spacing:-.02em;color:var(--ink);
}
;margin:0;display:inline-block}
.rel-date{font-family:var(--mono);font-size:.74rem;color:var(--muted)}
.tag{
font-family:var(--mono);font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;
Expand Down
156 changes: 154 additions & 2 deletions docs/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ build_page(){
title="$(meta title "$src")"
desc="$(meta desc "$src")"
navkey="$(meta nav "$src")"
# short label for breadcrumbs: the title up to its first em dash
crumb="$(printf '%s' "$title" | sed 's/ *—.*//')"
prev="$(meta prev "$src")"
next="$(meta next "$src")"

Expand Down Expand Up @@ -71,6 +73,69 @@ build_page(){
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
HEAD

# Breadcrumbs on every page: gives a crawler the site hierarchy, and is
# what produces the path shown under a search result.
if [ "$out" != index.html ]; then
cat <<CRUMB
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"snare","item":"$SITE"},
{"@type":"ListItem","position":2,"name":"$crumb","item":"$SITE$out"}]}
</script>
CRUMB
fi

# Page-specific structured data. A HowTo on the response walkthrough is the
# one that matters: "how do I remove this malware" is what someone types
# mid-incident, and HowTo is the type that answers it.
case "$out" in
infected.html)
cat <<HOWTO
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"HowTo",
"name":"Remove a committed supply-chain dropper from your repositories",
"description":"$desc","url":"$SITE$out",
"totalTime":"PT30M",
"tool":[{"@type":"HowToTool","name":"snare"}],
"step":[
{"@type":"HowToStep","position":1,"name":"Rotate your credentials",
"text":"Stealing credentials is the objective; removing the payload does not un-steal a token. Revoke npm write tokens first, then GitHub tokens, SSH keys and cloud keys.","url":"$SITE$out#rotate"},
{"@type":"HowToStep","position":2,"name":"Clean the machine you push from",
"text":"This family injects into commits as they leave an already-infected machine, so cleaning a repository first is wasted work.","url":"$SITE$out#machine"},
{"@type":"HowToStep","position":3,"name":"Clean the repositories",
"text":"snare fix is a dry run by default and always backs up first. Purging history rewrites every commit SHA, so every clone must be re-cloned rather than pulled.","url":"$SITE$out#repos"},
{"@type":"HowToStep","position":4,"name":"Tell your collaborators",
"text":"They may be infected from the same source, and a rewritten history breaks their clones without explanation.","url":"$SITE$out#notify"}]}
</script>
HOWTO
;;
install.html)
cat <<INST
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"snare",
"applicationCategory":"SecurityApplication","operatingSystem":"macOS, Linux, Windows, WSL",
"softwareVersion":"$VERSION","url":"$SITE$out","codeRepository":"$REPO",
"license":"https://opensource.org/licenses/MIT",
"description":"$desc",
"offers":{"@type":"Offer","price":"0","priceCurrency":"USD"}}
</script>
INST
;;
check.html)
cat <<FAQ
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
{"@type":"Question","name":"How do I know if my machine is infected with this malware?",
"acceptedAnswer":{"@type":"Answer","text":"Look for a node process running inline code with obfuscated globals, any file carrying the operator's wallet address, a .vscode/tasks.json task set to runOn folderOpen, and font files whose first four bytes are not a real font magic. A genuine .woff2 begins with wOF2."}},
{"@type":"Question","name":"Does npm audit detect this?",
"acceptedAnswer":{"@type":"Answer","text":"No. The dropper is committed into the repository itself rather than pulled from the registry, so there is no malicious dependency for npm audit, the lockfile or Dependabot to report."}},
{"@type":"Question","name":"My process check came back clean. Am I safe?",
"acceptedAnswer":{"@type":"Answer","text":"Not necessarily. The loader runs when a build runs or an editor opens the folder, then exits. A clean process check alongside infected repositories is the expected result, not a contradiction."}}]}
</script>
FAQ
;;
esac

# keyword + structured data only on the entry page
if [ "$out" = index.html ]; then
cat <<'KW'
Expand Down Expand Up @@ -101,6 +166,7 @@ JSONLD

# ---- nav ----
cat <<'NAVTOP'
<header class="site-header">
<nav class="nav" aria-label="Primary">
<div class="nav-in">
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
Expand Down Expand Up @@ -128,6 +194,7 @@ NAVTOP
</div>
</div>
</nav>
</header>

<main id="main">
NAVBOT
Expand Down Expand Up @@ -207,15 +274,100 @@ done
infected.html|commands.html|community.html) loc="$SITE$page"; pri="0.8" ;;
*) loc="$SITE$page"; pri="0.6" ;;
esac
printf ' <url><loc>%s</loc><priority>%s</priority></url>\n' "$loc" "$pri"
lm="$(date -u -r "$page" '+%Y-%m-%d' 2>/dev/null || date -u '+%Y-%m-%d')"
printf ' <url><loc>%s</loc><lastmod>%s</lastmod><priority>%s</priority></url>\n' "$loc" "$lm" "$pri"
done
echo '</urlset>'
} > sitemap.xml

{
echo '# snare — a free, open-source scanner for supply-chain malware'
echo '# committed directly into git repositories.'
echo '#'
echo '# Everything here is public and free to index, quote and train on.'
echo '# The whole point is that people find this before they are compromised,'
echo '# so the crawlers below are named explicitly rather than left to infer'
echo '# permission from the wildcard.'
echo
echo 'User-agent: *'
echo 'Allow: /'
echo
for ua in Googlebot Bingbot DuckDuckBot Slurp Baiduspider YandexBot \
GPTBot ChatGPT-User OAI-SearchBot ClaudeBot Claude-User \
Claude-SearchBot anthropic-ai PerplexityBot Perplexity-User \
Google-Extended Applebot Applebot-Extended Amazonbot Bytespider \
CCBot cohere-ai Diffbot FacebookBot meta-externalagent \
MistralAI-User TimpiBot YouBot; do
echo "User-agent: $ua"
echo 'Allow: /'
echo
done
echo "Sitemap: ${SITE}sitemap.xml"
} > robots.txt

echo "built $built pages + sitemap.xml, robots.txt (v$VERSION)"
# llms.txt — the emerging convention for handing an AI assistant a compact,
# authoritative summary instead of leaving it to infer one from nine pages.
{
cat <<LLMS
# snare

> A free, open-source command-line scanner for a supply-chain attack that
> \`npm audit\` cannot see: the dropper is committed directly into the git
> repository rather than pulled from the registry, so there is no malicious
> dependency, the lockfile is clean, and Dependabot has nothing to report.

Version ${VERSION}. MIT licensed. macOS, Linux, Windows (Git Bash) and WSL.
Needs bash, git, python3 and the GitHub CLI. Ships no credentials of its own
and uses the operator's own GitHub authentication.

## The threat

The payload is appended to a normal line in a build config after roughly 500
spaces, so the file looks untouched in an editor and in most diff views. It
executes on \`next dev\` or \`next build\`. A second variant uses a
\`.vscode/tasks.json\` task with \`"runOn": "folderOpen"\`, which fires the
moment the folder is opened in VS Code and runs a file disguised as a font — a
genuine .woff2 begins with the bytes wOF2. Neither route needs
\`npm install\`. The command-and-control address is read from the Ethereum
blockchain at runtime, so blocking one IP address achieves nothing.

Stealing credentials is the objective, not a side effect. The file in the
repository is delivery. Removing it does not un-steal a token.

## If someone is infected, the order matters

1. Rotate credentials first — npm write tokens before anything else, because a
stolen one lets the worm publish trojanised versions of the victim's other
packages under their own name.
2. Clean the machine they push from. This family injects into commits as they
leave an already-infected machine, so cleaning a repository first is wasted
work: it re-injects into whatever was just cleaned.
3. Then clean the repositories.
4. Then tell collaborators, who may be infected from the same source.

\`snare respond\` walks through all four, asking before every action.

## Two things people get wrong

- \`snare guard scan\` reporting "clean" does not mean the machine was never
infected. The loader runs when a build runs or an editor opens the folder,
and then exits.
- \`snare scan github\` reads branch tips through the API and cannot see
history or build configs. \`snare scan repo\` against a real clone is the
thorough check.

## Pages

- [Home](${SITE}): the threat, how it works, why npm audit misses it
- [Check your machine](${SITE}check.html): four commands, nothing to install
- [If you are infected](${SITE}infected.html): the response walkthrough
- [Install](${SITE}install.html): every platform, plus a prompt for AI assistants
- [Command reference](${SITE}commands.html): every command and its honest limits
- [Changelog](${SITE}changelog.html): every release, and what was broken before it
- [Report a bug](${SITE}security.html): false clean results wanted most of all
- [Community](${SITE}community.html): field reports and the open questions
- [Source](${REPO})
LLMS
} > llms.txt

echo "built $built pages + sitemap.xml, robots.txt, llms.txt (v$VERSION)"
13 changes: 10 additions & 3 deletions docs/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
<link rel="stylesheet" href="assets/site.css">
<script>/* apply a pinned theme before first paint so the page never flashes */
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
{"@type":"ListItem","position":2,"name":"Changelog","item":"https://avioflagos.github.io/snare/changelog.html"}]}
</script>
</head>
<body data-page="changelog">
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<nav class="nav" aria-label="Primary">
<div class="nav-in">
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
Expand All @@ -52,6 +58,7 @@
</div>
</div>
</nav>
</header>

<main id="main">
<div class="docs">
Expand Down Expand Up @@ -85,7 +92,7 @@ <h1>Every release, and what was broken before it</h1>

<section class="stack" id="how-to-check">
<div class="callout">
<h3>Checking what you are running</h3>
<h2>Checking what you are running</h2>
<p><code>snare version</code> shows your version and commit.
<code>snare update --check</code> compares <strong>commits</strong>, not just version
numbers — the version once stood still for 33 commits, so a user 14 commits behind was
Expand All @@ -100,7 +107,7 @@ <h3>Checking what you are running</h3>
<section class="stack" id="v1-1-0">
<div class="rel">
<div class="rel-head">
<span class="rel-v">1.1.0</span>
<h2 class="rel-v">1.1.0</h2>
<span class="rel-date">29 August 2026</span>
<span class="tag now">Current</span>
</div>
Expand Down Expand Up @@ -255,7 +262,7 @@ <h3>Changed</h3>
<section class="stack" id="v1-0-0">
<div class="rel">
<div class="rel-head">
<span class="rel-v">1.0.0</span>
<h2 class="rel-v">1.0.0</h2>
<span class="rel-date">25 August 2026</span>
<span class="tag">Initial release</span>
</div>
Expand Down
28 changes: 22 additions & 6 deletions docs/check.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,24 @@
<link rel="stylesheet" href="assets/site.css">
<script>/* apply a pinned theme before first paint so the page never flashes */
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
{"@type":"ListItem","position":2,"name":"Check your machine","item":"https://avioflagos.github.io/snare/check.html"}]}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
{"@type":"Question","name":"How do I know if my machine is infected with this malware?",
"acceptedAnswer":{"@type":"Answer","text":"Look for a node process running inline code with obfuscated globals, any file carrying the operator's wallet address, a .vscode/tasks.json task set to runOn folderOpen, and font files whose first four bytes are not a real font magic. A genuine .woff2 begins with wOF2."}},
{"@type":"Question","name":"Does npm audit detect this?",
"acceptedAnswer":{"@type":"Answer","text":"No. The dropper is committed into the repository itself rather than pulled from the registry, so there is no malicious dependency for npm audit, the lockfile or Dependabot to report."}},
{"@type":"Question","name":"My process check came back clean. Am I safe?",
"acceptedAnswer":{"@type":"Answer","text":"Not necessarily. The loader runs when a build runs or an editor opens the folder, then exits. A clean process check alongside infected repositories is the expected result, not a contradiction."}}]}
</script>
</head>
<body data-page="check">
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<nav class="nav" aria-label="Primary">
<div class="nav-in">
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
Expand All @@ -52,6 +67,7 @@
</div>
</div>
</nav>
</header>

<main id="main">
<div class="wrap page-head">
Expand All @@ -65,36 +81,36 @@ <h1>Four checks you can run right now</h1>
<div class="wrap">
<section class="stack" id="detect">
<div class="check">
<h3>1 · Is a loader running right now?</h3>
<h2>1 · Is a loader running right now?</h2>
<div class="panel"><div class="panel-bar"><span class="label">macOS · Linux</span><div class="bar-actions"></div></div>
<div class="scroller"><pre>ps -eo pid,args | grep -E "node .*-e .*global\[" | grep -v grep</pre></div></div>
<p class="verdict">No output means nothing is running. Any output is a live process — kill it.</p>
</div>

<div class="check">
<h3>2 · Does any file carry the operator's wallet?</h3>
<h2>2 · Does any file carry the operator's wallet?</h2>
<div class="panel"><div class="panel-bar"><span class="label">working tree</span><div class="bar-actions"></div></div>
<div class="scroller"><pre>grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .</pre></div></div>
<div class="panel" style="margin-top:8px"><div class="panel-bar"><span class="label">and the full history — a deleted file still lives in Git</span><div class="bar-actions"></div></div>
<div class="scroller"><pre>git log --all -S "0xa322E5f3" --pickaxe-regex --oneline</pre></div></div>
</div>

<div class="check">
<h3>3 · Is a task set to run when you open the folder?</h3>
<h2>3 · Is a task set to run when you open the folder?</h2>
<div class="panel"><div class="panel-bar"><span class="label">editor auto-run</span><div class="bar-actions"></div></div>
<div class="scroller"><pre>grep -rn "folderOpen" .vscode/tasks.json</pre></div></div>
<p class="verdict bad">Any hit executes the moment the folder opens. Treat it as hostile until proven otherwise.</p>
</div>

<div class="check">
<h3>4 · Are your font files actually fonts?</h3>
<h2>4 · Are your font files actually fonts?</h2>
<div class="panel"><div class="panel-bar"><span class="label">magic bytes</span><div class="bar-actions"></div></div>
<div class="scroller"><pre>head -c 4 public/fonts/*.woff2</pre></div></div>
<p class="verdict">Every real font prints <code>wOF2</code>. Spaces mean you are looking at a script.</p>
</div>

<div class="callout">
<h3>Look for the long line</h3>
<h2>Look for the long line</h2>
<p>The most reliable giveaway is geometry, not content. This finds any file padded out to hide
something past the edge of the screen — a hand-written config has no business being 9,000
characters wide.</p>
Expand Down Expand Up @@ -122,7 +138,7 @@ <h2>Read the outcome carefully</h2>
</a>
</div>
<div class="callout">
<h3>A clean result is narrower than it looks</h3>
<h2>A clean result is narrower than it looks</h2>
<p>Checks 2, 3 and 4 test <strong>one clone</strong> — not your other repositories, and not the
branches you do not have checked out. Check 1 reports what is running <em>right now</em>; the
loader fires when a build runs or an editor opens the folder, does its work, and exits, so a
Expand Down
7 changes: 7 additions & 0 deletions docs/commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
<link rel="stylesheet" href="assets/site.css">
<script>/* apply a pinned theme before first paint so the page never flashes */
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
{"@type":"ListItem","position":2,"name":"Command reference","item":"https://avioflagos.github.io/snare/commands.html"}]}
</script>
</head>
<body data-page="commands">
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<nav class="nav" aria-label="Primary">
<div class="nav-in">
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
Expand All @@ -52,6 +58,7 @@
</div>
</div>
</nav>
</header>

<main id="main">
<div class="docs">
Expand Down
7 changes: 7 additions & 0 deletions docs/community.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
<link rel="stylesheet" href="assets/site.css">
<script>/* apply a pinned theme before first paint so the page never flashes */
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
{"@type":"ListItem","position":2,"name":"Community","item":"https://avioflagos.github.io/snare/community.html"}]}
</script>
</head>
<body data-page="community">
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<nav class="nav" aria-label="Primary">
<div class="nav-in">
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
Expand All @@ -52,6 +58,7 @@
</div>
</div>
</nav>
</header>

<main id="main">
<div class="wrap page-head">
Expand Down
Loading
Loading