@preship/secrets 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import { SecretsConfig, SecretsResult, SecretRule } from '@preship/core';
2
+
3
+ /**
4
+ * Scan a project for leaked secrets, API keys, tokens, and credentials.
5
+ *
6
+ * The scanner uses a 3-stage pipeline for each file:
7
+ * Stage 1: Check if file is in allowPaths -- skip entirely
8
+ * Stage 2: Keyword prefilter -- check if any rule keyword exists in file content
9
+ * Stage 3: Run matching rules' regex patterns, check entropy, filter false positives
10
+ *
11
+ * @param projectPath - Absolute path to the project root
12
+ * @param config - Partial secrets configuration (merged with defaults)
13
+ * @returns SecretsResult with findings and statistics
14
+ */
15
+ declare function scanSecrets(projectPath: string, config?: Partial<SecretsConfig>): Promise<SecretsResult>;
16
+
17
+ /**
18
+ * Get all registered secret detection rules.
19
+ */
20
+ declare function getAllRules(): SecretRule[];
21
+ /**
22
+ * Get rules whose keywords appear in the given file content.
23
+ *
24
+ * This is the keyword prefilter stage: only rules with at least one
25
+ * matching keyword are returned. Rules with no keywords (like entropy-based
26
+ * rules) are always included since they cannot be prefiltered.
27
+ *
28
+ * @param content - The file content to check keywords against
29
+ * @returns Rules that have at least one keyword match (or no keywords at all)
30
+ */
31
+ declare function getRulesForKeywords(content: string): SecretRule[];
32
+
33
+ /**
34
+ * Shannon entropy calculator for secret detection.
35
+ *
36
+ * Shannon entropy measures the randomness/information density of a string.
37
+ * High-entropy strings are more likely to be secrets (API keys, tokens, etc.)
38
+ * because they contain more random characters compared to natural language.
39
+ *
40
+ * Formula: H = -sum(p(x) * log2(p(x))) for each unique character x
41
+ */
42
+ /**
43
+ * Calculate Shannon entropy of a string.
44
+ *
45
+ * @param str - The string to calculate entropy for
46
+ * @returns Shannon entropy value (bits per character). Range: 0 to log2(uniqueChars).
47
+ * Typical thresholds:
48
+ * - English text: ~3.5-4.0
49
+ * - Base64 encoded: ~5.0-6.0
50
+ * - Hex encoded: ~3.5-4.0
51
+ * - True random (base64 charset): ~5.9
52
+ */
53
+ declare function shannonEntropy(str: string): number;
54
+
55
+ /**
56
+ * Walk project directory recursively and collect scannable file paths.
57
+ *
58
+ * @param projectPath - Absolute path to the project root
59
+ * @param allowPaths - Relative paths to skip (glob-style, uses startsWith matching)
60
+ * @param scanPaths - If non-empty, only scan files under these relative paths
61
+ * @returns Array of file paths relative to projectPath
62
+ */
63
+ declare function walkProjectFiles(projectPath: string, allowPaths: string[], scanPaths: string[]): string[];
64
+
65
+ /**
66
+ * @preship/secrets -- Secrets and credential detection for PreShip
67
+ *
68
+ * Scans project files for leaked API keys, tokens, passwords, private keys,
69
+ * database connection strings, and other credentials. Uses a 3-stage pipeline:
70
+ *
71
+ * 1. File walking with .gitignore-style exclusions
72
+ * 2. Keyword prefiltering for fast file-level skipping
73
+ * 3. Regex pattern matching with entropy analysis and false-positive filtering
74
+ *
75
+ * Zero external dependencies -- uses only Node.js built-in fs, path, and crypto.
76
+ */
77
+ declare const SECRETS_MODULE_VERSION = "1.0.0";
78
+
79
+ export { SECRETS_MODULE_VERSION, getAllRules, getRulesForKeywords, scanSecrets, shannonEntropy, walkProjectFiles };
package/dist/index.js ADDED
@@ -0,0 +1,777 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SECRETS_MODULE_VERSION: () => SECRETS_MODULE_VERSION,
34
+ getAllRules: () => getAllRules,
35
+ getRulesForKeywords: () => getRulesForKeywords,
36
+ scanSecrets: () => scanSecrets,
37
+ shannonEntropy: () => shannonEntropy,
38
+ walkProjectFiles: () => walkProjectFiles
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/scanner.ts
43
+ var fs2 = __toESM(require("fs"));
44
+ var path2 = __toESM(require("path"));
45
+
46
+ // src/entropy.ts
47
+ function shannonEntropy(str) {
48
+ if (str.length === 0) {
49
+ return 0;
50
+ }
51
+ const freq = /* @__PURE__ */ new Map();
52
+ for (const char of str) {
53
+ freq.set(char, (freq.get(char) ?? 0) + 1);
54
+ }
55
+ const len = str.length;
56
+ let entropy = 0;
57
+ for (const count of freq.values()) {
58
+ const p = count / len;
59
+ if (p > 0) {
60
+ entropy -= p * Math.log2(p);
61
+ }
62
+ }
63
+ return entropy;
64
+ }
65
+
66
+ // src/rules/aws.ts
67
+ var awsRules = [
68
+ {
69
+ id: "aws-access-key",
70
+ description: "AWS Access Key ID",
71
+ severity: "critical",
72
+ keywords: ["AKIA"],
73
+ pattern: /AKIA[0-9A-Z]{16}/,
74
+ allowPatterns: [
75
+ /AKIAIOSFODNN7EXAMPLE/,
76
+ // AWS documentation example key
77
+ /AKIAI44QH8DHBEXAMPLE/
78
+ // Another AWS doc example
79
+ ]
80
+ },
81
+ {
82
+ id: "aws-secret-key",
83
+ description: "AWS Secret Access Key",
84
+ severity: "critical",
85
+ keywords: ["aws"],
86
+ // 40-character base64 string near aws_secret_access_key or similar keywords
87
+ pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY|aws_secret_key)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/,
88
+ allowPatterns: [
89
+ /wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY/
90
+ // AWS documentation example
91
+ ]
92
+ },
93
+ {
94
+ id: "aws-session-token",
95
+ description: "AWS Session Token",
96
+ severity: "high",
97
+ keywords: ["aws_session_token"],
98
+ // Long base64 string near aws_session_token keyword
99
+ pattern: /(?:aws_session_token|AWS_SESSION_TOKEN)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{100,})['"]?/
100
+ },
101
+ {
102
+ id: "aws-mws-key",
103
+ description: "Amazon Marketplace Web Service (MWS) Auth Token",
104
+ severity: "critical",
105
+ keywords: ["amzn.mws"],
106
+ pattern: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
107
+ }
108
+ ];
109
+
110
+ // src/rules/gcp.ts
111
+ var gcpRules = [
112
+ {
113
+ id: "gcp-api-key",
114
+ description: "Google Cloud API Key",
115
+ severity: "critical",
116
+ keywords: ["AIza"],
117
+ pattern: /AIza[0-9A-Za-z_-]{35}/,
118
+ allowPatterns: [
119
+ /AIzaSyA1234567890EXAMPLE_KEY_HERE/
120
+ // Placeholder
121
+ ]
122
+ },
123
+ {
124
+ id: "gcp-service-account",
125
+ description: "Google Cloud Service Account JSON",
126
+ severity: "critical",
127
+ keywords: ["service_account"],
128
+ // Detects the "type": "service_account" pattern in JSON files
129
+ pattern: /"type"\s*:\s*"service_account"/
130
+ },
131
+ {
132
+ id: "gcp-oauth-secret",
133
+ description: "Google OAuth Client Secret",
134
+ severity: "high",
135
+ keywords: ["client_secret"],
136
+ // client_secret near googleapis or google context
137
+ pattern: /(?:client_secret|CLIENT_SECRET)\s*[=:"]\s*['"]?([A-Za-z0-9_-]{24,})['"]?/
138
+ }
139
+ ];
140
+
141
+ // src/rules/github.ts
142
+ var githubRules = [
143
+ {
144
+ id: "github-pat",
145
+ description: "GitHub Personal Access Token",
146
+ severity: "high",
147
+ keywords: ["ghp_"],
148
+ pattern: /ghp_[0-9a-zA-Z]{36}/
149
+ },
150
+ {
151
+ id: "github-oauth",
152
+ description: "GitHub OAuth Access Token",
153
+ severity: "high",
154
+ keywords: ["gho_"],
155
+ pattern: /gho_[0-9a-zA-Z]{36}/
156
+ },
157
+ {
158
+ id: "github-app-token",
159
+ description: "GitHub App Token (User-to-Server or Server-to-Server)",
160
+ severity: "high",
161
+ keywords: ["ghu_", "ghs_"],
162
+ pattern: /(?:ghu|ghs)_[0-9a-zA-Z]{36}/
163
+ },
164
+ {
165
+ id: "github-fine-grained",
166
+ description: "GitHub Fine-Grained Personal Access Token",
167
+ severity: "high",
168
+ keywords: ["github_pat_"],
169
+ pattern: /github_pat_[0-9a-zA-Z]{22}_[0-9a-zA-Z]{59}/
170
+ }
171
+ ];
172
+
173
+ // src/rules/tokens.ts
174
+ var tokenRules = [
175
+ {
176
+ id: "stripe-secret-key",
177
+ description: "Stripe Secret API Key",
178
+ severity: "critical",
179
+ keywords: ["sk_live_"],
180
+ pattern: /sk_live_[0-9a-zA-Z]{24,}/
181
+ },
182
+ {
183
+ id: "stripe-restricted-key",
184
+ description: "Stripe Restricted API Key",
185
+ severity: "high",
186
+ keywords: ["rk_live_"],
187
+ pattern: /rk_live_[0-9a-zA-Z]{24,}/
188
+ },
189
+ {
190
+ id: "slack-bot-token",
191
+ description: "Slack Bot Token",
192
+ severity: "high",
193
+ keywords: ["xoxb-"],
194
+ pattern: /xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}/
195
+ },
196
+ {
197
+ id: "slack-webhook",
198
+ description: "Slack Incoming Webhook URL",
199
+ severity: "high",
200
+ keywords: ["hooks.slack.com"],
201
+ pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/
202
+ },
203
+ {
204
+ id: "twilio-api-key",
205
+ description: "Twilio API Key",
206
+ severity: "high",
207
+ keywords: ["twilio", "SK"],
208
+ pattern: /SK[0-9a-fA-F]{32}/
209
+ },
210
+ {
211
+ id: "sendgrid-api-key",
212
+ description: "SendGrid API Key",
213
+ severity: "high",
214
+ keywords: ["SG."],
215
+ pattern: /SG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43}/
216
+ },
217
+ {
218
+ id: "npm-token",
219
+ description: "npm Access Token",
220
+ severity: "high",
221
+ keywords: ["npm_"],
222
+ pattern: /npm_[0-9a-zA-Z]{36}/
223
+ },
224
+ {
225
+ id: "mailchimp-api-key",
226
+ description: "Mailchimp API Key",
227
+ severity: "medium",
228
+ keywords: ["mailchimp", "-us"],
229
+ pattern: /[0-9a-f]{32}-us[0-9]{1,2}/
230
+ }
231
+ ];
232
+
233
+ // src/rules/private-keys.ts
234
+ var privateKeyRules = [
235
+ {
236
+ id: "rsa-private-key",
237
+ description: "RSA Private Key",
238
+ severity: "critical",
239
+ keywords: ["BEGIN RSA PRIVATE KEY"],
240
+ pattern: /-----BEGIN RSA PRIVATE KEY-----/
241
+ },
242
+ {
243
+ id: "dsa-private-key",
244
+ description: "DSA Private Key",
245
+ severity: "critical",
246
+ keywords: ["BEGIN DSA PRIVATE KEY"],
247
+ pattern: /-----BEGIN DSA PRIVATE KEY-----/
248
+ },
249
+ {
250
+ id: "ec-private-key",
251
+ description: "EC Private Key",
252
+ severity: "critical",
253
+ keywords: ["BEGIN EC PRIVATE KEY"],
254
+ pattern: /-----BEGIN EC PRIVATE KEY-----/
255
+ },
256
+ {
257
+ id: "pgp-private-key",
258
+ description: "PGP Private Key Block",
259
+ severity: "critical",
260
+ keywords: ["BEGIN PGP PRIVATE KEY BLOCK"],
261
+ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/
262
+ },
263
+ {
264
+ id: "ssh-private-key",
265
+ description: "OpenSSH Private Key",
266
+ severity: "critical",
267
+ keywords: ["BEGIN OPENSSH PRIVATE KEY"],
268
+ pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/
269
+ },
270
+ {
271
+ id: "pkcs8-private-key",
272
+ description: "PKCS#8 Private Key",
273
+ severity: "critical",
274
+ keywords: ["BEGIN PRIVATE KEY"],
275
+ pattern: /-----BEGIN PRIVATE KEY-----/
276
+ }
277
+ ];
278
+
279
+ // src/rules/database.ts
280
+ var databaseRules = [
281
+ {
282
+ id: "postgres-uri",
283
+ description: "PostgreSQL Connection URI with Password",
284
+ severity: "critical",
285
+ keywords: ["postgres"],
286
+ // postgres:// or postgresql:// with user:password@host
287
+ pattern: /postgres(?:ql)?:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
288
+ allowPatterns: [
289
+ /postgres(?:ql)?:\/\/user(?:name)?:password@/,
290
+ // Common placeholder
291
+ /postgres(?:ql)?:\/\/postgres:postgres@localhost/
292
+ // Local dev default
293
+ ]
294
+ },
295
+ {
296
+ id: "mysql-uri",
297
+ description: "MySQL Connection URI with Password",
298
+ severity: "critical",
299
+ keywords: ["mysql"],
300
+ // mysql:// with user:password@host
301
+ pattern: /mysql:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
302
+ allowPatterns: [
303
+ /mysql:\/\/user(?:name)?:password@/,
304
+ // Common placeholder
305
+ /mysql:\/\/root:root@localhost/,
306
+ // Local dev default
307
+ /mysql:\/\/root:password@localhost/
308
+ // Local dev default
309
+ ]
310
+ },
311
+ {
312
+ id: "mongodb-uri",
313
+ description: "MongoDB Connection URI with Password",
314
+ severity: "critical",
315
+ keywords: ["mongodb"],
316
+ // mongodb:// or mongodb+srv:// with user:password@host
317
+ pattern: /mongodb(?:\+srv)?:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
318
+ allowPatterns: [
319
+ /mongodb(?:\+srv)?:\/\/user(?:name)?:password@/,
320
+ // Common placeholder
321
+ /mongodb(?:\+srv)?:\/\/root:example@/
322
+ // Docker docs example
323
+ ]
324
+ },
325
+ {
326
+ id: "redis-uri",
327
+ description: "Redis Connection URI with Password",
328
+ severity: "high",
329
+ keywords: ["redis"],
330
+ // redis:// or rediss:// with user:password@host
331
+ pattern: /rediss?:\/\/[^\s:]*:[^\s@]+@[^\s]+/,
332
+ allowPatterns: [
333
+ /rediss?:\/\/:password@localhost/
334
+ // Common placeholder
335
+ ]
336
+ },
337
+ {
338
+ id: "database-password",
339
+ description: "Database Password Assignment",
340
+ severity: "high",
341
+ keywords: ["db_password", "database_password", "DB_PASS"],
342
+ // Various database password environment variable patterns
343
+ pattern: /(?:db_password|database_password|DB_PASS(?:WORD)?|DB_PWD)\s*[=:]\s*['"][^'"]{8,}['"]/i,
344
+ allowPatterns: [
345
+ /['"](?:password|changeme|your[_-]?password|replace[_-]?me|TODO|xxx+)['"]/i
346
+ ]
347
+ }
348
+ ];
349
+
350
+ // src/rules/generic.ts
351
+ var GENERIC_ALLOW_PATTERNS = [
352
+ /['"](?:your[_-]?api[_-]?key(?:[_-]?here)?|INSERT[_-]?(?:TOKEN|KEY|SECRET)[_-]?HERE|xxx+|TODO|CHANGEME|REPLACE[_-]?ME|example|test|dummy|fake|placeholder|sample)['"]/i,
353
+ /['"](?:sk[_-]?test|pk[_-]?test|sk[_-]?example|pk[_-]?example)['"]/i,
354
+ /['"](?:\$\{|<[^>]+>|\{\{)/
355
+ // Template variable references: ${VAR}, <TOKEN>, {{VAR}}
356
+ ];
357
+ var genericRules = [
358
+ {
359
+ id: "generic-api-key",
360
+ description: "Generic API Key Assignment",
361
+ severity: "medium",
362
+ keywords: ["api_key", "api-key", "apikey", "API_KEY", "APIKEY"],
363
+ pattern: /(?:api[_-]?key|apikey|API[_-]?KEY|APIKEY)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
364
+ allowPatterns: GENERIC_ALLOW_PATTERNS
365
+ },
366
+ {
367
+ id: "generic-secret",
368
+ description: "Generic Secret Assignment",
369
+ severity: "medium",
370
+ keywords: ["secret", "SECRET"],
371
+ pattern: /(?:secret|SECRET|client_secret|CLIENT_SECRET|app_secret|APP_SECRET)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
372
+ allowPatterns: GENERIC_ALLOW_PATTERNS
373
+ },
374
+ {
375
+ id: "generic-password",
376
+ description: "Generic Password Assignment",
377
+ severity: "medium",
378
+ keywords: ["password", "passwd", "pwd", "PASSWORD", "PASSWD", "PWD"],
379
+ pattern: /(?:password|passwd|pwd|PASSWORD|PASSWD|PWD)\s*[=:]\s*['"][^'"]{8,}['"]/,
380
+ allowPatterns: [
381
+ ...GENERIC_ALLOW_PATTERNS,
382
+ /['"](?:password|changeme|admin|root|test(?:ing)?|development|12345678)['"]/i
383
+ ]
384
+ },
385
+ {
386
+ id: "generic-token",
387
+ description: "Generic Token Assignment",
388
+ severity: "medium",
389
+ keywords: ["token", "TOKEN", "access_token", "ACCESS_TOKEN"],
390
+ pattern: /(?:token|TOKEN|access_token|ACCESS_TOKEN|auth_token|AUTH_TOKEN)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
391
+ allowPatterns: GENERIC_ALLOW_PATTERNS
392
+ },
393
+ {
394
+ id: "jwt-token",
395
+ description: "JSON Web Token (JWT)",
396
+ severity: "medium",
397
+ keywords: ["eyJ"],
398
+ // JWT structure: base64url.base64url.signature
399
+ pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/
400
+ },
401
+ {
402
+ id: "env-file",
403
+ description: "Environment Configuration File with Secrets",
404
+ severity: "high",
405
+ keywords: ["_KEY=", "_SECRET=", "_TOKEN=", "_PASSWORD="],
406
+ // Detects KEY=value, SECRET=value patterns in .env-style files
407
+ // This rule is special: the scanner should also check if the filename is .env*
408
+ pattern: /(?:_KEY|_SECRET|_TOKEN|_PASSWORD|_API_KEY)\s*=\s*[^\s#]{8,}/,
409
+ allowPatterns: [
410
+ /=\s*(?:your[_-]?|INSERT|TODO|CHANGEME|REPLACE|<|"\$\{|\$\(|%)/i
411
+ ]
412
+ },
413
+ {
414
+ id: "high-entropy-base64",
415
+ description: "High-Entropy Base64 String (possible secret)",
416
+ severity: "low",
417
+ keywords: [],
418
+ // No keyword prefilter; this rule is opt-in via entropy threshold
419
+ pattern: /[A-Za-z0-9+/]{40,}={0,2}/,
420
+ entropyThreshold: 4.5,
421
+ allowPatterns: [
422
+ /^[A-Z]+$/,
423
+ // All uppercase (likely a constant name)
424
+ /^[a-z]+$/,
425
+ // All lowercase (likely a word)
426
+ /[A-Za-z]{40,}/
427
+ // Long alphabetic-only strings are typically not secrets
428
+ ]
429
+ },
430
+ {
431
+ id: "high-entropy-hex",
432
+ description: "High-Entropy Hex String (possible secret)",
433
+ severity: "low",
434
+ keywords: [],
435
+ // No keyword prefilter; this rule is opt-in via entropy threshold
436
+ pattern: /[0-9a-fA-F]{40,}/,
437
+ entropyThreshold: 3,
438
+ allowPatterns: [
439
+ /^0{10,}$/,
440
+ // All zeros (placeholder/null hash)
441
+ /^[fF]{10,}$/
442
+ // All f's (mask value)
443
+ ]
444
+ }
445
+ ];
446
+
447
+ // src/rules/index.ts
448
+ var ALL_RULES = [
449
+ ...awsRules,
450
+ ...gcpRules,
451
+ ...githubRules,
452
+ ...tokenRules,
453
+ ...privateKeyRules,
454
+ ...databaseRules,
455
+ ...genericRules
456
+ ];
457
+ function getAllRules() {
458
+ return ALL_RULES;
459
+ }
460
+ function getRulesForKeywords(content) {
461
+ return ALL_RULES.filter((rule) => {
462
+ if (rule.keywords.length === 0) {
463
+ return true;
464
+ }
465
+ return rule.keywords.some((keyword) => content.includes(keyword));
466
+ });
467
+ }
468
+
469
+ // src/walker.ts
470
+ var fs = __toESM(require("fs"));
471
+ var path = __toESM(require("path"));
472
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
473
+ "node_modules",
474
+ ".git",
475
+ "dist",
476
+ "build",
477
+ "coverage",
478
+ "__pycache__",
479
+ ".next",
480
+ ".nuxt",
481
+ ".cache",
482
+ ".turbo",
483
+ ".output"
484
+ ]);
485
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
486
+ ".png",
487
+ ".jpg",
488
+ ".jpeg",
489
+ ".gif",
490
+ ".ico",
491
+ ".svg",
492
+ ".webp",
493
+ ".bmp",
494
+ ".tiff",
495
+ ".woff",
496
+ ".woff2",
497
+ ".ttf",
498
+ ".eot",
499
+ ".otf",
500
+ ".pdf",
501
+ ".zip",
502
+ ".tar",
503
+ ".gz",
504
+ ".bz2",
505
+ ".xz",
506
+ ".7z",
507
+ ".rar",
508
+ ".jar",
509
+ ".war",
510
+ ".exe",
511
+ ".dll",
512
+ ".so",
513
+ ".dylib",
514
+ ".bin",
515
+ ".o",
516
+ ".obj",
517
+ ".class",
518
+ ".pyc",
519
+ ".pyo",
520
+ ".wasm",
521
+ ".mp3",
522
+ ".mp4",
523
+ ".avi",
524
+ ".mov",
525
+ ".mkv",
526
+ ".flac",
527
+ ".wav"
528
+ ]);
529
+ var ALLOWED_LOCKFILES = /* @__PURE__ */ new Set([
530
+ "package-lock.json",
531
+ "yarn.lock",
532
+ "pnpm-lock.yaml"
533
+ ]);
534
+ var MAX_FILE_SIZE = 1048576;
535
+ function walkProjectFiles(projectPath, allowPaths, scanPaths) {
536
+ const files = [];
537
+ const normalizedScanPaths = scanPaths.map((p) => p.replace(/\/+$/, ""));
538
+ const normalizedAllowPaths = allowPaths.map((p) => p.replace(/\/+$/, ""));
539
+ walkDirectory(projectPath, "", files, normalizedAllowPaths, normalizedScanPaths);
540
+ return files;
541
+ }
542
+ function walkDirectory(rootPath, relativePath, files, allowPaths, scanPaths) {
543
+ const absolutePath = relativePath ? path.join(rootPath, relativePath) : rootPath;
544
+ let entries;
545
+ try {
546
+ entries = fs.readdirSync(absolutePath, { withFileTypes: true });
547
+ } catch {
548
+ return;
549
+ }
550
+ for (const entry of entries) {
551
+ const entryRelative = relativePath ? path.join(relativePath, entry.name) : entry.name;
552
+ if (entry.isDirectory()) {
553
+ if (SKIP_DIRS.has(entry.name)) {
554
+ continue;
555
+ }
556
+ if (isAllowedPath(entryRelative, allowPaths)) {
557
+ continue;
558
+ }
559
+ if (scanPaths.length > 0 && !isUnderOrContainsScanPath(entryRelative, scanPaths)) {
560
+ continue;
561
+ }
562
+ walkDirectory(rootPath, entryRelative, files, allowPaths, scanPaths);
563
+ } else if (entry.isFile()) {
564
+ if (isAllowedPath(entryRelative, allowPaths)) {
565
+ continue;
566
+ }
567
+ if (scanPaths.length > 0 && !isUnderScanPath(entryRelative, scanPaths)) {
568
+ continue;
569
+ }
570
+ const ext = path.extname(entry.name).toLowerCase();
571
+ if (SKIP_EXTENSIONS.has(ext)) {
572
+ continue;
573
+ }
574
+ if (ext === ".lock" && !ALLOWED_LOCKFILES.has(entry.name)) {
575
+ continue;
576
+ }
577
+ try {
578
+ const stats = fs.statSync(path.join(rootPath, entryRelative));
579
+ if (stats.size > MAX_FILE_SIZE) {
580
+ continue;
581
+ }
582
+ } catch {
583
+ continue;
584
+ }
585
+ files.push(entryRelative);
586
+ }
587
+ }
588
+ }
589
+ function isAllowedPath(relativePath, allowPaths) {
590
+ return allowPaths.some(
591
+ (allowed) => relativePath === allowed || relativePath.startsWith(allowed + path.sep) || relativePath.startsWith(allowed + "/")
592
+ );
593
+ }
594
+ function isUnderScanPath(relativePath, scanPaths) {
595
+ return scanPaths.some((scanPath) => {
596
+ if (scanPath === "" || scanPath === ".") return true;
597
+ return relativePath === scanPath || relativePath.startsWith(scanPath + path.sep) || relativePath.startsWith(scanPath + "/");
598
+ });
599
+ }
600
+ function isUnderOrContainsScanPath(dirPath, scanPaths) {
601
+ return scanPaths.some((scanPath) => {
602
+ if (scanPath === "" || scanPath === ".") return true;
603
+ if (scanPath.startsWith(dirPath + path.sep) || scanPath.startsWith(dirPath + "/")) {
604
+ return true;
605
+ }
606
+ if (dirPath === scanPath || dirPath.startsWith(scanPath + path.sep) || dirPath.startsWith(scanPath + "/")) {
607
+ return true;
608
+ }
609
+ return false;
610
+ });
611
+ }
612
+
613
+ // src/scanner.ts
614
+ var DEFAULT_SECRETS_CONFIG = {
615
+ scanPaths: [],
616
+ allowPaths: [],
617
+ allowRules: []
618
+ };
619
+ async function scanSecrets(projectPath, config = {}) {
620
+ const startTime = Date.now();
621
+ const mergedConfig = {
622
+ ...DEFAULT_SECRETS_CONFIG,
623
+ ...config
624
+ };
625
+ const allRules = getAllRules().filter(
626
+ (rule) => !mergedConfig.allowRules.includes(rule.id)
627
+ );
628
+ const files = walkProjectFiles(
629
+ projectPath,
630
+ mergedConfig.allowPaths,
631
+ mergedConfig.scanPaths
632
+ );
633
+ const findings = [];
634
+ let filesScanned = 0;
635
+ for (const relativePath of files) {
636
+ const absolutePath = path2.join(projectPath, relativePath);
637
+ let content;
638
+ try {
639
+ content = fs2.readFileSync(absolutePath, "utf-8");
640
+ } catch {
641
+ continue;
642
+ }
643
+ filesScanned++;
644
+ const isEnvFile = isEnvironmentFile(relativePath);
645
+ const candidateRules = getRulesForKeywords(content).filter(
646
+ (rule) => !mergedConfig.allowRules.includes(rule.id)
647
+ );
648
+ if (candidateRules.length === 0 && !isEnvFile) {
649
+ continue;
650
+ }
651
+ const lines = content.split("\n");
652
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
653
+ const line = lines[lineIndex];
654
+ for (const rule of candidateRules) {
655
+ scanLineForRule(rule, line, lineIndex, relativePath, findings);
656
+ }
657
+ }
658
+ if (isEnvFile && hasEnvSecretContent(content)) {
659
+ const envRule = allRules.find((r) => r.id === "env-file");
660
+ if (envRule && !mergedConfig.allowRules.includes("env-file")) {
661
+ const hasEnvFindings = findings.some(
662
+ (f) => f.ruleId === "env-file" && f.file === relativePath
663
+ );
664
+ if (!hasEnvFindings) {
665
+ findings.push({
666
+ ruleId: "env-file",
667
+ description: "Environment file with potential secrets detected",
668
+ severity: "high",
669
+ file: relativePath,
670
+ line: 1,
671
+ column: 1,
672
+ match: path2.basename(relativePath)
673
+ });
674
+ }
675
+ }
676
+ }
677
+ }
678
+ const stats = {
679
+ critical: findings.filter((f) => f.severity === "critical").length,
680
+ high: findings.filter((f) => f.severity === "high").length,
681
+ medium: findings.filter((f) => f.severity === "medium").length,
682
+ low: findings.filter((f) => f.severity === "low").length
683
+ };
684
+ const passed = stats.critical === 0 && stats.high === 0;
685
+ return {
686
+ enabled: true,
687
+ passed,
688
+ filesScanned,
689
+ findings,
690
+ stats,
691
+ scanDurationMs: Date.now() - startTime
692
+ };
693
+ }
694
+ function scanLineForRule(rule, line, lineIndex, relativePath, findings) {
695
+ const regex = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
696
+ const match = regex.exec(line);
697
+ if (!match) {
698
+ return;
699
+ }
700
+ const matchedString = match[0];
701
+ const secretValue = match[1] ?? matchedString;
702
+ if (rule.allowPatterns && rule.allowPatterns.length > 0) {
703
+ const isAllowed = rule.allowPatterns.some(
704
+ (allowPattern) => allowPattern.test(matchedString)
705
+ );
706
+ if (isAllowed) {
707
+ return;
708
+ }
709
+ }
710
+ let entropy;
711
+ if (rule.entropyThreshold !== void 0) {
712
+ entropy = shannonEntropy(secretValue);
713
+ if (entropy < rule.entropyThreshold) {
714
+ return;
715
+ }
716
+ }
717
+ findings.push({
718
+ ruleId: rule.id,
719
+ description: rule.description,
720
+ severity: rule.severity,
721
+ file: relativePath,
722
+ line: lineIndex + 1,
723
+ // 1-indexed
724
+ column: match.index + 1,
725
+ // 1-indexed
726
+ match: redactSecret(matchedString, rule),
727
+ entropy
728
+ });
729
+ }
730
+ function redactSecret(value, rule) {
731
+ if (rule.id.endsWith("-private-key")) {
732
+ return value;
733
+ }
734
+ if (rule.id === "gcp-service-account") {
735
+ return '"type": "service_account"';
736
+ }
737
+ if (value.length < 12) {
738
+ return value.substring(0, 2) + "****";
739
+ }
740
+ return value.substring(0, 4) + "****" + value.substring(value.length - 4);
741
+ }
742
+ function isEnvironmentFile(relativePath) {
743
+ const basename2 = path2.basename(relativePath);
744
+ return basename2 === ".env" || basename2.startsWith(".env.") || basename2 === ".env.local" || basename2 === ".env.development" || basename2 === ".env.production" || basename2 === ".env.staging" || basename2 === ".env.test";
745
+ }
746
+ function hasEnvSecretContent(content) {
747
+ const lines = content.split("\n");
748
+ for (const line of lines) {
749
+ const trimmed = line.trim();
750
+ if (trimmed === "" || trimmed.startsWith("#")) {
751
+ continue;
752
+ }
753
+ const eqIndex = trimmed.indexOf("=");
754
+ if (eqIndex > 0) {
755
+ const value = trimmed.substring(eqIndex + 1).trim().replace(/^['"]|['"]$/g, "");
756
+ if (value === "" || value.startsWith("${") || value.startsWith("<") || value.startsWith("{{") || /^(your[_-]|INSERT|TODO|CHANGEME|REPLACE|xxx)/i.test(value)) {
757
+ continue;
758
+ }
759
+ if (value.length >= 8) {
760
+ return true;
761
+ }
762
+ }
763
+ }
764
+ return false;
765
+ }
766
+
767
+ // src/index.ts
768
+ var SECRETS_MODULE_VERSION = "1.0.0";
769
+ // Annotate the CommonJS export names for ESM import in node:
770
+ 0 && (module.exports = {
771
+ SECRETS_MODULE_VERSION,
772
+ getAllRules,
773
+ getRulesForKeywords,
774
+ scanSecrets,
775
+ shannonEntropy,
776
+ walkProjectFiles
777
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@preship/secrets",
3
+ "version": "1.0.0",
4
+ "description": "Secrets detection for PreShip — find leaked API keys, tokens, and credentials before shipping",
5
+ "author": "Cyfox Inc.",
6
+ "license": "Apache-2.0",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "require": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs --dts --clean",
21
+ "lint": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "@preship/core": "2.0.0"
25
+ }
26
+ }