
Jul 2026–Present
Warden — Software Supply-Chain Security Platform
- 2026-07–present
- active
- python · fastapi · react
- repo
- 2,908 backend tests · CI
- 2 stars
- head 942e3e9 · 2026-09-18
- ci: passing
- Joseph Wright of Derby, An Iron Forge, 1772 — public domain, Wikimedia Commons
Gives a Python package an allow, warn or block verdict — from what its code does, where it came from, and what is known about it.
- analyzers
- 14
- detected (synthetic)
- 13 / 14
- backend tests in CI
- 2,908
Problem
A single pip install runs third-party code with the developer's or the CI runner's privileges. Attackers exploit that with malicious publishes, typosquats, dependency confusion, and account takeovers of packages that were fine yesterday.
A vulnerability scanner cannot see any of this: there is no CVE for a package nobody has reported yet. Warden asks a different question — what does this package do, and who really published it? — and keeps vulnerability intelligence as a separate dimension, because malicious and vulnerable are different questions with different responses.
Approach
A FastAPI backend runs the package pipeline. For a name and version, Warden resolves the release on PyPI, downloads the artifact under a size cap, verifies the digest the registry published against the bytes received, and extracts it under hostile-archive guards; nothing analyzed is ever executed. Fourteen analyzers then run in parallel, each with its own timeout: metadata, typosquatting, AST behavior, install scripts, other install vectors (.pth start-up hooks, in-tree build backends, console scripts that shadow common commands such as pip, python or git), obfuscation, known indicators (IOC), file inventory, secrets, dependency confusion, provenance, YARA and Semgrep (optional tools that report themselves unavailable when not installed), and vulnerability intelligence from OSV, CISA KEV and FIRST EPSS.
Every analyzer emits the same Finding: a severity and a separate confidence, the file and line where known, CWE and MITRE ATT&CK mappings, and remediation. A correlation engine combines findings into named attack chains — credential theft followed by exfiltration, install-time droppers, obfuscated loaders — and a risk engine scores separate dimensions, behavioral and vulnerability risk among them, keeping the 0–100 risk score as a derived value. A policy-as-code engine maps the result to allow, warn or block.
Around the package pipeline sit project scans that parse manifests and lock files for dependency-hygiene and dependency-confusion checks, a dependency graph and CycloneDX 1.6 or SPDX 2.3 SBOMs; release-to-release behavioral diffs; offline analysis of container image archives; and a monitoring worker that watches packages for new releases. The REST API serves verdicts, a CLI gate fails a build on a chosen decision, and a React + TypeScript security console shows the results; a GitHub Action runs a project scan in a workflow, uploading SARIF before it fails on a chosen finding severity. Results persist to PostgreSQL, with Redis for caching and events.
acquisition
PyPI · digest check
safe extraction
guarded · no exec
14 analyzers
parallel · timeouts
correlation
named attack chains
risk engine
dimensions + guardrail
policy engine
allow · warn · block
Decisions
- Static analysis only, hostile input assumed
- Nothing analyzed is ever executed: the classic payload runs at install time, and running it to see is what the attacker wants. Archives are hostile by definition — format detected from magic bytes, every member checked for path traversal, and limits on count, size, depth and time that also cover skipped members, because skipping a member still decompresses it. The stated cost: static analysis misses runtime-only behavior. A gVisor sandbox is designed in docs/SANDBOX.md but not built, and the setting that would switch it on is refused.
- Bounded ML influence, from a measured failure
- The model — a calibrated RandomForest for probability, an IsolationForest for novelty — is not allowed to decide a verdict on its own. Fusion is max(rule, ml), so it can never lower a rule score, and measurement showed that alone is not enough: trained only on synthetic samples, the model separated the synthetic classes almost perfectly (hold-out PR-AUC ≈ 1.0) and still gave ordinary libraries such as requests, jinja2 and flask ~0.99 malicious probability. So below a rule score of 35 it may add at most 25 points, and a model-only opinion stays at or below the medium band, for review, instead of blocking a build. A small measured set of established PyPI projects is now mixed into training, their benign label recorded as an assumption. The stated cost: the model contributes less than its synthetic metrics suggest.
- Several numbers, not one blurred score
- Every finding carries a severity and a separate confidence, and policy rules fire only at or above a configured confidence, so capability-grade observations cannot block a build on their own; known-malware matches, critical attack chains and hash mismatches cannot be removed by any allowlist or exception. Behavioral and vulnerability risk are scored separately — two questions with different responses — and vulnerability risk is null, never 0, when intelligence is unavailable. The stated cost: more to reason about per finding, and two numbers to explain.
- Fail closed, and say which layers ran
- A partial scan must not read as clean. An analyzer that crashes or times out produces ANALYZER_ERROR, which raises risk rather than silently shrinking the evidence, and that result is not cached. Warden must also work without YARA, Semgrep or gitleaks installed, so a missing optional tool reports itself unavailable and the scan says so, instead of silently returning nothing found. The stated costs: large packages on slow links can surface as elevated risk, and coverage varies by deployment, so each scan reports which layers ran.
Evidence
| Detection, synthetic benchmark (malicious, 14) | 13 / 14 = 0.929 |
|---|---|
| Evasive variants, synthetic (4) | 4 / 4 = 1.0 |
| False positives, synthetic (benign, 8) | 1 / 8 = 0.125 — a warn, not a block |
| install-time download and execute (synthetic) | risk 100 — block |
| typosquat of requests (synthetic) | risk 47 — warn |
| SSH private key read and sent over a socket (synthetic) | risk 33 — allow (missed) |
| compiler call in setup.py (synthetic, benign) | risk 40 — warn (false positive) |
| Backend tests, CI run on 942e3e9 (2026-09-18) | 2,908 passed, 7 skipped, 1 xfailed |
Outcome
The detection numbers come from a synthetic benchmark, and the repo labels it as one. Twenty-two hand-written, inert packages — 14 malicious, 4 of them written to evade simple pattern matching, and 8 harmless look-alikes — go through the real pipeline offline under the default policy, each carrying an established package's registry facts. A malicious sample counts as detected when it draws a warn or a block: 13 of 14 malicious samples are detected, all 4 evasive ones among them, and 1 of 8 harmless look-alikes is flagged with a warning, and none of the 8 is blocked. The recorded run had YARA available; the same totals were measured with YARA and Semgrep both unavailable, which is how CI runs it. The miss is an SSH private key read and sent over a socket from a runtime module, allowed at risk 33: correlation will not turn that into an attack chain without corroboration, because legitimate SSH, deployment and upload tools do exactly this. The false positive is a compiler call in setup.py, surfaced for review rather than blocked or ignored. The repo's benchmark notes say the corpus was written by the same people who wrote the analyzers, so it shares their blind spots, and the README calls these numbers a regression baseline, not a real-world detection rate. Building the benchmark exposed and fixed five detection gaps and an ML over-escalation.
The engineering around the verdict: in the CI run on main at commit 942e3e9 (2026-09-18), 2,908 backend tests passed with 7 skipped, and every other job passed too — ruff lint, bandit and pip-audit security audits, a PostgreSQL migration round-trip with an app start-up smoke test, the frontend's lint, type-check, tests, build and npm audit, and container builds that fail on fixable critical or high Trivy findings. CI runs on every push to main and every pull request, and the benchmark itself is a CI regression gate. Docker Compose is the documented way to bring up the full stack; CI builds and scans the images but does not start that stack. Warden's own posture, as configured: refresh-token rotation, five-role RBAC, a hash-chained audit log with a verification endpoint, secrets redacted before anything is logged, stored or returned, and containers set to run as non-root on digest-pinned base images with a read-only root filesystem. Among the honest limitations the README lists: Warden reduces risk but does not eliminate it, and is no substitute for reviewing what you depend on; static analysis is evadable by sufficiently novel obfuscation; the ML model's metrics are synthetic hold-out numbers; provenance checks stop short of cryptographic signature verification; vulnerability intelligence is only as current as its sources; and the bundled indicator and popularity lists are point-in-time snapshots.
Next
The opt-in dynamic sandbox — designed in docs/SANDBOX.md, not built; the setting that would switch it on is refused
Transitive dependencies for project scans beyond what lock files record
Package analysis for npm and other ecosystems — npm manifests already feed SBOMs on main, not yet in a release; verdicts stay PyPI-only
A Marketplace release of the GitHub Action (it works from the repository today)
Next: the other case files
- Proactive Feasibility Scheduler
An evaluation study of ML-based GPU-cluster job scheduling — and a proven negative result.
- PlantPal+
Cross-platform app unifying plant care, fitness, and nutrition in one daily dashboard with streaks, reminders, and cloud sync.