@massu/core 1.10.7 → 1.12.0
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/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
package/src/security-scorer.ts
DELETED
|
@@ -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
|
-
}
|
package/src/security-utils.ts
DELETED
|
@@ -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
|
-
}
|