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.
- package/dist/sbom/generator.d.ts +42 -0
- package/dist/sbom/generator.d.ts.map +1 -1
- package/dist/sbom/generator.js +168 -7
- package/dist/secrets/allowlist.d.ts +38 -0
- package/dist/secrets/allowlist.d.ts.map +1 -0
- package/dist/secrets/allowlist.js +131 -0
- package/dist/secrets/config-loader.d.ts +25 -0
- package/dist/secrets/config-loader.d.ts.map +1 -0
- package/dist/secrets/config-loader.js +103 -0
- package/dist/secrets/contextual-risk.d.ts +19 -0
- package/dist/secrets/contextual-risk.d.ts.map +1 -0
- package/dist/secrets/contextual-risk.js +88 -0
- package/dist/secrets/git-scanner.d.ts +29 -0
- package/dist/secrets/git-scanner.d.ts.map +1 -0
- package/dist/secrets/git-scanner.js +109 -0
- package/dist/secrets/guardian.d.ts +70 -57
- package/dist/secrets/guardian.d.ts.map +1 -1
- package/dist/secrets/guardian.js +531 -258
- package/dist/secrets/index.d.ts +4 -0
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +11 -1
- package/dist/secrets/patterns.d.ts +39 -10
- package/dist/secrets/patterns.d.ts.map +1 -1
- package/dist/secrets/patterns.js +129 -71
- package/dist/secrets/pre-commit.d.ts.map +1 -1
- package/dist/secrets/pre-commit.js +1 -1
- package/dist/secrets/vault-integration.d.ts.map +1 -1
- package/dist/secrets/vault-integration.js +1 -0
- package/dist/supply-chain/vulnerability-db.d.ts +89 -16
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
- package/dist/supply-chain/vulnerability-db.js +404 -115
- package/dist/utils/semver.d.ts +37 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +109 -0
- package/package.json +17 -3
- package/src/__tests__/license/engine.test.ts +0 -250
- package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
- package/src/attack-surface/analyzer.ts +0 -153
- package/src/attack-surface/index.ts +0 -5
- package/src/index.ts +0 -21
- package/src/languages/index.ts +0 -91
- package/src/languages/java-analyzer.ts +0 -490
- package/src/languages/python-analyzer.ts +0 -498
- package/src/license/compatibility-matrix.ts +0 -366
- package/src/license/engine.ts +0 -346
- package/src/license/index.ts +0 -6
- package/src/sbom/generator.ts +0 -355
- package/src/sbom/index.ts +0 -5
- package/src/secrets/guardian.ts +0 -468
- package/src/secrets/index.ts +0 -10
- package/src/secrets/patterns.ts +0 -186
- package/src/secrets/pre-commit.ts +0 -158
- package/src/secrets/vault-integration.ts +0 -360
- package/src/secrets/vault-providers.ts +0 -446
- package/src/supply-chain/detector.ts +0 -253
- package/src/supply-chain/index.ts +0 -11
- package/src/supply-chain/malicious-db.ts +0 -103
- package/src/supply-chain/script-analyzer.ts +0 -194
- package/src/supply-chain/typosquat.ts +0 -302
- package/src/supply-chain/vulnerability-db.ts +0 -386
package/dist/secrets/guardian.js
CHANGED
|
@@ -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.
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const p = count / len;
|
|
16
|
-
entropy -= p * Math.log2(p);
|
|
31
|
+
async listDetections() {
|
|
32
|
+
return [];
|
|
17
33
|
}
|
|
18
|
-
return entropy;
|
|
19
34
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
if (
|
|
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
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
243
|
+
projectId,
|
|
106
244
|
filePath,
|
|
107
|
-
secretType:
|
|
245
|
+
secretType: meta.type,
|
|
246
|
+
risk: adjustedRisk,
|
|
108
247
|
maskedValue,
|
|
248
|
+
valueHash,
|
|
249
|
+
fingerprint,
|
|
109
250
|
location: {
|
|
110
|
-
line:
|
|
111
|
-
column,
|
|
112
|
-
snippet
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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 (
|
|
170
|
-
|
|
171
|
-
continue;
|
|
328
|
+
catch (err) {
|
|
329
|
+
this.logger.debug('Skipping unreadable file', { file, err: String(err) });
|
|
172
330
|
}
|
|
173
|
-
}
|
|
174
|
-
//
|
|
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
|
|
178
|
-
byType[
|
|
179
|
-
|
|
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
|
-
*
|
|
362
|
+
* Retrieve detections from store
|
|
203
363
|
*/
|
|
204
|
-
|
|
205
|
-
return
|
|
364
|
+
async getProjectReport(projectId) {
|
|
365
|
+
return this.store.listDetections(projectId);
|
|
206
366
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
return true;
|
|
565
|
+
confidence -= 0.3;
|
|
261
566
|
}
|
|
262
567
|
}
|
|
263
|
-
return false;
|
|
264
568
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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();
|