shieldcortex 4.0.2 → 4.2.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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Dependency Scanner
3
+ *
4
+ * Scans node_modules for:
5
+ * 1. Known malicious packages (CRITICAL)
6
+ * 2. Typosquat packages — Levenshtein distance 1-2 from popular packages (HIGH)
7
+ * 3. Suspicious postinstall scripts — network, exec, credential access patterns (HIGH/MEDIUM)
8
+ * 4. New packages with postinstall scripts published < 7 days ago (MEDIUM)
9
+ *
10
+ * Inspired by the axios 1.14.1 supply chain attack and the Claude Code
11
+ * typosquatting attacks discovered 31 Mar 2026.
12
+ *
13
+ * Usage:
14
+ * shieldcortex audit --deps # scan ./node_modules
15
+ * shieldcortex audit --deps-global # scan global npm prefix
16
+ * shieldcortex audit --deps-path /some/path # scan specific path
17
+ * shieldcortex audit --deps --quarantine # scan + quarantine CRITICAL/HIGH
18
+ * shieldcortex audit --deps --clean --force # scan + permanently delete CRITICAL
19
+ * shieldcortex audit --deps --auto-protect # scan + auto-quarantine CRITICAL
20
+ */
21
+ import type { AuditFinding, ScannerResult } from './types.js';
22
+ /**
23
+ * Compute Levenshtein distance between two strings.
24
+ * Standard dynamic-programming implementation, no external deps.
25
+ */
26
+ export declare function levenshtein(a: string, b: string): number;
27
+ export interface QuarantineManifest {
28
+ packageName: string;
29
+ version: string;
30
+ reason: string;
31
+ originalPath: string;
32
+ quarantinedAt: string;
33
+ severity: string;
34
+ }
35
+ /**
36
+ * Quarantine a package by moving it from node_modules to
37
+ * ~/.shieldcortex/quarantine/deps/<pkg>-<timestamp>/.
38
+ *
39
+ * Creates a manifest.json alongside the quarantined files.
40
+ * Only acts on CRITICAL and HIGH findings.
41
+ *
42
+ * @returns The quarantine destination path, or null if not quarantined.
43
+ */
44
+ export declare function quarantinePackage(finding: AuditFinding, nodeModulesPath: string): string | null;
45
+ /**
46
+ * Permanently delete a malicious package from node_modules.
47
+ *
48
+ * Only acts on CRITICAL findings (known malicious packages from blocklist).
49
+ *
50
+ * @returns The package name that was deleted, or null if not deleted.
51
+ */
52
+ export declare function cleanPackage(finding: AuditFinding, nodeModulesPath: string): string | null;
53
+ export interface DependencyScanOptions {
54
+ /** Absolute path to node_modules directory */
55
+ nodeModulesPath: string;
56
+ }
57
+ export declare function scanDependencies(opts: DependencyScanOptions): ScannerResult;
58
+ /**
59
+ * Resolve the node_modules path for a given mode.
60
+ */
61
+ export declare function resolveNodeModulesPath(mode: 'local' | 'global' | 'path', customPath?: string): string;
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Dependency Scanner
3
+ *
4
+ * Scans node_modules for:
5
+ * 1. Known malicious packages (CRITICAL)
6
+ * 2. Typosquat packages — Levenshtein distance 1-2 from popular packages (HIGH)
7
+ * 3. Suspicious postinstall scripts — network, exec, credential access patterns (HIGH/MEDIUM)
8
+ * 4. New packages with postinstall scripts published < 7 days ago (MEDIUM)
9
+ *
10
+ * Inspired by the axios 1.14.1 supply chain attack and the Claude Code
11
+ * typosquatting attacks discovered 31 Mar 2026.
12
+ *
13
+ * Usage:
14
+ * shieldcortex audit --deps # scan ./node_modules
15
+ * shieldcortex audit --deps-global # scan global npm prefix
16
+ * shieldcortex audit --deps-path /some/path # scan specific path
17
+ * shieldcortex audit --deps --quarantine # scan + quarantine CRITICAL/HIGH
18
+ * shieldcortex audit --deps --clean --force # scan + permanently delete CRITICAL
19
+ * shieldcortex audit --deps --auto-protect # scan + auto-quarantine CRITICAL
20
+ */
21
+ import { readdirSync, readFileSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'fs';
22
+ import { join, resolve } from 'path';
23
+ import { execSync } from 'child_process';
24
+ import { homedir } from 'os';
25
+ const LEARN_MORE_MALICIOUS = 'https://shieldcortex.ai/docs/threats/supply-chain';
26
+ const LEARN_MORE_TYPOSQUAT = 'https://shieldcortex.ai/docs/threats/typosquatting';
27
+ const LEARN_MORE_POSTINSTALL = 'https://shieldcortex.ai/docs/threats/malicious-scripts';
28
+ // ── 1. Known Malicious Packages ─────────────────────────────────────────────
29
+ const KNOWN_MALICIOUS = [
30
+ // Discovered packages — add more as they are found
31
+ 'plain-crypto-js',
32
+ 'color-diff-napi',
33
+ 'modifiers-napi',
34
+ // axios supply chain imposters (March/April 2026)
35
+ 'axios-http',
36
+ 'axios-utils',
37
+ // Claude Code typosquatting (31 Mar 2026)
38
+ 'claude-code-sdk',
39
+ 'claude-code-helper',
40
+ '@anthropic/claude-code-utils',
41
+ // Common malicious patterns seen in the wild
42
+ 'event-stream-http',
43
+ 'flatmap-stream',
44
+ 'getcookies',
45
+ 'eslint-scope-backdoor',
46
+ 'eslint-config-airbnb-standard',
47
+ 'xpc-connection',
48
+ ];
49
+ // ── 2. Popular Packages for Typosquat Detection ─────────────────────────────
50
+ const POPULAR_PACKAGES = [
51
+ 'axios', 'express', 'lodash', 'react', 'next', 'vue', 'crypto-js',
52
+ 'webpack', 'babel', 'typescript', 'eslint', 'prettier', 'jest',
53
+ 'mocha', 'chalk', 'commander', 'inquirer', 'dotenv', 'cors',
54
+ 'body-parser', 'mongoose', 'sequelize', 'prisma', 'socket.io',
55
+ 'jsonwebtoken', 'bcrypt', 'uuid', 'moment', 'dayjs', 'sharp',
56
+ 'puppeteer', 'playwright', 'onnxruntime-node', 'better-sqlite3',
57
+ ];
58
+ const SUSPICIOUS_PATTERNS = [
59
+ // Downloading payloads
60
+ { regex: /\bcurl\b/i, label: 'curl (payload download)' },
61
+ { regex: /\bwget\b/i, label: 'wget (payload download)' },
62
+ { regex: /\bfetch\s*\(/i, label: 'fetch() (HTTP request)' },
63
+ { regex: /https?:\/\//i, label: 'HTTP/HTTPS URL' },
64
+ // Executing commands
65
+ { regex: /\bexec\s*\(/i, label: 'exec() call' },
66
+ { regex: /\bspawn\s*\(/i, label: 'spawn() call' },
67
+ { regex: /child_process/i, label: 'child_process import' },
68
+ // OS detection (profiling the victim)
69
+ { regex: /os\.platform\s*\(/i, label: 'os.platform() (OS detection)' },
70
+ { regex: /os\.type\s*\(/i, label: 'os.type() (OS detection)' },
71
+ { regex: /process\.platform/i, label: 'process.platform (OS detection)' },
72
+ // Self-deletion / cleanup
73
+ { regex: /rm\s+-rf/i, label: 'rm -rf (file deletion)' },
74
+ { regex: /\bdel\s+\/[sqf]/i, label: 'del /s (Windows deletion)' },
75
+ { regex: /\bunlink\s*\(/i, label: 'unlink() (file deletion)' },
76
+ // Credential access
77
+ { regex: /\bprocess\.env\b/i, label: 'process.env (env access)' },
78
+ { regex: /\$HOME\b|\bHOME\b/, label: '$HOME (home dir access)' },
79
+ { regex: /\bUSERPROFILE\b/i, label: 'USERPROFILE (Windows home)' },
80
+ { regex: /\.ssh/i, label: '.ssh (SSH key access)' },
81
+ { regex: /\.aws/i, label: '.aws (AWS credentials)' },
82
+ { regex: /\.npmrc/i, label: '.npmrc (npm token access)' },
83
+ ];
84
+ // ── Levenshtein Distance ─────────────────────────────────────────────────────
85
+ /**
86
+ * Compute Levenshtein distance between two strings.
87
+ * Standard dynamic-programming implementation, no external deps.
88
+ */
89
+ export function levenshtein(a, b) {
90
+ if (a === b)
91
+ return 0;
92
+ if (a.length === 0)
93
+ return b.length;
94
+ if (b.length === 0)
95
+ return a.length;
96
+ // Use two-row rolling array to save memory
97
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
98
+ let curr = new Array(b.length + 1);
99
+ for (let i = 1; i <= a.length; i++) {
100
+ curr[0] = i;
101
+ for (let j = 1; j <= b.length; j++) {
102
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
103
+ curr[j] = Math.min(curr[j - 1] + 1, // insertion
104
+ prev[j] + 1, // deletion
105
+ prev[j - 1] + cost);
106
+ }
107
+ // Swap rows
108
+ const _swap = prev;
109
+ prev = curr;
110
+ curr = _swap;
111
+ }
112
+ return prev[b.length];
113
+ }
114
+ function readPackageJson(pkgPath) {
115
+ const pkgJsonPath = join(pkgPath, 'package.json');
116
+ if (!existsSync(pkgJsonPath))
117
+ return null;
118
+ try {
119
+ const raw = readFileSync(pkgJsonPath, 'utf-8');
120
+ return JSON.parse(raw);
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * List all top-level package names in a node_modules directory.
128
+ * Handles scoped packages (@org/pkg).
129
+ */
130
+ function listPackages(nodeModulesPath) {
131
+ if (!existsSync(nodeModulesPath))
132
+ return [];
133
+ let entries;
134
+ try {
135
+ entries = readdirSync(nodeModulesPath);
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ const packages = [];
141
+ for (const entry of entries) {
142
+ if (entry.startsWith('.'))
143
+ continue;
144
+ if (entry.startsWith('@')) {
145
+ // Scoped namespace — list sub-entries
146
+ try {
147
+ const scopedEntries = readdirSync(join(nodeModulesPath, entry));
148
+ for (const sub of scopedEntries) {
149
+ if (!sub.startsWith('.')) {
150
+ packages.push(`${entry}/${sub}`);
151
+ }
152
+ }
153
+ }
154
+ catch { /* skip */ }
155
+ }
156
+ else {
157
+ packages.push(entry);
158
+ }
159
+ }
160
+ return packages;
161
+ }
162
+ // ── Individual Checks ────────────────────────────────────────────────────────
163
+ function checkMalicious(name) {
164
+ return KNOWN_MALICIOUS.includes(name);
165
+ }
166
+ function checkTyposquat(name) {
167
+ // Strip scope for comparison — "@org/axois" → "axois"
168
+ const bare = name.includes('/') ? name.split('/').pop() : name;
169
+ for (const popular of POPULAR_PACKAGES) {
170
+ if (name === popular || bare === popular)
171
+ continue; // exact match, not a typosquat
172
+ const dist = levenshtein(bare, popular);
173
+ if (dist >= 1 && dist <= 2) {
174
+ return popular; // typosquat of this popular package
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+ function checkSuspiciousScripts(pkg) {
180
+ const scriptKeys = ['postinstall', 'preinstall', 'install'];
181
+ const matchedLabels = [];
182
+ for (const key of scriptKeys) {
183
+ const script = pkg.scripts?.[key];
184
+ if (!script)
185
+ continue;
186
+ for (const { regex, label } of SUSPICIOUS_PATTERNS) {
187
+ if (regex.test(script) && !matchedLabels.includes(label)) {
188
+ matchedLabels.push(label);
189
+ }
190
+ }
191
+ }
192
+ return { matchCount: matchedLabels.length, labels: matchedLabels };
193
+ }
194
+ function checkPackageAge(pkg) {
195
+ const hasPostInstall = !!pkg.scripts?.postinstall ||
196
+ !!pkg.scripts?.preinstall ||
197
+ !!pkg.scripts?.install;
198
+ if (!hasPostInstall)
199
+ return false;
200
+ // Try _time field first (some lockfiles embed it), then parse _resolved for date hints
201
+ const timeStr = pkg._time ?? pkg._resolved;
202
+ if (!timeStr)
203
+ return false;
204
+ // Extract an ISO date if present
205
+ const isoMatch = timeStr.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
206
+ if (!isoMatch)
207
+ return false;
208
+ const published = new Date(isoMatch[0]);
209
+ if (isNaN(published.getTime()))
210
+ return false;
211
+ const ageMs = Date.now() - published.getTime();
212
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
213
+ return ageMs < sevenDaysMs;
214
+ }
215
+ /**
216
+ * Quarantine a package by moving it from node_modules to
217
+ * ~/.shieldcortex/quarantine/deps/<pkg>-<timestamp>/.
218
+ *
219
+ * Creates a manifest.json alongside the quarantined files.
220
+ * Only acts on CRITICAL and HIGH findings.
221
+ *
222
+ * @returns The quarantine destination path, or null if not quarantined.
223
+ */
224
+ export function quarantinePackage(finding, nodeModulesPath) {
225
+ // Only quarantine CRITICAL and HIGH
226
+ if (finding.severity !== 'critical' && finding.severity !== 'high') {
227
+ return null;
228
+ }
229
+ // Extract package name from the finding title heuristically
230
+ // Title examples:
231
+ // "Known malicious package: plain-crypto-js"
232
+ // "Possible typosquat: "axois" → "axios""
233
+ // "Suspicious install script in "bad-pkg""
234
+ const nameMatch = finding.title.match(/^Known malicious package:\s+(.+)$/) ??
235
+ finding.title.match(/^Possible typosquat:\s+"([^"]+)"/) ??
236
+ finding.title.match(/^Suspicious install script in\s+"([^"]+)"/);
237
+ const pkgName = nameMatch?.[1]?.trim();
238
+ if (!pkgName)
239
+ return null;
240
+ // Resolve source path
241
+ const pkgParts = pkgName.split('/');
242
+ const srcPath = resolve(join(nodeModulesPath, ...pkgParts));
243
+ if (!existsSync(srcPath))
244
+ return null;
245
+ // Determine version from package.json
246
+ const pkg = readPackageJson(srcPath);
247
+ const version = pkg?.version ?? 'unknown';
248
+ // Build quarantine destination
249
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
250
+ const safeName = pkgName.replace(/\//g, '__');
251
+ const quarantineBase = join(homedir(), '.shieldcortex', 'quarantine', 'deps');
252
+ const destDir = join(quarantineBase, `${safeName}-${timestamp}`);
253
+ const pkgDestDir = join(destDir, 'pkg');
254
+ mkdirSync(destDir, { recursive: true });
255
+ // Move the package directory
256
+ renameSync(srcPath, pkgDestDir);
257
+ // Write manifest
258
+ const manifest = {
259
+ packageName: pkgName,
260
+ version,
261
+ reason: finding.title,
262
+ originalPath: srcPath,
263
+ quarantinedAt: new Date().toISOString(),
264
+ severity: finding.severity,
265
+ };
266
+ writeFileSync(join(destDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
267
+ return destDir;
268
+ }
269
+ /**
270
+ * Permanently delete a malicious package from node_modules.
271
+ *
272
+ * Only acts on CRITICAL findings (known malicious packages from blocklist).
273
+ *
274
+ * @returns The package name that was deleted, or null if not deleted.
275
+ */
276
+ export function cleanPackage(finding, nodeModulesPath) {
277
+ // Only clean CRITICAL findings
278
+ if (finding.severity !== 'critical') {
279
+ return null;
280
+ }
281
+ // Extract package name from the finding title
282
+ const nameMatch = finding.title.match(/^Known malicious package:\s+(.+)$/);
283
+ const pkgName = nameMatch?.[1]?.trim();
284
+ if (!pkgName)
285
+ return null;
286
+ const pkgParts = pkgName.split('/');
287
+ const srcPath = resolve(join(nodeModulesPath, ...pkgParts));
288
+ if (!existsSync(srcPath))
289
+ return null;
290
+ rmSync(srcPath, { recursive: true, force: true });
291
+ return pkgName;
292
+ }
293
+ export function scanDependencies(opts) {
294
+ const start = Date.now();
295
+ const { nodeModulesPath } = opts;
296
+ if (!existsSync(nodeModulesPath)) {
297
+ return {
298
+ name: 'Dependency Scanner',
299
+ itemsScanned: 0,
300
+ findings: [],
301
+ durationMs: Date.now() - start,
302
+ skipped: true,
303
+ skipReason: `node_modules not found at ${nodeModulesPath}`,
304
+ };
305
+ }
306
+ const packageNames = listPackages(nodeModulesPath);
307
+ const findings = [];
308
+ for (const name of packageNames) {
309
+ const pkgPath = join(nodeModulesPath, ...name.split('/'));
310
+ const pkg = readPackageJson(pkgPath);
311
+ // ── 1. Known malicious ──────────────────────────────────────────
312
+ if (checkMalicious(name)) {
313
+ findings.push({
314
+ scanner: 'dependency',
315
+ severity: 'critical',
316
+ title: `Known malicious package: ${name}`,
317
+ description: `"${name}" is a known malicious npm package. Remove it immediately and ` +
318
+ `audit your codebase for any data that may have been exfiltrated.`,
319
+ filePath: join(pkgPath, 'package.json'),
320
+ learnMoreUrl: LEARN_MORE_MALICIOUS,
321
+ });
322
+ }
323
+ // ── 2. Typosquat ────────────────────────────────────────────────
324
+ const typosquatOf = checkTyposquat(name);
325
+ if (typosquatOf) {
326
+ findings.push({
327
+ scanner: 'dependency',
328
+ severity: 'high',
329
+ title: `Possible typosquat: "${name}" → "${typosquatOf}"`,
330
+ description: `"${name}" is 1-2 characters away from the popular package "${typosquatOf}". ` +
331
+ `This may be a typosquatting attack. Verify the package is intentional.`,
332
+ filePath: join(pkgPath, 'package.json'),
333
+ learnMoreUrl: LEARN_MORE_TYPOSQUAT,
334
+ });
335
+ }
336
+ if (!pkg)
337
+ continue;
338
+ // ── 3. Suspicious postinstall ───────────────────────────────────
339
+ const { matchCount, labels } = checkSuspiciousScripts(pkg);
340
+ if (matchCount >= 1) {
341
+ const severity = matchCount >= 2 ? 'high' : 'medium';
342
+ findings.push({
343
+ scanner: 'dependency',
344
+ severity,
345
+ title: `Suspicious install script in "${name}"`,
346
+ description: `"${name}" has an install/postinstall script containing suspicious patterns: ` +
347
+ labels.join(', ') + '. Review before trusting this package.',
348
+ filePath: join(pkgPath, 'package.json'),
349
+ matchedText: labels.slice(0, 5).join(', '),
350
+ learnMoreUrl: LEARN_MORE_POSTINSTALL,
351
+ });
352
+ }
353
+ // ── 4. New package with postinstall ────────────────────────────
354
+ if (checkPackageAge(pkg)) {
355
+ findings.push({
356
+ scanner: 'dependency',
357
+ severity: 'medium',
358
+ title: `Newly published package with install script: "${name}"`,
359
+ description: `"${name}" was published within the last 7 days and has an install/postinstall ` +
360
+ `script. New packages with install scripts are a common supply-chain attack vector.`,
361
+ filePath: join(pkgPath, 'package.json'),
362
+ learnMoreUrl: LEARN_MORE_MALICIOUS,
363
+ });
364
+ }
365
+ }
366
+ return {
367
+ name: 'Dependency Scanner',
368
+ itemsScanned: packageNames.length,
369
+ findings,
370
+ durationMs: Date.now() - start,
371
+ };
372
+ }
373
+ /**
374
+ * Resolve the node_modules path for a given mode.
375
+ */
376
+ export function resolveNodeModulesPath(mode, customPath) {
377
+ switch (mode) {
378
+ case 'global': {
379
+ try {
380
+ const prefix = execSync('npm prefix -g', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
381
+ return join(prefix, 'lib', 'node_modules');
382
+ }
383
+ catch {
384
+ // Fallback common locations
385
+ return join('/usr/local/lib', 'node_modules');
386
+ }
387
+ }
388
+ case 'path':
389
+ return customPath ?? join(process.cwd(), 'node_modules');
390
+ case 'local':
391
+ default:
392
+ return join(process.cwd(), 'node_modules');
393
+ }
394
+ }
@@ -8,6 +8,7 @@ export { scanMemories } from './memory-scanner.js';
8
8
  export { scanMcpConfigs } from './mcp-config-scanner.js';
9
9
  export { scanEnvFiles } from './env-scanner.js';
10
10
  export { scanRulesFiles } from './rules-file-scanner.js';
11
+ export { scanDependencies, resolveNodeModulesPath, quarantinePackage, cleanPackage } from './dependency-scanner.js';
11
12
  export { formatTerminalReport, formatMarkdownReport, formatJsonReport } from './report-formatter.js';
12
13
  export { calculateGrade } from './types.js';
13
14
  export type { AuditFinding, AuditReport, AuditGrade, AuditSeverity, ScannerResult, } from './types.js';
@@ -8,5 +8,6 @@ export { scanMemories } from './memory-scanner.js';
8
8
  export { scanMcpConfigs } from './mcp-config-scanner.js';
9
9
  export { scanEnvFiles } from './env-scanner.js';
10
10
  export { scanRulesFiles } from './rules-file-scanner.js';
11
+ export { scanDependencies, resolveNodeModulesPath, quarantinePackage, cleanPackage } from './dependency-scanner.js';
11
12
  export { formatTerminalReport, formatMarkdownReport, formatJsonReport } from './report-formatter.js';
12
13
  export { calculateGrade } from './types.js';
@@ -9,6 +9,12 @@
9
9
  * npx shieldcortex audit --json # JSON output
10
10
  * npx shieldcortex audit --markdown # Markdown output
11
11
  * npx shieldcortex audit --ci # CI mode (exit code reflects grade)
12
+ * npx shieldcortex audit --deps # Also scan ./node_modules for malicious packages
13
+ * npx shieldcortex audit --deps-global # Also scan global npm node_modules
14
+ * npx shieldcortex audit --deps-path /p # Also scan specific node_modules path
15
+ * npx shieldcortex audit --deps --quarantine # Scan + quarantine CRITICAL/HIGH
16
+ * npx shieldcortex audit --deps --clean --force # Scan + permanently delete CRITICAL
17
+ * npx shieldcortex audit --deps --auto-protect # Scan + auto-quarantine CRITICAL
12
18
  */
13
19
  /**
14
20
  * Run the full audit.
package/dist/cli/audit.js CHANGED
@@ -9,19 +9,70 @@
9
9
  * npx shieldcortex audit --json # JSON output
10
10
  * npx shieldcortex audit --markdown # Markdown output
11
11
  * npx shieldcortex audit --ci # CI mode (exit code reflects grade)
12
+ * npx shieldcortex audit --deps # Also scan ./node_modules for malicious packages
13
+ * npx shieldcortex audit --deps-global # Also scan global npm node_modules
14
+ * npx shieldcortex audit --deps-path /p # Also scan specific node_modules path
15
+ * npx shieldcortex audit --deps --quarantine # Scan + quarantine CRITICAL/HIGH
16
+ * npx shieldcortex audit --deps --clean --force # Scan + permanently delete CRITICAL
17
+ * npx shieldcortex audit --deps --auto-protect # Scan + auto-quarantine CRITICAL
12
18
  */
13
- import { scanMemories, scanMcpConfigs, scanEnvFiles, scanRulesFiles, formatTerminalReport, formatMarkdownReport, formatJsonReport, calculateGrade, } from '../audit/index.js';
19
+ import { requireFeature, FeatureGatedError } from '../license/gate.js';
20
+ import { scanMemories, scanMcpConfigs, scanEnvFiles, scanRulesFiles, scanDependencies, resolveNodeModulesPath, quarantinePackage, cleanPackage, formatTerminalReport, formatMarkdownReport, formatJsonReport, calculateGrade, } from '../audit/index.js';
14
21
  function parseAuditArgs(args) {
15
- const options = { format: 'terminal', ci: false };
16
- for (const arg of args) {
17
- if (arg === '--json')
22
+ const options = {
23
+ format: 'terminal',
24
+ ci: false,
25
+ deps: false,
26
+ depsMode: 'local',
27
+ quarantine: false,
28
+ clean: false,
29
+ autoProtect: false,
30
+ force: false,
31
+ };
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i];
34
+ if (arg === '--json') {
18
35
  options.format = 'json';
19
- else if (arg === '--markdown' || arg === '--md')
36
+ }
37
+ else if (arg === '--markdown' || arg === '--md') {
20
38
  options.format = 'markdown';
39
+ }
21
40
  else if (arg === '--ci') {
22
41
  options.ci = true;
23
42
  options.format = 'json';
24
43
  }
44
+ else if (arg === '--deps') {
45
+ options.deps = true;
46
+ options.depsMode = 'local';
47
+ }
48
+ else if (arg === '--deps-global') {
49
+ options.deps = true;
50
+ options.depsMode = 'global';
51
+ }
52
+ else if (arg === '--deps-path') {
53
+ options.deps = true;
54
+ options.depsMode = 'path';
55
+ const next = args[i + 1];
56
+ if (next && !next.startsWith('--')) {
57
+ options.depsPath = next;
58
+ i++; // consume path argument
59
+ }
60
+ }
61
+ else if (arg === '--quarantine') {
62
+ options.quarantine = true;
63
+ options.deps = true;
64
+ }
65
+ else if (arg === '--clean') {
66
+ options.clean = true;
67
+ options.deps = true;
68
+ }
69
+ else if (arg === '--auto-protect') {
70
+ options.autoProtect = true;
71
+ options.deps = true;
72
+ }
73
+ else if (arg === '--force') {
74
+ options.force = true;
75
+ }
25
76
  }
26
77
  return options;
27
78
  }
@@ -31,6 +82,13 @@ function parseAuditArgs(args) {
31
82
  export async function handleAuditCommand(args) {
32
83
  const options = parseAuditArgs(args);
33
84
  const start = Date.now();
85
+ // --clean without --force: print warning and exit
86
+ if (options.clean && !options.force && !options.ci) {
87
+ console.error('\x1b[33m⚠ Are you sure? Use --force to confirm.\x1b[0m\n' +
88
+ ' --clean permanently deletes CRITICAL packages from node_modules.\n' +
89
+ ' Run: shieldcortex audit --deps --clean --force');
90
+ process.exit(1);
91
+ }
34
92
  // Get version from package.json
35
93
  let version = 'unknown';
36
94
  try {
@@ -56,30 +114,46 @@ export async function handleAuditCommand(args) {
56
114
  }
57
115
  // Run all scanners
58
116
  const scanners = [];
117
+ const totalSteps = options.deps ? 5 : 4;
59
118
  if (options.format === 'terminal')
60
- process.stdout.write(' \x1b[2m[1/4] Memory files...\x1b[0m');
119
+ process.stdout.write(` \x1b[2m[1/${totalSteps}] Memory files...\x1b[0m`);
61
120
  const memoryResult = scanMemories();
62
121
  scanners.push(memoryResult);
63
122
  if (options.format === 'terminal')
64
- process.stdout.write(`\r \x1b[32m[1/4]\x1b[0m Memory files \x1b[2m${memoryResult.durationMs}ms\x1b[0m\n`);
123
+ process.stdout.write(`\r \x1b[32m[1/${totalSteps}]\x1b[0m Memory files \x1b[2m${memoryResult.durationMs}ms\x1b[0m\n`);
65
124
  if (options.format === 'terminal')
66
- process.stdout.write(' \x1b[2m[2/4] MCP configs...\x1b[0m');
125
+ process.stdout.write(` \x1b[2m[2/${totalSteps}] MCP configs...\x1b[0m`);
67
126
  const mcpResult = scanMcpConfigs();
68
127
  scanners.push(mcpResult);
69
128
  if (options.format === 'terminal')
70
- process.stdout.write(`\r \x1b[32m[2/4]\x1b[0m MCP configs \x1b[2m${mcpResult.durationMs}ms\x1b[0m\n`);
129
+ process.stdout.write(`\r \x1b[32m[2/${totalSteps}]\x1b[0m MCP configs \x1b[2m${mcpResult.durationMs}ms\x1b[0m\n`);
71
130
  if (options.format === 'terminal')
72
- process.stdout.write(' \x1b[2m[3/4] Environment secrets...\x1b[0m');
131
+ process.stdout.write(` \x1b[2m[3/${totalSteps}] Environment secrets...\x1b[0m`);
73
132
  const envResult = scanEnvFiles();
74
133
  scanners.push(envResult);
75
134
  if (options.format === 'terminal')
76
- process.stdout.write(`\r \x1b[32m[3/4]\x1b[0m Environment \x1b[2m${envResult.durationMs}ms\x1b[0m\n`);
135
+ process.stdout.write(`\r \x1b[32m[3/${totalSteps}]\x1b[0m Environment \x1b[2m${envResult.durationMs}ms\x1b[0m\n`);
77
136
  if (options.format === 'terminal')
78
- process.stdout.write(' \x1b[2m[4/4] Rules files...\x1b[0m');
137
+ process.stdout.write(` \x1b[2m[4/${totalSteps}] Rules files...\x1b[0m`);
79
138
  const rulesResult = scanRulesFiles();
80
139
  scanners.push(rulesResult);
81
140
  if (options.format === 'terminal')
82
- process.stdout.write(`\r \x1b[32m[4/4]\x1b[0m Rules files \x1b[2m${rulesResult.durationMs}ms\x1b[0m\n\n`);
141
+ process.stdout.write(`\r \x1b[32m[4/${totalSteps}]\x1b[0m Rules files \x1b[2m${rulesResult.durationMs}ms\x1b[0m\n`);
142
+ // Optional: dependency scan
143
+ let nodeModulesPath = '';
144
+ if (options.deps) {
145
+ nodeModulesPath = resolveNodeModulesPath(options.depsMode, options.depsPath);
146
+ if (options.format === 'terminal') {
147
+ process.stdout.write(` \x1b[2m[5/${totalSteps}] Dependencies (${nodeModulesPath})...\x1b[0m`);
148
+ }
149
+ const depsResult = scanDependencies({ nodeModulesPath });
150
+ scanners.push(depsResult);
151
+ if (options.format === 'terminal') {
152
+ process.stdout.write(`\r \x1b[32m[5/${totalSteps}]\x1b[0m Dependencies \x1b[2m${depsResult.durationMs}ms\x1b[0m\n`);
153
+ }
154
+ }
155
+ if (options.format === 'terminal')
156
+ process.stdout.write('\n');
83
157
  // Aggregate findings
84
158
  const allFindings = scanners.flatMap(s => s.findings);
85
159
  // Sort by severity (critical first)
@@ -117,6 +191,102 @@ export async function handleAuditCommand(args) {
117
191
  console.log(formatTerminalReport(report));
118
192
  break;
119
193
  }
194
+ // ── Post-scan actions ────────────────────────────────────────────────────
195
+ const depFindings = allFindings.filter(f => f.scanner === 'dependency');
196
+ if (options.quarantine && depFindings.length > 0) {
197
+ try {
198
+ requireFeature('deps_quarantine');
199
+ }
200
+ catch (e) {
201
+ if (e instanceof FeatureGatedError) {
202
+ console.error('\n' + e.message);
203
+ process.exit(1);
204
+ }
205
+ throw e;
206
+ }
207
+ const eligible = depFindings.filter(f => f.severity === 'critical' || f.severity === 'high');
208
+ if (eligible.length === 0) {
209
+ if (options.format === 'terminal') {
210
+ console.log('\n\x1b[32m✓ No CRITICAL or HIGH dependency findings to quarantine.\x1b[0m');
211
+ }
212
+ }
213
+ else {
214
+ if (options.format === 'terminal') {
215
+ console.log(`\n\x1b[33m⚠ Quarantining ${eligible.length} package(s)...\x1b[0m`);
216
+ }
217
+ for (const finding of eligible) {
218
+ const dest = quarantinePackage(finding, nodeModulesPath);
219
+ if (dest && options.format === 'terminal') {
220
+ console.log(` \x1b[31m✗\x1b[0m Quarantined: ${finding.title}`);
221
+ console.log(` → ${dest}`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ if (options.clean && depFindings.length > 0) {
227
+ try {
228
+ requireFeature('deps_clean');
229
+ }
230
+ catch (e) {
231
+ if (e instanceof FeatureGatedError) {
232
+ console.error('\n' + e.message);
233
+ process.exit(1);
234
+ }
235
+ throw e;
236
+ }
237
+ const criticals = depFindings.filter(f => f.severity === 'critical');
238
+ if (criticals.length === 0) {
239
+ if (options.format === 'terminal') {
240
+ console.log('\n\x1b[32m✓ No CRITICAL dependency findings to clean.\x1b[0m');
241
+ }
242
+ }
243
+ else {
244
+ if (options.format === 'terminal') {
245
+ console.log(`\n\x1b[31m🗑 Cleaning ${criticals.length} CRITICAL package(s)...\x1b[0m`);
246
+ }
247
+ for (const finding of criticals) {
248
+ const deleted = cleanPackage(finding, nodeModulesPath);
249
+ if (deleted && options.format === 'terminal') {
250
+ console.log(` \x1b[31m✗\x1b[0m Deleted: ${deleted}`);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ if (options.autoProtect && depFindings.length > 0) {
256
+ try {
257
+ requireFeature('deps_auto_protect');
258
+ }
259
+ catch (e) {
260
+ if (e instanceof FeatureGatedError) {
261
+ console.error('\n' + e.message);
262
+ process.exit(1);
263
+ }
264
+ throw e;
265
+ }
266
+ const criticals = depFindings.filter(f => f.severity === 'critical');
267
+ const highs = depFindings.filter(f => f.severity === 'high');
268
+ if (criticals.length > 0) {
269
+ if (options.format === 'terminal') {
270
+ console.log(`\n\x1b[33m⚠ Auto-protect: quarantining ${criticals.length} CRITICAL package(s)...\x1b[0m`);
271
+ }
272
+ for (const finding of criticals) {
273
+ const dest = quarantinePackage(finding, nodeModulesPath);
274
+ if (dest && options.format === 'terminal') {
275
+ console.log(` \x1b[31m✗\x1b[0m Quarantined: ${finding.title}`);
276
+ console.log(` → ${dest}`);
277
+ }
278
+ }
279
+ }
280
+ if (highs.length > 0 && options.format === 'terminal') {
281
+ console.log(`\n\x1b[33m⚠ Auto-protect: ${highs.length} HIGH finding(s) detected (manual review required):\x1b[0m`);
282
+ for (const finding of highs) {
283
+ console.log(` \x1b[33m!\x1b[0m ${finding.title}`);
284
+ }
285
+ }
286
+ if (criticals.length === 0 && highs.length === 0 && options.format === 'terminal') {
287
+ console.log('\n\x1b[32m✓ Auto-protect: no CRITICAL or HIGH dependency findings.\x1b[0m');
288
+ }
289
+ }
120
290
  // Exit code
121
291
  if (options.ci) {
122
292
  // In CI mode: fail on critical or high findings
@@ -6,7 +6,7 @@
6
6
  * isFeatureEnabled('cloud_sync'); // returns boolean (soft check)
7
7
  */
8
8
  import { type LicenseTier } from './keys.js';
9
- export type GatedFeature = 'custom_injection_patterns' | 'custom_iron_dome_policies' | 'custom_firewall_rules' | 'audit_export' | 'skill_scanner_deep' | 'cloud_sync' | 'team_management' | 'shared_patterns' | 'cortex_learning';
9
+ export type GatedFeature = 'custom_injection_patterns' | 'custom_iron_dome_policies' | 'custom_firewall_rules' | 'audit_export' | 'skill_scanner_deep' | 'cloud_sync' | 'team_management' | 'shared_patterns' | 'cortex_learning' | 'deps_quarantine' | 'deps_clean' | 'deps_auto_protect' | 'deps_global_scan' | 'memory_types' | 'memory_scopes' | 'dream_mode' | 'llm_reranking' | 'positive_feedback';
10
10
  export declare class FeatureGatedError extends Error {
11
11
  feature: GatedFeature;
12
12
  requiredTier: LicenseTier;
@@ -17,6 +17,15 @@ const FEATURE_TIERS = {
17
17
  team_management: 'team',
18
18
  shared_patterns: 'team',
19
19
  cortex_learning: 'pro',
20
+ deps_quarantine: 'pro',
21
+ deps_clean: 'pro',
22
+ deps_auto_protect: 'pro',
23
+ deps_global_scan: 'pro',
24
+ memory_types: 'pro',
25
+ memory_scopes: 'team',
26
+ dream_mode: 'pro',
27
+ llm_reranking: 'pro',
28
+ positive_feedback: 'pro',
20
29
  };
21
30
  const FEATURE_DESCRIPTIONS = {
22
31
  custom_injection_patterns: 'Define up to 50 custom regex patterns for detecting domain-specific threats.',
@@ -28,6 +37,15 @@ const FEATURE_DESCRIPTIONS = {
28
37
  team_management: 'Manage team members, invites, and shared security policies.',
29
38
  shared_patterns: 'Share custom injection patterns and policies across your team.',
30
39
  cortex_learning: 'Systematic mistake learning with pre-flight checks, pattern detection, and rule graduation.',
40
+ deps_quarantine: 'Quarantine malicious packages — move threats to a safe holding area.',
41
+ deps_clean: 'Permanently remove known malicious packages from your project.',
42
+ deps_auto_protect: 'Automated scan + quarantine of critical threats on every install.',
43
+ deps_global_scan: 'Scan global npm installations for supply chain threats.',
44
+ memory_types: 'Typed memories (user/feedback/project/reference) for structured knowledge.',
45
+ memory_scopes: 'Private vs team memory scopes for multi-agent deployments.',
46
+ dream_mode: 'Background memory consolidation — merge duplicates, archive stale, detect contradictions.',
47
+ llm_reranking: 'LLM-powered memory reranking for precision recall.',
48
+ positive_feedback: 'Capture what worked, not just what failed — learn from success.',
31
49
  };
32
50
  // ── Error class ──────────────────────────────────────────
33
51
  export class FeatureGatedError extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shieldcortex",
3
- "version": "4.0.2",
3
+ "version": "4.2.0",
4
4
  "description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",