feat(audit): Add automation component logging
Add audit logging to: - WebhookHandler: connect/disconnect, receive, not_found, denied, HMAC verified - HeartbeatMonitor: start/stop, cycle, check, fail, recover - GmailWatcher: connect/disconnect lifecycle events All automation events now captured in audit log with proper context
This commit is contained in:
@@ -6,6 +6,7 @@ import type { GmailConfig } from '../config/schema.js';
|
||||
import type { ChannelAdapter, ChannelStatus, InboundMessage, OutboundMessage } from '../channels/types.js';
|
||||
import { parseInterval } from './heartbeat.js';
|
||||
import { sanitizeHtml } from '../utils/html.js';
|
||||
import { auditLogger } from '../audit/index.js';
|
||||
|
||||
/** Minimal interface for the parts of ChannelRegistry we need. */
|
||||
interface ChannelLookup {
|
||||
@@ -99,6 +100,7 @@ export class GmailWatcher implements ChannelAdapter {
|
||||
|
||||
this._status = 'connected';
|
||||
console.log(`GmailWatcher: Connected (poll_interval=${this.config.poll_interval ?? '300s'})`);
|
||||
auditLogger?.systemStart('GmailWatcher', { poll_interval: this.config.poll_interval });
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
@@ -107,11 +109,12 @@ export class GmailWatcher implements ChannelAdapter {
|
||||
this.pollTimer = undefined;
|
||||
}
|
||||
if (this.watchTimer) {
|
||||
clearInterval(this.watchTimer);
|
||||
clearTimeout(this.watchTimer);
|
||||
this.watchTimer = undefined;
|
||||
}
|
||||
this.oauth2Client = undefined;
|
||||
this._status = 'disconnected';
|
||||
auditLogger?.systemStop('GmailWatcher');
|
||||
}
|
||||
|
||||
async send(peerId: string, message: OutboundMessage): Promise<void> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { statfsSync, accessSync, constants as fsConstants } from 'fs';
|
||||
import { request } from 'http';
|
||||
import type { HeartbeatConfig, HeartbeatCheck } from '../config/schema.js';
|
||||
import type { ChannelAdapter, ChannelStatus, OutboundMessage } from '../channels/types.js';
|
||||
import { auditLogger } from '../audit/index.js';
|
||||
|
||||
/** Result of a single health check. */
|
||||
export interface CheckResult {
|
||||
@@ -77,6 +78,7 @@ export class HeartbeatMonitor {
|
||||
|
||||
const intervalMs = parseInterval(this.deps.config.interval);
|
||||
console.log(`HeartbeatMonitor: starting (interval=${this.deps.config.interval}, checks=[${this.deps.config.checks.join(', ')}])`);
|
||||
auditLogger?.systemStart('HeartbeatMonitor', { interval: this.deps.config.interval, checks: this.deps.config.checks });
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.runChecks().catch((err) => {
|
||||
@@ -96,6 +98,7 @@ export class HeartbeatMonitor {
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
auditLogger?.systemStop('HeartbeatMonitor');
|
||||
}
|
||||
|
||||
/** Run all configured checks and return the result. */
|
||||
@@ -136,6 +139,12 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
checks.push(result);
|
||||
auditLogger?.heartbeatCheck({
|
||||
check_name: result.name,
|
||||
healthy: result.healthy,
|
||||
message: result.message,
|
||||
duration_ms: result.durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
const healthy = checks.every((c) => c.healthy);
|
||||
@@ -154,16 +163,33 @@ export class HeartbeatMonitor {
|
||||
this.notifiedFailure = true;
|
||||
const failedChecks = checks.filter((c) => !c.healthy).map((c) => `${c.name}: ${c.message}`);
|
||||
await this.notify(`Heartbeat FAILING (${this.consecutiveFailures} consecutive failures):\n${failedChecks.join('\n')}`);
|
||||
|
||||
auditLogger?.heartbeatFail({
|
||||
checks_failed: failedChecks,
|
||||
consecutive_failures: this.consecutiveFailures,
|
||||
threshold: this.deps.config.failure_threshold,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.notifiedFailure) {
|
||||
// Recovery notification
|
||||
await this.notify(`Heartbeat RECOVERED after ${this.consecutiveFailures} consecutive failure(s). All checks passing.`);
|
||||
|
||||
auditLogger?.heartbeatRecover({
|
||||
consecutive_failures_before: this.consecutiveFailures,
|
||||
});
|
||||
}
|
||||
this.consecutiveFailures = 0;
|
||||
this.notifiedFailure = false;
|
||||
}
|
||||
|
||||
auditLogger?.heartbeatCycle({
|
||||
interval_ms: parseInterval(this.deps.config.interval),
|
||||
checks: this.deps.config.checks,
|
||||
healthy,
|
||||
consecutive_failures: this.consecutiveFailures,
|
||||
});
|
||||
|
||||
return heartbeatResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { WebhookConfig } from '../config/schema.js';
|
||||
import type { ChannelAdapter, ChannelStatus, InboundMessage, OutboundMessage } from '../channels/types.js';
|
||||
import { auditLogger } from '../audit/index.js';
|
||||
|
||||
/** Minimal interface for the parts of ChannelRegistry we need. */
|
||||
interface ChannelLookup {
|
||||
@@ -83,11 +84,13 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
const enabledCount = this.webhookConfigs.filter(w => w.enabled).length;
|
||||
if (enabledCount > 0) {
|
||||
console.log(`WebhookHandler: ${enabledCount} webhook(s) registered`);
|
||||
auditLogger?.systemStart('WebhookHandler', { webhooks_enabled: enabledCount });
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this._status = 'disconnected';
|
||||
auditLogger?.systemStop('WebhookHandler');
|
||||
}
|
||||
|
||||
async send(peerId: string, message: OutboundMessage): Promise<void> {
|
||||
@@ -118,12 +121,14 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
async handleRequest(webhookName: string, req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
||||
const webhook = this.webhooks.get(webhookName);
|
||||
if (!webhook) {
|
||||
auditLogger?.webhookNotFound(webhookName);
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Unknown webhook' }));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!webhook.enabled) {
|
||||
auditLogger?.webhookDenied(webhookName, 'Webhook disabled');
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Webhook disabled' }));
|
||||
return false;
|
||||
@@ -132,9 +137,12 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
const body = await readBody(req);
|
||||
|
||||
// Verify HMAC if secret is configured
|
||||
const signatureVerified = !webhook.secret;
|
||||
if (webhook.secret) {
|
||||
const signature = req.headers['x-webhook-signature'] as string | undefined;
|
||||
if (!signature || !verifyHmac(body, webhook.secret, signature)) {
|
||||
const verified = signature ? verifyHmac(body, webhook.secret, signature) : false;
|
||||
if (!verified) {
|
||||
auditLogger?.webhookDenied(webhookName, 'Invalid HMAC signature');
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid signature' }));
|
||||
return false;
|
||||
@@ -154,6 +162,14 @@ export class WebhookHandler implements ChannelAdapter {
|
||||
metadata: { webhookName, body },
|
||||
};
|
||||
|
||||
auditLogger?.webhookReceive({
|
||||
webhook_name: webhookName,
|
||||
body,
|
||||
signature_verified: signatureVerified,
|
||||
output_channel: webhook.output.channel,
|
||||
output_peer: webhook.output.peer,
|
||||
});
|
||||
|
||||
this.messageHandler?.(msg);
|
||||
|
||||
res.writeHead(202, { 'Content-Type': 'application/json' });
|
||||
|
||||
Reference in New Issue
Block a user