From 04bc4800025397a6c616d2b230e691cee359ee3e Mon Sep 17 00:00:00 2001 From: Avioflagos Date: Sat, 29 Aug 2026 15:44:33 +0100 Subject: [PATCH] docs: correct the document outline, and say what crawlers may do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
. 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 , 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. --- docs/assets/site.css | 4 +- docs/build.sh | 156 +++++++++++++++++++++++++++++++++++++++- docs/changelog.html | 13 +++- docs/check.html | 28 ++++++-- docs/commands.html | 7 ++ docs/community.html | 7 ++ docs/docs.html | 7 ++ docs/index.html | 2 + docs/infected.html | 23 ++++++ docs/install.html | 15 ++++ docs/llms.txt | 58 +++++++++++++++ docs/robots.txt | 93 ++++++++++++++++++++++++ docs/security.html | 7 ++ docs/sitemap.xml | 18 ++--- docs/src/changelog.html | 6 +- docs/src/check.html | 12 ++-- 16 files changed, 425 insertions(+), 31 deletions(-) create mode 100644 docs/llms.txt diff --git a/docs/assets/site.css b/docs/assets/site.css index f258c73..5640e79 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -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 */ @@ -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; diff --git a/docs/build.sh b/docs/build.sh index aa98b4a..0486ab0 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -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")" @@ -71,6 +73,69 @@ build_page(){ try{var t=localStorage.getItem("snare-theme");if(t)document.documentElement.dataset.theme=t;}catch(e){} 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 < +{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[ + {"@type":"ListItem","position":1,"name":"snare","item":"$SITE"}, + {"@type":"ListItem","position":2,"name":"$crumb","item":"$SITE$out"}]} + +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 < +{"@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"}]} + +HOWTO + ;; + install.html) + cat < +{"@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"}} + +INST + ;; + check.html) + cat < +{"@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."}}]} + +FAQ + ;; + esac + # keyword + structured data only on the entry page if [ "$out" = index.html ]; then cat <<'KW' @@ -101,6 +166,7 @@ JSONLD # ---- nav ---- cat <<'NAVTOP' +
NAVBOT @@ -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 ' %s%s\n' "$loc" "$pri" + lm="$(date -u -r "$page" '+%Y-%m-%d' 2>/dev/null || date -u '+%Y-%m-%d')" + printf ' %s%s%s\n' "$loc" "$lm" "$pri" done echo '' } > 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 < 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)" diff --git a/docs/changelog.html b/docs/changelog.html index fc1333d..89a65ee 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -25,9 +25,15 @@ + +
@@ -85,7 +92,7 @@

Every release, and what was broken before it

-

Checking what you are running

+

Checking what you are running

snare version shows your version and commit. snare update --check compares commits, not just version numbers — the version once stood still for 33 commits, so a user 14 commits behind was @@ -100,7 +107,7 @@

Checking what you are running

- 1.1.0 +

1.1.0

29 August 2026 Current
@@ -255,7 +262,7 @@

Changed

- 1.0.0 +

1.0.0

25 August 2026 Initial release
diff --git a/docs/check.html b/docs/check.html index ce3e9d1..f4dee30 100644 --- a/docs/check.html +++ b/docs/check.html @@ -25,9 +25,24 @@ + + +
+
@@ -65,14 +81,14 @@

Four checks you can run right now

-

1 · Is a loader running right now?

+

1 · Is a loader running right now?

macOS · Linux
ps -eo pid,args | grep -E "node .*-e .*global\[" | grep -v grep

No output means nothing is running. Any output is a live process — kill it.

-

2 · Does any file carry the operator's wallet?

+

2 · Does any file carry the operator's wallet?

working tree
grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .
and the full history — a deleted file still lives in Git
@@ -80,21 +96,21 @@

2 · Does any file carry the operator's wallet?

-

3 · Is a task set to run when you open the folder?

+

3 · Is a task set to run when you open the folder?

editor auto-run
grep -rn "folderOpen" .vscode/tasks.json

Any hit executes the moment the folder opens. Treat it as hostile until proven otherwise.

-

4 · Are your font files actually fonts?

+

4 · Are your font files actually fonts?

magic bytes
head -c 4 public/fonts/*.woff2

Every real font prints wOF2. Spaces mean you are looking at a script.

-

Look for the long line

+

Look for the long line

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.

@@ -122,7 +138,7 @@

Read the outcome carefully

-

A clean result is narrower than it looks

+

A clean result is narrower than it looks

Checks 2, 3 and 4 test one clone — not your other repositories, and not the branches you do not have checked out. Check 1 reports what is running right now; the loader fires when a build runs or an editor opens the folder, does its work, and exits, so a diff --git a/docs/commands.html b/docs/commands.html index f7deed6..cf71674 100644 --- a/docs/commands.html +++ b/docs/commands.html @@ -25,9 +25,15 @@ + +

+
diff --git a/docs/community.html b/docs/community.html index 07445cf..9a4d75d 100644 --- a/docs/community.html +++ b/docs/community.html @@ -25,9 +25,15 @@ + +
+
diff --git a/docs/docs.html b/docs/docs.html index 331bd89..0be94b3 100644 --- a/docs/docs.html +++ b/docs/docs.html @@ -25,9 +25,15 @@ + +
+
diff --git a/docs/index.html b/docs/index.html index 0840bda..c729bd8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -44,6 +44,7 @@ +
+
diff --git a/docs/infected.html b/docs/infected.html index 385bbba..e626a98 100644 --- a/docs/infected.html +++ b/docs/infected.html @@ -25,9 +25,31 @@ + + +
+
diff --git a/docs/install.html b/docs/install.html index d5a1d42..24b9fe2 100644 --- a/docs/install.html +++ b/docs/install.html @@ -25,9 +25,23 @@ + + +
+
diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000..0d4c1cf --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,58 @@ +# 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 1.1.0. 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](https://avioflagos.github.io/snare/): the threat, how it works, why npm audit misses it +- [Check your machine](https://avioflagos.github.io/snare/check.html): four commands, nothing to install +- [If you are infected](https://avioflagos.github.io/snare/infected.html): the response walkthrough +- [Install](https://avioflagos.github.io/snare/install.html): every platform, plus a prompt for AI assistants +- [Command reference](https://avioflagos.github.io/snare/commands.html): every command and its honest limits +- [Changelog](https://avioflagos.github.io/snare/changelog.html): every release, and what was broken before it +- [Report a bug](https://avioflagos.github.io/snare/security.html): false clean results wanted most of all +- [Community](https://avioflagos.github.io/snare/community.html): field reports and the open questions +- [Source](https://github.com/AviOfLagos/snare) diff --git a/docs/robots.txt b/docs/robots.txt index 9819dda..37aa4e2 100644 --- a/docs/robots.txt +++ b/docs/robots.txt @@ -1,3 +1,96 @@ +# snare — a free, open-source scanner for supply-chain malware +# committed directly into git repositories. +# +# Everything here is public and free to index, quote and train on. +# The whole point is that people find this before they are compromised, +# so the crawlers below are named explicitly rather than left to infer +# permission from the wildcard. + User-agent: * Allow: / + +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: DuckDuckBot +Allow: / + +User-agent: Slurp +Allow: / + +User-agent: Baiduspider +Allow: / + +User-agent: YandexBot +Allow: / + +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-User +Allow: / + +User-agent: Claude-SearchBot +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Perplexity-User +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Applebot +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: Amazonbot +Allow: / + +User-agent: Bytespider +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: cohere-ai +Allow: / + +User-agent: Diffbot +Allow: / + +User-agent: FacebookBot +Allow: / + +User-agent: meta-externalagent +Allow: / + +User-agent: MistralAI-User +Allow: / + +User-agent: TimpiBot +Allow: / + +User-agent: YouBot +Allow: / + Sitemap: https://avioflagos.github.io/snare/sitemap.xml diff --git a/docs/security.html b/docs/security.html index 8a0eb86..760e431 100644 --- a/docs/security.html +++ b/docs/security.html @@ -25,9 +25,15 @@ + +
+
diff --git a/docs/sitemap.xml b/docs/sitemap.xml index b2558a2..0ee109a 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1,12 +1,12 @@ - https://avioflagos.github.io/snare/1.0 - https://avioflagos.github.io/snare/check.html0.9 - https://avioflagos.github.io/snare/infected.html0.8 - https://avioflagos.github.io/snare/install.html0.9 - https://avioflagos.github.io/snare/docs.html0.9 - https://avioflagos.github.io/snare/commands.html0.8 - https://avioflagos.github.io/snare/community.html0.8 - https://avioflagos.github.io/snare/changelog.html0.6 - https://avioflagos.github.io/snare/security.html0.6 + https://avioflagos.github.io/snare/2026-08-291.0 + https://avioflagos.github.io/snare/check.html2026-08-290.9 + https://avioflagos.github.io/snare/infected.html2026-08-290.8 + https://avioflagos.github.io/snare/install.html2026-08-290.9 + https://avioflagos.github.io/snare/docs.html2026-08-290.9 + https://avioflagos.github.io/snare/commands.html2026-08-290.8 + https://avioflagos.github.io/snare/community.html2026-08-290.8 + https://avioflagos.github.io/snare/changelog.html2026-08-290.6 + https://avioflagos.github.io/snare/security.html2026-08-290.6 diff --git a/docs/src/changelog.html b/docs/src/changelog.html index e91613a..4f72f53 100644 --- a/docs/src/changelog.html +++ b/docs/src/changelog.html @@ -34,7 +34,7 @@

Every release, and what was broken before it

-

Checking what you are running

+

Checking what you are running

snare version shows your version and commit. snare update --check compares commits, not just version numbers — the version once stood still for 33 commits, so a user 14 commits behind was @@ -49,7 +49,7 @@

Checking what you are running

- 1.1.0 +

1.1.0

29 August 2026 Current
@@ -204,7 +204,7 @@

Changed

- 1.0.0 +

1.0.0

25 August 2026 Initial release
diff --git a/docs/src/check.html b/docs/src/check.html index 31511dd..bee6fff 100644 --- a/docs/src/check.html +++ b/docs/src/check.html @@ -14,14 +14,14 @@

Four checks you can run right now

-

1 · Is a loader running right now?

+

1 · Is a loader running right now?

macOS · Linux
ps -eo pid,args | grep -E "node .*-e .*global\[" | grep -v grep

No output means nothing is running. Any output is a live process — kill it.

-

2 · Does any file carry the operator's wallet?

+

2 · Does any file carry the operator's wallet?

working tree
grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .
and the full history — a deleted file still lives in Git
@@ -29,21 +29,21 @@

2 · Does any file carry the operator's wallet?

-

3 · Is a task set to run when you open the folder?

+

3 · Is a task set to run when you open the folder?

editor auto-run
grep -rn "folderOpen" .vscode/tasks.json

Any hit executes the moment the folder opens. Treat it as hostile until proven otherwise.

-

4 · Are your font files actually fonts?

+

4 · Are your font files actually fonts?

magic bytes
head -c 4 public/fonts/*.woff2

Every real font prints wOF2. Spaces mean you are looking at a script.

-

Look for the long line

+

Look for the long line

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.

@@ -71,7 +71,7 @@

Read the outcome carefully

-

A clean result is narrower than it looks

+

A clean result is narrower than it looks

Checks 2, 3 and 4 test one clone — not your other repositories, and not the branches you do not have checked out. Check 1 reports what is running right now; the loader fires when a build runs or an editor opens the folder, does its work, and exits, so a