@safetnsr/vet 1.15.1 → 1.16.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,3 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkGuard(cwd: string): CheckResult;
3
+ export declare function runGuardCommand(format: string, cwd?: string): Promise<void>;
@@ -0,0 +1,179 @@
1
+ import { join, extname } from 'node:path';
2
+ import { cachedRead } from '../file-cache.js';
3
+ import { walkFiles, c } from '../util.js';
4
+ // ── Constants ────────────────────────────────────────────────────────────────
5
+ const SCAN_EXTS = new Set(['.ts', '.js', '.mjs', '.cjs', '.sql', '.sh', '.bash', '.py', '.rb']);
6
+ const SQL_EXTS = new Set(['.sql', '.ts', '.js', '.mjs', '.cjs']);
7
+ const SHELL_EXTS = new Set(['.sh', '.bash', '.ts', '.js']);
8
+ const SKIP_DIRS = ['test', '__tests__'];
9
+ const SKIP_PATTERN = /\.(test|spec)\.[^.]+$/;
10
+ // SQL patterns (case-insensitive)
11
+ const DROP_TABLE_RE = /\bDROP\s+TABLE\b/i;
12
+ const DROP_DB_RE = /\bDROP\s+DATABASE\b/i;
13
+ const TRUNCATE_RE = /\bTRUNCATE\b(\s+TABLE\b)?/i;
14
+ const DELETE_FROM_RE = /\bDELETE\s+FROM\b/i;
15
+ const DELETE_WHERE_RE = /\bDELETE\s+FROM\b.*\bWHERE\b/i;
16
+ // Shell patterns
17
+ const RM_RF_RE = /\brm\s+-(r|rf|fr)\b/i;
18
+ const RMDIR_RE = /\brmdir\b/i;
19
+ const SHRED_RE = /\bshred\b/;
20
+ const TRUNCATE_CMD_RE = /\btruncate\s+--size\b/;
21
+ // JS exec patterns
22
+ const EXEC_CALL_RE = /\b(exec|execSync|spawn|spawnSync)\s*\(/;
23
+ // Migration path patterns
24
+ const MIGRATION_PATH_RE = /migrat|db[/\\]/i;
25
+ // Rollback function patterns
26
+ const ROLLBACK_RE = /\b(down|rollback|revert)\s*\(/;
27
+ // ── Helpers ──────────────────────────────────────────────────────────────────
28
+ function shouldSkip(relPath) {
29
+ const parts = relPath.split(/[/\\]/);
30
+ for (const part of parts) {
31
+ if (SKIP_DIRS.includes(part))
32
+ return true;
33
+ }
34
+ return SKIP_PATTERN.test(relPath);
35
+ }
36
+ function scanLine(line, lineNum, relPath, ext, issues) {
37
+ const isSqlExt = SQL_EXTS.has(ext);
38
+ const isShellExt = SHELL_EXTS.has(ext);
39
+ // Pass 1 — SQL patterns
40
+ if (isSqlExt) {
41
+ if (DROP_TABLE_RE.test(line)) {
42
+ issues.push({ severity: 'error', message: 'DROP TABLE without transaction', file: relPath, line: lineNum, fixable: false, fixHint: 'wrap in transaction or add rollback' });
43
+ }
44
+ if (DROP_DB_RE.test(line)) {
45
+ issues.push({ severity: 'error', message: 'DROP DATABASE detected', file: relPath, line: lineNum, fixable: false, fixHint: 'remove or gate behind confirmation' });
46
+ }
47
+ if (TRUNCATE_RE.test(line)) {
48
+ issues.push({ severity: 'error', message: 'TRUNCATE operation detected', file: relPath, line: lineNum, fixable: false, fixHint: 'use soft-delete or add rollback' });
49
+ }
50
+ if (DELETE_FROM_RE.test(line)) {
51
+ if (DELETE_WHERE_RE.test(line)) {
52
+ issues.push({ severity: 'warning', message: 'DELETE FROM with WHERE clause', file: relPath, line: lineNum, fixable: false, fixHint: 'consider soft-delete or add --dry-run check' });
53
+ }
54
+ else {
55
+ issues.push({ severity: 'error', message: 'DELETE FROM without WHERE clause', file: relPath, line: lineNum, fixable: false, fixHint: 'add WHERE clause or use TRUNCATE with rollback' });
56
+ }
57
+ }
58
+ }
59
+ // Pass 2 — Shell patterns
60
+ if (isShellExt) {
61
+ // Direct shell commands in .sh/.bash
62
+ if (ext === '.sh' || ext === '.bash') {
63
+ if (RM_RF_RE.test(line)) {
64
+ issues.push({ severity: 'error', message: 'rm -rf in shell script', file: relPath, line: lineNum, fixable: false, fixHint: 'use trash-cli or add confirmation gate' });
65
+ }
66
+ if (RMDIR_RE.test(line)) {
67
+ issues.push({ severity: 'error', message: 'rmdir in shell script', file: relPath, line: lineNum, fixable: false, fixHint: 'use trash-cli or add confirmation gate' });
68
+ }
69
+ if (SHRED_RE.test(line)) {
70
+ issues.push({ severity: 'error', message: 'shred command detected', file: relPath, line: lineNum, fixable: false, fixHint: 'remove or gate behind confirmation' });
71
+ }
72
+ if (TRUNCATE_CMD_RE.test(line)) {
73
+ issues.push({ severity: 'error', message: 'truncate --size command detected', file: relPath, line: lineNum, fixable: false, fixHint: 'remove or gate behind confirmation' });
74
+ }
75
+ }
76
+ // JS/TS exec/spawn calls with destructive commands
77
+ if (ext === '.ts' || ext === '.js') {
78
+ if (EXEC_CALL_RE.test(line)) {
79
+ if (RM_RF_RE.test(line)) {
80
+ issues.push({ severity: 'error', message: 'rm -rf in exec call', file: relPath, line: lineNum, fixable: false, fixHint: 'use trash-cli or add confirmation gate' });
81
+ }
82
+ if (RMDIR_RE.test(line)) {
83
+ issues.push({ severity: 'error', message: 'rmdir in exec call', file: relPath, line: lineNum, fixable: false, fixHint: 'use trash-cli or add confirmation gate' });
84
+ }
85
+ if (SHRED_RE.test(line)) {
86
+ issues.push({ severity: 'error', message: 'shred in exec call', file: relPath, line: lineNum, fixable: false, fixHint: 'remove or gate behind confirmation' });
87
+ }
88
+ if (TRUNCATE_CMD_RE.test(line)) {
89
+ issues.push({ severity: 'error', message: 'truncate --size in exec call', file: relPath, line: lineNum, fixable: false, fixHint: 'remove or gate behind confirmation' });
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ // ── Main check ───────────────────────────────────────────────────────────────
96
+ export function checkGuard(cwd) {
97
+ const issues = [];
98
+ const files = walkFiles(cwd);
99
+ for (const relPath of files) {
100
+ const ext = extname(relPath).toLowerCase();
101
+ if (!SCAN_EXTS.has(ext))
102
+ continue;
103
+ if (shouldSkip(relPath))
104
+ continue;
105
+ const fullPath = join(cwd, relPath);
106
+ let content;
107
+ try {
108
+ content = cachedRead(fullPath);
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ const rel = relPath;
114
+ const lines = content.split('\n');
115
+ const fileIssuesBefore = issues.length;
116
+ for (let i = 0; i < lines.length; i++) {
117
+ scanLine(lines[i], i + 1, rel, ext, issues);
118
+ }
119
+ // Pass 3 — Migration check
120
+ if (MIGRATION_PATH_RE.test(rel)) {
121
+ const hasDestructive = DROP_TABLE_RE.test(content) || DROP_DB_RE.test(content) ||
122
+ DELETE_FROM_RE.test(content) || TRUNCATE_RE.test(content);
123
+ const hasRollback = ROLLBACK_RE.test(content);
124
+ if (hasDestructive && !hasRollback) {
125
+ issues.push({
126
+ severity: 'warning',
127
+ message: 'migration with destructive operation but no rollback function',
128
+ file: rel,
129
+ fixable: false,
130
+ fixHint: 'add down() or rollback() function',
131
+ });
132
+ }
133
+ }
134
+ }
135
+ const errors = issues.filter(i => i.severity === 'error').length;
136
+ const warnings = issues.filter(i => i.severity === 'warning').length;
137
+ const total = errors + warnings;
138
+ const score = Math.max(0, 100 - (errors * 15) - (warnings * 5));
139
+ const summary = total === 0
140
+ ? 'no destructive patterns found'
141
+ : `${total} bomb sites found (${errors} fatal, ${warnings} warning)`;
142
+ return { name: 'guard', score, maxScore: 100, issues, summary };
143
+ }
144
+ // ── Subcommand output ────────────────────────────────────────────────────────
145
+ export async function runGuardCommand(format, cwd) {
146
+ const dir = cwd || process.cwd();
147
+ const result = checkGuard(dir);
148
+ if (format === 'json') {
149
+ console.log(JSON.stringify(result, null, 2));
150
+ return;
151
+ }
152
+ console.log(`\n ${c.bold}vet guard${c.reset} — destructive operation scanner\n`);
153
+ const errors = result.issues.filter(i => i.severity === 'error');
154
+ const warnings = result.issues.filter(i => i.severity === 'warning');
155
+ if (errors.length > 0) {
156
+ console.log(` ${c.red}FATAL${c.reset}`);
157
+ for (const issue of errors) {
158
+ const loc = issue.file ? (issue.line ? `${issue.file}:${issue.line}` : issue.file) : '';
159
+ console.log(` ${c.red}✗${c.reset} ${issue.message}${loc ? ` (${loc})` : ''}`);
160
+ if (issue.fixHint)
161
+ console.log(` ${c.dim}→ ${issue.fixHint}${c.reset}`);
162
+ }
163
+ console.log();
164
+ }
165
+ if (warnings.length > 0) {
166
+ console.log(` ${c.yellow}WARN${c.reset}`);
167
+ for (const issue of warnings) {
168
+ const loc = issue.file ? (issue.line ? `${issue.file}:${issue.line}` : issue.file) : '';
169
+ console.log(` ${c.yellow}⚠${c.reset} ${issue.message}${loc ? ` (${loc})` : ''}`);
170
+ if (issue.fixHint)
171
+ console.log(` ${c.dim}→ ${issue.fixHint}${c.reset}`);
172
+ }
173
+ console.log();
174
+ }
175
+ if (result.issues.length === 0) {
176
+ console.log(` ${c.green}no destructive patterns found${c.reset}\n`);
177
+ }
178
+ console.log(` ${result.summary}\n`);
179
+ }
package/dist/cli.js CHANGED
@@ -23,6 +23,7 @@ import { checkCompact, runCompactCommand } from './checks/compact.js';
23
23
  import { checkSubsidy, runSubsidyCommand } from './checks/subsidy.js';
24
24
  import { checkLoop, runLoopCommand } from './checks/loop.js';
25
25
  import { checkBloat, runBloatCommand } from './checks/bloat.js';
26
+ import { checkGuard, runGuardCommand } from './checks/guard.js';
26
27
  import { checkCompleteness } from './checks/completeness.js';
27
28
  import { score } from './scorer.js';
28
29
  import { reportPretty, reportJSON, reportBadge } from './reporter.js';
@@ -75,6 +76,7 @@ if (flags.has('--help') || flags.has('-h')) {
75
76
  npx @safetnsr/vet subsidy [--plan tier] [--since date] show AI cost vs subscription
76
77
  npx @safetnsr/vet loop [log] /loop session forensics — per-iteration timeline
77
78
  npx @safetnsr/vet bloat detect agent-generated code bloat
79
+ npx @safetnsr/vet guard [dir] scan for destructive operation bomb sites
78
80
 
79
81
  ${c.dim}categories:${c.reset}
80
82
  security (30%) scan, secrets, config, model usage
@@ -110,7 +112,7 @@ if (flags.has('--version') || flags.has('-v')) {
110
112
  }
111
113
  process.exit(0);
112
114
  }
113
- const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat'];
115
+ const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard'];
114
116
  const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
115
117
  const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
116
118
  const isCI = flags.has('--ci');
@@ -241,6 +243,17 @@ if (command === 'bloat') {
241
243
  }
242
244
  process.exit(0);
243
245
  }
246
+ if (command === 'guard') {
247
+ try {
248
+ const format = isJSON ? 'json' : 'ascii';
249
+ await runGuardCommand(format, cwd);
250
+ }
251
+ catch (e) {
252
+ console.error(`${c.red}guard failed:${c.reset}`, e instanceof Error ? e.message : e);
253
+ process.exit(1);
254
+ }
255
+ process.exit(0);
256
+ }
244
257
  if (!isGitRepo(cwd)) {
245
258
  console.error(`${c.red}not a git repository${c.reset}. vet operates on git repos.`);
246
259
  process.exit(1);
@@ -292,7 +305,7 @@ async function runChecks() {
292
305
  }
293
306
  }
294
307
  // Run ALL independent checks in parallel
295
- const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult,] = await Promise.all([
308
+ const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult,] = await Promise.all([
296
309
  withTimeout('scan', () => checkScan(cwd)),
297
310
  withTimeout('secrets', () => checkSecrets(cwd)),
298
311
  withTimeout('config', () => checkConfig(cwd, ignore)),
@@ -312,6 +325,7 @@ async function runChecks() {
312
325
  withTimeout('loop', () => checkLoop(cwd)),
313
326
  withTimeout('completeness', () => checkCompleteness(cwd, ignore)),
314
327
  withTimeout('bloat', () => checkBloat(cwd)),
328
+ withTimeout('guard', () => checkGuard(cwd)),
315
329
  ]);
316
330
  // Git-dependent checks (diff + history) — parallel with each other
317
331
  const [diffResult, historyResult] = await Promise.all([
@@ -321,7 +335,7 @@ async function runChecks() {
321
335
  // Clear file cache after all checks complete
322
336
  clearCache();
323
337
  return score(cwd, {
324
- security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult],
338
+ security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult, guardResult],
325
339
  integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult],
326
340
  debt: [readyResult, historyResult, debtResult, bloatResult],
327
341
  deps: [depsResult],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {