mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-11 22:46:38 +02:00
344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const HELP_TEXT = `
|
|
ShadowBroker DM root health poller
|
|
|
|
Usage:
|
|
node scripts/mesh/poll-dm-root-health-alerts.mjs [--once] [--base-url URL] [--alerts-path PATH]
|
|
|
|
Environment:
|
|
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
|
|
SB_DM_ROOT_ALERTS_PATH=/api/wormhole/dm/root-health/alerts
|
|
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
|
|
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
|
|
SB_DM_ROOT_INTERVAL_S=60
|
|
SB_DM_ROOT_TIMEOUT_MS=10000
|
|
SB_DM_ROOT_STATE_FILE=data/dm_root_health_bridge_state.json
|
|
SB_DM_ROOT_WARNING_WEBHOOK_URL=https://hooks.slack.example/services/...
|
|
SB_DM_ROOT_CRITICAL_WEBHOOK_URL=https://events.pagerduty.example/v2/enqueue
|
|
|
|
Flags:
|
|
--once Poll one time and exit with status 0/1/2
|
|
--base-url URL Override SB_DM_ROOT_BASE_URL
|
|
--alerts-path PATH Override SB_DM_ROOT_ALERTS_PATH
|
|
--help Show this text
|
|
|
|
Exit codes for --once:
|
|
0 = ok
|
|
1 = warning
|
|
2 = critical or fetch failure
|
|
`.trim();
|
|
|
|
function parseArgs(argv) {
|
|
const parsed = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const current = String(argv[index] || '').trim();
|
|
if (!current) continue;
|
|
if (current === '--once') {
|
|
parsed.once = true;
|
|
continue;
|
|
}
|
|
if (current === '--help' || current === '-h') {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
if ((current === '--base-url' || current === '--alerts-path') && index + 1 < argv.length) {
|
|
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
|
|
String(argv[index + 1] || '').trim();
|
|
index += 1;
|
|
}
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function normalizeUrl(baseUrl, alertsPath) {
|
|
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
|
|
const pathValue = String(alertsPath || '/api/wormhole/dm/root-health/alerts').trim();
|
|
if (!pathValue) {
|
|
return `${base}/api/wormhole/dm/root-health/alerts`;
|
|
}
|
|
return pathValue.startsWith('http://') || pathValue.startsWith('https://')
|
|
? pathValue
|
|
: `${base}/${pathValue.replace(/^\/+/, '')}`;
|
|
}
|
|
|
|
function parseHeader(rawValue) {
|
|
const raw = String(rawValue || '').trim();
|
|
if (!raw) return null;
|
|
const separator = raw.indexOf(':');
|
|
if (separator <= 0) return null;
|
|
const name = raw.slice(0, separator).trim();
|
|
const value = raw.slice(separator + 1).trim();
|
|
if (!name || !value) return null;
|
|
return [name, value];
|
|
}
|
|
|
|
function severityFromPayload(payload) {
|
|
const state = String(payload?.state || '').trim().toLowerCase();
|
|
if (state === 'critical') return 'critical';
|
|
if (state === 'warning') return 'warning';
|
|
if (state === 'ok') return 'ok';
|
|
if (payload?.page_required) return 'critical';
|
|
if (payload?.ticket_required) return 'warning';
|
|
return 'critical';
|
|
}
|
|
|
|
function buildFingerprint(payload) {
|
|
return JSON.stringify({
|
|
severity: severityFromPayload(payload),
|
|
state: String(payload?.state || '').trim().toLowerCase(),
|
|
page_required: Boolean(payload?.page_required),
|
|
ticket_required: Boolean(payload?.ticket_required),
|
|
active_alert_codes: Array.isArray(payload?.active_alert_codes) ? payload.active_alert_codes : [],
|
|
next_action: String(payload?.next_action || '').trim(),
|
|
alert_count: Number(payload?.alert_count || 0),
|
|
blocking_alert_count: Number(payload?.blocking_alert_count || 0),
|
|
warning_alert_count: Number(payload?.warning_alert_count || 0),
|
|
});
|
|
}
|
|
|
|
function buildSummary(payload) {
|
|
const severity = severityFromPayload(payload);
|
|
const activeAlertCodes = Array.isArray(payload?.active_alert_codes)
|
|
? payload.active_alert_codes.map((value) => String(value || '').trim()).filter(Boolean)
|
|
: [];
|
|
return {
|
|
severity,
|
|
state: String(payload?.state || '').trim().toLowerCase() || 'critical',
|
|
checkedAt: Number(payload?.checked_at || 0),
|
|
pageRequired: Boolean(payload?.page_required),
|
|
ticketRequired: Boolean(payload?.ticket_required),
|
|
recommendedCheckIntervalS: Number(payload?.recommended_check_interval_s || 60),
|
|
nextAction: String(payload?.next_action || '').trim(),
|
|
primaryAlert: String(payload?.primary_alert || '').trim(),
|
|
activeAlertCodes,
|
|
alertCount: Number(payload?.alert_count || 0),
|
|
blockingAlertCount: Number(payload?.blocking_alert_count || 0),
|
|
warningAlertCount: Number(payload?.warning_alert_count || 0),
|
|
fingerprint: buildFingerprint(payload),
|
|
raw: payload,
|
|
};
|
|
}
|
|
|
|
function failureSummary(detail) {
|
|
const message = String(detail || '').trim() || 'dm_root_health_poll_failed';
|
|
return {
|
|
severity: 'critical',
|
|
state: 'critical',
|
|
checkedAt: Math.floor(Date.now() / 1000),
|
|
pageRequired: true,
|
|
ticketRequired: true,
|
|
recommendedCheckIntervalS: 60,
|
|
nextAction: 'check_root_health_endpoint',
|
|
primaryAlert: message,
|
|
activeAlertCodes: ['dm_root_health_poll_failed'],
|
|
alertCount: 1,
|
|
blockingAlertCount: 1,
|
|
warningAlertCount: 0,
|
|
fingerprint: JSON.stringify({ severity: 'critical', error: message }),
|
|
raw: {
|
|
ok: false,
|
|
state: 'critical',
|
|
primary_alert: message,
|
|
active_alert_codes: ['dm_root_health_poll_failed'],
|
|
next_action: 'check_root_health_endpoint',
|
|
page_required: true,
|
|
ticket_required: true,
|
|
recommended_check_interval_s: 60,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function loadStateFile(stateFile) {
|
|
if (!stateFile) return {};
|
|
try {
|
|
const raw = await fs.readFile(stateFile, 'utf8');
|
|
const parsed = JSON.parse(raw);
|
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function writeStateFile(stateFile, value) {
|
|
if (!stateFile) return;
|
|
const targetPath = path.resolve(stateFile);
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.writeFile(targetPath, JSON.stringify(value, null, 2));
|
|
}
|
|
|
|
async function fetchAlerts(config) {
|
|
const headers = { Accept: 'application/json' };
|
|
const authHeader = parseHeader(config.authHeader);
|
|
if (authHeader) {
|
|
headers[authHeader[0]] = authHeader[1];
|
|
}
|
|
if (config.authCookie) {
|
|
headers.Cookie = config.authCookie;
|
|
}
|
|
const controller = new AbortController();
|
|
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
|
|
try {
|
|
const response = await fetch(config.url, {
|
|
method: 'GET',
|
|
headers,
|
|
signal: controller.signal,
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok || payload?.ok === false) {
|
|
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
|
|
throw new Error(detail || 'dm_root_health_alerts_failed');
|
|
}
|
|
return buildSummary(payload);
|
|
} finally {
|
|
globalThis.clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
async function postWebhook(targetUrl, summary, config) {
|
|
if (!targetUrl) {
|
|
return { delivered: false, reason: 'no_target' };
|
|
}
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
};
|
|
const payload = {
|
|
source: 'shadowbroker_dm_root_health_bridge',
|
|
sent_at: new Date().toISOString(),
|
|
severity: summary.severity,
|
|
monitoring_state: summary.state,
|
|
page_required: summary.pageRequired,
|
|
ticket_required: summary.ticketRequired,
|
|
primary_alert: summary.primaryAlert,
|
|
next_action: summary.nextAction,
|
|
active_alert_codes: summary.activeAlertCodes,
|
|
alert_count: summary.alertCount,
|
|
blocking_alert_count: summary.blockingAlertCount,
|
|
warning_alert_count: summary.warningAlertCount,
|
|
checked_at: summary.checkedAt,
|
|
raw: summary.raw,
|
|
};
|
|
const controller = new AbortController();
|
|
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
|
|
try {
|
|
const response = await fetch(targetUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
signal: controller.signal,
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`webhook_http_${response.status}`);
|
|
}
|
|
return { delivered: true, reason: 'sent' };
|
|
} finally {
|
|
globalThis.clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
async function maybeDeliverWebhook(summary, config) {
|
|
if (summary.severity === 'ok') {
|
|
return { delivered: false, reason: 'ok_state' };
|
|
}
|
|
const state = await loadStateFile(config.stateFile);
|
|
if (
|
|
String(state.last_fingerprint || '') === summary.fingerprint &&
|
|
String(state.last_severity || '') === summary.severity
|
|
) {
|
|
return { delivered: false, reason: 'duplicate' };
|
|
}
|
|
const targetUrl =
|
|
summary.severity === 'critical' ? config.criticalWebhookUrl : config.warningWebhookUrl;
|
|
const delivered = await postWebhook(targetUrl, summary, config);
|
|
if (delivered.delivered) {
|
|
await writeStateFile(config.stateFile, {
|
|
last_checked_at: summary.checkedAt,
|
|
last_severity: summary.severity,
|
|
last_fingerprint: summary.fingerprint,
|
|
last_target: targetUrl,
|
|
});
|
|
}
|
|
return delivered;
|
|
}
|
|
|
|
function printSummary(summary, webhookResult) {
|
|
const prefix = summary.severity.toUpperCase().padEnd(8, ' ');
|
|
const alerts = summary.activeAlertCodes.length > 0 ? summary.activeAlertCodes.join(',') : 'none';
|
|
const nextAction = summary.nextAction || 'none';
|
|
const webhookNote = webhookResult?.reason ? ` webhook=${webhookResult.reason}` : '';
|
|
console.log(
|
|
`[${prefix}] state=${summary.state} page=${summary.pageRequired} ticket=${summary.ticketRequired} ` +
|
|
`alerts=${alerts} next_action=${nextAction}${webhookNote}`,
|
|
);
|
|
}
|
|
|
|
function exitCodeForSeverity(severity) {
|
|
if (severity === 'ok') return 0;
|
|
if (severity === 'warning') return 1;
|
|
return 2;
|
|
}
|
|
|
|
async function sleep(ms) {
|
|
return new Promise((resolve) => {
|
|
globalThis.setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
async function pollOnce(config) {
|
|
try {
|
|
const summary = await fetchAlerts(config);
|
|
const webhookResult = await maybeDeliverWebhook(summary, config);
|
|
printSummary(summary, webhookResult);
|
|
return summary;
|
|
} catch (error) {
|
|
const summary = failureSummary(error instanceof Error ? error.message : 'dm_root_health_poll_failed');
|
|
const webhookResult = await maybeDeliverWebhook(summary, config).catch(() => ({
|
|
delivered: false,
|
|
reason: 'webhook_failed',
|
|
}));
|
|
printSummary(summary, webhookResult);
|
|
return summary;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(HELP_TEXT);
|
|
return;
|
|
}
|
|
|
|
const config = {
|
|
once: Boolean(args.once),
|
|
url: normalizeUrl(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL, args.alertsPath || process.env.SB_DM_ROOT_ALERTS_PATH),
|
|
authHeader: process.env.SB_DM_ROOT_AUTH_HEADER || '',
|
|
authCookie: process.env.SB_DM_ROOT_AUTH_COOKIE || '',
|
|
intervalMs: Math.max(5, Number(process.env.SB_DM_ROOT_INTERVAL_S || 60)) * 1000,
|
|
timeoutMs: Math.max(1000, Number(process.env.SB_DM_ROOT_TIMEOUT_MS || 10000)),
|
|
stateFile: process.env.SB_DM_ROOT_STATE_FILE || '',
|
|
warningWebhookUrl: process.env.SB_DM_ROOT_WARNING_WEBHOOK_URL || '',
|
|
criticalWebhookUrl: process.env.SB_DM_ROOT_CRITICAL_WEBHOOK_URL || '',
|
|
};
|
|
|
|
if (config.once) {
|
|
const summary = await pollOnce(config);
|
|
process.exitCode = exitCodeForSeverity(summary.severity);
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
const summary = await pollOnce(config);
|
|
const nextDelayMs = Math.max(
|
|
5000,
|
|
Number(summary.recommendedCheckIntervalS || config.intervalMs / 1000) * 1000,
|
|
);
|
|
await sleep(nextDelayMs || config.intervalMs);
|
|
}
|
|
}
|
|
|
|
await main();
|