autotel-audit 0.4.3 → 0.4.5
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/LICENSE +191 -21
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/LICENSE
CHANGED
|
@@ -1,21 +1,191 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and
|
|
10
|
+
distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by the
|
|
13
|
+
copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all other
|
|
16
|
+
entities that control, are controlled by, or are under common control with
|
|
17
|
+
that entity. For the purposes of this definition, "control" means (i) the
|
|
18
|
+
power, direct or indirect, to cause the direction or management of such
|
|
19
|
+
entity, whether by contract or otherwise, or (ii) ownership of fifty
|
|
20
|
+
percent (50%) or more of the outstanding shares, or (iii) beneficial
|
|
21
|
+
ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
|
24
|
+
permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation source,
|
|
28
|
+
and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical transformation
|
|
31
|
+
or translation of a Source form, including but not limited to compiled
|
|
32
|
+
object code, generated documentation, and conversions to other media types.
|
|
33
|
+
|
|
34
|
+
"Work" shall mean the work of authorship, whether in Source or Object form,
|
|
35
|
+
made available under the License, as indicated by a copyright notice that
|
|
36
|
+
is included in or attached to the work (an example is provided in the
|
|
37
|
+
Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object form,
|
|
40
|
+
that is based on (or derived from) the Work and for which the editorial
|
|
41
|
+
revisions, annotations, elaborations, or other modifications represent, as
|
|
42
|
+
a whole, an original work of authorship. For the purposes of this License,
|
|
43
|
+
Derivative Works shall not include works that remain separable from, or
|
|
44
|
+
merely link (or bind by name) to the interfaces of, the Work and Derivative
|
|
45
|
+
Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean any work of authorship, including the original
|
|
48
|
+
version of the Work and any modifications or additions to that Work or
|
|
49
|
+
Derivative Works thereof, that is intentionally submitted to Licensor for
|
|
50
|
+
inclusion in the Work by the copyright owner or by an individual or Legal
|
|
51
|
+
Entity authorized to submit on behalf of the copyright owner. For the
|
|
52
|
+
purposes of this definition, "submitted" means any form of electronic,
|
|
53
|
+
verbal, or written communication sent to the Licensor or its
|
|
54
|
+
representatives, including but not limited to communication on electronic
|
|
55
|
+
mailing lists, source code control systems, and issue tracking systems that
|
|
56
|
+
are managed by, or on behalf of, the Licensor for the purpose of discussing
|
|
57
|
+
and improving the Work, but excluding communication that is conspicuously
|
|
58
|
+
marked or otherwise designated in writing by the copyright owner as "Not a
|
|
59
|
+
Contribution."
|
|
60
|
+
|
|
61
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on
|
|
62
|
+
behalf of whom a Contribution has been received by Licensor and
|
|
63
|
+
subsequently incorporated within the Work.
|
|
64
|
+
|
|
65
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this
|
|
66
|
+
License, each Contributor hereby grants to You a perpetual, worldwide,
|
|
67
|
+
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
|
|
68
|
+
reproduce, prepare Derivative Works of, publicly display, publicly perform,
|
|
69
|
+
sublicense, and distribute the Work and such Derivative Works in Source or
|
|
70
|
+
Object form.
|
|
71
|
+
|
|
72
|
+
3. Grant of Patent License. Subject to the terms and conditions of this
|
|
73
|
+
License, each Contributor hereby grants to You a perpetual, worldwide,
|
|
74
|
+
non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
|
|
75
|
+
this section) patent license to make, have made, use, offer to sell, sell,
|
|
76
|
+
import, and otherwise transfer the Work, where such license applies only to
|
|
77
|
+
those patent claims licensable by such Contributor that are necessarily
|
|
78
|
+
infringed by their Contribution(s) alone or by combination of their
|
|
79
|
+
Contribution(s) with the Work to which such Contribution(s) was submitted.
|
|
80
|
+
If You institute patent litigation against any entity (including a
|
|
81
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
|
82
|
+
Contribution incorporated within the Work constitutes direct or
|
|
83
|
+
contributory patent infringement, then any patent licenses granted to You
|
|
84
|
+
under this License for that Work shall terminate as of the date such
|
|
85
|
+
litigation is filed.
|
|
86
|
+
|
|
87
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or
|
|
88
|
+
Derivative Works thereof in any medium, with or without modifications, and
|
|
89
|
+
in Source or Object form, provided that You meet the following conditions:
|
|
90
|
+
|
|
91
|
+
(a) You must give any other recipients of the Work or Derivative Works
|
|
92
|
+
a copy of this License; and
|
|
93
|
+
|
|
94
|
+
(b) You must cause any modified files to carry prominent notices
|
|
95
|
+
stating that You changed the files; and
|
|
96
|
+
|
|
97
|
+
(c) You must retain, in the Source form of any Derivative Works that
|
|
98
|
+
You distribute, all copyright, patent, trademark, and attribution notices
|
|
99
|
+
from the Source form of the Work, excluding those notices that do not
|
|
100
|
+
pertain to any part of the Derivative Works; and
|
|
101
|
+
|
|
102
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
103
|
+
distribution, then any Derivative Works that You distribute must include a
|
|
104
|
+
readable copy of the attribution notices contained within such NOTICE file,
|
|
105
|
+
excluding those notices that do not pertain to any part of the Derivative
|
|
106
|
+
Works, in at least one of the following places: within a NOTICE text file
|
|
107
|
+
distributed as part of the Derivative Works; within the Source form or
|
|
108
|
+
documentation, if provided along with the Derivative Works; or, within a
|
|
109
|
+
display generated by the Derivative Works, if and wherever such third-party
|
|
110
|
+
notices normally appear. The contents of the NOTICE file are for
|
|
111
|
+
informational purposes only and do not modify the License. You may add Your
|
|
112
|
+
own attribution notices within Derivative Works that You distribute,
|
|
113
|
+
alongside or as an addendum to the NOTICE text from the Work, provided that
|
|
114
|
+
such additional attribution notices cannot be construed as modifying the
|
|
115
|
+
License.
|
|
116
|
+
|
|
117
|
+
You may add Your own copyright statement to Your modifications and may
|
|
118
|
+
provide additional or different license terms and conditions for use,
|
|
119
|
+
reproduction, or distribution of Your modifications, or for any such
|
|
120
|
+
Derivative Works as a whole, provided Your use, reproduction, and
|
|
121
|
+
distribution of the Work otherwise complies with the conditions stated in
|
|
122
|
+
this License.
|
|
123
|
+
|
|
124
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
|
125
|
+
Contribution intentionally submitted for inclusion in the Work by You to
|
|
126
|
+
the Licensor shall be under the terms and conditions of this License,
|
|
127
|
+
without any additional terms or conditions. Notwithstanding the above,
|
|
128
|
+
nothing herein shall supersede or modify the terms of any separate license
|
|
129
|
+
agreement you may have executed with Licensor regarding such Contributions.
|
|
130
|
+
|
|
131
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
132
|
+
names, trademarks, service marks, or product names of the Licensor, except
|
|
133
|
+
as required for reasonable and customary use in describing the origin of
|
|
134
|
+
the Work and reproducing the content of the NOTICE file.
|
|
135
|
+
|
|
136
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
|
|
137
|
+
in writing, Licensor provides the Work (and each Contributor provides its
|
|
138
|
+
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
139
|
+
KIND, either express or implied, including, without limitation, any
|
|
140
|
+
warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
|
|
141
|
+
FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
|
|
142
|
+
determining the appropriateness of using or redistributing the Work and
|
|
143
|
+
assume any risks associated with Your exercise of permissions under this
|
|
144
|
+
License.
|
|
145
|
+
|
|
146
|
+
8. Limitation of Liability. In no event and under no legal theory, whether
|
|
147
|
+
in tort (including negligence), contract, or otherwise, unless required by
|
|
148
|
+
applicable law (such as deliberate and grossly negligent acts) or agreed to
|
|
149
|
+
in writing, shall any Contributor be liable to You for damages, including
|
|
150
|
+
any direct, indirect, special, incidental, or consequential damages of any
|
|
151
|
+
character arising as a result of this License or out of the use or
|
|
152
|
+
inability to use the Work (including but not limited to damages for loss of
|
|
153
|
+
goodwill, work stoppage, computer failure or malfunction, or any and all
|
|
154
|
+
other commercial damages or losses), even if such Contributor has been
|
|
155
|
+
advised of the possibility of such damages.
|
|
156
|
+
|
|
157
|
+
9. Accepting Warranty or Additional Liability. While redistributing the
|
|
158
|
+
Work or Derivative Works thereof, You may choose to offer, and charge a fee
|
|
159
|
+
for, acceptance of support, warranty, indemnity, or other liability
|
|
160
|
+
obligations and/or rights consistent with this License. However, in
|
|
161
|
+
accepting such obligations, You may act only on Your own behalf and on Your
|
|
162
|
+
sole responsibility, not on behalf of any other Contributor, and only if
|
|
163
|
+
You agree to indemnify, defend, and hold each Contributor harmless for any
|
|
164
|
+
liability incurred by, or claims asserted against, such Contributor by
|
|
165
|
+
reason of your accepting any such warranty or additional liability.
|
|
166
|
+
|
|
167
|
+
END OF TERMS AND CONDITIONS
|
|
168
|
+
|
|
169
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
170
|
+
|
|
171
|
+
To apply the Apache License to your work, attach the following boilerplate
|
|
172
|
+
notice, with the fields enclosed by brackets "[]" replaced with your own
|
|
173
|
+
identifying information. (Don't include the brackets!) The text should be
|
|
174
|
+
enclosed in the appropriate comment syntax for the file format. We also
|
|
175
|
+
recommend that a file or class name and description of purpose be included
|
|
176
|
+
on the same "printed page" as the copyright notice for easier
|
|
177
|
+
identification within third-party archives.
|
|
178
|
+
|
|
179
|
+
Copyright [yyyy] [name of copyright owner]
|
|
180
|
+
|
|
181
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
182
|
+
you may not use this file except in compliance with the License.
|
|
183
|
+
You may obtain a copy of the License at
|
|
184
|
+
|
|
185
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
186
|
+
|
|
187
|
+
Unless required by applicable law or agreed to in writing, software
|
|
188
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
189
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
190
|
+
See the License for the specific language governing permissions and
|
|
191
|
+
limitations under the License.
|
package/dist/index.cjs
CHANGED
|
@@ -438,7 +438,8 @@ function createSecuritySignalProcessor(options = {}) {
|
|
|
438
438
|
const traceId = readTraceId(span);
|
|
439
439
|
if (!traceId) return;
|
|
440
440
|
const nowMs = now();
|
|
441
|
-
|
|
441
|
+
const cutoff = nowMs - actionChainWindowMs;
|
|
442
|
+
pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);
|
|
442
443
|
if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
|
|
443
444
|
const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
|
|
444
445
|
untrustedByTrace.set(traceId, {
|
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<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -437,7 +437,8 @@ function createSecuritySignalProcessor(options = {}) {
|
|
|
437
437
|
const traceId = readTraceId(span);
|
|
438
438
|
if (!traceId) return;
|
|
439
439
|
const nowMs = now();
|
|
440
|
-
|
|
440
|
+
const cutoff = nowMs - actionChainWindowMs;
|
|
441
|
+
pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);
|
|
441
442
|
if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
|
|
442
443
|
const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
|
|
443
444
|
untrustedByTrace.set(traceId, {
|
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<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-audit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Audit-focused helpers for Autotel (force-keep + structured audit instrumentation)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"tail-sampling"
|
|
26
26
|
],
|
|
27
27
|
"author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
|
|
28
|
-
"license": "
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"autotel": "4.2.
|
|
30
|
+
"autotel": "4.2.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^
|
|
34
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
35
|
-
"@typescript-eslint/parser": "^8.
|
|
33
|
+
"@types/node": "^26.1.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.63.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.3",
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"typescript-eslint": "^8.
|
|
41
|
-
"vitest": "^4.1.
|
|
40
|
+
"typescript-eslint": "^8.63.0",
|
|
41
|
+
"vitest": "^4.1.10"
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|