corpus-core 0.1.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/autofix.d.ts +41 -0
- package/dist/autofix.js +159 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +9 -0
- package/dist/cve-database.json +396 -0
- package/dist/cve-patterns.d.ts +54 -0
- package/dist/cve-patterns.js +124 -0
- package/dist/engine.d.ts +6 -0
- package/dist/engine.js +71 -0
- package/dist/graph-engine.d.ts +56 -0
- package/dist/graph-engine.js +412 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +33 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +12 -0
- package/dist/memory.d.ts +67 -0
- package/dist/memory.js +261 -0
- package/dist/pattern-learner.d.ts +82 -0
- package/dist/pattern-learner.js +420 -0
- package/dist/scanners/code-safety.d.ts +13 -0
- package/dist/scanners/code-safety.js +114 -0
- package/dist/scanners/confidence-calibrator.d.ts +25 -0
- package/dist/scanners/confidence-calibrator.js +58 -0
- package/dist/scanners/context-poisoning.d.ts +18 -0
- package/dist/scanners/context-poisoning.js +48 -0
- package/dist/scanners/cross-user-firewall.d.ts +10 -0
- package/dist/scanners/cross-user-firewall.js +24 -0
- package/dist/scanners/dependency-checker.d.ts +15 -0
- package/dist/scanners/dependency-checker.js +203 -0
- package/dist/scanners/exfiltration-guard.d.ts +19 -0
- package/dist/scanners/exfiltration-guard.js +49 -0
- package/dist/scanners/index.d.ts +12 -0
- package/dist/scanners/index.js +12 -0
- package/dist/scanners/injection-firewall.d.ts +12 -0
- package/dist/scanners/injection-firewall.js +71 -0
- package/dist/scanners/scope-enforcer.d.ts +10 -0
- package/dist/scanners/scope-enforcer.js +30 -0
- package/dist/scanners/secret-detector.d.ts +34 -0
- package/dist/scanners/secret-detector.js +188 -0
- package/dist/scanners/session-hijack.d.ts +16 -0
- package/dist/scanners/session-hijack.js +53 -0
- package/dist/scanners/trust-score.d.ts +34 -0
- package/dist/scanners/trust-score.js +164 -0
- package/dist/scanners/undo-integrity.d.ts +9 -0
- package/dist/scanners/undo-integrity.js +38 -0
- package/dist/subprocess.d.ts +10 -0
- package/dist/subprocess.js +103 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +16 -0
- package/dist/yaml-evaluator.d.ts +12 -0
- package/dist/yaml-evaluator.js +105 -0
- package/package.json +36 -0
- package/src/cve-database.json +396 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus Graph Engine
|
|
3
|
+
*
|
|
4
|
+
* Auto-scans a TypeScript/JavaScript codebase and builds a structural graph.
|
|
5
|
+
* Nodes = functions, modules, classes. Edges = calls, imports, exports.
|
|
6
|
+
* This is the "immune system memory" -- Corpus learns what your codebase looks like.
|
|
7
|
+
*
|
|
8
|
+
* No manual configuration. No YAML to write. It figures it out.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
// ── AST-Free Parser (no dependencies, hackathon-fast) ────────────────────────
|
|
13
|
+
// Uses regex + structural analysis instead of ts-morph to avoid dependency hell.
|
|
14
|
+
// Good enough for function signatures, exports, imports, and guard clauses.
|
|
15
|
+
const FUNCTION_PATTERNS = [
|
|
16
|
+
// export function name(params): returnType
|
|
17
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
|
|
18
|
+
// export const name = (params): returnType =>
|
|
19
|
+
/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)(?:\s*:\s*([^=]+))?\s*=>/g,
|
|
20
|
+
// export const name = function(params)
|
|
21
|
+
/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
|
|
22
|
+
// class method: name(params): returnType
|
|
23
|
+
/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/gm,
|
|
24
|
+
];
|
|
25
|
+
const CLASS_PATTERN = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+(\w+(?:\s*,\s*\w+)*))?/g;
|
|
26
|
+
const IMPORT_PATTERN = /import\s+(?:(?:\{([^}]+)\})|(?:(\w+)))\s+from\s+['"]([^'"]+)['"]/g;
|
|
27
|
+
const EXPORT_PATTERN = /export\s+(?:(?:default\s+)?(?:function|class|const|let|var|async)\s+(\w+)|(?:\{([^}]+)\}))/g;
|
|
28
|
+
const GUARD_PATTERNS = [
|
|
29
|
+
/if\s*\(\s*!(\w+)\s*\)\s*(?:throw|return)/,
|
|
30
|
+
/if\s*\(\s*(\w+)\s*===?\s*(?:null|undefined|false|''|0)\s*\)\s*(?:throw|return)/,
|
|
31
|
+
/if\s*\(\s*!(\w+)\s*\|\|\s*/,
|
|
32
|
+
/(\w+)\s*\?\?\s*(?:throw|return)/,
|
|
33
|
+
];
|
|
34
|
+
function parseFile(filePath, content) {
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
const functions = [];
|
|
37
|
+
const imports = [];
|
|
38
|
+
const exports = [];
|
|
39
|
+
const classes = [];
|
|
40
|
+
// Parse imports
|
|
41
|
+
let match;
|
|
42
|
+
const importRegex = new RegExp(IMPORT_PATTERN.source, 'g');
|
|
43
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
44
|
+
const namedImports = match[1]?.split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean) ?? [];
|
|
45
|
+
const defaultImport = match[2] ? [match[2]] : [];
|
|
46
|
+
imports.push({
|
|
47
|
+
names: [...namedImports, ...defaultImport],
|
|
48
|
+
source: match[3],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Parse exports
|
|
52
|
+
const exportRegex = new RegExp(EXPORT_PATTERN.source, 'g');
|
|
53
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
54
|
+
if (match[1])
|
|
55
|
+
exports.push(match[1]);
|
|
56
|
+
if (match[2]) {
|
|
57
|
+
match[2].split(',').forEach(e => {
|
|
58
|
+
const name = e.trim().split(/\s+as\s+/)[0].trim();
|
|
59
|
+
if (name)
|
|
60
|
+
exports.push(name);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Parse classes
|
|
65
|
+
const classRegex = new RegExp(CLASS_PATTERN.source, 'g');
|
|
66
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
67
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
68
|
+
classes.push({
|
|
69
|
+
name: match[1],
|
|
70
|
+
extends_: match[2] || null,
|
|
71
|
+
line: lineNum,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Parse functions
|
|
75
|
+
for (const pattern of FUNCTION_PATTERNS) {
|
|
76
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
77
|
+
while ((match = regex.exec(content)) !== null) {
|
|
78
|
+
const name = match[1];
|
|
79
|
+
if (!name || name === 'if' || name === 'for' || name === 'while' || name === 'switch')
|
|
80
|
+
continue;
|
|
81
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
82
|
+
const params = match[2]
|
|
83
|
+
? match[2].split(',').map(p => p.trim()).filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
const returnType = match[3]?.trim() || null;
|
|
86
|
+
const exported = content.substring(Math.max(0, match.index - 20), match.index).includes('export') ||
|
|
87
|
+
exports.includes(name);
|
|
88
|
+
// Find guard clauses in the function body (first 10 lines)
|
|
89
|
+
const bodyStart = lineNum - 1;
|
|
90
|
+
const bodyLines = lines.slice(bodyStart, bodyStart + 10).join('\n');
|
|
91
|
+
const guards = [];
|
|
92
|
+
for (const guardPattern of GUARD_PATTERNS) {
|
|
93
|
+
const guardMatch = bodyLines.match(guardPattern);
|
|
94
|
+
if (guardMatch) {
|
|
95
|
+
guards.push(guardMatch[0].trim());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Find function calls in the body (next 50 lines)
|
|
99
|
+
const fullBody = lines.slice(bodyStart, bodyStart + 50).join('\n');
|
|
100
|
+
const callPattern = /(?<!\w)(\w+)\s*\(/g;
|
|
101
|
+
const calls = [];
|
|
102
|
+
let callMatch;
|
|
103
|
+
while ((callMatch = callPattern.exec(fullBody)) !== null) {
|
|
104
|
+
const calledName = callMatch[1];
|
|
105
|
+
if (calledName !== name && !['if', 'for', 'while', 'switch', 'return', 'throw', 'new', 'catch', 'typeof', 'instanceof', 'await', 'async', 'console', 'require', 'import'].includes(calledName)) {
|
|
106
|
+
if (!calls.includes(calledName))
|
|
107
|
+
calls.push(calledName);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
functions.push({
|
|
111
|
+
name,
|
|
112
|
+
params,
|
|
113
|
+
returnType,
|
|
114
|
+
exported,
|
|
115
|
+
line: lineNum,
|
|
116
|
+
guards,
|
|
117
|
+
calls,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { filePath, functions, imports, exports, classes };
|
|
122
|
+
}
|
|
123
|
+
// ── File Discovery ───────────────────────────────────────────────────────────
|
|
124
|
+
const SKIP_DIRS = new Set([
|
|
125
|
+
'node_modules', '.git', 'dist', '.next', '__pycache__', '.corpus',
|
|
126
|
+
'coverage', '.turbo', '.cache', 'build', '.vercel',
|
|
127
|
+
]);
|
|
128
|
+
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
129
|
+
function discoverFiles(rootDir) {
|
|
130
|
+
const files = [];
|
|
131
|
+
function walk(dir) {
|
|
132
|
+
const entries = readdirSync(dir);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith('.'))
|
|
135
|
+
continue;
|
|
136
|
+
const fullPath = path.join(dir, entry);
|
|
137
|
+
try {
|
|
138
|
+
const stat = statSync(fullPath);
|
|
139
|
+
if (stat.isDirectory()) {
|
|
140
|
+
walk(fullPath);
|
|
141
|
+
}
|
|
142
|
+
else if (CODE_EXTENSIONS.has(path.extname(entry))) {
|
|
143
|
+
files.push(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Skip unreadable files
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
walk(rootDir);
|
|
152
|
+
return files;
|
|
153
|
+
}
|
|
154
|
+
// ── Graph Builder ────────────────────────────────────────────────────────────
|
|
155
|
+
export function buildGraph(projectRoot) {
|
|
156
|
+
const files = discoverFiles(projectRoot);
|
|
157
|
+
const nodes = [];
|
|
158
|
+
const edges = [];
|
|
159
|
+
const parsedFiles = [];
|
|
160
|
+
// Parse all files
|
|
161
|
+
for (const filePath of files) {
|
|
162
|
+
try {
|
|
163
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
164
|
+
const parsed = parseFile(filePath, content);
|
|
165
|
+
parsedFiles.push(parsed);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Skip unparseable files
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Build node map for lookups
|
|
172
|
+
const functionMap = new Map(); // functionName -> nodeId
|
|
173
|
+
// Create nodes
|
|
174
|
+
for (const file of parsedFiles) {
|
|
175
|
+
const relPath = path.relative(projectRoot, file.filePath);
|
|
176
|
+
// Module node
|
|
177
|
+
const moduleId = `mod:${relPath}`;
|
|
178
|
+
nodes.push({
|
|
179
|
+
id: moduleId,
|
|
180
|
+
type: 'module',
|
|
181
|
+
name: path.basename(relPath, path.extname(relPath)),
|
|
182
|
+
file: relPath,
|
|
183
|
+
line: 1,
|
|
184
|
+
exported: true,
|
|
185
|
+
params: [],
|
|
186
|
+
returnType: null,
|
|
187
|
+
guards: [],
|
|
188
|
+
health: 'verified',
|
|
189
|
+
trustScore: 100,
|
|
190
|
+
});
|
|
191
|
+
// Function nodes
|
|
192
|
+
for (const fn of file.functions) {
|
|
193
|
+
const nodeId = `fn:${relPath}:${fn.name}`;
|
|
194
|
+
nodes.push({
|
|
195
|
+
id: nodeId,
|
|
196
|
+
type: 'function',
|
|
197
|
+
name: fn.name,
|
|
198
|
+
file: relPath,
|
|
199
|
+
line: fn.line,
|
|
200
|
+
exported: fn.exported,
|
|
201
|
+
params: fn.params,
|
|
202
|
+
returnType: fn.returnType,
|
|
203
|
+
guards: fn.guards,
|
|
204
|
+
health: 'verified',
|
|
205
|
+
trustScore: 100,
|
|
206
|
+
});
|
|
207
|
+
functionMap.set(`${relPath}:${fn.name}`, nodeId);
|
|
208
|
+
if (fn.exported) {
|
|
209
|
+
functionMap.set(fn.name, nodeId);
|
|
210
|
+
}
|
|
211
|
+
// Edge: function belongs to module
|
|
212
|
+
edges.push({ source: moduleId, target: nodeId, type: 'exports' });
|
|
213
|
+
}
|
|
214
|
+
// Class nodes
|
|
215
|
+
for (const cls of file.classes) {
|
|
216
|
+
const nodeId = `cls:${relPath}:${cls.name}`;
|
|
217
|
+
nodes.push({
|
|
218
|
+
id: nodeId,
|
|
219
|
+
type: 'class',
|
|
220
|
+
name: cls.name,
|
|
221
|
+
file: relPath,
|
|
222
|
+
line: cls.line,
|
|
223
|
+
exported: file.exports.includes(cls.name),
|
|
224
|
+
params: [],
|
|
225
|
+
returnType: null,
|
|
226
|
+
guards: [],
|
|
227
|
+
health: 'verified',
|
|
228
|
+
trustScore: 100,
|
|
229
|
+
});
|
|
230
|
+
if (cls.extends_) {
|
|
231
|
+
// We'll resolve the edge after all nodes are created
|
|
232
|
+
edges.push({ source: nodeId, target: `cls:*:${cls.extends_}`, type: 'extends' });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Import edges (module -> module)
|
|
236
|
+
for (const imp of file.imports) {
|
|
237
|
+
if (imp.source.startsWith('.')) {
|
|
238
|
+
// Local import: resolve relative path
|
|
239
|
+
const targetPath = resolveImportPath(file.filePath, imp.source, projectRoot);
|
|
240
|
+
if (targetPath) {
|
|
241
|
+
const targetModuleId = `mod:${path.relative(projectRoot, targetPath)}`;
|
|
242
|
+
edges.push({ source: moduleId, target: targetModuleId, type: 'imports' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Build call edges
|
|
248
|
+
for (const file of parsedFiles) {
|
|
249
|
+
const relPath = path.relative(projectRoot, file.filePath);
|
|
250
|
+
for (const fn of file.functions) {
|
|
251
|
+
const sourceId = `fn:${relPath}:${fn.name}`;
|
|
252
|
+
for (const calledName of fn.calls) {
|
|
253
|
+
// Try to resolve the called function
|
|
254
|
+
const targetId = functionMap.get(`${relPath}:${calledName}`) ||
|
|
255
|
+
functionMap.get(calledName);
|
|
256
|
+
if (targetId) {
|
|
257
|
+
edges.push({ source: sourceId, target: targetId, type: 'calls' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Resolve wildcard class extensions
|
|
263
|
+
for (const edge of edges) {
|
|
264
|
+
if (edge.target.startsWith('cls:*:')) {
|
|
265
|
+
const className = edge.target.replace('cls:*:', '');
|
|
266
|
+
const resolved = nodes.find(n => n.type === 'class' && n.name === className);
|
|
267
|
+
if (resolved) {
|
|
268
|
+
edge.target = resolved.id;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Compute health score
|
|
273
|
+
const totalFunctions = nodes.filter(n => n.type === 'function').length;
|
|
274
|
+
const totalExports = nodes.filter(n => n.exported && n.type === 'function').length;
|
|
275
|
+
const healthScore = nodes.length > 0
|
|
276
|
+
? Math.round(nodes.reduce((sum, n) => sum + n.trustScore, 0) / nodes.length)
|
|
277
|
+
: 100;
|
|
278
|
+
return {
|
|
279
|
+
version: 1,
|
|
280
|
+
created: new Date().toISOString(),
|
|
281
|
+
updated: new Date().toISOString(),
|
|
282
|
+
projectRoot,
|
|
283
|
+
nodes,
|
|
284
|
+
edges,
|
|
285
|
+
stats: {
|
|
286
|
+
totalFiles: files.length,
|
|
287
|
+
totalFunctions,
|
|
288
|
+
totalExports,
|
|
289
|
+
healthScore,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function resolveImportPath(fromFile, importPath, projectRoot) {
|
|
294
|
+
const dir = path.dirname(fromFile);
|
|
295
|
+
const candidates = [
|
|
296
|
+
path.resolve(dir, importPath + '.ts'),
|
|
297
|
+
path.resolve(dir, importPath + '.tsx'),
|
|
298
|
+
path.resolve(dir, importPath + '.js'),
|
|
299
|
+
path.resolve(dir, importPath + '.jsx'),
|
|
300
|
+
path.resolve(dir, importPath, 'index.ts'),
|
|
301
|
+
path.resolve(dir, importPath, 'index.js'),
|
|
302
|
+
];
|
|
303
|
+
for (const candidate of candidates) {
|
|
304
|
+
if (existsSync(candidate))
|
|
305
|
+
return candidate;
|
|
306
|
+
}
|
|
307
|
+
// Try without extension (might already have one)
|
|
308
|
+
if (existsSync(path.resolve(dir, importPath))) {
|
|
309
|
+
return path.resolve(dir, importPath);
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
export function diffFile(graph, filePath, newContent) {
|
|
314
|
+
const relPath = path.relative(graph.projectRoot, filePath);
|
|
315
|
+
const oldNodes = graph.nodes.filter(n => n.file === relPath && n.type === 'function');
|
|
316
|
+
const parsed = parseFile(filePath, newContent);
|
|
317
|
+
const newFunctions = parsed.functions;
|
|
318
|
+
const added = [];
|
|
319
|
+
const removed = [];
|
|
320
|
+
const modified = [];
|
|
321
|
+
const violations = [];
|
|
322
|
+
// Find removed functions
|
|
323
|
+
for (const oldNode of oldNodes) {
|
|
324
|
+
const stillExists = newFunctions.find(f => f.name === oldNode.name);
|
|
325
|
+
if (!stillExists) {
|
|
326
|
+
removed.push(oldNode);
|
|
327
|
+
if (oldNode.exported) {
|
|
328
|
+
violations.push(`REMOVED: Exported function '${oldNode.name}' was removed from ${relPath}. ` +
|
|
329
|
+
`This function may be used by other modules. Restore it or update all callers.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Find added and modified functions
|
|
334
|
+
for (const newFn of newFunctions) {
|
|
335
|
+
const oldNode = oldNodes.find(n => n.name === newFn.name);
|
|
336
|
+
if (!oldNode) {
|
|
337
|
+
// New function
|
|
338
|
+
added.push({
|
|
339
|
+
id: `fn:${relPath}:${newFn.name}`,
|
|
340
|
+
type: 'function',
|
|
341
|
+
name: newFn.name,
|
|
342
|
+
file: relPath,
|
|
343
|
+
line: newFn.line,
|
|
344
|
+
exported: newFn.exported,
|
|
345
|
+
params: newFn.params,
|
|
346
|
+
returnType: newFn.returnType,
|
|
347
|
+
guards: newFn.guards,
|
|
348
|
+
health: 'uncertain',
|
|
349
|
+
trustScore: 80,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Check for modifications
|
|
354
|
+
const changes = [];
|
|
355
|
+
// Check params changed
|
|
356
|
+
if (JSON.stringify(oldNode.params) !== JSON.stringify(newFn.params)) {
|
|
357
|
+
changes.push(`Parameters changed: [${oldNode.params.join(', ')}] -> [${newFn.params.join(', ')}]`);
|
|
358
|
+
}
|
|
359
|
+
// Check return type changed
|
|
360
|
+
if (oldNode.returnType !== newFn.returnType) {
|
|
361
|
+
changes.push(`Return type changed: ${oldNode.returnType || 'unknown'} -> ${newFn.returnType || 'unknown'}`);
|
|
362
|
+
}
|
|
363
|
+
// Check guard clauses removed (CRITICAL)
|
|
364
|
+
for (const oldGuard of oldNode.guards) {
|
|
365
|
+
if (!newFn.guards.some(g => g.includes(oldGuard.split('(')[0]))) {
|
|
366
|
+
changes.push(`Guard clause REMOVED: ${oldGuard}`);
|
|
367
|
+
violations.push(`GUARD REMOVED: Function '${newFn.name}' in ${relPath} had a safety guard '${oldGuard}' ` +
|
|
368
|
+
`that was removed. This may introduce a security vulnerability. Restore the guard clause.`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (changes.length > 0) {
|
|
372
|
+
modified.push({
|
|
373
|
+
before: oldNode,
|
|
374
|
+
after: {
|
|
375
|
+
...oldNode,
|
|
376
|
+
params: newFn.params,
|
|
377
|
+
returnType: newFn.returnType,
|
|
378
|
+
guards: newFn.guards,
|
|
379
|
+
health: violations.length > 0 ? 'violates' : 'uncertain',
|
|
380
|
+
trustScore: Math.max(0, oldNode.trustScore - (violations.length * 20)),
|
|
381
|
+
},
|
|
382
|
+
changes,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const verdict = violations.length > 0 ? 'VIOLATES' :
|
|
388
|
+
(added.length > 0 || modified.length > 0) ? 'UNCERTAIN' :
|
|
389
|
+
'VERIFIED';
|
|
390
|
+
return { added, removed, modified, verdict, violations };
|
|
391
|
+
}
|
|
392
|
+
// ── Save/Load ────────────────────────────────────────────────────────────────
|
|
393
|
+
export function saveGraph(graph, projectRoot) {
|
|
394
|
+
const corpusDir = path.join(projectRoot, '.corpus');
|
|
395
|
+
if (!existsSync(corpusDir)) {
|
|
396
|
+
mkdirSync(corpusDir, { recursive: true });
|
|
397
|
+
}
|
|
398
|
+
const graphPath = path.join(corpusDir, 'graph.json');
|
|
399
|
+
writeFileSync(graphPath, JSON.stringify(graph, null, 2));
|
|
400
|
+
return graphPath;
|
|
401
|
+
}
|
|
402
|
+
export function loadGraph(projectRoot) {
|
|
403
|
+
const graphPath = path.join(projectRoot, '.corpus', 'graph.json');
|
|
404
|
+
if (!existsSync(graphPath))
|
|
405
|
+
return null;
|
|
406
|
+
try {
|
|
407
|
+
return JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { evaluatePolicies } from './engine.js';
|
|
2
|
+
export type { EvalResult } from './engine.js';
|
|
3
|
+
export { evaluateYamlPolicies, loadPolicyFile } from './yaml-evaluator.js';
|
|
4
|
+
export { runJacWalker } from './subprocess.js';
|
|
5
|
+
export { buildLogEntry, sendLogEntry } from './log.js';
|
|
6
|
+
export * from './types.js';
|
|
7
|
+
export * from './constants.js';
|
|
8
|
+
export * from './scanners/index.js';
|
|
9
|
+
export { buildGraph, diffFile, saveGraph, loadGraph } from './graph-engine.js';
|
|
10
|
+
export type { CodebaseGraph, GraphNode, GraphEdge, GraphDiff } from './graph-engine.js';
|
|
11
|
+
export { checkFile, getHealthSummary } from './autofix.js';
|
|
12
|
+
export type { FixInstruction, ViolationDetail } from './autofix.js';
|
|
13
|
+
export { checkForCVEs, loadCVEPatterns, getCVESummary, type CVEPattern, type CVEFinding } from './cve-patterns.js';
|
|
14
|
+
export { learnFromFindings, getLearnedPatterns, shouldSuppress, getPatternIntelligence, addKnownPackages, categorizeRepo } from './pattern-learner.js';
|
|
15
|
+
export type { PatternSignature, LearnedPatterns } from './pattern-learner.js';
|
|
16
|
+
export { recordMemory, getFlagCount, getRecentViolations, getMemoryStats, getAllMemories, syncToBackboard, getBackboardMemories } from './memory.js';
|
|
17
|
+
export type { MemoryEntry, ImmuneMemory } from './memory.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { evaluatePolicies } from './engine.js';
|
|
2
|
+
export { evaluateYamlPolicies, loadPolicyFile } from './yaml-evaluator.js';
|
|
3
|
+
export { runJacWalker } from './subprocess.js';
|
|
4
|
+
export { buildLogEntry, sendLogEntry } from './log.js';
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export * from './constants.js';
|
|
7
|
+
export * from './scanners/index.js';
|
|
8
|
+
// Corpus Graph Engine (immune system)
|
|
9
|
+
export { buildGraph, diffFile, saveGraph, loadGraph } from './graph-engine.js';
|
|
10
|
+
// Corpus Auto-Fix Engine
|
|
11
|
+
export { checkFile, getHealthSummary } from './autofix.js';
|
|
12
|
+
// CVE Pattern Detection
|
|
13
|
+
export { checkForCVEs, loadCVEPatterns, getCVESummary } from './cve-patterns.js';
|
|
14
|
+
// Corpus Pattern Learner (evolving immune system)
|
|
15
|
+
export { learnFromFindings, getLearnedPatterns, shouldSuppress, getPatternIntelligence, addKnownPackages, categorizeRepo } from './pattern-learner.js';
|
|
16
|
+
// Corpus Immune Memory (Backboard.io + local fallback)
|
|
17
|
+
export { recordMemory, getFlagCount, getRecentViolations, getMemoryStats, getAllMemories, syncToBackboard, getBackboardMemories } from './memory.js';
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ActionLogEntry, PolicyResult, CorpusConfig, CorpusInput } from './types.js';
|
|
2
|
+
export declare function buildLogEntry(input: CorpusInput, result: PolicyResult, verdict: string, userDecision: 'CONFIRMED' | 'CANCELLED' | null, projectSlug: string, durationMs: number): ActionLogEntry;
|
|
3
|
+
/**
|
|
4
|
+
* Fire-and-forget log sender. Never awaited. Never throws.
|
|
5
|
+
* Sends only behavioral metadata, never actionPayload.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sendLogEntry(entry: ActionLogEntry, config: CorpusConfig): void;
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { debug } from './logger.js';
|
|
3
|
+
import { INGEST_PATH } from './constants.js';
|
|
4
|
+
export function buildLogEntry(input, result, verdict, userDecision, projectSlug, durationMs) {
|
|
5
|
+
return {
|
|
6
|
+
id: randomUUID(),
|
|
7
|
+
projectSlug,
|
|
8
|
+
actionType: input.actionType,
|
|
9
|
+
verdict: verdict,
|
|
10
|
+
userDecision,
|
|
11
|
+
policyTriggered: result.policyName,
|
|
12
|
+
blockReason: result.blockReason,
|
|
13
|
+
durationMs,
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fire-and-forget log sender. Never awaited. Never throws.
|
|
19
|
+
* Sends only behavioral metadata, never actionPayload.
|
|
20
|
+
*/
|
|
21
|
+
export function sendLogEntry(entry, config) {
|
|
22
|
+
const url = `${config.apiEndpoint}${INGEST_PATH}`;
|
|
23
|
+
fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'x-corpus-key': config.apiKey,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(entry),
|
|
30
|
+
}).catch((err) => {
|
|
31
|
+
debug('Failed to send log entry', err instanceof Error ? err.message : err);
|
|
32
|
+
});
|
|
33
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function debug(message: string, data?: unknown): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const enabled = process.env.CORPUS_DEBUG === 'true';
|
|
2
|
+
export function debug(message, data) {
|
|
3
|
+
if (!enabled)
|
|
4
|
+
return;
|
|
5
|
+
const prefix = `[corpus ${new Date().toISOString()}]`;
|
|
6
|
+
if (data !== undefined) {
|
|
7
|
+
process.stderr.write(`${prefix} ${message} ${JSON.stringify(data)}\n`);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
process.stderr.write(`${prefix} ${message}\n`);
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus Immune Memory -- Backboard.io Integration
|
|
3
|
+
*
|
|
4
|
+
* Stores behavioral baselines and health snapshots persistently across sessions.
|
|
5
|
+
* The longer you use Corpus, the smarter it gets. Memory powers:
|
|
6
|
+
* - "This function was flagged 3 times before"
|
|
7
|
+
* - Cross-session pattern recognition
|
|
8
|
+
* - Behavioral drift detection over time
|
|
9
|
+
*
|
|
10
|
+
* Falls back to local .corpus/memory.json when Backboard is unavailable.
|
|
11
|
+
*/
|
|
12
|
+
export interface MemoryEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'baseline' | 'violation' | 'fix' | 'pattern';
|
|
15
|
+
timestamp: string;
|
|
16
|
+
file: string;
|
|
17
|
+
functionName?: string;
|
|
18
|
+
content: string;
|
|
19
|
+
metadata: Record<string, unknown>;
|
|
20
|
+
flagCount?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ImmuneMemory {
|
|
23
|
+
entries: MemoryEntry[];
|
|
24
|
+
stats: {
|
|
25
|
+
totalEntries: number;
|
|
26
|
+
totalViolations: number;
|
|
27
|
+
totalFixes: number;
|
|
28
|
+
sessionsTracked: number;
|
|
29
|
+
lastUpdated: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Record a memory entry (violation, fix, baseline, pattern).
|
|
34
|
+
* Stores in Backboard.io if available, always stores locally.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordMemory(projectRoot: string, entry: Omit<MemoryEntry, 'id' | 'timestamp'>): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Get the flag count for a specific function across all sessions.
|
|
39
|
+
* "This function was flagged 3 times before."
|
|
40
|
+
*/
|
|
41
|
+
export declare function getFlagCount(projectRoot: string, file: string, functionName: string): number;
|
|
42
|
+
/**
|
|
43
|
+
* Get recent violations for the dashboard.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getRecentViolations(projectRoot: string, limit?: number): MemoryEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Get immune memory stats for display.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getMemoryStats(projectRoot: string): ImmuneMemory['stats'];
|
|
50
|
+
/**
|
|
51
|
+
* Get all memories (for syncing to Backboard.io or display).
|
|
52
|
+
*/
|
|
53
|
+
export declare function getAllMemories(projectRoot: string): ImmuneMemory;
|
|
54
|
+
/**
|
|
55
|
+
* Get memories stored in Backboard.io for this project.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getBackboardMemories(projectRoot: string): Promise<{
|
|
58
|
+
memories: any[];
|
|
59
|
+
total_count: number;
|
|
60
|
+
} | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Sync local memory to Backboard.io (for initial setup or recovery).
|
|
63
|
+
*/
|
|
64
|
+
export declare function syncToBackboard(projectRoot: string): Promise<{
|
|
65
|
+
synced: number;
|
|
66
|
+
errors: number;
|
|
67
|
+
}>;
|