soloshield
v0.2.0
Published
Fast, local, no-cloud security scanner for secrets, vulnerable dependencies, and AI-hallucinated (slopsquatted) packages. Runs as a CLI, pre-commit hook, or CI step.
Maintainers
Readme
soloshield
The zero-account, single-binary security check for solo devs and small teams who don't want to run three tools or sign up for a platform.
Secrets scanning and supply-chain scanning are both well-served by mature tools — but they're separate tools, and the best ones increasingly assume you're a team with a dashboard budget. soloshield is different on purpose:
- One binary, two checks. Secrets and dependency supply-chain risk (hallucinated/typosquatted packages, known CVEs) in a single tool instead of running Gitleaks plus a separate supply-chain scanner.
- No account, ever. Not "free tier" — there is no signup, no dashboard, no org to create. Point it at a directory and read the output.
- Genuinely offline-capable.
--no-depsgives you secrets-only scanning with zero network calls. Full mode makes only explicit, visible calls to public registries (npm/PyPI) and OSV.dev. - Actually tested on real, messy repos, not just clean examples — see the hardening notes below for the specific monorepo bugs this found and fixed before publishing.
Where this fits (and where it doesn't)
This is not trying to out-feature Socket.dev (behavioral analysis, npm-registry integration, built by a team with 1B+ monthly downloads of their own tools) or Gitleaks/TruffleHog (the mature, high-star standards for secrets alone). If you're a security team at a funded company, those are probably the better call, and running more than one layer is normal practice in this space.
soloshield is for the gap underneath that: a solo developer or a two-person team who wants a real check before shipping AI-assisted code, without creating an account or reading a pricing page first. If that's not you, the bigger tools above are worth your time instead.
What it catches
- Hardcoded secrets — AWS keys, GitHub tokens, Stripe keys, Slack tokens/webhooks, private key blocks, JWTs, and high-entropy generic API key assignments. Placeholder-aware (won't flag
"your_api_key_here"orSECRET_KEY = "dev"). - AI-hallucinated / nonexistent packages — checks every
package.json,requirements.txt, andpyproject.toml(PEP 621 + Poetry) dependency against the real npm/PyPI registries. Catches "slopsquatting": AI coding assistants sometimes invent plausible-looking package names that don't exist, which attackers then pre-register to serve malware. - Typosquat risk — flags dependency names suspiciously close (edit distance ≤2) to a well-known package (e.g.
expresvsexpress), while correctly treating PyPI/npm separator variants (charset_normalizervscharset-normalizer) as the same package, not a typosquat. - Known vulnerabilities — cross-references pinned versions against the OSV.dev open vulnerability database.
v1 hardening (this pass)
Real-world testing against live repos (express, flask, requests) surfaced and fixed two real bugs before they'd have hit a user:
- Critical false-positive storm fixed: the original
pyproject.tomlparser read every TOML key as if it were a package name, flagging things likebuild-backend,testpaths, andquote-styleas "hallucinated packages." Replaced with a proper TOML parser (smol-toml) that only reads actual PEP 621 / Poetry dependency fields. - Typosquat false positive fixed: PyPI/npm treat
-,_,.as equivalent separators in package names (charset_normalizer==charset-normalizer). Names are now normalized before comparison. .gitignoreawareness added: the file walker now reads and respects the repo's own.gitignore, on top of built-in ignores (node_modules,.git,dist, etc).- Config-based allowlisting added (
.aegisrc.json): teams can suppress specific known-safe findings without disabling the scanner entirely. - Standalone binary packaging: built with Node's Single Executable Application (SEA) feature. The output is a real, self-contained ELF/Mach-O/PE binary with the Node runtime embedded — verified to run with zero
PATHand no Node/npm installed on the host at all. This matters for adoption: no "install Node first" friction for CI runners, locked-down machines, or non-technical users.
Verified clean (0 false positives) against express.js, flask, and requests source trees; verified it still catches all planted issues in the test fixture (real secret pattern, hallucinated package, typosquatted package) both from source and from the compiled binary.
v1 hardening — pass 2 (large monorepo stress test)
Tested against a real ~30,000-file, 616-manifest monorepo (Next.js). This surfaced four more real issues, all fixed:
- Redundant network calls: 3,318 dependency lookups for only 988 unique packages (70% waste) — added an in-memory cache so each unique
ecosystem:packageis only checked once per scan. - Silent false negatives under load: firing hundreds of fully-concurrent requests caused OSV.dev to return a 403 on one request; since lookups fail-open, an unbounded scan could silently miss a real known vulnerability rather than surfacing an error. Fixed with a concurrency limiter (semaphore, 40 in-flight requests) plus a retry-with-backoff on 429/403 responses.
- Workspace/monorepo false positives:
"@next/swc": "workspace:*"was flagged as a critical "hallucinated package" — it's a normal internal workspace reference, never published standalone (confirmed: genuinely 404s on the real npm registry, but that's expected and not a security issue). Fixed two ways: (1) skipworkspace:/file:/link:/portal:protocol versions outright, and (2) cross-reference dependency names against everynamefield declared in the repo's ownpackage.jsonfiles, since some monorepos pin siblings with an ordinary-looking version number instead of the explicit workspace protocol. - npm alias syntax not parsed:
"loader-utils3": "npm:[email protected]"(npm's package-aliasing feature, common in build tooling that runs multiple major versions of a dependency side by side) was checking the local alias name instead of the real target package. Fixed by parsing thenpm:name@versionsyntax. - Typosquat cross-domain false positive:
cypresswas flagged as a typosquat ofexpress(edit distance 2) despite being an entirely unrelated, legitimate package. Real typosquats overwhelmingly preserve the first character of the target name; added a first-character match requirement to the heuristic, which removes this class of false positive while still catching same-prefix typos (expresvsexpress).
Net effect on the stress-test repo: package findings dropped from an initial 20+ (mostly false positives) down to 2 defensible ones (a test-fixture package inside the target repo's own test suite, and a fair "verify this" caution on a legitimately different, older package). Full scan time: ~53s for the 616-manifest monorepo — acceptable for CI, though noted as a tuning point in the roadmap below for very large repos with a tight CI budget.
Known remaining limitation: dependency manifests found inside the target repo's own test fixtures/snapshots (e.g. a test file that intentionally uses a fake package name to test some other tool's parsing) will still occasionally surface as "hallucinated" — because, from the registry's point of view, they genuinely don't exist. This is inherent to static scanning without semantic understanding of "is this file test data"; documented rather than papered over with path-based heuristics that risk suppressing real findings elsewhere.
Install & run (from source)
npm install
npm run build
node dist/index.js . # scan current directory
node dist/index.js . --json # machine-readable output
node dist/index.js . --no-deps # fully offline, secrets-only
node dist/index.js . --fail-on medium # stricter CI gate (default: high)Build the standalone binary
./scripts/build-binary.sh soloshield-linux # or soloshield-macos / soloshield.exe
./dist-bundle/soloshield-linux . # no node/npm required to run thisNote: SEA does not cross-compile — run the build script on each target OS. Do not strip the resulting binary; it corrupts the embedded blob (confirmed by testing — this crashes with a segfault).
Once published to npm, source install becomes:
npx soloshield .Config file (.aegisrc.json)
Drop this in your repo root to suppress specific known findings:
{
"allow": ["src/config.js:12", "npm:some-package-name"]
}- Secrets are keyed as
"<file>:<line>". - Package findings are keyed as
"<ecosystem>:<package-name>".
Integration points
- Pre-commit: see
.pre-commit-config.sample.yaml - CI (GitHub Actions): see
.github-workflow-sample/soloshield.yml - Manual / any other CI: just call the binary, check the exit code (
0= pass,1= findings at or above--fail-onthreshold)
Design principles
- No SaaS. No servers to run, no accounts, no telemetry. The only network calls are optional, explicit lookups to public registries (npm/PyPI) and OSV.dev for vulnerability data — skippable entirely with
--no-deps. - Fail-open on network issues. If a registry is unreachable, soloshield won't falsely flag your dependencies as hallucinated — it reports what it can verify.
- Low false-positive bias. Placeholder detection, entropy checks, separator normalization, and
.gitignoreawareness keep noise down so CI gates stay usable rather than becoming background noise engineers learn to ignore.
Roadmap (not yet built)
- Publish to npm registry (
npx soloshield) and set up cross-platform binary releases (GitHub Actions matrix build for Linux/macOS/Windows) — SEA does not cross-compile, so binaries must be built on each target OS - Expand popular-package seed list to a live top-N feed instead of the static seed list
- Additional ecosystems (Go modules, Rust crates)
- Baseline/diff mode: only fail on new findings vs. a committed baseline
- Tune concurrency limit (currently 40) with real production data if very large monorepos need it faster or safer
- Optional: heuristic to de-prioritize (not suppress) findings inside common test-fixture path patterns (
**/fixtures/**,**/snapshot*/**,**/test-data/**), surfaced as lower severity rather than filtered out entirely
