io.github.Baneado98/import-guardian
Catch AI-hallucinated (slopsquatted) npm imports in generated code before npm install.
Open source Open in the app JSON README (API)
About
Catch AI-hallucinated (slopsquatted) npm imports in generated code before npm install.
Details
- Kind
- MCP servers
- Topic
- Developer tools
- Publisher
- baneado98
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.3.0
- Last push
- 2026-06-26T07:54:12Z
- Repository state
- ativo
- Language
- Dockerfile
- License
- MIT
- Added
- 2026-08-29 03:01:42
- Updated
- 2026-08-29 03:01:42
- Origin id
io.github.Baneado98/import-guardian
README
# import-guardian πͺ
**Catch AI-hallucinated (slopsquatted) npm imports in generated code β _before_ you run `npm install`.**
An LLM just wrote some code. It might `import` a package that **doesn't exist** β a name the model confidently hallucinated. Attackers register exactly those names and ship malware. This is **slopsquatting**, and in 2026 it's one of the fastest-growing supply-chain attacks: code generators have been observed referencing **205,000+ unique non-existent package names**, and real hallucinated packages (`react-codeshift`, a fake `huggingface-cli` with 30k+ downloads) have already shown up in the wild.
`import-guardian` reads the generated code, extracts every npm package it imports, and tells you which ones are real β so an agent never installs a name its own model invented.
It's part of the guardian set: [**npm-guardian**](https://github.com/Baneado98/npm-guardian) audits a package you've _already chosen_ for CVEs and malicious behaviour, [**license-guardian**](https://github.com/Baneado98/license-guardian) audits the _licenses_ of your dependencies (GPL/AGPL/BUSL traps) before you ship, and [**lockfile-guardian**](https://github.com/Baneado98/lockfile-guardian) audits the resolved `package-lock.json` for integrity tampering and risky install scripts. `import-guardian` works one step earlier than all of them, at the moment the code β and its dependency names β are generated.
## What it catches
| | |
|---|---|
| π» **Hallucinated** | imports of packages that **do not exist on npm** β the model invented the name. Returns `BLOCK` with a "did you mean" to the closest real package. |
| πͺ€ **Fresh squats** | packages that _do_ exist but were published days ago, have no source repo, near-zero downloads, or are a 1β2 edit typo of a popular library. Returns `REVIEW`. |
| π§© **Import-aware** | resolves `import x from "@scope/pkg/sub"` β `@scope/pkg`, `lodash/fp` β `lodash`; skips Node built-ins (`fs`, `node:path`) and local paths. Plain regex, no AST, **zero runtime deps beyond the MCP SDK**. |
Verdicts: π’ `CLEAN` Β· π `REVIEW` Β· π΄ `BLOCK`.
## Use it as an MCP server (free)
```json
{
"mcpServers": {
"import-guardian": { "command": "npx", "args": ["-y", "import-guardian-mcp"] }
}
}
```
Tools:
- **`scan_code_imports`** β give it a block of generated JS/TS; it extracts and checks every npm import. _Run this on code you just generated, before its install command._
- **`check_packages`** β verify an explicit list of package names.
- **`verify_package`** β deep-check a single package name (existence, age, versions, repo, weekly downloads, edit distance to popular packages).
- **`audit_lockfile`** π _(premium)_ β the deep one. Give it a lockfile from **npm** (`package-lock.json`), **yarn** (`yarn.lock`), **pnpm** (`pnpm-lock.yaml`), **PyPI** (`requirements.txt` / `poetry.lock` / `Pipfile.lock`) or **Go** (`go.mod` / `go.sum`) and it resolves the **whole transitive tree** and audits every package for **dependency confusion**, transitive **typosquats / homoglyph clones**, **maintainer-takeover** fingerprints, nonexistent pins, and **install-script** risk. See below.
## Deep lockfile audit (premium) π
`scan_code_imports` looks at the handful of imports you can *see*. `audit_lockfile` looks at the **hundreds you can't** β the fully resolved dependency graph in your lockfile β and finds the supply-chain attacks an LLM cannot reason about from source alone:
| | |
|---|---|
| π― **Dependency confusion** | a package the lockfile resolves from a **private / internal registry** whose name is **unclaimed on the public index** β an attacker can publish that exact name publicly and shadow it. The single highest-impact supply-chain bug, and impossible to detect without the resolved lockfile + a live public-registry cross-check. On **PyPI** it's sharper still: `pip install --extra-index-url` merges your private index with public PyPI and installs the **highest version from either** (the 2021 attack that hit Microsoft / Apple / Tesla). Returns **critical**. |
| πͺ **Transitive typosquats** | homoglyph/Unicode-confusable clones (`rΠ΅act` with a Cyrillic *e*), transpositions (`axois`β`axios`), and registry-specific squats (PyPI `python3-dateutil` vs `python-dateutil`, normalized per PEP 503) across the **entire tree** β matched against a large per-ecosystem corpus of high-traffic packages. |
| π³οΈ **Maintainer takeover** | the fingerprint of a compromised maintainer: a **dormant package suddenly republished**, a sole-maintainer project, or a rapid republish burst β the pattern behind **event-stream**, **ua-parser-js**, **node-ipc** and PyPI's **ctx** hijack. |
| π **Install-script version-diffing** | the sharpest injection signal: a package whose **newest release ADDED** a `postinstall`/`preinstall`/`install`/`prepare` hook that the **previous release did not have**. A previously-clean dependency that suddenly gains code-execution-on-install is exactly how event-stream and ua-parser-js shipped β caught by diffing the two releases' metadata on the live registry. Returns **critical**, and tells you the last clean version to pin. |
| π¬ **Install-script PAYLOAD analysis** | goes past *whether* a hook exists to *what it does*: downloads the package **tarball**, gunzip+untars it **in-memory** (no deps), resolves the lifecycle command to its entry file, and statically analyzes the **actual code** for malware behavior β network egress, credential/env exfil, child-process exec, download-and-run, obfuscation. An **obfuscation-resistant AST/token analyzer** decodes `\x`/`\u`/octal escapes and folds split-string concatenation, so a sink hidden as `require("child_pro"+"cess")` or `global["ev"+"al"](β¦)` is still caught. The payload lives inside a binary artifact the buyer's agent never fetches β that's the access+compute moat. |
| π **PyPI sdist `setup.py` payload analysis** | the PyPI analogue of the npm tarball pass, and the sharpest PyPI surface: a **source distribution** runs `setup.py` β arbitrary Python β at **`pip install` time, before any of your code**. We download the **sdist** (anti-DoS capped), untar it in-memory, and statically analyze `setup.py` / `setup.cfg` / `pyproject.toml` for the stealer/loader pattern β `os.environ` exfil, `os.system`/`subprocess` exec, `.pypirc`/AWS/SSH credential theft, `urllib`/`requests` egress, `exec`/`eval`/`__import__` obfuscation, download-and-run, and a **non-standard build backend**. A **Python AST de-obfuscator** decodes `\x`/`\u`/octal escapes and folds split + implicit-adjacent strings, so `getattr(__import__("o"+"s"),"sys"+"tem")(β¦)` is still resolved. The `ctx` / `jeIlyfish` / `colourama` attack class β caught from an artifact the buyer's agent never fetches. **BLOCK is gated on the malware *combination*** (exfil/download-exec/packed-loader), so a legitimate C/Cython build that merely shells out to a compiler is reported as an informational note, **not** a false-positive block. |
| π¦ **Known-malware / known-vuln (live OSV cross-reference)** | every resolved package is cross-referenced β at its **exact pinned version** β against the live **[OSV.dev](https://osv.dev)** advisory database (which ingests the OpenSSF **Malicious Packages** feed as `MAL-*` plus GitHub/PyPI/Go/crates/Maven/Packagist advisories). If the version your lockfile pins is documented **malware** (e.g. `event-stream@3.3.6`, `ctx@0.1.2`), that's a **critical / BLOCK** β the strongest possible signal. The mapping is **version-specific**: `event-stream@3.3.6` is malware, `3.3.4` is clean, and only a live database query can tell them apart. This is a different *kind* of moat from the static passes above β the buyer's model doesn't hold the advisory corpus and can't resolve versionβadvisory. Queries are **batched, cached (TTL), concurrency-capped**, and degrade clean if OSV is unreachable (it never turns a clean package into a false positive). |
| ποΈ **Local build-backend follow-hop** (PyPI) | when `pyproject.toml` declares a **non-standard build backend that ships *inside* the sdist** (`backend-path = ["."]`), the package runs its **own code** as the PEP 517 backend at every build/install. We **resolve that module to its file in the sdist and analyze its real source** (and follow `setup.py`'s local imports one hop), instead of merely flagging the backend name β closing the gap where a payload hides one indirection away from `setup.py`. |
| π **Go `replace` hijack** | a `replace β¦ => host/path` in `go.mod` pointing at an **external host**, **live-verified** against the Go module proxy. If the replacement doesn't resolve (a private / attacker host), the code your build actually compiles isn't the module you audited or anything `go.sum` can vouch for. |
| π£ **Install-script & provenance risk** | packages in the resolved tree that run install scripts, brand-new pins, missing repos, and **near-zero real downloads** (verified against npm / pypistats / crates.io). |
**Seven ecosystems.** The same resolved-graph engine runs over **npm / yarn / pnpm**, **PyPI** (`requirements.txt`, `poetry.lock`, `Pipfile.lock`), **Go modules** (`go.mod`, `go.sum`), **Rust / Cargo** (`Cargo.lock`, `Cargo.toml`), **Java** (Maven `pom.xml`, Gradle `build.gradle` / `.kts` / `gradle.lockfile`) and **PHP / Composer** (`composer.json`, `composer.lock`) β each cross-checked against its own public index (npm registry, PyPI, the Go module proxy, crates.io, **Maven Central**, **Packagist**). On crates.io the matcher knows the `-`/`_` equivalence (`rustdecimal` vs `rust_decimal`); on Maven an internal `groupId:artifactId` unclaimed on Central is the dependency-confusion target; on Composer an internal `vendor/package` unregistered on Packagist while a private repo is merged with the public index is the confusion target, and a Packagist-**abandoned** package is flagged as a takeover risk. Large lockfiles are bounded: direct + private-source packages are always live-checked first, then transitive up to a cap, so one call never fans out to tens of thousands of requests.
```
POST /pro/audit { "lockfile": "<contents of package-lock.json>", "filename": "package-lock.json" }
```
Pay per call via **x402** (USDC on Base) or a prepaid Stripe key. The free `POST /audit` returns the verdict and issue counts by severity, but **withholds which packages are affected and why** β that's the paid report. This deep engine runs **server-side only** and is never shipped in the npm package.
### Example
```
scan_code_imports({ code: 'import shift from "react-codeshift";\nimport React from "react";' })
π REVIEW β 1 referenced package(s) look risky.
π react-codeshift (risk 57/100)
β’ No source repository linked.
β’ Only 1 published version.
β’ Only 3 downloads in the last week despite being 158 days old.
π’ react (ok) β established package.
```
## Free HTTP API
```
POST /scan { "code": "import x from 'reqeusts'\nimport y from 'lodash'" }
GET /verify?name=express
```
Hosted at **https://import-guardian.vercel.app** Β· try [`/verify?name=express`](https://import-guardian.vercel.app/verify?name=express) vs [`/verify?name=reqeusts`](https://import-guardian.vercel.app/verify?name=reqeusts).
## Pay-per-call (x402)
The `/pro/*` routes are gated by [x402](https://x402.org). Your agent pays **$0.02 USDC** per call automatically β no sign-up, no API key. Payment settles on-chain (USDC on Base). The server holds **no private key**; it only declares a public receiving address.
```
POST /pro/scan { "code": "β¦" } # 402 β pay β result, no rate limit
```
## How it works (and its limits)
- Existence + freshness come straight from the live npm registry (`registry.npmjs.org`) and the download-stats API β these are **facts**, not guesses, which is the moat: an agent can't reliably know on its own whether a name it generated is real and trusted.
- "Did you mean" uses Levenshtein distance against a curated list of high-impact packages attackers impersonate.
- It does **not** execute or install anything (read-only), and it intentionally errs toward `REVIEW` rather than silently passing a brand-new lookalike. It is a guardrail, not a guarantee β pair it with [npm-guardian](https://github.com/Baneado98/npm-guardian) for behavioural/CVE auditing of packages you decide to keep.
## License
MIT.