@stacksjs/ts-cloud 0.1.9 → 0.1.14

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 (150) hide show
  1. package/README.md +39 -377
  2. package/dist/bin/cli.js +1047 -424
  3. package/dist/index.d.ts +36 -3
  4. package/dist/index.js +76430 -7096
  5. package/package.json +7 -8
  6. package/dist/aws/acm.d.ts +0 -129
  7. package/dist/aws/application-autoscaling.d.ts +0 -282
  8. package/dist/aws/bedrock.d.ts +0 -2292
  9. package/dist/aws/client.d.ts +0 -79
  10. package/dist/aws/cloudformation.d.ts +0 -105
  11. package/dist/aws/cloudfront.d.ts +0 -265
  12. package/dist/aws/cloudwatch-logs.d.ts +0 -48
  13. package/dist/aws/comprehend.d.ts +0 -505
  14. package/dist/aws/connect.d.ts +0 -377
  15. package/dist/aws/deploy-imap.d.ts +0 -14
  16. package/dist/aws/dynamodb.d.ts +0 -176
  17. package/dist/aws/ec2.d.ts +0 -272
  18. package/dist/aws/ecr.d.ts +0 -149
  19. package/dist/aws/ecs.d.ts +0 -162
  20. package/dist/aws/elasticache.d.ts +0 -71
  21. package/dist/aws/elbv2.d.ts +0 -248
  22. package/dist/aws/email.d.ts +0 -175
  23. package/dist/aws/eventbridge.d.ts +0 -142
  24. package/dist/aws/iam.d.ts +0 -638
  25. package/dist/aws/imap-server.d.ts +0 -119
  26. package/dist/aws/index.d.ts +0 -192
  27. package/dist/aws/kendra.d.ts +0 -782
  28. package/dist/aws/lambda.d.ts +0 -232
  29. package/dist/aws/opensearch.d.ts +0 -87
  30. package/dist/aws/personalize.d.ts +0 -516
  31. package/dist/aws/polly.d.ts +0 -214
  32. package/dist/aws/rds.d.ts +0 -240
  33. package/dist/aws/rekognition.d.ts +0 -543
  34. package/dist/aws/route53-domains.d.ts +0 -113
  35. package/dist/aws/route53.d.ts +0 -215
  36. package/dist/aws/s3.d.ts +0 -212
  37. package/dist/aws/scheduler.d.ts +0 -140
  38. package/dist/aws/secrets-manager.d.ts +0 -170
  39. package/dist/aws/ses.d.ts +0 -288
  40. package/dist/aws/setup-phone.d.ts +0 -0
  41. package/dist/aws/setup-sms.d.ts +0 -115
  42. package/dist/aws/sms.d.ts +0 -304
  43. package/dist/aws/smtp-server.d.ts +0 -61
  44. package/dist/aws/sns.d.ts +0 -117
  45. package/dist/aws/sqs.d.ts +0 -65
  46. package/dist/aws/ssm.d.ts +0 -179
  47. package/dist/aws/sts.d.ts +0 -15
  48. package/dist/aws/support.d.ts +0 -104
  49. package/dist/aws/test-imap.d.ts +0 -0
  50. package/dist/aws/textract.d.ts +0 -403
  51. package/dist/aws/transcribe.d.ts +0 -60
  52. package/dist/aws/translate.d.ts +0 -358
  53. package/dist/aws/voice.d.ts +0 -219
  54. package/dist/config.d.ts +0 -7
  55. package/dist/deploy/index.d.ts +0 -2
  56. package/dist/deploy/static-site-external-dns.d.ts +0 -51
  57. package/dist/deploy/static-site.d.ts +0 -71
  58. package/dist/dns/cloudflare.d.ts +0 -52
  59. package/dist/dns/godaddy.d.ts +0 -38
  60. package/dist/dns/index.d.ts +0 -45
  61. package/dist/dns/porkbun.d.ts +0 -18
  62. package/dist/dns/route53-adapter.d.ts +0 -38
  63. package/dist/dns/types.d.ts +0 -77
  64. package/dist/dns/validator.d.ts +0 -78
  65. package/dist/generators/index.d.ts +0 -1
  66. package/dist/generators/infrastructure.d.ts +0 -30
  67. package/dist/push/apns.d.ts +0 -60
  68. package/dist/push/fcm.d.ts +0 -117
  69. package/dist/push/index.d.ts +0 -14
  70. package/dist/security/pre-deploy-scanner.d.ts +0 -69
  71. package/dist/ssl/acme-client.d.ts +0 -67
  72. package/dist/ssl/index.d.ts +0 -2
  73. package/dist/ssl/letsencrypt.d.ts +0 -48
  74. package/dist/types.d.ts +0 -1
  75. package/dist/utils/cli.d.ts +0 -123
  76. package/dist/validation/index.d.ts +0 -1
  77. package/dist/validation/template.d.ts +0 -23
  78. package/src/aws/acm.ts +0 -768
  79. package/src/aws/application-autoscaling.ts +0 -845
  80. package/src/aws/bedrock.ts +0 -4074
  81. package/src/aws/client.ts +0 -891
  82. package/src/aws/cloudformation.ts +0 -896
  83. package/src/aws/cloudfront.ts +0 -1531
  84. package/src/aws/cloudwatch-logs.ts +0 -154
  85. package/src/aws/comprehend.ts +0 -839
  86. package/src/aws/connect.ts +0 -1056
  87. package/src/aws/deploy-imap.ts +0 -384
  88. package/src/aws/dynamodb.ts +0 -340
  89. package/src/aws/ec2.ts +0 -1385
  90. package/src/aws/ecr.ts +0 -621
  91. package/src/aws/ecs.ts +0 -615
  92. package/src/aws/elasticache.ts +0 -301
  93. package/src/aws/elbv2.ts +0 -942
  94. package/src/aws/email.ts +0 -928
  95. package/src/aws/eventbridge.ts +0 -248
  96. package/src/aws/iam.ts +0 -1689
  97. package/src/aws/imap-server.ts +0 -2100
  98. package/src/aws/index.ts +0 -213
  99. package/src/aws/kendra.ts +0 -1097
  100. package/src/aws/lambda.ts +0 -786
  101. package/src/aws/opensearch.ts +0 -158
  102. package/src/aws/personalize.ts +0 -977
  103. package/src/aws/polly.ts +0 -559
  104. package/src/aws/rds.ts +0 -888
  105. package/src/aws/rekognition.ts +0 -846
  106. package/src/aws/route53-domains.ts +0 -359
  107. package/src/aws/route53.ts +0 -1046
  108. package/src/aws/s3.ts +0 -2334
  109. package/src/aws/scheduler.ts +0 -571
  110. package/src/aws/secrets-manager.ts +0 -769
  111. package/src/aws/ses.ts +0 -1081
  112. package/src/aws/setup-phone.ts +0 -104
  113. package/src/aws/setup-sms.ts +0 -580
  114. package/src/aws/sms.ts +0 -1735
  115. package/src/aws/smtp-server.ts +0 -531
  116. package/src/aws/sns.ts +0 -758
  117. package/src/aws/sqs.ts +0 -382
  118. package/src/aws/ssm.ts +0 -807
  119. package/src/aws/sts.ts +0 -92
  120. package/src/aws/support.ts +0 -391
  121. package/src/aws/test-imap.ts +0 -86
  122. package/src/aws/textract.ts +0 -780
  123. package/src/aws/transcribe.ts +0 -108
  124. package/src/aws/translate.ts +0 -641
  125. package/src/aws/voice.ts +0 -1379
  126. package/src/config.ts +0 -35
  127. package/src/deploy/index.ts +0 -7
  128. package/src/deploy/static-site-external-dns.ts +0 -945
  129. package/src/deploy/static-site.ts +0 -1175
  130. package/src/dns/cloudflare.ts +0 -548
  131. package/src/dns/godaddy.ts +0 -412
  132. package/src/dns/index.ts +0 -205
  133. package/src/dns/porkbun.ts +0 -362
  134. package/src/dns/route53-adapter.ts +0 -414
  135. package/src/dns/types.ts +0 -119
  136. package/src/dns/validator.ts +0 -369
  137. package/src/generators/index.ts +0 -5
  138. package/src/generators/infrastructure.ts +0 -1660
  139. package/src/index.ts +0 -163
  140. package/src/push/apns.ts +0 -452
  141. package/src/push/fcm.ts +0 -506
  142. package/src/push/index.ts +0 -58
  143. package/src/security/pre-deploy-scanner.ts +0 -655
  144. package/src/ssl/acme-client.ts +0 -478
  145. package/src/ssl/index.ts +0 -7
  146. package/src/ssl/letsencrypt.ts +0 -747
  147. package/src/types.ts +0 -2
  148. package/src/utils/cli.ts +0 -398
  149. package/src/validation/index.ts +0 -5
  150. package/src/validation/template.ts +0 -405
@@ -1,655 +0,0 @@
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
- }