@stacksjs/ts-cloud 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/aws/s3.d.ts +1 -1
  2. package/dist/bin/cli.js +223 -222
  3. package/dist/index.js +132 -132
  4. package/package.json +18 -16
  5. package/src/aws/acm.ts +768 -0
  6. package/src/aws/application-autoscaling.ts +845 -0
  7. package/src/aws/bedrock.ts +4074 -0
  8. package/src/aws/client.ts +891 -0
  9. package/src/aws/cloudformation.ts +896 -0
  10. package/src/aws/cloudfront.ts +1531 -0
  11. package/src/aws/cloudwatch-logs.ts +154 -0
  12. package/src/aws/comprehend.ts +839 -0
  13. package/src/aws/connect.ts +1056 -0
  14. package/src/aws/deploy-imap.ts +384 -0
  15. package/src/aws/dynamodb.ts +340 -0
  16. package/src/aws/ec2.ts +1385 -0
  17. package/src/aws/ecr.ts +621 -0
  18. package/src/aws/ecs.ts +615 -0
  19. package/src/aws/elasticache.ts +301 -0
  20. package/src/aws/elbv2.ts +942 -0
  21. package/src/aws/email.ts +928 -0
  22. package/src/aws/eventbridge.ts +248 -0
  23. package/src/aws/iam.ts +1689 -0
  24. package/src/aws/imap-server.ts +2100 -0
  25. package/src/aws/index.ts +213 -0
  26. package/src/aws/kendra.ts +1097 -0
  27. package/src/aws/lambda.ts +786 -0
  28. package/src/aws/opensearch.ts +158 -0
  29. package/src/aws/personalize.ts +977 -0
  30. package/src/aws/polly.ts +559 -0
  31. package/src/aws/rds.ts +888 -0
  32. package/src/aws/rekognition.ts +846 -0
  33. package/src/aws/route53-domains.ts +359 -0
  34. package/src/aws/route53.ts +1046 -0
  35. package/src/aws/s3.ts +2334 -0
  36. package/src/aws/scheduler.ts +571 -0
  37. package/src/aws/secrets-manager.ts +769 -0
  38. package/src/aws/ses.ts +1081 -0
  39. package/src/aws/setup-phone.ts +104 -0
  40. package/src/aws/setup-sms.ts +580 -0
  41. package/src/aws/sms.ts +1735 -0
  42. package/src/aws/smtp-server.ts +531 -0
  43. package/src/aws/sns.ts +758 -0
  44. package/src/aws/sqs.ts +382 -0
  45. package/src/aws/ssm.ts +807 -0
  46. package/src/aws/sts.ts +92 -0
  47. package/src/aws/support.ts +391 -0
  48. package/src/aws/test-imap.ts +86 -0
  49. package/src/aws/textract.ts +780 -0
  50. package/src/aws/transcribe.ts +108 -0
  51. package/src/aws/translate.ts +641 -0
  52. package/src/aws/voice.ts +1379 -0
  53. package/src/config.ts +35 -0
  54. package/src/deploy/index.ts +7 -0
  55. package/src/deploy/static-site-external-dns.ts +945 -0
  56. package/src/deploy/static-site.ts +1175 -0
  57. package/src/dns/cloudflare.ts +548 -0
  58. package/src/dns/godaddy.ts +412 -0
  59. package/src/dns/index.ts +205 -0
  60. package/src/dns/porkbun.ts +362 -0
  61. package/src/dns/route53-adapter.ts +414 -0
  62. package/src/dns/types.ts +119 -0
  63. package/src/dns/validator.ts +369 -0
  64. package/src/generators/index.ts +5 -0
  65. package/src/generators/infrastructure.ts +1660 -0
  66. package/src/index.ts +163 -0
  67. package/src/push/apns.ts +452 -0
  68. package/src/push/fcm.ts +506 -0
  69. package/src/push/index.ts +58 -0
  70. package/src/security/pre-deploy-scanner.ts +655 -0
  71. package/src/ssl/acme-client.ts +478 -0
  72. package/src/ssl/index.ts +7 -0
  73. package/src/ssl/letsencrypt.ts +747 -0
  74. package/src/types.ts +2 -0
  75. package/src/utils/cli.ts +398 -0
  76. package/src/validation/index.ts +5 -0
  77. package/src/validation/template.ts +405 -0
@@ -0,0 +1,655 @@
1
+ /**
2
+ * Pre-Deployment Security Scanner
3
+ * Scans source code for leaked secrets, credentials, and sensitive data before deployment
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
7
+ import { join, relative, extname } from 'node:path'
8
+
9
+ export interface SecretPattern {
10
+ name: string
11
+ pattern: RegExp
12
+ severity: 'critical' | 'high' | 'medium' | 'low'
13
+ description: string
14
+ }
15
+
16
+ export interface SecurityFinding {
17
+ file: string
18
+ line: number
19
+ column: number
20
+ match: string
21
+ pattern: SecretPattern
22
+ context: string
23
+ }
24
+
25
+ export interface ScanResult {
26
+ passed: boolean
27
+ findings: SecurityFinding[]
28
+ scannedFiles: number
29
+ duration: number
30
+ summary: {
31
+ critical: number
32
+ high: number
33
+ medium: number
34
+ low: number
35
+ }
36
+ }
37
+
38
+ export interface ScanOptions {
39
+ directory: string
40
+ exclude?: string[]
41
+ include?: string[]
42
+ skipPatterns?: string[]
43
+ maxFileSize?: number
44
+ failOnSeverity?: 'critical' | 'high' | 'medium' | 'low'
45
+ }
46
+
47
+ /**
48
+ * Common secret patterns to detect
49
+ */
50
+ export const SECRET_PATTERNS: SecretPattern[] = [
51
+ // AWS Credentials
52
+ {
53
+ name: 'AWS Access Key ID',
54
+ pattern: /(?:^|[^A-Z0-9])((AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16})(?:[^A-Z0-9]|$)/g,
55
+ severity: 'critical',
56
+ description: 'AWS Access Key ID detected',
57
+ },
58
+ {
59
+ name: 'AWS Secret Access Key',
60
+ pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key|secretAccessKey)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
61
+ severity: 'critical',
62
+ description: 'AWS Secret Access Key detected',
63
+ },
64
+ {
65
+ name: 'AWS Secret Key (Generic)',
66
+ pattern: /(?:^|['"`:=\s])([A-Za-z0-9/+=]{40})(?:['"`\s]|$)/g,
67
+ severity: 'high',
68
+ description: 'Potential AWS Secret Key (40-char base64)',
69
+ },
70
+
71
+ // API Keys (Generic)
72
+ {
73
+ name: 'Generic API Key',
74
+ pattern: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[=:]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?/gi,
75
+ severity: 'high',
76
+ description: 'Generic API key detected',
77
+ },
78
+
79
+ // Private Keys
80
+ {
81
+ name: 'RSA Private Key',
82
+ pattern: /-----BEGIN RSA PRIVATE KEY-----/g,
83
+ severity: 'critical',
84
+ description: 'RSA private key detected',
85
+ },
86
+ {
87
+ name: 'OpenSSH Private Key',
88
+ pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/g,
89
+ severity: 'critical',
90
+ description: 'OpenSSH private key detected',
91
+ },
92
+ {
93
+ name: 'EC Private Key',
94
+ pattern: /-----BEGIN EC PRIVATE KEY-----/g,
95
+ severity: 'critical',
96
+ description: 'EC private key detected',
97
+ },
98
+ {
99
+ name: 'PGP Private Key',
100
+ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
101
+ severity: 'critical',
102
+ description: 'PGP private key detected',
103
+ },
104
+
105
+ // Tokens
106
+ {
107
+ name: 'GitHub Token',
108
+ pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g,
109
+ severity: 'critical',
110
+ description: 'GitHub personal access token detected',
111
+ },
112
+ {
113
+ name: 'GitHub OAuth',
114
+ pattern: /github[_-]?oauth[_-]?token\s*[=:]\s*['"]?([A-Za-z0-9_]{40})['"]?/gi,
115
+ severity: 'critical',
116
+ description: 'GitHub OAuth token detected',
117
+ },
118
+ {
119
+ name: 'Slack Token',
120
+ pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/g,
121
+ severity: 'critical',
122
+ description: 'Slack token detected',
123
+ },
124
+ {
125
+ name: 'Slack Webhook',
126
+ pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g,
127
+ severity: 'high',
128
+ description: 'Slack webhook URL detected',
129
+ },
130
+ {
131
+ name: 'Discord Webhook',
132
+ pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g,
133
+ severity: 'high',
134
+ description: 'Discord webhook URL detected',
135
+ },
136
+ {
137
+ name: 'JWT Token',
138
+ pattern: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g,
139
+ severity: 'high',
140
+ description: 'JWT token detected',
141
+ },
142
+
143
+ // Cloud Provider Keys
144
+ {
145
+ name: 'Google API Key',
146
+ pattern: /AIza[0-9A-Za-z_-]{35}/g,
147
+ severity: 'critical',
148
+ description: 'Google API key detected',
149
+ },
150
+ {
151
+ name: 'Google OAuth ID',
152
+ pattern: /[0-9]+-[A-Za-z0-9_]{32}\.apps\.googleusercontent\.com/g,
153
+ severity: 'high',
154
+ description: 'Google OAuth client ID detected',
155
+ },
156
+ {
157
+ name: 'Firebase API Key',
158
+ pattern: /(?:firebase[_-]?api[_-]?key)\s*[=:]\s*['"]?([A-Za-z0-9_-]{39})['"]?/gi,
159
+ severity: 'critical',
160
+ description: 'Firebase API key detected',
161
+ },
162
+ {
163
+ name: 'Cloudflare API Token',
164
+ pattern: /(?:cloudflare[_-]?api[_-]?token|cf[_-]?api[_-]?token)\s*[=:]\s*['"]?([A-Za-z0-9_-]{40})['"]?/gi,
165
+ severity: 'critical',
166
+ description: 'Cloudflare API token detected',
167
+ },
168
+ {
169
+ name: 'Azure Client Secret',
170
+ pattern: /(?:azure[_-]?client[_-]?secret|client[_-]?secret)\s*[=:]\s*['"]?([A-Za-z0-9~._-]{34,})['"]?/gi,
171
+ severity: 'critical',
172
+ description: 'Azure client secret detected',
173
+ },
174
+ {
175
+ name: 'Heroku API Key',
176
+ pattern: /(?:heroku[_-]?api[_-]?key)\s*[=:]\s*['"]?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})['"]?/gi,
177
+ severity: 'critical',
178
+ description: 'Heroku API key detected',
179
+ },
180
+
181
+ // Database Credentials
182
+ {
183
+ name: 'Database Connection String',
184
+ pattern: /(?:mysql|postgres|postgresql|mongodb|redis|mongodb\+srv):\/\/[^:]+:[^@]+@[^/\s]+/gi,
185
+ severity: 'critical',
186
+ description: 'Database connection string with credentials detected',
187
+ },
188
+ {
189
+ name: 'Database Password',
190
+ pattern: /(?:db[_-]?password|database[_-]?password|mysql[_-]?password|postgres[_-]?password)\s*[=:]\s*['"]?([^'"\s]{8,})['"]?/gi,
191
+ severity: 'critical',
192
+ description: 'Database password detected',
193
+ },
194
+
195
+ // Payment/Financial
196
+ {
197
+ name: 'Stripe API Key',
198
+ pattern: /(?:sk|pk)_(?:test|live)_[0-9a-zA-Z]{24,}/g,
199
+ severity: 'critical',
200
+ description: 'Stripe API key detected',
201
+ },
202
+ {
203
+ name: 'PayPal Client ID',
204
+ pattern: /(?:paypal[_-]?client[_-]?id)\s*[=:]\s*['"]?([A-Za-z0-9_-]{80})['"]?/gi,
205
+ severity: 'high',
206
+ description: 'PayPal client ID detected',
207
+ },
208
+ {
209
+ name: 'Square Access Token',
210
+ pattern: /sq0[a-z]{3}-[0-9A-Za-z_-]{22,}/g,
211
+ severity: 'critical',
212
+ description: 'Square access token detected',
213
+ },
214
+
215
+ // Communication Services
216
+ {
217
+ name: 'Twilio API Key',
218
+ pattern: /SK[a-f0-9]{32}/g,
219
+ severity: 'critical',
220
+ description: 'Twilio API key detected',
221
+ },
222
+ {
223
+ name: 'SendGrid API Key',
224
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g,
225
+ severity: 'critical',
226
+ description: 'SendGrid API key detected',
227
+ },
228
+ {
229
+ name: 'Mailgun API Key',
230
+ pattern: /key-[0-9a-zA-Z]{32}/g,
231
+ severity: 'critical',
232
+ description: 'Mailgun API key detected',
233
+ },
234
+
235
+ // Authentication Secrets
236
+ {
237
+ name: 'Password in Code',
238
+ pattern: /(?:password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"](?!\s*[,\]])/gi,
239
+ severity: 'high',
240
+ description: 'Hardcoded password detected',
241
+ },
242
+ {
243
+ name: 'Secret/Token Assignment',
244
+ pattern: /(?:secret|token|auth[_-]?token|access[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9_\-/+=]{16,})['"](?!\s*[,\]])/gi,
245
+ severity: 'high',
246
+ description: 'Hardcoded secret or token detected',
247
+ },
248
+
249
+ // NPM/Package Registry
250
+ {
251
+ name: 'NPM Token',
252
+ pattern: /(?:npm[_-]?token)\s*[=:]\s*['"]?([A-Za-z0-9_-]{36})['"]?/gi,
253
+ severity: 'critical',
254
+ description: 'NPM token detected',
255
+ },
256
+
257
+ // SSH/Git
258
+ {
259
+ name: 'SSH Private Key Path Exposed',
260
+ pattern: /~\/\.ssh\/id_[a-z]+|\/home\/[^/]+\/\.ssh\/id_[a-z]+/g,
261
+ severity: 'medium',
262
+ description: 'SSH private key path exposed',
263
+ },
264
+
265
+ // Environment Variable Leaks
266
+ {
267
+ name: 'Env Variable with Secret',
268
+ pattern: /(?:process\.env\.)([A-Z_]*(?:SECRET|KEY|TOKEN|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*)\s*(?:===?\s*['"]([^'"]+)['"])?/g,
269
+ severity: 'medium',
270
+ description: 'Environment variable containing secret may be exposed',
271
+ },
272
+ ]
273
+
274
+ /**
275
+ * File extensions to scan by default
276
+ */
277
+ const DEFAULT_SCAN_EXTENSIONS = [
278
+ '.js',
279
+ '.jsx',
280
+ '.ts',
281
+ '.tsx',
282
+ '.mjs',
283
+ '.cjs',
284
+ '.vue',
285
+ '.svelte',
286
+ '.html',
287
+ '.htm',
288
+ '.css',
289
+ '.scss',
290
+ '.less',
291
+ '.json',
292
+ '.yaml',
293
+ '.yml',
294
+ '.toml',
295
+ '.xml',
296
+ '.env',
297
+ '.config',
298
+ '.conf',
299
+ ]
300
+
301
+ /**
302
+ * Directories to exclude by default
303
+ */
304
+ const DEFAULT_EXCLUDE_DIRS = [
305
+ 'node_modules',
306
+ '.git',
307
+ '.svn',
308
+ '.hg',
309
+ 'dist',
310
+ 'build',
311
+ 'coverage',
312
+ '.nyc_output',
313
+ '__pycache__',
314
+ '.pytest_cache',
315
+ 'vendor',
316
+ '.idea',
317
+ '.vscode',
318
+ '.turbo',
319
+ '.next',
320
+ '.nuxt',
321
+ ]
322
+
323
+ /**
324
+ * Files to exclude by default
325
+ */
326
+ const DEFAULT_EXCLUDE_FILES = [
327
+ 'package-lock.json',
328
+ 'yarn.lock',
329
+ 'pnpm-lock.yaml',
330
+ 'bun.lockb',
331
+ '*.min.js',
332
+ '*.min.css',
333
+ '*.map',
334
+ ]
335
+
336
+ /**
337
+ * Pre-deployment security scanner
338
+ */
339
+ export class PreDeployScanner {
340
+ private patterns: SecretPattern[]
341
+ private excludeDirs: string[]
342
+ private excludeFiles: string[]
343
+ private maxFileSize: number
344
+
345
+ constructor(options?: {
346
+ customPatterns?: SecretPattern[]
347
+ excludeDirs?: string[]
348
+ excludeFiles?: string[]
349
+ maxFileSize?: number
350
+ }) {
351
+ this.patterns = [...SECRET_PATTERNS, ...(options?.customPatterns || [])]
352
+ this.excludeDirs = [...DEFAULT_EXCLUDE_DIRS, ...(options?.excludeDirs || [])]
353
+ this.excludeFiles = [...DEFAULT_EXCLUDE_FILES, ...(options?.excludeFiles || [])]
354
+ this.maxFileSize = options?.maxFileSize || 1024 * 1024 // 1MB default
355
+ }
356
+
357
+ /**
358
+ * Scan a directory for secrets
359
+ */
360
+ async scan(options: ScanOptions): Promise<ScanResult> {
361
+ const startTime = Date.now()
362
+ const findings: SecurityFinding[] = []
363
+ let scannedFiles = 0
364
+
365
+ const { directory, exclude = [], include, skipPatterns = [] } = options
366
+ const failSeverity = options.failOnSeverity || 'critical'
367
+
368
+ if (!existsSync(directory)) {
369
+ throw new Error(`Directory not found: ${directory}`)
370
+ }
371
+
372
+ // Get all files to scan
373
+ const files = this.getFilesToScan(directory, [...this.excludeDirs, ...exclude], include)
374
+
375
+ // Scan each file
376
+ for (const file of files) {
377
+ const relativePath = relative(directory, file)
378
+
379
+ // Skip excluded files
380
+ if (this.shouldExcludeFile(relativePath)) {
381
+ continue
382
+ }
383
+
384
+ try {
385
+ const stat = statSync(file)
386
+
387
+ // Skip files that are too large
388
+ if (stat.size > this.maxFileSize) {
389
+ continue
390
+ }
391
+
392
+ const content = readFileSync(file, 'utf-8')
393
+ const fileFindings = this.scanContent(content, relativePath, skipPatterns)
394
+ findings.push(...fileFindings)
395
+ scannedFiles++
396
+ }
397
+ catch {
398
+ // Skip files that can't be read (binary, etc.)
399
+ continue
400
+ }
401
+ }
402
+
403
+ // Calculate summary
404
+ const summary = {
405
+ critical: findings.filter(f => f.pattern.severity === 'critical').length,
406
+ high: findings.filter(f => f.pattern.severity === 'high').length,
407
+ medium: findings.filter(f => f.pattern.severity === 'medium').length,
408
+ low: findings.filter(f => f.pattern.severity === 'low').length,
409
+ }
410
+
411
+ // Determine if scan passed based on severity threshold
412
+ const severityOrder = ['low', 'medium', 'high', 'critical']
413
+ const failIndex = severityOrder.indexOf(failSeverity)
414
+ let passed = true
415
+
416
+ for (let i = failIndex; i < severityOrder.length; i++) {
417
+ if (summary[severityOrder[i] as keyof typeof summary] > 0) {
418
+ passed = false
419
+ break
420
+ }
421
+ }
422
+
423
+ return {
424
+ passed,
425
+ findings,
426
+ scannedFiles,
427
+ duration: Date.now() - startTime,
428
+ summary,
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Scan content for secrets
434
+ */
435
+ private scanContent(content: string, filePath: string, skipPatterns: string[]): SecurityFinding[] {
436
+ const findings: SecurityFinding[] = []
437
+ const lines = content.split('\n')
438
+
439
+ for (const pattern of this.patterns) {
440
+ // Skip patterns if specified
441
+ if (skipPatterns.includes(pattern.name)) {
442
+ continue
443
+ }
444
+
445
+ // Reset regex lastIndex
446
+ pattern.pattern.lastIndex = 0
447
+
448
+ let match: RegExpExecArray | null
449
+ while ((match = pattern.pattern.exec(content)) !== null) {
450
+ // Find line number and column
451
+ const beforeMatch = content.substring(0, match.index)
452
+ const lineNumber = beforeMatch.split('\n').length
453
+ const lastNewline = beforeMatch.lastIndexOf('\n')
454
+ const column = match.index - lastNewline
455
+
456
+ // Get context (the line containing the match)
457
+ const contextLine = lines[lineNumber - 1] || ''
458
+
459
+ // Skip if it looks like a test/example/placeholder
460
+ if (this.isLikelyPlaceholder(match[0], contextLine)) {
461
+ continue
462
+ }
463
+
464
+ findings.push({
465
+ file: filePath,
466
+ line: lineNumber,
467
+ column,
468
+ match: this.maskSecret(match[0]),
469
+ pattern,
470
+ context: this.maskSecret(contextLine.trim()),
471
+ })
472
+ }
473
+ }
474
+
475
+ return findings
476
+ }
477
+
478
+ /**
479
+ * Check if a match is likely a placeholder/example
480
+ */
481
+ private isLikelyPlaceholder(match: string, context: string): boolean {
482
+ const placeholderIndicators = [
483
+ 'example',
484
+ 'placeholder',
485
+ 'your_',
486
+ 'YOUR_',
487
+ 'xxx',
488
+ 'XXX',
489
+ '***',
490
+ 'test',
491
+ 'TEST',
492
+ 'dummy',
493
+ 'DUMMY',
494
+ 'fake',
495
+ 'FAKE',
496
+ 'sample',
497
+ 'SAMPLE',
498
+ '<your',
499
+ '${',
500
+ '{{',
501
+ 'process.env',
502
+ 'import.meta.env',
503
+ 'CHANGEME',
504
+ 'TODO',
505
+ 'FIXME',
506
+ ]
507
+
508
+ const lowerMatch = match.toLowerCase()
509
+ const lowerContext = context.toLowerCase()
510
+
511
+ for (const indicator of placeholderIndicators) {
512
+ if (lowerMatch.includes(indicator.toLowerCase()) || lowerContext.includes(indicator.toLowerCase())) {
513
+ return true
514
+ }
515
+ }
516
+
517
+ // Check if it's in a comment
518
+ const trimmedContext = context.trim()
519
+ if (trimmedContext.startsWith('//') || trimmedContext.startsWith('#') || trimmedContext.startsWith('*') || trimmedContext.startsWith('/*')) {
520
+ // Only skip if it's clearly documentation
521
+ if (lowerContext.includes('example') || lowerContext.includes('format:') || lowerContext.includes('e.g.')) {
522
+ return true
523
+ }
524
+ }
525
+
526
+ return false
527
+ }
528
+
529
+ /**
530
+ * Mask a secret for display
531
+ */
532
+ private maskSecret(value: string): string {
533
+ if (value.length <= 8) {
534
+ return '*'.repeat(value.length)
535
+ }
536
+
537
+ const visibleChars = Math.min(4, Math.floor(value.length * 0.2))
538
+ return value.substring(0, visibleChars) + '*'.repeat(value.length - visibleChars * 2) + value.substring(value.length - visibleChars)
539
+ }
540
+
541
+ /**
542
+ * Get all files to scan in a directory
543
+ */
544
+ private getFilesToScan(dir: string, excludeDirs: string[], includeExtensions?: string[]): string[] {
545
+ const files: string[] = []
546
+ const extensions = includeExtensions || DEFAULT_SCAN_EXTENSIONS
547
+
548
+ const scan = (currentDir: string) => {
549
+ const entries = readdirSync(currentDir, { withFileTypes: true })
550
+
551
+ for (const entry of entries) {
552
+ const fullPath = join(currentDir, entry.name)
553
+
554
+ if (entry.isDirectory()) {
555
+ // Skip excluded directories
556
+ if (!excludeDirs.includes(entry.name)) {
557
+ scan(fullPath)
558
+ }
559
+ }
560
+ else if (entry.isFile()) {
561
+ const ext = extname(entry.name).toLowerCase()
562
+ // Include files with matching extensions or no extension (like .env files)
563
+ if (extensions.includes(ext) || entry.name.startsWith('.env') || entry.name.endsWith('.config')) {
564
+ files.push(fullPath)
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ scan(dir)
571
+ return files
572
+ }
573
+
574
+ /**
575
+ * Check if a file should be excluded
576
+ */
577
+ private shouldExcludeFile(filePath: string): boolean {
578
+ const fileName = filePath.split('/').pop() || ''
579
+
580
+ for (const pattern of this.excludeFiles) {
581
+ if (pattern.startsWith('*')) {
582
+ // Wildcard pattern
583
+ const suffix = pattern.substring(1)
584
+ if (fileName.endsWith(suffix)) {
585
+ return true
586
+ }
587
+ }
588
+ else if (fileName === pattern) {
589
+ return true
590
+ }
591
+ }
592
+
593
+ return false
594
+ }
595
+
596
+ /**
597
+ * Add custom patterns
598
+ */
599
+ addPattern(pattern: SecretPattern): void {
600
+ this.patterns.push(pattern)
601
+ }
602
+
603
+ /**
604
+ * Get all registered patterns
605
+ */
606
+ getPatterns(): SecretPattern[] {
607
+ return [...this.patterns]
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Convenience function to scan a directory
613
+ */
614
+ export async function scanForSecrets(options: ScanOptions): Promise<ScanResult> {
615
+ const scanner = new PreDeployScanner()
616
+ return scanner.scan(options)
617
+ }
618
+
619
+ /**
620
+ * Format scan results for CLI output
621
+ */
622
+ export function formatScanResults(result: ScanResult): string {
623
+ const lines: string[] = []
624
+
625
+ lines.push(`\nSecurity Scan Results`)
626
+ lines.push('='.repeat(50))
627
+ lines.push(`Files scanned: ${result.scannedFiles}`)
628
+ lines.push(`Duration: ${result.duration}ms`)
629
+ lines.push('')
630
+
631
+ lines.push('Summary:')
632
+ lines.push(` Critical: ${result.summary.critical}`)
633
+ lines.push(` High: ${result.summary.high}`)
634
+ lines.push(` Medium: ${result.summary.medium}`)
635
+ lines.push(` Low: ${result.summary.low}`)
636
+ lines.push('')
637
+
638
+ if (result.findings.length > 0) {
639
+ lines.push('Findings:')
640
+ lines.push('-'.repeat(50))
641
+
642
+ for (const finding of result.findings) {
643
+ lines.push(`\n[${finding.pattern.severity.toUpperCase()}] ${finding.pattern.name}`)
644
+ lines.push(` File: ${finding.file}:${finding.line}:${finding.column}`)
645
+ lines.push(` Match: ${finding.match}`)
646
+ lines.push(` Context: ${finding.context}`)
647
+ lines.push(` Description: ${finding.pattern.description}`)
648
+ }
649
+ }
650
+
651
+ lines.push('')
652
+ lines.push(result.passed ? '✓ Security scan passed' : '✗ Security scan failed')
653
+
654
+ return lines.join('\n')
655
+ }