Skip to content

Commit 566727b

Browse files
docs: correct the document outline, and say what crawlers may do (#37)
Semantics The nav was not inside any landmark, so a reader jumping by landmark had a main and a footer but no header. It is now wrapped in <header>. Two pages skipped a heading level. check.html went h1 straight to h3 for the four checks, which ARE the page's top-level sections, so they are h2 now. changelog.html did the same, and worse: the release version was a <span>, so the "Fixed"/"Added"/"Changed" groups floated with no release heading above them and any tool building an outline saw eight sibling h3s belonging to nothing. Release versions are h2 now, the groups sit under them, and the CSS mirrors the old span rule so nothing moves visually. All nine pages: exactly one h1, no skipped levels, one main, one header, one footer. Structured data Only index.html had any. Every page now carries a BreadcrumbList, and three carry the type that fits what someone is actually asking: infected.html HowTo, four steps with anchors — "how do I remove this malware" is what gets typed mid-incident install.html SoftwareApplication with the current version check.html FAQPage covering the three questions people ask: how do I know, why did npm audit miss it, and does a clean process check mean I am safe Crawlers robots.txt named 29 crawlers explicitly rather than leaving search engines and AI answering engines to infer permission from a bare wildcard, and says in a comment why: the point of this site is that people find it before they are compromised. llms.txt gives an assistant a compact authoritative summary — the threat, the response order, and the two things people reliably get wrong — instead of leaving it to reconstruct one from nine pages of HTML. sitemap.xml gained lastmod, so a crawler can tell what changed. Co-authored-by: Avioflagos <ellumainc@gmail.com>
1 parent 3b1fba3 commit 566727b

16 files changed

Lines changed: 425 additions & 31 deletions

docs/assets/site.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ td code{color:var(--accent)}
299299
background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--accent);
300300
border-radius:8px;padding:17px 19px;display:flex;flex-direction:column;gap:7px;
301301
}
302-
.callout h3{color:var(--accent)}
302+
.callout h2,.callout h3{color:var(--accent)}
303303
.callout p{font-size:.95rem}
304304

305305
/* footer lives in the components block below */
@@ -485,7 +485,7 @@ td code{color:var(--accent)}
485485
.rel-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px}
486486
.rel-v{
487487
font-family:var(--disp);font-size:1.45rem;font-weight:800;letter-spacing:-.02em;color:var(--ink);
488-
}
488+
;margin:0;display:inline-block}
489489
.rel-date{font-family:var(--mono);font-size:.74rem;color:var(--muted)}
490490
.tag{
491491
font-family:var(--mono);font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;

docs/build.sh

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ build_page(){
3737
title="$(meta title "$src")"
3838
desc="$(meta desc "$src")"
3939
navkey="$(meta nav "$src")"
40+
# short label for breadcrumbs: the title up to its first em dash
41+
crumb="$(printf '%s' "$title" | sed 's/ *—.*//')"
4042
prev="$(meta prev "$src")"
4143
next="$(meta next "$src")"
4244

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

76+
# Breadcrumbs on every page: gives a crawler the site hierarchy, and is
77+
# what produces the path shown under a search result.
78+
if [ "$out" != index.html ]; then
79+
cat <<CRUMB
80+
<script type="application/ld+json">
81+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
82+
{"@type":"ListItem","position":1,"name":"snare","item":"$SITE"},
83+
{"@type":"ListItem","position":2,"name":"$crumb","item":"$SITE$out"}]}
84+
</script>
85+
CRUMB
86+
fi
87+
88+
# Page-specific structured data. A HowTo on the response walkthrough is the
89+
# one that matters: "how do I remove this malware" is what someone types
90+
# mid-incident, and HowTo is the type that answers it.
91+
case "$out" in
92+
infected.html)
93+
cat <<HOWTO
94+
<script type="application/ld+json">
95+
{"@context":"https://schema.org","@type":"HowTo",
96+
"name":"Remove a committed supply-chain dropper from your repositories",
97+
"description":"$desc","url":"$SITE$out",
98+
"totalTime":"PT30M",
99+
"tool":[{"@type":"HowToTool","name":"snare"}],
100+
"step":[
101+
{"@type":"HowToStep","position":1,"name":"Rotate your credentials",
102+
"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"},
103+
{"@type":"HowToStep","position":2,"name":"Clean the machine you push from",
104+
"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"},
105+
{"@type":"HowToStep","position":3,"name":"Clean the repositories",
106+
"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"},
107+
{"@type":"HowToStep","position":4,"name":"Tell your collaborators",
108+
"text":"They may be infected from the same source, and a rewritten history breaks their clones without explanation.","url":"$SITE$out#notify"}]}
109+
</script>
110+
HOWTO
111+
;;
112+
install.html)
113+
cat <<INST
114+
<script type="application/ld+json">
115+
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"snare",
116+
"applicationCategory":"SecurityApplication","operatingSystem":"macOS, Linux, Windows, WSL",
117+
"softwareVersion":"$VERSION","url":"$SITE$out","codeRepository":"$REPO",
118+
"license":"https://opensource.org/licenses/MIT",
119+
"description":"$desc",
120+
"offers":{"@type":"Offer","price":"0","priceCurrency":"USD"}}
121+
</script>
122+
INST
123+
;;
124+
check.html)
125+
cat <<FAQ
126+
<script type="application/ld+json">
127+
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
128+
{"@type":"Question","name":"How do I know if my machine is infected with this malware?",
129+
"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."}},
130+
{"@type":"Question","name":"Does npm audit detect this?",
131+
"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."}},
132+
{"@type":"Question","name":"My process check came back clean. Am I safe?",
133+
"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."}}]}
134+
</script>
135+
FAQ
136+
;;
137+
esac
138+
74139
# keyword + structured data only on the entry page
75140
if [ "$out" = index.html ]; then
76141
cat <<'KW'
@@ -101,6 +166,7 @@ JSONLD
101166

102167
# ---- nav ----
103168
cat <<'NAVTOP'
169+
<header class="site-header">
104170
<nav class="nav" aria-label="Primary">
105171
<div class="nav-in">
106172
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
@@ -128,6 +194,7 @@ NAVTOP
128194
</div>
129195
</div>
130196
</nav>
197+
</header>
131198
132199
<main id="main">
133200
NAVBOT
@@ -207,15 +274,100 @@ done
207274
infected.html|commands.html|community.html) loc="$SITE$page"; pri="0.8" ;;
208275
*) loc="$SITE$page"; pri="0.6" ;;
209276
esac
210-
printf ' <url><loc>%s</loc><priority>%s</priority></url>\n' "$loc" "$pri"
277+
lm="$(date -u -r "$page" '+%Y-%m-%d' 2>/dev/null || date -u '+%Y-%m-%d')"
278+
printf ' <url><loc>%s</loc><lastmod>%s</lastmod><priority>%s</priority></url>\n' "$loc" "$lm" "$pri"
211279
done
212280
echo '</urlset>'
213281
} > sitemap.xml
214282

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

221-
echo "built $built pages + sitemap.xml, robots.txt (v$VERSION)"
308+
# llms.txt — the emerging convention for handing an AI assistant a compact,
309+
# authoritative summary instead of leaving it to infer one from nine pages.
310+
{
311+
cat <<LLMS
312+
# snare
313+
314+
> A free, open-source command-line scanner for a supply-chain attack that
315+
> \`npm audit\` cannot see: the dropper is committed directly into the git
316+
> repository rather than pulled from the registry, so there is no malicious
317+
> dependency, the lockfile is clean, and Dependabot has nothing to report.
318+
319+
Version ${VERSION}. MIT licensed. macOS, Linux, Windows (Git Bash) and WSL.
320+
Needs bash, git, python3 and the GitHub CLI. Ships no credentials of its own
321+
and uses the operator's own GitHub authentication.
322+
323+
## The threat
324+
325+
The payload is appended to a normal line in a build config after roughly 500
326+
spaces, so the file looks untouched in an editor and in most diff views. It
327+
executes on \`next dev\` or \`next build\`. A second variant uses a
328+
\`.vscode/tasks.json\` task with \`"runOn": "folderOpen"\`, which fires the
329+
moment the folder is opened in VS Code and runs a file disguised as a font — a
330+
genuine .woff2 begins with the bytes wOF2. Neither route needs
331+
\`npm install\`. The command-and-control address is read from the Ethereum
332+
blockchain at runtime, so blocking one IP address achieves nothing.
333+
334+
Stealing credentials is the objective, not a side effect. The file in the
335+
repository is delivery. Removing it does not un-steal a token.
336+
337+
## If someone is infected, the order matters
338+
339+
1. Rotate credentials first — npm write tokens before anything else, because a
340+
stolen one lets the worm publish trojanised versions of the victim's other
341+
packages under their own name.
342+
2. Clean the machine they push from. This family injects into commits as they
343+
leave an already-infected machine, so cleaning a repository first is wasted
344+
work: it re-injects into whatever was just cleaned.
345+
3. Then clean the repositories.
346+
4. Then tell collaborators, who may be infected from the same source.
347+
348+
\`snare respond\` walks through all four, asking before every action.
349+
350+
## Two things people get wrong
351+
352+
- \`snare guard scan\` reporting "clean" does not mean the machine was never
353+
infected. The loader runs when a build runs or an editor opens the folder,
354+
and then exits.
355+
- \`snare scan github\` reads branch tips through the API and cannot see
356+
history or build configs. \`snare scan repo\` against a real clone is the
357+
thorough check.
358+
359+
## Pages
360+
361+
- [Home](${SITE}): the threat, how it works, why npm audit misses it
362+
- [Check your machine](${SITE}check.html): four commands, nothing to install
363+
- [If you are infected](${SITE}infected.html): the response walkthrough
364+
- [Install](${SITE}install.html): every platform, plus a prompt for AI assistants
365+
- [Command reference](${SITE}commands.html): every command and its honest limits
366+
- [Changelog](${SITE}changelog.html): every release, and what was broken before it
367+
- [Report a bug](${SITE}security.html): false clean results wanted most of all
368+
- [Community](${SITE}community.html): field reports and the open questions
369+
- [Source](${REPO})
370+
LLMS
371+
} > llms.txt
372+
373+
echo "built $built pages + sitemap.xml, robots.txt, llms.txt (v$VERSION)"

docs/changelog.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
<link rel="stylesheet" href="assets/site.css">
2626
<script>/* apply a pinned theme before first paint so the page never flashes */
2727
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
28+
<script type="application/ld+json">
29+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
30+
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
31+
{"@type":"ListItem","position":2,"name":"Changelog","item":"https://avioflagos.github.io/snare/changelog.html"}]}
32+
</script>
2833
</head>
2934
<body data-page="changelog">
3035
<a class="skip" href="#main">Skip to content</a>
36+
<header class="site-header">
3137
<nav class="nav" aria-label="Primary">
3238
<div class="nav-in">
3339
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
@@ -52,6 +58,7 @@
5258
</div>
5359
</div>
5460
</nav>
61+
</header>
5562

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

8693
<section class="stack" id="how-to-check">
8794
<div class="callout">
88-
<h3>Checking what you are running</h3>
95+
<h2>Checking what you are running</h2>
8996
<p><code>snare version</code> shows your version and commit.
9097
<code>snare update --check</code> compares <strong>commits</strong>, not just version
9198
numbers — the version once stood still for 33 commits, so a user 14 commits behind was
@@ -100,7 +107,7 @@ <h3>Checking what you are running</h3>
100107
<section class="stack" id="v1-1-0">
101108
<div class="rel">
102109
<div class="rel-head">
103-
<span class="rel-v">1.1.0</span>
110+
<h2 class="rel-v">1.1.0</h2>
104111
<span class="rel-date">29 August 2026</span>
105112
<span class="tag now">Current</span>
106113
</div>
@@ -255,7 +262,7 @@ <h3>Changed</h3>
255262
<section class="stack" id="v1-0-0">
256263
<div class="rel">
257264
<div class="rel-head">
258-
<span class="rel-v">1.0.0</span>
265+
<h2 class="rel-v">1.0.0</h2>
259266
<span class="rel-date">25 August 2026</span>
260267
<span class="tag">Initial release</span>
261268
</div>

docs/check.html

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,24 @@
2525
<link rel="stylesheet" href="assets/site.css">
2626
<script>/* apply a pinned theme before first paint so the page never flashes */
2727
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
28+
<script type="application/ld+json">
29+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
30+
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
31+
{"@type":"ListItem","position":2,"name":"Check your machine","item":"https://avioflagos.github.io/snare/check.html"}]}
32+
</script>
33+
<script type="application/ld+json">
34+
{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
35+
{"@type":"Question","name":"How do I know if my machine is infected with this malware?",
36+
"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."}},
37+
{"@type":"Question","name":"Does npm audit detect this?",
38+
"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."}},
39+
{"@type":"Question","name":"My process check came back clean. Am I safe?",
40+
"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."}}]}
41+
</script>
2842
</head>
2943
<body data-page="check">
3044
<a class="skip" href="#main">Skip to content</a>
45+
<header class="site-header">
3146
<nav class="nav" aria-label="Primary">
3247
<div class="nav-in">
3348
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
@@ -52,6 +67,7 @@
5267
</div>
5368
</div>
5469
</nav>
70+
</header>
5571

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

7490
<div class="check">
75-
<h3>2 · Does any file carry the operator's wallet?</h3>
91+
<h2>2 · Does any file carry the operator's wallet?</h2>
7692
<div class="panel"><div class="panel-bar"><span class="label">working tree</span><div class="bar-actions"></div></div>
7793
<div class="scroller"><pre>grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .</pre></div></div>
7894
<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>
7995
<div class="scroller"><pre>git log --all -S "0xa322E5f3" --pickaxe-regex --oneline</pre></div></div>
8096
</div>
8197

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

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

96112
<div class="callout">
97-
<h3>Look for the long line</h3>
113+
<h2>Look for the long line</h2>
98114
<p>The most reliable giveaway is geometry, not content. This finds any file padded out to hide
99115
something past the edge of the screen — a hand-written config has no business being 9,000
100116
characters wide.</p>
@@ -122,7 +138,7 @@ <h2>Read the outcome carefully</h2>
122138
</a>
123139
</div>
124140
<div class="callout">
125-
<h3>A clean result is narrower than it looks</h3>
141+
<h2>A clean result is narrower than it looks</h2>
126142
<p>Checks 2, 3 and 4 test <strong>one clone</strong> — not your other repositories, and not the
127143
branches you do not have checked out. Check 1 reports what is running <em>right now</em>; the
128144
loader fires when a build runs or an editor opens the folder, does its work, and exits, so a

docs/commands.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
<link rel="stylesheet" href="assets/site.css">
2626
<script>/* apply a pinned theme before first paint so the page never flashes */
2727
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
28+
<script type="application/ld+json">
29+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
30+
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
31+
{"@type":"ListItem","position":2,"name":"Command reference","item":"https://avioflagos.github.io/snare/commands.html"}]}
32+
</script>
2833
</head>
2934
<body data-page="commands">
3035
<a class="skip" href="#main">Skip to content</a>
36+
<header class="site-header">
3137
<nav class="nav" aria-label="Primary">
3238
<div class="nav-in">
3339
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
@@ -52,6 +58,7 @@
5258
</div>
5359
</div>
5460
</nav>
61+
</header>
5562

5663
<main id="main">
5764
<div class="docs">

docs/community.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
<link rel="stylesheet" href="assets/site.css">
2626
<script>/* apply a pinned theme before first paint so the page never flashes */
2727
try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){}</script>
28+
<script type="application/ld+json">
29+
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[
30+
{"@type":"ListItem","position":1,"name":"snare","item":"https://avioflagos.github.io/snare/"},
31+
{"@type":"ListItem","position":2,"name":"Community","item":"https://avioflagos.github.io/snare/community.html"}]}
32+
</script>
2833
</head>
2934
<body data-page="community">
3035
<a class="skip" href="#main">Skip to content</a>
36+
<header class="site-header">
3137
<nav class="nav" aria-label="Primary">
3238
<div class="nav-in">
3339
<a class="brand" href="index.html"><span class="mark">sn</span>snare</a>
@@ -52,6 +58,7 @@
5258
</div>
5359
</div>
5460
</nav>
61+
</header>
5562

5663
<main id="main">
5764
<div class="wrap page-head">

0 commit comments

Comments
 (0)