@panguard-ai/panguard-guard 0.1.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.
Files changed (130) hide show
  1. package/dist/agent/analyze-agent.d.ts +62 -0
  2. package/dist/agent/analyze-agent.d.ts.map +1 -0
  3. package/dist/agent/analyze-agent.js +327 -0
  4. package/dist/agent/analyze-agent.js.map +1 -0
  5. package/dist/agent/detect-agent.d.ts +59 -0
  6. package/dist/agent/detect-agent.d.ts.map +1 -0
  7. package/dist/agent/detect-agent.js +214 -0
  8. package/dist/agent/detect-agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +15 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +14 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/report-agent.d.ts +122 -0
  14. package/dist/agent/report-agent.d.ts.map +1 -0
  15. package/dist/agent/report-agent.js +468 -0
  16. package/dist/agent/report-agent.js.map +1 -0
  17. package/dist/agent/respond-agent.d.ts +113 -0
  18. package/dist/agent/respond-agent.d.ts.map +1 -0
  19. package/dist/agent/respond-agent.js +749 -0
  20. package/dist/agent/respond-agent.js.map +1 -0
  21. package/dist/agent-client/index.d.ts +81 -0
  22. package/dist/agent-client/index.d.ts.map +1 -0
  23. package/dist/agent-client/index.js +170 -0
  24. package/dist/agent-client/index.js.map +1 -0
  25. package/dist/cli/index.d.ts +17 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +295 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/config.d.ts +23 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +108 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/daemon/index.d.ts +66 -0
  34. package/dist/daemon/index.d.ts.map +1 -0
  35. package/dist/daemon/index.js +284 -0
  36. package/dist/daemon/index.js.map +1 -0
  37. package/dist/dashboard/index.d.ts +78 -0
  38. package/dist/dashboard/index.d.ts.map +1 -0
  39. package/dist/dashboard/index.js +455 -0
  40. package/dist/dashboard/index.js.map +1 -0
  41. package/dist/guard-engine.d.ts +108 -0
  42. package/dist/guard-engine.d.ts.map +1 -0
  43. package/dist/guard-engine.js +740 -0
  44. package/dist/guard-engine.js.map +1 -0
  45. package/dist/index.d.ts +29 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/install/index.d.ts +23 -0
  50. package/dist/install/index.d.ts.map +1 -0
  51. package/dist/install/index.js +216 -0
  52. package/dist/install/index.js.map +1 -0
  53. package/dist/investigation/index.d.ts +80 -0
  54. package/dist/investigation/index.d.ts.map +1 -0
  55. package/dist/investigation/index.js +570 -0
  56. package/dist/investigation/index.js.map +1 -0
  57. package/dist/license/index.d.ts +46 -0
  58. package/dist/license/index.d.ts.map +1 -0
  59. package/dist/license/index.js +145 -0
  60. package/dist/license/index.js.map +1 -0
  61. package/dist/memory/baseline.d.ts +34 -0
  62. package/dist/memory/baseline.d.ts.map +1 -0
  63. package/dist/memory/baseline.js +224 -0
  64. package/dist/memory/baseline.js.map +1 -0
  65. package/dist/memory/index.d.ts +32 -0
  66. package/dist/memory/index.d.ts.map +1 -0
  67. package/dist/memory/index.js +58 -0
  68. package/dist/memory/index.js.map +1 -0
  69. package/dist/memory/learning.d.ts +35 -0
  70. package/dist/memory/learning.d.ts.map +1 -0
  71. package/dist/memory/learning.js +60 -0
  72. package/dist/memory/learning.js.map +1 -0
  73. package/dist/monitors/falco-monitor.d.ts +62 -0
  74. package/dist/monitors/falco-monitor.d.ts.map +1 -0
  75. package/dist/monitors/falco-monitor.js +226 -0
  76. package/dist/monitors/falco-monitor.js.map +1 -0
  77. package/dist/monitors/suricata-monitor.d.ts +80 -0
  78. package/dist/monitors/suricata-monitor.d.ts.map +1 -0
  79. package/dist/monitors/suricata-monitor.js +227 -0
  80. package/dist/monitors/suricata-monitor.js.map +1 -0
  81. package/dist/notify/email.d.ts +23 -0
  82. package/dist/notify/email.d.ts.map +1 -0
  83. package/dist/notify/email.js +124 -0
  84. package/dist/notify/email.js.map +1 -0
  85. package/dist/notify/index.d.ts +31 -0
  86. package/dist/notify/index.d.ts.map +1 -0
  87. package/dist/notify/index.js +70 -0
  88. package/dist/notify/index.js.map +1 -0
  89. package/dist/notify/line-notify.d.ts.map +1 -0
  90. package/dist/notify/slack.d.ts +21 -0
  91. package/dist/notify/slack.d.ts.map +1 -0
  92. package/dist/notify/slack.js +92 -0
  93. package/dist/notify/slack.js.map +1 -0
  94. package/dist/notify/telegram.d.ts +21 -0
  95. package/dist/notify/telegram.d.ts.map +1 -0
  96. package/dist/notify/telegram.js +89 -0
  97. package/dist/notify/telegram.js.map +1 -0
  98. package/dist/response/file-quarantine.d.ts +63 -0
  99. package/dist/response/file-quarantine.d.ts.map +1 -0
  100. package/dist/response/file-quarantine.js +137 -0
  101. package/dist/response/file-quarantine.js.map +1 -0
  102. package/dist/response/index.d.ts +4 -0
  103. package/dist/response/index.d.ts.map +1 -0
  104. package/dist/response/index.js +4 -0
  105. package/dist/response/index.js.map +1 -0
  106. package/dist/response/ip-blocker.d.ts +69 -0
  107. package/dist/response/ip-blocker.d.ts.map +1 -0
  108. package/dist/response/ip-blocker.js +191 -0
  109. package/dist/response/ip-blocker.js.map +1 -0
  110. package/dist/response/process-killer.d.ts +49 -0
  111. package/dist/response/process-killer.d.ts.map +1 -0
  112. package/dist/response/process-killer.js +230 -0
  113. package/dist/response/process-killer.js.map +1 -0
  114. package/dist/rules/builtin-rules.d.ts +12 -0
  115. package/dist/rules/builtin-rules.d.ts.map +1 -0
  116. package/dist/rules/builtin-rules.js +471 -0
  117. package/dist/rules/builtin-rules.js.map +1 -0
  118. package/dist/threat-cloud/client-id.d.ts +13 -0
  119. package/dist/threat-cloud/client-id.d.ts.map +1 -0
  120. package/dist/threat-cloud/client-id.js +38 -0
  121. package/dist/threat-cloud/client-id.js.map +1 -0
  122. package/dist/threat-cloud/index.d.ts +103 -0
  123. package/dist/threat-cloud/index.d.ts.map +1 -0
  124. package/dist/threat-cloud/index.js +386 -0
  125. package/dist/threat-cloud/index.js.map +1 -0
  126. package/dist/types.d.ts +336 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +42 -0
  129. package/dist/types.js.map +1 -0
  130. package/package.json +35 -0
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Respond Agent - Execute response actions with persistence, rollback, and escalation
3
+ * 回應代理 - 執行回應動作,支援持久化、回滾和漸進升級
4
+ *
5
+ * Third stage of the multi-agent pipeline. Determines and executes
6
+ * the appropriate response action based on verdict confidence levels
7
+ * and the configured action policy thresholds.
8
+ *
9
+ * Uses execFile (never exec) for all system commands to prevent
10
+ * command injection vulnerabilities.
11
+ *
12
+ * @module @panguard-ai/panguard-guard/agent/respond-agent
13
+ */
14
+ import { execFile } from 'node:child_process';
15
+ import { platform } from 'node:os';
16
+ import { appendFileSync, readFileSync, mkdirSync } from 'node:fs';
17
+ import { dirname } from 'node:path';
18
+ import { createLogger } from '@panguard-ai/core';
19
+ const logger = createLogger('panguard-guard:respond-agent');
20
+ /**
21
+ * Safety rules for auto-response actions
22
+ */
23
+ const SAFETY_RULES = {
24
+ whitelistedIPs: new Set(['127.0.0.1', '::1', 'localhost', '0.0.0.0']),
25
+ protectedProcesses: new Set([
26
+ 'sshd',
27
+ 'systemd',
28
+ 'init',
29
+ 'launchd',
30
+ 'loginwindow',
31
+ 'explorer.exe',
32
+ 'svchost.exe',
33
+ 'csrss.exe',
34
+ 'lsass.exe',
35
+ 'services.exe',
36
+ 'winlogon.exe',
37
+ 'wininit.exe',
38
+ 'panguard-guard',
39
+ 'node',
40
+ ]),
41
+ protectedAccounts: new Set(['root', 'Administrator', 'admin', 'SYSTEM', 'LocalSystem']),
42
+ /** Default auto-unblock duration: 1 hour */
43
+ defaultBlockDurationMs: 60 * 60 * 1000,
44
+ /** Extended block duration for repeat offenders: 24 hours */
45
+ repeatOffenderBlockDurationMs: 24 * 60 * 60 * 1000,
46
+ /** SIGKILL timeout after SIGTERM: 5 seconds */
47
+ sigkillTimeoutMs: 5000,
48
+ /** Network isolation requires confidence >= 95 */
49
+ networkIsolationMinConfidence: 95,
50
+ /** Violations before escalation */
51
+ escalationThreshold: 3,
52
+ };
53
+ /**
54
+ * Respond Agent determines and executes appropriate response actions
55
+ * with persistence, auto-unblock timers, SIGKILL fallback, and escalation.
56
+ */
57
+ export class RespondAgent {
58
+ actionPolicy;
59
+ mode;
60
+ actionCount = 0;
61
+ additionalWhitelistedIPs;
62
+ /** Action manifest for persistence and rollback */
63
+ manifest = [];
64
+ manifestPath;
65
+ /** Escalation tracker: target → record */
66
+ escalationMap = new Map();
67
+ /** Active unblock timers */
68
+ unblockTimers = new Map();
69
+ constructor(actionPolicy, mode, whitelistedIPs = [], dataDir = '/var/panguard-guard') {
70
+ this.actionPolicy = actionPolicy;
71
+ this.mode = mode;
72
+ this.additionalWhitelistedIPs = new Set(whitelistedIPs);
73
+ this.manifestPath = `${dataDir}/action-manifest.jsonl`;
74
+ // Ensure manifest directory exists
75
+ try {
76
+ mkdirSync(dirname(this.manifestPath), { recursive: true });
77
+ }
78
+ catch {
79
+ // Directory may already exist
80
+ }
81
+ // Load existing manifest on startup
82
+ this.loadManifest();
83
+ }
84
+ /** Update operating mode */
85
+ setMode(mode) {
86
+ this.mode = mode;
87
+ }
88
+ /**
89
+ * Execute response based on verdict with escalation awareness
90
+ */
91
+ async respond(verdict) {
92
+ // Learning mode: never take active response
93
+ if (this.mode === 'learning') {
94
+ logger.info('Learning mode: no response action taken');
95
+ return {
96
+ action: 'log_only',
97
+ success: true,
98
+ details: 'Learning mode - observation only',
99
+ timestamp: new Date().toISOString(),
100
+ };
101
+ }
102
+ const { confidence } = verdict;
103
+ // Check escalation: repeat offenders get lower thresholds
104
+ const target = this.extractTarget(verdict);
105
+ const escalation = target ? this.escalationMap.get(target) : undefined;
106
+ const isRepeatOffender = escalation && escalation.violationCount >= SAFETY_RULES.escalationThreshold;
107
+ // Repeat offenders: lower auto-respond threshold by 10%
108
+ const effectiveAutoRespond = isRepeatOffender
109
+ ? Math.max(50, this.actionPolicy.autoRespond - 10)
110
+ : this.actionPolicy.autoRespond;
111
+ // Auto-respond: execute the recommended action
112
+ if (confidence >= effectiveAutoRespond) {
113
+ if (isRepeatOffender) {
114
+ logger.warn(`Repeat offender ${target}: auto-responding at lower threshold ` +
115
+ `(${effectiveAutoRespond}% instead of ${this.actionPolicy.autoRespond}%)`);
116
+ }
117
+ // Track escalation
118
+ if (target)
119
+ this.trackEscalation(target);
120
+ return this.executeAction(verdict.recommendedAction, verdict);
121
+ }
122
+ // Notify: send alert but do not auto-execute
123
+ if (confidence >= this.actionPolicy.notifyAndWait) {
124
+ // Track escalation even for notify-level events
125
+ if (target)
126
+ this.trackEscalation(target);
127
+ return {
128
+ action: 'notify',
129
+ success: true,
130
+ details: `Notification sent. Verdict: ${verdict.conclusion}, ` +
131
+ `recommended: ${verdict.recommendedAction}`,
132
+ timestamp: new Date().toISOString(),
133
+ };
134
+ }
135
+ // Log only
136
+ return {
137
+ action: 'log_only',
138
+ success: true,
139
+ details: `Logged: ${verdict.conclusion} (confidence: ${confidence}%)`,
140
+ timestamp: new Date().toISOString(),
141
+ };
142
+ }
143
+ /**
144
+ * Rollback a previous action by manifest entry ID
145
+ */
146
+ async rollback(entryId) {
147
+ const entry = this.manifest.find((e) => e.id === entryId && !e.rolledBack);
148
+ if (!entry) {
149
+ return {
150
+ action: 'log_only',
151
+ success: false,
152
+ details: `No rollback-able action found for ID ${entryId}`,
153
+ timestamp: new Date().toISOString(),
154
+ };
155
+ }
156
+ let result;
157
+ switch (entry.action) {
158
+ case 'block_ip':
159
+ result = await this.unblockIP(entry.target);
160
+ break;
161
+ case 'isolate_file':
162
+ result = {
163
+ action: 'isolate_file',
164
+ success: false,
165
+ details: 'File restore requires manual intervention. Check quarantine directory.',
166
+ timestamp: new Date().toISOString(),
167
+ target: entry.target,
168
+ };
169
+ break;
170
+ default:
171
+ result = {
172
+ action: entry.action,
173
+ success: false,
174
+ details: `Rollback not supported for action: ${entry.action}`,
175
+ timestamp: new Date().toISOString(),
176
+ };
177
+ }
178
+ if (result.success) {
179
+ entry.rolledBack = true;
180
+ this.persistManifestEntry(entry);
181
+ }
182
+ return result;
183
+ }
184
+ /**
185
+ * Get all active (non-rolled-back) actions
186
+ */
187
+ getActiveActions() {
188
+ return this.manifest.filter((e) => !e.rolledBack);
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Action execution
192
+ // ---------------------------------------------------------------------------
193
+ async executeAction(action, verdict) {
194
+ this.actionCount++;
195
+ switch (action) {
196
+ case 'block_ip':
197
+ return this.blockIP(verdict);
198
+ case 'kill_process':
199
+ return this.killProcess(verdict);
200
+ case 'disable_account':
201
+ return this.disableAccount(verdict);
202
+ case 'isolate_file':
203
+ return this.isolateFile(verdict);
204
+ case 'notify':
205
+ return {
206
+ action: 'notify',
207
+ success: true,
208
+ details: 'Notification dispatched',
209
+ timestamp: new Date().toISOString(),
210
+ };
211
+ default:
212
+ return {
213
+ action: 'log_only',
214
+ success: true,
215
+ details: 'Action logged',
216
+ timestamp: new Date().toISOString(),
217
+ };
218
+ }
219
+ }
220
+ /**
221
+ * Block an IP address with auto-unblock timer
222
+ */
223
+ async blockIP(verdict) {
224
+ const ip = this.extractIP(verdict);
225
+ if (!ip) {
226
+ return {
227
+ action: 'block_ip',
228
+ success: false,
229
+ details: 'No IP address found in verdict evidence',
230
+ timestamp: new Date().toISOString(),
231
+ };
232
+ }
233
+ // Safety: check whitelisted IPs
234
+ if (SAFETY_RULES.whitelistedIPs.has(ip) || this.additionalWhitelistedIPs.has(ip)) {
235
+ logger.warn(`Refusing to block whitelisted IP: ${ip}`);
236
+ return {
237
+ action: 'block_ip',
238
+ success: false,
239
+ details: `IP ${ip} is whitelisted and cannot be blocked`,
240
+ timestamp: new Date().toISOString(),
241
+ target: ip,
242
+ };
243
+ }
244
+ // Validate IP format (IPv4 or IPv6)
245
+ if (!/^[\d.]+$/.test(ip) && !/^[a-fA-F\d:]+$/.test(ip)) {
246
+ return {
247
+ action: 'block_ip',
248
+ success: false,
249
+ details: `Invalid IP format: ${ip}`,
250
+ timestamp: new Date().toISOString(),
251
+ target: ip,
252
+ };
253
+ }
254
+ // Determine block duration based on repeat offender status
255
+ const escalation = this.escalationMap.get(ip);
256
+ const isRepeat = escalation && escalation.violationCount >= SAFETY_RULES.escalationThreshold;
257
+ const blockDuration = isRepeat
258
+ ? SAFETY_RULES.repeatOffenderBlockDurationMs
259
+ : SAFETY_RULES.defaultBlockDurationMs;
260
+ const os = platform();
261
+ try {
262
+ if (os === 'darwin') {
263
+ await execFilePromise('/sbin/pfctl', ['-t', 'panguard-guard_blocked', '-T', 'add', ip]);
264
+ }
265
+ else if (os === 'linux') {
266
+ await execFilePromise('/sbin/iptables', ['-A', 'INPUT', '-s', ip, '-j', 'DROP']);
267
+ }
268
+ else if (os === 'win32') {
269
+ await execFilePromise('netsh', [
270
+ 'advfirewall',
271
+ 'firewall',
272
+ 'add',
273
+ 'rule',
274
+ `name=PanguardGuard_Block_${ip}`,
275
+ 'dir=in',
276
+ 'action=block',
277
+ `remoteip=${ip}`,
278
+ ]);
279
+ }
280
+ const expiresAt = new Date(Date.now() + blockDuration).toISOString();
281
+ // Persist action to manifest
282
+ const entry = this.recordAction('block_ip', ip, verdict, expiresAt);
283
+ // Set auto-unblock timer
284
+ this.scheduleUnblock(ip, blockDuration, entry.id);
285
+ const durationStr = isRepeat ? '24h (repeat offender)' : '1h';
286
+ logger.info(`Blocked IP: ${ip} for ${durationStr} (auto-unblock scheduled)`);
287
+ return {
288
+ action: 'block_ip',
289
+ success: true,
290
+ details: `IP ${ip} blocked via ${os} firewall for ${durationStr}. Auto-unblock at ${expiresAt}`,
291
+ timestamp: new Date().toISOString(),
292
+ target: ip,
293
+ };
294
+ }
295
+ catch (err) {
296
+ const msg = err instanceof Error ? err.message : String(err);
297
+ logger.error(`Failed to block IP ${ip}: ${msg}`);
298
+ return {
299
+ action: 'block_ip',
300
+ success: false,
301
+ details: `Failed to block IP ${ip}: ${msg}`,
302
+ timestamp: new Date().toISOString(),
303
+ target: ip,
304
+ };
305
+ }
306
+ }
307
+ /**
308
+ * Unblock a previously blocked IP
309
+ */
310
+ async unblockIP(ip) {
311
+ const os = platform();
312
+ try {
313
+ if (os === 'darwin') {
314
+ await execFilePromise('/sbin/pfctl', ['-t', 'panguard-guard_blocked', '-T', 'delete', ip]);
315
+ }
316
+ else if (os === 'linux') {
317
+ await execFilePromise('/sbin/iptables', ['-D', 'INPUT', '-s', ip, '-j', 'DROP']);
318
+ }
319
+ else if (os === 'win32') {
320
+ await execFilePromise('netsh', [
321
+ 'advfirewall',
322
+ 'firewall',
323
+ 'delete',
324
+ 'rule',
325
+ `name=PanguardGuard_Block_${ip}`,
326
+ ]);
327
+ }
328
+ // Clear the unblock timer
329
+ const timer = this.unblockTimers.get(ip);
330
+ if (timer) {
331
+ clearTimeout(timer);
332
+ this.unblockTimers.delete(ip);
333
+ }
334
+ logger.info(`Unblocked IP: ${ip}`);
335
+ return {
336
+ action: 'block_ip',
337
+ success: true,
338
+ details: `IP ${ip} unblocked`,
339
+ timestamp: new Date().toISOString(),
340
+ target: ip,
341
+ };
342
+ }
343
+ catch (err) {
344
+ const msg = err instanceof Error ? err.message : String(err);
345
+ logger.error(`Failed to unblock IP ${ip}: ${msg}`);
346
+ return {
347
+ action: 'block_ip',
348
+ success: false,
349
+ details: `Failed to unblock IP ${ip}: ${msg}`,
350
+ timestamp: new Date().toISOString(),
351
+ target: ip,
352
+ };
353
+ }
354
+ }
355
+ /**
356
+ * Schedule auto-unblock after duration
357
+ */
358
+ scheduleUnblock(ip, durationMs, entryId) {
359
+ // Clear existing timer for this IP
360
+ const existing = this.unblockTimers.get(ip);
361
+ if (existing)
362
+ clearTimeout(existing);
363
+ const timer = setTimeout(async () => {
364
+ logger.info(`Auto-unblock timer expired for IP: ${ip}`);
365
+ const result = await this.unblockIP(ip);
366
+ if (result.success) {
367
+ const entry = this.manifest.find((e) => e.id === entryId);
368
+ if (entry) {
369
+ entry.rolledBack = true;
370
+ this.persistManifestEntry(entry);
371
+ }
372
+ }
373
+ this.unblockTimers.delete(ip);
374
+ }, durationMs);
375
+ // Don't hold the process open for unblock timers
376
+ if (timer.unref)
377
+ timer.unref();
378
+ this.unblockTimers.set(ip, timer);
379
+ }
380
+ /**
381
+ * Kill a process with SIGKILL fallback
382
+ */
383
+ async killProcess(verdict) {
384
+ const pid = this.extractPID(verdict);
385
+ if (!pid) {
386
+ return {
387
+ action: 'kill_process',
388
+ success: false,
389
+ details: 'No PID found in verdict evidence',
390
+ timestamp: new Date().toISOString(),
391
+ };
392
+ }
393
+ // Safety: check protected processes
394
+ const processName = this.extractProcessName(verdict);
395
+ if (processName && SAFETY_RULES.protectedProcesses.has(processName)) {
396
+ logger.warn(`Refusing to kill protected process: ${processName} (PID ${pid})`);
397
+ return {
398
+ action: 'kill_process',
399
+ success: false,
400
+ details: `Process ${processName} is protected and cannot be killed`,
401
+ timestamp: new Date().toISOString(),
402
+ target: String(pid),
403
+ };
404
+ }
405
+ // Safety: never kill our own process
406
+ if (pid === process.pid) {
407
+ logger.warn('Refusing to kill own process');
408
+ return {
409
+ action: 'kill_process',
410
+ success: false,
411
+ details: 'Cannot kill own process',
412
+ timestamp: new Date().toISOString(),
413
+ target: String(pid),
414
+ };
415
+ }
416
+ try {
417
+ // Step 1: Try SIGTERM (graceful)
418
+ process.kill(pid, 'SIGTERM');
419
+ logger.info(`Sent SIGTERM to PID ${pid}`);
420
+ // Step 2: Verify process is gone, fallback to SIGKILL
421
+ const isAlive = await this.waitForProcessExit(pid, SAFETY_RULES.sigkillTimeoutMs);
422
+ if (isAlive) {
423
+ try {
424
+ process.kill(pid, 'SIGKILL');
425
+ logger.warn(`SIGTERM failed, sent SIGKILL to PID ${pid}`);
426
+ }
427
+ catch {
428
+ // Process may have exited between check and kill
429
+ }
430
+ }
431
+ this.recordAction('kill_process', String(pid), verdict);
432
+ return {
433
+ action: 'kill_process',
434
+ success: true,
435
+ details: `Process PID ${pid} terminated${isAlive ? ' (SIGKILL required)' : ''}`,
436
+ timestamp: new Date().toISOString(),
437
+ target: String(pid),
438
+ };
439
+ }
440
+ catch (err) {
441
+ const msg = err instanceof Error ? err.message : String(err);
442
+ logger.error(`Failed to kill process ${pid}: ${msg}`);
443
+ return {
444
+ action: 'kill_process',
445
+ success: false,
446
+ details: `Failed to kill process ${pid}: ${msg}`,
447
+ timestamp: new Date().toISOString(),
448
+ target: String(pid),
449
+ };
450
+ }
451
+ }
452
+ /**
453
+ * Wait for a process to exit, return true if still alive after timeout
454
+ */
455
+ async waitForProcessExit(pid, timeoutMs) {
456
+ const start = Date.now();
457
+ while (Date.now() - start < timeoutMs) {
458
+ try {
459
+ // Signal 0 checks if process exists without killing it
460
+ process.kill(pid, 0);
461
+ // Process still alive, wait a bit
462
+ await new Promise((resolve) => setTimeout(resolve, 500));
463
+ }
464
+ catch {
465
+ // Process is gone
466
+ return false;
467
+ }
468
+ }
469
+ // Still alive after timeout
470
+ return true;
471
+ }
472
+ /**
473
+ * Disable a user account
474
+ */
475
+ async disableAccount(verdict) {
476
+ const username = this.extractUsername(verdict);
477
+ if (!username) {
478
+ return {
479
+ action: 'disable_account',
480
+ success: false,
481
+ details: 'No username found in verdict evidence',
482
+ timestamp: new Date().toISOString(),
483
+ };
484
+ }
485
+ if (SAFETY_RULES.protectedAccounts.has(username)) {
486
+ logger.warn(`Refusing to disable protected account: ${username}`);
487
+ return {
488
+ action: 'disable_account',
489
+ success: false,
490
+ details: `Account ${username} is protected and cannot be disabled`,
491
+ timestamp: new Date().toISOString(),
492
+ target: username,
493
+ };
494
+ }
495
+ if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
496
+ return {
497
+ action: 'disable_account',
498
+ success: false,
499
+ details: `Invalid username format: ${username}`,
500
+ timestamp: new Date().toISOString(),
501
+ target: username,
502
+ };
503
+ }
504
+ const os = platform();
505
+ try {
506
+ if (os === 'darwin') {
507
+ await execFilePromise('/usr/bin/dscl', [
508
+ '.',
509
+ '-create',
510
+ `/Users/${username}`,
511
+ 'AuthenticationAuthority',
512
+ ';DisabledUser;',
513
+ ]);
514
+ }
515
+ else if (os === 'linux') {
516
+ await execFilePromise('/usr/sbin/usermod', ['-L', username]);
517
+ }
518
+ else if (os === 'win32') {
519
+ await execFilePromise('net', ['user', username, '/active:no']);
520
+ }
521
+ this.recordAction('disable_account', username, verdict);
522
+ logger.info(`Disabled account: ${username}`);
523
+ return {
524
+ action: 'disable_account',
525
+ success: true,
526
+ details: `Account ${username} disabled`,
527
+ timestamp: new Date().toISOString(),
528
+ target: username,
529
+ };
530
+ }
531
+ catch (err) {
532
+ const msg = err instanceof Error ? err.message : String(err);
533
+ logger.error(`Failed to disable account ${username}: ${msg}`);
534
+ return {
535
+ action: 'disable_account',
536
+ success: false,
537
+ details: `Failed to disable account: ${msg}`,
538
+ timestamp: new Date().toISOString(),
539
+ target: username,
540
+ };
541
+ }
542
+ }
543
+ /**
544
+ * Isolate a file (move to quarantine) with metadata tracking
545
+ */
546
+ async isolateFile(verdict) {
547
+ const filePath = this.extractFilePath(verdict);
548
+ if (!filePath) {
549
+ return {
550
+ action: 'isolate_file',
551
+ success: false,
552
+ details: 'No file path found in verdict evidence',
553
+ timestamp: new Date().toISOString(),
554
+ };
555
+ }
556
+ try {
557
+ const quarantineDir = '/var/panguard-guard/quarantine';
558
+ const os = platform();
559
+ const mvCmd = os === 'win32' ? 'move' : '/bin/mv';
560
+ const fileName = filePath.split(/[/\\]/).pop() ?? 'unknown';
561
+ const dest = `${quarantineDir}/${Date.now()}_${fileName}`;
562
+ // Ensure quarantine directory exists
563
+ if (os !== 'win32') {
564
+ await execFilePromise('/bin/mkdir', ['-p', quarantineDir]);
565
+ }
566
+ await execFilePromise(mvCmd, [filePath, dest]);
567
+ // Record with metadata for forensics
568
+ this.recordAction('isolate_file', filePath, verdict);
569
+ // Write quarantine metadata alongside the file
570
+ try {
571
+ const metadata = {
572
+ originalPath: filePath,
573
+ quarantinedAt: new Date().toISOString(),
574
+ verdict: { conclusion: verdict.conclusion, confidence: verdict.confidence },
575
+ reasoning: verdict.reasoning,
576
+ };
577
+ appendFileSync(`${dest}.meta.json`, JSON.stringify(metadata, null, 2), 'utf-8');
578
+ }
579
+ catch {
580
+ // Non-critical: metadata write failure
581
+ }
582
+ logger.info(`Isolated file: ${filePath} -> ${dest}`);
583
+ return {
584
+ action: 'isolate_file',
585
+ success: true,
586
+ details: `File isolated: ${filePath} -> ${dest}`,
587
+ timestamp: new Date().toISOString(),
588
+ target: filePath,
589
+ };
590
+ }
591
+ catch (err) {
592
+ const msg = err instanceof Error ? err.message : String(err);
593
+ logger.error(`Failed to isolate file: ${msg}`);
594
+ return {
595
+ action: 'isolate_file',
596
+ success: false,
597
+ details: `Failed to isolate file: ${msg}`,
598
+ timestamp: new Date().toISOString(),
599
+ target: filePath,
600
+ };
601
+ }
602
+ }
603
+ // ---------------------------------------------------------------------------
604
+ // Action Manifest (persistence)
605
+ // ---------------------------------------------------------------------------
606
+ recordAction(action, target, verdict, expiresAt) {
607
+ const entry = {
608
+ id: `act-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
609
+ action,
610
+ target,
611
+ timestamp: new Date().toISOString(),
612
+ expiresAt,
613
+ rolledBack: false,
614
+ verdict: { conclusion: verdict.conclusion, confidence: verdict.confidence },
615
+ };
616
+ this.manifest.push(entry);
617
+ this.persistManifestEntry(entry);
618
+ return entry;
619
+ }
620
+ persistManifestEntry(entry) {
621
+ try {
622
+ appendFileSync(this.manifestPath, JSON.stringify(entry) + '\n', 'utf-8');
623
+ }
624
+ catch (err) {
625
+ const msg = err instanceof Error ? err.message : String(err);
626
+ logger.error(`Failed to persist action manifest: ${msg}`);
627
+ }
628
+ }
629
+ loadManifest() {
630
+ try {
631
+ const content = readFileSync(this.manifestPath, 'utf-8');
632
+ const lines = content.trim().split('\n').filter(Boolean);
633
+ for (const line of lines) {
634
+ try {
635
+ const entry = JSON.parse(line);
636
+ this.manifest.push(entry);
637
+ }
638
+ catch {
639
+ // Skip malformed lines
640
+ }
641
+ }
642
+ logger.info(`Loaded ${this.manifest.length} action manifest entries`);
643
+ }
644
+ catch {
645
+ // Manifest file may not exist yet
646
+ }
647
+ }
648
+ // ---------------------------------------------------------------------------
649
+ // Escalation tracking
650
+ // ---------------------------------------------------------------------------
651
+ trackEscalation(target) {
652
+ const now = new Date().toISOString();
653
+ const existing = this.escalationMap.get(target);
654
+ if (existing) {
655
+ existing.violationCount += 1;
656
+ existing.lastSeen = now;
657
+ }
658
+ else {
659
+ this.escalationMap.set(target, {
660
+ target,
661
+ violationCount: 1,
662
+ firstSeen: now,
663
+ lastSeen: now,
664
+ });
665
+ }
666
+ }
667
+ extractTarget(verdict) {
668
+ return (this.extractIP(verdict) ??
669
+ this.extractProcessName(verdict) ??
670
+ this.extractUsername(verdict) ??
671
+ this.extractFilePath(verdict));
672
+ }
673
+ // ---------------------------------------------------------------------------
674
+ // Evidence extraction helpers
675
+ // ---------------------------------------------------------------------------
676
+ extractIP(verdict) {
677
+ for (const e of verdict.evidence) {
678
+ const data = e.data;
679
+ if (data?.['ip'])
680
+ return data['ip'];
681
+ if (data?.['sourceIP'])
682
+ return data['sourceIP'];
683
+ }
684
+ return undefined;
685
+ }
686
+ extractPID(verdict) {
687
+ for (const e of verdict.evidence) {
688
+ const data = e.data;
689
+ if (data?.['pid'])
690
+ return Number(data['pid']);
691
+ }
692
+ return undefined;
693
+ }
694
+ extractUsername(verdict) {
695
+ for (const e of verdict.evidence) {
696
+ const data = e.data;
697
+ if (data?.['username'])
698
+ return data['username'];
699
+ }
700
+ return undefined;
701
+ }
702
+ extractFilePath(verdict) {
703
+ for (const e of verdict.evidence) {
704
+ const data = e.data;
705
+ if (data?.['filePath'])
706
+ return data['filePath'];
707
+ }
708
+ return undefined;
709
+ }
710
+ extractProcessName(verdict) {
711
+ for (const e of verdict.evidence) {
712
+ const data = e.data;
713
+ if (data?.['processName'])
714
+ return data['processName'];
715
+ }
716
+ return undefined;
717
+ }
718
+ /** Get total action count */
719
+ getActionCount() {
720
+ return this.actionCount;
721
+ }
722
+ /** Get escalation records */
723
+ getEscalationRecords() {
724
+ return new Map(this.escalationMap);
725
+ }
726
+ /** Cleanup: clear all timers (for graceful shutdown) */
727
+ destroy() {
728
+ for (const timer of this.unblockTimers.values()) {
729
+ clearTimeout(timer);
730
+ }
731
+ this.unblockTimers.clear();
732
+ }
733
+ }
734
+ // ---------------------------------------------------------------------------
735
+ // Utility
736
+ // ---------------------------------------------------------------------------
737
+ function execFilePromise(command, args) {
738
+ return new Promise((resolve, reject) => {
739
+ execFile(command, args, { timeout: 10000 }, (error, stdout) => {
740
+ if (error) {
741
+ reject(error);
742
+ }
743
+ else {
744
+ resolve(stdout);
745
+ }
746
+ });
747
+ });
748
+ }
749
+ //# sourceMappingURL=respond-agent.js.map