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