argus-radar 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shantanu Gopale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,119 @@
1
- # Temporary Holding Version
1
+ # ARGUS · supply-chain radar
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![ci](https://github.com/shantanugopale/argus/actions/workflows/ci.yml/badge.svg)](https://github.com/shantanugopale/argus/actions/workflows/ci.yml)
4
+ ![node >= 22.13](https://img.shields.io/badge/node-%3E%3D22.13-green)
5
+ ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
6
+ ![license](https://img.shields.io/badge/license-MIT-blue)
7
+
8
+ <p align="center">
9
+ <img src="docs/img/dashboard.png" alt="Argus dashboard — viral repos + scan registry" width="900" />
10
+ </p>
11
+
12
+ Your dependencies change faster than you review them. Argus tracks **what's trending on npm and GitHub right now**, verifies whether it's **safe to install, right now**, and gives you one verdict — with the evidence chain, not just a score.
13
+
14
+ ```text
15
+ verdict ▮▮▮▮▮▮▮▮▮▮ 100/100 CRITICAL — do not install
16
+ evidence
17
+ ◈ S10 Known vulnerabilities (+80)
18
+ OSV.dev lists 10 advisories for this package; worst severity CRITICAL: GHSA-29mw-wpgm-hmr9, …
19
+ ◈ S40 Bus-factor one (+20)
20
+ Only one maintainer (jdalton) for a package with 206,478,795 weekly downloads —
21
+ compromise of a single account redirects every consumer
22
+ ```
23
+
24
+ **Zero runtime dependencies.** The radar itself is Node ≥ 22.13 with nothing installed — `node apps/cli/argus.ts` just runs (TypeScript via native type-stripping).
25
+
26
+ **Live dashboard:** <https://shantanugopale.github.io/argus/> (static snapshot refreshed daily by CI, or run `node apps/api/main.ts` for live scanning).
27
+
28
+ ## Why
29
+
30
+ Vibe-coders and LLM-driven dev loops install packages from trending pages and model suggestions within seconds. Existing gates (audit, socket, etc.) run *after* you've already adopted a dependency — and none of them look at *velocity*. Argus checks at the moment of install:
31
+
32
+ - **Spot poisoning** — a familiar package whose daily downloads suddenly explode 8× over its own baseline
33
+ - **Shocked-repo injection** — a repo that trends today with 1,000+ stars but is under 6 months old
34
+ - **Classic malware** — install scripts, OSV advisories, typosquats against a built-in top-packages index
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ # npm source of truth: Node >= 22.13 (native TS type-stripping, zero installs)
40
+ node apps/cli/argus.ts scan chalk # verdict + exit code 0/10/20
41
+ node apps/cli/argus.ts trend # 14-day download-velocity radar + GitHub trending
42
+ node apps/cli/argus.ts watch # daemon: rescan persisted refs every 15 min
43
+ node apps/api/main.ts # dashboard on http://127.0.0.1:8787
44
+ ```
45
+
46
+ ### Scan any ref
47
+
48
+ | ref | gate |
49
+ | --- | --- |
50
+ | `scan npm:lodash` | explicit npm package |
51
+ | `scan lodash` | shorthand for `npm:` |
52
+ | `scan gh:owner/repo` | GitHub repo incl. dependency-propagation (scans deps!) |
53
+ | `scan chalk event-stream gh:octocat/Hello-World` | multiple refs, worst verdict wins |
54
+
55
+ <p align="center">
56
+ <img src="docs/img/scan.png" alt="argus scan npm:lodash verdict" width="900" />
57
+ </p>
58
+
59
+ ## The signals
60
+
61
+ | id | signal | what it catches |
62
+ | --- | --- | --- |
63
+ | S10 | Known vulnerabilities | CVEs / malware advisories via OSV.dev |
64
+ | S20 | Install scripts | `preinstall` / `postinstall` code execution surface |
65
+ | S30 | Fresh-publish spike | brand-new release on a high-velocity package |
66
+ | S40 | Bus-factor one | single maintainer holding millions of installs |
67
+ | S60 | Typosquat | Levenshtein distance vs built-in top-packages index |
68
+ | S70 | Version churn | too many releases in the last 21 days |
69
+ | S80 | Repo linkage | missing source repository on a high-traffic package |
70
+ | S90 | Repo trust basics | no license / no description on a repo |
71
+ | S100 | Viral-no-history | trending today + young repo (24h GitHub pulse) |
72
+ | S110 | Dep propagation | a gateway repo whose dependencies are risky on their own |
73
+
74
+ The merge is **evidence over scores**: findings sum (clamped to 100), severity floors apply, and the `severe-cap invariant` guarantees a report with no severe finding can never cross into HIGH_RISK territory.
75
+
76
+ ## Exit codes (scriptable gates)
77
+
78
+ | code | verdict |
79
+ | --- | --- |
80
+ | `0` | SAFE |
81
+ | `10` | NEEDS_REVIEW |
82
+ | `20` | HIGH_RISK / CRITICAL |
83
+ | `3` | package/repo not found |
84
+ | `5` | unexpected failure |
85
+
86
+ Pipe it into CI: `node apps/cli/argus.ts scan chalk || exit 1`.
87
+
88
+ ## Architecture
89
+
90
+ Hexagonal — pure engine, pluggable providers, thin front doors:
91
+
92
+ ```
93
+ engine/ pure logic, fully hermetic · merge, signals, velocity, corpus analysis
94
+ shell/ connectors (npm registry, OSV.dev, GitHub, npm downloads) + .argus/ persistence + fixture playback
95
+ apps/cli scan · trend · watch (exit-code contract)
96
+ apps/api loopback server: /api/scan · /api/trend · /api/scans · /healthz
97
+ apps/web zero-dependency static dashboard (served by apps/api)
98
+ ```
99
+
100
+ - **Determinism:** `ARGUS_FIXTURES_DIR` replays recorded provider snapshots — CI never touches the network.
101
+ - **Reproducibility:** every provider response is content-hashed and logged into the report.
102
+ - **Persistence:** `.argus/scans/*.json` (digest-verified, 60 s TTL cache) + `.argus/trend/latest.json`.
103
+ - **Tests:** 90+ (unit → integration → e2e incl. golden-corpus regression over 14 refs running through the full engine).
104
+ - **Gates:** `npm run lint && npm run typecheck && npm test` (ubuntu + windows matrix CI).
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ npm install # dev deps only (eslint + typescript)
110
+ npm test # 90+ tests, hermetic
111
+ npm run record # refresh fixture snapshots from live providers
112
+ npm run api # start the dashboard
113
+ ```
114
+
115
+ Docs: [`docs/01-PRD.md`](docs/01-PRD.md) · [`02-ARCHITECTURE.md`](docs/02-ARCHITECTURE.md) · [`03-THREAT-MODEL.md`](docs/03-THREAT-MODEL.md) · [`04-ADR.md`](docs/04-ADR.md) · [`05-SDLC-PLAN.md`](docs/05-SDLC-PLAN.md)
116
+
117
+ ## License
118
+
119
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,138 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>ARGUS · supply-chain radar</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body { background: #0b0f14; color: #d7e0ea; font-family: "Cascadia Code", Consolas, monospace; margin: 0; padding: 24px; }
11
+ h1 { font-size: 20px; letter-spacing: 2px; margin: 0 0 4px; }
12
+ .sub { color: #6b7a8c; font-size: 12px; margin-bottom: 24px; }
13
+ .panel { border: 1px solid #1d2733; border-radius: 10px; padding: 16px 18px; margin-bottom: 20px; background: #0e141b; }
14
+ h2 { font-size: 12px; letter-spacing: 1.5px; color: #8fb2d0; margin: 0 0 12px; text-transform: uppercase; }
15
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
16
+ td, th { padding: 6px 8px; border-bottom: 1px solid #16202b; text-align: left; }
17
+ th { color: #6b7a8c; font-weight: 400; font-size: 11px; letter-spacing: 1px; }
18
+ .spike-exploding { color: #ff6b6b; font-weight: bold; }
19
+ .spike-hot { color: #ffd166; }
20
+ .spike-rising { color: #d0a03b; }
21
+ .verdict-CRITICAL, .verdict-HIGH_RISK { color: #ff6b6b; }
22
+ .verdict-NEEDS_REVIEW { color: #ffd166; }
23
+ .verdict-SAFE { color: #7dd87d; }
24
+ .scanrow td:first-child { color: #7fd4f0; }
25
+ button { background: #14212e; color: #9fd2f5; border: 1px solid #1d3851; border-radius: 6px; padding: 8px 14px; font-family: inherit; cursor: pointer; }
26
+ button:hover { background: #1b2d3f; }
27
+ input { background: #0b1219; color: #d7e0ea; border: 1px solid #1d2733; border-radius: 6px; padding: 8px; font-family: inherit; width: 280px; }
28
+ code { color: #7fd4f0; }
29
+ footer { color: #45525f; font-size: 11px; margin-top: 30px; }
30
+ .empty { color: #55626e; font-size: 12px; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <h1>ARGUS ▸ supply-chain radar</h1>
35
+ <div class="sub">what's trending right now — and is it safe to install, right now · <code>exit codes 0/10/20</code></div>
36
+
37
+ <div class="panel">
38
+ <h2>Scan a ref (npm package or gh:owner/repo)</h2>
39
+ <input id="ref" placeholder="chalk" />
40
+ <button onclick="scan()">scan</button>
41
+ <div id="scanArea" style="margin-top: 12px"></div>
42
+ </div>
43
+
44
+ <div class="panel">
45
+ <h2>Download spikes (top 15 npm cohort · 14d)</h2>
46
+ <div id="spikes" class="empty">loading…</div>
47
+ </div>
48
+
49
+ <div class="panel">
50
+ <h2>Viral GitHub repos (24h)</h2>
51
+ <div id="repos" class="empty">loading…</div>
52
+ </div>
53
+
54
+ <div class="panel">
55
+ <h2>Scan drill-down (registry reports persisted in .argus/)</h2>
56
+ <div id="history" class="empty">loading…</div>
57
+ </div>
58
+
59
+ <footer>ARGUS v1.0 · zero-dependency static client · evidence over scores</footer>
60
+
61
+ <script>
62
+ async function j(url, opts) {
63
+ const r = await fetch(url, opts);
64
+ if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || ("HTTP " + r.status));
65
+ return r.json();
66
+ }
67
+
68
+ let SNAPSHOT_MODE = false;
69
+ async function withSnapshotFallback(apiUrl, key) {
70
+ try {
71
+ const r = await fetch(apiUrl);
72
+ if (r.ok) return await r.json();
73
+ } catch {}
74
+ SNAPSHOT_MODE = true;
75
+ const s = await fetch("snapshot.json", { cache: "no-store" }).then(r => r.ok ? r.json() : null).catch(() => null);
76
+ return s ? s[key] : null;
77
+ }
78
+
79
+ async function loadTrend() {
80
+ try {
81
+ const t = await withSnapshotFallback("/api/trend", "trend");
82
+ if (!t) throw new Error("no data");
83
+ if (SNAPSHOT_MODE) {
84
+ const sub = document.querySelector(".sub");
85
+ if (sub && !sub.dataset.tagged) { sub.textContent += " · static snapshot " + (t.generatedAt || ""); sub.dataset.tagged = "1"; }
86
+ }
87
+ const spikes = document.getElementById("spikes");
88
+ if (t.rows.length === 0) { spikes.innerHTML = '<span class="empty">no anomalous spikes in cohort</span>'; }
89
+ else {
90
+ spikes.innerHTML = "<table><tr><th>package</th><th>class</th><th>ratio</th><th>downloads/day</th><th></th></tr>" +
91
+ t.rows.slice(0, 12).map(r => "<tr><td><code>" + r.name + "</code></td><td class='spike-" + r.spike + "'>" + r.spike + "</td><td>" + r.burstRatio.toFixed(2) + "x" +
92
+ (r.cohortWide ? " (rel " + r.relativeRatio.toFixed(2) + "x)" : "") + "</td><td>" + r.latest.toLocaleString("en-US") + "</td>" +
93
+ "<td><button onclick=\"document.getElementById('ref').value='npm:" + r.name + "';scan()\">scan</button></td></tr>").join("") + "</table>";
94
+ }
95
+ const repos = document.getElementById("repos");
96
+ if (t.repos.length === 0) { repos.innerHTML = '<span class="empty">trending feed unavailable</span>'; }
97
+ else {
98
+ repos.innerHTML = "<table><tr><th>repo</th><th>★ today</th><th></th></tr>" +
99
+ t.repos.slice(0, 10).map(r => "<tr class='scanrow'><td><code>" + r.repo + "</code></td><td>" + r.starsThisPeriod.toLocaleString("en-US") + "</td>" +
100
+ "<td><button onclick=\"document.getElementById('ref').value='gh:" + r.repo + "';scan()\">scan</button></td></tr>").join("") + "</table>";
101
+ }
102
+ } catch (e) { document.getElementById("spikes").innerHTML = "error: " + e.message; }
103
+ }
104
+
105
+ async function scan() {
106
+ const ref = document.getElementById("ref").value.trim();
107
+ const area = document.getElementById("scanArea");
108
+ if (!ref) return;
109
+ area.innerHTML = "scanning " + ref + " …";
110
+ try {
111
+ const r = await j("/api/scan?ref=" + encodeURIComponent(ref));
112
+ const rows = r.findings.map(f => "<tr><td>" + f.id + "</td><td>" + f.title + "</td><td>+" + f.points + "</td><td class='verdict-" + r.verdict + "'>" + f.evidence + "</td></tr>").join("");
113
+ area.innerHTML =
114
+ "<div style='margin-bottom:8px'><span class='verdict-" + r.verdict + "' style='font-weight:bold'>" + r.verdict + "</span> · score " + r.score + "/100 · checked " + (r.persistedAt || r.checkedAt) + "</div>" +
115
+ (rows ? "<table><tr><th>sig</th><th>title</th><th>pts</th><th>evidence</th></tr>" + rows + "</table>" : "<span class='empty'>no signals fired — clean scan</span>");
116
+ loadHistory();
117
+ } catch (e) { area.innerHTML = "<span class='verdict-CRITICAL'>error: " + e.message + (SNAPSHOT_MODE ? " · static snapshot mode — run `node apps/api/main.ts` for live scans" : "") + "</span>"; }
118
+ }
119
+
120
+ async function loadHistory() {
121
+ try {
122
+ const items = await withSnapshotFallback("/api/scans", "scans");
123
+ if (!items) { document.getElementById("history").innerHTML = "<span class='empty'>no scans persisted yet — run one above</span>"; return; }
124
+ const el = document.getElementById("history");
125
+ if (items.length === 0) { el.innerHTML = "<span class='empty'>no scans persisted yet — run one above</span>"; return; }
126
+ el.innerHTML = "<table><tr><th>ref</th><th>verdict</th><th>score</th><th>persisted</th></tr>" +
127
+ items.slice(0, 12).map(i =>
128
+ "<tr class='scanrow'><td><code>" + i.ref + "</code></td><td class='verdict-" + i.verdict + "'>" + i.verdict +
129
+ "</td><td>" + i.score + "/100</td><td>" + new Date(i.persistedAt).toLocaleString("en-US") + "</td></tr>").join("") + "</table>";
130
+ } catch {}
131
+ }
132
+
133
+ document.getElementById("ref").addEventListener("keydown", e => { if (e.key === "Enter") scan(); });
134
+ loadTrend();
135
+ loadHistory();
136
+ </script>
137
+ </body>
138
+ </html>
package/bin/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // Argus launcher: bundled JS when installed from npm (node_modules forbids
3
+ // type-stripping), raw TypeScript via Node's native stripping during dev.
4
+ import { existsSync } from "node:fs";
5
+ import { spawnSync } from "node:child_process";
6
+ import { join, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const bundled = join(here, "..", "dist", "npm", "cli.js");
11
+ const devEntry = join(here, "..", "apps", "cli", "argus.ts");
12
+
13
+ const [entry, needsStrip] = existsSync(bundled) ? [bundled, false] : [devEntry, true];
14
+ const flags = needsStrip ? ["--experimental-strip-types"] : [];
15
+ const result = spawnSync(process.execPath, [...flags, entry, ...process.argv.slice(2)], {
16
+ stdio: "inherit",
17
+ env: process.env
18
+ });
19
+ process.exitCode = result.status ?? 1;