@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.
- package/dist/checks/sandbox.d.ts +3 -0
- package/dist/checks/sandbox.js +280 -0
- package/dist/checks/source-security.d.ts +2 -0
- package/dist/checks/source-security.js +153 -0
- package/dist/cli.js +18 -3
- package/dist/utils-bad.d.ts +6 -1
- package/dist/utils-bad.js +38 -5
- package/package.json +1 -1
|
@@ -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,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],
|
package/dist/utils-bad.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export declare function
|
|
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
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
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) { } }
|