@way_marks/server 0.9.0 → 2.0.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/dist/api/server.js +598 -0
- package/dist/approval/manager.js +249 -0
- package/dist/db/database.js +754 -185
- package/dist/escalation/manager.js +205 -0
- package/dist/notifications/service.js +457 -0
- package/dist/policies/engine.js +11 -5
- package/dist/policy/engine.js +420 -0
- package/dist/remediation/recommender.js +309 -0
- package/dist/risk/analyzer.js +453 -0
- package/dist/rollback/blocker.js +222 -0
- package/dist/rollback/manager.js +245 -0
- package/package.json +1 -1
- package/src/ui/index.html +862 -0
- package/dist/approvals/handler.test.js +0 -172
- package/dist/policies/engine.test.js +0 -241
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Risk Assessment Engine — Phase 4A
|
|
4
|
+
*
|
|
5
|
+
* Evaluates rollback safety based on:
|
|
6
|
+
* - Operation type risk (what tools were used)
|
|
7
|
+
* - Scale risk (how many actions)
|
|
8
|
+
* - Error pattern risk (what errors occurred)
|
|
9
|
+
* - Time risk (how old are the actions)
|
|
10
|
+
* - System state risk (current system load)
|
|
11
|
+
*
|
|
12
|
+
* Produces 0-10 risk score and safety level:
|
|
13
|
+
* - NONE (0)
|
|
14
|
+
* - LOW (1-2)
|
|
15
|
+
* - MEDIUM (3-4)
|
|
16
|
+
* - HIGH (5-7)
|
|
17
|
+
* - CRITICAL (8+)
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.assessRisk = assessRisk;
|
|
21
|
+
exports.calculateOperationTypeRisk = calculateOperationTypeRisk;
|
|
22
|
+
exports.calculateScaleRisk = calculateScaleRisk;
|
|
23
|
+
exports.calculateErrorPatternRisk = calculateErrorPatternRisk;
|
|
24
|
+
exports.calculateTimeRisk = calculateTimeRisk;
|
|
25
|
+
exports.calculateSystemStateRisk = calculateSystemStateRisk;
|
|
26
|
+
exports.getRiskLevel = getRiskLevel;
|
|
27
|
+
exports.generateRecommendations = generateRecommendations;
|
|
28
|
+
exports.shouldAutoBlock = shouldAutoBlock;
|
|
29
|
+
exports.getRiskSummary = getRiskSummary;
|
|
30
|
+
/**
|
|
31
|
+
* Assess overall risk of a session rollback
|
|
32
|
+
*/
|
|
33
|
+
function assessRisk(session, actions, systemState) {
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
const factors = [];
|
|
36
|
+
// 1. Operation type risk
|
|
37
|
+
const operationRisk = calculateOperationTypeRisk(actions);
|
|
38
|
+
factors.push({
|
|
39
|
+
category: 'operation_type',
|
|
40
|
+
weight: operationRisk.weight,
|
|
41
|
+
reason: operationRisk.reason,
|
|
42
|
+
sub_factors: operationRisk.sub_factors,
|
|
43
|
+
});
|
|
44
|
+
// 2. Scale risk
|
|
45
|
+
const scaleRisk = calculateScaleRisk(actions);
|
|
46
|
+
factors.push({
|
|
47
|
+
category: 'scale',
|
|
48
|
+
weight: scaleRisk.weight,
|
|
49
|
+
reason: scaleRisk.reason,
|
|
50
|
+
});
|
|
51
|
+
// 3. Error pattern risk
|
|
52
|
+
const errorRisk = calculateErrorPatternRisk(actions);
|
|
53
|
+
factors.push({
|
|
54
|
+
category: 'error_pattern',
|
|
55
|
+
weight: errorRisk.weight,
|
|
56
|
+
reason: errorRisk.reason,
|
|
57
|
+
sub_factors: errorRisk.sub_factors,
|
|
58
|
+
});
|
|
59
|
+
// 4. Time risk
|
|
60
|
+
const timeRisk = calculateTimeRisk(session);
|
|
61
|
+
factors.push({
|
|
62
|
+
category: 'time',
|
|
63
|
+
weight: timeRisk.weight,
|
|
64
|
+
reason: timeRisk.reason,
|
|
65
|
+
});
|
|
66
|
+
// 5. System state risk
|
|
67
|
+
const systemRisk = calculateSystemStateRisk(systemState);
|
|
68
|
+
factors.push({
|
|
69
|
+
category: 'system_state',
|
|
70
|
+
weight: systemRisk.weight,
|
|
71
|
+
reason: systemRisk.reason,
|
|
72
|
+
});
|
|
73
|
+
// Calculate overall score: map total weight (0-15) to score (0-10)
|
|
74
|
+
// Using divisor of 10.4 for balanced risk scaling
|
|
75
|
+
// Maps: low (3)→2.9, medium (4.3)→4.1, high (5.1)→4.9, critical (10.4)→10
|
|
76
|
+
const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
|
|
77
|
+
const score = Math.min(10, (totalWeight / 10.4) * 10);
|
|
78
|
+
// Determine risk level
|
|
79
|
+
const level = getRiskLevel(score);
|
|
80
|
+
// Generate recommendations
|
|
81
|
+
const recommendations = generateRecommendations(score, level, factors, actions);
|
|
82
|
+
return {
|
|
83
|
+
score,
|
|
84
|
+
level,
|
|
85
|
+
factors,
|
|
86
|
+
recommendations,
|
|
87
|
+
timestamp,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Calculate operation type risk
|
|
92
|
+
* High-risk tools: write_file, delete_file, bash, api_call with mutations
|
|
93
|
+
* Medium-risk: read operations that require consistency
|
|
94
|
+
* Low-risk: information gathering, metadata operations
|
|
95
|
+
*/
|
|
96
|
+
function calculateOperationTypeRisk(actions) {
|
|
97
|
+
const toolRisks = {
|
|
98
|
+
// HIGH RISK (2.0-2.5 points) - data modification/deletion
|
|
99
|
+
delete_file: 2.5,
|
|
100
|
+
bash: 2.0,
|
|
101
|
+
rmdir: 2.0,
|
|
102
|
+
// MEDIUM RISK (1.5-1.8 points) - write operations
|
|
103
|
+
write_file: 1.8,
|
|
104
|
+
mkdir: 1.5,
|
|
105
|
+
api_call: 1.5, // Depends on mutation type
|
|
106
|
+
// LOW RISK (0 points) - read-only
|
|
107
|
+
read_file: 0,
|
|
108
|
+
find_files: 0,
|
|
109
|
+
grep: 0,
|
|
110
|
+
};
|
|
111
|
+
const sub_factors = [];
|
|
112
|
+
let totalRisk = 0;
|
|
113
|
+
let highRiskCount = 0;
|
|
114
|
+
let deleteCount = 0;
|
|
115
|
+
let writeCount = 0;
|
|
116
|
+
let bashCount = 0;
|
|
117
|
+
for (const action of actions) {
|
|
118
|
+
const risk = toolRisks[action.tool_name] ?? 0.5; // Default medium for unknown tools
|
|
119
|
+
totalRisk += risk;
|
|
120
|
+
if (risk >= 2.0)
|
|
121
|
+
highRiskCount++;
|
|
122
|
+
if (action.tool_name === 'delete_file')
|
|
123
|
+
deleteCount++;
|
|
124
|
+
if (action.tool_name === 'write_file')
|
|
125
|
+
writeCount++;
|
|
126
|
+
if (action.tool_name === 'bash')
|
|
127
|
+
bashCount++;
|
|
128
|
+
sub_factors.push({
|
|
129
|
+
category: action.tool_name,
|
|
130
|
+
weight: risk,
|
|
131
|
+
reason: `Tool: ${action.tool_name}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const avgToolRisk = actions.length > 0 ? totalRisk / actions.length : 0;
|
|
135
|
+
const weight = Math.min(3, avgToolRisk);
|
|
136
|
+
let reason = `Average tool risk: ${avgToolRisk.toFixed(1)}/3.0`;
|
|
137
|
+
if (deleteCount > 0)
|
|
138
|
+
reason += ` (${deleteCount} delete operations)`;
|
|
139
|
+
if (writeCount > 0)
|
|
140
|
+
reason += ` (${writeCount} write operations)`;
|
|
141
|
+
if (bashCount > 0)
|
|
142
|
+
reason += ` (${bashCount} bash commands)`;
|
|
143
|
+
if (deleteCount === 0 && writeCount === 0 && bashCount === 0) {
|
|
144
|
+
reason += ' - read-only operations';
|
|
145
|
+
}
|
|
146
|
+
if (highRiskCount > actions.length / 2)
|
|
147
|
+
reason += ' - majority high-risk operations';
|
|
148
|
+
return { weight, reason, sub_factors };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Calculate scale risk based on number of actions
|
|
152
|
+
* 1 file = 0 risk
|
|
153
|
+
* 2-5 files = 0.5 risk
|
|
154
|
+
* 6-20 files = 1.5 risk
|
|
155
|
+
* 20+ files = 3 risk
|
|
156
|
+
*/
|
|
157
|
+
function calculateScaleRisk(actions) {
|
|
158
|
+
const count = actions.length;
|
|
159
|
+
let weight = 0;
|
|
160
|
+
if (count <= 1) {
|
|
161
|
+
weight = 0;
|
|
162
|
+
}
|
|
163
|
+
else if (count <= 5) {
|
|
164
|
+
weight = 0.5;
|
|
165
|
+
}
|
|
166
|
+
else if (count <= 20) {
|
|
167
|
+
weight = 1.5;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
weight = 3.0;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
weight,
|
|
174
|
+
reason: `${count} action(s) - ${count <= 1
|
|
175
|
+
? 'single operation'
|
|
176
|
+
: count <= 5
|
|
177
|
+
? 'small batch'
|
|
178
|
+
: count <= 20
|
|
179
|
+
? 'large batch'
|
|
180
|
+
: 'very large batch'}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Calculate error pattern risk
|
|
185
|
+
* No errors = 0
|
|
186
|
+
* Transient errors (timeout, network) = 0.5
|
|
187
|
+
* Permission errors = 1.5
|
|
188
|
+
* Data errors (validation, format) = 2.0
|
|
189
|
+
* System errors (crash, critical) = 3.0
|
|
190
|
+
*/
|
|
191
|
+
function calculateErrorPatternRisk(actions) {
|
|
192
|
+
const sub_factors = [];
|
|
193
|
+
const errorActions = actions.filter(a => a.status === 'error');
|
|
194
|
+
if (errorActions.length === 0) {
|
|
195
|
+
return {
|
|
196
|
+
weight: 0,
|
|
197
|
+
reason: 'No errors - all operations successful',
|
|
198
|
+
sub_factors: [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
let totalErrorRisk = 0;
|
|
202
|
+
let transientCount = 0;
|
|
203
|
+
let permissionCount = 0;
|
|
204
|
+
let dataCount = 0;
|
|
205
|
+
let systemCount = 0;
|
|
206
|
+
for (const action of errorActions) {
|
|
207
|
+
const errorMsg = (action.error_message || '').toLowerCase();
|
|
208
|
+
let errorRisk = 0.5; // Default: transient
|
|
209
|
+
if (errorMsg.includes('permission') ||
|
|
210
|
+
errorMsg.includes('denied') ||
|
|
211
|
+
errorMsg.includes('unauthorized')) {
|
|
212
|
+
errorRisk = 1.5;
|
|
213
|
+
permissionCount++;
|
|
214
|
+
}
|
|
215
|
+
else if (errorMsg.includes('validation') ||
|
|
216
|
+
errorMsg.includes('format') ||
|
|
217
|
+
errorMsg.includes('invalid')) {
|
|
218
|
+
errorRisk = 2.0;
|
|
219
|
+
dataCount++;
|
|
220
|
+
}
|
|
221
|
+
else if (errorMsg.includes('crash') ||
|
|
222
|
+
errorMsg.includes('segfault') ||
|
|
223
|
+
errorMsg.includes('segmentation') ||
|
|
224
|
+
errorMsg.includes('fatal') ||
|
|
225
|
+
errorMsg.includes('panic')) {
|
|
226
|
+
errorRisk = 3.0;
|
|
227
|
+
systemCount++;
|
|
228
|
+
}
|
|
229
|
+
else if (errorMsg.includes('timeout') || errorMsg.includes('network')) {
|
|
230
|
+
errorRisk = 0.5;
|
|
231
|
+
transientCount++;
|
|
232
|
+
}
|
|
233
|
+
totalErrorRisk += errorRisk;
|
|
234
|
+
sub_factors.push({
|
|
235
|
+
category: `error_${action.action_id}`,
|
|
236
|
+
weight: errorRisk,
|
|
237
|
+
reason: `${action.tool_name}: ${action.error_message?.substring(0, 50) || 'unknown error'}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const avgErrorRisk = totalErrorRisk / errorActions.length;
|
|
241
|
+
const weight = Math.min(3, avgErrorRisk);
|
|
242
|
+
let reason = `${errorActions.length} error(s) - average risk: ${avgErrorRisk.toFixed(1)}/3.0`;
|
|
243
|
+
if (systemCount > 0)
|
|
244
|
+
reason += ` (${systemCount} system errors)`;
|
|
245
|
+
if (dataCount > 0)
|
|
246
|
+
reason += ` (${dataCount} data errors)`;
|
|
247
|
+
if (permissionCount > 0)
|
|
248
|
+
reason += ` (${permissionCount} permission errors)`;
|
|
249
|
+
if (transientCount > 0)
|
|
250
|
+
reason += ` (${transientCount} transient errors)`;
|
|
251
|
+
return { weight, reason, sub_factors };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Calculate time risk based on how old actions are
|
|
255
|
+
* <5 minutes = 0
|
|
256
|
+
* 5-60 minutes = 0.5
|
|
257
|
+
* 1-6 hours = 1.5
|
|
258
|
+
* 6-24 hours = 2.0
|
|
259
|
+
* 24+ hours = 3.0
|
|
260
|
+
*/
|
|
261
|
+
function calculateTimeRisk(session) {
|
|
262
|
+
const createdAt = new Date(session.created_at);
|
|
263
|
+
const now = new Date();
|
|
264
|
+
const ageMs = now.getTime() - createdAt.getTime();
|
|
265
|
+
const ageMinutes = ageMs / (1000 * 60);
|
|
266
|
+
const ageHours = ageMinutes / 60;
|
|
267
|
+
const ageDays = ageHours / 24;
|
|
268
|
+
let weight = 0;
|
|
269
|
+
let ageStr = '';
|
|
270
|
+
if (ageMinutes < 5) {
|
|
271
|
+
weight = 0;
|
|
272
|
+
ageStr = `${Math.round(ageMinutes)}m ago - very fresh`;
|
|
273
|
+
}
|
|
274
|
+
else if (ageMinutes < 60) {
|
|
275
|
+
weight = 0.5;
|
|
276
|
+
ageStr = `${Math.round(ageMinutes)}m ago - recent`;
|
|
277
|
+
}
|
|
278
|
+
else if (ageHours < 6) {
|
|
279
|
+
weight = 1.5;
|
|
280
|
+
ageStr = `${Math.round(ageHours)}h ago - older`;
|
|
281
|
+
}
|
|
282
|
+
else if (ageHours < 24) {
|
|
283
|
+
weight = 2.0;
|
|
284
|
+
ageStr = `${Math.round(ageHours)}h ago - very old`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
weight = 3.0;
|
|
288
|
+
ageStr = `${Math.round(ageDays)}d ago - ancient`;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
weight,
|
|
292
|
+
reason: ageStr,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Calculate system state risk
|
|
297
|
+
* Low load (cpu <50%, memory <50%) = 0
|
|
298
|
+
* Moderate (cpu 50-75%, memory 50-75%) = 1.0
|
|
299
|
+
* High (cpu 75-90%, memory 75-90%) = 2.0
|
|
300
|
+
* Critical (cpu >90%, memory >90%) = 3.0
|
|
301
|
+
*/
|
|
302
|
+
function calculateSystemStateRisk(systemState) {
|
|
303
|
+
// Default: assume moderate load if no system state provided
|
|
304
|
+
if (!systemState) {
|
|
305
|
+
return {
|
|
306
|
+
weight: 1.0,
|
|
307
|
+
reason: 'System state unknown - assuming moderate load',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const avgLoad = (systemState.cpu_usage + systemState.memory_usage) / 2;
|
|
311
|
+
let weight = 0;
|
|
312
|
+
let description = '';
|
|
313
|
+
if (avgLoad < 50) {
|
|
314
|
+
weight = 0;
|
|
315
|
+
description = `Idle system (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
|
|
316
|
+
}
|
|
317
|
+
else if (avgLoad < 75) {
|
|
318
|
+
weight = 1.0;
|
|
319
|
+
description = `Moderate load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
|
|
320
|
+
}
|
|
321
|
+
else if (avgLoad < 90) {
|
|
322
|
+
weight = 2.0;
|
|
323
|
+
description = `High load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
weight = 3.0;
|
|
327
|
+
description = `Critical load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
|
|
328
|
+
}
|
|
329
|
+
if (systemState.active_users > 50) {
|
|
330
|
+
description += ` - ${systemState.active_users} active users`;
|
|
331
|
+
}
|
|
332
|
+
// Boost weight for very high request rates
|
|
333
|
+
if (systemState.request_rate > 1500) {
|
|
334
|
+
weight = Math.min(3, weight + 1.2);
|
|
335
|
+
description += ` - very high request rate (${systemState.request_rate} req/s)`;
|
|
336
|
+
}
|
|
337
|
+
else if (systemState.request_rate > 1000) {
|
|
338
|
+
weight = Math.min(3, weight + 0.5);
|
|
339
|
+
description += ` - high request rate (${systemState.request_rate} req/s)`;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
weight,
|
|
343
|
+
reason: description,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Determine risk level from score
|
|
348
|
+
*/
|
|
349
|
+
function getRiskLevel(score) {
|
|
350
|
+
if (score < 1)
|
|
351
|
+
return 'none';
|
|
352
|
+
if (score < 3)
|
|
353
|
+
return 'low';
|
|
354
|
+
if (score < 4.9)
|
|
355
|
+
return 'medium';
|
|
356
|
+
if (score < 8)
|
|
357
|
+
return 'high';
|
|
358
|
+
return 'critical';
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Generate recommendations based on risk assessment
|
|
362
|
+
*/
|
|
363
|
+
function generateRecommendations(score, level, factors, actions) {
|
|
364
|
+
const recommendations = [];
|
|
365
|
+
// Score-based recommendations
|
|
366
|
+
if (score < 2) {
|
|
367
|
+
recommendations.push('Low risk: Safe to proceed with rollback');
|
|
368
|
+
}
|
|
369
|
+
else if (score < 4) {
|
|
370
|
+
recommendations.push('Moderate risk: Recommend approval from team lead');
|
|
371
|
+
}
|
|
372
|
+
else if (score < 7) {
|
|
373
|
+
recommendations.push('High risk: Requires escalation to manager');
|
|
374
|
+
recommendations.push('Consider partial rollback of safe operations only');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
recommendations.push('Critical risk: Requires executive approval');
|
|
378
|
+
recommendations.push('Recommend staged rollback with verification');
|
|
379
|
+
recommendations.push('Consider manual remediation instead of full rollback');
|
|
380
|
+
}
|
|
381
|
+
// Factor-specific recommendations
|
|
382
|
+
const operationFactor = factors.find(f => f.category === 'operation_type');
|
|
383
|
+
if (operationFactor && operationFactor.weight > 1.5) {
|
|
384
|
+
recommendations.push('Identify safe read-only operations for selective rollback');
|
|
385
|
+
}
|
|
386
|
+
const scaleFactor = factors.find(f => f.category === 'scale');
|
|
387
|
+
if (scaleFactor && scaleFactor.weight > 1.5) {
|
|
388
|
+
recommendations.push('Consider staged rollback due to large action count');
|
|
389
|
+
}
|
|
390
|
+
const errorFactor = factors.find(f => f.category === 'error_pattern');
|
|
391
|
+
if (errorFactor && errorFactor.weight >= 1.5) {
|
|
392
|
+
recommendations.push('Multiple errors detected: review each error before rollback');
|
|
393
|
+
}
|
|
394
|
+
const timeFactor = factors.find(f => f.category === 'time');
|
|
395
|
+
if (timeFactor && timeFactor.weight > 1.5) {
|
|
396
|
+
recommendations.push('Operations are old: ensure no dependencies have changed');
|
|
397
|
+
}
|
|
398
|
+
const systemFactor = factors.find(f => f.category === 'system_state');
|
|
399
|
+
if (systemFactor && systemFactor.weight > 1.5) {
|
|
400
|
+
recommendations.push('System under high load: consider scheduling rollback during off-peak');
|
|
401
|
+
}
|
|
402
|
+
// Check for specific patterns
|
|
403
|
+
const deleteCount = actions.filter(a => a.tool_name === 'delete_file').length;
|
|
404
|
+
if (deleteCount > 0) {
|
|
405
|
+
recommendations.push(`${deleteCount} delete operation(s): verify backups before proceeding`);
|
|
406
|
+
}
|
|
407
|
+
const writeCount = actions.filter(a => a.tool_name === 'write_file').length;
|
|
408
|
+
if (writeCount > 5) {
|
|
409
|
+
recommendations.push('Multiple write operations: consider data consistency checks');
|
|
410
|
+
}
|
|
411
|
+
return recommendations;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if risk assessment should auto-block rollback
|
|
415
|
+
* Auto-block threshold: configurable, default 7.0
|
|
416
|
+
*/
|
|
417
|
+
function shouldAutoBlock(score, threshold = 7.0) {
|
|
418
|
+
return score >= threshold;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get human-readable risk summary
|
|
422
|
+
*/
|
|
423
|
+
function getRiskSummary(assessment) {
|
|
424
|
+
const score = assessment.score.toFixed(1);
|
|
425
|
+
const level = assessment.level.toUpperCase();
|
|
426
|
+
let summary = `Risk Level: ${level} (${score}/10)\n`;
|
|
427
|
+
summary += '\nRisk Factors:\n';
|
|
428
|
+
for (const factor of assessment.factors) {
|
|
429
|
+
summary += ` • ${factor.category}: ${factor.weight.toFixed(1)}/3.0 - ${factor.reason}\n`;
|
|
430
|
+
}
|
|
431
|
+
if (assessment.blocked_reason) {
|
|
432
|
+
summary += `\n⚠️ AUTO-BLOCKED: ${assessment.blocked_reason}\n`;
|
|
433
|
+
}
|
|
434
|
+
if (assessment.recommendations.length > 0) {
|
|
435
|
+
summary += '\nRecommendations:\n';
|
|
436
|
+
for (const rec of assessment.recommendations) {
|
|
437
|
+
summary += ` → ${rec}\n`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return summary;
|
|
441
|
+
}
|
|
442
|
+
exports.default = {
|
|
443
|
+
assessRisk,
|
|
444
|
+
calculateOperationTypeRisk,
|
|
445
|
+
calculateScaleRisk,
|
|
446
|
+
calculateErrorPatternRisk,
|
|
447
|
+
calculateTimeRisk,
|
|
448
|
+
calculateSystemStateRisk,
|
|
449
|
+
getRiskLevel,
|
|
450
|
+
generateRecommendations,
|
|
451
|
+
shouldAutoBlock,
|
|
452
|
+
getRiskSummary,
|
|
453
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-Block Rules Engine — Phase 4D
|
|
4
|
+
*
|
|
5
|
+
* Automatically blocks risky rollbacks based on:
|
|
6
|
+
* - Risk assessment score
|
|
7
|
+
* - Policy violations
|
|
8
|
+
* - Configured block rules
|
|
9
|
+
*
|
|
10
|
+
* Prevents rollback of:
|
|
11
|
+
* - High-risk operations (score > threshold)
|
|
12
|
+
* - Policy-violating operations
|
|
13
|
+
* - Manual block rules
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.evaluateAutoBlock = evaluateAutoBlock;
|
|
17
|
+
exports.createBlock = createBlock;
|
|
18
|
+
exports.unblockSession = unblockSession;
|
|
19
|
+
exports.getDefaultBlockRules = getDefaultBlockRules;
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate if rollback should be auto-blocked
|
|
22
|
+
*/
|
|
23
|
+
function evaluateAutoBlock(session, actions, riskAssessment, policyViolations, blockRules, riskThreshold = 7.0, userRole = 'user') {
|
|
24
|
+
const blockingRules = [];
|
|
25
|
+
const blockedAt = new Date().toISOString();
|
|
26
|
+
const blockId = generateBlockId();
|
|
27
|
+
// Check risk score threshold
|
|
28
|
+
const isHighRisk = riskAssessment.score >= riskThreshold;
|
|
29
|
+
// Check matching block rules
|
|
30
|
+
const matchingRules = blockRules.filter(rule => rule.enabled && evaluateBlockRuleCondition(rule.condition, actions, session));
|
|
31
|
+
blockingRules.push(...matchingRules);
|
|
32
|
+
// Check policy violations with block action
|
|
33
|
+
const blockingViolations = policyViolations.filter(v => v.action === 'block');
|
|
34
|
+
// Determine if blocked
|
|
35
|
+
const hasBlockingViolations = blockingViolations.length > 0;
|
|
36
|
+
const blocked = isHighRisk || blockingRules.length > 0 || hasBlockingViolations;
|
|
37
|
+
// Generate reason
|
|
38
|
+
let reason = '';
|
|
39
|
+
const reasons = [];
|
|
40
|
+
if (isHighRisk) {
|
|
41
|
+
reasons.push(`Risk score ${riskAssessment.score.toFixed(1)}/10 exceeds threshold ${riskThreshold}`);
|
|
42
|
+
}
|
|
43
|
+
if (blockingRules.length > 0) {
|
|
44
|
+
reasons.push(`${blockingRules.length} block rule(s) matched`);
|
|
45
|
+
}
|
|
46
|
+
if (hasBlockingViolations) {
|
|
47
|
+
reasons.push(`${blockingViolations.length} policy violation(s) require blocking`);
|
|
48
|
+
}
|
|
49
|
+
reason = reasons.join('; ');
|
|
50
|
+
// Determine override capability
|
|
51
|
+
const canOverride = userRole === 'admin' || userRole === 'super_admin';
|
|
52
|
+
const overrideRole = userRole === 'admin' || userRole === 'super_admin' ? 'admin' : 'admin';
|
|
53
|
+
const result = {
|
|
54
|
+
blocked,
|
|
55
|
+
reason: blocked ? reason : undefined,
|
|
56
|
+
blocking_rules: blockingRules,
|
|
57
|
+
policy_violations: blockingViolations,
|
|
58
|
+
risk_score: riskAssessment.score,
|
|
59
|
+
can_override: canOverride,
|
|
60
|
+
override_required_role: overrideRole,
|
|
61
|
+
};
|
|
62
|
+
if (blocked) {
|
|
63
|
+
result.block_id = blockId;
|
|
64
|
+
result.blocked_at = blockedAt;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Evaluate block rule condition
|
|
70
|
+
*/
|
|
71
|
+
function evaluateBlockRuleCondition(condition, actions, session) {
|
|
72
|
+
// Use same logic as policy conditions
|
|
73
|
+
// Check if any action matches
|
|
74
|
+
for (const action of actions) {
|
|
75
|
+
if (matchCondition(condition, action, session)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Simple condition matcher
|
|
83
|
+
*/
|
|
84
|
+
function matchCondition(condition, action, session) {
|
|
85
|
+
switch (condition.type) {
|
|
86
|
+
case 'operation_type':
|
|
87
|
+
case 'tool_name':
|
|
88
|
+
return matchString(action.tool_name, condition.value, condition.operator);
|
|
89
|
+
case 'action_count':
|
|
90
|
+
return matchNumeric(session.action_count, condition.value, condition.operator);
|
|
91
|
+
case 'file_pattern':
|
|
92
|
+
const target = action.target || '';
|
|
93
|
+
return matchString(target, condition.value, condition.operator);
|
|
94
|
+
case 'error_present':
|
|
95
|
+
const hasError = action.status === 'error';
|
|
96
|
+
return hasError === (condition.value === 'true');
|
|
97
|
+
default:
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* String comparison
|
|
103
|
+
*/
|
|
104
|
+
function matchString(actual, expected, operator) {
|
|
105
|
+
if (operator === 'equals' || operator === 'in') {
|
|
106
|
+
if (Array.isArray(expected)) {
|
|
107
|
+
return expected.includes(actual);
|
|
108
|
+
}
|
|
109
|
+
return actual === String(expected);
|
|
110
|
+
}
|
|
111
|
+
if (operator === 'contains') {
|
|
112
|
+
if (Array.isArray(expected)) {
|
|
113
|
+
return expected.some(e => actual.includes(String(e)));
|
|
114
|
+
}
|
|
115
|
+
return actual.includes(String(expected));
|
|
116
|
+
}
|
|
117
|
+
if (operator === 'matches_regex') {
|
|
118
|
+
try {
|
|
119
|
+
const regex = new RegExp(String(expected));
|
|
120
|
+
return regex.test(actual);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Numeric comparison
|
|
130
|
+
*/
|
|
131
|
+
function matchNumeric(actual, expected, operator) {
|
|
132
|
+
const expectedNum = typeof expected === 'number' ? expected : parseInt(String(expected), 10);
|
|
133
|
+
if (isNaN(expectedNum))
|
|
134
|
+
return false;
|
|
135
|
+
switch (operator) {
|
|
136
|
+
case 'equals':
|
|
137
|
+
return actual === expectedNum;
|
|
138
|
+
case 'greater_than':
|
|
139
|
+
return actual > expectedNum;
|
|
140
|
+
case 'less_than':
|
|
141
|
+
return actual < expectedNum;
|
|
142
|
+
default:
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a block for a session
|
|
148
|
+
*/
|
|
149
|
+
function createBlock(sessionId, riskScore, reason, ruleId, policyViolationCount = 0) {
|
|
150
|
+
return {
|
|
151
|
+
block_id: generateBlockId(),
|
|
152
|
+
session_id: sessionId,
|
|
153
|
+
rule_id: ruleId,
|
|
154
|
+
policy_violation_count: policyViolationCount,
|
|
155
|
+
risk_score: riskScore,
|
|
156
|
+
reason,
|
|
157
|
+
blocked_at: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Unblock a session (admin override)
|
|
162
|
+
*/
|
|
163
|
+
function unblockSession(block, unblockedBy, reason, overrideToken) {
|
|
164
|
+
return {
|
|
165
|
+
...block,
|
|
166
|
+
unblocked_at: new Date().toISOString(),
|
|
167
|
+
unblocked_by: unblockedBy,
|
|
168
|
+
unblock_reason: reason,
|
|
169
|
+
override_token: overrideToken,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate unique block ID
|
|
174
|
+
*/
|
|
175
|
+
function generateBlockId() {
|
|
176
|
+
return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get default block rules
|
|
180
|
+
*/
|
|
181
|
+
function getDefaultBlockRules() {
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
rule_id: 'block-production-delete-hours',
|
|
185
|
+
name: 'Production Delete During Off-Hours',
|
|
186
|
+
description: 'Block delete operations in production environment during off-hours',
|
|
187
|
+
condition: {
|
|
188
|
+
type: 'operation_type',
|
|
189
|
+
operator: 'equals',
|
|
190
|
+
value: 'delete_file',
|
|
191
|
+
},
|
|
192
|
+
action: 'block',
|
|
193
|
+
severity: 'error',
|
|
194
|
+
message: 'Cannot delete files during off-hours in production',
|
|
195
|
+
enabled: true,
|
|
196
|
+
created_at: new Date().toISOString(),
|
|
197
|
+
created_by: 'system',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
rule_id: 'block-large-batch-delete',
|
|
201
|
+
name: 'Large Batch Delete Protection',
|
|
202
|
+
description: 'Block deletion of more than 20 files in single session',
|
|
203
|
+
condition: {
|
|
204
|
+
type: 'action_count',
|
|
205
|
+
operator: 'greater_than',
|
|
206
|
+
value: 20,
|
|
207
|
+
},
|
|
208
|
+
action: 'require_approval',
|
|
209
|
+
severity: 'warning',
|
|
210
|
+
message: 'Large batch deletion requires approval',
|
|
211
|
+
enabled: true,
|
|
212
|
+
created_at: new Date().toISOString(),
|
|
213
|
+
created_by: 'system',
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
exports.default = {
|
|
218
|
+
evaluateAutoBlock,
|
|
219
|
+
createBlock,
|
|
220
|
+
unblockSession,
|
|
221
|
+
getDefaultBlockRules,
|
|
222
|
+
};
|