Offline software supply-chain integrity scanner — catches the attacks that have no CVE: typosquatting, dependency confusion, malicious install-time behavior, and lockfile tampering, across npm, PyPI, Go, and Cargo.
The threat in three sentences: attackers publish typosquats — packages one keystroke from famous names — and wait for a mistyped install. They exploit dependency confusion, registering your internal package names on public registries so resolvers fetch their code instead of yours. And they hide install-hook malware — code that runs the moment a package is installed, before you ever
importit — behind obfuscation and tampered lockfiles.
Stowaway is fully offline (zero network I/O at scan time), fully static
(scanned code is never executed), and deterministic (same input →
byte-identical output). It emits JSON, self-contained HTML, and SARIF 2.1.0
with a --fail-on CI merge gate. It complements CVE/SCA scanners — it does not
replace them (see docs/COMPARISON.md).
stowaway scan . --format all --fail-on P0 --internal-prefix mycorp-The scan finds two P0 packages: one whose install hook combines an install-time network call with bulk environment harvesting (escalated to CRITICAL) and also embeds a base64 literal that decodes to an ELF binary; and an internal package name that resolves from the public registry (dependency confusion). The non-zero exit code blocks the merge.
A single pass: walk the tree (excluding vendored trees and .gitignored paths,
with size caps), hand each ecosystem's manifests/lockfiles/install-hooks to its
parser, run all five rule families over the resulting ScanContext, fold the
findings into per-package priorities via the risk model, and emit. Every stage
is offline and deterministic; nothing scanned is ever executed.
git clone https://github.com/mk12002/Stowaway
cd Stowaway
pip install -e .Python 3.11+. Zero runtime dependencies — a security tool's own dependency tree is an attack surface, so Stowaway is standard-library only.
# Scan the current directory, write an HTML report to ./stowaway-report/
stowaway scan .
# Everything, all formats, CI gate on P0 findings
stowaway scan . --format all --out ./stowaway-report --fail-on P0
# Tell Stowaway about your internal namespace for high-confidence
# dependency-confusion checks (repeatable)
stowaway scan . --internal-prefix mycorp- --internal-prefix @mycorp/
# Accept reviewed findings so CI stays green without lowering the gate
stowaway scan . --write-baseline .stowaway-baseline.json # once, then edit reasons
stowaway scan . --baseline .stowaway-baseline.json --fail-on P0
# See every rule and its false-positive characteristics
stowaway rules listExit codes: 0 clean (or below the --fail-on threshold), 1 gate tripped,
2 usage error.
Stowaway is a plain Python package with a small, typed API. scan() returns a
Report of dataclasses; the three emitters are pure functions.
from stowaway.core.corpus import load_corpus
from stowaway.core.engine import scan
from stowaway.emitters import json_emitter
# Enable high-confidence dependency-confusion checks for your namespace.
corpus = load_corpus(internal_prefixes=("@mycorp/",))
report = scan("path/to/project", corpus)
print(report.project_score, report.priority_counts())
for f in report.findings:
print(f.rule_id, f.package.name, f.severity, f.confidence, f.evidence.file_path)
open("report.json", "w", newline="\n").write(json_emitter.emit(report))More: examples/python_api.py,
ci_gate.py,
baseline_workflow.py, and
emit_reports.py.
stowaway scan [PATH] [options] scan a project directory (default: cwd)
stowaway rules list print the full rule catalog + FP notes
stowaway version print the version
scan options:
| Flag | Default | Description |
|---|---|---|
--format {json,html,sarif,all} |
html |
Output format(s) to write |
--out DIR |
./stowaway-report |
Output directory |
--fail-on {P0,P1,P2,P3} |
— | Exit 1 if any package is at or above this priority |
--ecosystems LIST |
all | Comma-separated subset: npm,pypi,go,cargo |
--internal-prefix PREFIX |
— | Internal namespace prefix (repeatable); enables high-confidence R2 |
--corpus DIR |
bundled | Directory of <ecosystem>.txt corpus overrides |
--baseline PATH |
— | Suppress findings whose fingerprint is listed in this file |
--write-baseline PATH |
— | Write a baseline for all current findings, then exit 0 |
--quiet |
off | Suppress the summary and progress logs |
-v, --verbose |
off | Log scan progress to stderr (-v info, -vv debug) |
| Rule | Detects | Grounded in |
|---|---|---|
| R1 Typosquatting | Damerau-Levenshtein distance 1–2 (length-scaled), keyboard adjacency, homoglyphs, transpositions — plus structural transforms: prefix/suffix augmentation (dateutil→python3-dateutil), token reordering (python-nmap→nmap-python), pluralization, delimiter changes, npm scope-strip |
SpellBound / TypoGard / TypoSmart |
| R2 Dependency confusion | internal-namespace names resolvable from public registries (--internal-prefix); mixed-registry lockfiles |
— |
| R3 Install-time behavior (B1–B7) | network at install, env harvesting, credential-path access, obfuscation, payload drop (incl. decoded-payload magic-byte detection), persistence — with pair-wise combination escalation to CRITICAL | GuardDog / OSSGadget |
| R4 Lockfile integrity | manifest↔lock drift, missing/downgraded integrity hashes, registry-URL anomalies (http, IP-literal, non-standard) | — |
| R5 Structural signals | implausible version jumps (the "99.99.99" confusion shape), deep transitive chains | — |
stowaway rules list prints the full catalog with each rule's false-positive
notes. The complete (rule combination → priority) truth table is the
specification for the risk model and lives in
tests/test_risk.py.
- Combination escalations resolve first. Install-time network access plus environment harvesting in the same script is the classic exfiltration shape and escalates straight to CRITICAL / P0 — even though each signal alone is often innocent (telemetry, a native-module binary download).
- Corroboration outranks isolated signals. Two independent MEDIUM findings on one package beat one low-confidence HIGH.
- The project score (0–100) uses a transparent formula printed in every
report footer:
100 − 40·P0 − 15·P1 − 5·P2 − 1·P3, floored at 0.
Every finding carries a confidence (LOW/MEDIUM/HIGH) because heuristics are probabilistic — and Stowaway says so. Low-confidence rows read "verify", not "block". Calibration is the point: see docs/COMPARISON.md for how this maps to the typosquatting-detection literature.
- JSON — canonical, deterministic, every finding with a stable
fingerprint,rule_id,evidence(file:line + inert snippet),confidence,severity, andremediation. - HTML — a single self-contained file (inline CSS, no external requests), readable by a security lead: executive band, findings table with confidence visibly marked, per-ecosystem breakdown, full findings with evidence and concrete remediation, methodology + limitations footer. Shown at the top of this README — that is a real, unedited screenshot.
- SARIF 2.1.0 — one rule per detection rule, results with
level, physical locations, andpartialFingerprintsfor GitHub code-scanning dedup.
- name: Supply-chain gate
run: |
pip install stowaway # or: pip install -e path/to/stowaway
stowaway scan . --format sarif --out reports --fail-on P0
- uses: github/codeql-action/upload-sarif@v3 # optional: surface in code scanning
with: { sarif_file: reports/stowaway-report.sarif }A --baseline file lets a team acknowledge specific reviewed findings (each
with a written reason) so CI stays intentionally green for accepted risk
without weakening the gate for new findings.
Measured on the reference machine (Python 3.11, cold corpus warmed once):
| Workload | Time |
|---|---|
| Typical project (npm fixture: manifest + lockfile + install hook) | ~13 ms |
| 5,000-package lockfile | ~1.0 s |
| Bundled popularity corpus | 782 names across 4 ecosystems |
No network round-trips means latency is CPU-bound and identical offline.
Calibration on real projects: scanned against the unmodified upstream manifests of Flask, Express, ripgrep, and kubectl — 153 real dependencies across all four ecosystems — Stowaway produced 0 false positives (every project scored 100/100). Details and reproduction steps in docs/CALIBRATION.md.
Typosquat detection compares names against bundled per-ecosystem lists of widely
known packages (src/stowaway/data/popularity/*.txt) — names only, no fabricated
rankings. A stale corpus degrades typosquat recall; the corpus is versioned,
and you can supply your own with --corpus DIR (a directory containing any of
npm.txt, pypi.txt, go.txt, cargo.txt).
Adding an ecosystem means implementing the four-method Ecosystem ABC
(src/stowaway/ecosystems/base.py); adding a
behavioral rule means adding a pattern entry in
src/stowaway/rules/behavior.py. See
docs/CONTRIBUTING.md — including the inert-fixture policy,
which is a hard rule enforced by an automated guard test.
Runnable examples/ demonstrate both the CLI and the Python API
against a self-contained inert sample project.
Stowaway detects malware; the repository contains none. Every test fixture
is a benign mimic — a file that structurally resembles an attack pattern but
does nothing harmful (a postinstall that console.logs "simulated-network- call"; a base64 blob that decodes to HARMLESS FIXTURE …). This is enforced by
tests/test_inertness_guard.py, which scans the
repo's own fixtures and fails the build on any functional network code,
credential read, eval/exec of decoded content, or a base64 blob that decodes
to anything but the harmless marker. See docs/THREAT_MODEL.md.
- Offline by design. Registry-metadata signals (maintainer changes, publish timestamps, download counts) require network access and are out of scope — documented, never simulated. Tools like GuardDog and Packj cover that layer.
- Static by design. Determined obfuscation can evade static patterns; the arms race structurally favors attackers over time. Stowaway raises the cost of the common shapes; it cannot see through everything.
- Heuristics produce false positives. Every finding carries a confidence mark and every rule documents its false-positive modes. Low-confidence findings say "verify", not "block".
- A clean scan is evidence, not proof.
- Partial parsers.
pnpm-lock.yamlsupport is limited (stdlib-only policy: no YAML parser) and.gitignorehandling is a conservative subset (no negation patterns). Both are surfaced as scan warnings where relevant. - Not an SCA/CVE matcher, not a sandbox, not a license scanner. Stowaway complements those tools — see docs/COMPARISON.md.
Browsable docs site: https://mk12002.github.io/Stowaway/ (built from
docs/ with MkDocs Material).
- docs/COMPARISON.md — how Stowaway relates to GuardDog, OSSGadget, Packj, SCA scanners, and the research literature.
- docs/CALIBRATION.md — real-world false-positive results on public projects.
- docs/THREAT_MODEL.md — Stowaway's own security stance (it parses untrusted input).
- docs/CONTRIBUTING.md — adding ecosystems/rules; the inert-fixture law.
- docs/VISION.md — what it is, who it's for, non-goals.
- SECURITY.md — reporting a vulnerability; Stowaway's own guarantees. CHANGELOG.md — release history.
Stowaway is pre-1.0. Direction, roughly in priority order:
- More ecosystems — RubyGems and Maven parsers, behind the same
EcosystemABC (the hourglass keeps this N+M, not N×M). - Corpus tooling — a documented, reproducible refresh path for the bundled popularity lists (stale corpus degrades typosquat recall).
- pnpm/yaml lockfiles — fuller support without taking on a runtime YAML dependency (the zero-dep line holds).
- Richer structural signals — maintainer-free heuristics for suspicious version topologies and transitive-depth anomalies.
Registry-metadata signals (maintainer changes, publish timestamps, download counts) remain out of scope by design — they require network access. See docs/COMPARISON.md.
If you reference Stowaway in research or writing, please cite it via the
CITATION.cff file (GitHub renders a "Cite this repository"
button from it).
Once the repository is archived on Zenodo (see docs/RELEASING.md), add the DOI badge here — replace the placeholder with the concept DOI:
[](https://doi.org/10.5281/zenodo.XXXXXXX)Apache-2.0 — see LICENSE.
