guardrail-security 1.0.2 → 2.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.
Files changed (60) hide show
  1. package/dist/sbom/generator.d.ts +42 -0
  2. package/dist/sbom/generator.d.ts.map +1 -1
  3. package/dist/sbom/generator.js +168 -7
  4. package/dist/secrets/allowlist.d.ts +38 -0
  5. package/dist/secrets/allowlist.d.ts.map +1 -0
  6. package/dist/secrets/allowlist.js +131 -0
  7. package/dist/secrets/config-loader.d.ts +25 -0
  8. package/dist/secrets/config-loader.d.ts.map +1 -0
  9. package/dist/secrets/config-loader.js +103 -0
  10. package/dist/secrets/contextual-risk.d.ts +19 -0
  11. package/dist/secrets/contextual-risk.d.ts.map +1 -0
  12. package/dist/secrets/contextual-risk.js +88 -0
  13. package/dist/secrets/git-scanner.d.ts +29 -0
  14. package/dist/secrets/git-scanner.d.ts.map +1 -0
  15. package/dist/secrets/git-scanner.js +109 -0
  16. package/dist/secrets/guardian.d.ts +70 -57
  17. package/dist/secrets/guardian.d.ts.map +1 -1
  18. package/dist/secrets/guardian.js +531 -258
  19. package/dist/secrets/index.d.ts +4 -0
  20. package/dist/secrets/index.d.ts.map +1 -1
  21. package/dist/secrets/index.js +11 -1
  22. package/dist/secrets/patterns.d.ts +39 -10
  23. package/dist/secrets/patterns.d.ts.map +1 -1
  24. package/dist/secrets/patterns.js +129 -71
  25. package/dist/secrets/pre-commit.d.ts.map +1 -1
  26. package/dist/secrets/pre-commit.js +1 -1
  27. package/dist/secrets/vault-integration.d.ts.map +1 -1
  28. package/dist/secrets/vault-integration.js +1 -0
  29. package/dist/supply-chain/vulnerability-db.d.ts +89 -16
  30. package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
  31. package/dist/supply-chain/vulnerability-db.js +404 -115
  32. package/dist/utils/semver.d.ts +37 -0
  33. package/dist/utils/semver.d.ts.map +1 -0
  34. package/dist/utils/semver.js +109 -0
  35. package/package.json +17 -3
  36. package/src/__tests__/license/engine.test.ts +0 -250
  37. package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
  38. package/src/attack-surface/analyzer.ts +0 -153
  39. package/src/attack-surface/index.ts +0 -5
  40. package/src/index.ts +0 -21
  41. package/src/languages/index.ts +0 -91
  42. package/src/languages/java-analyzer.ts +0 -490
  43. package/src/languages/python-analyzer.ts +0 -498
  44. package/src/license/compatibility-matrix.ts +0 -366
  45. package/src/license/engine.ts +0 -346
  46. package/src/license/index.ts +0 -6
  47. package/src/sbom/generator.ts +0 -355
  48. package/src/sbom/index.ts +0 -5
  49. package/src/secrets/guardian.ts +0 -468
  50. package/src/secrets/index.ts +0 -10
  51. package/src/secrets/patterns.ts +0 -186
  52. package/src/secrets/pre-commit.ts +0 -158
  53. package/src/secrets/vault-integration.ts +0 -360
  54. package/src/secrets/vault-providers.ts +0 -446
  55. package/src/supply-chain/detector.ts +0 -253
  56. package/src/supply-chain/index.ts +0 -11
  57. package/src/supply-chain/malicious-db.ts +0 -103
  58. package/src/supply-chain/script-analyzer.ts +0 -194
  59. package/src/supply-chain/typosquat.ts +0 -302
  60. package/src/supply-chain/vulnerability-db.ts +0 -386
@@ -1,121 +1,262 @@
1
1
  "use strict";
2
+ /* secrets-guardian.ts
3
+ * Enterprise-grade secrets scanning with:
4
+ * - Correct regex flag handling (keeps i/m/s and adds g)
5
+ * - Fix index=0 bug
6
+ * - Binary + size guards
7
+ * - Optional persistence via injected store (no prisma=null)
8
+ * - Fingerprints/value hashes for dedupe
9
+ * - Concurrency controls
10
+ */
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.secretsGuardian = exports.SecretsGuardian = exports.SecretType = void 0;
4
- // Stub prisma for standalone use
5
- const prisma = null;
6
- // Local implementations of core utilities
7
- function calculateEntropy(str) {
8
- const len = str.length;
9
- const charCounts = {};
10
- for (const char of str) {
11
- charCounts[char] = (charCounts[char] || 0) + 1;
12
+ exports.secretsGuardian = exports.SecretsGuardian = exports.PrismaSecretStore = exports.NoopSecretStore = void 0;
13
+ const fs_1 = require("fs");
14
+ const glob_1 = require("glob");
15
+ const path_1 = require("path");
16
+ const crypto_1 = require("crypto");
17
+ const patterns_1 = require("./patterns");
18
+ const config_loader_1 = require("./config-loader");
19
+ const allowlist_1 = require("./allowlist");
20
+ const contextual_risk_1 = require("./contextual-risk");
21
+ const noopLogger = {
22
+ debug: () => undefined,
23
+ info: () => undefined,
24
+ warn: () => undefined,
25
+ error: () => undefined,
26
+ };
27
+ class NoopSecretStore {
28
+ async saveDetections() {
29
+ return;
12
30
  }
13
- let entropy = 0;
14
- for (const count of Object.values(charCounts)) {
15
- const p = count / len;
16
- entropy -= p * Math.log2(p);
31
+ async listDetections() {
32
+ return [];
17
33
  }
18
- return entropy;
19
34
  }
20
- function maskSensitiveValue(value) {
21
- if (value.length <= 8)
22
- return '***';
23
- return value.slice(0, 4) + '...' + value.slice(-4);
35
+ exports.NoopSecretStore = NoopSecretStore;
36
+ /**
37
+ * Minimal Prisma adapter (safe: stores hashes + masked only).
38
+ * NOTE: adjust model/table/columns to match your schema.
39
+ */
40
+ class PrismaSecretStore {
41
+ prisma;
42
+ constructor(prisma) {
43
+ this.prisma = prisma;
44
+ }
45
+ async saveDetections(projectId, detections) {
46
+ if (!this.prisma)
47
+ return;
48
+ // Best effort: if table/model doesn't exist, caller should not crash.
49
+ try {
50
+ // Upsert-ish by fingerprint (recommended).
51
+ // If you don't have unique(fingerprint), switch to createMany w/ skipDuplicates.
52
+ for (const d of detections) {
53
+ // @ts-ignore
54
+ await this.prisma.secretDetection.upsert({
55
+ where: { fingerprint: d.fingerprint },
56
+ update: {
57
+ confidence: d.confidence,
58
+ entropy: d.entropy,
59
+ isTest: d.isTest,
60
+ risk: d.risk,
61
+ maskedValue: d.maskedValue,
62
+ valueHash: d.valueHash,
63
+ location: d.location,
64
+ recommendation: d.recommendation,
65
+ secretType: d.secretType,
66
+ filePath: d.filePath,
67
+ projectId,
68
+ },
69
+ create: {
70
+ fingerprint: d.fingerprint,
71
+ projectId,
72
+ filePath: d.filePath,
73
+ secretType: d.secretType,
74
+ risk: d.risk,
75
+ maskedValue: d.maskedValue,
76
+ valueHash: d.valueHash,
77
+ location: d.location,
78
+ confidence: d.confidence,
79
+ entropy: d.entropy,
80
+ isTest: d.isTest,
81
+ isRevoked: d.isRevoked,
82
+ recommendation: d.recommendation,
83
+ },
84
+ });
85
+ }
86
+ }
87
+ catch {
88
+ // swallow (enterprise: you can log/telemetry this)
89
+ }
90
+ }
91
+ async listDetections(projectId) {
92
+ if (!this.prisma)
93
+ return [];
94
+ try {
95
+ // @ts-ignore
96
+ const rows = await this.prisma.secretDetection.findMany({
97
+ where: { projectId },
98
+ orderBy: { createdAt: 'desc' },
99
+ });
100
+ return rows.map((r) => ({
101
+ id: r.id,
102
+ projectId: r.projectId,
103
+ filePath: r.filePath,
104
+ secretType: r.secretType,
105
+ risk: (r.risk ?? 'medium'),
106
+ maskedValue: r.maskedValue,
107
+ valueHash: r.valueHash,
108
+ fingerprint: r.fingerprint,
109
+ location: r.location,
110
+ confidence: r.confidence,
111
+ entropy: r.entropy,
112
+ isTest: r.isTest,
113
+ isRevoked: r.isRevoked ?? false,
114
+ recommendation: r.recommendation,
115
+ }));
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ }
24
121
  }
25
- const patterns_1 = require("./patterns");
26
- const fs_1 = require("fs");
27
- const glob_1 = require("glob");
28
- const path_1 = require("path");
29
- // Define SecretType locally since it's not exported from database
30
- var SecretType;
31
- (function (SecretType) {
32
- SecretType["API_KEY"] = "api_key";
33
- SecretType["PASSWORD"] = "password";
34
- SecretType["TOKEN"] = "token";
35
- SecretType["CERTIFICATE"] = "certificate";
36
- SecretType["PRIVATE_KEY"] = "private_key";
37
- SecretType["DATABASE_URL"] = "database_url";
38
- SecretType["JWT_SECRET"] = "jwt_secret";
39
- SecretType["AWS_ACCESS_KEY"] = "aws_access_key";
40
- SecretType["OTHER"] = "other";
41
- SecretType["AWS_SECRET_KEY"] = "aws_secret_key";
42
- SecretType["GITHUB_TOKEN"] = "github_token";
43
- SecretType["GOOGLE_API_KEY"] = "google_api_key";
44
- SecretType["STRIPE_KEY"] = "stripe_key";
45
- SecretType["JWT_TOKEN"] = "jwt_token";
46
- SecretType["SLACK_TOKEN"] = "slack_token";
47
- SecretType["API_KEY_GENERIC"] = "api_key_generic";
48
- SecretType["PASSWORD_GENERIC"] = "password_generic";
49
- })(SecretType || (exports.SecretType = SecretType = {}));
122
+ exports.PrismaSecretStore = PrismaSecretStore;
50
123
  /**
51
124
  * Secrets & Credential Guardian
52
- *
53
- * Detects exposed secrets and credentials in code
54
125
  */
55
126
  class SecretsGuardian {
127
+ store;
128
+ logger;
129
+ compiledPatterns;
130
+ customPatternsCount = 0;
131
+ constructor(opts) {
132
+ this.store = opts?.store ?? new NoopSecretStore();
133
+ this.logger = opts?.logger ?? noopLogger;
134
+ this.compiledPatterns = patterns_1.SECRET_PATTERNS.map((p) => ({
135
+ meta: p,
136
+ regex: toGlobalRegex(p.pattern),
137
+ }));
138
+ }
139
+ /**
140
+ * Load custom patterns from project config
141
+ */
142
+ loadCustomPatterns(projectPath) {
143
+ try {
144
+ const customPatterns = (0, config_loader_1.loadCustomPatterns)(projectPath);
145
+ if (customPatterns.length > 0) {
146
+ const compiled = customPatterns.map((p) => ({
147
+ meta: p,
148
+ regex: toGlobalRegex(p.pattern),
149
+ }));
150
+ this.compiledPatterns = [...this.compiledPatterns, ...compiled];
151
+ this.customPatternsCount = customPatterns.length;
152
+ this.logger.info(`Loaded ${customPatterns.length} custom patterns`);
153
+ }
154
+ }
155
+ catch (err) {
156
+ if (err instanceof config_loader_1.ConfigValidationError) {
157
+ this.logger.error('Custom patterns validation failed', {
158
+ message: err.message,
159
+ details: err.details,
160
+ });
161
+ throw err;
162
+ }
163
+ this.logger.warn('Failed to load custom patterns', { error: String(err) });
164
+ }
165
+ }
56
166
  /**
57
167
  * Scan content for secrets
58
168
  */
59
- async scanContent(content, filePath, options = {}) {
169
+ async scanContent(content, filePath, projectId, options = {}, allowlist) {
60
170
  const detections = [];
61
171
  const lines = content.split('\n');
62
- for (const pattern of patterns_1.SECRET_PATTERNS) {
63
- const matches = [...content.matchAll(new RegExp(pattern.pattern, 'g'))];
64
- for (const match of matches) {
65
- if (!match.index)
172
+ const lineStarts = computeLineStarts(content);
173
+ const isTestPath = pathLooksLikeTest(filePath);
174
+ // Track seen values at each line to prevent duplicates from multiple patterns
175
+ const seenAtLine = new Set();
176
+ for (const { meta, regex } of this.compiledPatterns) {
177
+ for (const match of content.matchAll(regex)) {
178
+ if (match.index === undefined)
179
+ continue; // FIX: don't drop index 0
180
+ const rawCandidate = extractCandidate(match, meta);
181
+ const value = normalizeCandidate(rawCandidate);
182
+ if (!value)
66
183
  continue;
67
- // Extract the secret value (first capturing group)
68
- const value = match[1] || match[0];
69
- // Calculate entropy
70
- const entropy = this.calculateEntropy(value);
71
- // Check minimum entropy requirement
72
- if (pattern.minEntropy && entropy < pattern.minEntropy) {
184
+ // Position
185
+ const pos = positionFromIndex(lineStarts, match.index);
186
+ const snippet = (lines[pos.line - 1] ?? '').trim();
187
+ // Context exclusions (schema/validator/etc)
188
+ if (isExcludedByContext(snippet))
73
189
  continue;
74
- }
75
- // Find line number and column
76
- const beforeMatch = content.substring(0, match.index);
77
- const lineNumber = beforeMatch.split('\n').length;
78
- const lineStart = beforeMatch.lastIndexOf('\n') + 1;
79
- const column = match.index - lineStart + 1;
80
- // Get context
81
- const snippet = lines[lineNumber - 1] || '';
82
- // Check if it's a test value
83
- const isTest = this.isTestValue(value, snippet);
84
- // Check for false positives
85
- const isFalsePositive = this.isFalsePositive(value, pattern.type, snippet);
86
- if (isFalsePositive) {
190
+ // Test/example classification
191
+ const isTest = isTestPath || isTestValue(value, snippet);
192
+ // Known false positives
193
+ if (isFalsePositiveValue(value))
87
194
  continue;
88
- }
89
- // Calculate confidence
90
- const confidence = this.calculateConfidence(value, pattern, entropy, isTest);
91
- // Skip if below minimum confidence
92
- if (options.minConfidence && confidence < options.minConfidence) {
195
+ // Skip documented examples
196
+ if (isExamplePattern(value, filePath))
93
197
  continue;
94
- }
95
- // Skip if excluding tests
96
- if (options.excludeTests && isTest) {
198
+ // Entropy
199
+ const entropy = calculateEntropy(value);
200
+ if (meta.minEntropy && entropy < meta.minEntropy)
201
+ continue;
202
+ // Confidence
203
+ const confidence = calculateConfidence({
204
+ value,
205
+ meta,
206
+ entropy,
207
+ isTest,
208
+ snippet,
209
+ });
210
+ if (options.minConfidence !== undefined && confidence < options.minConfidence)
211
+ continue;
212
+ if (options.excludeTests && isTest)
213
+ continue;
214
+ const maskedValue = meta.redact ? meta.redact(value, match) : maskSensitiveValue(value);
215
+ const valueHash = sha256(value);
216
+ // Dedupe: skip if same value already detected on same line
217
+ const lineKey = `${pos.line}:${valueHash}`;
218
+ if (seenAtLine.has(lineKey))
219
+ continue;
220
+ seenAtLine.add(lineKey);
221
+ const fingerprint = sha256([
222
+ projectId,
223
+ filePath,
224
+ meta.type,
225
+ valueHash,
226
+ String(pos.line),
227
+ String(pos.column),
228
+ ].join('|'));
229
+ // Check allowlist
230
+ if (allowlist && allowlist.isAllowlisted(fingerprint)) {
97
231
  continue;
98
232
  }
99
- // Mask the value
100
- const maskedValue = this.maskValue(value);
101
- // Generate recommendation
102
- const recommendation = this.generateRecommendation(pattern.type, isTest);
103
- // Create detection object
233
+ // Adjust risk by context if enabled
234
+ let adjustedRisk = meta.risk;
235
+ if (options.useContextualRisk !== false) {
236
+ adjustedRisk = (0, contextual_risk_1.adjustRiskByContext)({
237
+ filePath,
238
+ entropy,
239
+ originalRisk: meta.risk,
240
+ });
241
+ }
104
242
  const detection = {
105
- id: undefined, // Will be set when saved to database
243
+ projectId,
106
244
  filePath,
107
- secretType: pattern.type,
245
+ secretType: meta.type,
246
+ risk: adjustedRisk,
108
247
  maskedValue,
248
+ valueHash,
249
+ fingerprint,
109
250
  location: {
110
- line: lineNumber,
111
- column,
112
- snippet: snippet.trim(),
251
+ line: pos.line,
252
+ column: pos.column,
253
+ snippet,
113
254
  },
114
255
  confidence,
115
256
  entropy,
116
257
  isTest,
117
258
  isRevoked: false,
118
- recommendation,
259
+ recommendation: generateRecommendation(meta.type, adjustedRisk, isTest),
119
260
  };
120
261
  detections.push(detection);
121
262
  }
@@ -123,231 +264,363 @@ class SecretsGuardian {
123
264
  return detections;
124
265
  }
125
266
  /**
126
- * Scan entire project
267
+ * Scan an entire project directory
127
268
  */
128
269
  async scanProject(projectPath, projectId, options = {}) {
270
+ // Load custom patterns if enabled
271
+ if (options.useCustomPatterns !== false) {
272
+ this.loadCustomPatterns(projectPath);
273
+ }
274
+ // Load allowlist if enabled
275
+ let allowlist;
276
+ if (options.useAllowlist !== false) {
277
+ allowlist = new allowlist_1.Allowlist(projectPath);
278
+ }
129
279
  const excludePatterns = [
130
280
  '**/node_modules/**',
281
+ '**/.git/**',
131
282
  '**/dist/**',
132
283
  '**/build/**',
133
- '**/.git/**',
134
284
  '**/coverage/**',
285
+ '**/.next/**',
135
286
  '**/*.min.js',
136
- ...(options.excludePatterns || []),
287
+ '**/*.map',
288
+ ...(options.excludePatterns ?? []),
137
289
  ];
138
- // Find all files to scan
139
290
  const files = await (0, glob_1.glob)('**/*', {
140
291
  cwd: projectPath,
141
292
  ignore: excludePatterns,
142
293
  nodir: true,
294
+ dot: true,
295
+ follow: false,
143
296
  });
297
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? 2 * 1024 * 1024; // 2MB default
298
+ const concurrency = Math.max(1, Math.min(64, options.concurrency ?? 8));
299
+ const skipBinaryFiles = options.skipBinaryFiles ?? true;
144
300
  const allDetections = [];
145
301
  let scannedFiles = 0;
146
- for (const file of files) {
302
+ let skippedLarge = 0;
303
+ let skippedBinary = 0;
304
+ const initialDetectionCount = 0;
305
+ await runWithConcurrency(files, concurrency, async (file) => {
306
+ const fullPath = (0, path_1.join)(projectPath, file);
147
307
  try {
148
- const fullPath = (0, path_1.join)(projectPath, file);
149
- const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
150
- const detections = await this.scanContent(content, file, options);
151
- // Save to database
152
- for (const detection of detections) {
153
- try {
154
- // @ts-ignore - secretDetection may not exist in schema yet
155
- await prisma.secretDetection.create({
156
- data: {
157
- projectId: 'default',
158
- filePath: detection.filePath
159
- }
160
- });
161
- }
162
- catch (error) {
163
- // Table may not exist - continue
164
- }
308
+ const st = (0, fs_1.statSync)(fullPath);
309
+ if (!st.isFile())
310
+ return;
311
+ if (st.size > maxFileSizeBytes) {
312
+ this.logger.debug('Skipping large file', { file, size: st.size });
313
+ skippedLarge++;
314
+ return;
315
+ }
316
+ const buf = (0, fs_1.readFileSync)(fullPath);
317
+ if (skipBinaryFiles && looksBinary(buf)) {
318
+ this.logger.debug('Skipping binary file', { file });
319
+ skippedBinary++;
320
+ return;
165
321
  }
166
- allDetections.push(...detections);
322
+ const content = buf.toString('utf-8');
323
+ const detections = await this.scanContent(content, file, projectId, options, allowlist);
324
+ if (detections.length)
325
+ allDetections.push(...detections);
167
326
  scannedFiles++;
168
327
  }
169
- catch (error) {
170
- // Skip files that can't be read
171
- continue;
328
+ catch (err) {
329
+ this.logger.debug('Skipping unreadable file', { file, err: String(err) });
172
330
  }
173
- }
174
- // Generate summary
331
+ });
332
+ // Persist (best effort)
333
+ await this.store.saveDetections(projectId, allDetections);
334
+ // Summary
175
335
  const byType = {};
176
336
  const byRisk = { high: 0, medium: 0, low: 0 };
177
- for (const detection of allDetections) {
178
- byType[detection.secretType] = (byType[detection.secretType] || 0) + 1;
179
- if (detection.confidence >= 0.8) {
180
- byRisk.high++;
181
- }
182
- else if (detection.confidence >= 0.5) {
183
- byRisk.medium++;
184
- }
185
- else {
186
- byRisk.low++;
187
- }
337
+ for (const d of allDetections) {
338
+ byType[d.secretType] = (byType[d.secretType] ?? 0) + 1;
339
+ byRisk[d.risk]++;
188
340
  }
341
+ const allowlistSuppressed = allowlist ? (initialDetectionCount - allDetections.length) : 0;
189
342
  return {
190
343
  projectId,
191
344
  totalFiles: files.length,
192
345
  scannedFiles,
346
+ skippedFiles: skippedLarge + skippedBinary,
193
347
  detections: allDetections,
194
348
  summary: {
195
349
  totalSecrets: allDetections.length,
196
350
  byType,
197
351
  byRisk,
198
352
  },
353
+ performance: {
354
+ skippedLarge,
355
+ skippedBinary,
356
+ allowlistSuppressed,
357
+ customPatternsLoaded: this.customPatternsCount,
358
+ },
199
359
  };
200
360
  }
201
361
  /**
202
- * Calculate entropy for randomness detection
362
+ * Retrieve detections from store
203
363
  */
204
- calculateEntropy(str) {
205
- return calculateEntropy(str);
364
+ async getProjectReport(projectId) {
365
+ return this.store.listDetections(projectId);
206
366
  }
207
- /**
208
- * Check if likely test/example value
209
- */
210
- isTestValue(value, context) {
211
- const lowerValue = value.toLowerCase();
212
- const lowerContext = context.toLowerCase();
213
- // Check value itself
214
- for (const pattern of patterns_1.TEST_PATTERNS) {
215
- if (pattern.test(lowerValue)) {
216
- return true;
217
- }
218
- }
219
- // Check context
220
- if (lowerContext.includes('test') ||
221
- lowerContext.includes('example') ||
222
- lowerContext.includes('demo') ||
223
- lowerContext.includes('fixture') ||
224
- lowerContext.includes('mock')) {
225
- return true;
226
- }
367
+ }
368
+ exports.SecretsGuardian = SecretsGuardian;
369
+ /** Singleton (uses Noop store unless you wire it) */
370
+ exports.secretsGuardian = new SecretsGuardian();
371
+ /* ------------------------------ helpers ------------------------------ */
372
+ function toGlobalRegex(re) {
373
+ // Preserve existing flags (i/m/s/u/y) and add g if missing.
374
+ const flags = re.flags.includes('g') ? re.flags : re.flags + 'g';
375
+ return new RegExp(re.source, flags);
376
+ }
377
+ function extractCandidate(match, meta) {
378
+ const group = meta.valueGroup !== undefined
379
+ ? meta.valueGroup
380
+ : match.length > 1
381
+ ? 1
382
+ : 0;
383
+ const v = match[group] ?? match[0] ?? '';
384
+ return String(v);
385
+ }
386
+ function normalizeCandidate(v) {
387
+ return v
388
+ .trim()
389
+ .replace(/^['"`]/, '')
390
+ .replace(/['"`]$/, '')
391
+ .replace(/[;,)\]]+$/, '')
392
+ .trim();
393
+ }
394
+ function sha256(input) {
395
+ return (0, crypto_1.createHash)('sha256').update(input, 'utf8').digest('hex');
396
+ }
397
+ function calculateEntropy(str) {
398
+ const len = str.length;
399
+ if (len === 0)
400
+ return 0;
401
+ const counts = {};
402
+ for (const ch of str)
403
+ counts[ch] = (counts[ch] ?? 0) + 1;
404
+ let entropy = 0;
405
+ for (const n of Object.values(counts)) {
406
+ const p = n / len;
407
+ entropy -= p * Math.log2(p);
408
+ }
409
+ return entropy;
410
+ }
411
+ function maskSensitiveValue(value) {
412
+ const v = value.trim();
413
+ if (v.length <= 12)
414
+ return '***';
415
+ return `${v.slice(0, 4)}...${v.slice(-4)}`;
416
+ }
417
+ function computeLineStarts(content) {
418
+ const starts = [0];
419
+ for (let i = 0; i < content.length; i++) {
420
+ if (content.charCodeAt(i) === 10 /* \n */)
421
+ starts.push(i + 1);
422
+ }
423
+ return starts;
424
+ }
425
+ function positionFromIndex(lineStarts, index) {
426
+ // Binary search for rightmost line start <= index
427
+ let lo = 0;
428
+ let hi = lineStarts.length - 1;
429
+ while (lo <= hi) {
430
+ const mid = (lo + hi) >> 1;
431
+ if ((lineStarts[mid] ?? 0) <= index)
432
+ lo = mid + 1;
433
+ else
434
+ hi = mid - 1;
435
+ }
436
+ const line = Math.max(1, hi + 1);
437
+ const column = index - (lineStarts[hi] ?? 0) + 1;
438
+ return { line, column };
439
+ }
440
+ function looksBinary(buf) {
441
+ // quick heuristic: null byte presence or high non-text ratio
442
+ const len = Math.min(buf.length, 8000);
443
+ if (len === 0)
227
444
  return false;
445
+ let suspicious = 0;
446
+ for (let i = 0; i < len; i++) {
447
+ const b = buf[i] ?? 0;
448
+ if (b === 0)
449
+ return true; // null byte
450
+ // allow common whitespace + UTF-8 bytes; count control chars
451
+ if (b < 9 || (b > 13 && b < 32))
452
+ suspicious++;
228
453
  }
229
- /**
230
- * Check for false positives
231
- */
232
- isFalsePositive(value, type, _context) {
233
- const lowerValue = value.toLowerCase();
234
- // Check against known false positives
235
- if (patterns_1.FALSE_POSITIVE_VALUES.has(lowerValue)) {
454
+ return suspicious / len > 0.15;
455
+ }
456
+ function pathLooksLikeTest(filePath) {
457
+ const p = filePath.toLowerCase();
458
+ return (p.includes('/__tests__/') ||
459
+ p.includes('\\__tests__\\') ||
460
+ p.includes('/__mocks__/') ||
461
+ p.includes('\\__mocks__\\') ||
462
+ p.includes('/test/') ||
463
+ p.includes('\\test\\') ||
464
+ p.endsWith('.spec.ts') ||
465
+ p.endsWith('.spec.tsx') ||
466
+ p.endsWith('.test.ts') ||
467
+ p.endsWith('.test.tsx'));
468
+ }
469
+ function isTestValue(value, contextLine) {
470
+ const v = value.toLowerCase();
471
+ const c = contextLine.toLowerCase();
472
+ for (const re of patterns_1.TEST_PATTERNS) {
473
+ if (re.test(v) || re.test(c))
236
474
  return true;
237
- }
238
- // Check for placeholder patterns
239
- if (/^(x+|0+|1+|a+)$/i.test(value)) {
475
+ }
476
+ // Super-common placeholders
477
+ if (/(^x{6,}$|^0{6,}$|^1{6,}$|^a{6,}$)/i.test(value))
478
+ return true;
479
+ if (/(.)\1{10,}/.test(value))
480
+ return true;
481
+ return false;
482
+ }
483
+ function isExcludedByContext(contextLine) {
484
+ const c = contextLine.toLowerCase();
485
+ return patterns_1.CONTEXT_EXCLUSION_PATTERNS.some((re) => re.test(c));
486
+ }
487
+ function isExamplePattern(value, filePath) {
488
+ const lower = value.toLowerCase();
489
+ const pathLower = filePath.toLowerCase();
490
+ // Skip patterns.ts examples (documented examples)
491
+ if (pathLower.includes('patterns.ts') || pathLower.includes('patterns.js')) {
492
+ return true;
493
+ }
494
+ // Common example values
495
+ const exampleValues = [
496
+ 'akiaiosfodnn7example',
497
+ 'asiaiosfodnn7example',
498
+ 'wjalrxutnfemi/k7mdeng/bpxrficyexamplekey',
499
+ 'aizasydagmwka4jsxz-hjgw7isln_3nambgewqe',
500
+ 'sk_live_1234567890abcdefghijklmn',
501
+ 'xoxb-0000000000-0000000000-0000000000',
502
+ 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
503
+ ];
504
+ for (const ex of exampleValues) {
505
+ if (lower.includes(ex) || ex.includes(lower))
240
506
  return true;
241
- }
242
- // Check for repeated characters (likely placeholder)
243
- if (/(.)\1{10,}/.test(value)) {
507
+ }
508
+ // "EXAMPLE" in the value
509
+ if (lower.includes('example'))
510
+ return true;
511
+ return false;
512
+ }
513
+ function isFalsePositiveValue(value) {
514
+ const lower = value.toLowerCase();
515
+ if (patterns_1.FALSE_POSITIVE_VALUES.has(lower))
516
+ return true;
517
+ // obvious placeholders
518
+ if (/^(x+|0+|1+|a+)$/i.test(value))
519
+ return true;
520
+ if (/(.)\1{10,}/.test(value))
521
+ return true;
522
+ // extremely low variety in a long string
523
+ if (value.length >= 24) {
524
+ const unique = new Set(value).size;
525
+ if (unique <= 3)
244
526
  return true;
245
- }
246
- // JWT-specific false positive checks
247
- if (type === SecretType.JWT_TOKEN) {
248
- // Very simple/short payload might be example
527
+ }
528
+ return false;
529
+ }
530
+ function decodeBase64Url(segment) {
531
+ // base64url -> base64
532
+ const b64 = segment.replace(/-/g, '+').replace(/_/g, '/');
533
+ const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4));
534
+ return Buffer.from(b64 + pad, 'base64').toString('utf8');
535
+ }
536
+ function calculateConfidence(args) {
537
+ const { value, meta, entropy, isTest } = args;
538
+ // Base by risk: conservative defaults
539
+ let confidence = meta.risk === 'high' ? 0.8 : meta.risk === 'medium' ? 0.7 : 0.6;
540
+ // Entropy boosts (generic)
541
+ if (entropy >= 4.8)
542
+ confidence += 0.15;
543
+ else if (entropy >= 4.2)
544
+ confidence += 0.1;
545
+ else if (entropy >= 3.6)
546
+ confidence += 0.05;
547
+ // Pattern-specific sanity checks
548
+ if (meta.type === patterns_1.SecretType.AWS_ACCESS_KEY && /^(AKIA|ASIA)/.test(value))
549
+ confidence += 0.1;
550
+ if (meta.type === patterns_1.SecretType.GITHUB_TOKEN && /^(ghp|gho|ghu|ghs|ghr)_/.test(value))
551
+ confidence += 0.1;
552
+ // JWT: validate structure to reduce false positives
553
+ if (meta.type === patterns_1.SecretType.JWT_TOKEN) {
554
+ const parts = value.split('.');
555
+ if (parts.length !== 3)
556
+ confidence -= 0.25;
557
+ else {
249
558
  try {
250
- const parts = value.split('.');
251
- if (parts.length === 3 && parts[1]) {
252
- const payload = Buffer.from(parts[1], 'base64').toString();
253
- if (payload.length < 20) {
254
- return true;
255
- }
256
- }
559
+ const payload = decodeBase64Url(parts[1] ?? '');
560
+ // If it doesn't look like JSON, down-weight
561
+ if (!payload.includes('{') || payload.length < 20)
562
+ confidence -= 0.2;
257
563
  }
258
564
  catch {
259
- // Invalid JWT, might be false positive
260
- return true;
565
+ confidence -= 0.3;
261
566
  }
262
567
  }
263
- return false;
264
568
  }
265
- /**
266
- * Calculate confidence score
267
- */
268
- calculateConfidence(value, pattern, entropy, isTest) {
269
- let confidence = 0.7; // Base confidence
270
- // Increase confidence for high entropy
271
- if (entropy > 4.5) {
272
- confidence += 0.2;
273
- }
274
- else if (entropy > 4.0) {
275
- confidence += 0.1;
276
- }
277
- // Decrease confidence for test values
278
- if (isTest) {
279
- confidence -= 0.3;
280
- }
281
- // Pattern-specific adjustments
282
- if (pattern.type === SecretType.AWS_ACCESS_KEY && value.startsWith('AKIA')) {
283
- confidence += 0.1;
284
- }
285
- if (pattern.type === SecretType.GITHUB_TOKEN && /^gh[pos]_/.test(value)) {
286
- confidence += 0.1;
287
- }
288
- return Math.max(0, Math.min(1, confidence));
569
+ // Down-weight test/example
570
+ if (isTest)
571
+ confidence -= 0.3;
572
+ return clamp01(confidence);
573
+ }
574
+ function clamp01(n) {
575
+ return Math.max(0, Math.min(1, n));
576
+ }
577
+ function generateRecommendation(type, risk, isTest) {
578
+ if (isTest) {
579
+ return {
580
+ action: 'remove',
581
+ reason: 'Test/example credential detected in codebase',
582
+ remediation: 'Remove the credential from the repo. Use environment variables for local dev, and use mocks/fixtures that do not contain real secrets.',
583
+ };
289
584
  }
290
- /**
291
- * Mask secret for safe logging
292
- */
293
- maskValue(value) {
294
- return maskSensitiveValue(value);
585
+ // Always rotate on these types
586
+ const rotateTypes = new Set([
587
+ patterns_1.SecretType.PRIVATE_KEY,
588
+ patterns_1.SecretType.AWS_SECRET_KEY,
589
+ patterns_1.SecretType.GITHUB_TOKEN,
590
+ patterns_1.SecretType.STRIPE_KEY,
591
+ patterns_1.SecretType.DATABASE_URL,
592
+ ]);
593
+ if (rotateTypes.has(type) || risk === 'high') {
594
+ return {
595
+ action: 'revoke_and_rotate',
596
+ reason: 'High-risk credential exposure',
597
+ remediation: 'Immediately revoke/rotate the credential. Audit usage, invalidate sessions if applicable, and re-issue via a secrets manager (AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault).',
598
+ };
295
599
  }
296
- /**
297
- * Generate recommendation
298
- */
299
- generateRecommendation(type, isTest) {
300
- if (isTest) {
301
- return {
302
- action: 'remove',
303
- reason: 'Test credential detected in code',
304
- remediation: 'Remove test credentials and use mocking instead',
305
- };
306
- }
307
- // High-risk secrets require rotation
308
- const highRiskTypes = [
309
- 'AWS_SECRET_KEY',
310
- 'GITHUB_TOKEN',
311
- 'STRIPE_KEY',
312
- 'PRIVATE_KEY',
313
- ];
314
- if (highRiskTypes.includes(type)) {
315
- return {
316
- action: 'revoke_and_rotate',
317
- reason: 'High-risk credential exposed in code',
318
- remediation: 'Immediately revoke this credential and rotate to a new one. Store in secure vault or environment variables.',
319
- };
320
- }
321
- // Medium-risk can use env vars
600
+ if (risk === 'medium') {
322
601
  return {
323
602
  action: 'move_to_env',
324
603
  reason: 'Credential should not be hardcoded',
325
- remediation: 'Move to environment variables or secure vault (e.g., AWS Secrets Manager, HashiCorp Vault)',
604
+ remediation: 'Move the value to environment variables (or a secrets manager) and reference it at runtime. Ensure CI/CD injects it securely.',
326
605
  };
327
606
  }
328
- /**
329
- * Get project secrets report
330
- */
331
- async getProjectReport(projectId) {
332
- // @ts-ignore - secretDetection may not exist in schema yet
333
- const detections = await prisma.secretDetection.findMany({
334
- where: { projectId },
335
- orderBy: { createdAt: 'desc' },
336
- });
337
- return detections.map((s) => ({
338
- id: s.id,
339
- filePath: s.filePath,
340
- secretType: s.secretType,
341
- maskedValue: s.maskedValue,
342
- location: s.location,
343
- confidence: s.confidence,
344
- entropy: s.entropy,
345
- isTest: s.isTest,
346
- isRevoked: s.isRevoked,
347
- recommendation: s.recommendation,
348
- }));
349
- }
607
+ return {
608
+ action: 'use_vault',
609
+ reason: 'Sensitive value detected',
610
+ remediation: 'Store in a secrets manager and load via runtime injection. Consider tightening repo protections and adding pre-commit scanning.',
611
+ };
612
+ }
613
+ async function runWithConcurrency(items, concurrency, worker) {
614
+ let i = 0;
615
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
616
+ while (true) {
617
+ const idx = i++;
618
+ if (idx >= items.length)
619
+ return;
620
+ const item = items[idx];
621
+ if (item !== undefined)
622
+ await worker(item);
623
+ }
624
+ });
625
+ await Promise.all(runners);
350
626
  }
351
- exports.SecretsGuardian = SecretsGuardian;
352
- // Export singleton
353
- exports.secretsGuardian = new SecretsGuardian();