From 542e120088039eec3f9a983d9458e9bfb4eae1be Mon Sep 17 00:00:00 2001 From: ravendevhub Date: Sat, 29 Aug 2026 13:36:48 +0630 Subject: [PATCH] feat(diagnostics): add sanitized configuration snapshot for troubleshooting (#695) - Create createDiagnosticSnapshot generating security-hardened runtime metadata - Ensure runtime network, contract bindings, and feature toggles are visible - Guarantee zero secret key or webhook token leakage with strict redaction - Add unit test in config-snapshot.test.ts and docs in docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md --- docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md | 51 ++++++++++ listener/src/utils/config-snapshot.test.ts | 32 ++++++ listener/src/utils/config-snapshot.ts | 109 +++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md create mode 100644 listener/src/utils/config-snapshot.test.ts create mode 100644 listener/src/utils/config-snapshot.ts diff --git a/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md b/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md new file mode 100644 index 00000000..86fd9893 --- /dev/null +++ b/docs/CONFIGURATION_DIAGNOSTIC_SNAPSHOT.md @@ -0,0 +1,51 @@ +# 🔍 Configuration Diagnostic Snapshot Specification + +This document details the sanitized configuration diagnostic representation for NotifyChain (Issue #695). + +--- + +## 1. Overview & Security Guarantee + +When diagnosing operational failures or inspecting runtime health, operators require visibility into runtime parameters, network endpoints, contract bindings, and enabled feature flags without exposing API keys, private keys, or webhook tokens. + +### Security Guarantees: +* **Zero Secret Leakage**: Passwords, private keys (`S...`), and webhook authorization tokens are never included. +* **Redacted Sanitization**: URLs with embedded tokens are masked automatically. + +--- + +## 2. Snapshot Schema + +```json +{ + "system": { + "nodeEnv": "production", + "nodeVersion": "v20.x", + "uptimeSeconds": 3600, + "timestamp": "2026-08-29T12:00:00.000Z" + }, + "network": { + "networkPassphrase": "Test SDF Network ; September 2015", + "rpcUrl": "https://soroban-testnet.stellar.org", + "pollIntervalMs": 5000 + }, + "contracts": { + "configuredCount": 2, + "addresses": ["CA7...", "CB8..."] + }, + "features": { + "analyticsEnabled": true, + "retrySchedulerEnabled": true, + "cleanupEnabled": true, + "deadLetterQueueEnabled": true + }, + "providers": { + "discordEnabled": true, + "webhookEnabled": true + }, + "security": { + "credentialsRedacted": true, + "secretsPresent": true + } +} +``` diff --git a/listener/src/utils/config-snapshot.test.ts b/listener/src/utils/config-snapshot.test.ts new file mode 100644 index 00000000..35e0e6a2 --- /dev/null +++ b/listener/src/utils/config-snapshot.test.ts @@ -0,0 +1,32 @@ +import { createDiagnosticSnapshot } from './config-snapshot'; + +describe('Diagnostic Configuration Snapshot (Issue #695)', () => { + test('generates valid diagnostic snapshot without leaking secrets', () => { + const mockEnv = { + NODE_ENV: 'production', + STELLAR_RPC_URL: 'https://soroban-testnet.stellar.org', + STELLAR_SECRET_KEY: 'SCZANGBA5YHTNYVVV4C3U252E2B6P6IRKD45DCAHSKV2U2B6P6IRKD45', + DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/123/SecretWebhookToken', + CONTRACT_ADDRESSES: JSON.stringify([ + { address: 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA64P7TV5A4W' }, + ]), + POLL_INTERVAL_MS: '3000', + }; + + const snapshot = createDiagnosticSnapshot(mockEnv); + + expect(snapshot.system.nodeEnv).toBe('production'); + expect(snapshot.network.pollIntervalMs).toBe(3000); + expect(snapshot.contracts.configuredCount).toBe(1); + expect(snapshot.contracts.addresses).toContain( + 'CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA64P7TV5A4W' + ); + expect(snapshot.providers.discordEnabled).toBe(true); + expect(snapshot.security.credentialsRedacted).toBe(true); + + // Strict Security Invariant: Ensure raw secret string is NOT in the JSON serialization + const serialized = JSON.stringify(snapshot); + expect(serialized).not.toContain('SecretWebhookToken'); + expect(serialized).not.toContain('SCZANGBA5YHTNYVVV4C3U252E2B6P6IRKD45DCAHSKV2U2B6P6IRKD45'); + }); +}); diff --git a/listener/src/utils/config-snapshot.ts b/listener/src/utils/config-snapshot.ts new file mode 100644 index 00000000..5c70b4d2 --- /dev/null +++ b/listener/src/utils/config-snapshot.ts @@ -0,0 +1,109 @@ +/** + * Diagnostic Configuration Snapshot Utility (Issue #695) + * + * Produces a sanitized, security-hardened snapshot of runtime settings + * and environment configurations for operator diagnostics and troubleshooting. + */ + +import { redactSensitiveData, redactString } from './redact'; + +export interface DiagnosticConfigSnapshot { + system: { + nodeEnv: string; + nodeVersion: string; + uptimeSeconds: number; + timestamp: string; + }; + network: { + networkPassphrase: string; + rpcUrl: string; + pollIntervalMs: number; + horizonUrl?: string; + }; + contracts: { + configuredCount: number; + addresses: string[]; + }; + features: { + analyticsEnabled: boolean; + retrySchedulerEnabled: boolean; + cleanupEnabled: boolean; + deadLetterQueueEnabled: boolean; + }; + providers: { + discordEnabled: boolean; + webhookEnabled: boolean; + }; + security: { + credentialsRedacted: boolean; + secretsPresent: boolean; + }; +} + +/** + * Creates a sanitized diagnostic configuration representation. + * All sensitive values, credentials, and API keys are strictly redacted. + */ +export function createDiagnosticSnapshot( + customEnv: Record = process.env +): DiagnosticConfigSnapshot { + const nodeEnv = customEnv.NODE_ENV || 'development'; + const rpcUrl = customEnv.STELLAR_RPC_URL || customEnv.SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org'; + const passphrase = customEnv.STELLAR_NETWORK_PASSPHRASE || 'Test SDF Network ; September 2015'; + const pollIntervalMs = parseInt(customEnv.POLL_INTERVAL_MS || '5000', 10); + + // Parse configured contract addresses safely + let contractAddresses: string[] = []; + try { + const rawContracts = customEnv.CONTRACT_ADDRESSES; + if (rawContracts) { + const parsed = JSON.parse(rawContracts); + if (Array.isArray(parsed)) { + contractAddresses = parsed.map((c) => (typeof c === 'object' && c?.address ? c.address : String(c))); + } + } + } catch { + // If parse fails, fallback safely + } + + const hasDiscord = Boolean(customEnv.DISCORD_WEBHOOK_URL); + const hasSecrets = Boolean( + customEnv.STELLAR_SECRET_KEY || + customEnv.API_KEYS || + customEnv.JWT_SECRET || + customEnv.DISCORD_WEBHOOK_URL + ); + + return { + system: { + nodeEnv, + nodeVersion: process.version, + uptimeSeconds: Math.floor(process.uptime ? process.uptime() : 0), + timestamp: new Date().toISOString(), + }, + network: { + networkPassphrase: redactString(passphrase), + rpcUrl: redactString(rpcUrl), + pollIntervalMs: isNaN(pollIntervalMs) ? 5000 : pollIntervalMs, + horizonUrl: customEnv.HORIZON_URL ? redactString(customEnv.HORIZON_URL) : undefined, + }, + contracts: { + configuredCount: contractAddresses.length, + addresses: contractAddresses, + }, + features: { + analyticsEnabled: customEnv.ENABLE_ANALYTICS !== 'false', + retrySchedulerEnabled: customEnv.ENABLE_RETRY_SCHEDULER !== 'false', + cleanupEnabled: customEnv.ENABLE_CLEANUP !== 'false', + deadLetterQueueEnabled: customEnv.ENABLE_DLQ !== 'false', + }, + providers: { + discordEnabled: hasDiscord, + webhookEnabled: Boolean(customEnv.WEBHOOK_SECRET || customEnv.WEBHOOK_URL), + }, + security: { + credentialsRedacted: true, + secretsPresent: hasSecrets, + }, + }; +}