@safetnsr/vet 0.6.0 → 1.0.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/README.md +2 -1
- package/dist/categories.d.ts +9 -0
- package/dist/categories.js +79 -0
- package/dist/checks/config.js +8 -8
- package/dist/checks/debt.js +9 -9
- package/dist/checks/deps.js +5 -5
- package/dist/checks/diff.js +4 -4
- package/dist/checks/history.js +4 -4
- package/dist/checks/integrity.d.ts +2 -0
- package/dist/checks/integrity.js +317 -0
- package/dist/checks/map.d.ts +25 -0
- package/dist/checks/map.js +256 -0
- package/dist/checks/models.js +6 -6
- package/dist/checks/owasp.d.ts +2 -0
- package/dist/checks/owasp.js +794 -0
- package/dist/checks/ready.js +7 -7
- package/dist/checks/receipt.js +5 -5
- package/dist/checks/scan.js +3 -3
- package/dist/checks/secrets.js +4 -4
- package/dist/cli.js +76 -47
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +56 -25
- package/dist/scorer.d.ts +7 -1
- package/dist/scorer.js +4 -14
- package/dist/types.d.ts +11 -1
- package/package.json +2 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { walkFiles, readFile, c } from '../util.js';
|
|
4
|
+
// ── Agent config filenames to discover ───────────────────────────────────────
|
|
5
|
+
export const AGENT_CONFIG_FILES = [
|
|
6
|
+
'CLAUDE.md',
|
|
7
|
+
'AGENTS.md',
|
|
8
|
+
'.cursorrules',
|
|
9
|
+
'codex.md',
|
|
10
|
+
'.github/copilot-instructions.md',
|
|
11
|
+
'cursor.json',
|
|
12
|
+
'.cursor/rules',
|
|
13
|
+
'copilot-instructions.md',
|
|
14
|
+
];
|
|
15
|
+
// ── Parse all agent config files present in cwd ──────────────────────────────
|
|
16
|
+
export function parseAgentConfigs(cwd) {
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const name of AGENT_CONFIG_FILES) {
|
|
19
|
+
const full = join(cwd, name);
|
|
20
|
+
if (existsSync(full)) {
|
|
21
|
+
found.push(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
// ── Extract file/dir references from config file content ─────────────────────
|
|
27
|
+
export function extractRefs(content, cwd) {
|
|
28
|
+
const refs = new Set();
|
|
29
|
+
// Patterns to extract:
|
|
30
|
+
// 1. Backtick paths: `path/to/file.ts` or `./path`
|
|
31
|
+
const backtickPat = /`([^`\s]+)`/g;
|
|
32
|
+
// 2. Inline code in markdown: single-line code with path-like content
|
|
33
|
+
const codePat = /`([./][^`\s]+)`/g;
|
|
34
|
+
// 3. Explicit path patterns in text: word/word or ./word or ~/word
|
|
35
|
+
const pathPat = /(?:^|\s)((?:\.\/|\.\.\/|~\/)?(?:[a-zA-Z0-9_-]+\/)+[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])/gm;
|
|
36
|
+
// 4. Absolute paths starting with /
|
|
37
|
+
const absPat = /(?:^|\s)(\/(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+)/gm;
|
|
38
|
+
const extractFromPattern = (pat) => {
|
|
39
|
+
let match;
|
|
40
|
+
pat.lastIndex = 0;
|
|
41
|
+
while ((match = pat.exec(content)) !== null) {
|
|
42
|
+
const raw = match[1].trim();
|
|
43
|
+
// Skip URLs
|
|
44
|
+
if (raw.startsWith('http://') || raw.startsWith('https://') || raw.includes('://'))
|
|
45
|
+
continue;
|
|
46
|
+
// Skip if looks like a domain
|
|
47
|
+
if (/^[a-z]+\.[a-z]{2,}/.test(raw) && !raw.includes('/'))
|
|
48
|
+
continue;
|
|
49
|
+
refs.add(raw);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
extractFromPattern(backtickPat);
|
|
53
|
+
extractFromPattern(codePat);
|
|
54
|
+
extractFromPattern(pathPat);
|
|
55
|
+
extractFromPattern(absPat);
|
|
56
|
+
// Filter to only refs that actually exist on disk (relative to cwd)
|
|
57
|
+
const resolved = [];
|
|
58
|
+
for (const ref of refs) {
|
|
59
|
+
let resolvedPath;
|
|
60
|
+
if (ref.startsWith('/')) {
|
|
61
|
+
resolvedPath = ref;
|
|
62
|
+
}
|
|
63
|
+
else if (ref.startsWith('~/')) {
|
|
64
|
+
resolvedPath = join(process.env.HOME || '/root', ref.slice(2));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
resolvedPath = join(cwd, ref);
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(resolvedPath)) {
|
|
70
|
+
// Store as relative to cwd
|
|
71
|
+
const rel = ref.startsWith('/') ? ref : ref;
|
|
72
|
+
resolved.push(rel.replace(/^\.\//, ''));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...new Set(resolved)];
|
|
76
|
+
}
|
|
77
|
+
// ── Classify all codebase files ───────────────────────────────────────────────
|
|
78
|
+
export function classifyFiles(cwd, configPaths, refs) {
|
|
79
|
+
const allFiles = walkFiles(cwd);
|
|
80
|
+
const configSet = new Set(configPaths);
|
|
81
|
+
// Build a set of ref prefixes for directory matching
|
|
82
|
+
const refSet = new Set(refs);
|
|
83
|
+
// Also include files whose parent directory is referenced
|
|
84
|
+
function isReferencedByRef(file) {
|
|
85
|
+
if (refSet.has(file))
|
|
86
|
+
return true;
|
|
87
|
+
// Check if any ref is a directory prefix of this file
|
|
88
|
+
for (const ref of refSet) {
|
|
89
|
+
if (file.startsWith(ref + '/') || file.startsWith(ref.replace(/\/$/, '') + '/')) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const classified = [];
|
|
96
|
+
for (const file of allFiles) {
|
|
97
|
+
let tier;
|
|
98
|
+
if (configSet.has(file)) {
|
|
99
|
+
tier = 'config';
|
|
100
|
+
}
|
|
101
|
+
else if (isReferencedByRef(file)) {
|
|
102
|
+
tier = 'visible';
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
tier = 'invisible';
|
|
106
|
+
}
|
|
107
|
+
classified.push({ path: file, tier });
|
|
108
|
+
}
|
|
109
|
+
// Also add config files that walkFiles might have missed (e.g. .github/copilot-instructions.md)
|
|
110
|
+
for (const cp of configPaths) {
|
|
111
|
+
if (!classified.find(f => f.path === cp)) {
|
|
112
|
+
classified.push({ path: cp, tier: 'config' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return classified;
|
|
116
|
+
}
|
|
117
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
118
|
+
export async function checkMap(cwd) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
// Discover agent configs
|
|
121
|
+
const configPaths = parseAgentConfigs(cwd);
|
|
122
|
+
// Extract all refs from all config files
|
|
123
|
+
const allRefs = [];
|
|
124
|
+
for (const cp of configPaths) {
|
|
125
|
+
const content = readFile(join(cwd, cp));
|
|
126
|
+
if (content) {
|
|
127
|
+
const refs = extractRefs(content, cwd);
|
|
128
|
+
allRefs.push(...refs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const uniqueRefs = [...new Set(allRefs)];
|
|
132
|
+
// Classify files
|
|
133
|
+
const classified = classifyFiles(cwd, configPaths, uniqueRefs);
|
|
134
|
+
const configFiles = classified.filter(f => f.tier === 'config').map(f => f.path);
|
|
135
|
+
const visibleFiles = classified.filter(f => f.tier === 'visible').map(f => f.path);
|
|
136
|
+
const invisibleFiles = classified.filter(f => f.tier === 'invisible').map(f => f.path);
|
|
137
|
+
const total = classified.length;
|
|
138
|
+
const visible = visibleFiles.length + configFiles.length;
|
|
139
|
+
const visible_pct = total > 0 ? Math.round((visible / total) * 100) : 0;
|
|
140
|
+
// Issues
|
|
141
|
+
if (configPaths.length === 0) {
|
|
142
|
+
issues.push({
|
|
143
|
+
severity: 'warning',
|
|
144
|
+
message: 'no agent config files found (CLAUDE.md, .cursorrules, etc.) — agent has no guided context',
|
|
145
|
+
fixable: true,
|
|
146
|
+
fixHint: 'run: npx @safetnsr/vet init',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (visible_pct < 20 && total > 0) {
|
|
150
|
+
issues.push({
|
|
151
|
+
severity: 'warning',
|
|
152
|
+
message: `agent is mostly blind: only ${visible_pct}% of codebase is referenced in agent configs`,
|
|
153
|
+
fixable: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Surface top invisible dirs as info
|
|
157
|
+
const invisibleDirs = new Set();
|
|
158
|
+
for (const f of invisibleFiles) {
|
|
159
|
+
const parts = f.split('/');
|
|
160
|
+
if (parts.length > 1)
|
|
161
|
+
invisibleDirs.add(parts[0]);
|
|
162
|
+
}
|
|
163
|
+
const topInvisibleDirs = [...invisibleDirs].slice(0, 5);
|
|
164
|
+
if (topInvisibleDirs.length > 0 && invisibleFiles.length > 0) {
|
|
165
|
+
issues.push({
|
|
166
|
+
severity: 'info',
|
|
167
|
+
message: `top invisible directories: ${topInvisibleDirs.join(', ')}`,
|
|
168
|
+
fixable: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const mapData = {
|
|
172
|
+
config: configFiles,
|
|
173
|
+
visible: visibleFiles,
|
|
174
|
+
invisible: invisibleFiles,
|
|
175
|
+
stats: { total, visible_pct },
|
|
176
|
+
};
|
|
177
|
+
const summary = configPaths.length === 0
|
|
178
|
+
? `no agent config files — all ${total} files invisible`
|
|
179
|
+
: `${visible_pct}% visible to agent (${visible}/${total} files)`;
|
|
180
|
+
return {
|
|
181
|
+
name: 'map',
|
|
182
|
+
score: visible_pct,
|
|
183
|
+
maxScore: 100,
|
|
184
|
+
issues,
|
|
185
|
+
summary,
|
|
186
|
+
mapData,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ── Terminal renderer ─────────────────────────────────────────────────────────
|
|
190
|
+
export function renderMapReport(result, asJson) {
|
|
191
|
+
const { mapData } = result;
|
|
192
|
+
if (asJson) {
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
config: mapData.config,
|
|
195
|
+
visible: mapData.visible,
|
|
196
|
+
invisible: mapData.invisible,
|
|
197
|
+
stats: mapData.stats,
|
|
198
|
+
}, null, 2);
|
|
199
|
+
}
|
|
200
|
+
const lines = [];
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push(` ${c.bold}vet map${c.reset} — agent visibility\n`);
|
|
203
|
+
lines.push(` ${c.dim}score:${c.reset} ${c.bold}${mapData.stats.visible_pct}%${c.reset} visible to agent`);
|
|
204
|
+
lines.push(` ${c.dim}total:${c.reset} ${mapData.stats.total} files`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
// Config files tier
|
|
207
|
+
if (mapData.config.length > 0) {
|
|
208
|
+
lines.push(` ${c.yellow}${c.bold}config files${c.reset} ${c.dim}(${mapData.config.length})${c.reset}`);
|
|
209
|
+
for (const f of mapData.config.slice(0, 10)) {
|
|
210
|
+
lines.push(` ${c.yellow}●${c.reset} ${c.dim}${f}${c.reset}`);
|
|
211
|
+
}
|
|
212
|
+
if (mapData.config.length > 10) {
|
|
213
|
+
lines.push(` ${c.dim} ... and ${mapData.config.length - 10} more${c.reset}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push('');
|
|
216
|
+
}
|
|
217
|
+
// Visible files tier
|
|
218
|
+
if (mapData.visible.length > 0) {
|
|
219
|
+
lines.push(` ${c.green}${c.bold}visible to agent${c.reset} ${c.dim}(${mapData.visible.length})${c.reset}`);
|
|
220
|
+
for (const f of mapData.visible.slice(0, 15)) {
|
|
221
|
+
lines.push(` ${c.green}●${c.reset} ${f}`);
|
|
222
|
+
}
|
|
223
|
+
if (mapData.visible.length > 15) {
|
|
224
|
+
lines.push(` ${c.dim} ... and ${mapData.visible.length - 15} more${c.reset}`);
|
|
225
|
+
}
|
|
226
|
+
lines.push('');
|
|
227
|
+
}
|
|
228
|
+
// Invisible dirs summary
|
|
229
|
+
if (mapData.invisible.length > 0) {
|
|
230
|
+
const invisibleDirs = new Map();
|
|
231
|
+
for (const f of mapData.invisible) {
|
|
232
|
+
const parts = f.split('/');
|
|
233
|
+
const dir = parts.length > 1 ? parts[0] : '(root)';
|
|
234
|
+
invisibleDirs.set(dir, (invisibleDirs.get(dir) || 0) + 1);
|
|
235
|
+
}
|
|
236
|
+
const sortedDirs = [...invisibleDirs.entries()].sort((a, b) => b[1] - a[1]);
|
|
237
|
+
lines.push(` ${c.dim}${c.bold}invisible to agent${c.reset} ${c.dim}(${mapData.invisible.length} files)${c.reset}`);
|
|
238
|
+
for (const [dir, count] of sortedDirs.slice(0, 8)) {
|
|
239
|
+
lines.push(` ${c.dim}○ ${dir}/ (${count} files)${c.reset}`);
|
|
240
|
+
}
|
|
241
|
+
if (sortedDirs.length > 8) {
|
|
242
|
+
lines.push(` ${c.dim} ... and ${sortedDirs.length - 8} more directories${c.reset}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
// Issues
|
|
247
|
+
for (const issue of result.issues) {
|
|
248
|
+
const icon = issue.severity === 'warning' ? c.yellow + '⚠' : c.dim + 'ℹ';
|
|
249
|
+
lines.push(` ${icon}${c.reset} ${issue.message}`);
|
|
250
|
+
if (issue.fixHint)
|
|
251
|
+
lines.push(` ${c.dim}→ ${issue.fixHint}${c.reset}`);
|
|
252
|
+
}
|
|
253
|
+
if (result.issues.length > 0)
|
|
254
|
+
lines.push('');
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
package/dist/checks/models.js
CHANGED
|
@@ -22,11 +22,11 @@ async function tryModelGraveyard(cwd) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
const score = Math.max(0,
|
|
25
|
+
const score = Math.max(0, 100 - issues.length * 20);
|
|
26
26
|
return {
|
|
27
27
|
name: 'models',
|
|
28
|
-
score: Math.min(
|
|
29
|
-
maxScore:
|
|
28
|
+
score: Math.min(100, score),
|
|
29
|
+
maxScore: 100,
|
|
30
30
|
issues,
|
|
31
31
|
summary: issues.length === 0
|
|
32
32
|
? `${report.filesScanned} files scanned (via model-graveyard) — all current`
|
|
@@ -111,11 +111,11 @@ function builtinModels(cwd, ignore) {
|
|
|
111
111
|
fixHint: `replace "${model}" with "${info.replacement}"`,
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
|
-
const score = Math.max(0,
|
|
114
|
+
const score = Math.max(0, 100 - issues.length * 20);
|
|
115
115
|
return {
|
|
116
116
|
name: 'models',
|
|
117
|
-
score: Math.min(
|
|
118
|
-
maxScore:
|
|
117
|
+
score: Math.min(100, score),
|
|
118
|
+
maxScore: 100,
|
|
119
119
|
issues,
|
|
120
120
|
summary: issues.length === 0 ? 'all model references current' : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`,
|
|
121
121
|
};
|