keysentinel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * KeySentinel - GitHub Action for scanning PR diffs for secrets
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const core = __importStar(require("@actions/core"));
40
+ const mask_1 = require("./mask");
41
+ const config_1 = require("./config");
42
+ const patterns_1 = require("./patterns");
43
+ const github_1 = require("./github");
44
+ const scanner_1 = require("./scanner");
45
+ async function run() {
46
+ try {
47
+ core.info('KeySentinel starting...');
48
+ const config = (0, config_1.loadConfig)();
49
+ core.debug(`Config: failOn=${config.failOn}, maxFiles=${config.maxFiles}`);
50
+ const token = core.getInput('github_token', { required: true });
51
+ const octokit = (0, github_1.createOctokit)(token);
52
+ const prContext = (0, github_1.getPRContext)();
53
+ if (!prContext) {
54
+ core.warning('Not running in a pull request context. Skipping scan.');
55
+ return;
56
+ }
57
+ const { owner, repo, pullNumber } = prContext;
58
+ core.info(`Scanning PR #${pullNumber} in ${owner}/${repo}`);
59
+ const files = await (0, github_1.getPRFiles)(octokit, owner, repo, pullNumber, config.maxFiles);
60
+ core.info(`Found ${files.length} file(s) in PR`);
61
+ if (files.length === 0) {
62
+ core.info('No files to scan');
63
+ return;
64
+ }
65
+ const patterns = (0, patterns_1.getEnabledPatterns)(config.patterns);
66
+ const headSha = (0, github_1.getPRHeadSha)();
67
+ const allFindings = [];
68
+ let filesScanned = 0;
69
+ let filesSkipped = 0;
70
+ for (const file of files) {
71
+ if (file.status === 'removed') {
72
+ filesSkipped++;
73
+ continue;
74
+ }
75
+ if ((0, config_1.shouldIgnoreFile)(file.filename, config.ignore)) {
76
+ core.debug(`Ignoring file: ${file.filename}`);
77
+ filesSkipped++;
78
+ continue;
79
+ }
80
+ let addedLines = [];
81
+ if (file.patch) {
82
+ addedLines = (0, scanner_1.extractAddedLines)(file.patch);
83
+ }
84
+ else {
85
+ core.debug(`No patch for ${file.filename}, fetching content`);
86
+ const content = await (0, github_1.getFileContent)(octokit, owner, repo, file.filename, headSha);
87
+ if (content) {
88
+ const lines = content.split('\n');
89
+ addedLines = lines.map((line, i) => ({ line, lineNumber: i + 1 }));
90
+ }
91
+ }
92
+ if (addedLines.length === 0) {
93
+ filesSkipped++;
94
+ continue;
95
+ }
96
+ const fileFindings = (0, scanner_1.scanLines)(file.filename, addedLines, config, patterns);
97
+ allFindings.push(...fileFindings);
98
+ filesScanned++;
99
+ }
100
+ core.info(`Scanned ${filesScanned} file(s), skipped ${filesSkipped} file(s)`);
101
+ core.info(`Found ${allFindings.length} potential secret(s)`);
102
+ core.setOutput('secrets_found', allFindings.length.toString());
103
+ const safeFindings = allFindings.map(f => ({
104
+ file: f.file,
105
+ line: f.line,
106
+ type: f.type,
107
+ severity: f.severity,
108
+ confidence: f.confidence,
109
+ snippet: f.snippet,
110
+ }));
111
+ core.setOutput('findings', JSON.stringify(safeFindings));
112
+ if (allFindings.length > 0) {
113
+ const report = (0, scanner_1.generateReport)(allFindings, filesScanned);
114
+ await (0, github_1.upsertComment)(octokit, owner, repo, pullNumber, report);
115
+ }
116
+ else if (config.postNoFindings) {
117
+ const report = (0, scanner_1.generateReport)([], filesScanned);
118
+ await (0, github_1.upsertComment)(octokit, owner, repo, pullNumber, report);
119
+ }
120
+ else {
121
+ await (0, github_1.deleteExistingComment)(octokit, owner, repo, pullNumber);
122
+ }
123
+ if ((0, scanner_1.shouldFail)(allFindings, config.failOn)) {
124
+ for (const finding of allFindings) {
125
+ core.warning(`${finding.severity.toUpperCase()}: ${finding.type} in ${finding.file}:${finding.line ?? 'N/A'} - ${(0, mask_1.maskSecret)(finding.rawValue)}`);
126
+ }
127
+ core.setFailed(`KeySentinel found ${allFindings.length} potential secret(s) at or above "${config.failOn}" severity`);
128
+ }
129
+ core.info('KeySentinel completed');
130
+ }
131
+ catch (error) {
132
+ if (error instanceof Error) {
133
+ core.setFailed(`KeySentinel failed: ${error.message}`);
134
+ }
135
+ else {
136
+ core.setFailed('KeySentinel failed with an unknown error');
137
+ }
138
+ }
139
+ }
140
+ run();
package/lib/mask.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Mask sensitive data for safe display
3
+ */
4
+ /**
5
+ * Mask a secret value, showing only first 3 and last 3 characters
6
+ * @param value The secret value to mask
7
+ * @param showChars Number of characters to show at start and end (default 3)
8
+ */
9
+ export declare function maskSecret(value: string, showChars?: number): string;
10
+ /**
11
+ * Mask a line of text containing a secret
12
+ * Shows context around the secret while masking the sensitive part
13
+ */
14
+ export declare function maskLine(line: string, secretValue: string, maxLineLength?: number): string;
15
+ /**
16
+ * Mask multiple occurrences of secrets in a text
17
+ */
18
+ export declare function maskMultiple(text: string, secrets: string[]): string;
19
+ /**
20
+ * Redact a value for logging (complete masking)
21
+ */
22
+ export declare function redact(value: string): string;
package/lib/mask.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Mask sensitive data for safe display
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.maskSecret = maskSecret;
7
+ exports.maskLine = maskLine;
8
+ exports.maskMultiple = maskMultiple;
9
+ exports.redact = redact;
10
+ /**
11
+ * Mask a secret value, showing only first 3 and last 3 characters
12
+ * @param value The secret value to mask
13
+ * @param showChars Number of characters to show at start and end (default 3)
14
+ */
15
+ function maskSecret(value, showChars = 3) {
16
+ if (!value)
17
+ return '***';
18
+ const len = value.length;
19
+ // If string is too short to meaningfully mask, mask entirely
20
+ if (len <= showChars * 2 + 3) {
21
+ return '*'.repeat(Math.min(len, 10));
22
+ }
23
+ const start = value.slice(0, showChars);
24
+ const end = value.slice(-showChars);
25
+ const maskedLen = Math.min(len - (showChars * 2), 20);
26
+ return `${start}${'*'.repeat(maskedLen)}${end}`;
27
+ }
28
+ /**
29
+ * Mask a line of text containing a secret
30
+ * Shows context around the secret while masking the sensitive part
31
+ */
32
+ function maskLine(line, secretValue, maxLineLength = 100) {
33
+ if (!line || !secretValue)
34
+ return line;
35
+ const maskedSecret = maskSecret(secretValue);
36
+ let maskedLine = line.replace(secretValue, maskedSecret);
37
+ // Truncate if too long
38
+ if (maskedLine.length > maxLineLength) {
39
+ const secretPos = maskedLine.indexOf(maskedSecret);
40
+ if (secretPos === -1) {
41
+ // Secret not found (shouldn't happen), just truncate
42
+ return maskedLine.slice(0, maxLineLength - 3) + '...';
43
+ }
44
+ // Try to keep the masked secret visible
45
+ const contextBefore = 20;
46
+ const contextAfter = 20;
47
+ const start = Math.max(0, secretPos - contextBefore);
48
+ const end = Math.min(maskedLine.length, secretPos + maskedSecret.length + contextAfter);
49
+ let result = maskedLine.slice(start, end);
50
+ if (start > 0) {
51
+ result = '...' + result;
52
+ }
53
+ if (end < maskedLine.length) {
54
+ result = result + '...';
55
+ }
56
+ return result;
57
+ }
58
+ return maskedLine;
59
+ }
60
+ /**
61
+ * Mask multiple occurrences of secrets in a text
62
+ */
63
+ function maskMultiple(text, secrets) {
64
+ let result = text;
65
+ for (const secret of secrets) {
66
+ if (secret && result.includes(secret)) {
67
+ result = result.split(secret).join(maskSecret(secret));
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ /**
73
+ * Redact a value for logging (complete masking)
74
+ */
75
+ function redact(value) {
76
+ if (!value)
77
+ return '[REDACTED]';
78
+ return `[REDACTED:${value.length} chars]`;
79
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Secret detection patterns and entropy calculation
3
+ */
4
+ export type Severity = 'high' | 'medium' | 'low';
5
+ export interface SecretPattern {
6
+ name: string;
7
+ pattern: RegExp;
8
+ severity: Severity;
9
+ group: string;
10
+ }
11
+ export interface Finding {
12
+ file: string;
13
+ line: number | null;
14
+ type: string;
15
+ severity: Severity;
16
+ confidence: 'high' | 'medium' | 'low';
17
+ snippet: string;
18
+ rawValue: string;
19
+ }
20
+ export declare const SECRET_PATTERNS: SecretPattern[];
21
+ /**
22
+ * Calculate Shannon entropy of a string
23
+ */
24
+ export declare function calculateEntropy(str: string): number;
25
+ /**
26
+ * Check if a string looks like base64
27
+ */
28
+ export declare function isBase64Like(str: string): boolean;
29
+ /**
30
+ * Check if string is a common non-secret pattern
31
+ */
32
+ export declare function isLikelyNonSecret(str: string): boolean;
33
+ export interface EntropyConfig {
34
+ enabled: boolean;
35
+ minLength: number;
36
+ threshold: number;
37
+ ignoreBase64Like: boolean;
38
+ }
39
+ /**
40
+ * Detect high-entropy strings that might be secrets
41
+ */
42
+ export declare function detectHighEntropyStrings(text: string, config: EntropyConfig): {
43
+ value: string;
44
+ entropy: number;
45
+ }[];
46
+ /**
47
+ * Get patterns filtered by enabled groups
48
+ */
49
+ export declare function getEnabledPatterns(enabledGroups?: Record<string, boolean>): SecretPattern[];
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ /**
3
+ * Secret detection patterns and entropy calculation
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SECRET_PATTERNS = void 0;
7
+ exports.calculateEntropy = calculateEntropy;
8
+ exports.isBase64Like = isBase64Like;
9
+ exports.isLikelyNonSecret = isLikelyNonSecret;
10
+ exports.detectHighEntropyStrings = detectHighEntropyStrings;
11
+ exports.getEnabledPatterns = getEnabledPatterns;
12
+ // Secret detection patterns organized by group
13
+ exports.SECRET_PATTERNS = [
14
+ // AWS
15
+ {
16
+ name: 'AWS Access Key ID',
17
+ pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
18
+ severity: 'high',
19
+ group: 'aws'
20
+ },
21
+ {
22
+ name: 'AWS Secret Access Key',
23
+ pattern: /\b([A-Za-z0-9/+=]{40})\b/g,
24
+ severity: 'high',
25
+ group: 'aws'
26
+ },
27
+ // GitHub
28
+ {
29
+ name: 'GitHub Personal Access Token',
30
+ pattern: /\b(ghp_[a-zA-Z0-9]{36})\b/g,
31
+ severity: 'high',
32
+ group: 'github'
33
+ },
34
+ {
35
+ name: 'GitHub OAuth Access Token',
36
+ pattern: /\b(gho_[a-zA-Z0-9]{36})\b/g,
37
+ severity: 'high',
38
+ group: 'github'
39
+ },
40
+ {
41
+ name: 'GitHub App Token',
42
+ pattern: /\b(ghu_[a-zA-Z0-9]{36})\b/g,
43
+ severity: 'high',
44
+ group: 'github'
45
+ },
46
+ {
47
+ name: 'GitHub Fine-Grained Token',
48
+ pattern: /\b(github_pat_[a-zA-Z0-9_]{22,82})\b/g,
49
+ severity: 'high',
50
+ group: 'github'
51
+ },
52
+ // Slack
53
+ {
54
+ name: 'Slack Bot Token',
55
+ pattern: /\b(xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24})\b/g,
56
+ severity: 'high',
57
+ group: 'slack'
58
+ },
59
+ {
60
+ name: 'Slack User Token',
61
+ pattern: /\b(xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24})\b/g,
62
+ severity: 'high',
63
+ group: 'slack'
64
+ },
65
+ {
66
+ name: 'Slack Webhook URL',
67
+ pattern: /https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8,}\/B[a-zA-Z0-9_]{8,}\/[a-zA-Z0-9_]{24}/g,
68
+ severity: 'high',
69
+ group: 'slack'
70
+ },
71
+ // Generic API Keys
72
+ {
73
+ name: 'Generic API Key',
74
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi,
75
+ severity: 'medium',
76
+ group: 'generic'
77
+ },
78
+ {
79
+ name: 'Generic Secret',
80
+ pattern: /(?:secret|secret[_-]?key)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{16,})['"]?/gi,
81
+ severity: 'medium',
82
+ group: 'generic'
83
+ },
84
+ {
85
+ name: 'Generic Password',
86
+ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi,
87
+ severity: 'medium',
88
+ group: 'generic'
89
+ },
90
+ {
91
+ name: 'Bearer Token',
92
+ pattern: /Bearer\s+([a-zA-Z0-9_\-.~+/]+=*)/gi,
93
+ severity: 'medium',
94
+ group: 'generic'
95
+ },
96
+ {
97
+ name: 'Basic Auth',
98
+ pattern: /Basic\s+([a-zA-Z0-9+/=]{20,})/gi,
99
+ severity: 'medium',
100
+ group: 'generic'
101
+ },
102
+ // Private Keys
103
+ {
104
+ name: 'RSA Private Key',
105
+ pattern: /-----BEGIN RSA PRIVATE KEY-----/g,
106
+ severity: 'high',
107
+ group: 'keys'
108
+ },
109
+ {
110
+ name: 'SSH Private Key',
111
+ pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/g,
112
+ severity: 'high',
113
+ group: 'keys'
114
+ },
115
+ {
116
+ name: 'PGP Private Key',
117
+ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
118
+ severity: 'high',
119
+ group: 'keys'
120
+ },
121
+ {
122
+ name: 'EC Private Key',
123
+ pattern: /-----BEGIN EC PRIVATE KEY-----/g,
124
+ severity: 'high',
125
+ group: 'keys'
126
+ },
127
+ // Cloud Providers
128
+ {
129
+ name: 'Google API Key',
130
+ pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g,
131
+ severity: 'high',
132
+ group: 'google'
133
+ },
134
+ {
135
+ name: 'Google OAuth Client Secret',
136
+ pattern: /\b([a-zA-Z0-9_-]{24}\.apps\.googleusercontent\.com)\b/g,
137
+ severity: 'medium',
138
+ group: 'google'
139
+ },
140
+ // Stripe
141
+ {
142
+ name: 'Stripe Live Key',
143
+ pattern: /\bsk_live_[0-9a-zA-Z]{24,}\b/g,
144
+ severity: 'high',
145
+ group: 'stripe'
146
+ },
147
+ {
148
+ name: 'Stripe Test Key',
149
+ pattern: /\bsk_test_[0-9a-zA-Z]{24,}\b/g,
150
+ severity: 'low',
151
+ group: 'stripe'
152
+ },
153
+ {
154
+ name: 'Stripe Restricted Key',
155
+ pattern: /\brk_live_[0-9a-zA-Z]{24,}\b/g,
156
+ severity: 'high',
157
+ group: 'stripe'
158
+ },
159
+ // Database Connection Strings
160
+ {
161
+ name: 'Database Connection String',
162
+ pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/gi,
163
+ severity: 'high',
164
+ group: 'database'
165
+ },
166
+ // Twilio
167
+ {
168
+ name: 'Twilio API Key',
169
+ pattern: /\bSK[0-9a-fA-F]{32}\b/g,
170
+ severity: 'high',
171
+ group: 'twilio'
172
+ },
173
+ // SendGrid
174
+ {
175
+ name: 'SendGrid API Key',
176
+ pattern: /\bSG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}\b/g,
177
+ severity: 'high',
178
+ group: 'sendgrid'
179
+ },
180
+ // Mailchimp
181
+ {
182
+ name: 'Mailchimp API Key',
183
+ pattern: /\b[0-9a-f]{32}-us[0-9]{1,2}\b/g,
184
+ severity: 'medium',
185
+ group: 'mailchimp'
186
+ },
187
+ // NPM
188
+ {
189
+ name: 'NPM Token',
190
+ pattern: /\bnpm_[a-zA-Z0-9]{36}\b/g,
191
+ severity: 'high',
192
+ group: 'npm'
193
+ },
194
+ // Discord
195
+ {
196
+ name: 'Discord Bot Token',
197
+ pattern: /\b[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}\b/g,
198
+ severity: 'high',
199
+ group: 'discord'
200
+ },
201
+ {
202
+ name: 'Discord Webhook URL',
203
+ pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[a-zA-Z0-9_-]+/g,
204
+ severity: 'medium',
205
+ group: 'discord'
206
+ },
207
+ // Heroku
208
+ {
209
+ name: 'Heroku API Key',
210
+ pattern: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
211
+ severity: 'medium',
212
+ group: 'heroku'
213
+ },
214
+ // JWT
215
+ {
216
+ name: 'JSON Web Token',
217
+ pattern: /\beyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+\b/g,
218
+ severity: 'medium',
219
+ group: 'jwt'
220
+ },
221
+ ];
222
+ /**
223
+ * Calculate Shannon entropy of a string
224
+ */
225
+ function calculateEntropy(str) {
226
+ if (!str || str.length === 0)
227
+ return 0;
228
+ const freq = {};
229
+ for (const char of str) {
230
+ freq[char] = (freq[char] || 0) + 1;
231
+ }
232
+ let entropy = 0;
233
+ const len = str.length;
234
+ for (const count of Object.values(freq)) {
235
+ const p = count / len;
236
+ entropy -= p * Math.log2(p);
237
+ }
238
+ return entropy;
239
+ }
240
+ /**
241
+ * Check if a string looks like base64
242
+ */
243
+ function isBase64Like(str) {
244
+ // Base64 typically has uniform character distribution from a limited alphabet
245
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
246
+ return base64Regex.test(str) && str.length % 4 === 0;
247
+ }
248
+ /**
249
+ * Check if string is a common non-secret pattern
250
+ */
251
+ function isLikelyNonSecret(str) {
252
+ // Common patterns that are not secrets
253
+ const nonSecretPatterns = [
254
+ /^[0-9]+$/, // Pure numbers
255
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i, // UUID (often non-secret)
256
+ /^(true|false|null|undefined|none)$/i, // Boolean/null literals
257
+ /^[a-z]+(_[a-z]+)*$/i, // snake_case identifiers
258
+ /^[a-z]+(-[a-z]+)*$/i, // kebab-case identifiers
259
+ /^v?\d+\.\d+\.\d+/, // Version strings
260
+ /^https?:\/\/[^\s@]+$/, // URLs without credentials
261
+ /^[\w.+-]+@[\w.-]+\.[a-z]{2,}$/i, // Email addresses (not secrets themselves)
262
+ /^sha[0-9]{3}:[a-f0-9]{64}$/i, // SHA hashes
263
+ /^[0-9a-f]{64}$/i, // Hex hashes (SHA256)
264
+ /^[0-9a-f]{40}$/i, // Git commit hashes
265
+ ];
266
+ return nonSecretPatterns.some(p => p.test(str));
267
+ }
268
+ /**
269
+ * Detect high-entropy strings that might be secrets
270
+ */
271
+ function detectHighEntropyStrings(text, config) {
272
+ if (!config.enabled)
273
+ return [];
274
+ const results = [];
275
+ // Match potential tokens: alphanumeric strings with special chars
276
+ const tokenPattern = /\b[a-zA-Z0-9_\-+/]{20,}\b/g;
277
+ let match;
278
+ while ((match = tokenPattern.exec(text)) !== null) {
279
+ const candidate = match[0];
280
+ // Skip if too short
281
+ if (candidate.length < config.minLength)
282
+ continue;
283
+ // Skip known non-secrets
284
+ if (isLikelyNonSecret(candidate))
285
+ continue;
286
+ // Skip base64-like if configured
287
+ if (config.ignoreBase64Like && isBase64Like(candidate))
288
+ continue;
289
+ const entropy = calculateEntropy(candidate);
290
+ if (entropy >= config.threshold) {
291
+ results.push({ value: candidate, entropy });
292
+ }
293
+ }
294
+ return results;
295
+ }
296
+ /**
297
+ * Get patterns filtered by enabled groups
298
+ */
299
+ function getEnabledPatterns(enabledGroups) {
300
+ if (!enabledGroups)
301
+ return exports.SECRET_PATTERNS;
302
+ return exports.SECRET_PATTERNS.filter(pattern => {
303
+ const groupSetting = enabledGroups[pattern.group];
304
+ // If group is explicitly disabled, filter out
305
+ if (groupSetting === false)
306
+ return false;
307
+ // Otherwise include
308
+ return true;
309
+ });
310
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Core scanning logic - pure functions for testability
3
+ */
4
+ import { Finding, Severity, SecretPattern } from './patterns';
5
+ import { Config } from './config';
6
+ /**
7
+ * Extract added lines from a unified diff patch
8
+ * Returns array of { line: text, lineNumber: number }
9
+ */
10
+ export declare function extractAddedLines(patch: string): {
11
+ line: string;
12
+ lineNumber: number;
13
+ }[];
14
+ /**
15
+ * Scan text for secrets using regex patterns
16
+ */
17
+ export declare function scanWithPatterns(text: string, patterns: SecretPattern[], allowlist: RegExp[]): {
18
+ pattern: SecretPattern;
19
+ match: string;
20
+ index: number;
21
+ }[];
22
+ /**
23
+ * Scan a single file's added lines for secrets
24
+ */
25
+ export declare function scanLines(filename: string, addedLines: {
26
+ line: string;
27
+ lineNumber: number;
28
+ }[], config: Config, patterns: SecretPattern[]): Finding[];
29
+ /**
30
+ * Generate markdown report from findings
31
+ */
32
+ export declare function generateReport(findings: Finding[], filesScanned: number): string;
33
+ /**
34
+ * Check if workflow should fail based on severity threshold
35
+ */
36
+ export declare function shouldFail(findings: Finding[], failOn: Severity | 'off'): boolean;