guardrail-security 1.0.1 → 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 (66) hide show
  1. package/dist/attack-surface/analyzer.d.ts.map +1 -1
  2. package/dist/attack-surface/analyzer.js +3 -2
  3. package/dist/license/engine.d.ts.map +1 -1
  4. package/dist/license/engine.js +3 -2
  5. package/dist/sbom/generator.d.ts +42 -0
  6. package/dist/sbom/generator.d.ts.map +1 -1
  7. package/dist/sbom/generator.js +168 -7
  8. package/dist/secrets/allowlist.d.ts +38 -0
  9. package/dist/secrets/allowlist.d.ts.map +1 -0
  10. package/dist/secrets/allowlist.js +131 -0
  11. package/dist/secrets/config-loader.d.ts +25 -0
  12. package/dist/secrets/config-loader.d.ts.map +1 -0
  13. package/dist/secrets/config-loader.js +103 -0
  14. package/dist/secrets/contextual-risk.d.ts +19 -0
  15. package/dist/secrets/contextual-risk.d.ts.map +1 -0
  16. package/dist/secrets/contextual-risk.js +88 -0
  17. package/dist/secrets/git-scanner.d.ts +29 -0
  18. package/dist/secrets/git-scanner.d.ts.map +1 -0
  19. package/dist/secrets/git-scanner.js +109 -0
  20. package/dist/secrets/guardian.d.ts +70 -57
  21. package/dist/secrets/guardian.d.ts.map +1 -1
  22. package/dist/secrets/guardian.js +532 -240
  23. package/dist/secrets/index.d.ts +4 -0
  24. package/dist/secrets/index.d.ts.map +1 -1
  25. package/dist/secrets/index.js +11 -1
  26. package/dist/secrets/patterns.d.ts +39 -10
  27. package/dist/secrets/patterns.d.ts.map +1 -1
  28. package/dist/secrets/patterns.js +129 -71
  29. package/dist/secrets/pre-commit.d.ts.map +1 -1
  30. package/dist/secrets/pre-commit.js +1 -1
  31. package/dist/secrets/vault-integration.d.ts.map +1 -1
  32. package/dist/secrets/vault-integration.js +1 -0
  33. package/dist/supply-chain/detector.d.ts.map +1 -1
  34. package/dist/supply-chain/detector.js +4 -3
  35. package/dist/supply-chain/vulnerability-db.d.ts +89 -16
  36. package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
  37. package/dist/supply-chain/vulnerability-db.js +404 -115
  38. package/dist/utils/semver.d.ts +37 -0
  39. package/dist/utils/semver.d.ts.map +1 -0
  40. package/dist/utils/semver.js +109 -0
  41. package/package.json +17 -4
  42. package/src/__tests__/license/engine.test.ts +0 -250
  43. package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
  44. package/src/attack-surface/analyzer.ts +0 -152
  45. package/src/attack-surface/index.ts +0 -5
  46. package/src/index.ts +0 -21
  47. package/src/languages/index.ts +0 -91
  48. package/src/languages/java-analyzer.ts +0 -490
  49. package/src/languages/python-analyzer.ts +0 -498
  50. package/src/license/compatibility-matrix.ts +0 -366
  51. package/src/license/engine.ts +0 -345
  52. package/src/license/index.ts +0 -6
  53. package/src/sbom/generator.ts +0 -355
  54. package/src/sbom/index.ts +0 -5
  55. package/src/secrets/guardian.ts +0 -448
  56. package/src/secrets/index.ts +0 -10
  57. package/src/secrets/patterns.ts +0 -186
  58. package/src/secrets/pre-commit.ts +0 -158
  59. package/src/secrets/vault-integration.ts +0 -360
  60. package/src/secrets/vault-providers.ts +0 -446
  61. package/src/supply-chain/detector.ts +0 -252
  62. package/src/supply-chain/index.ts +0 -11
  63. package/src/supply-chain/malicious-db.ts +0 -103
  64. package/src/supply-chain/script-analyzer.ts +0 -194
  65. package/src/supply-chain/typosquat.ts +0 -302
  66. package/src/supply-chain/vulnerability-db.ts +0 -386
@@ -1,102 +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
- const database_1 = require("@guardrail/database");
5
- const core_1 = require("@guardrail/core");
6
- const patterns_1 = require("./patterns");
12
+ exports.secretsGuardian = exports.SecretsGuardian = exports.PrismaSecretStore = exports.NoopSecretStore = void 0;
7
13
  const fs_1 = require("fs");
8
14
  const glob_1 = require("glob");
9
15
  const path_1 = require("path");
10
- // Define SecretType locally since it's not exported from database
11
- var SecretType;
12
- (function (SecretType) {
13
- SecretType["API_KEY"] = "api_key";
14
- SecretType["PASSWORD"] = "password";
15
- SecretType["TOKEN"] = "token";
16
- SecretType["CERTIFICATE"] = "certificate";
17
- SecretType["PRIVATE_KEY"] = "private_key";
18
- SecretType["DATABASE_URL"] = "database_url";
19
- SecretType["JWT_SECRET"] = "jwt_secret";
20
- SecretType["AWS_ACCESS_KEY"] = "aws_access_key";
21
- SecretType["OTHER"] = "other";
22
- SecretType["AWS_SECRET_KEY"] = "aws_secret_key";
23
- SecretType["GITHUB_TOKEN"] = "github_token";
24
- SecretType["GOOGLE_API_KEY"] = "google_api_key";
25
- SecretType["STRIPE_KEY"] = "stripe_key";
26
- SecretType["JWT_TOKEN"] = "jwt_token";
27
- SecretType["SLACK_TOKEN"] = "slack_token";
28
- SecretType["API_KEY_GENERIC"] = "api_key_generic";
29
- SecretType["PASSWORD_GENERIC"] = "password_generic";
30
- })(SecretType || (exports.SecretType = SecretType = {}));
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;
30
+ }
31
+ async listDetections() {
32
+ return [];
33
+ }
34
+ }
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
+ }
121
+ }
122
+ exports.PrismaSecretStore = PrismaSecretStore;
31
123
  /**
32
124
  * Secrets & Credential Guardian
33
- *
34
- * Detects exposed secrets and credentials in code
35
125
  */
36
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
+ }
37
166
  /**
38
167
  * Scan content for secrets
39
168
  */
40
- async scanContent(content, filePath, options = {}) {
169
+ async scanContent(content, filePath, projectId, options = {}, allowlist) {
41
170
  const detections = [];
42
171
  const lines = content.split('\n');
43
- for (const pattern of patterns_1.SECRET_PATTERNS) {
44
- const matches = [...content.matchAll(new RegExp(pattern.pattern, 'g'))];
45
- for (const match of matches) {
46
- 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)
47
183
  continue;
48
- // Extract the secret value (first capturing group)
49
- const value = match[1] || match[0];
50
- // Calculate entropy
51
- const entropy = this.calculateEntropy(value);
52
- // Check minimum entropy requirement
53
- 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))
54
189
  continue;
55
- }
56
- // Find line number and column
57
- const beforeMatch = content.substring(0, match.index);
58
- const lineNumber = beforeMatch.split('\n').length;
59
- const lineStart = beforeMatch.lastIndexOf('\n') + 1;
60
- const column = match.index - lineStart + 1;
61
- // Get context
62
- const snippet = lines[lineNumber - 1] || '';
63
- // Check if it's a test value
64
- const isTest = this.isTestValue(value, snippet);
65
- // Check for false positives
66
- const isFalsePositive = this.isFalsePositive(value, pattern.type, snippet);
67
- if (isFalsePositive) {
190
+ // Test/example classification
191
+ const isTest = isTestPath || isTestValue(value, snippet);
192
+ // Known false positives
193
+ if (isFalsePositiveValue(value))
68
194
  continue;
69
- }
70
- // Calculate confidence
71
- const confidence = this.calculateConfidence(value, pattern, entropy, isTest);
72
- // Skip if below minimum confidence
73
- if (options.minConfidence && confidence < options.minConfidence) {
195
+ // Skip documented examples
196
+ if (isExamplePattern(value, filePath))
74
197
  continue;
75
- }
76
- // Skip if excluding tests
77
- 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)) {
78
231
  continue;
79
232
  }
80
- // Mask the value
81
- const maskedValue = this.maskValue(value);
82
- // Generate recommendation
83
- const recommendation = this.generateRecommendation(pattern.type, isTest);
84
- // 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
+ }
85
242
  const detection = {
86
- id: undefined, // Will be set when saved to database
243
+ projectId,
87
244
  filePath,
88
- secretType: pattern.type,
245
+ secretType: meta.type,
246
+ risk: adjustedRisk,
89
247
  maskedValue,
248
+ valueHash,
249
+ fingerprint,
90
250
  location: {
91
- line: lineNumber,
92
- column,
93
- snippet: snippet.trim(),
251
+ line: pos.line,
252
+ column: pos.column,
253
+ snippet,
94
254
  },
95
255
  confidence,
96
256
  entropy,
97
257
  isTest,
98
258
  isRevoked: false,
99
- recommendation,
259
+ recommendation: generateRecommendation(meta.type, adjustedRisk, isTest),
100
260
  };
101
261
  detections.push(detection);
102
262
  }
@@ -104,231 +264,363 @@ class SecretsGuardian {
104
264
  return detections;
105
265
  }
106
266
  /**
107
- * Scan entire project
267
+ * Scan an entire project directory
108
268
  */
109
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
+ }
110
279
  const excludePatterns = [
111
280
  '**/node_modules/**',
281
+ '**/.git/**',
112
282
  '**/dist/**',
113
283
  '**/build/**',
114
- '**/.git/**',
115
284
  '**/coverage/**',
285
+ '**/.next/**',
116
286
  '**/*.min.js',
117
- ...(options.excludePatterns || []),
287
+ '**/*.map',
288
+ ...(options.excludePatterns ?? []),
118
289
  ];
119
- // Find all files to scan
120
290
  const files = await (0, glob_1.glob)('**/*', {
121
291
  cwd: projectPath,
122
292
  ignore: excludePatterns,
123
293
  nodir: true,
294
+ dot: true,
295
+ follow: false,
124
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;
125
300
  const allDetections = [];
126
301
  let scannedFiles = 0;
127
- 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);
128
307
  try {
129
- const fullPath = (0, path_1.join)(projectPath, file);
130
- const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
131
- const detections = await this.scanContent(content, file, options);
132
- // Save to database
133
- for (const detection of detections) {
134
- try {
135
- // @ts-ignore - secretDetection may not exist in schema yet
136
- await database_1.prisma.secretDetection.create({
137
- data: {
138
- projectId: 'default',
139
- filePath: detection.filePath
140
- }
141
- });
142
- }
143
- catch (error) {
144
- // Table may not exist - continue
145
- }
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;
146
321
  }
147
- 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);
148
326
  scannedFiles++;
149
327
  }
150
- catch (error) {
151
- // Skip files that can't be read
152
- continue;
328
+ catch (err) {
329
+ this.logger.debug('Skipping unreadable file', { file, err: String(err) });
153
330
  }
154
- }
155
- // Generate summary
331
+ });
332
+ // Persist (best effort)
333
+ await this.store.saveDetections(projectId, allDetections);
334
+ // Summary
156
335
  const byType = {};
157
336
  const byRisk = { high: 0, medium: 0, low: 0 };
158
- for (const detection of allDetections) {
159
- byType[detection.secretType] = (byType[detection.secretType] || 0) + 1;
160
- if (detection.confidence >= 0.8) {
161
- byRisk.high++;
162
- }
163
- else if (detection.confidence >= 0.5) {
164
- byRisk.medium++;
165
- }
166
- else {
167
- byRisk.low++;
168
- }
337
+ for (const d of allDetections) {
338
+ byType[d.secretType] = (byType[d.secretType] ?? 0) + 1;
339
+ byRisk[d.risk]++;
169
340
  }
341
+ const allowlistSuppressed = allowlist ? (initialDetectionCount - allDetections.length) : 0;
170
342
  return {
171
343
  projectId,
172
344
  totalFiles: files.length,
173
345
  scannedFiles,
346
+ skippedFiles: skippedLarge + skippedBinary,
174
347
  detections: allDetections,
175
348
  summary: {
176
349
  totalSecrets: allDetections.length,
177
350
  byType,
178
351
  byRisk,
179
352
  },
353
+ performance: {
354
+ skippedLarge,
355
+ skippedBinary,
356
+ allowlistSuppressed,
357
+ customPatternsLoaded: this.customPatternsCount,
358
+ },
180
359
  };
181
360
  }
182
361
  /**
183
- * Calculate entropy for randomness detection
362
+ * Retrieve detections from store
184
363
  */
185
- calculateEntropy(str) {
186
- return (0, core_1.calculateEntropy)(str);
364
+ async getProjectReport(projectId) {
365
+ return this.store.listDetections(projectId);
187
366
  }
188
- /**
189
- * Check if likely test/example value
190
- */
191
- isTestValue(value, context) {
192
- const lowerValue = value.toLowerCase();
193
- const lowerContext = context.toLowerCase();
194
- // Check value itself
195
- for (const pattern of patterns_1.TEST_PATTERNS) {
196
- if (pattern.test(lowerValue)) {
197
- return true;
198
- }
199
- }
200
- // Check context
201
- if (lowerContext.includes('test') ||
202
- lowerContext.includes('example') ||
203
- lowerContext.includes('demo') ||
204
- lowerContext.includes('fixture') ||
205
- lowerContext.includes('mock')) {
206
- return true;
207
- }
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)
208
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++;
209
453
  }
210
- /**
211
- * Check for false positives
212
- */
213
- isFalsePositive(value, type, _context) {
214
- const lowerValue = value.toLowerCase();
215
- // Check against known false positives
216
- 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))
217
474
  return true;
218
- }
219
- // Check for placeholder patterns
220
- 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))
221
506
  return true;
222
- }
223
- // Check for repeated characters (likely placeholder)
224
- 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)
225
526
  return true;
226
- }
227
- // JWT-specific false positive checks
228
- if (type === SecretType.JWT_TOKEN) {
229
- // 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 {
230
558
  try {
231
- const parts = value.split('.');
232
- if (parts.length === 3 && parts[1]) {
233
- const payload = Buffer.from(parts[1], 'base64').toString();
234
- if (payload.length < 20) {
235
- return true;
236
- }
237
- }
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;
238
563
  }
239
564
  catch {
240
- // Invalid JWT, might be false positive
241
- return true;
565
+ confidence -= 0.3;
242
566
  }
243
567
  }
244
- return false;
245
568
  }
246
- /**
247
- * Calculate confidence score
248
- */
249
- calculateConfidence(value, pattern, entropy, isTest) {
250
- let confidence = 0.7; // Base confidence
251
- // Increase confidence for high entropy
252
- if (entropy > 4.5) {
253
- confidence += 0.2;
254
- }
255
- else if (entropy > 4.0) {
256
- confidence += 0.1;
257
- }
258
- // Decrease confidence for test values
259
- if (isTest) {
260
- confidence -= 0.3;
261
- }
262
- // Pattern-specific adjustments
263
- if (pattern.type === SecretType.AWS_ACCESS_KEY && value.startsWith('AKIA')) {
264
- confidence += 0.1;
265
- }
266
- if (pattern.type === SecretType.GITHUB_TOKEN && /^gh[pos]_/.test(value)) {
267
- confidence += 0.1;
268
- }
269
- 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
+ };
270
584
  }
271
- /**
272
- * Mask secret for safe logging
273
- */
274
- maskValue(value) {
275
- return (0, core_1.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
+ };
276
599
  }
277
- /**
278
- * Generate recommendation
279
- */
280
- generateRecommendation(type, isTest) {
281
- if (isTest) {
282
- return {
283
- action: 'remove',
284
- reason: 'Test credential detected in code',
285
- remediation: 'Remove test credentials and use mocking instead',
286
- };
287
- }
288
- // High-risk secrets require rotation
289
- const highRiskTypes = [
290
- 'AWS_SECRET_KEY',
291
- 'GITHUB_TOKEN',
292
- 'STRIPE_KEY',
293
- 'PRIVATE_KEY',
294
- ];
295
- if (highRiskTypes.includes(type)) {
296
- return {
297
- action: 'revoke_and_rotate',
298
- reason: 'High-risk credential exposed in code',
299
- remediation: 'Immediately revoke this credential and rotate to a new one. Store in secure vault or environment variables.',
300
- };
301
- }
302
- // Medium-risk can use env vars
600
+ if (risk === 'medium') {
303
601
  return {
304
602
  action: 'move_to_env',
305
603
  reason: 'Credential should not be hardcoded',
306
- 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.',
307
605
  };
308
606
  }
309
- /**
310
- * Get project secrets report
311
- */
312
- async getProjectReport(projectId) {
313
- // @ts-ignore - secretDetection may not exist in schema yet
314
- const detections = await database_1.prisma.secretDetection.findMany({
315
- where: { projectId },
316
- orderBy: { createdAt: 'desc' },
317
- });
318
- return detections.map((s) => ({
319
- id: s.id,
320
- filePath: s.filePath,
321
- secretType: s.secretType,
322
- maskedValue: s.maskedValue,
323
- location: s.location,
324
- confidence: s.confidence,
325
- entropy: s.entropy,
326
- isTest: s.isTest,
327
- isRevoked: s.isRevoked,
328
- recommendation: s.recommendation,
329
- }));
330
- }
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);
331
626
  }
332
- exports.SecretsGuardian = SecretsGuardian;
333
- // Export singleton
334
- exports.secretsGuardian = new SecretsGuardian();