autotel-audit 0.3.1 → 0.4.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.
- package/dist/index.cjs +107 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +69 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/context.ts +0 -145
- package/src/index.test.ts +0 -183
- package/src/index.ts +0 -153
- package/src/lazy-counter.ts +0 -24
- package/src/security-heartbeat.test.ts +0 -65
- package/src/security-heartbeat.ts +0 -63
- package/src/security-signals.test.ts +0 -490
- package/src/security-signals.ts +0 -472
- package/src/security.test.ts +0 -342
- package/src/security.ts +0 -334
package/dist/index.cjs
CHANGED
|
@@ -133,6 +133,15 @@ function countSecurityEvent(metadata) {
|
|
|
133
133
|
severity: metadata.severity ?? "info"
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
+
function applySecurityEventAttributes(sink, metadata, options = {}) {
|
|
137
|
+
if (options.metrics !== false) countSecurityEvent(metadata);
|
|
138
|
+
if (options.forceKeep !== false) {
|
|
139
|
+
sink.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
140
|
+
sink.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_KEEP, true);
|
|
141
|
+
sink.setAttribute(autotel_security_schema.SECURITY_ATTR.forceKeep, true);
|
|
142
|
+
}
|
|
143
|
+
for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) sink.setAttribute(key, value);
|
|
144
|
+
}
|
|
136
145
|
/**
|
|
137
146
|
* Record a security event on the active trace and request logger.
|
|
138
147
|
*
|
|
@@ -321,6 +330,21 @@ function resolveLlmConfig(option) {
|
|
|
321
330
|
}
|
|
322
331
|
};
|
|
323
332
|
}
|
|
333
|
+
const MCP_TOOL_UNTRUSTED = "mcp.tool.untrusted_content";
|
|
334
|
+
const MCP_TOOL_DESTRUCTIVE = "mcp.tool.destructive";
|
|
335
|
+
const MCP_TOOL_NAME = "mcp.tool.name";
|
|
336
|
+
function pruneExpiredUntrustedTraces(hits, cutoff) {
|
|
337
|
+
for (const [traceId, hit] of hits) if (hit.timestamp < cutoff) hits.delete(traceId);
|
|
338
|
+
}
|
|
339
|
+
function readTraceId(span) {
|
|
340
|
+
const fromContext = span.spanContext?.traceId;
|
|
341
|
+
if (typeof fromContext === "string" && fromContext.length > 0) return fromContext;
|
|
342
|
+
const fromAttr = readAttribute(span.attributes, ["trace_id"]);
|
|
343
|
+
return typeof fromAttr === "string" && fromAttr.length > 0 ? fromAttr : void 0;
|
|
344
|
+
}
|
|
345
|
+
function readBooleanAttribute(attributes, key) {
|
|
346
|
+
return readAttribute(attributes, [key]) === true;
|
|
347
|
+
}
|
|
324
348
|
function createSecuritySignalProcessor(options = {}) {
|
|
325
349
|
const detect = options.detectSuspiciousRequests !== false;
|
|
326
350
|
const forceKeep = options.forceKeepSuspicious !== false;
|
|
@@ -333,6 +357,9 @@ function createSecuritySignalProcessor(options = {}) {
|
|
|
333
357
|
};
|
|
334
358
|
const burst = resolveBurstConfig(options.burst);
|
|
335
359
|
const llm = resolveLlmConfig(options.llm);
|
|
360
|
+
const detectActionChains = options.detectSuspiciousActionChains !== false;
|
|
361
|
+
const actionChainWindowMs = options.actionChainWindowMs ?? 3e5;
|
|
362
|
+
const untrustedByTrace = /* @__PURE__ */ new Map();
|
|
336
363
|
const counters = {
|
|
337
364
|
suspicious: lazyCounter(autotel_security_schema.SECURITY_METRICS.httpSuspicious, "Requests matching suspicious-path patterns"),
|
|
338
365
|
denied: lazyCounter(autotel_security_schema.SECURITY_METRICS.httpDenied, "HTTP responses with denied status codes (401/403/429)"),
|
|
@@ -406,8 +433,53 @@ function createSecuritySignalProcessor(options = {}) {
|
|
|
406
433
|
});
|
|
407
434
|
}
|
|
408
435
|
}
|
|
436
|
+
function checkSuspiciousActionChain(span) {
|
|
437
|
+
if (!detectActionChains) return;
|
|
438
|
+
const traceId = readTraceId(span);
|
|
439
|
+
if (!traceId) return;
|
|
440
|
+
const nowMs = now();
|
|
441
|
+
pruneExpiredUntrustedTraces(untrustedByTrace, nowMs - actionChainWindowMs);
|
|
442
|
+
if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
|
|
443
|
+
const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
|
|
444
|
+
untrustedByTrace.set(traceId, {
|
|
445
|
+
timestamp: nowMs,
|
|
446
|
+
...typeof toolName === "string" && { toolName }
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) return;
|
|
451
|
+
const prior = untrustedByTrace.get(traceId);
|
|
452
|
+
if (!prior) return;
|
|
453
|
+
untrustedByTrace.delete(traceId);
|
|
454
|
+
const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
|
|
455
|
+
const elapsedMs = nowMs - prior.timestamp;
|
|
456
|
+
count("anomaly", { signal: "llm_action_chain_suspicious" });
|
|
457
|
+
emit({
|
|
458
|
+
signal: "llm_action_chain_suspicious",
|
|
459
|
+
traceId,
|
|
460
|
+
elapsedMs,
|
|
461
|
+
...typeof toolName === "string" && { toolName },
|
|
462
|
+
...prior.toolName !== void 0 && { untrustedTool: prior.toolName }
|
|
463
|
+
});
|
|
464
|
+
applySecurityEventAttributes(span, {
|
|
465
|
+
name: "llm.action_chain.suspicious",
|
|
466
|
+
category: "llm",
|
|
467
|
+
outcome: "denied",
|
|
468
|
+
severity: "warning",
|
|
469
|
+
reason: "untrusted_then_destructive",
|
|
470
|
+
targetType: "trace",
|
|
471
|
+
targetId: traceId,
|
|
472
|
+
...typeof toolName === "string" && { destructiveTool: toolName },
|
|
473
|
+
...prior.toolName !== void 0 && { untrustedTool: prior.toolName },
|
|
474
|
+
elapsedMs
|
|
475
|
+
}, {
|
|
476
|
+
forceKeep: true,
|
|
477
|
+
metrics: false
|
|
478
|
+
});
|
|
479
|
+
}
|
|
409
480
|
return {
|
|
410
481
|
onStart(span) {
|
|
482
|
+
checkSuspiciousActionChain(span);
|
|
411
483
|
if (!detect) return;
|
|
412
484
|
const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);
|
|
413
485
|
if (typeof target !== "string" || target.length === 0) return;
|
|
@@ -461,6 +533,39 @@ function startSecurityHeartbeat(options = {}) {
|
|
|
461
533
|
} };
|
|
462
534
|
}
|
|
463
535
|
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/mcp-bridge.ts
|
|
538
|
+
/**
|
|
539
|
+
* Create a bridge callback for MCP security observability → `securityEvent()`.
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```typescript
|
|
543
|
+
* import { createMcpSecurityEventBridge } from 'autotel-audit';
|
|
544
|
+
* import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';
|
|
545
|
+
*
|
|
546
|
+
* instrumentMcpClient(client, {
|
|
547
|
+
* securityClassifier: heuristicInjectionClassifier(),
|
|
548
|
+
* bridgeSecurityEvents: true,
|
|
549
|
+
* securityEventBridge: createMcpSecurityEventBridge(),
|
|
550
|
+
* });
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
function createMcpSecurityEventBridge(options = {}) {
|
|
554
|
+
return (metadata) => {
|
|
555
|
+
const { toolName, verdict, source, ...rest } = metadata;
|
|
556
|
+
securityEvent({
|
|
557
|
+
...rest,
|
|
558
|
+
category: "llm",
|
|
559
|
+
...toolName !== void 0 && {
|
|
560
|
+
targetType: "tool",
|
|
561
|
+
targetId: toolName
|
|
562
|
+
},
|
|
563
|
+
...verdict !== void 0 && { verdict },
|
|
564
|
+
...source !== void 0 && { source }
|
|
565
|
+
}, options);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
464
569
|
//#endregion
|
|
465
570
|
//#region src/index.ts
|
|
466
571
|
function flattenAuditAttributes(metadata) {
|
|
@@ -527,6 +632,8 @@ async function withAudit(metadata, fn, options = {}) {
|
|
|
527
632
|
|
|
528
633
|
//#endregion
|
|
529
634
|
exports.SUSPICIOUS_REQUEST_PATTERNS = SUSPICIOUS_REQUEST_PATTERNS;
|
|
635
|
+
exports.applySecurityEventAttributes = applySecurityEventAttributes;
|
|
636
|
+
exports.createMcpSecurityEventBridge = createMcpSecurityEventBridge;
|
|
530
637
|
exports.createSecuritySignalProcessor = createSecuritySignalProcessor;
|
|
531
638
|
exports.forceKeepAuditEvent = forceKeepAuditEvent;
|
|
532
639
|
exports.hashIdentifier = hashIdentifier;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["otelTrace","SECURITY_ATTR","REDACTOR_PATTERNS","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_DENIED_STATUSES","SECURITY_METRICS","HTTP_STATUS_ATTRIBUTES","SECURITY_ATTR","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP"],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n return {\n onStart(span) {\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["otelTrace","SECURITY_ATTR","REDACTOR_PATTERNS","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_DENIED_STATUSES","SECURITY_METRICS","HTTP_STATUS_ATTRIBUTES","SECURITY_ATTR","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP"],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -32,7 +32,7 @@ type SecurityOutcome = 'success' | 'failure' | 'denied' | 'blocked' | 'error';
|
|
|
32
32
|
* Well-known security event names. Free-form names are allowed —
|
|
33
33
|
* this union exists for autocomplete and consistency across services.
|
|
34
34
|
*/
|
|
35
|
-
type SuggestedSecurityEventName = 'auth.login.success' | 'auth.login.failed' | 'auth.mfa.failed' | 'auth.session.revoked' | 'auth.password.reset' | 'auth.account.locked' | 'access.denied' | 'access.role.changed' | 'access.permission.changed' | 'access.tenant.violation' | 'admin.action' | 'config.changed' | 'secret.accessed' | 'secret.rotation.failed' | 'api_key.created' | 'api_key.revoked' | 'rate_limit.exceeded' | 'validation.failed' | 'webhook.signature.failed' | 'dependency.scan.failed' | 'llm.prompt_injection.detected' | 'llm.tool_call.denied' | 'llm.output.blocked';
|
|
35
|
+
type SuggestedSecurityEventName = 'auth.login.success' | 'auth.login.failed' | 'auth.mfa.failed' | 'auth.session.revoked' | 'auth.password.reset' | 'auth.account.locked' | 'access.denied' | 'access.role.changed' | 'access.permission.changed' | 'access.tenant.violation' | 'admin.action' | 'config.changed' | 'secret.accessed' | 'secret.rotation.failed' | 'api_key.created' | 'api_key.revoked' | 'rate_limit.exceeded' | 'validation.failed' | 'webhook.signature.failed' | 'dependency.scan.failed' | 'llm.prompt_injection.detected' | 'llm.tool_call.denied' | 'llm.output.blocked' | 'llm.output.budget_exceeded' | 'llm.guard.triggered' | 'llm.action_chain.suspicious' | 'llm.manifest.suspicious' | 'llm.plan.risk.elevated';
|
|
36
36
|
interface SecurityEventMetadata {
|
|
37
37
|
/** Stable, dot-separated event name, e.g. `auth.login.failed`. */
|
|
38
38
|
name: SuggestedSecurityEventName | (string & {});
|
|
@@ -76,6 +76,10 @@ interface SecurityEventOptions {
|
|
|
76
76
|
onMissingContext?: OnMissingContext;
|
|
77
77
|
}
|
|
78
78
|
type WithSecurityOptions = SecurityEventOptions;
|
|
79
|
+
interface SecurityAttributeSink {
|
|
80
|
+
setAttribute(key: string, value: string | number | boolean | string[] | number[] | boolean[]): unknown;
|
|
81
|
+
}
|
|
82
|
+
declare function applySecurityEventAttributes(sink: SecurityAttributeSink, metadata: SecurityEventMetadata, options?: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'>): void;
|
|
79
83
|
/**
|
|
80
84
|
* Record a security event on the active trace and request logger.
|
|
81
85
|
*
|
|
@@ -152,10 +156,16 @@ declare function hashIdentifier(value: string, options?: HashIdentifierOptions):
|
|
|
152
156
|
type AttributeValue = string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>;
|
|
153
157
|
interface MutableSpanLike {
|
|
154
158
|
attributes: Record<string, AttributeValue | undefined>;
|
|
159
|
+
spanContext?: {
|
|
160
|
+
traceId: string;
|
|
161
|
+
};
|
|
155
162
|
setAttribute(key: string, value: AttributeValue): unknown;
|
|
156
163
|
}
|
|
157
164
|
interface ReadableSpanLike {
|
|
158
165
|
attributes: Record<string, AttributeValue | undefined>;
|
|
166
|
+
spanContext?: {
|
|
167
|
+
traceId: string;
|
|
168
|
+
};
|
|
159
169
|
}
|
|
160
170
|
interface SecuritySignalProcessor {
|
|
161
171
|
onStart(span: MutableSpanLike, parentContext?: unknown): void;
|
|
@@ -195,7 +205,17 @@ interface LlmTokenBudgetSignal {
|
|
|
195
205
|
budget: number;
|
|
196
206
|
windowMs: number;
|
|
197
207
|
}
|
|
198
|
-
|
|
208
|
+
interface LlmActionChainSuspiciousSignal {
|
|
209
|
+
signal: 'llm_action_chain_suspicious';
|
|
210
|
+
/** Trace where the suspicious chain was observed. */
|
|
211
|
+
traceId: string;
|
|
212
|
+
/** Tool that followed untrusted-content processing. */
|
|
213
|
+
toolName?: string;
|
|
214
|
+
/** Milliseconds since the untrusted tool call on the same trace. */
|
|
215
|
+
elapsedMs: number;
|
|
216
|
+
untrustedTool?: string;
|
|
217
|
+
}
|
|
218
|
+
type SecuritySignal = SuspiciousRequestSignal | AuthFailureBurstSignal | LlmExcessiveTokensSignal | LlmTokenBudgetSignal | LlmActionChainSuspiciousSignal;
|
|
199
219
|
interface BurstOptions {
|
|
200
220
|
/** HTTP statuses counted toward a burst. Default `[401, 403]`. */
|
|
201
221
|
statuses?: number[];
|
|
@@ -249,6 +269,14 @@ interface SecuritySignalProcessorOptions {
|
|
|
249
269
|
* Enabled with the per-call ceiling by default; pass `false` to disable.
|
|
250
270
|
*/
|
|
251
271
|
llm?: LlmSignalOptions | false;
|
|
272
|
+
/**
|
|
273
|
+
* Detect destructive MCP tool calls that follow untrusted-content tool usage
|
|
274
|
+
* on the same trace (Google's "read email then send externally" pattern).
|
|
275
|
+
* Default true.
|
|
276
|
+
*/
|
|
277
|
+
detectSuspiciousActionChains?: boolean;
|
|
278
|
+
/** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */
|
|
279
|
+
actionChainWindowMs?: number;
|
|
252
280
|
/** Emit `autotel.security.*` metrics. Default true. */
|
|
253
281
|
metrics?: boolean;
|
|
254
282
|
/** Called whenever a signal fires. Keep it fast and non-throwing. */
|
|
@@ -294,6 +322,44 @@ interface SecurityHeartbeat {
|
|
|
294
322
|
}
|
|
295
323
|
declare function startSecurityHeartbeat(options?: SecurityHeartbeatOptions): SecurityHeartbeat;
|
|
296
324
|
//#endregion
|
|
325
|
+
//#region src/mcp-bridge.d.ts
|
|
326
|
+
/**
|
|
327
|
+
* Metadata emitted when MCP protocol-boundary signals are bridged to the
|
|
328
|
+
* unified `security.*` schema. Used by `autotel-mcp-instrumentation` when
|
|
329
|
+
* `bridgeSecurityEvents` is enabled.
|
|
330
|
+
*/
|
|
331
|
+
interface McpBridgedSecurityEvent {
|
|
332
|
+
name: SecurityEventMetadata['name'];
|
|
333
|
+
category: 'llm';
|
|
334
|
+
outcome: SecurityEventMetadata['outcome'];
|
|
335
|
+
severity?: SecurityEventMetadata['severity'];
|
|
336
|
+
reason?: string;
|
|
337
|
+
toolName?: string;
|
|
338
|
+
verdict?: string;
|
|
339
|
+
source?: string;
|
|
340
|
+
[key: string]: unknown;
|
|
341
|
+
}
|
|
342
|
+
interface McpSecurityEventBridgeOptions extends SecurityEventOptions {
|
|
343
|
+
/** Optional fixed audit context for bridged events. */
|
|
344
|
+
ctx?: AuditContext;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Create a bridge callback for MCP security observability → `securityEvent()`.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* import { createMcpSecurityEventBridge } from 'autotel-audit';
|
|
352
|
+
* import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';
|
|
353
|
+
*
|
|
354
|
+
* instrumentMcpClient(client, {
|
|
355
|
+
* securityClassifier: heuristicInjectionClassifier(),
|
|
356
|
+
* bridgeSecurityEvents: true,
|
|
357
|
+
* securityEventBridge: createMcpSecurityEventBridge(),
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
declare function createMcpSecurityEventBridge(options?: McpSecurityEventBridgeOptions): (metadata: McpBridgedSecurityEvent) => void;
|
|
362
|
+
//#endregion
|
|
297
363
|
//#region src/index.d.ts
|
|
298
364
|
interface AuditMetadata {
|
|
299
365
|
action: string;
|
|
@@ -318,5 +384,5 @@ declare function forceKeepAuditEvent(ctx?: AuditContext): void;
|
|
|
318
384
|
declare function setAuditAttributes(metadata: AuditMetadata, ctx?: AuditContext): void;
|
|
319
385
|
declare function withAudit<T>(metadata: AuditMetadata, fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>, options?: WithAuditOptions): Promise<T>;
|
|
320
386
|
//#endregion
|
|
321
|
-
export { type AuditContext, AuditMetadata, AuthFailureBurstSignal, BurstOptions, HashIdentifierOptions, LlmExcessiveTokensSignal, LlmSignalOptions, LlmTokenBudgetSignal, type OnMissingContext, SUSPICIOUS_REQUEST_PATTERNS, SecurityEventCategory, SecurityEventMetadata, SecurityEventOptions, SecurityHeartbeat, SecurityHeartbeatOptions, SecurityOutcome, type SecuritySeverity, SecuritySignal, SecuritySignalProcessor, SecuritySignalProcessorOptions, SuggestedSecurityEventName, SuspiciousRequestSignal, WithAuditOptions, WithSecurityOptions, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
|
|
387
|
+
export { type AuditContext, AuditMetadata, AuthFailureBurstSignal, BurstOptions, HashIdentifierOptions, LlmActionChainSuspiciousSignal, LlmExcessiveTokensSignal, LlmSignalOptions, LlmTokenBudgetSignal, McpBridgedSecurityEvent, McpSecurityEventBridgeOptions, type OnMissingContext, SUSPICIOUS_REQUEST_PATTERNS, SecurityEventCategory, SecurityEventMetadata, SecurityEventOptions, SecurityHeartbeat, SecurityHeartbeatOptions, SecurityOutcome, type SecuritySeverity, SecuritySignal, SecuritySignalProcessor, SecuritySignalProcessorOptions, SuggestedSecurityEventName, SuspiciousRequestSignal, WithAuditOptions, WithSecurityOptions, applySecurityEventAttributes, createMcpSecurityEventBridge, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
|
|
322
388
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/index.ts"],"mappings":";;;;UAEiB,YAAA;EACf,OAAA;EACA,MAAA;EACA,aAAA;EACA,YAAA,CAAa,GAAA,UAAa,KAAA;EAC1B,aAAA,CACE,KAAA,EAAO,MAAM;AAAA;;;;;;;;AAAqE;AAuDtF;;KAAY,gBAAA;;;AA7DZ;;;;AAAA,KC8BY,qBAAA;AAAA,KAYA,eAAA;;;;;KAWA,0BAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"mappings":"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"}
|