vetguard 0.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/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VERSION,
4
+ checkPackage,
5
+ renderJson,
6
+ renderSarif,
7
+ renderTerminal,
8
+ resolveExitCode,
9
+ scanProject
10
+ } from "./chunk-XXICU3EZ.js";
11
+
12
+ // src/cli.ts
13
+ import { parseArgs } from "util";
14
+ import process from "process";
15
+ var HELP = `vetguard ${VERSION} - scan npm dependencies for AI-era supply-chain threats
16
+
17
+ Usage:
18
+ vetguard scan [dir] Scan a project's dependencies (defaults to cwd)
19
+ vetguard check <pkg> Vet a single package before installing
20
+ (e.g. vetguard check react-codeshift, foo@1.2.3)
21
+ vetguard --help Show this help
22
+ vetguard --version Show version
23
+
24
+ Options:
25
+ --offline Do not contact the registry; unverifiable facts
26
+ are reported as "could not verify", never "safe".
27
+ --json Print the report as JSON instead of text.
28
+ --sarif Print SARIF 2.1.0 for GitHub code scanning.
29
+ --fail-on <severity> Exit non-zero only when a finding at or above this
30
+ severity exists (critical|high|medium|low|info).
31
+ Default: any finding exits non-zero.
32
+
33
+ vetguard never executes the code it scans. When it cannot verify something it
34
+ says so rather than calling it safe.`;
35
+ var SEVERITIES = ["critical", "high", "medium", "low", "info"];
36
+ async function main(argv) {
37
+ const { values, positionals } = parseArgs({
38
+ args: argv,
39
+ allowPositionals: true,
40
+ options: {
41
+ help: { type: "boolean", short: "h" },
42
+ version: { type: "boolean", short: "v" },
43
+ offline: { type: "boolean" },
44
+ json: { type: "boolean" },
45
+ sarif: { type: "boolean" },
46
+ "fail-on": { type: "string" }
47
+ }
48
+ });
49
+ if (values.version) {
50
+ console.log(VERSION);
51
+ return 0;
52
+ }
53
+ const command = positionals[0];
54
+ if (values.help || command === void 0 || command === "help") {
55
+ console.log(HELP);
56
+ return 0;
57
+ }
58
+ const failOnRaw = values["fail-on"];
59
+ if (failOnRaw !== void 0 && !SEVERITIES.includes(failOnRaw)) {
60
+ console.error(`Invalid --fail-on value: ${failOnRaw} (expected ${SEVERITIES.join(", ")})`);
61
+ return 2;
62
+ }
63
+ const options = {
64
+ offline: values.offline === true,
65
+ json: values.json === true,
66
+ sarif: values.sarif === true,
67
+ failOn: failOnRaw
68
+ };
69
+ if (command === "scan") {
70
+ return runScan(positionals[1] ?? process.cwd(), options);
71
+ }
72
+ if (command === "check") {
73
+ const spec = positionals[1];
74
+ if (spec === void 0) {
75
+ console.error("check requires a package name, e.g. vetguard check express\n");
76
+ console.error(HELP);
77
+ return 2;
78
+ }
79
+ return runCheck(spec, options);
80
+ }
81
+ console.error(`Unknown command: ${command}
82
+ `);
83
+ console.error(HELP);
84
+ return 2;
85
+ }
86
+ async function runScan(dir, options) {
87
+ let report;
88
+ try {
89
+ report = await scanProject(dir, { offline: options.offline });
90
+ } catch (err) {
91
+ console.error(err instanceof Error ? err.message : String(err));
92
+ return 2;
93
+ }
94
+ return emit(report, options);
95
+ }
96
+ async function runCheck(specInput, options) {
97
+ const report = await checkPackage(specInput, { offline: options.offline });
98
+ return emit(report, options);
99
+ }
100
+ function emit(report, options) {
101
+ const output = options.sarif ? renderSarif(report, VERSION) : options.json ? renderJson(report, VERSION) : renderTerminal(report);
102
+ console.log(output);
103
+ return resolveExitCode(report, options.failOn);
104
+ }
105
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
106
+ console.error(err instanceof Error ? err.stack : String(err));
107
+ process.exit(2);
108
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Core data model. These types are the contract between collectors (which do
3
+ * all IO) and detectors (pure functions that judge). Nothing here does IO.
4
+ */
5
+ type Severity = "critical" | "high" | "medium" | "low" | "info";
6
+ /** How sure we are the finding is real, distinct from how bad it would be. */
7
+ type Confidence = "high" | "medium" | "low";
8
+ /** How a package entered the dependency graph, so findings can be traced back. */
9
+ type DependencyKind = "prod" | "dev" | "peer" | "optional" | "bundled" | "transitive";
10
+ /** How the version was specified, which decides what we can verify about it. */
11
+ type DependencySource = "registry" | "git" | "file" | "link" | "alias" | "workspace" | "unknown";
12
+ /**
13
+ * The facts a collector gathers about one resolved dependency. Detectors read
14
+ * these; they never reach out to the network or filesystem themselves. Fields
15
+ * are optional because a fact may be genuinely unknowable (offline, private
16
+ * registry, git dependency), and "unknown" must never be treated as "safe".
17
+ */
18
+ interface PackageFacts {
19
+ name: string;
20
+ /** Resolved version from the lockfile, if any. */
21
+ version?: string;
22
+ requestedRange?: string;
23
+ kind: DependencyKind;
24
+ source: DependencySource;
25
+ /** Whether the registry has any record of this name at all. */
26
+ existsOnRegistry?: boolean;
27
+ /** Whether this exact version is still published. */
28
+ versionPublished?: boolean;
29
+ firstPublishAt?: string;
30
+ latestPublishAt?: string;
31
+ /** Days since the package name was first published, computed at collection time. */
32
+ ageDays?: number;
33
+ /** Number of published versions of this name. */
34
+ versionCount?: number;
35
+ weeklyDownloads?: number;
36
+ hasInstallScript?: boolean;
37
+ repositoryUrl?: string;
38
+ integrity?: string;
39
+ resolvedUrl?: string;
40
+ /** Where this fact set came from, for traceable verdicts. */
41
+ evidencePath?: string;
42
+ }
43
+ /** A single detector result. Evidence is always quoted, never live content. */
44
+ interface Finding {
45
+ ruleId: string;
46
+ severity: Severity;
47
+ confidence: Confidence;
48
+ packageName: string;
49
+ packageVersion?: string;
50
+ title: string;
51
+ /** One line: why this matters. */
52
+ detail: string;
53
+ /** Concrete, escaped, truncated evidence backing the finding. */
54
+ evidence?: string;
55
+ location?: string;
56
+ }
57
+ /** A pure detector: facts in, findings out. No IO, no side effects. */
58
+ interface Detector {
59
+ id: string;
60
+ description: string;
61
+ detect(pkg: PackageFacts): Finding[];
62
+ }
63
+ type ScanVerdict = "clean" | "findings" | "could-not-verify";
64
+ interface Report {
65
+ verdict: ScanVerdict;
66
+ target: string;
67
+ ecosystem: string;
68
+ packagesScanned: number;
69
+ findings: Finding[];
70
+ /** Packages we could not fully verify (offline, unsupported source, etc.). */
71
+ unverified: string[];
72
+ generatedAt: string;
73
+ /** Whether the scan read a resolved lockfile tree or only the manifest. */
74
+ basis?: "lockfile" | "manifest";
75
+ /** Non-fatal notices, e.g. an unsupported lockfile that forced a manifest fallback. */
76
+ warnings?: string[];
77
+ }
78
+ declare const SEVERITY_ORDER: Record<Severity, number>;
79
+
80
+ /**
81
+ * Runs every detector over every package's facts and aggregates a report.
82
+ * This is deliberately pure: given the same facts and detectors it produces
83
+ * the same report, which is what makes CI runs deterministic and testable.
84
+ */
85
+ declare function runDetectors(facts: PackageFacts[], detectors: Detector[], context: {
86
+ target: string;
87
+ ecosystem: string;
88
+ unverified: string[];
89
+ generatedAt: string;
90
+ }): Report;
91
+
92
+ /** All built-in detectors. New detectors are registered here. */
93
+ declare const builtinDetectors: Detector[];
94
+
95
+ /** Renders a report as plain text. No color codes yet; kept dependency-free. */
96
+ declare function renderTerminal(report: Report): string;
97
+
98
+ /** Bumped when the JSON shape changes in a way consumers must adapt to. */
99
+ declare const JSON_SCHEMA_VERSION = 1;
100
+ /** Renders a report as stable, machine-readable JSON for CI and tooling. */
101
+ declare function renderJson(report: Report, toolVersion: string): string;
102
+
103
+ /**
104
+ * Renders a report as SARIF 2.1.0 for GitHub code scanning. Findings become
105
+ * results anchored to the manifest or lockfile; every built-in detector is
106
+ * advertised as a rule so the tool's coverage is discoverable.
107
+ */
108
+ declare function renderSarif(report: Report, toolVersion: string, detectors?: Detector[]): string;
109
+
110
+ /**
111
+ * The process exit code for a report. No findings is always 0. With no
112
+ * threshold any finding is 1; with a threshold only a finding at or above that
113
+ * severity is 1, so CI can gate on high-severity results alone.
114
+ */
115
+ declare function resolveExitCode(report: Report, failOn: Severity | undefined): number;
116
+
117
+ /**
118
+ * Classifies a version specifier into how we can verify it. A registry range
119
+ * is checkable against the registry; git/file/link/alias/workspace specifiers
120
+ * are not resolvable the same way and must be reported honestly, not assumed
121
+ * safe.
122
+ */
123
+ declare function classifySource(spec: string): DependencySource;
124
+ /**
125
+ * Reads declared dependencies from a package.json. This is manifest-level only
126
+ * (what was asked for), not the resolved lockfile graph; the lockfile collector
127
+ * (Phase 1) supplies resolved versions and the full transitive tree.
128
+ */
129
+ declare function readManifestFacts(dir: string): Promise<PackageFacts[]>;
130
+
131
+ /** Names of non-npm lockfiles present in `dir`, so the caller can warn instead of silently skipping. */
132
+ declare function detectOtherLockfiles(dir: string): Promise<string[]>;
133
+ type LockfileOutcome = {
134
+ status: "ok";
135
+ facts: PackageFacts[];
136
+ lockfileVersion: number;
137
+ } | {
138
+ status: "unsupported";
139
+ reason: string;
140
+ } | {
141
+ status: "absent";
142
+ };
143
+ /** Extracts the package name from a lockfile path key, handling nesting and scopes. */
144
+ declare function nameFromLockPath(lockPath: string): string | undefined;
145
+ /**
146
+ * Reads resolved dependency facts from a package-lock.json (v2 or v3). Returns
147
+ * `absent` when there is no lockfile and `unsupported` for a shape we do not
148
+ * parse (v1, or a missing `packages` map), so the caller can fall back to the
149
+ * manifest and say so rather than silently skipping.
150
+ */
151
+ declare function readLockfile(dir: string): Promise<LockfileOutcome>;
152
+
153
+ /**
154
+ * npm registry client. This is a collector: it does IO and returns facts. It
155
+ * never judges. Every path degrades honestly, a lookup that could not be
156
+ * completed returns status "unverified", never a value that reads as "safe".
157
+ */
158
+ /** Parsed, flattened facts from a registry packument. */
159
+ interface Packument {
160
+ name: string;
161
+ firstPublishAt?: string;
162
+ latestPublishAt?: string;
163
+ latestVersion?: string;
164
+ versionCount: number;
165
+ /** Whether the resolved version is still published, when a version is asked. */
166
+ requestedVersionPublished?: boolean;
167
+ hasInstallScript: boolean;
168
+ repositoryUrl?: string;
169
+ }
170
+ type RegistryLookup = {
171
+ status: "found";
172
+ packument: Packument;
173
+ } | {
174
+ status: "not-found";
175
+ } | {
176
+ status: "unverified";
177
+ reason: string;
178
+ };
179
+ interface RegistryClientOptions {
180
+ /** Injected for tests; defaults to global fetch. */
181
+ fetchImpl?: typeof fetch;
182
+ registryUrl?: string;
183
+ offline?: boolean;
184
+ timeoutMs?: number;
185
+ }
186
+ interface RegistryClient {
187
+ getPackument(name: string, version?: string): Promise<RegistryLookup>;
188
+ }
189
+ /** Scoped names keep the leading @ but the internal slash must be encoded. */
190
+ declare function encodePackageName(name: string): string;
191
+ declare function createRegistryClient(options?: RegistryClientOptions): RegistryClient;
192
+
193
+ /**
194
+ * npm downloads API client. A collector: returns the last-week download count
195
+ * for a package, or `undefined` when the count cannot be established (offline,
196
+ * error, or an unsupported name such as a scoped package, which the point API
197
+ * does not serve). `undefined` means "unknown", never "zero" or "safe".
198
+ */
199
+ interface DownloadsClientOptions {
200
+ fetchImpl?: typeof fetch;
201
+ apiUrl?: string;
202
+ offline?: boolean;
203
+ timeoutMs?: number;
204
+ }
205
+ interface DownloadsClient {
206
+ getWeeklyDownloads(name: string): Promise<number | undefined>;
207
+ }
208
+ declare function createDownloadsClient(options?: DownloadsClientOptions): DownloadsClient;
209
+
210
+ interface EnrichmentResult {
211
+ facts: PackageFacts[];
212
+ /** Names whose registry facts could not be established (offline, error). */
213
+ unverified: string[];
214
+ }
215
+ interface EnrichOptions {
216
+ concurrency?: number;
217
+ downloads?: DownloadsClient;
218
+ /** Injected for a deterministic age computation in tests. */
219
+ now?: () => Date;
220
+ }
221
+ /**
222
+ * Enriches manifest facts with registry and downloads facts. A collector: it
223
+ * performs the IO and folds the results into `PackageFacts` for the pure
224
+ * detectors to judge. Only registry-sourced dependencies are looked up; git,
225
+ * file, link, alias, and workspace specifiers have no registry record and are
226
+ * left as they are.
227
+ */
228
+ declare function enrichWithRegistry(input: PackageFacts[], client: RegistryClient, options?: EnrichOptions): Promise<EnrichmentResult>;
229
+
230
+ interface PackageSpec {
231
+ name: string;
232
+ version?: string;
233
+ }
234
+ /**
235
+ * Parses a `check` argument like `express`, `express@4.18.2`, `@scope/pkg`, or
236
+ * `@scope/pkg@1.0.0`. The leading @ of a scope is not a version separator, so
237
+ * the split looks for an @ after the first character only.
238
+ */
239
+ declare function parsePackageSpec(input: string): PackageSpec;
240
+
241
+ interface ScanOptions {
242
+ offline?: boolean;
243
+ /** Injected in tests so the suite never touches the network. */
244
+ client?: RegistryClient;
245
+ downloads?: DownloadsClient;
246
+ detectors?: Detector[];
247
+ /** Injected for a deterministic `generatedAt` and age in tests. */
248
+ now?: () => Date;
249
+ }
250
+ /**
251
+ * Scans a project. Prefers the resolved package-lock.json tree (v2/v3); falls
252
+ * back to the manifest's declared dependencies when the lockfile is absent or
253
+ * an unsupported shape, recording a warning so the fallback is never silent.
254
+ */
255
+ declare function scanProject(dir: string, options?: ScanOptions): Promise<Report>;
256
+ /** Vets a single package spec (`name`, `name@version`, `@scope/name@version`). */
257
+ declare function checkPackage(specInput: string, options?: ScanOptions): Promise<Report>;
258
+
259
+ /** Public library API. The CLI is a thin consumer of these exports. */
260
+
261
+ declare const VERSION: string;
262
+
263
+ export { type Confidence, type DependencyKind, type DependencySource, type Detector, type DownloadsClient, type DownloadsClientOptions, type EnrichOptions, type EnrichmentResult, type Finding, JSON_SCHEMA_VERSION, type LockfileOutcome, type PackageFacts, type PackageSpec, type Packument, type RegistryClient, type RegistryClientOptions, type RegistryLookup, type Report, SEVERITY_ORDER, type ScanOptions, type ScanVerdict, type Severity, VERSION, builtinDetectors, checkPackage, classifySource, createDownloadsClient, createRegistryClient, detectOtherLockfiles, encodePackageName, enrichWithRegistry, nameFromLockPath, parsePackageSpec, readLockfile, readManifestFacts, renderJson, renderSarif, renderTerminal, resolveExitCode, runDetectors, scanProject };
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import {
2
+ JSON_SCHEMA_VERSION,
3
+ SEVERITY_ORDER,
4
+ VERSION,
5
+ builtinDetectors,
6
+ checkPackage,
7
+ classifySource,
8
+ createDownloadsClient,
9
+ createRegistryClient,
10
+ detectOtherLockfiles,
11
+ encodePackageName,
12
+ enrichWithRegistry,
13
+ nameFromLockPath,
14
+ parsePackageSpec,
15
+ readLockfile,
16
+ readManifestFacts,
17
+ renderJson,
18
+ renderSarif,
19
+ renderTerminal,
20
+ resolveExitCode,
21
+ runDetectors,
22
+ scanProject
23
+ } from "./chunk-XXICU3EZ.js";
24
+ export {
25
+ JSON_SCHEMA_VERSION,
26
+ SEVERITY_ORDER,
27
+ VERSION,
28
+ builtinDetectors,
29
+ checkPackage,
30
+ classifySource,
31
+ createDownloadsClient,
32
+ createRegistryClient,
33
+ detectOtherLockfiles,
34
+ encodePackageName,
35
+ enrichWithRegistry,
36
+ nameFromLockPath,
37
+ parsePackageSpec,
38
+ readLockfile,
39
+ readManifestFacts,
40
+ renderJson,
41
+ renderSarif,
42
+ renderTerminal,
43
+ resolveExitCode,
44
+ runDetectors,
45
+ scanProject
46
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "vetguard",
3
+ "version": "0.0.0",
4
+ "description": "Local-first scanner for AI-era npm supply-chain threats: hallucinated (slopsquatted) dependencies, typosquats, malicious young packages, and prompt injection aimed at coding agents.",
5
+ "keywords": [
6
+ "npm",
7
+ "security",
8
+ "supply-chain",
9
+ "slopsquatting",
10
+ "typosquatting",
11
+ "vulnerability",
12
+ "sca",
13
+ "cli"
14
+ ],
15
+ "license": "Apache-2.0",
16
+ "homepage": "https://github.com/tallyguard/vetguard#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/tallyguard/vetguard.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/tallyguard/vetguard/issues"
23
+ },
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "bin": {
29
+ "vetguard": "./dist/cli.js"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "dev": "tsx src/cli.ts",
47
+ "build": "tsup",
48
+ "typecheck": "tsc --noEmit",
49
+ "lint": "eslint .",
50
+ "format": "prettier --write .",
51
+ "format:check": "prettier --check .",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "scan": "tsx src/cli.ts scan",
55
+ "check": "npm run build && node dist/cli.js",
56
+ "refresh:popular": "node scripts/refresh-popular.mjs",
57
+ "prepack": "npm run build"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/js": "^9.17.0",
61
+ "@types/node": "^22.10.0",
62
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
63
+ "@typescript-eslint/parser": "^8.18.0",
64
+ "eslint": "^9.17.0",
65
+ "globals": "^17.7.0",
66
+ "prettier": "^3.4.2",
67
+ "tsup": "^8.3.5",
68
+ "tsx": "^4.19.2",
69
+ "typescript": "^5.7.2",
70
+ "vitest": "^2.1.8"
71
+ }
72
+ }