@vibecheckai/cli 3.2.6 → 3.4.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/bin/registry.js +306 -90
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +136 -141
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/entitlements-v2.js +96 -505
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/scan-output.js +18 -19
- package/bin/runners/lib/ship-output.js +18 -25
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +105 -205
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +77 -45
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +103 -21
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1152 -856
- package/mcp-server/lib/api-client.cjs +13 -0
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +194 -383
- package/mcp-server/tools-v3.js +495 -533
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/lib/api-client.js +0 -269
- package/mcp-server/package-lock.json +0 -165
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Machine Timeline Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds event timelines from multiple sources.
|
|
5
|
+
* Correlates agent actions with outcomes and incidents.
|
|
6
|
+
*
|
|
7
|
+
* Codename: Time Machine
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const { timeMachineLogger: log, getErrorMessage } = require("../logger.js");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} TimelineEvent
|
|
18
|
+
* @property {string} id - Event ID
|
|
19
|
+
* @property {Date} timestamp - When event occurred
|
|
20
|
+
* @property {string} type - Event type
|
|
21
|
+
* @property {string} source - Event source (firewall, conductor, git, etc.)
|
|
22
|
+
* @property {string} summary - Human-readable summary
|
|
23
|
+
* @property {Object} details - Full event details
|
|
24
|
+
* @property {string[]} relatedEvents - IDs of related events
|
|
25
|
+
* @property {Object} [causalLink] - Link to caused/causing events
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ForensicTimeline
|
|
30
|
+
* @property {string} timelineId - Timeline ID
|
|
31
|
+
* @property {Object} incident - Incident info if applicable
|
|
32
|
+
* @property {TimelineEvent[]} events - All events in timeline
|
|
33
|
+
* @property {Object[]} causalChain - Causal relationships
|
|
34
|
+
* @property {TimelineEvent} [rootCause] - Identified root cause
|
|
35
|
+
* @property {Date} generatedAt - When timeline was generated
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Event types for timeline
|
|
40
|
+
*/
|
|
41
|
+
const EVENT_TYPES = {
|
|
42
|
+
PROPOSAL_SUBMITTED: "proposal_submitted",
|
|
43
|
+
PROPOSAL_ALLOWED: "proposal_allowed",
|
|
44
|
+
PROPOSAL_BLOCKED: "proposal_blocked",
|
|
45
|
+
PROPOSAL_WARNED: "proposal_warned",
|
|
46
|
+
OVERRIDE_USED: "override_used",
|
|
47
|
+
FILE_CHANGED: "file_changed",
|
|
48
|
+
SESSION_STARTED: "session_started",
|
|
49
|
+
SESSION_ENDED: "session_ended",
|
|
50
|
+
LOCK_ACQUIRED: "lock_acquired",
|
|
51
|
+
LOCK_RELEASED: "lock_released",
|
|
52
|
+
CONFLICT_DETECTED: "conflict_detected",
|
|
53
|
+
INCIDENT_REPORTED: "incident_reported",
|
|
54
|
+
BUILD_FAILED: "build_failed",
|
|
55
|
+
TEST_FAILED: "test_failed",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Timeline Builder class
|
|
60
|
+
*/
|
|
61
|
+
class TimelineBuilder {
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
64
|
+
this.auditDir = path.join(this.projectRoot, ".vibecheck", "audit");
|
|
65
|
+
this.packetsDir = path.join(this.projectRoot, ".vibecheck", "packets");
|
|
66
|
+
this.incidentsDir = path.join(this.projectRoot, ".vibecheck", "incidents");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a timeline for a time range
|
|
71
|
+
* @param {Object} options - Build options
|
|
72
|
+
* @returns {ForensicTimeline} Built timeline
|
|
73
|
+
*/
|
|
74
|
+
async buildTimeline(options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
startTime,
|
|
77
|
+
endTime = new Date(),
|
|
78
|
+
file = null,
|
|
79
|
+
agentId = null,
|
|
80
|
+
includeGit = true,
|
|
81
|
+
includeIncidents = true,
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const timelineId = `timeline_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
85
|
+
const events = [];
|
|
86
|
+
|
|
87
|
+
// Load firewall events
|
|
88
|
+
const firewallEvents = await this.loadFirewallEvents(startTime, endTime, file, agentId);
|
|
89
|
+
events.push(...firewallEvents);
|
|
90
|
+
|
|
91
|
+
// Load conductor events
|
|
92
|
+
const conductorEvents = await this.loadConductorEvents(startTime, endTime, agentId);
|
|
93
|
+
events.push(...conductorEvents);
|
|
94
|
+
|
|
95
|
+
// Load git events if requested
|
|
96
|
+
if (includeGit) {
|
|
97
|
+
const gitEvents = await this.loadGitEvents(startTime, endTime, file);
|
|
98
|
+
events.push(...gitEvents);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Load incident events if requested
|
|
102
|
+
let incidentInfo = null;
|
|
103
|
+
if (includeIncidents) {
|
|
104
|
+
const incidentEvents = await this.loadIncidentEvents(startTime, endTime, file);
|
|
105
|
+
events.push(...incidentEvents);
|
|
106
|
+
|
|
107
|
+
// Find the main incident if any
|
|
108
|
+
incidentInfo = incidentEvents.find(e => e.type === EVENT_TYPES.INCIDENT_REPORTED)?.details;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sort by timestamp
|
|
112
|
+
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
113
|
+
|
|
114
|
+
// Build causal relationships
|
|
115
|
+
const { causalChain, rootCause } = this.buildCausalChain(events);
|
|
116
|
+
|
|
117
|
+
// Link related events
|
|
118
|
+
this.linkRelatedEvents(events);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
timelineId,
|
|
122
|
+
incident: incidentInfo,
|
|
123
|
+
events,
|
|
124
|
+
causalChain,
|
|
125
|
+
rootCause,
|
|
126
|
+
generatedAt: new Date(),
|
|
127
|
+
options,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Load firewall events from audit log
|
|
133
|
+
* @param {Date} startTime - Start time
|
|
134
|
+
* @param {Date} endTime - End time
|
|
135
|
+
* @param {string} file - File filter
|
|
136
|
+
* @param {string} agentId - Agent filter
|
|
137
|
+
* @returns {TimelineEvent[]} Firewall events
|
|
138
|
+
*/
|
|
139
|
+
async loadFirewallEvents(startTime, endTime, file, agentId) {
|
|
140
|
+
const events = [];
|
|
141
|
+
const auditFile = path.join(this.auditDir, "firewall-events.jsonl");
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(auditFile)) {
|
|
144
|
+
return events;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const content = fs.readFileSync(auditFile, "utf-8");
|
|
149
|
+
const lines = content.trim().split("\n").filter(l => l);
|
|
150
|
+
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
try {
|
|
153
|
+
const raw = JSON.parse(line);
|
|
154
|
+
const eventTime = new Date(raw.timestamp);
|
|
155
|
+
|
|
156
|
+
// Apply filters
|
|
157
|
+
if (startTime && eventTime < new Date(startTime)) continue;
|
|
158
|
+
if (endTime && eventTime > new Date(endTime)) continue;
|
|
159
|
+
if (file && raw.file !== file && !raw.file?.includes(file)) continue;
|
|
160
|
+
if (agentId && raw.agentId !== agentId) continue;
|
|
161
|
+
|
|
162
|
+
events.push({
|
|
163
|
+
id: raw.id || `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
164
|
+
timestamp: eventTime,
|
|
165
|
+
type: this.mapVerdictToEventType(raw.verdict, raw.action),
|
|
166
|
+
source: "firewall",
|
|
167
|
+
summary: this.buildFirewallSummary(raw),
|
|
168
|
+
details: raw,
|
|
169
|
+
relatedEvents: [],
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
// Skip invalid lines
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
log.warn(`Failed to load firewall events: ${getErrorMessage(error)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return events;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Load conductor events
|
|
184
|
+
* @param {Date} startTime - Start time
|
|
185
|
+
* @param {Date} endTime - End time
|
|
186
|
+
* @param {string} agentId - Agent filter
|
|
187
|
+
* @returns {TimelineEvent[]} Conductor events
|
|
188
|
+
*/
|
|
189
|
+
async loadConductorEvents(startTime, endTime, agentId) {
|
|
190
|
+
const events = [];
|
|
191
|
+
const conductorFile = path.join(this.auditDir, "conductor-events.jsonl");
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(conductorFile)) {
|
|
194
|
+
return events;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const content = fs.readFileSync(conductorFile, "utf-8");
|
|
199
|
+
const lines = content.trim().split("\n").filter(l => l);
|
|
200
|
+
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
try {
|
|
203
|
+
const raw = JSON.parse(line);
|
|
204
|
+
const eventTime = new Date(raw.timestamp);
|
|
205
|
+
|
|
206
|
+
if (startTime && eventTime < new Date(startTime)) continue;
|
|
207
|
+
if (endTime && eventTime > new Date(endTime)) continue;
|
|
208
|
+
if (agentId && raw.agentId !== agentId) continue;
|
|
209
|
+
|
|
210
|
+
events.push({
|
|
211
|
+
id: raw.id || `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
212
|
+
timestamp: eventTime,
|
|
213
|
+
type: raw.type || raw.action,
|
|
214
|
+
source: "conductor",
|
|
215
|
+
summary: this.buildConductorSummary(raw),
|
|
216
|
+
details: raw,
|
|
217
|
+
relatedEvents: [],
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
// Skip invalid lines
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
log.warn(`Failed to load conductor events: ${getErrorMessage(error)}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return events;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load git events (commits, merges)
|
|
232
|
+
* @param {Date} startTime - Start time
|
|
233
|
+
* @param {Date} endTime - End time
|
|
234
|
+
* @param {string} file - File filter
|
|
235
|
+
* @returns {TimelineEvent[]} Git events
|
|
236
|
+
*/
|
|
237
|
+
async loadGitEvents(startTime, endTime, file) {
|
|
238
|
+
const events = [];
|
|
239
|
+
|
|
240
|
+
// This would typically call git log
|
|
241
|
+
// For now, return empty - would be implemented with git integration
|
|
242
|
+
|
|
243
|
+
return events;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Load incident events
|
|
248
|
+
* @param {Date} startTime - Start time
|
|
249
|
+
* @param {Date} endTime - End time
|
|
250
|
+
* @param {string} file - File filter
|
|
251
|
+
* @returns {TimelineEvent[]} Incident events
|
|
252
|
+
*/
|
|
253
|
+
async loadIncidentEvents(startTime, endTime, file) {
|
|
254
|
+
const events = [];
|
|
255
|
+
const incidentsFile = path.join(this.incidentsDir, "incidents.jsonl");
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(incidentsFile)) {
|
|
258
|
+
return events;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const content = fs.readFileSync(incidentsFile, "utf-8");
|
|
263
|
+
const lines = content.trim().split("\n").filter(l => l);
|
|
264
|
+
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
try {
|
|
267
|
+
const raw = JSON.parse(line);
|
|
268
|
+
const eventTime = new Date(raw.timestamp || raw.reportedAt);
|
|
269
|
+
|
|
270
|
+
if (startTime && eventTime < new Date(startTime)) continue;
|
|
271
|
+
if (endTime && eventTime > new Date(endTime)) continue;
|
|
272
|
+
if (file && !raw.affectedFiles?.includes(file)) continue;
|
|
273
|
+
|
|
274
|
+
events.push({
|
|
275
|
+
id: raw.id || raw.incidentId,
|
|
276
|
+
timestamp: eventTime,
|
|
277
|
+
type: EVENT_TYPES.INCIDENT_REPORTED,
|
|
278
|
+
source: "incident",
|
|
279
|
+
summary: `Incident: ${raw.title || raw.description || "Unknown"}`,
|
|
280
|
+
details: raw,
|
|
281
|
+
relatedEvents: [],
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
// Skip invalid lines
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
log.warn(`Failed to load incident events: ${getErrorMessage(error)}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return events;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Map verdict to event type
|
|
296
|
+
* @param {string} verdict - Verdict
|
|
297
|
+
* @param {string} action - Action
|
|
298
|
+
* @returns {string} Event type
|
|
299
|
+
*/
|
|
300
|
+
mapVerdictToEventType(verdict, action) {
|
|
301
|
+
if (action === "override") return EVENT_TYPES.OVERRIDE_USED;
|
|
302
|
+
|
|
303
|
+
switch (verdict) {
|
|
304
|
+
case "ALLOW":
|
|
305
|
+
return EVENT_TYPES.PROPOSAL_ALLOWED;
|
|
306
|
+
case "BLOCK":
|
|
307
|
+
return EVENT_TYPES.PROPOSAL_BLOCKED;
|
|
308
|
+
case "WARN":
|
|
309
|
+
return EVENT_TYPES.PROPOSAL_WARNED;
|
|
310
|
+
default:
|
|
311
|
+
return EVENT_TYPES.PROPOSAL_SUBMITTED;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Build summary for firewall event
|
|
317
|
+
* @param {Object} raw - Raw event data
|
|
318
|
+
* @returns {string} Summary
|
|
319
|
+
*/
|
|
320
|
+
buildFirewallSummary(raw) {
|
|
321
|
+
const parts = [];
|
|
322
|
+
|
|
323
|
+
if (raw.verdict) {
|
|
324
|
+
parts.push(`[${raw.verdict}]`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (raw.agentId) {
|
|
328
|
+
parts.push(`Agent: ${raw.agentId}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (raw.file) {
|
|
332
|
+
parts.push(`File: ${path.basename(raw.file)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (raw.intent) {
|
|
336
|
+
parts.push(raw.intent.slice(0, 50) + (raw.intent.length > 50 ? "..." : ""));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return parts.join(" | ") || "Firewall event";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build summary for conductor event
|
|
344
|
+
* @param {Object} raw - Raw event data
|
|
345
|
+
* @returns {string} Summary
|
|
346
|
+
*/
|
|
347
|
+
buildConductorSummary(raw) {
|
|
348
|
+
const parts = [];
|
|
349
|
+
|
|
350
|
+
if (raw.type || raw.action) {
|
|
351
|
+
parts.push(`[${raw.type || raw.action}]`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (raw.agentId) {
|
|
355
|
+
parts.push(`Agent: ${raw.agentId}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (raw.sessionId) {
|
|
359
|
+
parts.push(`Session: ${raw.sessionId.slice(0, 12)}...`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return parts.join(" | ") || "Conductor event";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Build causal chain from events
|
|
367
|
+
* @param {TimelineEvent[]} events - Events to analyze
|
|
368
|
+
* @returns {Object} Causal chain and root cause
|
|
369
|
+
*/
|
|
370
|
+
buildCausalChain(events) {
|
|
371
|
+
const causalChain = [];
|
|
372
|
+
let rootCause = null;
|
|
373
|
+
|
|
374
|
+
// Find incident events
|
|
375
|
+
const incidents = events.filter(e => e.type === EVENT_TYPES.INCIDENT_REPORTED);
|
|
376
|
+
|
|
377
|
+
if (incidents.length === 0) {
|
|
378
|
+
return { causalChain, rootCause };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// For each incident, trace back to find potential causes
|
|
382
|
+
for (const incident of incidents) {
|
|
383
|
+
const affectedFiles = incident.details.affectedFiles || [];
|
|
384
|
+
const incidentTime = new Date(incident.timestamp);
|
|
385
|
+
|
|
386
|
+
// Find events that affected the same files before the incident
|
|
387
|
+
const potentialCauses = events.filter(e => {
|
|
388
|
+
const eventTime = new Date(e.timestamp);
|
|
389
|
+
if (eventTime >= incidentTime) return false;
|
|
390
|
+
|
|
391
|
+
const eventFile = e.details?.file;
|
|
392
|
+
if (!eventFile) return false;
|
|
393
|
+
|
|
394
|
+
return affectedFiles.some(f => f.includes(eventFile) || eventFile.includes(f));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Look for overrides or blocked proposals that were overridden
|
|
398
|
+
for (const cause of potentialCauses) {
|
|
399
|
+
if (cause.type === EVENT_TYPES.OVERRIDE_USED ||
|
|
400
|
+
(cause.type === EVENT_TYPES.PROPOSAL_ALLOWED && cause.details?.overrideUsed)) {
|
|
401
|
+
causalChain.push({
|
|
402
|
+
cause: cause.id,
|
|
403
|
+
effect: incident.id,
|
|
404
|
+
relationship: "override_led_to_incident",
|
|
405
|
+
confidence: 0.8,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (!rootCause) {
|
|
409
|
+
rootCause = cause;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Also look for the first suspicious change
|
|
415
|
+
const firstSuspicious = potentialCauses.find(e =>
|
|
416
|
+
e.type === EVENT_TYPES.PROPOSAL_ALLOWED &&
|
|
417
|
+
(e.details?.riskScore || 0) >= 50
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (firstSuspicious && !rootCause) {
|
|
421
|
+
rootCause = firstSuspicious;
|
|
422
|
+
causalChain.push({
|
|
423
|
+
cause: firstSuspicious.id,
|
|
424
|
+
effect: incident.id,
|
|
425
|
+
relationship: "high_risk_change_led_to_incident",
|
|
426
|
+
confidence: 0.6,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { causalChain, rootCause };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Link related events together
|
|
436
|
+
* @param {TimelineEvent[]} events - Events to link
|
|
437
|
+
*/
|
|
438
|
+
linkRelatedEvents(events) {
|
|
439
|
+
// Group events by file
|
|
440
|
+
const byFile = new Map();
|
|
441
|
+
|
|
442
|
+
for (const event of events) {
|
|
443
|
+
const file = event.details?.file;
|
|
444
|
+
if (file) {
|
|
445
|
+
if (!byFile.has(file)) {
|
|
446
|
+
byFile.set(file, []);
|
|
447
|
+
}
|
|
448
|
+
byFile.get(file).push(event);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Link events that share the same file
|
|
453
|
+
for (const [, fileEvents] of byFile) {
|
|
454
|
+
for (let i = 0; i < fileEvents.length; i++) {
|
|
455
|
+
for (let j = i + 1; j < fileEvents.length; j++) {
|
|
456
|
+
fileEvents[i].relatedEvents.push(fileEvents[j].id);
|
|
457
|
+
fileEvents[j].relatedEvents.push(fileEvents[i].id);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Group events by session
|
|
463
|
+
const bySession = new Map();
|
|
464
|
+
|
|
465
|
+
for (const event of events) {
|
|
466
|
+
const sessionId = event.details?.sessionId;
|
|
467
|
+
if (sessionId) {
|
|
468
|
+
if (!bySession.has(sessionId)) {
|
|
469
|
+
bySession.set(sessionId, []);
|
|
470
|
+
}
|
|
471
|
+
bySession.get(sessionId).push(event);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Link events in same session
|
|
476
|
+
for (const [, sessionEvents] of bySession) {
|
|
477
|
+
for (let i = 0; i < sessionEvents.length; i++) {
|
|
478
|
+
for (let j = i + 1; j < sessionEvents.length; j++) {
|
|
479
|
+
if (!sessionEvents[i].relatedEvents.includes(sessionEvents[j].id)) {
|
|
480
|
+
sessionEvents[i].relatedEvents.push(sessionEvents[j].id);
|
|
481
|
+
}
|
|
482
|
+
if (!sessionEvents[j].relatedEvents.includes(sessionEvents[i].id)) {
|
|
483
|
+
sessionEvents[j].relatedEvents.push(sessionEvents[i].id);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Generate a timeline report
|
|
492
|
+
* @param {ForensicTimeline} timeline - Timeline to report on
|
|
493
|
+
* @returns {Object} Report
|
|
494
|
+
*/
|
|
495
|
+
generateReport(timeline) {
|
|
496
|
+
const eventCounts = {};
|
|
497
|
+
const sourceCounts = {};
|
|
498
|
+
|
|
499
|
+
for (const event of timeline.events) {
|
|
500
|
+
eventCounts[event.type] = (eventCounts[event.type] || 0) + 1;
|
|
501
|
+
sourceCounts[event.source] = (sourceCounts[event.source] || 0) + 1;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
timelineId: timeline.timelineId,
|
|
506
|
+
generatedAt: timeline.generatedAt,
|
|
507
|
+
totalEvents: timeline.events.length,
|
|
508
|
+
eventCounts,
|
|
509
|
+
sourceCounts,
|
|
510
|
+
hasIncident: !!timeline.incident,
|
|
511
|
+
rootCauseIdentified: !!timeline.rootCause,
|
|
512
|
+
causalChainLength: timeline.causalChain.length,
|
|
513
|
+
timeRange: {
|
|
514
|
+
start: timeline.events[0]?.timestamp,
|
|
515
|
+
end: timeline.events[timeline.events.length - 1]?.timestamp,
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Create a timeline builder instance
|
|
523
|
+
* @param {Object} options - Options
|
|
524
|
+
* @returns {TimelineBuilder} Timeline builder
|
|
525
|
+
*/
|
|
526
|
+
function createTimelineBuilder(options = {}) {
|
|
527
|
+
return new TimelineBuilder(options);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
module.exports = { TimelineBuilder, createTimelineBuilder, EVENT_TYPES };
|