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,188 @@
|
|
|
1
|
+
// Standard secret patterns (regex-based)
|
|
2
|
+
const SECRET_PATTERNS = [
|
|
3
|
+
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g, severity: 'CRITICAL', message: 'AWS Access Key ID detected' },
|
|
4
|
+
{ name: 'AWS Secret Key', regex: /(?:aws_secret_access_key|secret_key)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi, severity: 'CRITICAL', message: 'AWS Secret Access Key detected' },
|
|
5
|
+
{ name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9_]{36,255}/g, severity: 'CRITICAL', message: 'GitHub token detected' },
|
|
6
|
+
{ name: 'OpenAI API Key', regex: /sk-[A-Za-z0-9]{20,}/g, severity: 'CRITICAL', message: 'OpenAI API key detected' },
|
|
7
|
+
{ name: 'Anthropic API Key', regex: /sk-ant-[A-Za-z0-9-]{20,}/g, severity: 'CRITICAL', message: 'Anthropic API key detected' },
|
|
8
|
+
{ name: 'Stripe Key', regex: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/g, severity: 'CRITICAL', message: 'Stripe API key detected' },
|
|
9
|
+
{ name: 'Database URL', regex: /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/g, severity: 'CRITICAL', message: 'Database connection string with credentials detected' },
|
|
10
|
+
{ name: 'Private Key', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, severity: 'CRITICAL', message: 'Private key detected' },
|
|
11
|
+
{ name: 'Generic API Key Assignment', regex: /(?:api_key|apikey|api_secret|secret_key|auth_token|access_token)\s*[=:]\s*['"]([A-Za-z0-9_\-]{16,})['"](?!\s*(?:\/\/|#)\s*(?:placeholder|example|test|fake|dummy))/gi, severity: 'WARNING', message: 'Possible API key or secret in assignment' },
|
|
12
|
+
{ name: 'Bearer Token', regex: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/g, severity: 'WARNING', message: 'Bearer token detected' },
|
|
13
|
+
{ name: 'Password Assignment', regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"](?!\s*(?:\/\/|#)\s*(?:placeholder|example|test|fake|dummy))/gi, severity: 'WARNING', message: 'Hardcoded password detected' },
|
|
14
|
+
{ name: 'Slack Token', regex: /xox[baprs]-[0-9a-zA-Z-]{10,}/g, severity: 'CRITICAL', message: 'Slack token detected' },
|
|
15
|
+
{ name: 'Twilio Key', regex: /SK[0-9a-fA-F]{32}/g, severity: 'CRITICAL', message: 'Twilio API key detected' },
|
|
16
|
+
{ name: 'SendGrid Key', regex: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, severity: 'CRITICAL', message: 'SendGrid API key detected' },
|
|
17
|
+
];
|
|
18
|
+
// AI-specific patterns (heuristic, not regex-only)
|
|
19
|
+
const AI_PATTERNS = {
|
|
20
|
+
// Env var inlined instead of referenced
|
|
21
|
+
inlinedEnvVar: /(?:const|let|var)\s+\w+\s*=\s*['"](?:postgres|mysql|mongodb|redis|https?):\/\/[^'"]+['"]/g,
|
|
22
|
+
// Real values in .env.example or .env.sample
|
|
23
|
+
realValueInExample: /^[A-Z_]+=(?!your_|<|placeholder|example|changeme|xxx|TODO)[A-Za-z0-9_\-]{16,}/gm,
|
|
24
|
+
// Hardcoded URLs with credentials
|
|
25
|
+
hardcodedCredUrl: /['"]https?:\/\/[^:]+:[^@]+@[^'"]+['"]/g,
|
|
26
|
+
// Disabled SSL
|
|
27
|
+
disabledSsl: /rejectUnauthorized\s*:\s*false/g,
|
|
28
|
+
// Overly permissive CORS
|
|
29
|
+
wildcardCors: /(?:cors|origin)\s*[=:]\s*['"]\*['"]/gi,
|
|
30
|
+
};
|
|
31
|
+
// Known placeholders and test values to SKIP (reduce false positives)
|
|
32
|
+
const PLACEHOLDER_PATTERNS = [
|
|
33
|
+
/^sk-(?:test|fake|dummy|placeholder|example|xxx|your)[_-]/i,
|
|
34
|
+
/^(?:test|fake|dummy|placeholder|example|changeme|TODO|your_)/i,
|
|
35
|
+
/^(?:xxx|aaa|bbb|123|abc)/i,
|
|
36
|
+
/<[A-Z_]+>/, // <YOUR_API_KEY> style
|
|
37
|
+
/^(?:pk_test|sk_test)_/, // Stripe test keys are fine
|
|
38
|
+
];
|
|
39
|
+
function isPlaceholder(value) {
|
|
40
|
+
return PLACEHOLDER_PATTERNS.some((p) => p.test(value));
|
|
41
|
+
}
|
|
42
|
+
function isTestFile(filepath) {
|
|
43
|
+
const lower = filepath.toLowerCase();
|
|
44
|
+
return (lower.includes('test') ||
|
|
45
|
+
lower.includes('spec') ||
|
|
46
|
+
lower.includes('mock') ||
|
|
47
|
+
lower.includes('fixture') ||
|
|
48
|
+
lower.includes('__tests__'));
|
|
49
|
+
}
|
|
50
|
+
function redactValue(value) {
|
|
51
|
+
if (value.length <= 8)
|
|
52
|
+
return '***';
|
|
53
|
+
return value.slice(0, 4) + '...' + value.slice(-4);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Scans file content for secrets and AI-specific security patterns.
|
|
57
|
+
* Returns findings sorted by severity (CRITICAL first).
|
|
58
|
+
*/
|
|
59
|
+
export function detectSecrets(content, filepath, options) {
|
|
60
|
+
const findings = [];
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
const skipTests = options?.skipTests ?? true;
|
|
63
|
+
const skipPlaceholders = options?.skipPlaceholders ?? true;
|
|
64
|
+
if (skipTests && isTestFile(filepath))
|
|
65
|
+
return findings;
|
|
66
|
+
// Standard secret patterns
|
|
67
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
68
|
+
pattern.regex.lastIndex = 0;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
71
|
+
const value = match[1] ?? match[0];
|
|
72
|
+
if (skipPlaceholders && isPlaceholder(value))
|
|
73
|
+
continue;
|
|
74
|
+
const beforeMatch = content.slice(0, match.index);
|
|
75
|
+
const line = beforeMatch.split('\n').length;
|
|
76
|
+
const lastNewline = beforeMatch.lastIndexOf('\n');
|
|
77
|
+
const column = match.index - lastNewline;
|
|
78
|
+
findings.push({
|
|
79
|
+
severity: pattern.severity,
|
|
80
|
+
type: pattern.name,
|
|
81
|
+
value,
|
|
82
|
+
redacted: redactValue(value),
|
|
83
|
+
line,
|
|
84
|
+
column,
|
|
85
|
+
file: filepath,
|
|
86
|
+
message: pattern.message,
|
|
87
|
+
isAiPattern: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// AI-specific: env vars inlined in code
|
|
92
|
+
const isExampleFile = /\.example|\.sample|\.template/i.test(filepath);
|
|
93
|
+
if (!isExampleFile) {
|
|
94
|
+
AI_PATTERNS.inlinedEnvVar.lastIndex = 0;
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = AI_PATTERNS.inlinedEnvVar.exec(content)) !== null) {
|
|
97
|
+
const beforeMatch = content.slice(0, match.index);
|
|
98
|
+
const line = beforeMatch.split('\n').length;
|
|
99
|
+
findings.push({
|
|
100
|
+
severity: 'WARNING',
|
|
101
|
+
type: 'Inlined Environment Variable',
|
|
102
|
+
value: match[0],
|
|
103
|
+
redacted: redactValue(match[0]),
|
|
104
|
+
line,
|
|
105
|
+
column: 0,
|
|
106
|
+
file: filepath,
|
|
107
|
+
message: 'URL with credentials hardcoded in code instead of read from environment variable. AI coding tools often inline values instead of referencing process.env.',
|
|
108
|
+
isAiPattern: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// AI-specific: real values in .env.example
|
|
113
|
+
if (isExampleFile) {
|
|
114
|
+
AI_PATTERNS.realValueInExample.lastIndex = 0;
|
|
115
|
+
let match;
|
|
116
|
+
while ((match = AI_PATTERNS.realValueInExample.exec(content)) !== null) {
|
|
117
|
+
const beforeMatch = content.slice(0, match.index);
|
|
118
|
+
const line = beforeMatch.split('\n').length;
|
|
119
|
+
findings.push({
|
|
120
|
+
severity: 'WARNING',
|
|
121
|
+
type: 'Real Value in Example File',
|
|
122
|
+
value: match[0],
|
|
123
|
+
redacted: redactValue(match[0]),
|
|
124
|
+
line,
|
|
125
|
+
column: 0,
|
|
126
|
+
file: filepath,
|
|
127
|
+
message: 'Example/template file contains what looks like a real value instead of a placeholder. AI often copies real .env values into example files.',
|
|
128
|
+
isAiPattern: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// AI-specific: disabled SSL
|
|
133
|
+
AI_PATTERNS.disabledSsl.lastIndex = 0;
|
|
134
|
+
let sslMatch;
|
|
135
|
+
while ((sslMatch = AI_PATTERNS.disabledSsl.exec(content)) !== null) {
|
|
136
|
+
const beforeMatch = content.slice(0, sslMatch.index);
|
|
137
|
+
const line = beforeMatch.split('\n').length;
|
|
138
|
+
findings.push({
|
|
139
|
+
severity: 'INFO',
|
|
140
|
+
type: 'Disabled SSL Verification',
|
|
141
|
+
value: sslMatch[0],
|
|
142
|
+
redacted: sslMatch[0],
|
|
143
|
+
line,
|
|
144
|
+
column: 0,
|
|
145
|
+
file: filepath,
|
|
146
|
+
message: 'SSL verification disabled. AI adds this to "make it work" during development but it should not ship to production.',
|
|
147
|
+
isAiPattern: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// AI-specific: wildcard CORS
|
|
151
|
+
AI_PATTERNS.wildcardCors.lastIndex = 0;
|
|
152
|
+
let corsMatch;
|
|
153
|
+
while ((corsMatch = AI_PATTERNS.wildcardCors.exec(content)) !== null) {
|
|
154
|
+
const beforeMatch = content.slice(0, corsMatch.index);
|
|
155
|
+
const line = beforeMatch.split('\n').length;
|
|
156
|
+
findings.push({
|
|
157
|
+
severity: 'WARNING',
|
|
158
|
+
type: 'Wildcard CORS',
|
|
159
|
+
value: corsMatch[0],
|
|
160
|
+
redacted: corsMatch[0],
|
|
161
|
+
line,
|
|
162
|
+
column: 0,
|
|
163
|
+
file: filepath,
|
|
164
|
+
message: 'CORS set to wildcard (*). AI defaults to permissive CORS to avoid errors during development.',
|
|
165
|
+
isAiPattern: true,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Sort by severity: CRITICAL > WARNING > INFO
|
|
169
|
+
const severityOrder = { CRITICAL: 0, WARNING: 1, INFO: 2 };
|
|
170
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
171
|
+
return findings;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Scans multiple files and returns aggregate results.
|
|
175
|
+
*/
|
|
176
|
+
export function scanFiles(files, options) {
|
|
177
|
+
const start = Date.now();
|
|
178
|
+
const allFindings = [];
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
const findings = detectSecrets(file.content, file.path, options);
|
|
181
|
+
allFindings.push(...findings);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
findings: allFindings,
|
|
185
|
+
scannedFiles: files.length,
|
|
186
|
+
scanTimeMs: Date.now() - start,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SessionEvent {
|
|
2
|
+
/** Timestamp in seconds since epoch (NOT milliseconds). Use Date.now() / 1000. */
|
|
3
|
+
timestampEpoch: number;
|
|
4
|
+
sourceApp: string;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
contentLength?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionCheckResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
severity: 'OK' | 'SUSPICIOUS' | 'BLOCKED';
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validates session event patterns to detect automated injection attempts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkSessionIntegrity(events: SessionEvent[], maxEventsPerSecond?: number, minIntervalMs?: number): SessionCheckResult;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates session event patterns to detect automated injection attempts.
|
|
3
|
+
*/
|
|
4
|
+
export function checkSessionIntegrity(events, maxEventsPerSecond = 3, minIntervalMs = 200) {
|
|
5
|
+
if (events.length < 2) {
|
|
6
|
+
return { valid: true, severity: 'OK', message: '' };
|
|
7
|
+
}
|
|
8
|
+
const sorted = [...events].sort((a, b) => a.timestampEpoch - b.timestampEpoch);
|
|
9
|
+
// Check rapid-fire events
|
|
10
|
+
let rapidCount = 0;
|
|
11
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
12
|
+
const intervalMs = (sorted[i].timestampEpoch - sorted[i - 1].timestampEpoch) * 1000;
|
|
13
|
+
if (intervalMs < minIntervalMs) {
|
|
14
|
+
rapidCount++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (rapidCount > 2) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
severity: 'BLOCKED',
|
|
21
|
+
message: `Automated session detected: ${rapidCount} events with <${minIntervalMs}ms intervals.`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Check event rate
|
|
25
|
+
const timeSpan = sorted[sorted.length - 1].timestampEpoch - sorted[0].timestampEpoch;
|
|
26
|
+
if (timeSpan > 0) {
|
|
27
|
+
const rate = sorted.length / timeSpan;
|
|
28
|
+
if (rate > maxEventsPerSecond) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
severity: 'BLOCKED',
|
|
32
|
+
message: `Event rate ${rate.toFixed(1)}/s exceeds max of ${maxEventsPerSecond}/s.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Check for suspiciously uniform content
|
|
37
|
+
const sources = new Set(sorted.map((e) => e.sourceApp));
|
|
38
|
+
if (sources.size === 1 && sorted.length > 3) {
|
|
39
|
+
const lengths = sorted.map((e) => e.contentLength ?? 0).filter((l) => l > 0);
|
|
40
|
+
if (lengths.length > 3) {
|
|
41
|
+
const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
42
|
+
const variance = lengths.reduce((a, l) => a + (l - avg) ** 2, 0) / lengths.length;
|
|
43
|
+
if (variance < 10) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
severity: 'SUSPICIOUS',
|
|
47
|
+
message: `Suspicious: ${sorted.length} events from '${[...sources][0]}' with near-identical content lengths.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { valid: true, severity: 'OK', message: '' };
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface TrustFinding {
|
|
2
|
+
severity: 'CRITICAL' | 'WARNING' | 'INFO';
|
|
3
|
+
type: string;
|
|
4
|
+
line: number;
|
|
5
|
+
message: string;
|
|
6
|
+
fix: string;
|
|
7
|
+
isAiPattern: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface FileTrustResult {
|
|
10
|
+
file: string;
|
|
11
|
+
score: number;
|
|
12
|
+
findings: TrustFinding[];
|
|
13
|
+
lines: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CodebaseTrustResult {
|
|
16
|
+
score: number;
|
|
17
|
+
files: FileTrustResult[];
|
|
18
|
+
totalFiles: number;
|
|
19
|
+
criticalFiles: number;
|
|
20
|
+
warningFiles: number;
|
|
21
|
+
cleanFiles: number;
|
|
22
|
+
scanTimeMs: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute trust score for a single file.
|
|
26
|
+
*/
|
|
27
|
+
export declare function computeFileTrust(content: string, filepath: string): FileTrustResult;
|
|
28
|
+
/**
|
|
29
|
+
* Compute trust score for an entire codebase.
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeCodebaseTrust(files: {
|
|
32
|
+
path: string;
|
|
33
|
+
content: string;
|
|
34
|
+
}[]): CodebaseTrustResult;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { detectSecrets } from './secret-detector.js';
|
|
2
|
+
import { checkCodeSafety } from './code-safety.js';
|
|
3
|
+
import { scanPayload } from './exfiltration-guard.js';
|
|
4
|
+
// Severity deductions
|
|
5
|
+
const DEDUCTIONS = {
|
|
6
|
+
CRITICAL: 15,
|
|
7
|
+
WARNING: 5,
|
|
8
|
+
INFO: 1,
|
|
9
|
+
};
|
|
10
|
+
const CAPS = {
|
|
11
|
+
CRITICAL: 60,
|
|
12
|
+
WARNING: 25,
|
|
13
|
+
INFO: 10,
|
|
14
|
+
};
|
|
15
|
+
// Test file multiplier
|
|
16
|
+
const TEST_MULTIPLIER = 0.5;
|
|
17
|
+
function isTestFile(filepath) {
|
|
18
|
+
const l = filepath.toLowerCase();
|
|
19
|
+
return l.includes('test') || l.includes('spec') || l.includes('__tests__') || l.includes('fixture');
|
|
20
|
+
}
|
|
21
|
+
// Fix suggestions for each finding type
|
|
22
|
+
const FIX_MAP = {
|
|
23
|
+
'AWS Access Key': 'Move to environment variable: process.env.AWS_ACCESS_KEY_ID',
|
|
24
|
+
'GitHub Token': 'Move to environment variable: process.env.GITHUB_TOKEN',
|
|
25
|
+
'OpenAI API Key': 'Move to environment variable: process.env.OPENAI_API_KEY',
|
|
26
|
+
'Anthropic API Key': 'Move to environment variable: process.env.ANTHROPIC_API_KEY',
|
|
27
|
+
'Stripe Key': 'Move to environment variable: process.env.STRIPE_SECRET_KEY',
|
|
28
|
+
'Database URL': 'Move to environment variable: process.env.DATABASE_URL',
|
|
29
|
+
'Private Key': 'Move to a secure key store or .env file (never commit)',
|
|
30
|
+
'Slack Token': 'Move to environment variable: process.env.SLACK_TOKEN',
|
|
31
|
+
'SendGrid Key': 'Move to environment variable: process.env.SENDGRID_API_KEY',
|
|
32
|
+
'Generic API Key Assignment': 'Replace with process.env.YOUR_KEY_NAME',
|
|
33
|
+
'Bearer Token': 'Load from secure token store, not hardcoded',
|
|
34
|
+
'Password Assignment': 'Move to environment variable or secret manager',
|
|
35
|
+
'Inlined Environment Variable': 'Use process.env.VARIABLE_NAME instead of hardcoding the URL',
|
|
36
|
+
'Real Value in Example File': 'Replace with a placeholder like YOUR_VALUE_HERE',
|
|
37
|
+
'Disabled SSL Verification': 'Set rejectUnauthorized: true (only disable in development)',
|
|
38
|
+
'Wildcard CORS': 'Set a specific origin: process.env.CORS_ORIGIN',
|
|
39
|
+
'eval_usage': 'Use JSON.parse() for data or Function() for dynamic code',
|
|
40
|
+
'innerHTML_assignment': 'Use textContent for text, or sanitize with DOMPurify',
|
|
41
|
+
'sql_concatenation': 'Use parameterized queries or an ORM',
|
|
42
|
+
'exec_usage': 'Use execFile() with an argument array',
|
|
43
|
+
'disabled_auth': 'Ensure authentication is enabled in production',
|
|
44
|
+
'hardcoded_ip': 'Use process.env.HOST for binding address',
|
|
45
|
+
'chmod_777': 'Use minimum required permissions (755 for executables, 644 for files)',
|
|
46
|
+
'console_log_sensitive': 'Remove sensitive data from log output or use a debug wrapper',
|
|
47
|
+
'todo_security': 'Address this security TODO before shipping to production',
|
|
48
|
+
'no_verify_flag': 'Remove bypass flags before production deployment',
|
|
49
|
+
'debug_endpoint': 'Gate behind authentication or remove before deployment',
|
|
50
|
+
'wildcard_permissions': 'Use least-privilege: specify exact actions and resources',
|
|
51
|
+
};
|
|
52
|
+
function getFix(type) {
|
|
53
|
+
return FIX_MAP[type] ?? 'Review and fix manually';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute trust score for a single file.
|
|
57
|
+
*/
|
|
58
|
+
export function computeFileTrust(content, filepath) {
|
|
59
|
+
const findings = [];
|
|
60
|
+
const lines = content.split('\n').length;
|
|
61
|
+
const isTF = isTestFile(filepath);
|
|
62
|
+
// Run secret detector
|
|
63
|
+
const secrets = detectSecrets(content, filepath, { skipTests: false, skipPlaceholders: true });
|
|
64
|
+
for (const s of secrets) {
|
|
65
|
+
findings.push({
|
|
66
|
+
severity: s.severity,
|
|
67
|
+
type: s.type,
|
|
68
|
+
line: s.line,
|
|
69
|
+
message: s.message,
|
|
70
|
+
fix: getFix(s.type),
|
|
71
|
+
isAiPattern: s.isAiPattern,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Run code safety
|
|
75
|
+
const safety = checkCodeSafety(content, filepath);
|
|
76
|
+
for (const s of safety) {
|
|
77
|
+
findings.push({
|
|
78
|
+
severity: s.severity,
|
|
79
|
+
type: s.rule,
|
|
80
|
+
line: s.line,
|
|
81
|
+
message: s.message,
|
|
82
|
+
fix: s.suggestion,
|
|
83
|
+
isAiPattern: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Run PII scan
|
|
87
|
+
const pii = scanPayload(content);
|
|
88
|
+
if (pii.hasPii) {
|
|
89
|
+
for (const m of pii.matches) {
|
|
90
|
+
findings.push({
|
|
91
|
+
severity: m.type === 'SSN' || m.type === 'CREDIT_CARD' ? 'CRITICAL' : 'INFO',
|
|
92
|
+
type: `PII: ${m.type}`,
|
|
93
|
+
line: 0,
|
|
94
|
+
message: `${m.type} found in source code`,
|
|
95
|
+
fix: `Remove or redact ${m.type} from source code`,
|
|
96
|
+
isAiPattern: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Error handling checks (regex-based)
|
|
101
|
+
const fetchWithoutTry = /(?:await\s+)?fetch\s*\([^)]*\)/g;
|
|
102
|
+
let fetchMatch;
|
|
103
|
+
while ((fetchMatch = fetchWithoutTry.exec(content)) !== null) {
|
|
104
|
+
const before = content.slice(Math.max(0, fetchMatch.index - 200), fetchMatch.index);
|
|
105
|
+
if (!before.includes('try') && !before.includes('catch') && !before.includes('.catch')) {
|
|
106
|
+
const line = content.slice(0, fetchMatch.index).split('\n').length;
|
|
107
|
+
findings.push({
|
|
108
|
+
severity: 'WARNING',
|
|
109
|
+
type: 'unhandled_fetch',
|
|
110
|
+
line,
|
|
111
|
+
message: 'fetch() call without error handling',
|
|
112
|
+
fix: 'Wrap in try/catch or add .catch() handler',
|
|
113
|
+
isAiPattern: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Compute score
|
|
118
|
+
let criticalDeductions = 0;
|
|
119
|
+
let warningDeductions = 0;
|
|
120
|
+
let infoDeductions = 0;
|
|
121
|
+
for (const f of findings) {
|
|
122
|
+
const mult = isTF ? TEST_MULTIPLIER : 1;
|
|
123
|
+
if (f.severity === 'CRITICAL') {
|
|
124
|
+
criticalDeductions += DEDUCTIONS.CRITICAL * mult;
|
|
125
|
+
}
|
|
126
|
+
else if (f.severity === 'WARNING') {
|
|
127
|
+
warningDeductions += DEDUCTIONS.WARNING * mult;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
infoDeductions += DEDUCTIONS.INFO * mult;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
criticalDeductions = Math.min(criticalDeductions, CAPS.CRITICAL);
|
|
134
|
+
warningDeductions = Math.min(warningDeductions, CAPS.WARNING);
|
|
135
|
+
infoDeductions = Math.min(infoDeductions, CAPS.INFO);
|
|
136
|
+
const score = Math.max(0, Math.round(100 - criticalDeductions - warningDeductions - infoDeductions));
|
|
137
|
+
return { file: filepath, score, findings, lines };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Compute trust score for an entire codebase.
|
|
141
|
+
*/
|
|
142
|
+
export function computeCodebaseTrust(files) {
|
|
143
|
+
const start = Date.now();
|
|
144
|
+
const results = [];
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const result = computeFileTrust(file.content, file.path);
|
|
147
|
+
results.push(result);
|
|
148
|
+
}
|
|
149
|
+
// Weighted average by lines of code (exclude tiny files)
|
|
150
|
+
const scoredFiles = results.filter((r) => r.lines >= 5);
|
|
151
|
+
const totalLines = scoredFiles.reduce((s, r) => s + r.lines, 0);
|
|
152
|
+
const weightedScore = totalLines > 0
|
|
153
|
+
? scoredFiles.reduce((s, r) => s + r.score * r.lines, 0) / totalLines
|
|
154
|
+
: 100;
|
|
155
|
+
return {
|
|
156
|
+
score: Math.round(weightedScore),
|
|
157
|
+
files: results.sort((a, b) => a.score - b.score),
|
|
158
|
+
totalFiles: results.length,
|
|
159
|
+
criticalFiles: results.filter((r) => r.score < 50).length,
|
|
160
|
+
warningFiles: results.filter((r) => r.score >= 50 && r.score < 80).length,
|
|
161
|
+
cleanFiles: results.filter((r) => r.score >= 80).length,
|
|
162
|
+
scanTimeMs: Date.now() - start,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type UndoCapability = 'REVERSIBLE' | 'BEST_EFFORT' | 'IRREVERSIBLE';
|
|
2
|
+
export interface UndoCheckResult {
|
|
3
|
+
capability: UndoCapability;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether an action can actually be undone.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkUndoIntegrity(actionType: string, hasUndoFn: boolean): UndoCheckResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const IRREVERSIBLE = [
|
|
2
|
+
'send_email', 'send_sms', 'send_message', 'post_tweet', 'publish_post',
|
|
3
|
+
'post_to_linkedin', 'make_payment', 'transfer_funds', 'charge_card',
|
|
4
|
+
'delete_account', 'deactivate_account', 'notify_user', 'send_notification',
|
|
5
|
+
'submit_form', 'place_order',
|
|
6
|
+
];
|
|
7
|
+
const BEST_EFFORT = [
|
|
8
|
+
'create_calendar_event', 'share_document', 'invite_user',
|
|
9
|
+
'update_record', 'publish_article', 'grant_access',
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* Checks whether an action can actually be undone.
|
|
13
|
+
*/
|
|
14
|
+
export function checkUndoIntegrity(actionType, hasUndoFn) {
|
|
15
|
+
const lower = actionType.toLowerCase();
|
|
16
|
+
for (const action of IRREVERSIBLE) {
|
|
17
|
+
if (lower.startsWith(action)) {
|
|
18
|
+
return {
|
|
19
|
+
capability: 'IRREVERSIBLE',
|
|
20
|
+
message: hasUndoFn
|
|
21
|
+
? `'${actionType}' has an undo function but the action is irreversible (e.g., email already delivered).`
|
|
22
|
+
: `'${actionType}' is irreversible and no undo function was provided.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const action of BEST_EFFORT) {
|
|
27
|
+
if (lower.startsWith(action)) {
|
|
28
|
+
return {
|
|
29
|
+
capability: 'BEST_EFFORT',
|
|
30
|
+
message: `'${actionType}' can be partially undone but side effects (notifications, cache) may persist.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
capability: hasUndoFn ? 'REVERSIBLE' : 'BEST_EFFORT',
|
|
36
|
+
message: hasUndoFn ? '' : `'${actionType}' appears reversible but no undo function was provided.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PolicyResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs a Jac walker as a subprocess and returns its PolicyResult.
|
|
4
|
+
*
|
|
5
|
+
* If the subprocess times out, errors, or returns invalid JSON:
|
|
6
|
+
* Returns CONFIRM verdict (safe default -- never silently execute).
|
|
7
|
+
*
|
|
8
|
+
* This function NEVER throws. All errors become CONFIRM verdicts.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runJacWalker(jacFilePath: string, walkerName: string, args: Record<string, unknown>, timeoutMs: number): Promise<PolicyResult>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { debug } from './logger.js';
|
|
4
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB max stdout/stderr
|
|
5
|
+
/**
|
|
6
|
+
* Runs a Jac walker as a subprocess and returns its PolicyResult.
|
|
7
|
+
*
|
|
8
|
+
* If the subprocess times out, errors, or returns invalid JSON:
|
|
9
|
+
* Returns CONFIRM verdict (safe default -- never silently execute).
|
|
10
|
+
*
|
|
11
|
+
* This function NEVER throws. All errors become CONFIRM verdicts.
|
|
12
|
+
*/
|
|
13
|
+
export async function runJacWalker(jacFilePath, walkerName, args, timeoutMs) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const safeDefault = {
|
|
16
|
+
verdict: 'CONFIRM',
|
|
17
|
+
blockReason: null,
|
|
18
|
+
message: 'Policy check could not complete. Confirm to proceed.',
|
|
19
|
+
policyName: `${walkerName}:error`,
|
|
20
|
+
requiresConfirmation: true,
|
|
21
|
+
};
|
|
22
|
+
// Validate inputs
|
|
23
|
+
const resolvedPath = path.resolve(jacFilePath);
|
|
24
|
+
if (!resolvedPath.endsWith('.jac')) {
|
|
25
|
+
debug(`Invalid jac file path: ${jacFilePath}`);
|
|
26
|
+
resolve(safeDefault);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(walkerName)) {
|
|
30
|
+
debug(`Invalid walker name: ${walkerName}`);
|
|
31
|
+
resolve(safeDefault);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let timedOut = false;
|
|
35
|
+
let resolved = false;
|
|
36
|
+
let stdout = '';
|
|
37
|
+
let stderr = '';
|
|
38
|
+
let outputExceeded = false;
|
|
39
|
+
function resolveOnce(result) {
|
|
40
|
+
if (resolved)
|
|
41
|
+
return;
|
|
42
|
+
resolved = true;
|
|
43
|
+
resolve(result);
|
|
44
|
+
}
|
|
45
|
+
const proc = spawn('jac', ['run', resolvedPath, '--entrypoint', walkerName], {
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
timedOut = true;
|
|
50
|
+
proc.kill('SIGTERM');
|
|
51
|
+
debug(`Jac walker ${walkerName} timed out after ${timeoutMs}ms`);
|
|
52
|
+
resolveOnce(safeDefault);
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
proc.stdin.write(JSON.stringify(args));
|
|
55
|
+
proc.stdin.end();
|
|
56
|
+
proc.stdout.on('data', (chunk) => {
|
|
57
|
+
stdout += chunk.toString();
|
|
58
|
+
if (stdout.length > MAX_OUTPUT_BYTES && !outputExceeded) {
|
|
59
|
+
outputExceeded = true;
|
|
60
|
+
proc.kill('SIGTERM');
|
|
61
|
+
debug(`Jac walker ${walkerName} output exceeded ${MAX_OUTPUT_BYTES} bytes`);
|
|
62
|
+
resolveOnce(safeDefault);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
proc.stderr.on('data', (chunk) => {
|
|
66
|
+
stderr += chunk.toString();
|
|
67
|
+
if (stderr.length > MAX_OUTPUT_BYTES && !outputExceeded) {
|
|
68
|
+
outputExceeded = true;
|
|
69
|
+
proc.kill('SIGTERM');
|
|
70
|
+
debug(`Jac walker ${walkerName} stderr exceeded ${MAX_OUTPUT_BYTES} bytes`);
|
|
71
|
+
resolveOnce(safeDefault);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
proc.on('close', (code) => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
if (timedOut)
|
|
77
|
+
return;
|
|
78
|
+
if (stderr) {
|
|
79
|
+
debug(`Jac walker ${walkerName} stderr:`, stderr);
|
|
80
|
+
}
|
|
81
|
+
if (code !== 0) {
|
|
82
|
+
debug(`Jac walker ${walkerName} exited with code ${code}`);
|
|
83
|
+
resolveOnce(safeDefault);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const result = JSON.parse(stdout.trim());
|
|
88
|
+
resolveOnce(result);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
debug(`Jac walker ${walkerName} returned invalid JSON:`, stdout);
|
|
92
|
+
resolveOnce(safeDefault);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
proc.on('error', (err) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
if (!timedOut) {
|
|
98
|
+
debug(`Jac walker ${walkerName} process error:`, err.message);
|
|
99
|
+
resolveOnce(safeDefault);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|