Skip to content

Stowaway

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.

Python 3.11+ License Apache-2.0 Runtime deps: 0 Tests: 136 Offline

Stowaway HTML report — executive summary and findings table

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 import it — 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).


See it work

stowaway scan . --format all --fail-on P0 --internal-prefix mycorp-

Terminal: a scan that trips the P0 gate

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.

How it works

Stowaway scan pipeline

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.

Install

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.

Quickstart

# 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 list

Exit codes: 0 clean (or below the --fail-on threshold), 1 gate tripped, 2 usage error.

Use it as a library

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.

CLI reference

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)

Detection rules

Rule Detects Grounded in
R1 Typosquatting Damerau-Levenshtein distance 1–2 (length-scaled), keyboard adjacency, homoglyphs, transpositions — plus structural transforms: prefix/suffix augmentation (dateutilpython3-dateutil), token reordering (python-nmapnmap-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.

How findings become priorities

  • 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.

Output formats

  • JSON — canonical, deterministic, every finding with a stable fingerprint, rule_id, evidence (file:line + inert snippet), confidence, severity, and remediation.
  • 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, and partialFingerprints for GitHub code-scanning dedup.

Using Stowaway as a CI gate

- 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.

Performance

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.

The popularity corpus

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).

Extending

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.

Safety of this repository

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.

Limitations — read this

  • 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.yaml support is limited (stdlib-only policy: no YAML parser) and .gitignore handling 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.

Documentation

Browsable docs site: https://mk12002.github.io/Stowaway/ (built from docs/ with MkDocs Material).

Roadmap

Stowaway is pre-1.0. Direction, roughly in priority order:

  • More ecosystems — RubyGems and Maven parsers, behind the same Ecosystem ABC (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.

Citing Stowaway

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:

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.XXXXXXX.svg)](https://doi.org/10.5281/zenodo.XXXXXXX)

License

Apache-2.0 — see LICENSE.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages