autotel-audit 0.4.5 → 0.4.7
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/README.md +38 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -4,21 +4,21 @@ Audit and security-event helpers for `autotel`. Provides structured audit loggin
|
|
|
4
4
|
|
|
5
5
|
## What it provides
|
|
6
6
|
|
|
7
|
-
- **`withAudit(...)
|
|
8
|
-
- **`forceKeepAuditEvent(...)
|
|
9
|
-
- **`setAuditAttributes(...)
|
|
10
|
-
- **`securityEvent(...)`** / **`withSecurity(...)
|
|
11
|
-
- **`createSecuritySignalProcessor(...)
|
|
12
|
-
- **`hashIdentifier(...)
|
|
7
|
+
- **`withAudit(...)`**: Wraps an operation with audit metadata, automatic outcome tagging (success/failure), and optional immediate emit
|
|
8
|
+
- **`forceKeepAuditEvent(...)`**: Marks the current trace to bypass tail-drop sampling for compliance/audit trails
|
|
9
|
+
- **`setAuditAttributes(...)`**: Writes normalized `audit.*` attributes on the active span with automatic type conversion
|
|
10
|
+
- **`securityEvent(...)`** / **`withSecurity(...)`**: Typed security events (OWASP A09-aligned) with a stable `security.*` schema, force-keep by default, a credential-key guard, and automatic counter metrics
|
|
11
|
+
- **`createSecuritySignalProcessor(...)`**: Zero-code security signals derived from the HTTP spans you already have: suspicious-path flagging, denied-response metrics, and per-client auth-failure burst detection
|
|
12
|
+
- **`hashIdentifier(...)`**: Stable one-way digest for correlating PII-bearing identifiers (emails, IPs) without logging raw values
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **Structured Metadata
|
|
17
|
-
- **Automatic Outcome Tagging
|
|
18
|
-
- **Sampling Bypass
|
|
19
|
-
- **Type-Safe Attributes
|
|
20
|
-
- **Request Context Integration
|
|
21
|
-
- **Compliance Ready
|
|
16
|
+
- **Structured Metadata**: Enforce consistent audit schemas with `AuditMetadata` interface
|
|
17
|
+
- **Automatic Outcome Tagging**: Operations auto-tagged as `success` or `failure` (override with explicit `outcome` field)
|
|
18
|
+
- **Sampling Bypass**: Force critical audit events through tail-sampling with `forceKeepAuditEvent()` or `options.forceKeep`
|
|
19
|
+
- **Type-Safe Attributes**: Automatic serialization of complex types (Objects, Dates, Arrays) to OpenTelemetry-compatible values
|
|
20
|
+
- **Request Context Integration**: Propagates actor ID, resource, and action across structured logs
|
|
21
|
+
- **Compliance Ready**: Emit audit events immediately (`emitNow: true`) for real-time compliance systems
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
@@ -47,13 +47,13 @@ Wraps an async operation with audit metadata and handles success/failure outcome
|
|
|
47
47
|
|
|
48
48
|
**Parameters:**
|
|
49
49
|
|
|
50
|
-
- `metadata: AuditMetadata
|
|
51
|
-
- `fn: (ctx, logger) => Promise<T
|
|
52
|
-
- `options?: WithAuditOptions
|
|
53
|
-
- `emitNow?: boolean
|
|
54
|
-
- `forceKeep?: boolean
|
|
55
|
-
- `ctx?: AuditContext
|
|
56
|
-
- `logger?: RequestLogger
|
|
50
|
+
- `metadata: AuditMetadata`: Audit event metadata (action, resource, actor, etc.)
|
|
51
|
+
- `fn: (ctx, logger) => Promise<T>`: Async function receiving audit context and request logger
|
|
52
|
+
- `options?: WithAuditOptions`: Optional configuration:
|
|
53
|
+
- `emitNow?: boolean`: Immediately emit the audit event (default: false)
|
|
54
|
+
- `forceKeep?: boolean`: Force event through tail-sampling (default: true)
|
|
55
|
+
- `ctx?: AuditContext`: Provide custom audit context (auto-resolved from trace if omitted)
|
|
56
|
+
- `logger?: RequestLogger`: Override the request logger instance
|
|
57
57
|
|
|
58
58
|
**Example with custom context:**
|
|
59
59
|
|
|
@@ -233,10 +233,10 @@ securityEvent({
|
|
|
233
233
|
|
|
234
234
|
### Design guarantees
|
|
235
235
|
|
|
236
|
-
- **Force-keep by default
|
|
237
|
-
- **Credential-key guard
|
|
238
|
-
- **Stable schema
|
|
239
|
-
- **Hash, don't log
|
|
236
|
+
- **Force-keep by default**: an attack you sampled away is an attack you cannot investigate. Opt out per-event with `forceKeep: false` for high-volume info events.
|
|
237
|
+
- **Credential-key guard**: values under credential-shaped keys (`token`, `apiKey`, `password`, …, reusing autotel core's `REDACTOR_PATTERNS.sensitiveKey`) are never emitted, even by accident. Dropped key names are recorded in `security.dropped_keys`.
|
|
238
|
+
- **Stable schema**: `security.event` / `security.category` / `security.outcome` / `security.severity` are always present; everything else flattens under `security.*`.
|
|
239
|
+
- **Hash, don't log**: `hashIdentifier(value, { salt })` produces a stable sha256 digest for emails/IPs. Never log secrets, hashed or not.
|
|
240
240
|
|
|
241
241
|
### Wrapping security-sensitive operations
|
|
242
242
|
|
|
@@ -264,14 +264,14 @@ await withSecurity(
|
|
|
264
264
|
|
|
265
265
|
### `securityEvent` vs `withAudit`
|
|
266
266
|
|
|
267
|
-
- **`withAudit
|
|
268
|
-
- **`securityEvent
|
|
267
|
+
- **`withAudit`**: compliance trail for business operations ("who did what to which resource").
|
|
268
|
+
- **`securityEvent`**: detection signal for security-relevant behaviour ("is the system being abused"). Categories, outcomes, and severities are closed unions so alerting rules don't drift.
|
|
269
269
|
|
|
270
270
|
They compose: a sensitive admin operation can carry both an `audit.*` trail and a `security.*` event.
|
|
271
271
|
|
|
272
272
|
### Metrics for alerting
|
|
273
273
|
|
|
274
|
-
Every `securityEvent()` also increments the `autotel.security.events` counter (attributes: `event`, `category`, `outcome`, `severity`) so security teams can alert on **rates
|
|
274
|
+
Every `securityEvent()` also increments the `autotel.security.events` counter (attributes: `event`, `category`, `outcome`, `severity`) so security teams can alert on **rates**, failed-login spikes, denied-access bursts, without log-based alerting. Disable per-event with `metrics: false`.
|
|
275
275
|
|
|
276
276
|
> Cardinality: the event name is a counter attribute. Keep names to a stable catalogue; never interpolate user input into them.
|
|
277
277
|
|
|
@@ -281,7 +281,7 @@ This package's job ends at emitting **structured, correlated, redaction-safe, sa
|
|
|
281
281
|
|
|
282
282
|
## Zero-Code Security Signals
|
|
283
283
|
|
|
284
|
-
Most security-relevant traffic never reaches your handlers
|
|
284
|
+
Most security-relevant traffic never reaches your handlers. Scanners probing `/.env`, traversal attempts, credential stuffing producing 401 storms. `createSecuritySignalProcessor()` derives security signals from the **HTTP spans your instrumentation already produces**, with no per-route code:
|
|
285
285
|
|
|
286
286
|
```ts
|
|
287
287
|
import { init } from 'autotel';
|
|
@@ -301,19 +301,19 @@ init({
|
|
|
301
301
|
|
|
302
302
|
What it does:
|
|
303
303
|
|
|
304
|
-
| Signal
|
|
305
|
-
|
|
304
|
+
| Signal | When | Output |
|
|
305
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
306
306
|
| **Suspicious request** | Request path matches a probe pattern (path traversal, `/.env` / `/.git` / `/etc/passwd` probes, SQLi/XSS probes, null bytes) | Span flagged `security.suspicious_request=true` + `security.signal=<pattern>`, **force-kept through tail sampling**, `autotel.security.http.suspicious` counter, `onSignal` callback |
|
|
307
|
-
| **Denied response**
|
|
308
|
-
| **Auth-failure burst** | One client crosses N denied responses (default 10) inside a sliding window (default 60s), keyed by `client.address`
|
|
307
|
+
| **Denied response** | Response status is 401/403/429 (configurable) | `autotel.security.http.denied{status}` counter |
|
|
308
|
+
| **Auth-failure burst** | One client crosses N denied responses (default 10) inside a sliding window (default 60s), keyed by `client.address` | `autotel.security.anomaly` counter + `onSignal` callback: fired **once per crossing**, so alert volume stays bounded under attack |
|
|
309
309
|
|
|
310
|
-
Why this pairing matters: a credential-stuffing run at 10% baseline sampling is invisible in traces and a `/.env` probe is one boring 404 in your logs
|
|
310
|
+
Why this pairing matters: a credential-stuffing run at 10% baseline sampling is invisible in traces and a `/.env` probe is one boring 404 in your logs. But flagged spans **bypass tail sampling**, and the counters give security teams something to alert on. The interesting traffic is guaranteed to exist in your backend.
|
|
311
311
|
|
|
312
312
|
Design notes:
|
|
313
313
|
|
|
314
|
-
- Patterns are **conservative** (scanner/probe traffic, not a WAF)
|
|
315
|
-
- Burst tracking is **per-process** with bounded memory (`maxKeys`, default 10k clients, oldest evicted)
|
|
316
|
-
- Both metric emission and your `onSignal` callback are guarded
|
|
314
|
+
- Patterns are **conservative** (scanner/probe traffic, not a WAF): `union+station+select` in a search query does not flag. Extend with `extraPatterns`.
|
|
315
|
+
- Burst tracking is **per-process** with bounded memory (`maxKeys`, default 10k clients, oldest evicted): random-IP floods can't grow the map forever. For fleet-wide correlation, alert on the metrics in your backend instead.
|
|
316
|
+
- Both metric emission and your `onSignal` callback are guarded: they can never break the span pipeline.
|
|
317
317
|
|
|
318
318
|
## Integration with Observability Backends
|
|
319
319
|
|
|
@@ -325,6 +325,6 @@ Audit attributes are standard OpenTelemetry span attributes and work with any OT
|
|
|
325
325
|
|
|
326
326
|
## See Also
|
|
327
327
|
|
|
328
|
-
- **[Advanced Features](/advanced)
|
|
329
|
-
- **[Request Logging](/integrations/logging)
|
|
330
|
-
- **[Autotel Core](/)
|
|
328
|
+
- **[Advanced Features](/advanced)**: Trace helpers, metadata flattening, isolated tracer providers
|
|
329
|
+
- **[Request Logging](/integrations/logging)**: Structured request context and event emission
|
|
330
|
+
- **[Autotel Core](/)**: `trace()`, `span()`, and request context patterns
|
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/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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"}
|
|
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<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n >,\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(\n value: number,\n attributes?: Record<string, string | number | boolean>,\n ): 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' | 'failure' | 'denied' | 'blocked' | '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: string | number | boolean | string[] | number[] | 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(\n flattenSecurityAttributes(metadata),\n )) {\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 =\n 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(\n metadata.severity ?? 'info',\n 'error',\n ),\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(\n key: string,\n now: number,\n weight = 1,\n ): { 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\n ? fromAttr\n : 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, [\n 'gen_ai.usage.input_tokens',\n ]);\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 && {\n targetType: 'tool',\n targetId: toolName,\n }),\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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RequestLogger } from "autotel";
|
|
2
2
|
import { SecuritySeverity } from "autotel/security-schema";
|
|
3
|
-
|
|
4
3
|
//#region src/context.d.ts
|
|
5
4
|
interface AuditContext {
|
|
6
5
|
traceId: string;
|
|
@@ -243,13 +242,15 @@ interface LlmSignalOptions {
|
|
|
243
242
|
* Off unless configured.
|
|
244
243
|
*/
|
|
245
244
|
tokenBudget?: {
|
|
246
|
-
budget: number;
|
|
245
|
+
budget: number;
|
|
246
|
+
/** Window size in milliseconds. Default 300_000 (5 min). */
|
|
247
247
|
windowMs?: number;
|
|
248
248
|
/**
|
|
249
249
|
* Span attribute identifying the consumer. Default `enduser.id`
|
|
250
250
|
* (falls back to `client.address`).
|
|
251
251
|
*/
|
|
252
|
-
keyAttribute?: string;
|
|
252
|
+
keyAttribute?: string;
|
|
253
|
+
/** Max distinct keys tracked (oldest evicted). Default 10_000. */
|
|
253
254
|
maxKeys?: number;
|
|
254
255
|
};
|
|
255
256
|
}
|
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/mcp-bridge.ts","../src/index.ts"],"mappings":"
|
|
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":";;;UAEiB;EACf;EACA;EACA;EACA,aAAa,aAAa;EAC1B,cACE,OAAO;;;;;;;;;;;;KA0DC;;;;;;;KClCA;KAYA;;;;;KAOA;UA8BK;;EAEf,MAAM;EACN,UAAU;EACV,SAAS;;EAET,WAAW;;EAEX;EACA;EACA;EACA;;EAEA;GACC;;UAGc;EACf,MAAM;;;;;;EAMN;EACA;EACA,SAAS;;;;;;;;;EAST;;;;;;EAMA,mBAAmB;;KAGT,sBAAsB;UAExB;EACR,aACE,aACA;;iBAyEY,6BACd,MAAM,uBACN,UAAU,uBACV,UAAS,KAAK;;;;;;;;;;;;;;;;;;;;iBAsCA,cACd,UAAU,uBACV,UAAS;;;;;;;;;;;;;;iBA2DW,aAAa,GACjC,UAAU,uBACV,KAAK,KAAK,cAAc,QAAQ,kBAAkB,IAAI,QAAQ,IAC9D,UAAS,sBACR,QAAQ;UAmCM;;EAEf;;EAEA;;;;;;;;iBASc,eACd,eACA,UAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;KC7TN,6CAID,mCACA,mCACA;UAEM;EACR,YAAY,eAAe;EAC3B;IAAgB;;EAChB,aAAa,aAAa,OAAO;;UAGzB;EACR,YAAY,eAAe;EAC3B;IAAgB;;;UAGD;EACf,QAAQ,MAAM,iBAAiB;EAC/B,MAAM,MAAM;EACZ,YAAY;EACZ,cAAc;;UAGC;EACf;;EAEA;;EAEA;;UAGe;EACf;;EAEA;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;;EAEA;;EAEA;EACA;;KAGU,iBACR,0BACA,yBACA,2BACA,uBACA;UAEa;;EAEf;;EAEA;;EAEA;;;;;EAKA;;EAEA;;UAGe;;;;;EAKf;;;;;;EAMA;IACE;;IAEA;;;;;IAKA;;IAEA;;;UAIa;;EAEf;;EAEA,gBAAgB,eAAe;;EAE/B;;EAEA;;EAEA,QAAQ;;;;;EAKR,MAAM;;;;;;EAMN;;EAEA;;EAEA;;EAEA,YAAY,QAAQ;;EAEpB;;;;;;cAOW,6BAA6B,eAAe;iBAsKzC,8BACd,UAAS,iCACR;;;;;;;;;;;;;;;;;;;;;;UCxVc;;EAEf;;EAEA,aAAa;;UAGE;EACf;;iBAGc,uBACd,UAAS,2BACR;;;;;;;;UCxBc;EACf,MAAM;EACN;EACA,SAAS;EACT,WAAW;EACX;EACA;EACA;EACA;GACC;;UAGc,sCAAsC;;EAErD,MAAM;;;;;;;;;;;;;;;;;iBAkBQ,6BACd,UAAS,iCACP,UAAU;;;UCtBG;EACf;EACA;EACA;EACA;EACA;GACC;;UAGc;EACf,MAAM;EACN;EACA;EACA,SAAS;;;;;EAKT,mBAAmB;;iBAuBL,oBAAoB,MAAM;iBAQ1B,mBACd,UAAU,eACV,MAAM;iBAOc,UAAU,GAC9B,UAAU,eACV,KAAK,KAAK,cAAc,QAAQ,kBAAkB,IAAI,QAAQ,IAC9D,UAAS,mBACR,QAAQ"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RequestLogger } from "autotel";
|
|
2
2
|
import { SecuritySeverity } from "autotel/security-schema";
|
|
3
|
-
|
|
4
3
|
//#region src/context.d.ts
|
|
5
4
|
interface AuditContext {
|
|
6
5
|
traceId: string;
|
|
@@ -243,13 +242,15 @@ interface LlmSignalOptions {
|
|
|
243
242
|
* Off unless configured.
|
|
244
243
|
*/
|
|
245
244
|
tokenBudget?: {
|
|
246
|
-
budget: number;
|
|
245
|
+
budget: number;
|
|
246
|
+
/** Window size in milliseconds. Default 300_000 (5 min). */
|
|
247
247
|
windowMs?: number;
|
|
248
248
|
/**
|
|
249
249
|
* Span attribute identifying the consumer. Default `enduser.id`
|
|
250
250
|
* (falls back to `client.address`).
|
|
251
251
|
*/
|
|
252
|
-
keyAttribute?: string;
|
|
252
|
+
keyAttribute?: string;
|
|
253
|
+
/** Max distinct keys tracked (oldest evicted). Default 10_000. */
|
|
253
254
|
maxKeys?: number;
|
|
254
255
|
};
|
|
255
256
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"mappings":";;;UAEiB;EACf;EACA;EACA;EACA,aAAa,aAAa;EAC1B,cACE,OAAO;;;;;;;;;;;;KA0DC;;;;;;;KClCA;KAYA;;;;;KAOA;UA8BK;;EAEf,MAAM;EACN,UAAU;EACV,SAAS;;EAET,WAAW;;EAEX;EACA;EACA;EACA;;EAEA;GACC;;UAGc;EACf,MAAM;;;;;;EAMN;EACA;EACA,SAAS;;;;;;;;;EAST;;;;;;EAMA,mBAAmB;;KAGT,sBAAsB;UAExB;EACR,aACE,aACA;;iBAyEY,6BACd,MAAM,uBACN,UAAU,uBACV,UAAS,KAAK;;;;;;;;;;;;;;;;;;;;iBAsCA,cACd,UAAU,uBACV,UAAS;;;;;;;;;;;;;;iBA2DW,aAAa,GACjC,UAAU,uBACV,KAAK,KAAK,cAAc,QAAQ,kBAAkB,IAAI,QAAQ,IAC9D,UAAS,sBACR,QAAQ;UAmCM;;EAEf;;EAEA;;;;;;;;iBASc,eACd,eACA,UAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;KC7TN,6CAID,mCACA,mCACA;UAEM;EACR,YAAY,eAAe;EAC3B;IAAgB;;EAChB,aAAa,aAAa,OAAO;;UAGzB;EACR,YAAY,eAAe;EAC3B;IAAgB;;;UAGD;EACf,QAAQ,MAAM,iBAAiB;EAC/B,MAAM,MAAM;EACZ,YAAY;EACZ,cAAc;;UAGC;EACf;;EAEA;;EAEA;;UAGe;EACf;;EAEA;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;;EAEA;EACA;EACA;;UAGe;EACf;;EAEA;;EAEA;;EAEA;EACA;;KAGU,iBACR,0BACA,yBACA,2BACA,uBACA;UAEa;;EAEf;;EAEA;;EAEA;;;;;EAKA;;EAEA;;UAGe;;;;;EAKf;;;;;;EAMA;IACE;;IAEA;;;;;IAKA;;IAEA;;;UAIa;;EAEf;;EAEA,gBAAgB,eAAe;;EAE/B;;EAEA;;EAEA,QAAQ;;;;;EAKR,MAAM;;;;;;EAMN;;EAEA;;EAEA;;EAEA,YAAY,QAAQ;;EAEpB;;;;;;cAOW,6BAA6B,eAAe;iBAsKzC,8BACd,UAAS,iCACR;;;;;;;;;;;;;;;;;;;;;;UCxVc;;EAEf;;EAEA,aAAa;;UAGE;EACf;;iBAGc,uBACd,UAAS,2BACR;;;;;;;;UCxBc;EACf,MAAM;EACN;EACA,SAAS;EACT,WAAW;EACX;EACA;EACA;EACA;GACC;;UAGc,sCAAsC;;EAErD,MAAM;;;;;;;;;;;;;;;;;iBAkBQ,6BACd,UAAS,iCACP,UAAU;;;UCtBG;EACf;EACA;EACA;EACA;EACA;GACC;;UAGc;EACf,MAAM;EACN;EACA;EACA,SAAS;;;;;EAKT,mBAAmB;;iBAuBL,oBAAoB,MAAM;iBAQ1B,mBACd,UAAU,eACV,MAAM;iBAOc,UAAU,GAC9B,UAAU,eACV,KAAK,KAAK,cAAc,QAAQ,kBAAkB,IAAI,QAAQ,IAC9D,UAAS,mBACR,QAAQ"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"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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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"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<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n >,\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(\n value: number,\n attributes?: Record<string, string | number | boolean>,\n ): 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' | 'failure' | 'denied' | 'blocked' | '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: string | number | boolean | string[] | number[] | 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(\n flattenSecurityAttributes(metadata),\n )) {\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 =\n 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(\n metadata.severity ?? 'info',\n 'error',\n ),\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(\n key: string,\n now: number,\n weight = 1,\n ): { 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\n ? fromAttr\n : 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, [\n 'gen_ai.usage.input_tokens',\n ]);\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 && {\n targetType: 'tool',\n targetId: toolName,\n }),\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-audit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Audit-focused helpers for Autotel (force-keep + structured audit instrumentation)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
"author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"autotel": "
|
|
30
|
+
"autotel": "5.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^26.1.
|
|
34
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
35
|
-
"@typescript-eslint/parser": "^8.
|
|
33
|
+
"@types/node": "^26.1.1",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.65.0",
|
|
36
36
|
"eslint-config-prettier": "^10.1.8",
|
|
37
37
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
38
|
-
"tsdown": "^0.22.
|
|
38
|
+
"tsdown": "^0.22.12",
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"typescript-eslint": "^8.
|
|
40
|
+
"typescript-eslint": "^8.65.0",
|
|
41
41
|
"vitest": "^4.1.10"
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|