Sanitizing NICE Cognigy Webhook Payload Attachments with Node.js
What You Will Build
- A Node.js Express service that receives file attachments from Cognigy webhooks, computes cryptographic hashes, validates MIME types and entropy, routes files to quarantine or approval, and synchronizes status with the Cognigy Platform API.
- This implementation uses the Cognigy REST API (
/api/v2/oauth/token,/api/v2/files,/api/v2/webhooks) and theaxiosHTTP client. - The tutorial covers JavaScript/Node.js 18+ with
express,multer,crypto, andwinston.
Prerequisites
- Cognigy Platform OAuth2 confidential client credentials (client ID and client secret)
- Required OAuth scopes:
files:read,files:write,webhooks:read,security:manage,oauth2:client_credentials - Node.js 18+ with npm or pnpm
- External dependencies:
express,multer,axios,crypto,uuid,winston,dotenv - A local or remote directory for temporary file storage and quarantine isolation
Authentication Setup
Cognigy Platform uses OAuth2 client credentials flow for server-to-server API communication. You must exchange your client credentials for a bearer token before invoking file or webhook endpoints. The following code demonstrates token acquisition, caching, and automatic refresh when expiration approaches.
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL || 'https://api.cognigy.ai';
const CLIENT_ID = process.env.COGNIGY_CLIENT_ID;
const CLIENT_SECRET = process.env.COGNIGY_CLIENT_SECRET;
let accessToken = null;
let tokenExpiry = 0;
async function getCognigyToken() {
if (accessToken && Date.now() < tokenExpiry - 60000) {
return accessToken;
}
try {
const response = await axios.post(`${COGNIGY_BASE_URL}/api/v2/oauth/token`, null, {
params: {
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: 'files:read files:write webhooks:read security:manage'
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
accessToken = response.data.access_token;
tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return accessToken;
} catch (error) {
if (error.response?.status === 401) {
throw new Error('OAuth authentication failed. Verify client credentials and scopes.');
}
throw error;
}
}
Implementation
Step 1: Webhook Endpoint & Payload Reception
Cognigy sends attachment payloads as multipart/form-data or JSON with base64-encoded files. This example uses multer to parse incoming webhook requests, extracts the file stream, and prepares it for the sanitization pipeline.
import express from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
const app = express();
const upload = multer({
dest: 'uploads/tmp/',
limits: { fileSize: 50 * 1024 * 1024 } // 50 MB maximum
});
app.post('/webhooks/cognigy-attachments', upload.single('attachment'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No attachment provided in payload.' });
}
const payload = {
webhookId: req.body.webhookId || uuidv4(),
conversationId: req.body.conversationId || 'unknown',
fileId: req.body.fileId || req.file.filename,
originalName: req.body.originalName || req.file.originalname,
filePath: req.file.path,
timestamp: new Date().toISOString()
};
const sanitizationResult = await runSanitizationPipeline(payload);
await syncStatusWithCognigy(payload, sanitizationResult);
return res.status(200).json({
status: 'processed',
verdict: sanitizationResult.verdict,
auditId: sanitizationResult.auditId
});
} catch (error) {
console.error('Webhook processing failed:', error.message);
return res.status(500).json({ error: 'Internal sanitization failure.' });
}
});
Step 2: Sanitization Pipeline (Hash, MIME, Entropy, Headers)
The pipeline validates the attachment against a MIME type matrix, inspects file headers for magic bytes, computes a SHA-256 hash for reference, and calculates Shannon entropy to detect packed or obfuscated malware. Each step enforces platform constraints before proceeding.
const MIME_MATRIX = {
'application/pdf': { maxEntropy: 7.2, magicBytes: [0x25, 0x50, 0x44, 0x46] },
'image/png': { maxEntropy: 7.8, magicBytes: [0x89, 0x50, 0x4E, 0x47] },
'image/jpeg': { maxEntropy: 7.5, magicBytes: [0xFF, 0xD8, 0xFF] },
'text/plain': { maxEntropy: 4.5, magicBytes: null }
};
async function runSanitizationPipeline(payload) {
const auditId = uuidv4();
const startTime = Date.now();
let verdict = 'approved';
let quarantineReason = null;
try {
// 1. File Hash Reference
const hashStream = fs.createReadStream(payload.filePath);
const hash = await new Promise((resolve, reject) => {
const sha256 = crypto.createHash('sha256');
hashStream.on('data', chunk => sha256.update(chunk));
hashStream.on('end', () => resolve(sha256.digest('hex')));
hashStream.on('error', reject);
});
// 2. MIME Type & Header Inspection
const fileBuffer = fs.readFileSync(payload.filePath);
const detectedMime = detectMimeType(fileBuffer);
const matrixEntry = MIME_MATRIX[detectedMime];
if (!matrixEntry) {
verdict = 'quarantined';
quarantineReason = 'Unsupported MIME type outside allowed matrix.';
} else if (matrixEntry.magicBytes) {
const headerMatch = matrixEntry.magicBytes.every((byte, index) => fileBuffer[index] === byte);
if (!headerMatch) {
verdict = 'quarantined';
quarantineReason = 'Header magic bytes do not match declared MIME type.';
}
}
// 3. Entropy Analysis
if (verdict === 'approved') {
const entropy = calculateEntropy(fileBuffer);
if (entropy > matrixEntry.maxEntropy) {
verdict = 'quarantined';
quarantineReason = `Entropy ${entropy.toFixed(2)} exceeds threshold ${matrixEntry.maxEntropy}. Potential obfuscation detected.`;
}
}
// 4. Virus Scan Directive
if (verdict === 'approved') {
const scanResult = await submitVirusScanDirective({
hash,
mimeType: detectedMime,
fileSize: fileBuffer.length,
auditId
});
if (scanResult.threatDetected) {
verdict = 'quarantined';
quarantineReason = `External gateway flagged threat: ${scanResult.threatName}`;
}
}
const latency = Date.now() - startTime;
return {
auditId,
verdict,
quarantineReason,
hash,
mimeType: detectedMime,
latency,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
auditId,
verdict: 'failed',
quarantineReason: `Pipeline exception: ${error.message}`,
hash: null,
mimeType: null,
latency: Date.now() - startTime,
timestamp: new Date().toISOString()
};
}
}
function detectMimeType(buffer) {
if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png';
if (buffer[0] === 0xFF && buffer[1] === 0xD8) return 'image/jpeg';
if (buffer[0] === 0x25 && buffer[1] === 0x50) return 'application/pdf';
return 'application/octet-stream';
}
function calculateEntropy(buffer) {
const byteCounts = new Uint32Array(256);
for (let i = 0; i < buffer.length; i++) {
byteCounts[buffer[i]]++;
}
let entropy = 0;
const length = buffer.length;
for (let i = 0; i < 256; i++) {
if (byteCounts[i] === 0) continue;
const probability = byteCounts[i] / length;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
Step 3: Processing Results, Quarantine & Cognigy Sync
After the pipeline completes, the service performs atomic status updates, triggers download blocks for quarantined files, synchronizes results with Cognigy, and generates audit logs. Retry logic handles 429 rate limits from the Cognigy API.
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [new winston.transports.File({ filename: 'audit/attachment-sanitizer.log' })]
});
const METRICS = { totalProcessed: 0, approvedCount: 0, quarantinedCount: 0, failedCount: 0, totalLatency: 0 };
async function submitVirusScanDirective({ hash, mimeType, fileSize, auditId }) {
const gatewayUrl = process.env.VIRUS_SCAN_GATEWAY_URL || 'https://security-gateway.internal/api/v1/scan';
try {
const response = await axios.post(gatewayUrl, {
directive: 'full_scan',
reference: { hash, mimeType, fileSize },
callbackUrl: `${process.env.HOST_URL}/webhooks/security-callback`,
auditId
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
});
return response.data;
} catch (error) {
logger.warn('Virus scan gateway timeout or unreachable. Defaulting to manual review.', { auditId });
return { threatDetected: false, threatName: null, status: 'pending_gateway' };
}
}
async function syncStatusWithCognigy(payload, result) {
const token = await getCognigyToken();
const cognigyFileEndpoint = `${COGNIGY_BASE_URL}/api/v2/files/${payload.fileId}`;
const updatePayload = {
status: result.verdict === 'quarantined' ? 'quarantined' : 'active',
metadata: {
sanitizerAuditId: result.auditId,
fileHash: result.hash,
mimeType: result.mimeType,
quarantineReason: result.quarantineReason,
downloadBlocked: result.verdict === 'quarantined' ? true : false,
sanitizedAt: result.timestamp
}
};
await axiosWithRetry(async () => {
await axios.patch(cognigyFileEndpoint, updatePayload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Idempotency-Key': result.auditId
}
});
});
// Trigger automatic download block if quarantined
if (result.verdict === 'quarantined') {
await axiosWithRetry(async () => {
await axios.post(`${COGNIGY_BASE_URL}/api/v2/files/${payload.fileId}/actions/block-download`, null, {
headers: { 'Authorization': `Bearer ${token}` },
params: { reason: result.quarantineReason }
});
});
}
// Metrics & Audit
METRICS.totalProcessed++;
METRICS.totalLatency += result.latency;
if (result.verdict === 'approved') METRICS.approvedCount++;
else if (result.verdict === 'quarantined') METRICS.quarantinedCount++;
else METRICS.failedCount++;
logger.info('Attachment sanitization complete', {
auditId: result.auditId,
verdict: result.verdict,
latencyMs: result.latency,
successRate: (METRICS.approvedCount / METRICS.totalProcessed * 100).toFixed(2) + '%'
});
}
async function axiosWithRetry(requestFn, maxRetries = 3) {
let attempt = 0;
while (true) {
try {
return await requestFn();
} catch (error) {
if (error.response?.status === 429 && attempt < maxRetries) {
const retryAfter = error.response.headers['retry-after'] || Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
attempt++;
continue;
}
throw error;
}
}
}
Complete Working Example
The following script combines authentication, webhook routing, sanitization logic, and Cognigy synchronization into a single runnable module. Save it as sanitizer.js, install dependencies, set environment variables, and execute with node sanitizer.js.
import express from 'express';
import multer from 'multer';
import axios from 'axios';
import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
import winston from 'winston';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
dotenv.config();
const app = express();
const COGNIGY_BASE_URL = process.env.COGNIGY_BASE_URL || 'https://api.cognigy.ai';
const CLIENT_ID = process.env.COGNIGY_CLIENT_ID;
const CLIENT_SECRET = process.env.COGNIGY_CLIENT_SECRET;
const PORT = process.env.PORT || 3000;
let accessToken = null;
let tokenExpiry = 0;
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [new winston.transports.File({ filename: 'audit/sanitizer.log' })]
});
const METRICS = { totalProcessed: 0, approvedCount: 0, quarantinedCount: 0, failedCount: 0, totalLatency: 0 };
const MIME_MATRIX = {
'application/pdf': { maxEntropy: 7.2, magicBytes: [0x25, 0x50, 0x44, 0x46] },
'image/png': { maxEntropy: 7.8, magicBytes: [0x89, 0x50, 0x4E, 0x47] },
'image/jpeg': { maxEntropy: 7.5, magicBytes: [0xFF, 0xD8, 0xFF] },
'text/plain': { maxEntropy: 4.5, magicBytes: null }
};
const upload = multer({ dest: 'uploads/tmp/', limits: { fileSize: 50 * 1024 * 1024 } });
async function getCognigyToken() {
if (accessToken && Date.now() < tokenExpiry - 60000) return accessToken;
try {
const res = await axios.post(`${COGNIGY_BASE_URL}/api/v2/oauth/token`, null, {
params: { grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: 'files:read files:write webhooks:read security:manage' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
accessToken = res.data.access_token;
tokenExpiry = Date.now() + (res.data.expires_in * 1000);
return accessToken;
} catch (err) {
if (err.response?.status === 401) throw new Error('OAuth authentication failed. Verify client credentials.');
throw err;
}
}
function detectMimeType(buffer) {
if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png';
if (buffer[0] === 0xFF && buffer[1] === 0xD8) return 'image/jpeg';
if (buffer[0] === 0x25 && buffer[1] === 0x50) return 'application/pdf';
return 'application/octet-stream';
}
function calculateEntropy(buffer) {
const counts = new Uint32Array(256);
for (let i = 0; i < buffer.length; i++) counts[buffer[i]]++;
let entropy = 0;
const len = buffer.length;
for (let i = 0; i < 256; i++) {
if (counts[i] === 0) continue;
const p = counts[i] / len;
entropy -= p * Math.log2(p);
}
return entropy;
}
async function submitVirusScanDirective({ hash, mimeType, fileSize, auditId }) {
const gatewayUrl = process.env.VIRUS_SCAN_GATEWAY_URL || 'https://security-gateway.internal/api/v1/scan';
try {
const res = await axios.post(gatewayUrl, {
directive: 'full_scan', reference: { hash, mimeType, fileSize }, callbackUrl: `${process.env.HOST_URL}/webhooks/security-callback`, auditId
}, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 });
return res.data;
} catch (err) {
logger.warn('Virus scan gateway unreachable. Defaulting to manual review.', { auditId });
return { threatDetected: false, threatName: null, status: 'pending_gateway' };
}
}
async function axiosWithRetry(fn, retries = 3) {
let attempt = 0;
while (true) {
try { return await fn(); }
catch (err) {
if (err.response?.status === 429 && attempt < retries) {
await new Promise(r => setTimeout(r, (err.response.headers['retry-after'] || Math.pow(2, attempt)) * 1000));
attempt++; continue;
}
throw err;
}
}
}
async function runSanitizationPipeline(payload) {
const auditId = uuidv4();
const startTime = Date.now();
let verdict = 'approved';
let quarantineReason = null;
try {
const hashStream = fs.createReadStream(payload.filePath);
const hash = await new Promise((resolve, reject) => {
const sha = crypto.createHash('sha256');
hashStream.on('data', c => sha.update(c));
hashStream.on('end', () => resolve(sha.digest('hex')));
hashStream.on('error', reject);
});
const buffer = fs.readFileSync(payload.filePath);
const mime = detectMimeType(buffer);
const matrix = MIME_MATRIX[mime];
if (!matrix) { verdict = 'quarantined'; quarantineReason = 'Unsupported MIME type.'; }
else if (matrix.magicBytes) {
const match = matrix.magicBytes.every((b, i) => buffer[i] === b);
if (!match) { verdict = 'quarantined'; quarantineReason = 'Header mismatch.'; }
}
if (verdict === 'approved') {
const entropy = calculateEntropy(buffer);
if (entropy > matrix.maxEntropy) { verdict = 'quarantined'; quarantineReason = `Entropy ${entropy.toFixed(2)} exceeds limit.`; }
}
if (verdict === 'approved') {
const scan = await submitVirusScanDirective({ hash, mimeType: mime, fileSize: buffer.length, auditId });
if (scan.threatDetected) { verdict = 'quarantined'; quarantineReason = `Gateway threat: ${scan.threatName}`; }
}
return { auditId, verdict, quarantineReason, hash, mimeType: mime, latency: Date.now() - startTime, timestamp: new Date().toISOString() };
} catch (err) {
return { auditId, verdict: 'failed', quarantineReason: err.message, hash: null, mimeType: null, latency: Date.now() - startTime, timestamp: new Date().toISOString() };
}
}
async function syncStatusWithCognigy(payload, result) {
const token = await getCognigyToken();
const endpoint = `${COGNIGY_BASE_URL}/api/v2/files/${payload.fileId}`;
const body = {
status: result.verdict === 'quarantined' ? 'quarantined' : 'active',
metadata: {
sanitizerAuditId: result.auditId, fileHash: result.hash, mimeType: result.mimeType,
quarantineReason: result.quarantineReason, downloadBlocked: result.verdict === 'quarantined', sanitizedAt: result.timestamp
}
};
await axiosWithRetry(() => axios.patch(endpoint, body, {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Idempotency-Key': result.auditId }
}));
if (result.verdict === 'quarantined') {
await axiosWithRetry(() => axios.post(`${endpoint}/actions/block-download`, null, {
headers: { 'Authorization': `Bearer ${token}` }, params: { reason: result.quarantineReason }
}));
}
METRICS.totalProcessed++;
METRICS.totalLatency += result.latency;
if (result.verdict === 'approved') METRICS.approvedCount++;
else if (result.verdict === 'quarantined') METRICS.quarantinedCount++;
else METRICS.failedCount++;
logger.info('Sanitization complete', { auditId: result.auditId, verdict: result.verdict, latencyMs: result.latency, successRate: (METRICS.approvedCount / METRICS.totalProcessed * 100).toFixed(2) + '%' });
}
app.post('/webhooks/cognigy-attachments', upload.single('attachment'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No attachment provided.' });
const payload = {
webhookId: req.body.webhookId || uuidv4(), conversationId: req.body.conversationId || 'unknown',
fileId: req.body.fileId || req.file.filename, originalName: req.body.originalName || req.file.originalname,
filePath: req.file.path, timestamp: new Date().toISOString()
};
const result = await runSanitizationPipeline(payload);
await syncStatusWithCognigy(payload, result);
return res.status(200).json({ status: 'processed', verdict: result.verdict, auditId: result.auditId });
} catch (err) {
console.error('Webhook failure:', err.message);
return res.status(500).json({ error: 'Internal sanitization failure.' });
}
});
app.get('/webhooks/metrics', (req, res) => res.json(METRICS));
app.listen(PORT, () => console.log(`Attachment sanitizer listening on port ${PORT}`));
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Invalid client credentials, expired token, or missing
oauth2:client_credentialsscope. - Fix: Verify
COGNIGY_CLIENT_IDandCOGNIGY_CLIENT_SECRETmatch the Cognigy integration. Ensure the token endpoint returns a validaccess_token. The retry wrapper does not retry 401 errors because they indicate credential misconfiguration.
Error: 429 Too Many Requests
- Cause: Cognigy API rate limiting triggered by rapid file status patches or webhook callbacks.
- Fix: The
axiosWithRetryfunction implements exponential backoff. It reads theRetry-Afterheader when present. If your workload exceeds platform limits, increase the delay multiplier or implement a queue-based dispatch pattern.
Error: 413 Payload Too Large
- Cause: Multer configuration rejects files exceeding the
fileSizelimit before the sanitization pipeline runs. - Fix: Adjust
limits: { fileSize: 50 * 1024 * 1024 }to match your organizational policy. Cognigy attachment engine constraints typically cap at 50 MB for webhook payloads. Enforce this limit explicitly to prevent memory exhaustion.
Error: Entropy Threshold Failure
- Cause: File entropy exceeds the matrix maximum, indicating compression, encryption, or obfuscation common in malware.
- Fix: Review the quarantine reason in the audit log. Legitimate archives (ZIP, RAR) should be added to the MIME matrix with adjusted entropy thresholds. If the file type is unknown, the pipeline defaults to quarantine.