@nexus_js/audit 0.6.0 → 0.7.1

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/src/engine.ts DELETED
@@ -1,322 +0,0 @@
1
- /**
2
- * Nexus Audit Engine — CVE Database via OSV (Open Source Vulnerabilities).
3
- *
4
- * Uses the free, open Google OSV API (https://osv.dev) — no API key required.
5
- * Responses are cached locally in ~/.nexus/cache/ for offline operation.
6
- *
7
- * API reference: https://google.github.io/osv.dev/api/
8
- *
9
- * Cache strategy:
10
- * - Queries are cached per package@version for 24h (TTL configurable)
11
- * - On offline: stale cache is used transparently (no error)
12
- * - Cache location: ~/.nexus/cache/osv/{pkg-name}.json
13
- *
14
- * OSV severity mapping to Nexus levels:
15
- * CRITICAL → critical (CVSS ≥ 9.0 or explicit CRITICAL)
16
- * HIGH → high (CVSS 7.0–8.9)
17
- * MEDIUM → medium (CVSS 4.0–6.9)
18
- * LOW → low (CVSS < 4.0)
19
- */
20
-
21
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
22
- import { join } from 'node:path';
23
- import { homedir } from 'node:os';
24
-
25
- const OSV_API = 'https://api.osv.dev/v1/query';
26
- const CACHE_DIR = join(homedir(), '.nexus', 'cache', 'osv');
27
- const CACHE_TTL_MS = 24 * 60 * 60 * 1_000; // 24h
28
-
29
- // ── Types ─────────────────────────────────────────────────────────────────────
30
-
31
- export type VulnSeverity = 'critical' | 'high' | 'medium' | 'low' | 'unknown';
32
-
33
- export interface Vulnerability {
34
- id: string; // e.g. 'GHSA-29mw-wpgm-hmr9' or 'CVE-2024-1234'
35
- summary: string;
36
- severity: VulnSeverity;
37
- cvss?: number | undefined; // CVSS v3 base score
38
- aliases: string[]; // CVE IDs and other aliases
39
- affectedVersions: string; // human-readable range
40
- fixedIn?: string | undefined; // first patched version
41
- references: string[]; // advisory URLs
42
- published: string; // ISO date
43
- }
44
-
45
- export interface AuditResult {
46
- package: string;
47
- version: string;
48
- status: 'safe' | 'vulnerable' | 'unknown';
49
- vulns: Vulnerability[];
50
- /** True if result came from local cache */
51
- cached: boolean;
52
- /** True if result is stale (TTL expired) but used because offline */
53
- stale: boolean;
54
- checkedAt: number;
55
- }
56
-
57
- interface CacheEntry {
58
- result: AuditResult;
59
- expiresAt: number;
60
- version: string;
61
- }
62
-
63
- interface OsvResponse {
64
- vulns?: OsvVuln[];
65
- }
66
-
67
- interface OsvVuln {
68
- id: string;
69
- summary?: string;
70
- aliases?: string[];
71
- published: string;
72
- references?: Array<{ url: string }>;
73
- severity?: Array<{ type: string; score: string }>;
74
- affected?: Array<{
75
- package?: { name: string; ecosystem: string };
76
- ranges?: Array<{ type: string; events: Array<{ introduced?: string; fixed?: string }> }>;
77
- versions?: string[];
78
- }>;
79
- }
80
-
81
- // ── Cache helpers ─────────────────────────────────────────────────────────────
82
-
83
- async function ensureCacheDir(): Promise<void> {
84
- try {
85
- await mkdir(CACHE_DIR, { recursive: true });
86
- } catch { /* already exists */ }
87
- }
88
-
89
- function cacheKey(pkg: string): string {
90
- return pkg.replace(/\//g, '__').replace(/@/g, '_at_');
91
- }
92
-
93
- async function readCache(pkg: string): Promise<CacheEntry | null> {
94
- try {
95
- const raw = await readFile(join(CACHE_DIR, `${cacheKey(pkg)}.json`), 'utf-8');
96
- return JSON.parse(raw) as CacheEntry;
97
- } catch { return null; }
98
- }
99
-
100
- async function writeCache(pkg: string, entry: CacheEntry): Promise<void> {
101
- await ensureCacheDir();
102
- try {
103
- await writeFile(join(CACHE_DIR, `${cacheKey(pkg)}.json`), JSON.stringify(entry, null, 2));
104
- } catch { /* cache write failure is non-fatal */ }
105
- }
106
-
107
- // ── OSV API ───────────────────────────────────────────────────────────────────
108
-
109
- function parseSeverity(vuln: OsvVuln): { severity: VulnSeverity; cvss?: number } {
110
- const severityBlock = vuln.severity ?? [];
111
-
112
- // Look for CVSS v3 score first
113
- for (const s of severityBlock) {
114
- if (s.type === 'CVSS_V3' || s.type === 'CVSS_V4') {
115
- // Extract numeric score from vector string (e.g. "CVSS:3.1/AV:N/AC:L/...")
116
- // OSV also sometimes provides the numeric score directly
117
- const scoreMatch = s.score.match(/^(\d+\.\d+)$/) ??
118
- s.score.match(/\/(\d+\.\d+)$/);
119
- const score = scoreMatch ? parseFloat(scoreMatch[1] ?? '0') : 0;
120
- let sev: VulnSeverity = 'unknown';
121
- if (score >= 9.0) sev = 'critical';
122
- else if (score >= 7.0) sev = 'high';
123
- else if (score >= 4.0) sev = 'medium';
124
- else if (score > 0) sev = 'low';
125
- return { severity: sev, cvss: score };
126
- }
127
- }
128
-
129
- // Fall back to explicit severity labels in the ID
130
- const id = vuln.id.toUpperCase();
131
- if (id.includes('CRITICAL')) return { severity: 'critical' };
132
- if (id.includes('HIGH')) return { severity: 'high' };
133
- if (id.includes('MEDIUM')) return { severity: 'medium' };
134
- if (id.includes('LOW')) return { severity: 'low' };
135
- return { severity: 'unknown' };
136
- }
137
-
138
- function parseFixedVersion(vuln: OsvVuln): string | undefined {
139
- for (const affected of vuln.affected ?? []) {
140
- for (const range of affected.ranges ?? []) {
141
- for (const event of range.events ?? []) {
142
- if (event.fixed) return event.fixed;
143
- }
144
- }
145
- }
146
- return undefined;
147
- }
148
-
149
- function parseAffectedVersions(vuln: OsvVuln): string {
150
- const parts: string[] = [];
151
- for (const affected of vuln.affected ?? []) {
152
- for (const range of affected.ranges ?? []) {
153
- let intro: string | undefined;
154
- let fixed: string | undefined;
155
- for (const event of range.events ?? []) {
156
- if (event.introduced) intro = event.introduced;
157
- if (event.fixed) fixed = event.fixed;
158
- }
159
- if (intro && fixed) parts.push(`>=${intro} <${fixed}`);
160
- else if (intro) parts.push(`>=${intro}`);
161
- else if (fixed) parts.push(`<${fixed}`);
162
- }
163
- // Direct version list
164
- if (affected.versions?.length) {
165
- parts.push(affected.versions.slice(0, 5).join(', ') + (affected.versions.length > 5 ? '...' : ''));
166
- }
167
- }
168
- return parts.join('; ') || 'all versions';
169
- }
170
-
171
- async function fetchFromOSV(pkg: string, version?: string): Promise<OsvResponse | null> {
172
- const body: Record<string, unknown> = {
173
- package: { name: pkg, ecosystem: 'npm' },
174
- };
175
- if (version) body['version'] = version;
176
-
177
- const controller = new AbortController();
178
- const timeout = setTimeout(() => controller.abort(), 8_000);
179
-
180
- try {
181
- const res = await fetch(OSV_API, {
182
- method: 'POST',
183
- headers: { 'content-type': 'application/json' },
184
- body: JSON.stringify(body),
185
- signal: controller.signal,
186
- });
187
- if (!res.ok) return null;
188
- return await res.json() as OsvResponse;
189
- } catch {
190
- return null; // network error — caller falls back to cache
191
- } finally {
192
- clearTimeout(timeout);
193
- }
194
- }
195
-
196
- // ── Public API ────────────────────────────────────────────────────────────────
197
-
198
- /**
199
- * Audits a single package for known CVEs using the OSV database.
200
- * Results are cached locally for 24h.
201
- *
202
- * @param pkg Package name (e.g. 'lodash' or '@angular/core')
203
- * @param version Specific version to check (e.g. '4.17.20'). If omitted, checks all versions.
204
- */
205
- export async function auditPackage(pkg: string, version?: string): Promise<AuditResult> {
206
- const cached = await readCache(pkg);
207
- const now = Date.now();
208
-
209
- // Fresh cache hit
210
- if (cached && now < cached.expiresAt) {
211
- return { ...cached.result, cached: true, stale: false };
212
- }
213
-
214
- // Try to fetch from OSV
215
- const osvData = await fetchFromOSV(pkg, version);
216
-
217
- if (!osvData) {
218
- // Offline or API error — use stale cache if available
219
- if (cached) {
220
- return { ...cached.result, cached: true, stale: true };
221
- }
222
- // No cache at all
223
- return {
224
- package: pkg, version: version ?? '*',
225
- status: 'unknown', vulns: [],
226
- cached: false, stale: false,
227
- checkedAt: now,
228
- };
229
- }
230
-
231
- // Parse OSV response
232
- const vulns: Vulnerability[] = (osvData.vulns ?? []).map((v) => {
233
- const { severity, cvss } = parseSeverity(v);
234
- return {
235
- id: v.id,
236
- summary: v.summary ?? 'No summary available',
237
- severity,
238
- cvss,
239
- aliases: v.aliases ?? [],
240
- affectedVersions: parseAffectedVersions(v),
241
- fixedIn: parseFixedVersion(v),
242
- references: (v.references ?? []).map((r) => r.url),
243
- published: v.published,
244
- };
245
- });
246
-
247
- // Sort by severity
248
- const SORDER: Record<VulnSeverity, number> = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
249
- vulns.sort((a, b) => SORDER[a.severity] - SORDER[b.severity]);
250
-
251
- const result: AuditResult = {
252
- package: pkg,
253
- version: version ?? '*',
254
- status: vulns.length > 0 ? 'vulnerable' : 'safe',
255
- vulns,
256
- cached: false,
257
- stale: false,
258
- checkedAt: now,
259
- };
260
-
261
- await writeCache(pkg, { result, expiresAt: now + CACHE_TTL_MS, version: version ?? '*' });
262
- return result;
263
- }
264
-
265
- /**
266
- * Audits all dependencies in a package.json.
267
- * Runs queries in parallel (max 6 concurrent) to avoid rate limiting.
268
- */
269
- export async function auditDependencies(
270
- deps: Record<string, string>,
271
- ): Promise<Map<string, AuditResult>> {
272
- const results = new Map<string, AuditResult>();
273
- const entries = Object.entries(deps);
274
- const BATCH = 6;
275
-
276
- for (let i = 0; i < entries.length; i += BATCH) {
277
- const batch = entries.slice(i, i + BATCH);
278
- const settled = await Promise.allSettled(
279
- batch.map(async ([name, versionRange]) => {
280
- // Strip semver range prefix for OSV query
281
- const version = versionRange.replace(/^[\^~>=<]/, '').split(' ')[0];
282
- const result = await auditPackage(name, version);
283
- return [name, result] as [string, AuditResult];
284
- }),
285
- );
286
- for (const s of settled) {
287
- if (s.status === 'fulfilled') {
288
- results.set(s.value[0], s.value[1]);
289
- }
290
- }
291
- }
292
-
293
- return results;
294
- }
295
-
296
- /** Returns only vulnerable packages, sorted by severity. */
297
- export function filterVulnerable(results: Map<string, AuditResult>): AuditResult[] {
298
- const SORDER: Record<VulnSeverity, number> = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
299
- return [...results.values()]
300
- .filter((r) => r.status === 'vulnerable')
301
- .sort((a, b) => {
302
- const as = a.vulns[0]?.severity ?? 'unknown';
303
- const bs = b.vulns[0]?.severity ?? 'unknown';
304
- return SORDER[as] - SORDER[bs];
305
- });
306
- }
307
-
308
- /** Invalidates the local cache for a specific package (forces re-fetch). */
309
- export async function invalidateCache(pkg: string): Promise<void> {
310
- const { unlink } = await import('node:fs/promises');
311
- try {
312
- await unlink(join(CACHE_DIR, `${cacheKey(pkg)}.json`));
313
- } catch { /* file may not exist */ }
314
- }
315
-
316
- /** Clears the entire OSV cache (all packages). */
317
- export async function clearCache(): Promise<void> {
318
- const { rm } = await import('node:fs/promises');
319
- try {
320
- await rm(CACHE_DIR, { recursive: true, force: true });
321
- } catch { /* ignore */ }
322
- }
package/src/override.ts DELETED
@@ -1,168 +0,0 @@
1
- /**
2
- * Nexus Security Override Policy — "Ghost Wall" exceptions.
3
- *
4
- * Sometimes a library is vulnerable but:
5
- * - The patch isn't yet on a stable release
6
- * - You only use the affected function in a build-time context (never in production)
7
- * - Your organization has an accepted risk decision documented elsewhere
8
- *
9
- * Overrides are explicit, time-limited exceptions that:
10
- * 1. Require a documented reason
11
- * 2. Expire automatically — the build fails again after the date
12
- * 3. Are logged in every audit report
13
- * 4. Do NOT suppress warnings (only prevent build failure)
14
- *
15
- * Usage in nexus.config.ts:
16
- *
17
- * ```ts
18
- * import { defineNexusConfig } from '@nexus_js/core';
19
- *
20
- * export default defineNexusConfig({
21
- * security: {
22
- * hardened: true,
23
- * allowVulnerable: {
24
- * 'pdfkit': {
25
- * cve: 'CVE-2024-29415',
26
- * reason: 'Used in build-time PDF generation only. Not in the client bundle. ' +
27
- * 'Patched version releases 2026-05-15 according to maintainer.',
28
- * expires: '2026-06-01',
29
- * },
30
- * },
31
- * },
32
- * });
33
- * ```
34
- *
35
- * After `expires`, the build fails again, forcing re-evaluation.
36
- * If the patch is available, update the dependency. If not, extend the override with a new reason.
37
- */
38
-
39
- export interface VulnerabilityOverride {
40
- /** CVE ID or OSV ID being overridden */
41
- cve: string;
42
- /** REQUIRED: human-readable business justification */
43
- reason: string;
44
- /**
45
- * ISO date string (YYYY-MM-DD). After this date, the override expires
46
- * and the build will fail again. Maximum: 180 days from today.
47
- */
48
- expires: string;
49
- }
50
-
51
- export type AllowVulnerableConfig = Record<string, VulnerabilityOverride>;
52
-
53
- export interface OverrideValidation {
54
- valid: boolean;
55
- expired: boolean;
56
- daysLeft: number;
57
- message: string;
58
- }
59
-
60
- /**
61
- * Validates an override at build time.
62
- * Returns { valid: false } if expired or malformed.
63
- */
64
- export function validateOverride(
65
- pkg: string,
66
- override: VulnerabilityOverride,
67
- ): OverrideValidation {
68
- const expiry = new Date(override.expires);
69
-
70
- if (isNaN(expiry.getTime())) {
71
- return {
72
- valid: false,
73
- expired: false,
74
- daysLeft: 0,
75
- message: `[Nexus Security] Override for "${pkg}" has an invalid expiry date: "${override.expires}". Use YYYY-MM-DD format.`,
76
- };
77
- }
78
-
79
- const now = new Date();
80
- const daysLeft = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
81
- const expired = daysLeft <= 0;
82
-
83
- if (expired) {
84
- return {
85
- valid: false,
86
- expired: true,
87
- daysLeft: 0,
88
- message:
89
- `[Nexus Security] ⛔ Override for "${pkg}" (${override.cve}) EXPIRED on ${override.expires}.\n` +
90
- ` Original reason: "${override.reason}"\n` +
91
- ` The vulnerability must now be addressed. Options:\n` +
92
- ` 1. Update "${pkg}" to a patched version\n` +
93
- ` 2. Replace "${pkg}" with a safe alternative\n` +
94
- ` 3. Extend the override with a new expiry date (requires fresh justification)`,
95
- };
96
- }
97
-
98
- // Warn when approaching expiry
99
- const warningDays = 14;
100
- const warning = daysLeft <= warningDays
101
- ? `\n ⚠️ Override for "${pkg}" expires in ${daysLeft} day${daysLeft === 1 ? '' : 's'} (${override.expires}).`
102
- : '';
103
-
104
- return {
105
- valid: true,
106
- expired: false,
107
- daysLeft,
108
- message: `[Nexus Security] ⚠️ OVERRIDE ACTIVE for "${pkg}" (${override.cve}).${warning}\n Reason: "${override.reason}"`,
109
- };
110
- }
111
-
112
- /**
113
- * Checks if a package+CVE combination is covered by an active override.
114
- * Returns null if no override applies (build should fail on critical CVE).
115
- */
116
- export function findOverride(
117
- pkg: string,
118
- cveId: string,
119
- overrides: AllowVulnerableConfig,
120
- ): OverrideValidation | null {
121
- const override = overrides[pkg];
122
- if (!override) return null;
123
-
124
- // Check if the CVE matches (direct or alias)
125
- const configCve = override.cve.toUpperCase().trim();
126
- const queryCve = cveId.toUpperCase().trim();
127
-
128
- if (configCve !== queryCve && !queryCve.includes(configCve) && !configCve.includes(queryCve)) {
129
- return null;
130
- }
131
-
132
- return validateOverride(pkg, override);
133
- }
134
-
135
- /**
136
- * Formats all active overrides for display in audit output.
137
- * Groups by status: active, expiring soon, expired.
138
- */
139
- export function formatOverrides(
140
- overrides: AllowVulnerableConfig,
141
- ): { active: string[]; expiringSoon: string[]; expired: string[] } {
142
- const active: string[] = [];
143
- const expiringSoon: string[] = [];
144
- const expired: string[] = [];
145
-
146
- for (const [pkg, override] of Object.entries(overrides)) {
147
- const v = validateOverride(pkg, override);
148
- if (v.expired) {
149
- expired.push(`${pkg} (${override.cve}) — expired ${override.expires}`);
150
- } else if (v.daysLeft <= 14) {
151
- expiringSoon.push(`${pkg} (${override.cve}) — expires in ${v.daysLeft} days`);
152
- } else {
153
- active.push(`${pkg} (${override.cve}) — ${v.daysLeft} days left`);
154
- }
155
- }
156
-
157
- return { active, expiringSoon, expired };
158
- }
159
-
160
- /**
161
- * Returns the maximum safe override duration (180 days from now).
162
- * Overrides beyond this are rejected to prevent permanent exceptions.
163
- */
164
- export function maxOverrideDate(): string {
165
- const d = new Date();
166
- d.setDate(d.getDate() + 180);
167
- return d.toISOString().split('T')[0] as string;
168
- }
package/src/scanner.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * Nexus Package Scanner — reads package.json and coordinates audits.
3
- *
4
- * Combines:
5
- * - CVE scanning via OSV (engine.ts)
6
- * - Supply chain risk via npm registry (supply-chain.ts)
7
- * - Override policy validation (override.ts)
8
- *
9
- * Entry point for both the CLI and the Vite plugin.
10
- */
11
-
12
- import { readFile } from 'node:fs/promises';
13
- import { join } from 'node:path';
14
-
15
- import { auditDependencies, filterVulnerable, type AuditResult, type VulnSeverity } from './engine.js';
16
- import { auditSupplyChain, type SupplyChainResult } from './supply-chain.js';
17
- import { findOverride, validateOverride, formatOverrides, type AllowVulnerableConfig } from './override.js';
18
-
19
- export type { AuditResult, VulnSeverity, SupplyChainResult, AllowVulnerableConfig };
20
- export { filterVulnerable };
21
-
22
- // ── Types ─────────────────────────────────────────────────────────────────────
23
-
24
- export interface ScanOptions {
25
- root: string;
26
- /** Include devDependencies in the scan (default: false) */
27
- includeDev?: boolean;
28
- /** Supply chain guard (default: true) */
29
- supplyChain?: boolean;
30
- /** Override exceptions from nexus.config.ts security.allowVulnerable */
31
- allowVulnerable?: AllowVulnerableConfig;
32
- /**
33
- * If true, critical CVEs that are not overridden will throw (for build-time blocking).
34
- * If false, they are reported but don't throw.
35
- */
36
- failOnCritical?: boolean;
37
- /** Minimum severity to report (default: 'low') */
38
- minSeverity?: VulnSeverity;
39
- }
40
-
41
- export interface ScanResult {
42
- scannedPackages: number;
43
- vulnerable: AuditResult[];
44
- supplyChain: Map<string, SupplyChainResult>;
45
- overrideStatus: ReturnType<typeof formatOverrides>;
46
- blockedPackages: string[]; // packages that would block a build
47
- durationMs: number;
48
- }
49
-
50
- // ── Main scanner ──────────────────────────────────────────────────────────────
51
-
52
- /**
53
- * Full dependency scan: CVE + supply chain + override validation.
54
- * This is the core function called by both `nexus audit` and the Vite plugin.
55
- */
56
- export async function scanProject(opts: ScanOptions): Promise<ScanResult> {
57
- const t0 = Date.now();
58
-
59
- // Read package.json
60
- const pkgPath = join(opts.root, 'package.json');
61
- let pkgJson: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
62
- try {
63
- pkgJson = JSON.parse(await readFile(pkgPath, 'utf-8')) as typeof pkgJson;
64
- } catch {
65
- throw new Error(`[Nexus Audit] Cannot read package.json at ${pkgPath}`);
66
- }
67
-
68
- // Collect deps
69
- const deps: Record<string, string> = {
70
- ...pkgJson.dependencies,
71
- ...(opts.includeDev ? pkgJson.devDependencies : {}),
72
- };
73
-
74
- const scannedPackages = Object.keys(deps).length;
75
-
76
- // Parallel: CVE scan + supply chain scan
77
- const [cveResults, scResults] = await Promise.all([
78
- auditDependencies(deps),
79
- opts.supplyChain !== false ? auditSupplyChain(deps) : Promise.resolve(new Map<string, SupplyChainResult>()),
80
- ]);
81
-
82
- const vulnerable = filterVulnerable(cveResults);
83
-
84
- // Override policy validation
85
- const overrides = opts.allowVulnerable ?? {};
86
- const overrideStats = formatOverrides(overrides);
87
- const blockedPackages: string[] = [];
88
-
89
- const SORDER: Record<VulnSeverity, number> = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
90
- const minSev = opts.minSeverity ?? 'low';
91
-
92
- for (const result of vulnerable) {
93
- for (const vuln of result.vulns) {
94
- if (SORDER[vuln.severity] > SORDER[minSev]) continue;
95
-
96
- if (vuln.severity !== 'critical' && vuln.severity !== 'high') continue;
97
-
98
- // Check if there's a valid override
99
- const overrideResult = findOverride(result.package, vuln.id, overrides)
100
- ?? findOverride(result.package, vuln.aliases[0] ?? '', overrides);
101
-
102
- if (overrideResult?.valid) {
103
- // Override is active — report but don't block
104
- continue;
105
- }
106
-
107
- if (!overrideResult || overrideResult.expired) {
108
- // No override or expired → block
109
- if (!blockedPackages.includes(result.package)) {
110
- blockedPackages.push(result.package);
111
- }
112
- }
113
- }
114
- }
115
-
116
- const scanResult: ScanResult = {
117
- scannedPackages,
118
- vulnerable,
119
- supplyChain: scResults,
120
- overrideStatus: overrideStats,
121
- blockedPackages,
122
- durationMs: Date.now() - t0,
123
- };
124
-
125
- // Throw for build-time blocking
126
- if (opts.failOnCritical && blockedPackages.length > 0) {
127
- const details = blockedPackages
128
- .map((pkg) => {
129
- const r = cveResults.get(pkg);
130
- const topVuln = r?.vulns[0];
131
- return topVuln
132
- ? ` "${pkg}" — ${topVuln.id} (${topVuln.severity.toUpperCase()}): ${topVuln.summary}`
133
- : ` "${pkg}"`;
134
- })
135
- .join('\n');
136
-
137
- throw new NexusSecurityError(
138
- `[Nexus Security] 🛑 BUILD BLOCKED — ${blockedPackages.length} critical/high CVE${blockedPackages.length > 1 ? 's' : ''} detected:\n\n` +
139
- `${details}\n\n` +
140
- `Options:\n` +
141
- ` 1. Run \`nexus fix\` to automatically update to patched versions\n` +
142
- ` 2. Add a time-limited override in nexus.config.ts (security.allowVulnerable)\n` +
143
- ` 3. Run \`nexus audit\` for full details and suggested fixes`,
144
- blockedPackages,
145
- vulnerable,
146
- );
147
- }
148
-
149
- return scanResult;
150
- }
151
-
152
- // ── Error class ───────────────────────────────────────────────────────────────
153
-
154
- export class NexusSecurityError extends Error {
155
- constructor(
156
- message: string,
157
- public readonly blockedPackages: string[],
158
- public readonly vulnerable: AuditResult[],
159
- ) {
160
- super(message);
161
- this.name = 'NexusSecurityError';
162
- }
163
- }
164
-
165
- // ── Single-package audit (for Vite plugin import hook) ───────────────────────
166
-
167
- export { auditPackage } from './engine.js';
168
- export { checkSupplyChain } from './supply-chain.js';
169
- export { validateOverride, findOverride } from './override.js';