@girardmedia/bootspring 2.0.21 → 2.0.23
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/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Security Command
|
|
3
|
+
* Run security scans and store scores
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @command security
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
// Interfaces
|
|
14
|
+
interface ConfigModule {
|
|
15
|
+
load: () => { _projectRoot: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UtilsModule {
|
|
19
|
+
COLORS: {
|
|
20
|
+
cyan: string;
|
|
21
|
+
bold: string;
|
|
22
|
+
reset: string;
|
|
23
|
+
dim: string;
|
|
24
|
+
green: string;
|
|
25
|
+
yellow: string;
|
|
26
|
+
red: string;
|
|
27
|
+
};
|
|
28
|
+
createSpinner: (text: string) => {
|
|
29
|
+
start: () => { succeed: (text: string) => void; fail: (text: string) => void; warn: (text: string) => void };
|
|
30
|
+
succeed: (text: string) => void;
|
|
31
|
+
fail: (text: string) => void;
|
|
32
|
+
warn: (text: string) => void;
|
|
33
|
+
};
|
|
34
|
+
createProgressBar?: (value: number, max: number, width: number) => string;
|
|
35
|
+
parseArgs: (args: string[]) => ParsedArgs;
|
|
36
|
+
print: {
|
|
37
|
+
error: (msg: string) => void;
|
|
38
|
+
success: (msg: string) => void;
|
|
39
|
+
warn: (msg: string) => void;
|
|
40
|
+
dim: (msg: string) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ProjectStateModule {
|
|
45
|
+
getOrCreateState: (projectRoot: string) => ProjectState;
|
|
46
|
+
loadState: (projectRoot: string) => ProjectState | null;
|
|
47
|
+
saveState: (projectRoot: string, state: ProjectState) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ParsedArgs {
|
|
51
|
+
_: string[];
|
|
52
|
+
skip?: string;
|
|
53
|
+
quiet?: boolean;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
[key: string]: string | boolean | string[] | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SeveritySummary {
|
|
59
|
+
critical: number;
|
|
60
|
+
high: number;
|
|
61
|
+
moderate: number;
|
|
62
|
+
low: number;
|
|
63
|
+
info?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CheckResult {
|
|
67
|
+
score: number;
|
|
68
|
+
issues: Issue[];
|
|
69
|
+
summary: SeveritySummary | HeadersSummary | EnvSummary | GitignoreSummary;
|
|
70
|
+
message?: string | undefined;
|
|
71
|
+
error?: string | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface HeadersSummary {
|
|
75
|
+
missing: number;
|
|
76
|
+
configured: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface EnvSummary {
|
|
80
|
+
warnings: number;
|
|
81
|
+
good: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface GitignoreSummary {
|
|
85
|
+
missing: number;
|
|
86
|
+
covered: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Issue {
|
|
90
|
+
severity: string;
|
|
91
|
+
title: string;
|
|
92
|
+
package?: string | undefined;
|
|
93
|
+
fixAvailable?: boolean | undefined;
|
|
94
|
+
message?: string | undefined;
|
|
95
|
+
file?: string | undefined;
|
|
96
|
+
count?: number | undefined;
|
|
97
|
+
current?: string | undefined;
|
|
98
|
+
recommended?: string | undefined;
|
|
99
|
+
fix?: string | undefined;
|
|
100
|
+
recommendation?: string | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface SecurityCheck {
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
weight: number;
|
|
107
|
+
run: (projectRoot: string) => Promise<CheckResult>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ScanResults {
|
|
111
|
+
timestamp: string;
|
|
112
|
+
overallScore: number;
|
|
113
|
+
checks: Record<string, CheckResult>;
|
|
114
|
+
summary: SeveritySummary;
|
|
115
|
+
recommendations: Recommendation[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface Recommendation {
|
|
119
|
+
priority: string;
|
|
120
|
+
title: string;
|
|
121
|
+
action: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface ProjectState {
|
|
125
|
+
security?: SecurityState | undefined;
|
|
126
|
+
health?: { breakdown?: { security?: number | undefined } | undefined } | undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface SecurityState {
|
|
130
|
+
score: number;
|
|
131
|
+
lastScan: string | null;
|
|
132
|
+
history: Array<{ score: number; date: string }>;
|
|
133
|
+
summary?: SeveritySummary | undefined;
|
|
134
|
+
checks?: Record<string, { score: number; issues: number }> | undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ScanOptions {
|
|
138
|
+
skip?: string[] | undefined;
|
|
139
|
+
quiet?: boolean | undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface AuditMetadata {
|
|
143
|
+
vulnerabilities?: {
|
|
144
|
+
critical?: number | undefined;
|
|
145
|
+
high?: number | undefined;
|
|
146
|
+
moderate?: number | undefined;
|
|
147
|
+
low?: number | undefined;
|
|
148
|
+
info?: number | undefined;
|
|
149
|
+
} | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface VulnerabilityData {
|
|
153
|
+
severity: string;
|
|
154
|
+
via?: Array<{ title?: string | undefined }> | undefined;
|
|
155
|
+
fixAvailable?: boolean | undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface AuditOutput {
|
|
159
|
+
metadata?: AuditMetadata | undefined;
|
|
160
|
+
vulnerabilities?: Record<string, VulnerabilityData> | undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Lazy load modules
|
|
164
|
+
const config = require('../core/config') as ConfigModule;
|
|
165
|
+
const utils = require('../core/utils') as UtilsModule;
|
|
166
|
+
const projectState = require('../core/project-state') as ProjectStateModule;
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Security Check Definitions
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
const SECURITY_CHECKS: Record<string, SecurityCheck> = {
|
|
173
|
+
dependencies: {
|
|
174
|
+
name: 'Dependency Audit',
|
|
175
|
+
description: 'Check for vulnerable npm packages',
|
|
176
|
+
weight: 30,
|
|
177
|
+
run: runDependencyAudit
|
|
178
|
+
},
|
|
179
|
+
secrets: {
|
|
180
|
+
name: 'Secrets Detection',
|
|
181
|
+
description: 'Scan for exposed secrets and API keys',
|
|
182
|
+
weight: 25,
|
|
183
|
+
run: runSecretsDetection
|
|
184
|
+
},
|
|
185
|
+
permissions: {
|
|
186
|
+
name: 'File Permissions',
|
|
187
|
+
description: 'Check for insecure file permissions',
|
|
188
|
+
weight: 10,
|
|
189
|
+
run: runPermissionsCheck
|
|
190
|
+
},
|
|
191
|
+
headers: {
|
|
192
|
+
name: 'Security Headers',
|
|
193
|
+
description: 'Verify security headers configuration',
|
|
194
|
+
weight: 15,
|
|
195
|
+
run: runHeadersCheck
|
|
196
|
+
},
|
|
197
|
+
env: {
|
|
198
|
+
name: 'Environment Variables',
|
|
199
|
+
description: 'Check .env files for best practices',
|
|
200
|
+
weight: 10,
|
|
201
|
+
run: runEnvCheck
|
|
202
|
+
},
|
|
203
|
+
gitignore: {
|
|
204
|
+
name: 'Gitignore Coverage',
|
|
205
|
+
description: 'Verify sensitive files are ignored',
|
|
206
|
+
weight: 10,
|
|
207
|
+
run: runGitignoreCheck
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Severity weights for scoring
|
|
212
|
+
const SEVERITY_WEIGHTS: Record<string, number> = {
|
|
213
|
+
critical: 40,
|
|
214
|
+
high: 25,
|
|
215
|
+
moderate: 10,
|
|
216
|
+
low: 5,
|
|
217
|
+
info: 1
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Security Check Implementations
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run npm audit for dependency vulnerabilities
|
|
226
|
+
*/
|
|
227
|
+
async function runDependencyAudit(projectRoot: string): Promise<CheckResult> {
|
|
228
|
+
const result: CheckResult = {
|
|
229
|
+
score: 100,
|
|
230
|
+
issues: [],
|
|
231
|
+
summary: { critical: 0, high: 0, moderate: 0, low: 0, info: 0 }
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Check if package-lock.json exists
|
|
236
|
+
const lockFile = path.join(projectRoot, 'package-lock.json');
|
|
237
|
+
if (!fs.existsSync(lockFile)) {
|
|
238
|
+
return {
|
|
239
|
+
score: 100,
|
|
240
|
+
issues: [],
|
|
241
|
+
summary: result.summary,
|
|
242
|
+
message: 'No package-lock.json found'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const output = execSync('npm audit --json 2>/dev/null || true', {
|
|
247
|
+
cwd: projectRoot,
|
|
248
|
+
encoding: 'utf-8',
|
|
249
|
+
maxBuffer: 10 * 1024 * 1024
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (output.trim()) {
|
|
253
|
+
const audit = JSON.parse(output) as AuditOutput;
|
|
254
|
+
|
|
255
|
+
if (audit.metadata?.vulnerabilities) {
|
|
256
|
+
const vulns = audit.metadata.vulnerabilities;
|
|
257
|
+
const summary = result.summary as SeveritySummary;
|
|
258
|
+
summary.critical = vulns.critical || 0;
|
|
259
|
+
summary.high = vulns.high || 0;
|
|
260
|
+
summary.moderate = vulns.moderate || 0;
|
|
261
|
+
summary.low = vulns.low || 0;
|
|
262
|
+
summary.info = vulns.info || 0;
|
|
263
|
+
|
|
264
|
+
// Calculate score based on vulnerabilities
|
|
265
|
+
const deductions =
|
|
266
|
+
(summary.critical * (SEVERITY_WEIGHTS.critical || 0)) +
|
|
267
|
+
(summary.high * (SEVERITY_WEIGHTS.high || 0)) +
|
|
268
|
+
(summary.moderate * (SEVERITY_WEIGHTS.moderate || 0)) +
|
|
269
|
+
(summary.low * (SEVERITY_WEIGHTS.low || 0));
|
|
270
|
+
|
|
271
|
+
result.score = Math.max(0, 100 - deductions);
|
|
272
|
+
|
|
273
|
+
// Add top issues
|
|
274
|
+
if (audit.vulnerabilities) {
|
|
275
|
+
const vulnList = Object.entries(audit.vulnerabilities).slice(0, 5);
|
|
276
|
+
for (const [name, data] of vulnList) {
|
|
277
|
+
result.issues.push({
|
|
278
|
+
severity: data.severity,
|
|
279
|
+
package: name,
|
|
280
|
+
title: data.via?.[0]?.title || 'Vulnerability found',
|
|
281
|
+
fixAvailable: data.fixAvailable
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
result.issues.push({
|
|
289
|
+
severity: 'info',
|
|
290
|
+
title: 'Could not run npm audit',
|
|
291
|
+
message: (error as Error).message
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Scan for exposed secrets
|
|
300
|
+
*/
|
|
301
|
+
async function runSecretsDetection(projectRoot: string): Promise<CheckResult> {
|
|
302
|
+
const result: CheckResult = {
|
|
303
|
+
score: 100,
|
|
304
|
+
issues: [],
|
|
305
|
+
summary: { critical: 0, high: 0, moderate: 0, low: 0 }
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Patterns to detect secrets
|
|
309
|
+
const secretPatterns: Array<{ pattern: RegExp; name: string; severity: string }> = [
|
|
310
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/gi, name: 'API Key', severity: 'high' },
|
|
311
|
+
{ pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: 'Password/Secret', severity: 'critical' },
|
|
312
|
+
{ pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, name: 'Private Key', severity: 'critical' },
|
|
313
|
+
{ pattern: /(?:aws[_-]?(?:access|secret)[_-]?(?:key|id))\s*[:=]\s*['"][A-Z0-9]{16,}['"]/gi, name: 'AWS Credentials', severity: 'critical' },
|
|
314
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub Token', severity: 'high' },
|
|
315
|
+
{ pattern: /sk-[a-zA-Z0-9]{48}/g, name: 'OpenAI API Key', severity: 'high' },
|
|
316
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/g, name: 'Slack Token', severity: 'high' },
|
|
317
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, name: 'AWS Access Key ID', severity: 'critical' },
|
|
318
|
+
{ pattern: /stripe[_-]?(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24}/gi, name: 'Stripe Key', severity: 'high' }
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// Files to scan (exclude node_modules, .git, etc.)
|
|
322
|
+
const excludeDirs = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
|
|
323
|
+
const includeExts = ['.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml', '.env.example', '.config.js'];
|
|
324
|
+
|
|
325
|
+
function scanDir(dir: string, depth = 0): void {
|
|
326
|
+
if (depth > 5) return; // Limit depth
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
330
|
+
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const fullPath = path.join(dir, entry.name);
|
|
333
|
+
|
|
334
|
+
if (entry.isDirectory()) {
|
|
335
|
+
if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
|
336
|
+
scanDir(fullPath, depth + 1);
|
|
337
|
+
}
|
|
338
|
+
} else if (entry.isFile()) {
|
|
339
|
+
const ext = path.extname(entry.name);
|
|
340
|
+
|
|
341
|
+
// Skip .env files (they're supposed to have secrets but should be gitignored)
|
|
342
|
+
if (entry.name === '.env' || entry.name.endsWith('.env.local')) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (includeExts.includes(ext) || entry.name.includes('.env.')) {
|
|
347
|
+
scanFile(fullPath);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// Ignore permission errors
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function scanFile(filePath: string): void {
|
|
357
|
+
try {
|
|
358
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
359
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
360
|
+
|
|
361
|
+
for (const { pattern, name, severity } of secretPatterns) {
|
|
362
|
+
pattern.lastIndex = 0; // Reset regex
|
|
363
|
+
const matches = content.match(pattern);
|
|
364
|
+
|
|
365
|
+
if (matches) {
|
|
366
|
+
const summary = result.summary as SeveritySummary;
|
|
367
|
+
const currentCount = (summary[severity as keyof SeveritySummary] as number | undefined) || 0;
|
|
368
|
+
(summary as unknown as Record<string, number>)[severity] = currentCount + matches.length;
|
|
369
|
+
result.issues.push({
|
|
370
|
+
severity,
|
|
371
|
+
title: `Potential ${name} exposed`,
|
|
372
|
+
file: relativePath,
|
|
373
|
+
count: matches.length
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// Ignore read errors
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
scanDir(projectRoot);
|
|
383
|
+
|
|
384
|
+
// Calculate score
|
|
385
|
+
const summary = result.summary as SeveritySummary;
|
|
386
|
+
const deductions =
|
|
387
|
+
(summary.critical * (SEVERITY_WEIGHTS.critical || 0)) +
|
|
388
|
+
(summary.high * (SEVERITY_WEIGHTS.high || 0)) +
|
|
389
|
+
(summary.moderate * (SEVERITY_WEIGHTS.moderate || 0)) +
|
|
390
|
+
(summary.low * (SEVERITY_WEIGHTS.low || 0));
|
|
391
|
+
|
|
392
|
+
result.score = Math.max(0, 100 - deductions);
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check file permissions
|
|
399
|
+
*/
|
|
400
|
+
async function runPermissionsCheck(projectRoot: string): Promise<CheckResult> {
|
|
401
|
+
const result: CheckResult = {
|
|
402
|
+
score: 100,
|
|
403
|
+
issues: [],
|
|
404
|
+
summary: { critical: 0, high: 0, moderate: 0, low: 0 }
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Check common sensitive files
|
|
408
|
+
const sensitiveFiles: Array<{ path: string; recommended: string; severity: string }> = [
|
|
409
|
+
{ path: '.env', recommended: '600', severity: 'high' },
|
|
410
|
+
{ path: '.env.local', recommended: '600', severity: 'high' },
|
|
411
|
+
{ path: 'id_rsa', recommended: '600', severity: 'critical' },
|
|
412
|
+
{ path: '.ssh/id_rsa', recommended: '600', severity: 'critical' },
|
|
413
|
+
{ path: 'credentials.json', recommended: '600', severity: 'high' },
|
|
414
|
+
{ path: 'serviceAccount.json', recommended: '600', severity: 'high' }
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
for (const { path: filePath, recommended, severity } of sensitiveFiles) {
|
|
418
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
419
|
+
|
|
420
|
+
if (fs.existsSync(fullPath)) {
|
|
421
|
+
try {
|
|
422
|
+
const stats = fs.statSync(fullPath);
|
|
423
|
+
const mode = (stats.mode & parseInt('777', 8)).toString(8);
|
|
424
|
+
|
|
425
|
+
// Check if file is world-readable
|
|
426
|
+
if (stats.mode & parseInt('004', 8)) {
|
|
427
|
+
const summary = result.summary as SeveritySummary;
|
|
428
|
+
const currentVal = (summary as unknown as Record<string, number>)[severity] || 0;
|
|
429
|
+
(summary as unknown as Record<string, number>)[severity] = currentVal + 1;
|
|
430
|
+
result.issues.push({
|
|
431
|
+
severity,
|
|
432
|
+
title: `${filePath} is world-readable`,
|
|
433
|
+
current: mode,
|
|
434
|
+
recommended,
|
|
435
|
+
fix: `chmod ${recommended} ${filePath}`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore stat errors
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const summary = result.summary as SeveritySummary;
|
|
445
|
+
const deductions =
|
|
446
|
+
(summary.critical * 20) +
|
|
447
|
+
(summary.high * 10) +
|
|
448
|
+
(summary.moderate * 5);
|
|
449
|
+
|
|
450
|
+
result.score = Math.max(0, 100 - deductions);
|
|
451
|
+
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Check security headers configuration
|
|
457
|
+
*/
|
|
458
|
+
async function runHeadersCheck(projectRoot: string): Promise<CheckResult> {
|
|
459
|
+
const result: CheckResult = {
|
|
460
|
+
score: 100,
|
|
461
|
+
issues: [],
|
|
462
|
+
summary: { missing: 0, configured: 0 } as HeadersSummary
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Check for Next.js security headers
|
|
466
|
+
const nextConfig = path.join(projectRoot, 'next.config.js');
|
|
467
|
+
const nextConfigMjs = path.join(projectRoot, 'next.config.mjs');
|
|
468
|
+
const vercelJson = path.join(projectRoot, 'vercel.json');
|
|
469
|
+
|
|
470
|
+
const requiredHeaders = [
|
|
471
|
+
'X-Content-Type-Options',
|
|
472
|
+
'X-Frame-Options',
|
|
473
|
+
'X-XSS-Protection',
|
|
474
|
+
'Strict-Transport-Security',
|
|
475
|
+
'Content-Security-Policy'
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
let hasHeaderConfig = false;
|
|
479
|
+
const summary = result.summary as HeadersSummary;
|
|
480
|
+
|
|
481
|
+
// Check next.config.js
|
|
482
|
+
for (const configPath of [nextConfig, nextConfigMjs]) {
|
|
483
|
+
if (fs.existsSync(configPath)) {
|
|
484
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
485
|
+
if (content.includes('headers') || content.includes('securityHeaders')) {
|
|
486
|
+
hasHeaderConfig = true;
|
|
487
|
+
|
|
488
|
+
// Check which headers are configured
|
|
489
|
+
for (const header of requiredHeaders) {
|
|
490
|
+
if (content.includes(header)) {
|
|
491
|
+
summary.configured++;
|
|
492
|
+
} else {
|
|
493
|
+
summary.missing++;
|
|
494
|
+
result.issues.push({
|
|
495
|
+
severity: 'moderate',
|
|
496
|
+
title: `Missing ${header}`,
|
|
497
|
+
file: path.basename(configPath)
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check vercel.json
|
|
506
|
+
if (fs.existsSync(vercelJson)) {
|
|
507
|
+
const content = fs.readFileSync(vercelJson, 'utf-8');
|
|
508
|
+
if (content.includes('headers')) {
|
|
509
|
+
hasHeaderConfig = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!hasHeaderConfig) {
|
|
514
|
+
result.score = 50;
|
|
515
|
+
result.issues.push({
|
|
516
|
+
severity: 'moderate',
|
|
517
|
+
title: 'No security headers configured',
|
|
518
|
+
recommendation: 'Add security headers to next.config.js or vercel.json'
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
result.score = Math.max(0, 100 - (summary.missing * 10));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Check environment variable best practices
|
|
529
|
+
*/
|
|
530
|
+
async function runEnvCheck(projectRoot: string): Promise<CheckResult> {
|
|
531
|
+
const result: CheckResult = {
|
|
532
|
+
score: 100,
|
|
533
|
+
issues: [],
|
|
534
|
+
summary: { warnings: 0, good: 0 } as EnvSummary
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const envExample = path.join(projectRoot, '.env.example');
|
|
538
|
+
const envLocal = path.join(projectRoot, '.env.local');
|
|
539
|
+
const envFile = path.join(projectRoot, '.env');
|
|
540
|
+
const summary = result.summary as EnvSummary;
|
|
541
|
+
|
|
542
|
+
// Check if .env.example exists
|
|
543
|
+
if (!fs.existsSync(envExample)) {
|
|
544
|
+
result.issues.push({
|
|
545
|
+
severity: 'low',
|
|
546
|
+
title: 'Missing .env.example',
|
|
547
|
+
recommendation: 'Create .env.example to document required environment variables'
|
|
548
|
+
});
|
|
549
|
+
summary.warnings++;
|
|
550
|
+
} else {
|
|
551
|
+
summary.good++;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Check if .env is in .gitignore
|
|
555
|
+
const gitignore = path.join(projectRoot, '.gitignore');
|
|
556
|
+
if (fs.existsSync(gitignore)) {
|
|
557
|
+
const content = fs.readFileSync(gitignore, 'utf-8');
|
|
558
|
+
|
|
559
|
+
if (!content.includes('.env') || content.includes('.env.example')) {
|
|
560
|
+
if (fs.existsSync(envFile) || fs.existsSync(envLocal)) {
|
|
561
|
+
result.issues.push({
|
|
562
|
+
severity: 'high',
|
|
563
|
+
title: '.env files may not be properly gitignored',
|
|
564
|
+
recommendation: 'Ensure .env and .env.local are in .gitignore'
|
|
565
|
+
});
|
|
566
|
+
summary.warnings++;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
summary.good++;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check for hardcoded values that should be env vars
|
|
574
|
+
const configFiles = ['next.config.js', 'next.config.mjs', 'config.js'];
|
|
575
|
+
const hardcodedPatterns = [
|
|
576
|
+
/https?:\/\/[a-z0-9-]+\.(supabase|neon|planetscale)\.co/gi,
|
|
577
|
+
/mongodb\+srv:\/\//gi,
|
|
578
|
+
/postgres:\/\//gi
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
for (const configFile of configFiles) {
|
|
582
|
+
const filePath = path.join(projectRoot, configFile);
|
|
583
|
+
if (fs.existsSync(filePath)) {
|
|
584
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
585
|
+
|
|
586
|
+
for (const pattern of hardcodedPatterns) {
|
|
587
|
+
pattern.lastIndex = 0;
|
|
588
|
+
if (pattern.test(content) && !content.includes('process.env')) {
|
|
589
|
+
result.issues.push({
|
|
590
|
+
severity: 'moderate',
|
|
591
|
+
title: `Potential hardcoded URL in ${configFile}`,
|
|
592
|
+
recommendation: 'Use environment variables for database/service URLs'
|
|
593
|
+
});
|
|
594
|
+
summary.warnings++;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
result.score = Math.max(0, 100 - (summary.warnings * 15));
|
|
601
|
+
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Check gitignore for sensitive file coverage
|
|
607
|
+
*/
|
|
608
|
+
async function runGitignoreCheck(projectRoot: string): Promise<CheckResult> {
|
|
609
|
+
const result: CheckResult = {
|
|
610
|
+
score: 100,
|
|
611
|
+
issues: [],
|
|
612
|
+
summary: { missing: 0, covered: 0 } as GitignoreSummary
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const gitignore = path.join(projectRoot, '.gitignore');
|
|
616
|
+
|
|
617
|
+
if (!fs.existsSync(gitignore)) {
|
|
618
|
+
return {
|
|
619
|
+
score: 50,
|
|
620
|
+
issues: [{ severity: 'high', title: 'No .gitignore file found' }],
|
|
621
|
+
summary: { missing: 1, covered: 0 } as GitignoreSummary
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const content = fs.readFileSync(gitignore, 'utf-8');
|
|
626
|
+
const summary = result.summary as GitignoreSummary;
|
|
627
|
+
|
|
628
|
+
// Sensitive patterns that should be gitignored
|
|
629
|
+
const requiredPatterns: Array<{ pattern: string; severity: string; name: string }> = [
|
|
630
|
+
{ pattern: '.env', severity: 'high', name: 'Environment files' },
|
|
631
|
+
{ pattern: '.env.local', severity: 'high', name: 'Local env files' },
|
|
632
|
+
{ pattern: 'node_modules', severity: 'moderate', name: 'Node modules' },
|
|
633
|
+
{ pattern: '*.pem', severity: 'critical', name: 'PEM certificates' },
|
|
634
|
+
{ pattern: '*.key', severity: 'critical', name: 'Key files' },
|
|
635
|
+
{ pattern: '.DS_Store', severity: 'low', name: 'macOS files' },
|
|
636
|
+
{ pattern: 'coverage', severity: 'low', name: 'Coverage reports' }
|
|
637
|
+
];
|
|
638
|
+
|
|
639
|
+
for (const { pattern, severity, name } of requiredPatterns) {
|
|
640
|
+
if (content.includes(pattern)) {
|
|
641
|
+
summary.covered++;
|
|
642
|
+
} else {
|
|
643
|
+
// Check if file exists before warning
|
|
644
|
+
const patternPath = path.join(projectRoot, pattern.replace('*', ''));
|
|
645
|
+
if (fs.existsSync(patternPath) || pattern.includes('*')) {
|
|
646
|
+
summary.missing++;
|
|
647
|
+
result.issues.push({
|
|
648
|
+
severity,
|
|
649
|
+
title: `${name} (${pattern}) not in .gitignore`,
|
|
650
|
+
recommendation: `Add ${pattern} to .gitignore`
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const deductions =
|
|
657
|
+
(result.issues.filter(i => i.severity === 'critical').length * 20) +
|
|
658
|
+
(result.issues.filter(i => i.severity === 'high').length * 15) +
|
|
659
|
+
(result.issues.filter(i => i.severity === 'moderate').length * 5);
|
|
660
|
+
|
|
661
|
+
result.score = Math.max(0, 100 - deductions);
|
|
662
|
+
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ============================================================================
|
|
667
|
+
// Main Security Functions
|
|
668
|
+
// ============================================================================
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Run all security checks and calculate score
|
|
672
|
+
*/
|
|
673
|
+
async function runFullScan(projectRoot: string, options: ScanOptions = {}): Promise<ScanResults> {
|
|
674
|
+
const results: ScanResults = {
|
|
675
|
+
timestamp: new Date().toISOString(),
|
|
676
|
+
overallScore: 0,
|
|
677
|
+
checks: {},
|
|
678
|
+
summary: {
|
|
679
|
+
critical: 0,
|
|
680
|
+
high: 0,
|
|
681
|
+
moderate: 0,
|
|
682
|
+
low: 0,
|
|
683
|
+
info: 0
|
|
684
|
+
},
|
|
685
|
+
recommendations: []
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
console.log(`
|
|
689
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Scan${utils.COLORS.reset}
|
|
690
|
+
${utils.COLORS.dim}Running comprehensive security checks...${utils.COLORS.reset}
|
|
691
|
+
`);
|
|
692
|
+
|
|
693
|
+
let totalWeight = 0;
|
|
694
|
+
let weightedScore = 0;
|
|
695
|
+
|
|
696
|
+
for (const [id, check] of Object.entries(SECURITY_CHECKS)) {
|
|
697
|
+
if (options.skip && options.skip.includes(id)) {
|
|
698
|
+
console.log(`${utils.COLORS.dim}○${utils.COLORS.reset} ${check.name}: skipped`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const spinner = utils.createSpinner(`${check.name}: ${check.description}`).start();
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const result = await check.run(projectRoot);
|
|
706
|
+
results.checks[id] = result;
|
|
707
|
+
|
|
708
|
+
// Update summary
|
|
709
|
+
if (result.summary) {
|
|
710
|
+
for (const [severity, count] of Object.entries(result.summary)) {
|
|
711
|
+
if (results.summary[severity as keyof SeveritySummary] !== undefined) {
|
|
712
|
+
results.summary[severity as keyof SeveritySummary] += count as number;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Calculate weighted score
|
|
718
|
+
totalWeight += check.weight;
|
|
719
|
+
weightedScore += result.score * check.weight;
|
|
720
|
+
|
|
721
|
+
// Show result
|
|
722
|
+
if (result.score >= 90) {
|
|
723
|
+
spinner.succeed(`${check.name}: ${utils.COLORS.green}${result.score}%${utils.COLORS.reset}`);
|
|
724
|
+
} else if (result.score >= 70) {
|
|
725
|
+
spinner.warn(`${check.name}: ${utils.COLORS.yellow}${result.score}%${utils.COLORS.reset}`);
|
|
726
|
+
} else {
|
|
727
|
+
spinner.fail(`${check.name}: ${utils.COLORS.red}${result.score}%${utils.COLORS.reset}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Show critical issues
|
|
731
|
+
if (!options.quiet && result.issues.length > 0) {
|
|
732
|
+
const criticalIssues = result.issues.filter(i =>
|
|
733
|
+
i.severity === 'critical' || i.severity === 'high'
|
|
734
|
+
);
|
|
735
|
+
for (const issue of criticalIssues.slice(0, 3)) {
|
|
736
|
+
const color = issue.severity === 'critical' ? utils.COLORS.red : utils.COLORS.yellow;
|
|
737
|
+
console.log(` ${color}└ ${issue.title}${utils.COLORS.reset}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
spinner.fail(`${check.name}: error`);
|
|
742
|
+
results.checks[id] = { score: 0, error: (error as Error).message, issues: [], summary: { critical: 0, high: 0, moderate: 0, low: 0 } };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Calculate overall score
|
|
747
|
+
results.overallScore = totalWeight > 0
|
|
748
|
+
? Math.round(weightedScore / totalWeight)
|
|
749
|
+
: 0;
|
|
750
|
+
|
|
751
|
+
// Generate recommendations
|
|
752
|
+
results.recommendations = generateRecommendations(results);
|
|
753
|
+
|
|
754
|
+
return results;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Generate recommendations based on scan results
|
|
759
|
+
*/
|
|
760
|
+
function generateRecommendations(results: ScanResults): Recommendation[] {
|
|
761
|
+
const recommendations: Recommendation[] = [];
|
|
762
|
+
|
|
763
|
+
// Priority: critical issues first
|
|
764
|
+
if (results.summary.critical > 0) {
|
|
765
|
+
recommendations.push({
|
|
766
|
+
priority: 'critical',
|
|
767
|
+
title: `Fix ${results.summary.critical} critical security issue(s)`,
|
|
768
|
+
action: 'Review critical findings and address immediately'
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (results.summary.high > 0) {
|
|
773
|
+
recommendations.push({
|
|
774
|
+
priority: 'high',
|
|
775
|
+
title: `Address ${results.summary.high} high priority issue(s)`,
|
|
776
|
+
action: 'Run `npm audit fix` for dependency issues'
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Check-specific recommendations
|
|
781
|
+
const secretsScore = results.checks.secrets?.score;
|
|
782
|
+
if (secretsScore !== undefined && secretsScore < 100) {
|
|
783
|
+
recommendations.push({
|
|
784
|
+
priority: 'high',
|
|
785
|
+
title: 'Remove exposed secrets from codebase',
|
|
786
|
+
action: 'Move secrets to .env files and rotate compromised credentials'
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const headersScore = results.checks.headers?.score;
|
|
791
|
+
if (headersScore !== undefined && headersScore < 70) {
|
|
792
|
+
recommendations.push({
|
|
793
|
+
priority: 'moderate',
|
|
794
|
+
title: 'Configure security headers',
|
|
795
|
+
action: 'Add security headers to next.config.js or vercel.json'
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const gitignoreScore = results.checks.gitignore?.score;
|
|
800
|
+
if (gitignoreScore !== undefined && gitignoreScore < 90) {
|
|
801
|
+
recommendations.push({
|
|
802
|
+
priority: 'moderate',
|
|
803
|
+
title: 'Update .gitignore',
|
|
804
|
+
action: 'Ensure sensitive files are excluded from version control'
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return recommendations;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Store security score in project state
|
|
813
|
+
*/
|
|
814
|
+
function storeSecurityScore(projectRoot: string, results: ScanResults): SecurityState {
|
|
815
|
+
const state = projectState.getOrCreateState(projectRoot);
|
|
816
|
+
|
|
817
|
+
// Add security section if not exists
|
|
818
|
+
if (!state.security) {
|
|
819
|
+
state.security = {
|
|
820
|
+
score: 0,
|
|
821
|
+
lastScan: null,
|
|
822
|
+
history: []
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Update security data
|
|
827
|
+
state.security = {
|
|
828
|
+
score: results.overallScore,
|
|
829
|
+
lastScan: results.timestamp,
|
|
830
|
+
summary: results.summary,
|
|
831
|
+
checks: Object.fromEntries(
|
|
832
|
+
Object.entries(results.checks).map(([id, check]) => [
|
|
833
|
+
id,
|
|
834
|
+
{ score: check.score, issues: check.issues?.length || 0 }
|
|
835
|
+
])
|
|
836
|
+
),
|
|
837
|
+
history: [
|
|
838
|
+
{ score: results.overallScore, date: results.timestamp },
|
|
839
|
+
...(state.security.history || []).slice(0, 9) // Keep last 10
|
|
840
|
+
]
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// Update health breakdown if exists
|
|
844
|
+
if (state.health?.breakdown) {
|
|
845
|
+
state.health.breakdown.security = Math.round(results.overallScore * 0.25); // 25% of overall health
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
projectState.saveState(projectRoot, state);
|
|
849
|
+
|
|
850
|
+
return state.security;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Show security status
|
|
855
|
+
*/
|
|
856
|
+
function showStatus(projectRoot: string): void {
|
|
857
|
+
const state = projectState.loadState(projectRoot);
|
|
858
|
+
|
|
859
|
+
console.log(`
|
|
860
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Security Status${utils.COLORS.reset}
|
|
861
|
+
`);
|
|
862
|
+
|
|
863
|
+
if (!state?.security?.lastScan) {
|
|
864
|
+
console.log(`${utils.COLORS.dim}No security scan has been run yet.${utils.COLORS.reset}`);
|
|
865
|
+
console.log(`${utils.COLORS.dim}Run 'bootspring security scan' to perform a scan.${utils.COLORS.reset}`);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const sec = state.security;
|
|
870
|
+
const scoreColor = sec.score >= 90 ? utils.COLORS.green :
|
|
871
|
+
sec.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
|
|
872
|
+
|
|
873
|
+
console.log(`${utils.COLORS.bold}Overall Score${utils.COLORS.reset}`);
|
|
874
|
+
console.log(` ${scoreColor}${utils.COLORS.bold}${sec.score}%${utils.COLORS.reset}`);
|
|
875
|
+
const lastScanDate = sec.lastScan ? new Date(sec.lastScan).toLocaleString() : 'Unknown';
|
|
876
|
+
console.log(` ${utils.COLORS.dim}Last scan: ${lastScanDate}${utils.COLORS.reset}`);
|
|
877
|
+
console.log();
|
|
878
|
+
|
|
879
|
+
if (sec.summary) {
|
|
880
|
+
console.log(`${utils.COLORS.bold}Issues Found${utils.COLORS.reset}`);
|
|
881
|
+
console.log(` ${utils.COLORS.red}Critical: ${sec.summary.critical || 0}${utils.COLORS.reset}`);
|
|
882
|
+
console.log(` ${utils.COLORS.yellow}High: ${sec.summary.high || 0}${utils.COLORS.reset}`);
|
|
883
|
+
console.log(` ${utils.COLORS.cyan}Moderate: ${sec.summary.moderate || 0}${utils.COLORS.reset}`);
|
|
884
|
+
console.log(` ${utils.COLORS.dim}Low: ${sec.summary.low || 0}${utils.COLORS.reset}`);
|
|
885
|
+
console.log();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (sec.checks) {
|
|
889
|
+
console.log(`${utils.COLORS.bold}Check Scores${utils.COLORS.reset}`);
|
|
890
|
+
for (const [id, check] of Object.entries(sec.checks)) {
|
|
891
|
+
const name = SECURITY_CHECKS[id]?.name || id;
|
|
892
|
+
const checkColor = check.score >= 90 ? utils.COLORS.green :
|
|
893
|
+
check.score >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
|
|
894
|
+
const bar = utils.createProgressBar ?
|
|
895
|
+
utils.createProgressBar(check.score, 100, 20) :
|
|
896
|
+
`${check.score}%`;
|
|
897
|
+
console.log(` ${name.padEnd(20)} ${checkColor}${bar}${utils.COLORS.reset}`);
|
|
898
|
+
}
|
|
899
|
+
console.log();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (sec.history && sec.history.length > 1) {
|
|
903
|
+
console.log(`${utils.COLORS.bold}Score History${utils.COLORS.reset}`);
|
|
904
|
+
const firstHistory = sec.history[0];
|
|
905
|
+
const secondHistory = sec.history[1];
|
|
906
|
+
if (firstHistory && secondHistory) {
|
|
907
|
+
const trend = firstHistory.score - secondHistory.score;
|
|
908
|
+
const trendIcon = trend > 0 ? '↑' : trend < 0 ? '↓' : '→';
|
|
909
|
+
const trendColor = trend > 0 ? utils.COLORS.green : trend < 0 ? utils.COLORS.red : utils.COLORS.dim;
|
|
910
|
+
console.log(` ${trendColor}${trendIcon} ${Math.abs(trend)} points from last scan${utils.COLORS.reset}`);
|
|
911
|
+
console.log(` ${utils.COLORS.dim}${sec.history.slice(0, 5).map(h => h.score).join(' → ')}${utils.COLORS.reset}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Show detailed findings
|
|
918
|
+
*/
|
|
919
|
+
function showFindings(projectRoot: string): void {
|
|
920
|
+
const state = projectState.loadState(projectRoot);
|
|
921
|
+
|
|
922
|
+
if (!state?.security?.lastScan) {
|
|
923
|
+
console.log(`${utils.COLORS.dim}No security scan results. Run 'bootspring security scan' first.${utils.COLORS.reset}`);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Re-run scan to get detailed findings
|
|
928
|
+
console.log(`${utils.COLORS.dim}Re-scanning for detailed findings...${utils.COLORS.reset}\n`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Quick security check
|
|
933
|
+
*/
|
|
934
|
+
async function runQuickCheck(projectRoot: string): Promise<void> {
|
|
935
|
+
console.log(`
|
|
936
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Quick Security Check${utils.COLORS.reset}
|
|
937
|
+
`);
|
|
938
|
+
|
|
939
|
+
// Just run dependency audit and secrets detection
|
|
940
|
+
const results = {
|
|
941
|
+
dependencies: await runDependencyAudit(projectRoot),
|
|
942
|
+
secrets: await runSecretsDetection(projectRoot)
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const depSummary = results.dependencies.summary as SeveritySummary;
|
|
946
|
+
const secretsSummary = results.secrets.summary as SeveritySummary;
|
|
947
|
+
|
|
948
|
+
const issues =
|
|
949
|
+
(depSummary.critical || 0) +
|
|
950
|
+
(depSummary.high || 0) +
|
|
951
|
+
(secretsSummary.critical || 0) +
|
|
952
|
+
(secretsSummary.high || 0);
|
|
953
|
+
|
|
954
|
+
if (issues === 0) {
|
|
955
|
+
utils.print.success('No critical security issues found');
|
|
956
|
+
} else {
|
|
957
|
+
utils.print.warn(`Found ${issues} high/critical security issue(s)`);
|
|
958
|
+
console.log(`${utils.COLORS.dim}Run 'bootspring security scan' for full details${utils.COLORS.reset}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Show help
|
|
964
|
+
*/
|
|
965
|
+
function showHelp(): void {
|
|
966
|
+
console.log(`
|
|
967
|
+
${utils.COLORS.cyan}${utils.COLORS.bold}🔒 Bootspring Security${utils.COLORS.reset}
|
|
968
|
+
${utils.COLORS.dim}Security scanning and score tracking${utils.COLORS.reset}
|
|
969
|
+
|
|
970
|
+
${utils.COLORS.bold}Usage:${utils.COLORS.reset}
|
|
971
|
+
bootspring security <command> [options]
|
|
972
|
+
|
|
973
|
+
${utils.COLORS.bold}Commands:${utils.COLORS.reset}
|
|
974
|
+
${utils.COLORS.cyan}scan${utils.COLORS.reset} Run full security scan and store score
|
|
975
|
+
${utils.COLORS.cyan}quick${utils.COLORS.reset} Quick check (deps + secrets only)
|
|
976
|
+
${utils.COLORS.cyan}status${utils.COLORS.reset} Show current security score and history
|
|
977
|
+
${utils.COLORS.cyan}findings${utils.COLORS.reset} Show detailed findings from last scan
|
|
978
|
+
|
|
979
|
+
${utils.COLORS.bold}Options:${utils.COLORS.reset}
|
|
980
|
+
--skip <check> Skip specific check (deps, secrets, headers, etc.)
|
|
981
|
+
--quiet Suppress detailed output
|
|
982
|
+
--json Output results as JSON
|
|
983
|
+
|
|
984
|
+
${utils.COLORS.bold}Checks Performed:${utils.COLORS.reset}
|
|
985
|
+
${utils.COLORS.dim}dependencies${utils.COLORS.reset} npm audit for vulnerable packages
|
|
986
|
+
${utils.COLORS.dim}secrets${utils.COLORS.reset} Scan for exposed API keys and passwords
|
|
987
|
+
${utils.COLORS.dim}permissions${utils.COLORS.reset} Check file permissions on sensitive files
|
|
988
|
+
${utils.COLORS.dim}headers${utils.COLORS.reset} Verify security headers configuration
|
|
989
|
+
${utils.COLORS.dim}env${utils.COLORS.reset} Check .env best practices
|
|
990
|
+
${utils.COLORS.dim}gitignore${utils.COLORS.reset} Verify sensitive files are ignored
|
|
991
|
+
|
|
992
|
+
${utils.COLORS.bold}Examples:${utils.COLORS.reset}
|
|
993
|
+
bootspring security scan
|
|
994
|
+
bootspring security scan --skip headers
|
|
995
|
+
bootspring security quick
|
|
996
|
+
bootspring security status
|
|
997
|
+
`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Main run function
|
|
1002
|
+
*/
|
|
1003
|
+
export async function run(args: string[]): Promise<void> {
|
|
1004
|
+
const parsedArgs = utils.parseArgs(args);
|
|
1005
|
+
const subcommand = parsedArgs._[0];
|
|
1006
|
+
const cfg = config.load();
|
|
1007
|
+
const projectRoot = cfg._projectRoot;
|
|
1008
|
+
|
|
1009
|
+
switch (subcommand) {
|
|
1010
|
+
case 'scan':
|
|
1011
|
+
case 'full': {
|
|
1012
|
+
const results = await runFullScan(projectRoot, {
|
|
1013
|
+
skip: parsedArgs.skip ? [parsedArgs.skip] : [],
|
|
1014
|
+
quiet: parsedArgs.quiet
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// Store score
|
|
1018
|
+
storeSecurityScore(projectRoot, results);
|
|
1019
|
+
|
|
1020
|
+
// Show summary
|
|
1021
|
+
console.log();
|
|
1022
|
+
const scoreColor = results.overallScore >= 90 ? utils.COLORS.green :
|
|
1023
|
+
results.overallScore >= 70 ? utils.COLORS.yellow : utils.COLORS.red;
|
|
1024
|
+
|
|
1025
|
+
console.log(`${utils.COLORS.bold}Security Score: ${scoreColor}${results.overallScore}%${utils.COLORS.reset}`);
|
|
1026
|
+
|
|
1027
|
+
if (results.recommendations.length > 0) {
|
|
1028
|
+
console.log(`\n${utils.COLORS.bold}Top Recommendations:${utils.COLORS.reset}`);
|
|
1029
|
+
for (const rec of results.recommendations.slice(0, 3)) {
|
|
1030
|
+
const icon = rec.priority === 'critical' ? '🔴' :
|
|
1031
|
+
rec.priority === 'high' ? '🟡' : '🔵';
|
|
1032
|
+
console.log(` ${icon} ${rec.title}`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
console.log(`\n${utils.COLORS.dim}Score saved to planning/PROJECT_STATE.json${utils.COLORS.reset}`);
|
|
1037
|
+
|
|
1038
|
+
if (parsedArgs.json) {
|
|
1039
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Exit with error if critical issues
|
|
1043
|
+
if (results.summary.critical > 0) {
|
|
1044
|
+
process.exitCode = 1;
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
case 'quick':
|
|
1050
|
+
case 'check':
|
|
1051
|
+
await runQuickCheck(projectRoot);
|
|
1052
|
+
break;
|
|
1053
|
+
|
|
1054
|
+
case 'status':
|
|
1055
|
+
showStatus(projectRoot);
|
|
1056
|
+
break;
|
|
1057
|
+
|
|
1058
|
+
case 'findings':
|
|
1059
|
+
case 'details':
|
|
1060
|
+
showFindings(projectRoot);
|
|
1061
|
+
break;
|
|
1062
|
+
|
|
1063
|
+
case 'help':
|
|
1064
|
+
case '-h':
|
|
1065
|
+
case '--help':
|
|
1066
|
+
showHelp();
|
|
1067
|
+
break;
|
|
1068
|
+
|
|
1069
|
+
default:
|
|
1070
|
+
if (!subcommand) {
|
|
1071
|
+
showHelp();
|
|
1072
|
+
} else {
|
|
1073
|
+
utils.print.error(`Unknown command: ${subcommand}`);
|
|
1074
|
+
showHelp();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export { runFullScan, storeSecurityScore, SECURITY_CHECKS };
|