@massu/core 1.10.8 → 1.12.1

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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,405 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import type Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tools.ts';
6
- import { getConfig } from './config.ts';
7
- import { existsSync, readFileSync } from 'fs';
8
- import { ensureWithinRoot, enforceSeverityFloors } from './security-utils.ts';
9
-
10
- // ============================================================
11
- // Security Risk Scoring
12
- // ============================================================
13
-
14
- /** Prefix a base tool name with the configured tool prefix. */
15
- function p(baseName: string): string {
16
- return `${getConfig().toolPrefix}_${baseName}`;
17
- }
18
-
19
- export interface SecurityFinding {
20
- pattern: string;
21
- severity: 'critical' | 'high' | 'medium' | 'low';
22
- line: number;
23
- description: string;
24
- }
25
-
26
- interface SecurityPattern {
27
- regex: RegExp;
28
- severity: SecurityFinding['severity'];
29
- description: string;
30
- fileFilter?: RegExp;
31
- }
32
-
33
- /** Default security patterns. Configurable via security.patterns in config. */
34
- const DEFAULT_SECURITY_PATTERNS: SecurityPattern[] = [
35
- {
36
- regex: /\bexec\s*\(\s*[`"'].*\$\{/,
37
- severity: 'critical',
38
- description: 'Potential command injection via template literal in exec()',
39
- },
40
- {
41
- regex: /publicProcedure\s*\.\s*mutation/,
42
- severity: 'critical',
43
- description: 'Mutation without authentication (publicProcedure)',
44
- fileFilter: /\.(ts|tsx)$/,
45
- },
46
- {
47
- regex: /(password|secret|token|api_key)\s*[:=]\s*['"][^'"]{8,}['"]/i,
48
- severity: 'critical',
49
- description: 'Hardcoded credential or secret',
50
- },
51
- {
52
- regex: /\bdangerouslySetInnerHTML\b/,
53
- severity: 'high',
54
- description: 'XSS risk via dangerouslySetInnerHTML',
55
- fileFilter: /\.tsx$/,
56
- },
57
- {
58
- regex: /\.raw\s*\(`/,
59
- severity: 'high',
60
- description: 'Raw SQL query with template literal (SQL injection risk)',
61
- },
62
- {
63
- regex: /eval\s*\(/,
64
- severity: 'high',
65
- description: 'Use of eval() - code injection risk',
66
- },
67
- {
68
- regex: /process\.env\.\w+.*\bconsole\.(log|info|debug)/,
69
- severity: 'medium',
70
- description: 'Environment variable logged to console',
71
- },
72
- {
73
- regex: /catch\s*\([^)]*\)\s*\{[^}]*res\.(json|send)\([^)]*err/,
74
- severity: 'medium',
75
- description: 'Error details exposed in response',
76
- },
77
- {
78
- regex: /Access-Control-Allow-Origin.*\*/,
79
- severity: 'medium',
80
- description: 'Overly permissive CORS (allows all origins)',
81
- },
82
- {
83
- regex: /new\s+URL\s*\(\s*(?:req|input|params|query)/,
84
- severity: 'medium',
85
- description: 'URL constructed from user input (SSRF risk)',
86
- },
87
- {
88
- regex: /JSON\.parse\s*\(\s*(?:req|input|body|params)/,
89
- severity: 'low',
90
- description: 'JSON.parse on user input without try/catch',
91
- },
92
- {
93
- regex: /prototype\s*:/,
94
- severity: 'high',
95
- description: 'Prototype key in object literal (prototype pollution risk)',
96
- },
97
- ];
98
-
99
- /** Default severity weights. Configurable via security.severity_weights */
100
- const DEFAULT_SEVERITY_WEIGHTS: Record<string, number> = {
101
- critical: 25,
102
- high: 15,
103
- medium: 8,
104
- low: 3,
105
- };
106
-
107
- /**
108
- * Get severity weights from config or defaults.
109
- */
110
- function getSeverityWeights(): Record<string, number> {
111
- const configWeights = getConfig().security?.severity_weights;
112
- if (!configWeights) return DEFAULT_SEVERITY_WEIGHTS;
113
- return enforceSeverityFloors(configWeights, DEFAULT_SEVERITY_WEIGHTS);
114
- }
115
-
116
- /**
117
- * Score security risk for a file.
118
- * Returns 0 (safe) to 100 (critical risk).
119
- */
120
- export function scoreFileSecurity(filePath: string, projectRoot: string): {
121
- riskScore: number;
122
- findings: SecurityFinding[];
123
- } {
124
- let absPath: string;
125
- try {
126
- absPath = ensureWithinRoot(filePath, projectRoot);
127
- } catch {
128
- return {
129
- riskScore: 100,
130
- findings: [{
131
- pattern: 'path_traversal',
132
- severity: 'critical',
133
- line: 0,
134
- description: `Path traversal blocked: "${filePath}" resolves outside project root`,
135
- }],
136
- };
137
- }
138
- if (!existsSync(absPath)) {
139
- return { riskScore: 0, findings: [] };
140
- }
141
-
142
- let source: string;
143
- try {
144
- source = readFileSync(absPath, 'utf-8');
145
- } catch {
146
- return { riskScore: 0, findings: [] };
147
- }
148
-
149
- const findings: SecurityFinding[] = [];
150
- const lines = source.split('\n');
151
-
152
- for (let i = 0; i < lines.length; i++) {
153
- const line = lines[i];
154
- for (const pattern of DEFAULT_SECURITY_PATTERNS) {
155
- if (pattern.fileFilter && !pattern.fileFilter.test(filePath)) continue;
156
- if (pattern.regex.test(line)) {
157
- findings.push({
158
- pattern: pattern.regex.source.slice(0, 50),
159
- severity: pattern.severity,
160
- line: i + 1,
161
- description: pattern.description,
162
- });
163
- }
164
- }
165
- }
166
-
167
- // Calculate risk score
168
- const severityWeights = getSeverityWeights();
169
- let riskScore = 0;
170
-
171
- for (const finding of findings) {
172
- riskScore += severityWeights[finding.severity] ?? 0;
173
- }
174
-
175
- return {
176
- riskScore: Math.min(100, riskScore),
177
- findings,
178
- };
179
- }
180
-
181
- /**
182
- * Store security score for a file.
183
- */
184
- export function storeSecurityScore(
185
- db: Database.Database,
186
- sessionId: string,
187
- filePath: string,
188
- riskScore: number,
189
- findings: SecurityFinding[]
190
- ): void {
191
- db.prepare(`
192
- INSERT INTO security_scores
193
- (session_id, file_path, risk_score, findings)
194
- VALUES (?, ?, ?, ?)
195
- `).run(sessionId, filePath, riskScore, JSON.stringify(findings));
196
- }
197
-
198
- // ============================================================
199
- // MCP Tool Definitions & Handlers
200
- // ============================================================
201
-
202
- export function getSecurityToolDefinitions(): ToolDefinition[] {
203
- return [
204
- {
205
- name: p('security_score'),
206
- description: 'Security risk score for a file. Detects SQL injection, XSS, hardcoded secrets, auth gaps, and more.',
207
- inputSchema: {
208
- type: 'object',
209
- properties: {
210
- file_path: { type: 'string', description: 'File path relative to project root' },
211
- session_id: { type: 'string', description: 'Get scores for an entire session' },
212
- },
213
- required: [],
214
- },
215
- },
216
- {
217
- name: p('security_heatmap'),
218
- description: 'Security risk heat map. Files ranked by risk score with summary findings.',
219
- inputSchema: {
220
- type: 'object',
221
- properties: {
222
- threshold: { type: 'number', description: 'Show files above this risk score (default: 30)' },
223
- },
224
- required: [],
225
- },
226
- },
227
- {
228
- name: p('security_trend'),
229
- description: 'Security posture over time. Average risk scores and most improved/degraded areas.',
230
- inputSchema: {
231
- type: 'object',
232
- properties: {
233
- days: { type: 'number', description: 'Days to look back (default: 30)' },
234
- },
235
- required: [],
236
- },
237
- },
238
- ];
239
- }
240
-
241
- const SECURITY_BASE_NAMES = new Set(['security_score', 'security_heatmap', 'security_trend']);
242
-
243
- export function isSecurityTool(name: string): boolean {
244
- const pfx = getConfig().toolPrefix + '_';
245
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
246
- return SECURITY_BASE_NAMES.has(baseName);
247
- }
248
-
249
- export function handleSecurityToolCall(
250
- name: string,
251
- args: Record<string, unknown>,
252
- memoryDb: Database.Database
253
- ): ToolResult {
254
- try {
255
- const pfx = getConfig().toolPrefix + '_';
256
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
257
-
258
- switch (baseName) {
259
- case 'security_score':
260
- return handleSecurityScore(args, memoryDb);
261
- case 'security_heatmap':
262
- return handleSecurityHeatmap(args, memoryDb);
263
- case 'security_trend':
264
- return handleSecurityTrend(args, memoryDb);
265
- default:
266
- return text(`Unknown security tool: ${name}`);
267
- }
268
- } catch (error) {
269
- return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('security_score')} { file_path: "src/..." }, ${p('security_heatmap')} { threshold: 30 }`);
270
- }
271
- }
272
-
273
- function handleSecurityScore(args: Record<string, unknown>, db: Database.Database): ToolResult {
274
- const filePath = args.file_path as string | undefined;
275
- const sessionId = args.session_id as string | undefined;
276
-
277
- if (filePath) {
278
- const config = getConfig();
279
- const { riskScore, findings } = scoreFileSecurity(filePath, config.project.root);
280
-
281
- // Store the result
282
- const session = db.prepare(
283
- "SELECT session_id FROM sessions WHERE status = 'active' ORDER BY started_at_epoch DESC LIMIT 1"
284
- ).get() as { session_id: string } | undefined;
285
- if (session) {
286
- storeSecurityScore(db, session.session_id, filePath, riskScore, findings);
287
- }
288
-
289
- const lines = [
290
- `## Security Score: ${filePath}`,
291
- `Risk: **${riskScore}/100** ${riskScore === 0 ? '(clean)' : riskScore < 30 ? '(low risk)' : riskScore < 60 ? '(medium risk)' : '(HIGH RISK)'}`,
292
- '',
293
- ];
294
-
295
- if (findings.length > 0) {
296
- lines.push('### Findings');
297
- for (const f of findings) {
298
- lines.push(`- **[${f.severity.toUpperCase()}]** L${f.line}: ${f.description}`);
299
- }
300
- } else {
301
- lines.push(`No security findings detected (checked ${DEFAULT_SECURITY_PATTERNS.length} patterns including command injection, XSS, hardcoded secrets, and auth gaps).`);
302
- }
303
-
304
- return text(lines.join('\n'));
305
- }
306
-
307
- if (sessionId) {
308
- const scores = db.prepare(`
309
- SELECT file_path, risk_score, findings FROM security_scores
310
- WHERE session_id = ?
311
- ORDER BY risk_score DESC
312
- `).all(sessionId) as Array<Record<string, unknown>>;
313
-
314
- if (scores.length === 0) {
315
- return text(`No security scores for session ${sessionId.slice(0, 8)}... Security scores are generated when files are scanned. Try: ${p('security_score')} { file_path: "src/server/api/routers/example.ts" } to scan a file, or ${p('security_heatmap')} {} to see all scanned files.`);
316
- }
317
-
318
- const lines = [
319
- `## Security Scores for Session ${sessionId.slice(0, 12)}...`,
320
- '',
321
- '| File | Risk Score | Findings |',
322
- '|------|-----------|----------|',
323
- ];
324
-
325
- for (const s of scores) {
326
- const findingCount = JSON.parse(s.findings as string).length;
327
- lines.push(`| ${s.file_path} | ${s.risk_score} | ${findingCount} |`);
328
- }
329
-
330
- return text(lines.join('\n'));
331
- }
332
-
333
- return text(`Usage: ${p('security_score')} { file_path: "src/server/api/routers/example.ts" } to scan a file, or ${p('security_score')} { session_id: "..." } to see all scores for a session.`);
334
- }
335
-
336
- function handleSecurityHeatmap(args: Record<string, unknown>, db: Database.Database): ToolResult {
337
- const threshold = (args.threshold as number) ?? 30;
338
-
339
- const files = db.prepare(`
340
- SELECT file_path, MAX(risk_score) as max_risk, COUNT(*) as scan_count
341
- FROM security_scores
342
- GROUP BY file_path
343
- HAVING max_risk >= ?
344
- ORDER BY max_risk DESC
345
- LIMIT 50
346
- `).all(threshold) as Array<Record<string, unknown>>;
347
-
348
- if (files.length === 0) {
349
- return text(`No files with risk score >= ${threshold}. ${threshold > 0 ? `Try lowering the threshold or scan files with ${p('security_score')} { file_path: "..." }.` : 'No security scans recorded yet. Scan files to build the heat map.'}`);
350
- }
351
-
352
- const lines = [
353
- `## Security Heat Map (threshold: ${threshold})`,
354
- `Files at risk: ${files.length}`,
355
- '',
356
- '| Risk | File | Scans |',
357
- '|------|------|-------|',
358
- ];
359
-
360
- for (const f of files) {
361
- const risk = f.max_risk as number;
362
- const indicator = risk >= 60 ? 'HIGH' : risk >= 30 ? 'MEDIUM' : 'LOW';
363
- lines.push(`| ${risk} [${indicator}] | ${f.file_path} | ${f.scan_count} |`);
364
- }
365
-
366
- return text(lines.join('\n'));
367
- }
368
-
369
- function handleSecurityTrend(args: Record<string, unknown>, db: Database.Database): ToolResult {
370
- const days = (args.days as number) ?? 30;
371
-
372
- const rows = db.prepare(`
373
- SELECT date(created_at) as day,
374
- AVG(risk_score) as avg_risk,
375
- MAX(risk_score) as max_risk,
376
- COUNT(*) as files_scanned
377
- FROM security_scores
378
- WHERE created_at >= datetime('now', ?)
379
- GROUP BY date(created_at)
380
- ORDER BY day ASC
381
- `).all(`-${days} days`) as Array<Record<string, unknown>>;
382
-
383
- if (rows.length === 0) {
384
- return text(`No security scan data in the last ${days} days. Security trends build as files are scanned across sessions. Try: ${p('security_score')} { file_path: "src/server/api/routers/example.ts" } to scan a file, or try a longer time range with { days: 90 }.`);
385
- }
386
-
387
- const lines = [
388
- `## Security Trend (${days} days)`,
389
- '',
390
- '| Date | Avg Risk | Max Risk | Files Scanned |',
391
- '|------|----------|----------|---------------|',
392
- ];
393
-
394
- for (const row of rows) {
395
- lines.push(
396
- `| ${row.day} | ${(row.avg_risk as number).toFixed(1)} | ${row.max_risk} | ${row.files_scanned} |`
397
- );
398
- }
399
-
400
- return text(lines.join('\n'));
401
- }
402
-
403
- function text(content: string): ToolResult {
404
- return { content: [{ type: 'text', text: content }] };
405
- }
@@ -1,133 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { resolve, normalize } from 'path';
5
-
6
- // ============================================================
7
- // Shared Security Utilities
8
- // ============================================================
9
-
10
- /**
11
- * Ensure a resolved file path stays within the project root.
12
- * Prevents path traversal attacks via `../../etc/passwd` style inputs.
13
- *
14
- * @returns The resolved absolute path if safe
15
- * @throws Error if the path escapes the project root
16
- */
17
- export function ensureWithinRoot(filePath: string, projectRoot: string): string {
18
- const resolvedRoot = resolve(projectRoot);
19
- const resolvedPath = resolve(resolvedRoot, filePath);
20
- const normalizedPath = normalize(resolvedPath);
21
- const normalizedRoot = normalize(resolvedRoot);
22
-
23
- if (!normalizedPath.startsWith(normalizedRoot + '/') && normalizedPath !== normalizedRoot) {
24
- throw new Error(`Path traversal blocked: "${filePath}" resolves outside project root`);
25
- }
26
-
27
- return normalizedPath;
28
- }
29
-
30
- /**
31
- * Escape regex special characters in a string.
32
- * Use when building RegExp from user/config-provided strings.
33
- */
34
- export function escapeRegex(str: string): string {
35
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
36
- }
37
-
38
- /**
39
- * Validate that a regex pattern is safe to compile.
40
- * Rejects patterns likely to cause catastrophic backtracking (ReDoS).
41
- *
42
- * Checks:
43
- * 1. Pattern length limit (prevents memory exhaustion)
44
- * 2. Nested quantifiers (e.g., `(a+)+`, `(a*)*`, `(a+)*`)
45
- * 3. Tries compilation with a timeout guard
46
- *
47
- * @returns The compiled RegExp if safe, or null if rejected
48
- */
49
- export function safeRegex(pattern: string, flags?: string): RegExp | null {
50
- // Length guard
51
- if (pattern.length > 500) return null;
52
-
53
- // Detect nested quantifiers - the primary cause of ReDoS
54
- // Matches patterns like (X+)+, (X*)+, (X+)*, (X{n,})+, etc.
55
- if (/(\([^)]*[+*}][^)]*\))[+*{]/.test(pattern)) return null;
56
-
57
- // Detect alternation with overlapping repetition: (a|a)+
58
- // This is harder to detect perfectly, so we use a conservative heuristic
59
- if (/\([^)]*\|[^)]*\)[+*]{1,2}/.test(pattern) && pattern.length > 100) return null;
60
-
61
- try {
62
- return new RegExp(pattern, flags);
63
- } catch {
64
- return null;
65
- }
66
- }
67
-
68
- /**
69
- * Convert a glob pattern to a safe regex.
70
- * Unlike naive star-replacement, this escapes all other special chars
71
- * first and uses non-greedy matching to prevent backtracking.
72
- */
73
- export function globToSafeRegex(glob: string): RegExp {
74
- // Escape everything except * and **
75
- const escaped = glob
76
- .split('**')
77
- .map(segment =>
78
- segment
79
- .split('*')
80
- .map(part => escapeRegex(part))
81
- .join('[^/]*')
82
- )
83
- .join('.*');
84
-
85
- return new RegExp(`^${escaped}$`);
86
- }
87
-
88
- /**
89
- * Redact sensitive patterns from text before storage.
90
- * Replaces URLs, email addresses, API keys, tokens, and file paths
91
- * with redaction placeholders.
92
- */
93
- export function redactSensitiveContent(text: string): string {
94
- return text
95
- // API keys / tokens (sk-..., ghp_..., xoxb-..., etc.)
96
- .replace(/\b(sk-|ghp_|gho_|xoxb-|xoxp-|AKIA)[A-Za-z0-9_-]{10,}\b/g, '[REDACTED_KEY]')
97
- // Bearer tokens
98
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, 'Bearer [REDACTED_TOKEN]')
99
- // Connection strings with passwords (must run BEFORE email to avoid partial match)
100
- .replace(/:\/\/[^:]+:[^@\s]+@/g, '://[REDACTED_CREDENTIALS]@')
101
- // URLs with auth tokens in query params
102
- .replace(/(https?:\/\/[^\s]+[?&](?:token|key|secret|password|auth)=)[^\s&]*/gi, '$1[REDACTED]')
103
- // Email addresses
104
- .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]')
105
- // Absolute file paths (preserve relative)
106
- .replace(/(?:\/Users\/|\/home\/|C:\\Users\\)[^\s"'`]+/g, '[REDACTED_PATH]');
107
- }
108
-
109
- /** Minimum severity weight floors to prevent disabling security scoring via config. */
110
- export const MINIMUM_SEVERITY_WEIGHTS: Record<string, number> = {
111
- critical: 10,
112
- high: 5,
113
- medium: 2,
114
- low: 1,
115
- };
116
-
117
- /**
118
- * Merge config severity weights with minimum floors.
119
- * Prevents configs from zeroing out severity weights to disable scoring.
120
- */
121
- export function enforceSeverityFloors(
122
- configWeights: Record<string, number>,
123
- defaults: Record<string, number>
124
- ): Record<string, number> {
125
- const result: Record<string, number> = { ...defaults };
126
-
127
- for (const [severity, configValue] of Object.entries(configWeights)) {
128
- const floor = MINIMUM_SEVERITY_WEIGHTS[severity] ?? 1;
129
- result[severity] = Math.max(configValue, floor);
130
- }
131
-
132
- return result;
133
- }