@safetnsr/vet 1.23.0 → 1.25.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 checkSandbox(cwd: string): Promise<CheckResult>;
3
+ export declare function runSandboxCommand(cwd: string, flags: Set<string>): Promise<void>;
@@ -0,0 +1,280 @@
1
+ import { join } from 'node:path';
2
+ import { statSync, existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { readFile, c } from '../util.js';
5
+ // ── Constants ────────────────────────────────────────────────────────────────
6
+ const SENSITIVE_DIRS = [
7
+ '~/.ssh',
8
+ '~/.aws',
9
+ '~/.gnupg',
10
+ '~/.config/gcloud',
11
+ '~/.kube',
12
+ '~/.docker',
13
+ '~/.npmrc',
14
+ '~/.pypirc',
15
+ '~/.netrc',
16
+ ];
17
+ const ENV_PATTERNS = [
18
+ /KEY/i,
19
+ /SECRET/i,
20
+ /TOKEN/i,
21
+ /PASSWORD/i,
22
+ /CREDENTIAL/i,
23
+ /AUTH/i,
24
+ ];
25
+ const NETWORK_RESTRICTION_PATTERNS = [
26
+ /allowedUrls/i,
27
+ /blockedUrls/i,
28
+ /networkRestrict/i,
29
+ /network.*allow/i,
30
+ /network.*block/i,
31
+ /allowlist/i,
32
+ /denylist/i,
33
+ /block.*network/i,
34
+ ];
35
+ // ── Probe 1: Sensitive dirs ───────────────────────────────────────────────────
36
+ function probeSensitiveDirs() {
37
+ const issues = [];
38
+ let deduction = 0;
39
+ const home = homedir();
40
+ for (const dir of SENSITIVE_DIRS) {
41
+ const resolved = dir.replace('~', home);
42
+ try {
43
+ statSync(resolved);
44
+ // accessible
45
+ deduction += 1;
46
+ issues.push({
47
+ severity: 'error',
48
+ message: `Sensitive directory accessible: ${dir}`,
49
+ fixable: false,
50
+ fixHint: 'Run agent in a sandboxed environment (Docker, VM, chroot) to restrict fs access',
51
+ });
52
+ }
53
+ catch {
54
+ // not accessible — good
55
+ }
56
+ }
57
+ return { deduction, issues };
58
+ }
59
+ // ── Probe 2: Env var leaks ────────────────────────────────────────────────────
60
+ function probeEnvVars() {
61
+ const issues = [];
62
+ let rawDeduction = 0;
63
+ for (const key of Object.keys(process.env)) {
64
+ const matches = ENV_PATTERNS.some(re => re.test(key));
65
+ if (matches) {
66
+ rawDeduction += 0.5;
67
+ issues.push({
68
+ severity: 'warning',
69
+ message: `Sensitive env var exposed: ${key}`,
70
+ fixable: false,
71
+ fixHint: 'Use a secrets manager or strip sensitive vars before running agent',
72
+ });
73
+ }
74
+ }
75
+ const deduction = Math.min(rawDeduction, 3);
76
+ return { deduction, issues };
77
+ }
78
+ // ── Probe 3: Network rules ────────────────────────────────────────────────────
79
+ function probeNetworkRules(cwd) {
80
+ const issues = [];
81
+ let deduction = 0;
82
+ const filesToCheck = ['CLAUDE.md', 'AGENTS.md'];
83
+ let found = false;
84
+ for (const filename of filesToCheck) {
85
+ const content = readFile(join(cwd, filename));
86
+ if (!content)
87
+ continue;
88
+ const hasRestriction = NETWORK_RESTRICTION_PATTERNS.some(re => re.test(content));
89
+ if (hasRestriction) {
90
+ found = true;
91
+ break;
92
+ }
93
+ }
94
+ if (!found) {
95
+ deduction = 1;
96
+ issues.push({
97
+ severity: 'warning',
98
+ message: 'No network restriction rules found in CLAUDE.md or AGENTS.md',
99
+ fixable: false,
100
+ fixHint: 'Add allowedUrls or blockedUrls rules to CLAUDE.md to limit agent network access',
101
+ });
102
+ }
103
+ return { deduction, issues };
104
+ }
105
+ // ── Probe 4: MCP permissions ──────────────────────────────────────────────────
106
+ function probeMcpPermissions(cwd) {
107
+ const issues = [];
108
+ let rawDeduction = 0;
109
+ const settingsPath = join(cwd, '.claude', 'settings.json');
110
+ if (!existsSync(settingsPath)) {
111
+ issues.push({
112
+ severity: 'info',
113
+ message: 'No .claude/settings.json found — cannot audit MCP permissions',
114
+ fixable: false,
115
+ fixHint: 'Create .claude/settings.json with explicit MCP permission scopes',
116
+ });
117
+ return { deduction: 0, issues };
118
+ }
119
+ let settings;
120
+ try {
121
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
122
+ }
123
+ catch {
124
+ issues.push({
125
+ severity: 'warning',
126
+ message: 'Failed to parse .claude/settings.json',
127
+ file: '.claude/settings.json',
128
+ fixable: false,
129
+ });
130
+ return { deduction: 0, issues };
131
+ }
132
+ // Check mcpServers for tools with filesystem:write or no path restrictions
133
+ const mcpServers = settings.mcpServers || {};
134
+ for (const [serverName, server] of Object.entries(mcpServers)) {
135
+ const srv = server;
136
+ const tools = srv.tools || [];
137
+ for (const tool of tools) {
138
+ const permissions = tool.permissions || [];
139
+ const hasWriteAccess = permissions.includes('filesystem:write');
140
+ const hasNoPathRestriction = !permissions.some(p => p.startsWith('path:'));
141
+ if (hasWriteAccess && hasNoPathRestriction) {
142
+ rawDeduction += 1;
143
+ issues.push({
144
+ severity: 'error',
145
+ message: `MCP tool with unrestricted filesystem:write: ${serverName}/${tool.name || 'unknown'}`,
146
+ file: '.claude/settings.json',
147
+ fixable: false,
148
+ fixHint: 'Add path: restrictions to limit filesystem write access',
149
+ });
150
+ }
151
+ }
152
+ // Also check top-level permissions on the server
153
+ const serverPermissions = srv.permissions || [];
154
+ const hasWriteAccess = serverPermissions.includes('filesystem:write');
155
+ const hasNoPathRestriction = !serverPermissions.some((p) => p.startsWith('path:'));
156
+ if (hasWriteAccess && hasNoPathRestriction) {
157
+ rawDeduction += 1;
158
+ issues.push({
159
+ severity: 'error',
160
+ message: `MCP server with unrestricted filesystem:write: ${serverName}`,
161
+ file: '.claude/settings.json',
162
+ fixable: false,
163
+ fixHint: 'Add path: restrictions to limit filesystem write access',
164
+ });
165
+ }
166
+ }
167
+ const deduction = Math.min(rawDeduction, 2);
168
+ return { deduction, issues };
169
+ }
170
+ // ── Blast radius score ────────────────────────────────────────────────────────
171
+ function blastRadiusLabel(score) {
172
+ if (score >= 9)
173
+ return 'minimal — agent is tightly sandboxed';
174
+ if (score >= 7)
175
+ return 'low — some exposure, mostly contained';
176
+ if (score >= 5)
177
+ return 'moderate — agent can access sensitive resources';
178
+ if (score >= 3)
179
+ return 'high — agent has broad filesystem and secret access';
180
+ return 'critical — agent is running in a fully open environment';
181
+ }
182
+ // ── Main check ───────────────────────────────────────────────────────────────
183
+ export async function checkSandbox(cwd) {
184
+ const allIssues = [];
185
+ const sensitiveDirs = probeSensitiveDirs();
186
+ const envVars = probeEnvVars();
187
+ const networkRules = probeNetworkRules(cwd);
188
+ const mcpPerms = probeMcpPermissions(cwd);
189
+ allIssues.push(...sensitiveDirs.issues);
190
+ allIssues.push(...envVars.issues);
191
+ allIssues.push(...networkRules.issues);
192
+ allIssues.push(...mcpPerms.issues);
193
+ const totalDeduction = sensitiveDirs.deduction + envVars.deduction + networkRules.deduction + mcpPerms.deduction;
194
+ const sandboxScore = Math.max(0, Math.min(10, 10 - totalDeduction));
195
+ const score = Math.round(sandboxScore * 10);
196
+ const label = blastRadiusLabel(sandboxScore);
197
+ const summary = `blast radius score ${sandboxScore.toFixed(1)}/10 — ${label}`;
198
+ return {
199
+ name: 'sandbox',
200
+ score,
201
+ maxScore: 100,
202
+ issues: allIssues,
203
+ summary,
204
+ };
205
+ }
206
+ // ── Subcommand output ────────────────────────────────────────────────────────
207
+ export async function runSandboxCommand(cwd, flags) {
208
+ const result = await checkSandbox(cwd);
209
+ const sandboxScore = result.score / 10;
210
+ if (flags.has('--json')) {
211
+ console.log(JSON.stringify({
212
+ score: sandboxScore,
213
+ scoreOutOf100: result.score,
214
+ maxScore: result.maxScore,
215
+ blastRadius: blastRadiusLabel(sandboxScore),
216
+ issues: result.issues,
217
+ summary: result.summary,
218
+ }, null, 2));
219
+ return;
220
+ }
221
+ console.log(`\n ${c.bold}vet sandbox${c.reset} — agent runtime blast radius\n`);
222
+ // Table header
223
+ const labelW = 30;
224
+ const statusW = 12;
225
+ console.log(` ${c.dim}${'─'.repeat(labelW + statusW + 6)}${c.reset}`);
226
+ console.log(` ${pad('Probe', labelW)} ${pad('Status', statusW)}`);
227
+ console.log(` ${c.dim}${'─'.repeat(labelW + statusW + 6)}${c.reset}`);
228
+ // Probe 1: Sensitive dirs
229
+ const sensitiveDirIssues = result.issues.filter(i => i.message.startsWith('Sensitive directory'));
230
+ const sensitiveDirStatus = sensitiveDirIssues.length === 0
231
+ ? `${c.green}PASS${c.reset}`
232
+ : `${c.red}FAIL (${sensitiveDirIssues.length})${c.reset}`;
233
+ console.log(` ${pad('Sensitive dirs', labelW)} ${sensitiveDirStatus}`);
234
+ for (const issue of sensitiveDirIssues) {
235
+ console.log(` ${c.red}✗${c.reset} ${issue.message}`);
236
+ }
237
+ // Probe 2: Env var leaks
238
+ const envIssues = result.issues.filter(i => i.message.startsWith('Sensitive env var'));
239
+ const envStatus = envIssues.length === 0
240
+ ? `${c.green}PASS${c.reset}`
241
+ : `${c.yellow}WARN (${envIssues.length})${c.reset}`;
242
+ console.log(` ${pad('Env var exposure', labelW)} ${envStatus}`);
243
+ for (const issue of envIssues.slice(0, 5)) {
244
+ console.log(` ${c.yellow}⚠${c.reset} ${issue.message}`);
245
+ }
246
+ if (envIssues.length > 5) {
247
+ console.log(` ${c.dim}... and ${envIssues.length - 5} more${c.reset}`);
248
+ }
249
+ // Probe 3: Network rules
250
+ const netIssues = result.issues.filter(i => i.message.includes('network restriction'));
251
+ const netStatus = netIssues.length === 0
252
+ ? `${c.green}PASS${c.reset}`
253
+ : `${c.yellow}WARN${c.reset}`;
254
+ console.log(` ${pad('Network restrictions', labelW)} ${netStatus}`);
255
+ for (const issue of netIssues) {
256
+ console.log(` ${c.yellow}⚠${c.reset} ${issue.message}`);
257
+ }
258
+ // Probe 4: MCP permissions
259
+ const mcpIssues = result.issues.filter(i => i.message.includes('MCP'));
260
+ const mcpInfoIssues = result.issues.filter(i => i.message.includes('.claude/settings.json'));
261
+ const mcpStatus = mcpIssues.length > 0
262
+ ? `${c.red}FAIL (${mcpIssues.length})${c.reset}`
263
+ : mcpInfoIssues.length > 0
264
+ ? `${c.dim}N/A${c.reset}`
265
+ : `${c.green}PASS${c.reset}`;
266
+ console.log(` ${pad('MCP permissions', labelW)} ${mcpStatus}`);
267
+ for (const issue of mcpIssues) {
268
+ console.log(` ${c.red}✗${c.reset} ${issue.message}`);
269
+ }
270
+ console.log(` ${c.dim}${'─'.repeat(labelW + statusW + 6)}${c.reset}`);
271
+ // Score
272
+ const scoreColor = sandboxScore >= 7 ? c.green : sandboxScore >= 4 ? c.yellow : c.red;
273
+ console.log(`\n blast radius score ${scoreColor}${sandboxScore.toFixed(1)}/10${c.reset}`);
274
+ console.log(` if compromised ${blastRadiusLabel(sandboxScore)}\n`);
275
+ }
276
+ // ── String helpers ───────────────────────────────────────────────────────────
277
+ function pad(s, w) {
278
+ const clean = s.replace(/\x1b\[[0-9;]*m/g, '');
279
+ return s + ' '.repeat(Math.max(0, w - clean.length));
280
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkSourceSecurity(cwd: string): CheckResult;
@@ -0,0 +1,153 @@
1
+ import { join, relative } from 'node:path';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import { cachedReadFile as cachedRead } from '../file-cache.js';
4
+ const SOURCE_PATTERNS = [
5
+ {
6
+ id: 'eval',
7
+ regex: /\beval\s*\(/,
8
+ severity: 'error',
9
+ message: 'eval() usage — arbitrary code execution risk',
10
+ },
11
+ {
12
+ id: 'exec-sync',
13
+ regex: /\bexecSync\s*\(|\bexecFileSync\s*\(/,
14
+ severity: 'warning',
15
+ message: 'execSync/execFileSync — synchronous shell execution, injection risk if user input flows in',
16
+ },
17
+ {
18
+ id: 'child-process-exec',
19
+ regex: /\brequire\s*\(\s*['"]child_process['"]\s*\)/,
20
+ severity: 'warning',
21
+ message: 'child_process require — verify no untrusted input reaches shell commands',
22
+ },
23
+ {
24
+ id: 'function-constructor',
25
+ regex: /new\s+Function\s*\(/,
26
+ severity: 'error',
27
+ message: 'new Function() — dynamic code generation, equivalent to eval()',
28
+ },
29
+ {
30
+ id: 'innerhtml',
31
+ regex: /\.innerHTML\s*=|dangerouslySetInnerHTML/,
32
+ severity: 'warning',
33
+ message: 'innerHTML/dangerouslySetInnerHTML — XSS risk if content is not sanitized',
34
+ },
35
+ {
36
+ id: 'hardcoded-jwt',
37
+ regex: /['"]eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
38
+ severity: 'error',
39
+ message: 'hardcoded JWT token detected',
40
+ },
41
+ {
42
+ id: 'hardcoded-private-key',
43
+ regex: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
44
+ severity: 'error',
45
+ message: 'hardcoded private key detected',
46
+ },
47
+ {
48
+ id: 'disable-tls',
49
+ regex: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|rejectUnauthorized\s*:\s*false/,
50
+ severity: 'error',
51
+ message: 'TLS verification disabled — man-in-the-middle risk',
52
+ },
53
+ {
54
+ id: 'sql-concat',
55
+ regex: /(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)\s+.*\$\{|(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)\s+.*\+\s*(?:req\.|params\.|query\.|body\.)/i,
56
+ severity: 'error',
57
+ message: 'SQL query string concatenation — SQL injection risk',
58
+ },
59
+ ];
60
+ // ── Source file collection ────────────────────────────────────────────────────
61
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
62
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', 'vendor', '__pycache__']);
63
+ const MAX_FILES = 500;
64
+ const MAX_FILE_SIZE = 512 * 1024; // 512KB
65
+ function collectSourceFiles(cwd, maxFiles = MAX_FILES) {
66
+ const files = [];
67
+ function walk(dir, depth) {
68
+ if (depth > 8 || files.length >= maxFiles)
69
+ return;
70
+ try {
71
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
72
+ if (files.length >= maxFiles)
73
+ break;
74
+ if (entry.isDirectory()) {
75
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
76
+ continue;
77
+ walk(join(dir, entry.name), depth + 1);
78
+ }
79
+ else if (entry.isFile()) {
80
+ const ext = entry.name.slice(entry.name.lastIndexOf('.'));
81
+ if (SOURCE_EXTENSIONS.has(ext)) {
82
+ const full = join(dir, entry.name);
83
+ try {
84
+ if (statSync(full).size <= MAX_FILE_SIZE) {
85
+ files.push(full);
86
+ }
87
+ }
88
+ catch { /* skip */ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ catch { /* skip */ }
94
+ }
95
+ walk(cwd, 0);
96
+ return files;
97
+ }
98
+ // ── Main check ───────────────────────────────────────────────────────────────
99
+ export function checkSourceSecurity(cwd) {
100
+ const files = collectSourceFiles(cwd);
101
+ const issues = [];
102
+ for (const filePath of files) {
103
+ try {
104
+ const content = cachedRead(filePath);
105
+ if (!content)
106
+ continue;
107
+ const relPath = relative(cwd, filePath);
108
+ const lines = content.split('\n');
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const line = lines[i];
111
+ // Skip comments
112
+ const trimmed = line.trim();
113
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
114
+ continue;
115
+ // Skip test files for some patterns
116
+ const isTest = relPath.includes('.test.') || relPath.includes('.spec.') || relPath.includes('__tests__');
117
+ for (const pattern of SOURCE_PATTERNS) {
118
+ // execSync in util files and non-test is fine for CLI tools — only flag in src/
119
+ if (pattern.id === 'exec-sync' && !relPath.startsWith('src/'))
120
+ continue;
121
+ // Skip innerHTML in test files
122
+ if (pattern.id === 'innerhtml' && isTest)
123
+ continue;
124
+ if (pattern.regex.test(line)) {
125
+ pattern.regex.lastIndex = 0;
126
+ issues.push({
127
+ severity: pattern.severity,
128
+ message: pattern.message,
129
+ file: relPath,
130
+ line: i + 1,
131
+ fixable: false,
132
+ });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ catch { /* skip */ }
138
+ }
139
+ const errors = issues.filter(i => i.severity === 'error').length;
140
+ const warnings = issues.filter(i => i.severity === 'warning').length;
141
+ const score = Math.max(0, 100 - errors * 30 - warnings * 10);
142
+ return {
143
+ name: 'source-security',
144
+ score,
145
+ maxScore: 100,
146
+ issues,
147
+ summary: files.length === 0
148
+ ? 'no source files found'
149
+ : issues.length === 0
150
+ ? `${files.length} source files scanned, clean`
151
+ : `${issues.length} security finding${issues.length !== 1 ? 's' : ''} in source code`,
152
+ };
153
+ }
package/dist/cli.js CHANGED
@@ -30,9 +30,11 @@ import { checkSubsidy, runSubsidyCommand } from './checks/subsidy.js';
30
30
  import { checkLoop, runLoopCommand } from './checks/loop.js';
31
31
  import { checkBloat, runBloatCommand } from './checks/bloat.js';
32
32
  import { checkGuard, runGuardCommand } from './checks/guard.js';
33
+ import { checkSandbox, runSandboxCommand } from './checks/sandbox.js';
33
34
  import { checkExplain, runExplainCommand } from './checks/explain.js';
34
35
  import { checkContext, runContextCommand } from './checks/context.js';
35
36
  import { checkSplit, runSplitCommand } from './checks/split.js';
37
+ import { checkSourceSecurity } from './checks/source-security.js';
36
38
  import { checkCompleteness } from './checks/completeness.js';
37
39
  import { score } from './scorer.js';
38
40
  import { toGrade } from './categories.js';
@@ -91,6 +93,7 @@ if (flags.has('--help') || flags.has('-h')) {
91
93
  npx @safetnsr/vet explain [--since REF] [--verbose] [--json] risk-tier agent changes
92
94
  npx @safetnsr/vet context [dir] audit agent context files for token cost + stale sections
93
95
  npx @safetnsr/vet split [--since HEAD~1] [--apply] [--force] [--json] split AI mega-commits into atomic commits
96
+ npx @safetnsr/vet sandbox [dir] score agent runtime blast radius
94
97
 
95
98
  ${c.dim}categories:${c.reset}
96
99
  security (30%) scan, secrets, config, model usage
@@ -127,7 +130,7 @@ if (flags.has('--version') || flags.has('-v')) {
127
130
  }
128
131
  process.exit(0);
129
132
  }
130
- const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context', 'split'];
133
+ const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context', 'split', 'sandbox'];
131
134
  const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
132
135
  const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
133
136
  const isCI = flags.has('--ci');
@@ -305,6 +308,16 @@ if (command === 'explain') {
305
308
  }
306
309
  process.exit(0);
307
310
  }
311
+ if (command === 'sandbox') {
312
+ try {
313
+ await runSandboxCommand(cwd, flags);
314
+ }
315
+ catch (e) {
316
+ console.error(`${c.red}sandbox failed:${c.reset}`, e instanceof Error ? e.message : e);
317
+ process.exit(1);
318
+ }
319
+ process.exit(0);
320
+ }
308
321
  if (!isGitRepo(cwd)) {
309
322
  console.error(`${c.red}not a git repository${c.reset}. vet operates on git repos.`);
310
323
  process.exit(1);
@@ -406,8 +419,9 @@ async function runChecks() {
406
419
  }
407
420
  }
408
421
  // Run ALL independent checks in parallel
409
- const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult, contextResult, splitResult,] = await Promise.all([
422
+ const [scanResult, sourceSecurityResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult, contextResult, splitResult, sandboxResult,] = await Promise.all([
410
423
  withTimeout('scan', () => checkScan(cwd)),
424
+ withTimeout('source-security', () => checkSourceSecurity(cwd)),
411
425
  withTimeout('secrets', () => checkSecrets(cwd)),
412
426
  withTimeout('config', () => checkConfig(cwd, ignore)),
413
427
  withTimeout('models', () => checkModels(cwd, ignore)),
@@ -436,6 +450,7 @@ async function runChecks() {
436
450
  withTimeout('clones', () => checkClones(cwd), 60_000),
437
451
  withTimeout('context', () => checkContext(cwd)),
438
452
  withTimeout('split', () => checkSplit(cwd)),
453
+ withTimeout('sandbox', () => checkSandbox(cwd)),
439
454
  ]);
440
455
  // Git-dependent checks (diff + history) — parallel with each other
441
456
  const [diffResult, historyResult] = await Promise.all([
@@ -445,7 +460,7 @@ async function runChecks() {
445
460
  // Clear file cache after all checks complete
446
461
  clearCache();
447
462
  return score(cwd, {
448
- security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult, guardResult],
463
+ security: [scanResult, sourceSecurityResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult, guardResult, sandboxResult],
449
464
  integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, explainResult],
450
465
  debt: [readyResult, historyResult, debtResult, bloatResult, clonesResult, splitResult],
451
466
  deps: [depsResult],
@@ -1 +1,6 @@
1
- export declare function bad(): void;
1
+ export declare function fetchData(url: any): Promise<any>;
2
+ export declare function processItems(items: any): any[];
3
+ export declare const exec: any;
4
+ export declare function runCmd(cmd: string): any;
5
+ export declare function deepClone(obj: any): any;
6
+ export declare function dangerousEval(code: string): any;
package/dist/utils-bad.js CHANGED
@@ -1,6 +1,39 @@
1
- const SECRET = "sk-abc123456789";
2
- // TODO: remove this hack
3
- export function bad() { try {
4
- throw new Error();
1
+ // TODO: clean this up eventually
2
+ // HACK: workaround for broken API
3
+ const API_KEY = "sk-proj-abc123secretkey456def789";
4
+ const DB_PASSWORD = "admin123!@#";
5
+ export async function fetchData(url) {
6
+ // @ts-ignore
7
+ const res = await fetch(url);
8
+ const data = await res.json();
9
+ try {
10
+ return data;
11
+ }
12
+ catch (e) {
13
+ // swallow error silently
14
+ }
15
+ }
16
+ export function processItems(items) {
17
+ var result = [];
18
+ for (var i = 0; i < items.length; i++) {
19
+ for (var j = 0; j < items[i].children.length; j++) {
20
+ for (var k = 0; k < items[i].children[j].values.length; k++) {
21
+ if (items[i].children[j].values[k] !== null && items[i].children[j].values[k] !== undefined) {
22
+ result.push(items[i].children[j].values[k]);
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+ export const exec = require('child_process').execSync;
30
+ export function runCmd(cmd) {
31
+ return exec(cmd).toString();
32
+ }
33
+ // copied from stackoverflow
34
+ export function deepClone(obj) {
35
+ return JSON.parse(JSON.stringify(obj));
36
+ }
37
+ export function dangerousEval(code) {
38
+ return eval(code);
5
39
  }
6
- catch (e) { } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {