guardrail-security 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/dist/attack-surface/analyzer.d.ts +50 -0
- package/dist/attack-surface/analyzer.d.ts.map +1 -0
- package/dist/attack-surface/analyzer.js +83 -0
- package/dist/attack-surface/index.d.ts +5 -0
- package/dist/attack-surface/index.d.ts.map +1 -0
- package/dist/attack-surface/index.js +20 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/languages/index.d.ts +21 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +78 -0
- package/dist/languages/java-analyzer.d.ts +72 -0
- package/dist/languages/java-analyzer.d.ts.map +1 -0
- package/dist/languages/java-analyzer.js +417 -0
- package/dist/languages/python-analyzer.d.ts +70 -0
- package/dist/languages/python-analyzer.d.ts.map +1 -0
- package/dist/languages/python-analyzer.js +425 -0
- package/dist/license/compatibility-matrix.d.ts +28 -0
- package/dist/license/compatibility-matrix.d.ts.map +1 -0
- package/dist/license/compatibility-matrix.js +323 -0
- package/dist/license/engine.d.ts +77 -0
- package/dist/license/engine.d.ts.map +1 -0
- package/dist/license/engine.js +264 -0
- package/dist/license/index.d.ts +6 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/index.js +21 -0
- package/dist/sbom/generator.d.ts +108 -0
- package/dist/sbom/generator.d.ts.map +1 -0
- package/dist/sbom/generator.js +271 -0
- package/dist/sbom/index.d.ts +5 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +20 -0
- package/dist/secrets/guardian.d.ts +113 -0
- package/dist/secrets/guardian.d.ts.map +1 -0
- package/dist/secrets/guardian.js +334 -0
- package/dist/secrets/index.d.ts +10 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +30 -0
- package/dist/secrets/patterns.d.ts +42 -0
- package/dist/secrets/patterns.d.ts.map +1 -0
- package/dist/secrets/patterns.js +165 -0
- package/dist/secrets/pre-commit.d.ts +39 -0
- package/dist/secrets/pre-commit.d.ts.map +1 -0
- package/dist/secrets/pre-commit.js +127 -0
- package/dist/secrets/vault-integration.d.ts +83 -0
- package/dist/secrets/vault-integration.d.ts.map +1 -0
- package/dist/secrets/vault-integration.js +295 -0
- package/dist/secrets/vault-providers.d.ts +110 -0
- package/dist/secrets/vault-providers.d.ts.map +1 -0
- package/dist/secrets/vault-providers.js +417 -0
- package/dist/supply-chain/detector.d.ts +80 -0
- package/dist/supply-chain/detector.d.ts.map +1 -0
- package/dist/supply-chain/detector.js +168 -0
- package/dist/supply-chain/index.d.ts +11 -0
- package/dist/supply-chain/index.d.ts.map +1 -0
- package/dist/supply-chain/index.js +26 -0
- package/dist/supply-chain/malicious-db.d.ts +41 -0
- package/dist/supply-chain/malicious-db.d.ts.map +1 -0
- package/dist/supply-chain/malicious-db.js +82 -0
- package/dist/supply-chain/script-analyzer.d.ts +54 -0
- package/dist/supply-chain/script-analyzer.d.ts.map +1 -0
- package/dist/supply-chain/script-analyzer.js +160 -0
- package/dist/supply-chain/typosquat.d.ts +58 -0
- package/dist/supply-chain/typosquat.d.ts.map +1 -0
- package/dist/supply-chain/typosquat.js +257 -0
- package/dist/supply-chain/vulnerability-db.d.ts +114 -0
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -0
- package/dist/supply-chain/vulnerability-db.js +310 -0
- package/package.json +34 -0
- package/src/__tests__/license/engine.test.ts +250 -0
- package/src/__tests__/supply-chain/typosquat.test.ts +191 -0
- package/src/attack-surface/analyzer.ts +152 -0
- package/src/attack-surface/index.ts +5 -0
- package/src/index.ts +21 -0
- package/src/languages/index.ts +91 -0
- package/src/languages/java-analyzer.ts +490 -0
- package/src/languages/python-analyzer.ts +498 -0
- package/src/license/compatibility-matrix.ts +366 -0
- package/src/license/engine.ts +345 -0
- package/src/license/index.ts +6 -0
- package/src/sbom/generator.ts +355 -0
- package/src/sbom/index.ts +5 -0
- package/src/secrets/guardian.ts +448 -0
- package/src/secrets/index.ts +10 -0
- package/src/secrets/patterns.ts +186 -0
- package/src/secrets/pre-commit.ts +158 -0
- package/src/secrets/vault-integration.ts +360 -0
- package/src/secrets/vault-providers.ts +446 -0
- package/src/supply-chain/detector.ts +252 -0
- package/src/supply-chain/index.ts +11 -0
- package/src/supply-chain/malicious-db.ts +103 -0
- package/src/supply-chain/script-analyzer.ts +194 -0
- package/src/supply-chain/typosquat.ts +302 -0
- package/src/supply-chain/vulnerability-db.ts +386 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { prisma } from '@guardrail/database';
|
|
2
|
+
import { calculateEntropy, maskSensitiveValue } from '@guardrail/core';
|
|
3
|
+
import { SECRET_PATTERNS, TEST_PATTERNS, FALSE_POSITIVE_VALUES, SecretPattern } from './patterns';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
// Define SecretType locally since it's not exported from database
|
|
9
|
+
export enum SecretType {
|
|
10
|
+
API_KEY = 'api_key',
|
|
11
|
+
PASSWORD = 'password',
|
|
12
|
+
TOKEN = 'token',
|
|
13
|
+
CERTIFICATE = 'certificate',
|
|
14
|
+
PRIVATE_KEY = 'private_key',
|
|
15
|
+
DATABASE_URL = 'database_url',
|
|
16
|
+
JWT_SECRET = 'jwt_secret',
|
|
17
|
+
AWS_ACCESS_KEY = 'aws_access_key',
|
|
18
|
+
OTHER = 'other',
|
|
19
|
+
AWS_SECRET_KEY = 'aws_secret_key',
|
|
20
|
+
GITHUB_TOKEN = 'github_token',
|
|
21
|
+
GOOGLE_API_KEY = 'google_api_key',
|
|
22
|
+
STRIPE_KEY = 'stripe_key',
|
|
23
|
+
JWT_TOKEN = 'jwt_token',
|
|
24
|
+
SLACK_TOKEN = 'slack_token',
|
|
25
|
+
API_KEY_GENERIC = 'api_key_generic',
|
|
26
|
+
PASSWORD_GENERIC = 'password_generic'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Secret detection result
|
|
31
|
+
*/
|
|
32
|
+
export interface SecretDetection {
|
|
33
|
+
id?: string;
|
|
34
|
+
filePath: string;
|
|
35
|
+
secretType: SecretType;
|
|
36
|
+
maskedValue: string;
|
|
37
|
+
location: {
|
|
38
|
+
line: number;
|
|
39
|
+
column: number;
|
|
40
|
+
snippet: string;
|
|
41
|
+
};
|
|
42
|
+
confidence: number;
|
|
43
|
+
entropy: number;
|
|
44
|
+
isTest: boolean;
|
|
45
|
+
isRevoked: boolean;
|
|
46
|
+
recommendation: {
|
|
47
|
+
action: 'remove' | 'move_to_env' | 'use_vault' | 'revoke_and_rotate';
|
|
48
|
+
reason: string;
|
|
49
|
+
remediation: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Scan options
|
|
55
|
+
*/
|
|
56
|
+
export interface ScanOptions {
|
|
57
|
+
excludeTests?: boolean;
|
|
58
|
+
minConfidence?: number;
|
|
59
|
+
excludePatterns?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Project scan report
|
|
64
|
+
*/
|
|
65
|
+
export interface ProjectScanReport {
|
|
66
|
+
projectId: string;
|
|
67
|
+
totalFiles: number;
|
|
68
|
+
scannedFiles: number;
|
|
69
|
+
detections: SecretDetection[];
|
|
70
|
+
summary: {
|
|
71
|
+
totalSecrets: number;
|
|
72
|
+
byType: Record<string, number>;
|
|
73
|
+
byRisk: { high: number; medium: number; low: number };
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Secrets & Credential Guardian
|
|
79
|
+
*
|
|
80
|
+
* Detects exposed secrets and credentials in code
|
|
81
|
+
*/
|
|
82
|
+
export class SecretsGuardian {
|
|
83
|
+
/**
|
|
84
|
+
* Scan content for secrets
|
|
85
|
+
*/
|
|
86
|
+
async scanContent(
|
|
87
|
+
content: string,
|
|
88
|
+
filePath: string,
|
|
89
|
+
options: ScanOptions = {}
|
|
90
|
+
): Promise<SecretDetection[]> {
|
|
91
|
+
const detections: SecretDetection[] = [];
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
|
|
94
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
95
|
+
const matches = [...content.matchAll(new RegExp(pattern.pattern, 'g'))];
|
|
96
|
+
|
|
97
|
+
for (const match of matches) {
|
|
98
|
+
if (!match.index) continue;
|
|
99
|
+
|
|
100
|
+
// Extract the secret value (first capturing group)
|
|
101
|
+
const value = match[1] || match[0];
|
|
102
|
+
|
|
103
|
+
// Calculate entropy
|
|
104
|
+
const entropy = this.calculateEntropy(value);
|
|
105
|
+
|
|
106
|
+
// Check minimum entropy requirement
|
|
107
|
+
if (pattern.minEntropy && entropy < pattern.minEntropy) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find line number and column
|
|
112
|
+
const beforeMatch = content.substring(0, match.index);
|
|
113
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
114
|
+
const lineStart = beforeMatch.lastIndexOf('\n') + 1;
|
|
115
|
+
const column = match.index - lineStart + 1;
|
|
116
|
+
|
|
117
|
+
// Get context
|
|
118
|
+
const snippet = lines[lineNumber - 1] || '';
|
|
119
|
+
|
|
120
|
+
// Check if it's a test value
|
|
121
|
+
const isTest = this.isTestValue(value, snippet);
|
|
122
|
+
|
|
123
|
+
// Check for false positives
|
|
124
|
+
const isFalsePositive = this.isFalsePositive(value, pattern.type, snippet);
|
|
125
|
+
|
|
126
|
+
if (isFalsePositive) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate confidence
|
|
131
|
+
const confidence = this.calculateConfidence(value, pattern, entropy, isTest);
|
|
132
|
+
|
|
133
|
+
// Skip if below minimum confidence
|
|
134
|
+
if (options.minConfidence && confidence < options.minConfidence) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Skip if excluding tests
|
|
139
|
+
if (options.excludeTests && isTest) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mask the value
|
|
144
|
+
const maskedValue = this.maskValue(value);
|
|
145
|
+
|
|
146
|
+
// Generate recommendation
|
|
147
|
+
const recommendation = this.generateRecommendation(pattern.type, isTest);
|
|
148
|
+
|
|
149
|
+
// Create detection object
|
|
150
|
+
const detection: SecretDetection = {
|
|
151
|
+
id: undefined, // Will be set when saved to database
|
|
152
|
+
filePath,
|
|
153
|
+
secretType: pattern.type,
|
|
154
|
+
maskedValue,
|
|
155
|
+
location: {
|
|
156
|
+
line: lineNumber,
|
|
157
|
+
column,
|
|
158
|
+
snippet: snippet.trim(),
|
|
159
|
+
},
|
|
160
|
+
confidence,
|
|
161
|
+
entropy,
|
|
162
|
+
isTest,
|
|
163
|
+
isRevoked: false,
|
|
164
|
+
recommendation,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
detections.push(detection);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return detections;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Scan entire project
|
|
176
|
+
*/
|
|
177
|
+
async scanProject(
|
|
178
|
+
projectPath: string,
|
|
179
|
+
projectId: string,
|
|
180
|
+
options: ScanOptions = {}
|
|
181
|
+
): Promise<ProjectScanReport> {
|
|
182
|
+
const excludePatterns = [
|
|
183
|
+
'**/node_modules/**',
|
|
184
|
+
'**/dist/**',
|
|
185
|
+
'**/build/**',
|
|
186
|
+
'**/.git/**',
|
|
187
|
+
'**/coverage/**',
|
|
188
|
+
'**/*.min.js',
|
|
189
|
+
...(options.excludePatterns || []),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// Find all files to scan
|
|
193
|
+
const files = await glob('**/*', {
|
|
194
|
+
cwd: projectPath,
|
|
195
|
+
ignore: excludePatterns,
|
|
196
|
+
nodir: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const allDetections: SecretDetection[] = [];
|
|
200
|
+
let scannedFiles = 0;
|
|
201
|
+
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
try {
|
|
204
|
+
const fullPath = join(projectPath, file);
|
|
205
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
206
|
+
|
|
207
|
+
const detections = await this.scanContent(content, file, options);
|
|
208
|
+
|
|
209
|
+
// Save to database
|
|
210
|
+
for (const detection of detections) {
|
|
211
|
+
try {
|
|
212
|
+
// @ts-ignore - secretDetection may not exist in schema yet
|
|
213
|
+
await prisma.secretDetection.create({
|
|
214
|
+
data: {
|
|
215
|
+
projectId: 'default',
|
|
216
|
+
filePath: detection.filePath
|
|
217
|
+
} as any
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Table may not exist - continue
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
allDetections.push(...detections);
|
|
225
|
+
scannedFiles++;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// Skip files that can't be read
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Generate summary
|
|
233
|
+
const byType: Record<string, number> = {};
|
|
234
|
+
const byRisk = { high: 0, medium: 0, low: 0 };
|
|
235
|
+
|
|
236
|
+
for (const detection of allDetections) {
|
|
237
|
+
byType[detection.secretType] = (byType[detection.secretType] || 0) + 1;
|
|
238
|
+
|
|
239
|
+
if (detection.confidence >= 0.8) {
|
|
240
|
+
byRisk.high++;
|
|
241
|
+
} else if (detection.confidence >= 0.5) {
|
|
242
|
+
byRisk.medium++;
|
|
243
|
+
} else {
|
|
244
|
+
byRisk.low++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
projectId,
|
|
250
|
+
totalFiles: files.length,
|
|
251
|
+
scannedFiles,
|
|
252
|
+
detections: allDetections,
|
|
253
|
+
summary: {
|
|
254
|
+
totalSecrets: allDetections.length,
|
|
255
|
+
byType,
|
|
256
|
+
byRisk,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Calculate entropy for randomness detection
|
|
263
|
+
*/
|
|
264
|
+
private calculateEntropy(str: string): number {
|
|
265
|
+
return calculateEntropy(str);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if likely test/example value
|
|
270
|
+
*/
|
|
271
|
+
private isTestValue(value: string, context: string): boolean {
|
|
272
|
+
const lowerValue = value.toLowerCase();
|
|
273
|
+
const lowerContext = context.toLowerCase();
|
|
274
|
+
|
|
275
|
+
// Check value itself
|
|
276
|
+
for (const pattern of TEST_PATTERNS) {
|
|
277
|
+
if (pattern.test(lowerValue)) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check context
|
|
283
|
+
if (
|
|
284
|
+
lowerContext.includes('test') ||
|
|
285
|
+
lowerContext.includes('example') ||
|
|
286
|
+
lowerContext.includes('demo') ||
|
|
287
|
+
lowerContext.includes('fixture') ||
|
|
288
|
+
lowerContext.includes('mock')
|
|
289
|
+
) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check for false positives
|
|
298
|
+
*/
|
|
299
|
+
private isFalsePositive(value: string, type: SecretType, _context: string): boolean {
|
|
300
|
+
const lowerValue = value.toLowerCase();
|
|
301
|
+
|
|
302
|
+
// Check against known false positives
|
|
303
|
+
if (FALSE_POSITIVE_VALUES.has(lowerValue)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for placeholder patterns
|
|
308
|
+
if (/^(x+|0+|1+|a+)$/i.test(value)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check for repeated characters (likely placeholder)
|
|
313
|
+
if (/(.)\1{10,}/.test(value)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// JWT-specific false positive checks
|
|
318
|
+
if (type === SecretType.JWT_TOKEN) {
|
|
319
|
+
// Very simple/short payload might be example
|
|
320
|
+
try {
|
|
321
|
+
const parts = value.split('.');
|
|
322
|
+
if (parts.length === 3 && parts[1]) {
|
|
323
|
+
const payload = Buffer.from(parts[1], 'base64').toString();
|
|
324
|
+
if (payload.length < 20) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
// Invalid JWT, might be false positive
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Calculate confidence score
|
|
339
|
+
*/
|
|
340
|
+
private calculateConfidence(
|
|
341
|
+
value: string,
|
|
342
|
+
pattern: SecretPattern,
|
|
343
|
+
entropy: number,
|
|
344
|
+
isTest: boolean
|
|
345
|
+
): number {
|
|
346
|
+
let confidence = 0.7; // Base confidence
|
|
347
|
+
|
|
348
|
+
// Increase confidence for high entropy
|
|
349
|
+
if (entropy > 4.5) {
|
|
350
|
+
confidence += 0.2;
|
|
351
|
+
} else if (entropy > 4.0) {
|
|
352
|
+
confidence += 0.1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Decrease confidence for test values
|
|
356
|
+
if (isTest) {
|
|
357
|
+
confidence -= 0.3;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Pattern-specific adjustments
|
|
361
|
+
if (pattern.type === SecretType.AWS_ACCESS_KEY && value.startsWith('AKIA')) {
|
|
362
|
+
confidence += 0.1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (pattern.type === SecretType.GITHUB_TOKEN && /^gh[pos]_/.test(value)) {
|
|
366
|
+
confidence += 0.1;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return Math.max(0, Math.min(1, confidence));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Mask secret for safe logging
|
|
374
|
+
*/
|
|
375
|
+
private maskValue(value: string): string {
|
|
376
|
+
return maskSensitiveValue(value);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generate recommendation
|
|
381
|
+
*/
|
|
382
|
+
private generateRecommendation(
|
|
383
|
+
type: SecretType,
|
|
384
|
+
isTest: boolean
|
|
385
|
+
): {
|
|
386
|
+
action: 'remove' | 'move_to_env' | 'use_vault' | 'revoke_and_rotate';
|
|
387
|
+
reason: string;
|
|
388
|
+
remediation: string;
|
|
389
|
+
} {
|
|
390
|
+
if (isTest) {
|
|
391
|
+
return {
|
|
392
|
+
action: 'remove',
|
|
393
|
+
reason: 'Test credential detected in code',
|
|
394
|
+
remediation: 'Remove test credentials and use mocking instead',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// High-risk secrets require rotation
|
|
399
|
+
const highRiskTypes: SecretType[] = [
|
|
400
|
+
'AWS_SECRET_KEY' as SecretType,
|
|
401
|
+
'GITHUB_TOKEN' as SecretType,
|
|
402
|
+
'STRIPE_KEY' as SecretType,
|
|
403
|
+
'PRIVATE_KEY' as SecretType,
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
if (highRiskTypes.includes(type)) {
|
|
407
|
+
return {
|
|
408
|
+
action: 'revoke_and_rotate',
|
|
409
|
+
reason: 'High-risk credential exposed in code',
|
|
410
|
+
remediation: 'Immediately revoke this credential and rotate to a new one. Store in secure vault or environment variables.',
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Medium-risk can use env vars
|
|
415
|
+
return {
|
|
416
|
+
action: 'move_to_env',
|
|
417
|
+
reason: 'Credential should not be hardcoded',
|
|
418
|
+
remediation: 'Move to environment variables or secure vault (e.g., AWS Secrets Manager, HashiCorp Vault)',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get project secrets report
|
|
424
|
+
*/
|
|
425
|
+
async getProjectReport(projectId: string): Promise<SecretDetection[]> {
|
|
426
|
+
// @ts-ignore - secretDetection may not exist in schema yet
|
|
427
|
+
const detections = await prisma.secretDetection.findMany({
|
|
428
|
+
where: { projectId },
|
|
429
|
+
orderBy: { createdAt: 'desc' },
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return detections.map((s: any) => ({
|
|
433
|
+
id: s.id,
|
|
434
|
+
filePath: s.filePath,
|
|
435
|
+
secretType: s.secretType as SecretType,
|
|
436
|
+
maskedValue: s.maskedValue,
|
|
437
|
+
location: s.location as any,
|
|
438
|
+
confidence: s.confidence,
|
|
439
|
+
entropy: s.entropy,
|
|
440
|
+
isTest: s.isTest,
|
|
441
|
+
isRevoked: s.isRevoked,
|
|
442
|
+
recommendation: s.recommendation as any,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Export singleton
|
|
448
|
+
export const secretsGuardian = new SecretsGuardian();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets & Credential Guardian
|
|
3
|
+
*
|
|
4
|
+
* Detects and prevents exposure of secrets and credentials
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export * from './patterns';
|
|
8
|
+
export { secretsGuardian, SecretsGuardian } from './guardian';
|
|
9
|
+
export { preCommitHook } from './pre-commit';
|
|
10
|
+
export { vaultIntegration } from './vault-integration';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Define SecretType locally since it's not exported from database
|
|
2
|
+
export enum SecretType {
|
|
3
|
+
API_KEY = 'api_key',
|
|
4
|
+
PASSWORD = 'password',
|
|
5
|
+
TOKEN = 'token',
|
|
6
|
+
CERTIFICATE = 'certificate',
|
|
7
|
+
PRIVATE_KEY = 'private_key',
|
|
8
|
+
DATABASE_URL = 'database_url',
|
|
9
|
+
JWT_SECRET = 'jwt_secret',
|
|
10
|
+
AWS_ACCESS_KEY = 'aws_access_key',
|
|
11
|
+
OTHER = 'other',
|
|
12
|
+
AWS_SECRET_KEY = 'aws_secret_key',
|
|
13
|
+
GITHUB_TOKEN = 'github_token',
|
|
14
|
+
GOOGLE_API_KEY = 'google_api_key',
|
|
15
|
+
STRIPE_KEY = 'stripe_key',
|
|
16
|
+
JWT_TOKEN = 'jwt_token',
|
|
17
|
+
SLACK_TOKEN = 'slack_token',
|
|
18
|
+
API_KEY_GENERIC = 'api_key_generic',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Secret detection pattern
|
|
23
|
+
*/
|
|
24
|
+
export interface SecretPattern {
|
|
25
|
+
type: SecretType;
|
|
26
|
+
name: string;
|
|
27
|
+
pattern: RegExp;
|
|
28
|
+
minEntropy?: number;
|
|
29
|
+
description: string;
|
|
30
|
+
examples: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Comprehensive secret detection patterns
|
|
35
|
+
*/
|
|
36
|
+
export const SECRET_PATTERNS: SecretPattern[] = [
|
|
37
|
+
// AWS Access Keys
|
|
38
|
+
{
|
|
39
|
+
type: 'AWS_ACCESS_KEY' as SecretType,
|
|
40
|
+
name: 'AWS Access Key ID',
|
|
41
|
+
pattern: /(AKIA[0-9A-Z]{16})/,
|
|
42
|
+
minEntropy: 3.5,
|
|
43
|
+
description: 'AWS Access Key ID (starts with AKIA)',
|
|
44
|
+
examples: ['AKIAIOSFODNN7EXAMPLE'],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// AWS Secret Keys
|
|
48
|
+
{
|
|
49
|
+
type: 'AWS_SECRET_KEY' as SecretType,
|
|
50
|
+
name: 'AWS Secret Access Key',
|
|
51
|
+
pattern: /aws[_\s]*secret[_\s]*access[_\s]*key[_\s]*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/i,
|
|
52
|
+
minEntropy: 4.5,
|
|
53
|
+
description: 'AWS Secret Access Key (40 characters)',
|
|
54
|
+
examples: ['aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'],
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// GitHub Personal Access Tokens
|
|
58
|
+
{
|
|
59
|
+
type: 'GITHUB_TOKEN' as SecretType,
|
|
60
|
+
name: 'GitHub Personal Access Token',
|
|
61
|
+
pattern: /(ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghu_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36})/,
|
|
62
|
+
description: 'GitHub Personal Access Token (ghp_, gho_, ghu_, ghs_, ghr_)',
|
|
63
|
+
examples: ['ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Google API Keys
|
|
67
|
+
{
|
|
68
|
+
type: 'GOOGLE_API_KEY' as SecretType,
|
|
69
|
+
name: 'Google API Key',
|
|
70
|
+
pattern: /(AIza[0-9A-Za-z\-_]{35})/,
|
|
71
|
+
description: 'Google API Key (starts with AIza)',
|
|
72
|
+
examples: ['AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe'],
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Stripe API Keys
|
|
76
|
+
{
|
|
77
|
+
type: 'STRIPE_KEY' as SecretType,
|
|
78
|
+
name: 'Stripe API Key',
|
|
79
|
+
pattern: /(sk_live_[0-9a-zA-Z]{24,}|pk_live_[0-9a-zA-Z]{24,}|rk_live_[0-9a-zA-Z]{24,})/,
|
|
80
|
+
description: 'Stripe Live API Key',
|
|
81
|
+
examples: ['sk_live_1234567890abcdefghijklmn'],
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// JWT Tokens
|
|
85
|
+
{
|
|
86
|
+
type: 'JWT_TOKEN' as SecretType,
|
|
87
|
+
name: 'JWT Token',
|
|
88
|
+
pattern: /(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+)/,
|
|
89
|
+
minEntropy: 4.0,
|
|
90
|
+
description: 'JSON Web Token (JWT)',
|
|
91
|
+
examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'],
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Private Keys
|
|
95
|
+
{
|
|
96
|
+
type: 'PRIVATE_KEY' as SecretType,
|
|
97
|
+
name: 'Private Key',
|
|
98
|
+
pattern: /(-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----[\s\S]*?-----END (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----)/,
|
|
99
|
+
description: 'Private Key (RSA, EC, OpenSSH, DSA)',
|
|
100
|
+
examples: ['-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgk...\\n-----END PRIVATE KEY-----'],
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Database URLs with credentials
|
|
104
|
+
{
|
|
105
|
+
type: 'DATABASE_URL' as SecretType,
|
|
106
|
+
name: 'Database URL with Password',
|
|
107
|
+
pattern: /(postgres|mysql|mongodb|redis):\/\/[a-zA-Z0-9_-]+:([a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+)@[a-zA-Z0-9.-]+:[0-9]+/,
|
|
108
|
+
minEntropy: 3.0,
|
|
109
|
+
description: 'Database connection string with embedded password',
|
|
110
|
+
examples: ['postgresql://user:password123@localhost:5432/dbname'],
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Slack Tokens
|
|
114
|
+
{
|
|
115
|
+
type: 'SLACK_TOKEN' as SecretType,
|
|
116
|
+
name: 'Slack Token',
|
|
117
|
+
pattern: /(xox[pboa]-[0-9]{10,13}-[0-9]{10,13}-[0-9]{10,13}-[a-z0-9]{32})/,
|
|
118
|
+
description: 'Slack Bot/User/App Token',
|
|
119
|
+
examples: ['xoxb-0000000000-0000000000-0000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'],
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Generic API Keys (high entropy)
|
|
123
|
+
{
|
|
124
|
+
type: 'API_KEY_GENERIC' as SecretType,
|
|
125
|
+
name: 'Generic API Key',
|
|
126
|
+
pattern: /(?:api[_\s-]?key|apikey|access[_\s-]?token|auth[_\s-]?token|secret[_\s-]?key)[_\s]*[=:]\s*['"]?([a-zA-Z0-9_\-]{32,})['"]?/i,
|
|
127
|
+
minEntropy: 4.0,
|
|
128
|
+
description: 'Generic API key or access token (high entropy)',
|
|
129
|
+
examples: ['api_key = abcdef1234567890abcdef1234567890'],
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Generic Passwords
|
|
133
|
+
{
|
|
134
|
+
type: 'PASSWORD_GENERIC' as SecretType,
|
|
135
|
+
name: 'Generic Password',
|
|
136
|
+
pattern: /(?:password|passwd|pwd)[_\s]*[=:]\s*['"]([^'"]{8,})['"]|(?:password|passwd|pwd)[_\s]*[=:]\s*([^\s]{8,})/i,
|
|
137
|
+
minEntropy: 3.0,
|
|
138
|
+
description: 'Generic password in configuration',
|
|
139
|
+
examples: ['password = "MySecretP@ssw0rd"'],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Test/example value patterns (to exclude false positives)
|
|
145
|
+
*/
|
|
146
|
+
export const TEST_PATTERNS = [
|
|
147
|
+
/test/i,
|
|
148
|
+
/example/i,
|
|
149
|
+
/sample/i,
|
|
150
|
+
/demo/i,
|
|
151
|
+
/fake/i,
|
|
152
|
+
/dummy/i,
|
|
153
|
+
/placeholder/i,
|
|
154
|
+
/\*{3,}/,
|
|
155
|
+
/x{3,}/i,
|
|
156
|
+
/0{5,}/,
|
|
157
|
+
/1{5,}/,
|
|
158
|
+
/abc{3,}/i,
|
|
159
|
+
/qwerty/i,
|
|
160
|
+
/password123/i,
|
|
161
|
+
/changeme/i,
|
|
162
|
+
/your[_-]?key/i,
|
|
163
|
+
/your[_-]?secret/i,
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Common false positive values
|
|
168
|
+
*/
|
|
169
|
+
export const FALSE_POSITIVE_VALUES = new Set([
|
|
170
|
+
'example',
|
|
171
|
+
'test',
|
|
172
|
+
'sample',
|
|
173
|
+
'demo',
|
|
174
|
+
'placeholder',
|
|
175
|
+
'your_key_here',
|
|
176
|
+
'your_secret_here',
|
|
177
|
+
'xxx',
|
|
178
|
+
'yyy',
|
|
179
|
+
'zzz',
|
|
180
|
+
'***',
|
|
181
|
+
'000000000000',
|
|
182
|
+
'111111111111',
|
|
183
|
+
'abcdefghijklmnopqrstuvwxyz',
|
|
184
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
185
|
+
'1234567890',
|
|
186
|
+
]);
|