envspect 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/dist/index.js ADDED
@@ -0,0 +1,692 @@
1
+ // src/core/parser.ts
2
+ import { readFile } from "fs/promises";
3
+ async function parseEnvFile(filePath) {
4
+ try {
5
+ const content = await readFile(filePath, "utf8");
6
+ return parseEnvContent(content, filePath);
7
+ } catch (error) {
8
+ throw new Error(`Failed to read file at ${filePath}: ${error.message}`);
9
+ }
10
+ }
11
+ function parseEnvContent(content, filePath = "unknown") {
12
+ const entries = [];
13
+ const errors = [];
14
+ const lines = content.split(/\r?\n/);
15
+ let i = 0;
16
+ while (i < lines.length) {
17
+ const rawLine = lines[i];
18
+ const trimmed = rawLine.trim();
19
+ if (!trimmed) {
20
+ i++;
21
+ continue;
22
+ }
23
+ if (trimmed.startsWith("#")) {
24
+ entries.push({
25
+ key: "",
26
+ value: "",
27
+ line: i + 1,
28
+ isComment: true,
29
+ comment: trimmed.substring(1).trim(),
30
+ raw: rawLine
31
+ });
32
+ i++;
33
+ continue;
34
+ }
35
+ const matchLine = trimmed.startsWith("export ") ? trimmed.substring(7).trimStart() : trimmed;
36
+ const eqIdx = matchLine.indexOf("=");
37
+ if (eqIdx === -1) {
38
+ errors.push({ line: i + 1, message: 'Malformed line, missing "="', raw: rawLine });
39
+ i++;
40
+ continue;
41
+ }
42
+ const key = matchLine.substring(0, eqIdx).trim();
43
+ let valueStr = matchLine.substring(eqIdx + 1).trim();
44
+ let comment = void 0;
45
+ if (valueStr.startsWith('"') || valueStr.startsWith("'") || valueStr.startsWith("`")) {
46
+ const quoteChar = valueStr[0];
47
+ if (quoteChar === '"' || quoteChar === "`") {
48
+ let endIndex = valueStr.indexOf(quoteChar, 1);
49
+ let valueLines = [valueStr];
50
+ let multilineRaw = [rawLine];
51
+ let currentLineIdx = i;
52
+ while (endIndex === -1 && currentLineIdx < lines.length - 1) {
53
+ currentLineIdx++;
54
+ const nextLine = lines[currentLineIdx];
55
+ valueLines.push(nextLine);
56
+ multilineRaw.push(nextLine);
57
+ endIndex = nextLine.indexOf(quoteChar);
58
+ }
59
+ if (endIndex === -1) {
60
+ errors.push({ line: i + 1, message: `Unmatched ${quoteChar}`, raw: multilineRaw.join("\n") });
61
+ i = currentLineIdx + 1;
62
+ continue;
63
+ } else {
64
+ valueStr = valueLines.join("\n");
65
+ const finalRaw = multilineRaw.join("\n");
66
+ const extractedValue = valueStr.substring(1, valueStr.lastIndexOf(quoteChar));
67
+ const afterQuote = valueStr.substring(valueStr.lastIndexOf(quoteChar) + 1).trim();
68
+ if (afterQuote.startsWith("#")) {
69
+ comment = afterQuote.substring(1).trim();
70
+ }
71
+ entries.push({
72
+ key,
73
+ value: extractedValue,
74
+ line: i + 1,
75
+ isComment: false,
76
+ comment,
77
+ raw: finalRaw
78
+ });
79
+ i = currentLineIdx + 1;
80
+ continue;
81
+ }
82
+ } else {
83
+ const endIndex = valueStr.indexOf(quoteChar, 1);
84
+ if (endIndex === -1) {
85
+ errors.push({ line: i + 1, message: "Unmatched single quote", raw: rawLine });
86
+ i++;
87
+ continue;
88
+ }
89
+ const extractedValue = valueStr.substring(1, endIndex);
90
+ const afterQuote = valueStr.substring(endIndex + 1).trim();
91
+ if (afterQuote.startsWith("#")) {
92
+ comment = afterQuote.substring(1).trim();
93
+ }
94
+ entries.push({
95
+ key,
96
+ value: extractedValue,
97
+ line: i + 1,
98
+ isComment: false,
99
+ comment,
100
+ raw: rawLine
101
+ });
102
+ i++;
103
+ continue;
104
+ }
105
+ }
106
+ const hashIdx = valueStr.indexOf("#");
107
+ let value = valueStr;
108
+ if (hashIdx !== -1) {
109
+ value = valueStr.substring(0, hashIdx).trim();
110
+ comment = valueStr.substring(hashIdx + 1).trim();
111
+ }
112
+ entries.push({
113
+ key,
114
+ value,
115
+ line: i + 1,
116
+ isComment: false,
117
+ comment,
118
+ raw: rawLine
119
+ });
120
+ i++;
121
+ }
122
+ return { entries, filePath, errors };
123
+ }
124
+
125
+ // src/core/differ.ts
126
+ function diffEnvFiles(source, target, options = {}) {
127
+ const sourceMap = /* @__PURE__ */ new Map();
128
+ const targetMap = /* @__PURE__ */ new Map();
129
+ for (const entry of source) {
130
+ if (!entry.isComment && entry.key) {
131
+ sourceMap.set(entry.key, entry);
132
+ }
133
+ }
134
+ for (const entry of target) {
135
+ if (!entry.isComment && entry.key) {
136
+ targetMap.set(entry.key, entry);
137
+ }
138
+ }
139
+ const missing = [];
140
+ const extra = [];
141
+ const empty = [];
142
+ let matched = 0;
143
+ for (const [key, entry] of sourceMap.entries()) {
144
+ if (!targetMap.has(key)) {
145
+ missing.push(entry);
146
+ } else {
147
+ matched++;
148
+ }
149
+ }
150
+ for (const [key, entry] of targetMap.entries()) {
151
+ if (!sourceMap.has(key)) {
152
+ extra.push(entry);
153
+ }
154
+ if (entry.value.trim() === "") {
155
+ empty.push(entry);
156
+ }
157
+ }
158
+ return {
159
+ missing,
160
+ extra,
161
+ empty,
162
+ matched,
163
+ sourceFile: "source",
164
+ targetFile: "target"
165
+ };
166
+ }
167
+
168
+ // src/core/scanner.ts
169
+ import { readdir, readFile as readFile2 } from "fs/promises";
170
+ import { join } from "path";
171
+ var severityLevels = {
172
+ critical: 4,
173
+ high: 3,
174
+ medium: 2,
175
+ low: 1
176
+ };
177
+ function globToRegex(glob) {
178
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
179
+ return new RegExp(`^${escaped}$`);
180
+ }
181
+ function matchGlobs(path, globs) {
182
+ if (globs.length === 0) return true;
183
+ return globs.some((glob) => globToRegex(glob).test(path));
184
+ }
185
+ async function walkDir(dir, includeGlobs, excludeGlobs, fileList = [], cwd = dir) {
186
+ try {
187
+ const files = await readdir(dir, { withFileTypes: true });
188
+ for (const file of files) {
189
+ const res = join(dir, file.name);
190
+ const relPath = res.substring(cwd.length + 1).replace(/\\/g, "/");
191
+ if (file.isDirectory()) {
192
+ if (!matchGlobs(relPath, excludeGlobs)) {
193
+ await walkDir(res, includeGlobs, excludeGlobs, fileList, cwd);
194
+ }
195
+ } else {
196
+ if (matchGlobs(relPath, includeGlobs) && !matchGlobs(relPath, excludeGlobs)) {
197
+ fileList.push(res);
198
+ }
199
+ }
200
+ }
201
+ } catch (error) {
202
+ }
203
+ return fileList;
204
+ }
205
+ async function scanDirectory(options) {
206
+ const start = performance.now();
207
+ const findings = [];
208
+ let filesScanned = 0;
209
+ const targetFiles = await walkDir(options.cwd, options.include, options.exclude, [], options.cwd);
210
+ const minSevLevel = severityLevels[options.minSeverity];
211
+ for (const file of targetFiles) {
212
+ filesScanned++;
213
+ try {
214
+ const content = await readFile2(file, "utf8");
215
+ const lines = content.split(/\r?\n/);
216
+ const fileName = file.replace(/\\/g, "/").split("/").pop() || "";
217
+ const isEnvFile = fileName.endsWith(".env") || fileName.startsWith(".env");
218
+ for (let i = 0; i < lines.length; i++) {
219
+ const line = lines[i];
220
+ for (const rule of options.rules) {
221
+ if (severityLevels[rule.severity] < minSevLevel) {
222
+ continue;
223
+ }
224
+ if (isEnvFile && !rule.allowInEnvFiles) {
225
+ continue;
226
+ }
227
+ const match = rule.pattern.exec(line);
228
+ if (match) {
229
+ const matchedText = match[0];
230
+ const redacted = matchedText.length > 4 ? matchedText.substring(0, 4) + "*".repeat(matchedText.length - 4) : "****";
231
+ findings.push({
232
+ rule,
233
+ filePath: file.substring(options.cwd.length + 1),
234
+ // relative path
235
+ line: i + 1,
236
+ match: redacted,
237
+ context: line.trim().substring(0, 100)
238
+ // max 100 chars
239
+ });
240
+ }
241
+ }
242
+ }
243
+ } catch (e) {
244
+ }
245
+ }
246
+ return {
247
+ findings,
248
+ filesScanned,
249
+ rulesApplied: options.rules.length,
250
+ duration: performance.now() - start
251
+ };
252
+ }
253
+
254
+ // src/utils/logger.ts
255
+ import pc from "picocolors";
256
+ var log = {
257
+ /** Informational message */
258
+ info: (msg) => console.log(pc.cyan("\u2139"), msg),
259
+ /** Success message */
260
+ success: (msg) => console.log(pc.green("\u2714"), msg),
261
+ /** Warning message */
262
+ warn: (msg) => console.log(pc.yellow("\u26A0"), msg),
263
+ /** Error message */
264
+ error: (msg) => console.error(pc.red("\u2716"), msg),
265
+ /** Debug message (only when ENVSPECT_DEBUG is set) */
266
+ debug: (msg) => {
267
+ if (process.env.ENVSPECT_DEBUG) {
268
+ console.log(pc.gray(`[debug] ${msg}`));
269
+ }
270
+ },
271
+ /** Dim/muted text */
272
+ dim: (msg) => console.log(pc.dim(msg)),
273
+ /** Bold header */
274
+ header: (msg) => console.log(`
275
+ ${pc.bold(pc.underline(msg))}
276
+ `),
277
+ /** Raw console.log (no prefix) */
278
+ raw: (msg) => console.log(msg),
279
+ /** Blank line */
280
+ newline: () => console.log()
281
+ };
282
+ var c = {
283
+ red: pc.red,
284
+ green: pc.green,
285
+ yellow: pc.yellow,
286
+ cyan: pc.cyan,
287
+ dim: pc.dim,
288
+ bold: pc.bold,
289
+ underline: pc.underline,
290
+ gray: pc.gray
291
+ };
292
+
293
+ // src/core/reporter.ts
294
+ function formatScanResult(result, format) {
295
+ const { diff, duration } = result;
296
+ if (format === "json") {
297
+ return JSON.stringify(result, null, 2);
298
+ }
299
+ if (format === "minimal") {
300
+ let out2 = "";
301
+ diff.missing.forEach((m) => out2 += `MISSING: ${m.key}
302
+ `);
303
+ diff.extra.forEach((e) => out2 += `EXTRA: ${e.key}
304
+ `);
305
+ diff.empty.forEach((e) => out2 += `EMPTY: ${e.key}
306
+ `);
307
+ return out2.trim();
308
+ }
309
+ let out = c.bold(`\u250C\u2500 Env Diff Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
310
+ `);
311
+ out += `\u2502 Source: ${diff.sourceFile}
312
+ `;
313
+ out += `\u2502 Target: ${diff.targetFile}
314
+ `;
315
+ out += `\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
316
+
317
+ `;
318
+ if (diff.missing.length > 0) {
319
+ out += c.red(c.bold(`Missing Keys (${diff.missing.length}):
320
+ `));
321
+ diff.missing.forEach((m) => out += ` - ${c.red(m.key)}
322
+ `);
323
+ }
324
+ if (diff.extra.length > 0) {
325
+ out += c.yellow(c.bold(`
326
+ Extra Keys (${diff.extra.length}):
327
+ `));
328
+ diff.extra.forEach((e) => out += ` - ${c.yellow(e.key)}
329
+ `);
330
+ }
331
+ if (diff.empty.length > 0) {
332
+ out += c.cyan(c.bold(`
333
+ Empty Keys (${diff.empty.length}):
334
+ `));
335
+ diff.empty.forEach((e) => out += ` - ${c.cyan(e.key)}
336
+ `);
337
+ }
338
+ out += c.gray(`
339
+ Matched: ${diff.matched} | Duration: ${duration.toFixed(2)}ms
340
+ `);
341
+ return out;
342
+ }
343
+ function formatAuditReport(report, format) {
344
+ if (format === "json") {
345
+ return JSON.stringify(report, null, 2);
346
+ }
347
+ if (format === "minimal") {
348
+ return report.findings.map((f) => `${f.filePath}:${f.line} [${f.rule.severity}] ${f.rule.name} - ${f.match}`).join("\n");
349
+ }
350
+ let out = c.bold(`\u250C\u2500 Audit Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
351
+ `);
352
+ out += `\u2502 Files Scanned: ${report.filesScanned}
353
+ `;
354
+ out += `\u2502 Findings: ${report.findings.length}
355
+ `;
356
+ out += `\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
357
+
358
+ `;
359
+ if (report.findings.length === 0) {
360
+ out += c.green("No secrets or issues found. \u2728\n");
361
+ } else {
362
+ report.findings.forEach((f) => {
363
+ let colorFn = c.cyan;
364
+ if (f.rule.severity === "critical") colorFn = c.red;
365
+ else if (f.rule.severity === "high") colorFn = c.yellow;
366
+ out += `${colorFn(c.bold(`[${f.rule.severity.toUpperCase()}]`))} ${f.rule.name} in ${f.filePath}:${f.line}
367
+ `;
368
+ out += ` Match: ${f.match}
369
+ `;
370
+ out += ` Context: ${c.dim(f.context)}
371
+
372
+ `;
373
+ });
374
+ }
375
+ out += c.gray(`Duration: ${report.duration.toFixed(2)}ms
376
+ `);
377
+ return out;
378
+ }
379
+
380
+ // src/commands/scan.ts
381
+ import { existsSync as existsSync2 } from "fs";
382
+ import { resolve as resolve2 } from "path";
383
+ import { createSpinner } from "nanospinner";
384
+
385
+ // src/types/index.ts
386
+ var ExitCode = {
387
+ SUCCESS: 0,
388
+ SCAN_ISSUES_FOUND: 1,
389
+ AUDIT_SECRETS_FOUND: 2,
390
+ FILE_NOT_FOUND: 3,
391
+ CONFIG_ERROR: 4,
392
+ ERROR: 5
393
+ };
394
+
395
+ // src/utils/config.ts
396
+ import { readFile as readFile3 } from "fs/promises";
397
+ import { resolve, join as join2 } from "path";
398
+ import { existsSync } from "fs";
399
+ var CONFIG_FILES = [
400
+ ".envspectrc",
401
+ ".envspectrc.json",
402
+ "envspect.config.json"
403
+ ];
404
+ async function loadConfig(cwd) {
405
+ const root = resolve(cwd || process.cwd());
406
+ for (const fileName of CONFIG_FILES) {
407
+ const filePath = join2(root, fileName);
408
+ if (!existsSync(filePath)) continue;
409
+ try {
410
+ const content = await readFile3(filePath, "utf-8");
411
+ const config = JSON.parse(content);
412
+ log.debug(`Loaded config from ${fileName}`);
413
+ return config;
414
+ } catch (err) {
415
+ log.warn(`Failed to parse config file: ${fileName}`);
416
+ log.debug(String(err));
417
+ return {};
418
+ }
419
+ }
420
+ log.debug("No config file found, using defaults");
421
+ return {};
422
+ }
423
+ var defaults = {
424
+ envFile: ".env",
425
+ exampleFile: ".env.example",
426
+ format: "table",
427
+ scan: {
428
+ strict: false
429
+ },
430
+ audit: {
431
+ include: ["**/*.{ts,tsx,js,jsx,py,rb,go,java,php,rs,yaml,yml,toml,json,xml,sh}"],
432
+ exclude: [
433
+ "**/node_modules/**",
434
+ "**/dist/**",
435
+ "**/build/**",
436
+ "**/.git/**",
437
+ "**/coverage/**",
438
+ "**/*.min.js",
439
+ "**/vendor/**",
440
+ "**/package-lock.json",
441
+ "**/pnpm-lock.yaml",
442
+ "**/yarn.lock"
443
+ ],
444
+ severity: "low"
445
+ }
446
+ };
447
+
448
+ // src/commands/scan.ts
449
+ async function runScan(options) {
450
+ const cwd = options.cwd || process.cwd();
451
+ const config = await loadConfig(cwd);
452
+ const finalOptions = {
453
+ envFile: options.envFile ?? config.envFile ?? defaults.envFile,
454
+ exampleFile: options.exampleFile ?? config.exampleFile ?? defaults.exampleFile,
455
+ strict: options.strict ?? config.scan?.strict ?? defaults.scan.strict,
456
+ format: options.format ?? config.format ?? defaults.format,
457
+ silent: options.silent ?? false,
458
+ cwd
459
+ };
460
+ const envPath = resolve2(cwd, finalOptions.envFile);
461
+ const examplePath = resolve2(cwd, finalOptions.exampleFile);
462
+ if (!existsSync2(envPath)) {
463
+ log.error(`Missing .env file at ${envPath}`);
464
+ process.exit(ExitCode.FILE_NOT_FOUND);
465
+ }
466
+ if (!existsSync2(examplePath)) {
467
+ log.error(`Missing .env.example file at ${examplePath}`);
468
+ process.exit(ExitCode.FILE_NOT_FOUND);
469
+ }
470
+ const spinner = createSpinner("Scanning environment files...").start();
471
+ try {
472
+ const start = performance.now();
473
+ const envResult = await parseEnvFile(envPath);
474
+ const exampleResult = await parseEnvFile(examplePath);
475
+ if (envResult.errors.length > 0 || exampleResult.errors.length > 0) {
476
+ spinner.error({ text: "Failed to parse environment files" });
477
+ process.exit(ExitCode.ERROR);
478
+ }
479
+ const diff = diffEnvFiles(exampleResult.entries, envResult.entries, {
480
+ strict: finalOptions.strict
481
+ });
482
+ diff.sourceFile = examplePath;
483
+ diff.targetFile = envPath;
484
+ const duration = performance.now() - start;
485
+ spinner.success({ text: "Scan complete" });
486
+ const output = formatScanResult({ diff, duration }, finalOptions.format);
487
+ console.log(output);
488
+ if (diff.missing.length > 0 || diff.extra.length > 0) {
489
+ process.exit(ExitCode.SCAN_ISSUES_FOUND);
490
+ } else {
491
+ process.exit(ExitCode.SUCCESS);
492
+ }
493
+ } catch (error) {
494
+ spinner.error({ text: "An unexpected error occurred during scan" });
495
+ if (error instanceof Error) {
496
+ log.error(error.message);
497
+ }
498
+ process.exit(ExitCode.ERROR);
499
+ }
500
+ }
501
+
502
+ // src/commands/audit.ts
503
+ import { resolve as resolve3 } from "path";
504
+ import { createSpinner as createSpinner2 } from "nanospinner";
505
+
506
+ // src/rules/default-rules.ts
507
+ var defaultRules = [
508
+ // ── AWS ──
509
+ {
510
+ id: "aws-access-key",
511
+ name: "AWS Access Key ID",
512
+ description: "Detects AWS Access Key IDs (starts with AKIA)",
513
+ pattern: /(?:^|[^A-Za-z0-9/+=])AKIA[0-9A-Z]{16}(?:[^A-Za-z0-9/+=]|$)/,
514
+ severity: "critical",
515
+ allowInEnvFiles: true
516
+ },
517
+ {
518
+ id: "aws-secret-key",
519
+ name: "AWS Secret Access Key",
520
+ description: "Detects AWS Secret Access Keys (40 char base64)",
521
+ pattern: /(?:aws_secret_access_key|aws_secret)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i,
522
+ severity: "critical",
523
+ allowInEnvFiles: true
524
+ },
525
+ // ── API Keys (Generic) ──
526
+ {
527
+ id: "generic-api-key",
528
+ name: "Generic API Key Assignment",
529
+ description: "Detects hardcoded API key assignments in source code",
530
+ pattern: /(?:api_key|apikey|api_secret|apisecret)\s*[=:]\s*['"][A-Za-z0-9_\-]{16,}['"]/i,
531
+ severity: "high",
532
+ allowInEnvFiles: true
533
+ },
534
+ // ── Private Keys ──
535
+ {
536
+ id: "private-key",
537
+ name: "Private Key",
538
+ description: "Detects PEM-encoded private keys",
539
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
540
+ severity: "critical",
541
+ allowInEnvFiles: false
542
+ },
543
+ // ── GitHub ──
544
+ {
545
+ id: "github-token",
546
+ name: "GitHub Token",
547
+ description: "Detects GitHub personal access tokens and fine-grained tokens",
548
+ pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/,
549
+ severity: "critical",
550
+ allowInEnvFiles: true
551
+ },
552
+ // ── Stripe ──
553
+ {
554
+ id: "stripe-key",
555
+ name: "Stripe API Key",
556
+ description: "Detects Stripe secret and publishable keys",
557
+ pattern: /(?:sk|pk)_(?:test|live)_[A-Za-z0-9]{24,}/,
558
+ severity: "critical",
559
+ allowInEnvFiles: true
560
+ },
561
+ // ── JWT ──
562
+ {
563
+ id: "jwt-token",
564
+ name: "JWT Token",
565
+ description: "Detects hardcoded JWT tokens",
566
+ pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
567
+ severity: "high",
568
+ allowInEnvFiles: true
569
+ },
570
+ // ── Database URLs ──
571
+ {
572
+ id: "database-url",
573
+ name: "Database Connection String",
574
+ description: "Detects database connection strings with credentials",
575
+ pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/i,
576
+ severity: "high",
577
+ allowInEnvFiles: true
578
+ },
579
+ // ── Passwords ──
580
+ {
581
+ id: "password-assignment",
582
+ name: "Password Assignment",
583
+ description: "Detects hardcoded password assignments in source code",
584
+ pattern: /(?:password|passwd|pwd|secret)\s*[=:]\s*['"][^'"]{8,}['"]/i,
585
+ severity: "high",
586
+ allowInEnvFiles: true
587
+ },
588
+ // ── Google ──
589
+ {
590
+ id: "google-api-key",
591
+ name: "Google API Key",
592
+ description: "Detects Google API keys",
593
+ pattern: /AIza[0-9A-Za-z_-]{35}/,
594
+ severity: "high",
595
+ allowInEnvFiles: true
596
+ },
597
+ // ── Slack ──
598
+ {
599
+ id: "slack-token",
600
+ name: "Slack Token",
601
+ description: "Detects Slack bot and user tokens",
602
+ pattern: /xox[bporsca]-[0-9]{10,}-[A-Za-z0-9-]+/,
603
+ severity: "high",
604
+ allowInEnvFiles: true
605
+ },
606
+ // ── SendGrid ──
607
+ {
608
+ id: "sendgrid-key",
609
+ name: "SendGrid API Key",
610
+ description: "Detects SendGrid API keys",
611
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/,
612
+ severity: "high",
613
+ allowInEnvFiles: true
614
+ },
615
+ // ── Twilio ──
616
+ {
617
+ id: "twilio-key",
618
+ name: "Twilio API Key",
619
+ description: "Detects Twilio Account SID and Auth tokens",
620
+ pattern: /(?:AC[a-z0-9]{32}|SK[a-z0-9]{32})/,
621
+ severity: "high",
622
+ allowInEnvFiles: true
623
+ },
624
+ // ── npm ──
625
+ {
626
+ id: "npm-token",
627
+ name: "npm Access Token",
628
+ description: "Detects npm access tokens",
629
+ pattern: /npm_[A-Za-z0-9]{36}/,
630
+ severity: "critical",
631
+ allowInEnvFiles: true
632
+ },
633
+ // ── Discord ──
634
+ {
635
+ id: "discord-token",
636
+ name: "Discord Bot Token",
637
+ description: "Detects Discord bot tokens",
638
+ pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/,
639
+ severity: "critical",
640
+ allowInEnvFiles: true
641
+ }
642
+ ];
643
+
644
+ // src/commands/audit.ts
645
+ async function runAudit(options) {
646
+ const cwd = options.cwd || process.cwd();
647
+ const config = await loadConfig(cwd);
648
+ const finalOptions = {
649
+ include: options.include ?? config.audit?.include ?? defaults.audit.include,
650
+ exclude: options.exclude ?? config.audit?.exclude ?? defaults.audit.exclude,
651
+ severity: options.severity ?? config.audit?.severity ?? defaults.audit.severity,
652
+ format: options.format ?? config.format ?? defaults.format,
653
+ silent: options.silent ?? false,
654
+ cwd
655
+ };
656
+ const spinner = createSpinner2("Auditing codebase for secrets...").start();
657
+ try {
658
+ const report = await scanDirectory({
659
+ cwd: resolve3(cwd),
660
+ rules: defaultRules,
661
+ include: finalOptions.include,
662
+ exclude: finalOptions.exclude,
663
+ minSeverity: finalOptions.severity
664
+ });
665
+ spinner.success({ text: "Audit complete" });
666
+ const output = formatAuditReport(report, finalOptions.format);
667
+ console.log(output);
668
+ if (report.findings.length > 0) {
669
+ process.exit(ExitCode.AUDIT_SECRETS_FOUND);
670
+ } else {
671
+ process.exit(ExitCode.SUCCESS);
672
+ }
673
+ } catch (error) {
674
+ spinner.error({ text: "An unexpected error occurred during audit" });
675
+ if (error instanceof Error) {
676
+ log.error(error.message);
677
+ }
678
+ process.exit(ExitCode.ERROR);
679
+ }
680
+ }
681
+ export {
682
+ defaultRules,
683
+ diffEnvFiles,
684
+ formatAuditReport,
685
+ formatScanResult,
686
+ parseEnvContent,
687
+ parseEnvFile,
688
+ runAudit,
689
+ runScan,
690
+ scanDirectory
691
+ };
692
+ //# sourceMappingURL=index.js.map