@girardmedia/bootspring 1.2.0 → 2.0.3

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 (253) hide show
  1. package/README.md +107 -14
  2. package/bin/bootspring.js +166 -27
  3. package/cli/agent.js +189 -17
  4. package/cli/analyze.js +499 -0
  5. package/cli/audit.js +557 -0
  6. package/cli/auth.js +495 -38
  7. package/cli/billing.js +302 -0
  8. package/cli/build.js +695 -0
  9. package/cli/business.js +109 -26
  10. package/cli/checkpoint-utils.js +168 -0
  11. package/cli/checkpoint.js +639 -0
  12. package/cli/cloud-sync.js +447 -0
  13. package/cli/content.js +198 -0
  14. package/cli/context.js +1 -1
  15. package/cli/deploy.js +543 -0
  16. package/cli/fundraise.js +112 -50
  17. package/cli/github-cmd.js +435 -0
  18. package/cli/health.js +477 -0
  19. package/cli/init.js +84 -13
  20. package/cli/legal.js +107 -95
  21. package/cli/log.js +2 -2
  22. package/cli/loop.js +976 -73
  23. package/cli/manager.js +711 -0
  24. package/cli/metrics.js +480 -0
  25. package/cli/monitor.js +812 -0
  26. package/cli/onboard.js +521 -0
  27. package/cli/orchestrator.js +12 -24
  28. package/cli/prd.js +594 -0
  29. package/cli/preseed-start.js +1483 -0
  30. package/cli/preseed.js +2302 -0
  31. package/cli/project.js +436 -0
  32. package/cli/quality.js +233 -0
  33. package/cli/security.js +913 -0
  34. package/cli/seed.js +1441 -5
  35. package/cli/skill.js +273 -211
  36. package/cli/suggest.js +989 -0
  37. package/cli/switch.js +453 -0
  38. package/cli/visualize.js +527 -0
  39. package/cli/watch.js +769 -0
  40. package/cli/workspace.js +607 -0
  41. package/core/analyze-workflow.js +1134 -0
  42. package/core/api-client.js +535 -22
  43. package/core/audit-workflow.js +1350 -0
  44. package/core/build-orchestrator.js +480 -0
  45. package/core/build-state.js +577 -0
  46. package/core/checkpoint-engine.js +408 -0
  47. package/core/config.js +1109 -26
  48. package/core/context-loader.js +21 -1
  49. package/core/deploy-workflow.js +836 -0
  50. package/core/entitlements.js +93 -22
  51. package/core/github-sync.js +610 -0
  52. package/core/index.js +8 -1
  53. package/core/ingest.js +1111 -0
  54. package/core/metrics-engine.js +768 -0
  55. package/core/onboard-workflow.js +1007 -0
  56. package/core/preseed-workflow.js +934 -0
  57. package/core/preseed.js +1617 -0
  58. package/core/project-context.js +325 -0
  59. package/core/project-state.js +694 -0
  60. package/core/r2-sync.js +583 -0
  61. package/core/scaffold.js +525 -7
  62. package/core/session.js +258 -0
  63. package/core/task-extractor.js +758 -0
  64. package/core/telemetry.js +28 -6
  65. package/core/tier-enforcement.js +737 -0
  66. package/core/utils.js +38 -14
  67. package/generators/questionnaire.js +15 -12
  68. package/generators/sections/ai.js +7 -7
  69. package/generators/sections/content.js +300 -0
  70. package/generators/sections/index.js +3 -0
  71. package/generators/sections/plugins.js +7 -6
  72. package/generators/templates/build-planning.template.js +596 -0
  73. package/generators/templates/content.template.js +819 -0
  74. package/generators/templates/index.js +2 -1
  75. package/hooks/git-autopilot.js +1250 -0
  76. package/hooks/index.js +9 -0
  77. package/intelligence/agent-collab.js +2057 -0
  78. package/intelligence/auto-suggest.js +634 -0
  79. package/intelligence/content-gen.js +1589 -0
  80. package/intelligence/cross-project.js +1647 -0
  81. package/intelligence/index.js +184 -0
  82. package/intelligence/learning/insights.json +517 -7
  83. package/intelligence/learning/pattern-learner.js +1008 -14
  84. package/intelligence/memory/decision-tracker.js +1431 -31
  85. package/intelligence/memory/decisions.jsonl +0 -0
  86. package/intelligence/orchestrator.js +2896 -1
  87. package/intelligence/prd.js +92 -1
  88. package/intelligence/recommendation-weights.json +14 -2
  89. package/intelligence/recommendations.js +463 -9
  90. package/intelligence/workflow-composer.js +1451 -0
  91. package/marketplace/index.d.ts +324 -0
  92. package/marketplace/index.js +1921 -0
  93. package/mcp/contracts/mcp-contract.v1.json +342 -4
  94. package/mcp/registry.js +680 -3
  95. package/mcp/response-formatter.js +23 -0
  96. package/mcp/tools/assist-tool.js +78 -4
  97. package/mcp/tools/autopilot-tool.js +408 -0
  98. package/mcp/tools/content-tool.js +571 -0
  99. package/mcp/tools/dashboard-tool.js +251 -5
  100. package/mcp/tools/mvp-tool.js +344 -0
  101. package/mcp/tools/plugin-tool.js +23 -1
  102. package/mcp/tools/prd-tool.js +579 -0
  103. package/mcp/tools/seed-tool.js +447 -0
  104. package/mcp/tools/skill-tool.js +43 -14
  105. package/mcp/tools/suggest-tool.js +147 -0
  106. package/package.json +15 -6
  107. package/agents/README.md +0 -93
  108. package/agents/ai-integration-expert/context.md +0 -386
  109. package/agents/api-expert/context.md +0 -416
  110. package/agents/architecture-expert/context.md +0 -454
  111. package/agents/auth-expert/context.md +0 -399
  112. package/agents/backend-expert/context.md +0 -483
  113. package/agents/business-strategy-expert/context.md +0 -180
  114. package/agents/code-review-expert/context.md +0 -365
  115. package/agents/competitive-analysis-expert/context.md +0 -239
  116. package/agents/data-modeling-expert/context.md +0 -352
  117. package/agents/database-expert/context.md +0 -250
  118. package/agents/devops-expert/context.md +0 -446
  119. package/agents/email-expert/context.md +0 -379
  120. package/agents/financial-expert/context.md +0 -213
  121. package/agents/frontend-expert/context.md +0 -364
  122. package/agents/fundraising-expert/context.md +0 -257
  123. package/agents/growth-expert/context.md +0 -249
  124. package/agents/index.js +0 -140
  125. package/agents/investor-relations-expert/context.md +0 -266
  126. package/agents/legal-expert/context.md +0 -284
  127. package/agents/marketing-expert/context.md +0 -236
  128. package/agents/monitoring-expert/context.md +0 -362
  129. package/agents/operations-expert/context.md +0 -279
  130. package/agents/partnerships-expert/context.md +0 -286
  131. package/agents/payment-expert/context.md +0 -340
  132. package/agents/performance-expert/context.md +0 -377
  133. package/agents/private-equity-expert/context.md +0 -246
  134. package/agents/railway-expert/context.md +0 -284
  135. package/agents/research-expert/context.md +0 -245
  136. package/agents/sales-expert/context.md +0 -241
  137. package/agents/security-expert/context.md +0 -343
  138. package/agents/testing-expert/context.md +0 -414
  139. package/agents/ui-ux-expert/context.md +0 -448
  140. package/agents/vercel-expert/context.md +0 -426
  141. package/skills/index.js +0 -787
  142. package/skills/patterns/README.md +0 -163
  143. package/skills/patterns/ai/agents.md +0 -281
  144. package/skills/patterns/ai/claude.md +0 -138
  145. package/skills/patterns/ai/embeddings.md +0 -150
  146. package/skills/patterns/ai/rag.md +0 -266
  147. package/skills/patterns/ai/streaming.md +0 -170
  148. package/skills/patterns/ai/structured-output.md +0 -162
  149. package/skills/patterns/ai/tools.md +0 -154
  150. package/skills/patterns/analytics/tracking.md +0 -220
  151. package/skills/patterns/api/errors.md +0 -296
  152. package/skills/patterns/api/graphql.md +0 -440
  153. package/skills/patterns/api/middleware.md +0 -279
  154. package/skills/patterns/api/openapi.md +0 -285
  155. package/skills/patterns/api/rate-limiting.md +0 -231
  156. package/skills/patterns/api/route-handler.md +0 -217
  157. package/skills/patterns/api/server-action.md +0 -249
  158. package/skills/patterns/api/versioning.md +0 -443
  159. package/skills/patterns/api/webhooks.md +0 -247
  160. package/skills/patterns/auth/clerk.md +0 -132
  161. package/skills/patterns/auth/mfa.md +0 -313
  162. package/skills/patterns/auth/nextauth.md +0 -140
  163. package/skills/patterns/auth/oauth.md +0 -237
  164. package/skills/patterns/auth/rbac.md +0 -152
  165. package/skills/patterns/auth/session-management.md +0 -367
  166. package/skills/patterns/auth/session.md +0 -120
  167. package/skills/patterns/database/audit.md +0 -177
  168. package/skills/patterns/database/migrations.md +0 -177
  169. package/skills/patterns/database/pagination.md +0 -230
  170. package/skills/patterns/database/pooling.md +0 -357
  171. package/skills/patterns/database/prisma.md +0 -180
  172. package/skills/patterns/database/relations.md +0 -187
  173. package/skills/patterns/database/seeding.md +0 -246
  174. package/skills/patterns/database/soft-delete.md +0 -153
  175. package/skills/patterns/database/transactions.md +0 -162
  176. package/skills/patterns/deployment/ci-cd.md +0 -231
  177. package/skills/patterns/deployment/docker.md +0 -188
  178. package/skills/patterns/deployment/monitoring.md +0 -387
  179. package/skills/patterns/deployment/vercel.md +0 -160
  180. package/skills/patterns/email/resend.md +0 -143
  181. package/skills/patterns/email/templates.md +0 -245
  182. package/skills/patterns/email/transactional.md +0 -503
  183. package/skills/patterns/email/verification.md +0 -176
  184. package/skills/patterns/files/download.md +0 -243
  185. package/skills/patterns/files/upload.md +0 -239
  186. package/skills/patterns/i18n/nextintl.md +0 -188
  187. package/skills/patterns/logging/structured.md +0 -292
  188. package/skills/patterns/notifications/email-queue.md +0 -248
  189. package/skills/patterns/notifications/push.md +0 -279
  190. package/skills/patterns/payments/checkout.md +0 -303
  191. package/skills/patterns/payments/invoices.md +0 -287
  192. package/skills/patterns/payments/portal.md +0 -245
  193. package/skills/patterns/payments/stripe.md +0 -272
  194. package/skills/patterns/payments/subscriptions.md +0 -300
  195. package/skills/patterns/payments/usage.md +0 -279
  196. package/skills/patterns/performance/caching.md +0 -276
  197. package/skills/patterns/performance/code-splitting.md +0 -233
  198. package/skills/patterns/performance/edge.md +0 -254
  199. package/skills/patterns/performance/isr.md +0 -266
  200. package/skills/patterns/performance/lazy-loading.md +0 -281
  201. package/skills/patterns/realtime/sse.md +0 -327
  202. package/skills/patterns/realtime/websockets.md +0 -336
  203. package/skills/patterns/search/filtering.md +0 -329
  204. package/skills/patterns/search/fulltext.md +0 -260
  205. package/skills/patterns/security/audit-logging.md +0 -444
  206. package/skills/patterns/security/csrf.md +0 -234
  207. package/skills/patterns/security/headers.md +0 -252
  208. package/skills/patterns/security/sanitization.md +0 -258
  209. package/skills/patterns/security/secrets.md +0 -261
  210. package/skills/patterns/security/validation.md +0 -268
  211. package/skills/patterns/security/xss.md +0 -229
  212. package/skills/patterns/seo/metadata.md +0 -252
  213. package/skills/patterns/state/context.md +0 -349
  214. package/skills/patterns/state/react-query.md +0 -313
  215. package/skills/patterns/state/url-state.md +0 -482
  216. package/skills/patterns/state/zustand.md +0 -262
  217. package/skills/patterns/testing/api.md +0 -259
  218. package/skills/patterns/testing/component.md +0 -233
  219. package/skills/patterns/testing/coverage.md +0 -207
  220. package/skills/patterns/testing/fixtures.md +0 -225
  221. package/skills/patterns/testing/integration.md +0 -436
  222. package/skills/patterns/testing/mocking.md +0 -177
  223. package/skills/patterns/testing/playwright.md +0 -162
  224. package/skills/patterns/testing/snapshot.md +0 -175
  225. package/skills/patterns/testing/vitest.md +0 -307
  226. package/skills/patterns/ui/accordions.md +0 -395
  227. package/skills/patterns/ui/cards.md +0 -299
  228. package/skills/patterns/ui/dropdowns.md +0 -476
  229. package/skills/patterns/ui/empty-states.md +0 -320
  230. package/skills/patterns/ui/forms.md +0 -405
  231. package/skills/patterns/ui/inputs.md +0 -319
  232. package/skills/patterns/ui/layouts.md +0 -282
  233. package/skills/patterns/ui/loading.md +0 -291
  234. package/skills/patterns/ui/modals.md +0 -338
  235. package/skills/patterns/ui/navigation.md +0 -374
  236. package/skills/patterns/ui/tables.md +0 -407
  237. package/skills/patterns/ui/toasts.md +0 -300
  238. package/skills/patterns/ui/tooltips.md +0 -396
  239. package/skills/patterns/utils/dates.md +0 -435
  240. package/skills/patterns/utils/errors.md +0 -451
  241. package/skills/patterns/utils/formatting.md +0 -345
  242. package/skills/patterns/utils/validation.md +0 -434
  243. package/templates/bootspring.config.js +0 -83
  244. package/templates/business/business-model-canvas.md +0 -246
  245. package/templates/business/business-plan.md +0 -266
  246. package/templates/business/competitive-analysis.md +0 -312
  247. package/templates/fundraising/data-room-checklist.md +0 -300
  248. package/templates/fundraising/investor-research.md +0 -243
  249. package/templates/fundraising/pitch-deck-outline.md +0 -253
  250. package/templates/legal/gdpr-checklist.md +0 -339
  251. package/templates/legal/privacy-policy.md +0 -285
  252. package/templates/legal/terms-of-service.md +0 -222
  253. package/templates/mcp.json +0 -9
@@ -0,0 +1,913 @@
1
+ /**
2
+ * Bootspring Security Command
3
+ * Run security scans and store scores
4
+ *
5
+ * @package bootspring
6
+ * @command security
7
+ */
8
+
9
+ const { execSync } = require('child_process');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const config = require('../core/config');
13
+ const utils = require('../core/utils');
14
+ const projectState = require('../core/project-state');
15
+
16
+ // ============================================================================
17
+ // Security Check Definitions
18
+ // ============================================================================
19
+
20
+ const SECURITY_CHECKS = {
21
+ dependencies: {
22
+ name: 'Dependency Audit',
23
+ description: 'Check for vulnerable npm packages',
24
+ weight: 30,
25
+ run: runDependencyAudit
26
+ },
27
+ secrets: {
28
+ name: 'Secrets Detection',
29
+ description: 'Scan for exposed secrets and API keys',
30
+ weight: 25,
31
+ run: runSecretsDetection
32
+ },
33
+ permissions: {
34
+ name: 'File Permissions',
35
+ description: 'Check for insecure file permissions',
36
+ weight: 10,
37
+ run: runPermissionsCheck
38
+ },
39
+ headers: {
40
+ name: 'Security Headers',
41
+ description: 'Verify security headers configuration',
42
+ weight: 15,
43
+ run: runHeadersCheck
44
+ },
45
+ env: {
46
+ name: 'Environment Variables',
47
+ description: 'Check .env files for best practices',
48
+ weight: 10,
49
+ run: runEnvCheck
50
+ },
51
+ gitignore: {
52
+ name: 'Gitignore Coverage',
53
+ description: 'Verify sensitive files are ignored',
54
+ weight: 10,
55
+ run: runGitignoreCheck
56
+ }
57
+ };
58
+
59
+ // Severity weights for scoring
60
+ const SEVERITY_WEIGHTS = {
61
+ critical: 40,
62
+ high: 25,
63
+ moderate: 10,
64
+ low: 5,
65
+ info: 1
66
+ };
67
+
68
+ // ============================================================================
69
+ // Security Check Implementations
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Run npm audit for dependency vulnerabilities
74
+ */
75
+ async function runDependencyAudit(projectRoot) {
76
+ const result = {
77
+ score: 100,
78
+ issues: [],
79
+ summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0 }
80
+ };
81
+
82
+ try {
83
+ // Check if package-lock.json exists
84
+ const lockFile = path.join(projectRoot, 'package-lock.json');
85
+ if (!fs.existsSync(lockFile)) {
86
+ return {
87
+ score: 100,
88
+ issues: [],
89
+ summary: result.summary,
90
+ message: 'No package-lock.json found'
91
+ };
92
+ }
93
+
94
+ const output = execSync('npm audit --json 2>/dev/null || true', {
95
+ cwd: projectRoot,
96
+ encoding: 'utf-8',
97
+ maxBuffer: 10 * 1024 * 1024
98
+ });
99
+
100
+ if (output.trim()) {
101
+ const audit = JSON.parse(output);
102
+
103
+ if (audit.metadata?.vulnerabilities) {
104
+ const vulns = audit.metadata.vulnerabilities;
105
+ result.summary = {
106
+ critical: vulns.critical || 0,
107
+ high: vulns.high || 0,
108
+ moderate: vulns.moderate || 0,
109
+ low: vulns.low || 0,
110
+ info: vulns.info || 0
111
+ };
112
+
113
+ // Calculate score based on vulnerabilities
114
+ const deductions =
115
+ (result.summary.critical * SEVERITY_WEIGHTS.critical) +
116
+ (result.summary.high * SEVERITY_WEIGHTS.high) +
117
+ (result.summary.moderate * SEVERITY_WEIGHTS.moderate) +
118
+ (result.summary.low * SEVERITY_WEIGHTS.low);
119
+
120
+ result.score = Math.max(0, 100 - deductions);
121
+
122
+ // Add top issues
123
+ if (audit.vulnerabilities) {
124
+ const vulnList = Object.entries(audit.vulnerabilities).slice(0, 5);
125
+ for (const [name, data] of vulnList) {
126
+ result.issues.push({
127
+ severity: data.severity,
128
+ package: name,
129
+ title: data.via?.[0]?.title || 'Vulnerability found',
130
+ fixAvailable: data.fixAvailable
131
+ });
132
+ }
133
+ }
134
+ }
135
+ }
136
+ } catch (error) {
137
+ result.issues.push({
138
+ severity: 'info',
139
+ title: 'Could not run npm audit',
140
+ message: error.message
141
+ });
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Scan for exposed secrets
149
+ */
150
+ async function runSecretsDetection(projectRoot) {
151
+ const result = {
152
+ score: 100,
153
+ issues: [],
154
+ summary: { critical: 0, high: 0, moderate: 0, low: 0 }
155
+ };
156
+
157
+ // Patterns to detect secrets
158
+ const secretPatterns = [
159
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/gi, name: 'API Key', severity: 'high' },
160
+ { pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: 'Password/Secret', severity: 'critical' },
161
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, name: 'Private Key', severity: 'critical' },
162
+ { pattern: /(?:aws[_-]?(?:access|secret)[_-]?(?:key|id))\s*[:=]\s*['"][A-Z0-9]{16,}['"]/gi, name: 'AWS Credentials', severity: 'critical' },
163
+ { pattern: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub Token', severity: 'high' },
164
+ { pattern: /sk-[a-zA-Z0-9]{48}/g, name: 'OpenAI API Key', severity: 'high' },
165
+ { pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/g, name: 'Slack Token', severity: 'high' },
166
+ { pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS Access Key ID', severity: 'critical' },
167
+ { pattern: /stripe[_-]?(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24}/gi, name: 'Stripe Key', severity: 'high' }
168
+ ];
169
+
170
+ // Files to scan (exclude node_modules, .git, etc.)
171
+ const excludeDirs = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
172
+ const includeExts = ['.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml', '.env.example', '.config.js'];
173
+
174
+ function scanDir(dir, depth = 0) {
175
+ if (depth > 5) return; // Limit depth
176
+
177
+ try {
178
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
179
+
180
+ for (const entry of entries) {
181
+ const fullPath = path.join(dir, entry.name);
182
+
183
+ if (entry.isDirectory()) {
184
+ if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
185
+ scanDir(fullPath, depth + 1);
186
+ }
187
+ } else if (entry.isFile()) {
188
+ const ext = path.extname(entry.name);
189
+
190
+ // Skip .env files (they're supposed to have secrets but should be gitignored)
191
+ if (entry.name === '.env' || entry.name.endsWith('.env.local')) {
192
+ continue;
193
+ }
194
+
195
+ if (includeExts.includes(ext) || entry.name.includes('.env.')) {
196
+ scanFile(fullPath);
197
+ }
198
+ }
199
+ }
200
+ } catch {
201
+ // Ignore permission errors
202
+ }
203
+ }
204
+
205
+ function scanFile(filePath) {
206
+ try {
207
+ const content = fs.readFileSync(filePath, 'utf-8');
208
+ const relativePath = path.relative(projectRoot, filePath);
209
+
210
+ for (const { pattern, name, severity } of secretPatterns) {
211
+ pattern.lastIndex = 0; // Reset regex
212
+ const matches = content.match(pattern);
213
+
214
+ if (matches) {
215
+ result.summary[severity] = (result.summary[severity] || 0) + matches.length;
216
+ result.issues.push({
217
+ severity,
218
+ title: `Potential ${name} exposed`,
219
+ file: relativePath,
220
+ count: matches.length
221
+ });
222
+ }
223
+ }
224
+ } catch {
225
+ // Ignore read errors
226
+ }
227
+ }
228
+
229
+ scanDir(projectRoot);
230
+
231
+ // Calculate score
232
+ const deductions =
233
+ (result.summary.critical * SEVERITY_WEIGHTS.critical) +
234
+ (result.summary.high * SEVERITY_WEIGHTS.high) +
235
+ (result.summary.moderate * SEVERITY_WEIGHTS.moderate) +
236
+ (result.summary.low * SEVERITY_WEIGHTS.low);
237
+
238
+ result.score = Math.max(0, 100 - deductions);
239
+
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Check file permissions
245
+ */
246
+ async function runPermissionsCheck(projectRoot) {
247
+ const result = {
248
+ score: 100,
249
+ issues: [],
250
+ summary: { critical: 0, high: 0, moderate: 0, low: 0 }
251
+ };
252
+
253
+ // Check common sensitive files
254
+ const sensitiveFiles = [
255
+ { path: '.env', recommended: '600', severity: 'high' },
256
+ { path: '.env.local', recommended: '600', severity: 'high' },
257
+ { path: 'id_rsa', recommended: '600', severity: 'critical' },
258
+ { path: '.ssh/id_rsa', recommended: '600', severity: 'critical' },
259
+ { path: 'credentials.json', recommended: '600', severity: 'high' },
260
+ { path: 'serviceAccount.json', recommended: '600', severity: 'high' }
261
+ ];
262
+
263
+ for (const { path: filePath, recommended, severity } of sensitiveFiles) {
264
+ const fullPath = path.join(projectRoot, filePath);
265
+
266
+ if (fs.existsSync(fullPath)) {
267
+ try {
268
+ const stats = fs.statSync(fullPath);
269
+ const mode = (stats.mode & parseInt('777', 8)).toString(8);
270
+
271
+ // Check if file is world-readable
272
+ if (stats.mode & parseInt('004', 8)) {
273
+ result.summary[severity]++;
274
+ result.issues.push({
275
+ severity,
276
+ title: `${filePath} is world-readable`,
277
+ current: mode,
278
+ recommended,
279
+ fix: `chmod ${recommended} ${filePath}`
280
+ });
281
+ }
282
+ } catch {
283
+ // Ignore stat errors
284
+ }
285
+ }
286
+ }
287
+
288
+ const deductions =
289
+ (result.summary.critical * 20) +
290
+ (result.summary.high * 10) +
291
+ (result.summary.moderate * 5);
292
+
293
+ result.score = Math.max(0, 100 - deductions);
294
+
295
+ return result;
296
+ }
297
+
298
+ /**
299
+ * Check security headers configuration
300
+ */
301
+ async function runHeadersCheck(projectRoot) {
302
+ const result = {
303
+ score: 100,
304
+ issues: [],
305
+ summary: { missing: 0, configured: 0 }
306
+ };
307
+
308
+ // Check for Next.js security headers
309
+ const nextConfig = path.join(projectRoot, 'next.config.js');
310
+ const nextConfigMjs = path.join(projectRoot, 'next.config.mjs');
311
+ const vercelJson = path.join(projectRoot, 'vercel.json');
312
+
313
+ const requiredHeaders = [
314
+ 'X-Content-Type-Options',
315
+ 'X-Frame-Options',
316
+ 'X-XSS-Protection',
317
+ 'Strict-Transport-Security',
318
+ 'Content-Security-Policy'
319
+ ];
320
+
321
+ let hasHeaderConfig = false;
322
+
323
+ // Check next.config.js
324
+ for (const configPath of [nextConfig, nextConfigMjs]) {
325
+ if (fs.existsSync(configPath)) {
326
+ const content = fs.readFileSync(configPath, 'utf-8');
327
+ if (content.includes('headers') || content.includes('securityHeaders')) {
328
+ hasHeaderConfig = true;
329
+
330
+ // Check which headers are configured
331
+ for (const header of requiredHeaders) {
332
+ if (content.includes(header)) {
333
+ result.summary.configured++;
334
+ } else {
335
+ result.summary.missing++;
336
+ result.issues.push({
337
+ severity: 'moderate',
338
+ title: `Missing ${header}`,
339
+ file: path.basename(configPath)
340
+ });
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ // Check vercel.json
348
+ if (fs.existsSync(vercelJson)) {
349
+ const content = fs.readFileSync(vercelJson, 'utf-8');
350
+ if (content.includes('headers')) {
351
+ hasHeaderConfig = true;
352
+ }
353
+ }
354
+
355
+ if (!hasHeaderConfig) {
356
+ result.score = 50;
357
+ result.issues.push({
358
+ severity: 'moderate',
359
+ title: 'No security headers configured',
360
+ recommendation: 'Add security headers to next.config.js or vercel.json'
361
+ });
362
+ } else {
363
+ result.score = Math.max(0, 100 - (result.summary.missing * 10));
364
+ }
365
+
366
+ return result;
367
+ }
368
+
369
+ /**
370
+ * Check environment variable best practices
371
+ */
372
+ async function runEnvCheck(projectRoot) {
373
+ const result = {
374
+ score: 100,
375
+ issues: [],
376
+ summary: { warnings: 0, good: 0 }
377
+ };
378
+
379
+ const envExample = path.join(projectRoot, '.env.example');
380
+ const envLocal = path.join(projectRoot, '.env.local');
381
+ const envFile = path.join(projectRoot, '.env');
382
+
383
+ // Check if .env.example exists
384
+ if (!fs.existsSync(envExample)) {
385
+ result.issues.push({
386
+ severity: 'low',
387
+ title: 'Missing .env.example',
388
+ recommendation: 'Create .env.example to document required environment variables'
389
+ });
390
+ result.summary.warnings++;
391
+ } else {
392
+ result.summary.good++;
393
+ }
394
+
395
+ // Check if .env is in .gitignore
396
+ const gitignore = path.join(projectRoot, '.gitignore');
397
+ if (fs.existsSync(gitignore)) {
398
+ const content = fs.readFileSync(gitignore, 'utf-8');
399
+
400
+ if (!content.includes('.env') || content.includes('.env.example')) {
401
+ if (fs.existsSync(envFile) || fs.existsSync(envLocal)) {
402
+ result.issues.push({
403
+ severity: 'high',
404
+ title: '.env files may not be properly gitignored',
405
+ recommendation: 'Ensure .env and .env.local are in .gitignore'
406
+ });
407
+ result.summary.warnings++;
408
+ }
409
+ } else {
410
+ result.summary.good++;
411
+ }
412
+ }
413
+
414
+ // Check for hardcoded values that should be env vars
415
+ const configFiles = ['next.config.js', 'next.config.mjs', 'config.js'];
416
+ const hardcodedPatterns = [
417
+ /https?:\/\/[a-z0-9-]+\.(supabase|neon|planetscale)\.co/gi,
418
+ /mongodb\+srv:\/\//gi,
419
+ /postgres:\/\//gi
420
+ ];
421
+
422
+ for (const configFile of configFiles) {
423
+ const filePath = path.join(projectRoot, configFile);
424
+ if (fs.existsSync(filePath)) {
425
+ const content = fs.readFileSync(filePath, 'utf-8');
426
+
427
+ for (const pattern of hardcodedPatterns) {
428
+ pattern.lastIndex = 0;
429
+ if (pattern.test(content) && !content.includes('process.env')) {
430
+ result.issues.push({
431
+ severity: 'moderate',
432
+ title: `Potential hardcoded URL in ${configFile}`,
433
+ recommendation: 'Use environment variables for database/service URLs'
434
+ });
435
+ result.summary.warnings++;
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ result.score = Math.max(0, 100 - (result.summary.warnings * 15));
442
+
443
+ return result;
444
+ }
445
+
446
+ /**
447
+ * Check gitignore for sensitive file coverage
448
+ */
449
+ async function runGitignoreCheck(projectRoot) {
450
+ const result = {
451
+ score: 100,
452
+ issues: [],
453
+ summary: { missing: 0, covered: 0 }
454
+ };
455
+
456
+ const gitignore = path.join(projectRoot, '.gitignore');
457
+
458
+ if (!fs.existsSync(gitignore)) {
459
+ return {
460
+ score: 50,
461
+ issues: [{ severity: 'high', title: 'No .gitignore file found' }],
462
+ summary: { missing: 1, covered: 0 }
463
+ };
464
+ }
465
+
466
+ const content = fs.readFileSync(gitignore, 'utf-8');
467
+
468
+ // Sensitive patterns that should be gitignored
469
+ const requiredPatterns = [
470
+ { pattern: '.env', severity: 'high', name: 'Environment files' },
471
+ { pattern: '.env.local', severity: 'high', name: 'Local env files' },
472
+ { pattern: 'node_modules', severity: 'moderate', name: 'Node modules' },
473
+ { pattern: '*.pem', severity: 'critical', name: 'PEM certificates' },
474
+ { pattern: '*.key', severity: 'critical', name: 'Key files' },
475
+ { pattern: '.DS_Store', severity: 'low', name: 'macOS files' },
476
+ { pattern: 'coverage', severity: 'low', name: 'Coverage reports' }
477
+ ];
478
+
479
+ for (const { pattern, severity, name } of requiredPatterns) {
480
+ if (content.includes(pattern)) {
481
+ result.summary.covered++;
482
+ } else {
483
+ // Check if file exists before warning
484
+ const patternPath = path.join(projectRoot, pattern.replace('*', ''));
485
+ if (fs.existsSync(patternPath) || pattern.includes('*')) {
486
+ result.summary.missing++;
487
+ result.issues.push({
488
+ severity,
489
+ title: `${name} (${pattern}) not in .gitignore`,
490
+ recommendation: `Add ${pattern} to .gitignore`
491
+ });
492
+ }
493
+ }
494
+ }
495
+
496
+ const deductions =
497
+ (result.issues.filter(i => i.severity === 'critical').length * 20) +
498
+ (result.issues.filter(i => i.severity === 'high').length * 15) +
499
+ (result.issues.filter(i => i.severity === 'moderate').length * 5);
500
+
501
+ result.score = Math.max(0, 100 - deductions);
502
+
503
+ return result;
504
+ }
505
+
506
+ // ============================================================================
507
+ // Main Security Functions
508
+ // ============================================================================
509
+
510
+ /**
511
+ * Run all security checks and calculate score
512
+ */
513
+ async function runFullScan(projectRoot, options = {}) {
514
+ const results = {
515
+ timestamp: new Date().toISOString(),
516
+ overallScore: 0,
517
+ checks: {},
518
+ summary: {
519
+ critical: 0,
520
+ high: 0,
521
+ moderate: 0,
522
+ low: 0,
523
+ info: 0
524
+ },
525
+ recommendations: []
526
+ };
527
+
528
+ console.log(`
529
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Scan${utils.COLORS.reset}
530
+ ${utils.COLORS.dim}Running comprehensive security checks...${utils.COLORS.reset}
531
+ `);
532
+
533
+ let totalWeight = 0;
534
+ let weightedScore = 0;
535
+
536
+ for (const [id, check] of Object.entries(SECURITY_CHECKS)) {
537
+ if (options.skip && options.skip.includes(id)) {
538
+ console.log(`${utils.COLORS.dim}○${utils.COLORS.reset} ${check.name}: skipped`);
539
+ continue;
540
+ }
541
+
542
+ const spinner = utils.createSpinner(`${check.name}: ${check.description}`).start();
543
+
544
+ try {
545
+ const result = await check.run(projectRoot);
546
+ results.checks[id] = result;
547
+
548
+ // Update summary
549
+ if (result.summary) {
550
+ for (const [severity, count] of Object.entries(result.summary)) {
551
+ if (results.summary[severity] !== undefined) {
552
+ results.summary[severity] += count;
553
+ }
554
+ }
555
+ }
556
+
557
+ // Calculate weighted score
558
+ totalWeight += check.weight;
559
+ weightedScore += result.score * check.weight;
560
+
561
+ // Show result
562
+ if (result.score >= 90) {
563
+ spinner.succeed(`${check.name}: ${utils.COLORS.green}${result.score}%${utils.COLORS.reset}`);
564
+ } else if (result.score >= 70) {
565
+ spinner.warn(`${check.name}: ${utils.COLORS.yellow}${result.score}%${utils.COLORS.reset}`);
566
+ } else {
567
+ spinner.fail(`${check.name}: ${utils.COLORS.red}${result.score}%${utils.COLORS.reset}`);
568
+ }
569
+
570
+ // Show critical issues
571
+ if (!options.quiet && result.issues.length > 0) {
572
+ const criticalIssues = result.issues.filter(i =>
573
+ i.severity === 'critical' || i.severity === 'high'
574
+ );
575
+ for (const issue of criticalIssues.slice(0, 3)) {
576
+ const color = issue.severity === 'critical' ? utils.COLORS.red : utils.COLORS.yellow;
577
+ console.log(` ${color}└ ${issue.title}${utils.COLORS.reset}`);
578
+ }
579
+ }
580
+ } catch (error) {
581
+ spinner.fail(`${check.name}: error`);
582
+ results.checks[id] = { score: 0, error: error.message };
583
+ }
584
+ }
585
+
586
+ // Calculate overall score
587
+ results.overallScore = totalWeight > 0
588
+ ? Math.round(weightedScore / totalWeight)
589
+ : 0;
590
+
591
+ // Generate recommendations
592
+ results.recommendations = generateRecommendations(results);
593
+
594
+ return results;
595
+ }
596
+
597
+ /**
598
+ * Generate recommendations based on scan results
599
+ */
600
+ function generateRecommendations(results) {
601
+ const recommendations = [];
602
+
603
+ // Priority: critical issues first
604
+ if (results.summary.critical > 0) {
605
+ recommendations.push({
606
+ priority: 'critical',
607
+ title: `Fix ${results.summary.critical} critical security issue(s)`,
608
+ action: 'Review critical findings and address immediately'
609
+ });
610
+ }
611
+
612
+ if (results.summary.high > 0) {
613
+ recommendations.push({
614
+ priority: 'high',
615
+ title: `Address ${results.summary.high} high priority issue(s)`,
616
+ action: 'Run `npm audit fix` for dependency issues'
617
+ });
618
+ }
619
+
620
+ // Check-specific recommendations
621
+ if (results.checks.secrets?.score < 100) {
622
+ recommendations.push({
623
+ priority: 'high',
624
+ title: 'Remove exposed secrets from codebase',
625
+ action: 'Move secrets to .env files and rotate compromised credentials'
626
+ });
627
+ }
628
+
629
+ if (results.checks.headers?.score < 70) {
630
+ recommendations.push({
631
+ priority: 'moderate',
632
+ title: 'Configure security headers',
633
+ action: 'Add security headers to next.config.js or vercel.json'
634
+ });
635
+ }
636
+
637
+ if (results.checks.gitignore?.score < 90) {
638
+ recommendations.push({
639
+ priority: 'moderate',
640
+ title: 'Update .gitignore',
641
+ action: 'Ensure sensitive files are excluded from version control'
642
+ });
643
+ }
644
+
645
+ return recommendations;
646
+ }
647
+
648
+ /**
649
+ * Store security score in project state
650
+ */
651
+ function storeSecurityScore(projectRoot, results) {
652
+ const state = projectState.getOrCreateState(projectRoot);
653
+
654
+ // Add security section if not exists
655
+ if (!state.security) {
656
+ state.security = {
657
+ score: 0,
658
+ lastScan: null,
659
+ history: []
660
+ };
661
+ }
662
+
663
+ // Update security data
664
+ state.security = {
665
+ score: results.overallScore,
666
+ lastScan: results.timestamp,
667
+ summary: results.summary,
668
+ checks: Object.fromEntries(
669
+ Object.entries(results.checks).map(([id, check]) => [
670
+ id,
671
+ { score: check.score, issues: check.issues?.length || 0 }
672
+ ])
673
+ ),
674
+ history: [
675
+ { score: results.overallScore, date: results.timestamp },
676
+ ...(state.security.history || []).slice(0, 9) // Keep last 10
677
+ ]
678
+ };
679
+
680
+ // Update health breakdown if exists
681
+ if (state.health?.breakdown) {
682
+ state.health.breakdown.security = Math.round(results.overallScore * 0.25); // 25% of overall health
683
+ }
684
+
685
+ projectState.saveState(projectRoot, state);
686
+
687
+ return state.security;
688
+ }
689
+
690
+ /**
691
+ * Show security status
692
+ */
693
+ function showStatus(projectRoot) {
694
+ const state = projectState.loadState(projectRoot);
695
+
696
+ console.log(`
697
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Status${utils.COLORS.reset}
698
+ `);
699
+
700
+ if (!state?.security?.lastScan) {
701
+ console.log(`${utils.COLORS.dim}No security scan has been run yet.${utils.COLORS.reset}`);
702
+ console.log(`${utils.COLORS.dim}Run 'bootspring security scan' to perform a scan.${utils.COLORS.reset}`);
703
+ return;
704
+ }
705
+
706
+ const sec = state.security;
707
+ const scoreColor = sec.score >= 90 ? utils.COLORS.green :
708
+ sec.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
709
+
710
+ console.log(`${utils.COLORS.bold}Overall Score${utils.COLORS.reset}`);
711
+ console.log(` ${scoreColor}${utils.COLORS.bold}${sec.score}%${utils.COLORS.reset}`);
712
+ console.log(` ${utils.COLORS.dim}Last scan: ${new Date(sec.lastScan).toLocaleString()}${utils.COLORS.reset}`);
713
+ console.log();
714
+
715
+ if (sec.summary) {
716
+ console.log(`${utils.COLORS.bold}Issues Found${utils.COLORS.reset}`);
717
+ console.log(` ${utils.COLORS.red}Critical: ${sec.summary.critical || 0}${utils.COLORS.reset}`);
718
+ console.log(` ${utils.COLORS.yellow}High: ${sec.summary.high || 0}${utils.COLORS.reset}`);
719
+ console.log(` ${utils.COLORS.cyan}Moderate: ${sec.summary.moderate || 0}${utils.COLORS.reset}`);
720
+ console.log(` ${utils.COLORS.dim}Low: ${sec.summary.low || 0}${utils.COLORS.reset}`);
721
+ console.log();
722
+ }
723
+
724
+ if (sec.checks) {
725
+ console.log(`${utils.COLORS.bold}Check Scores${utils.COLORS.reset}`);
726
+ for (const [id, check] of Object.entries(sec.checks)) {
727
+ const name = SECURITY_CHECKS[id]?.name || id;
728
+ const checkColor = check.score >= 90 ? utils.COLORS.green :
729
+ check.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
730
+ const bar = utils.createProgressBar ?
731
+ utils.createProgressBar(check.score, 100, 20) :
732
+ `${check.score}%`;
733
+ console.log(` ${name.padEnd(20)} ${checkColor}${bar}${utils.COLORS.reset}`);
734
+ }
735
+ console.log();
736
+ }
737
+
738
+ if (sec.history && sec.history.length > 1) {
739
+ console.log(`${utils.COLORS.bold}Score History${utils.COLORS.reset}`);
740
+ const trend = sec.history[0].score - sec.history[1].score;
741
+ const trendIcon = trend > 0 ? '↑' : trend < 0 ? '↓' : '→';
742
+ const trendColor = trend > 0 ? utils.COLORS.green : trend < 0 ? utils.COLORS.red : utils.COLORS.dim;
743
+ console.log(` ${trendColor}${trendIcon} ${Math.abs(trend)} points from last scan${utils.COLORS.reset}`);
744
+ console.log(` ${utils.COLORS.dim}${sec.history.slice(0, 5).map(h => h.score).join(' → ')}${utils.COLORS.reset}`);
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Show detailed findings
750
+ */
751
+ function showFindings(projectRoot) {
752
+ const state = projectState.loadState(projectRoot);
753
+
754
+ if (!state?.security?.lastScan) {
755
+ console.log(`${utils.COLORS.dim}No security scan results. Run 'bootspring security scan' first.${utils.COLORS.reset}`);
756
+ return;
757
+ }
758
+
759
+ // Re-run scan to get detailed findings
760
+ console.log(`${utils.COLORS.dim}Re-scanning for detailed findings...${utils.COLORS.reset}\n`);
761
+ }
762
+
763
+ /**
764
+ * Quick security check
765
+ */
766
+ async function runQuickCheck(projectRoot) {
767
+ console.log(`
768
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Quick Security Check${utils.COLORS.reset}
769
+ `);
770
+
771
+ // Just run dependency audit and secrets detection
772
+ const results = {
773
+ dependencies: await runDependencyAudit(projectRoot),
774
+ secrets: await runSecretsDetection(projectRoot)
775
+ };
776
+
777
+ const issues =
778
+ (results.dependencies.summary?.critical || 0) +
779
+ (results.dependencies.summary?.high || 0) +
780
+ (results.secrets.summary?.critical || 0) +
781
+ (results.secrets.summary?.high || 0);
782
+
783
+ if (issues === 0) {
784
+ utils.print.success('No critical security issues found');
785
+ } else {
786
+ utils.print.warn(`Found ${issues} high/critical security issue(s)`);
787
+ console.log(`${utils.COLORS.dim}Run 'bootspring security scan' for full details${utils.COLORS.reset}`);
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Show help
793
+ */
794
+ function showHelp() {
795
+ console.log(`
796
+ ${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Bootspring Security${utils.COLORS.reset}
797
+ ${utils.COLORS.dim}Security scanning and score tracking${utils.COLORS.reset}
798
+
799
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
800
+ bootspring security <command> [options]
801
+
802
+ ${utils.COLORS.bold}Commands:${utils.COLORS.reset}
803
+ ${utils.COLORS.cyan}scan${utils.COLORS.reset} Run full security scan and store score
804
+ ${utils.COLORS.cyan}quick${utils.COLORS.reset} Quick check (deps + secrets only)
805
+ ${utils.COLORS.cyan}status${utils.COLORS.reset} Show current security score and history
806
+ ${utils.COLORS.cyan}findings${utils.COLORS.reset} Show detailed findings from last scan
807
+
808
+ ${utils.COLORS.bold}Options:${utils.COLORS.reset}
809
+ --skip <check> Skip specific check (deps, secrets, headers, etc.)
810
+ --quiet Suppress detailed output
811
+ --json Output results as JSON
812
+
813
+ ${utils.COLORS.bold}Checks Performed:${utils.COLORS.reset}
814
+ ${utils.COLORS.dim}dependencies${utils.COLORS.reset} npm audit for vulnerable packages
815
+ ${utils.COLORS.dim}secrets${utils.COLORS.reset} Scan for exposed API keys and passwords
816
+ ${utils.COLORS.dim}permissions${utils.COLORS.reset} Check file permissions on sensitive files
817
+ ${utils.COLORS.dim}headers${utils.COLORS.reset} Verify security headers configuration
818
+ ${utils.COLORS.dim}env${utils.COLORS.reset} Check .env best practices
819
+ ${utils.COLORS.dim}gitignore${utils.COLORS.reset} Verify sensitive files are ignored
820
+
821
+ ${utils.COLORS.bold}Examples:${utils.COLORS.reset}
822
+ bootspring security scan
823
+ bootspring security scan --skip headers
824
+ bootspring security quick
825
+ bootspring security status
826
+ `);
827
+ }
828
+
829
+ /**
830
+ * Main run function
831
+ */
832
+ async function run(args) {
833
+ const parsedArgs = utils.parseArgs(args);
834
+ const subcommand = parsedArgs._[0];
835
+ const cfg = config.load();
836
+ const projectRoot = cfg._projectRoot;
837
+
838
+ switch (subcommand) {
839
+ case 'scan':
840
+ case 'full': {
841
+ const results = await runFullScan(projectRoot, {
842
+ skip: parsedArgs.skip ? [parsedArgs.skip] : [],
843
+ quiet: parsedArgs.quiet
844
+ });
845
+
846
+ // Store score
847
+ const stored = storeSecurityScore(projectRoot, results);
848
+
849
+ // Show summary
850
+ console.log();
851
+ const scoreColor = results.overallScore >= 90 ? utils.COLORS.green :
852
+ results.overallScore >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
853
+
854
+ console.log(`${utils.COLORS.bold}Security Score: ${scoreColor}${results.overallScore}%${utils.COLORS.reset}`);
855
+
856
+ if (results.recommendations.length > 0) {
857
+ console.log(`\n${utils.COLORS.bold}Top Recommendations:${utils.COLORS.reset}`);
858
+ for (const rec of results.recommendations.slice(0, 3)) {
859
+ const icon = rec.priority === 'critical' ? '🔴' :
860
+ rec.priority === 'high' ? '🟡' : '🔵';
861
+ console.log(` ${icon} ${rec.title}`);
862
+ }
863
+ }
864
+
865
+ console.log(`\n${utils.COLORS.dim}Score saved to planning/PROJECT_STATE.json${utils.COLORS.reset}`);
866
+
867
+ if (parsedArgs.json) {
868
+ console.log(JSON.stringify(results, null, 2));
869
+ }
870
+
871
+ // Exit with error if critical issues
872
+ if (results.summary.critical > 0) {
873
+ process.exitCode = 1;
874
+ }
875
+ break;
876
+ }
877
+
878
+ case 'quick':
879
+ case 'check':
880
+ await runQuickCheck(projectRoot);
881
+ break;
882
+
883
+ case 'status':
884
+ showStatus(projectRoot);
885
+ break;
886
+
887
+ case 'findings':
888
+ case 'details':
889
+ showFindings(projectRoot);
890
+ break;
891
+
892
+ case 'help':
893
+ case '-h':
894
+ case '--help':
895
+ showHelp();
896
+ break;
897
+
898
+ default:
899
+ if (!subcommand) {
900
+ showHelp();
901
+ } else {
902
+ utils.print.error(`Unknown command: ${subcommand}`);
903
+ showHelp();
904
+ }
905
+ }
906
+ }
907
+
908
+ module.exports = {
909
+ run,
910
+ runFullScan,
911
+ storeSecurityScore,
912
+ SECURITY_CHECKS
913
+ };