autotel-audit 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +107 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +69 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/context.ts +0 -145
- package/src/index.test.ts +0 -183
- package/src/index.ts +0 -153
- package/src/lazy-counter.ts +0 -24
- package/src/security-heartbeat.test.ts +0 -65
- package/src/security-heartbeat.ts +0 -63
- package/src/security-signals.test.ts +0 -490
- package/src/security-signals.ts +0 -472
- package/src/security.test.ts +0 -342
- package/src/security.ts +0 -334
package/src/security-signals.ts
DELETED
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AUTOTEL_SAMPLING_TAIL_EVALUATED,
|
|
3
|
-
AUTOTEL_SAMPLING_TAIL_KEEP,
|
|
4
|
-
} from 'autotel';
|
|
5
|
-
import {
|
|
6
|
-
HTTP_STATUS_ATTRIBUTES,
|
|
7
|
-
SECURITY_ATTR,
|
|
8
|
-
SECURITY_DENIED_STATUSES,
|
|
9
|
-
SECURITY_METRICS,
|
|
10
|
-
} from 'autotel/security-schema';
|
|
11
|
-
import { lazyCounter } from './lazy-counter';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Zero-code security signal derivation from spans you already have.
|
|
15
|
-
*
|
|
16
|
-
* `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:
|
|
17
|
-
*
|
|
18
|
-
* - flags suspicious request paths (traversal, `.env`/`.git` probes,
|
|
19
|
-
* SQLi/XSS probes) at span start, marking them `security.suspicious_request`
|
|
20
|
-
* and force-keeping them through tail sampling
|
|
21
|
-
* - counts denied responses (401/403/429 by default) into the
|
|
22
|
-
* `autotel.security.http.denied` metric
|
|
23
|
-
* - detects auth-failure bursts per client (sliding window) and surfaces
|
|
24
|
-
* them via the `autotel.security.anomaly` metric and an `onSignal` callback
|
|
25
|
-
*
|
|
26
|
-
* ```typescript
|
|
27
|
-
* init({
|
|
28
|
-
* service: 'api',
|
|
29
|
-
* spanProcessors: [createSecuritySignalProcessor()],
|
|
30
|
-
* });
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* Detection rules, alert thresholds, and dashboards belong in your
|
|
34
|
-
* observability backend — this processor's job is to make the signals
|
|
35
|
-
* exist, survive sampling, and stay queryable under a stable schema.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
// Structural subset of @opentelemetry/sdk-trace-base types — kept local so
|
|
39
|
-
// autotel-audit adds no new dependencies. Objects returned here satisfy the
|
|
40
|
-
// real SpanProcessor interface structurally (must mirror @opentelemetry/api's
|
|
41
|
-
// AttributeValue, including nullable array entries).
|
|
42
|
-
type AttributeValue =
|
|
43
|
-
| string
|
|
44
|
-
| number
|
|
45
|
-
| boolean
|
|
46
|
-
| Array<null | undefined | string>
|
|
47
|
-
| Array<null | undefined | number>
|
|
48
|
-
| Array<null | undefined | boolean>;
|
|
49
|
-
|
|
50
|
-
interface MutableSpanLike {
|
|
51
|
-
attributes: Record<string, AttributeValue | undefined>;
|
|
52
|
-
setAttribute(key: string, value: AttributeValue): unknown;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ReadableSpanLike {
|
|
56
|
-
attributes: Record<string, AttributeValue | undefined>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface SecuritySignalProcessor {
|
|
60
|
-
onStart(span: MutableSpanLike, parentContext?: unknown): void;
|
|
61
|
-
onEnd(span: ReadableSpanLike): void;
|
|
62
|
-
shutdown(): Promise<void>;
|
|
63
|
-
forceFlush(): Promise<void>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface SuspiciousRequestSignal {
|
|
67
|
-
signal: 'suspicious_request';
|
|
68
|
-
/** Which pattern matched, e.g. `path_traversal`. */
|
|
69
|
-
pattern: string;
|
|
70
|
-
/** The matched request path/URL (as found on the span). */
|
|
71
|
-
target: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface AuthFailureBurstSignal {
|
|
75
|
-
signal: 'auth_failure_burst';
|
|
76
|
-
/** Value of the configured key attribute (e.g. client address). */
|
|
77
|
-
key: string;
|
|
78
|
-
/** Denied responses observed inside the window. */
|
|
79
|
-
count: number;
|
|
80
|
-
windowMs: number;
|
|
81
|
-
status: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface LlmExcessiveTokensSignal {
|
|
85
|
-
signal: 'llm_excessive_tokens';
|
|
86
|
-
/** Total tokens consumed by the single LLM call. */
|
|
87
|
-
tokens: number;
|
|
88
|
-
maxTokens: number;
|
|
89
|
-
model?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface LlmTokenBudgetSignal {
|
|
93
|
-
signal: 'llm_token_budget_exceeded';
|
|
94
|
-
/** Value of the configured key attribute (e.g. end-user id). */
|
|
95
|
-
key: string;
|
|
96
|
-
/** Tokens consumed inside the window. */
|
|
97
|
-
tokens: number;
|
|
98
|
-
budget: number;
|
|
99
|
-
windowMs: number;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type SecuritySignal =
|
|
103
|
-
| SuspiciousRequestSignal
|
|
104
|
-
| AuthFailureBurstSignal
|
|
105
|
-
| LlmExcessiveTokensSignal
|
|
106
|
-
| LlmTokenBudgetSignal;
|
|
107
|
-
|
|
108
|
-
export interface BurstOptions {
|
|
109
|
-
/** HTTP statuses counted toward a burst. Default `[401, 403]`. */
|
|
110
|
-
statuses?: number[];
|
|
111
|
-
/** Denied responses within the window that trigger a signal. Default 10. */
|
|
112
|
-
threshold?: number;
|
|
113
|
-
/** Sliding window size in milliseconds. Default 60_000. */
|
|
114
|
-
windowMs?: number;
|
|
115
|
-
/**
|
|
116
|
-
* Span attribute identifying the client. Default `client.address`
|
|
117
|
-
* (falls back to `http.client_ip`).
|
|
118
|
-
*/
|
|
119
|
-
keyAttribute?: string;
|
|
120
|
-
/** Max distinct clients tracked (oldest evicted). Default 10_000. */
|
|
121
|
-
maxKeys?: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface LlmSignalOptions {
|
|
125
|
-
/**
|
|
126
|
-
* Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).
|
|
127
|
-
* Default 100_000. Pass `false` to disable the per-call check.
|
|
128
|
-
*/
|
|
129
|
-
maxTokensPerCall?: number | false;
|
|
130
|
-
/**
|
|
131
|
-
* Sliding-window token budget per key — catches slow-drip abuse that
|
|
132
|
-
* stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).
|
|
133
|
-
* Off unless configured.
|
|
134
|
-
*/
|
|
135
|
-
tokenBudget?: {
|
|
136
|
-
budget: number;
|
|
137
|
-
/** Window size in milliseconds. Default 300_000 (5 min). */
|
|
138
|
-
windowMs?: number;
|
|
139
|
-
/**
|
|
140
|
-
* Span attribute identifying the consumer. Default `enduser.id`
|
|
141
|
-
* (falls back to `client.address`).
|
|
142
|
-
*/
|
|
143
|
-
keyAttribute?: string;
|
|
144
|
-
/** Max distinct keys tracked (oldest evicted). Default 10_000. */
|
|
145
|
-
maxKeys?: number;
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export interface SecuritySignalProcessorOptions {
|
|
150
|
-
/** Flag suspicious request paths on span start. Default true. */
|
|
151
|
-
detectSuspiciousRequests?: boolean;
|
|
152
|
-
/** Additional name → pattern pairs checked against the request target. */
|
|
153
|
-
extraPatterns?: Record<string, RegExp>;
|
|
154
|
-
/** Force-keep flagged spans through tail sampling. Default true. */
|
|
155
|
-
forceKeepSuspicious?: boolean;
|
|
156
|
-
/** HTTP statuses counted as denied. Default `[401, 403, 429]`. */
|
|
157
|
-
deniedStatuses?: number[];
|
|
158
|
-
/** Burst detection over denied responses. Pass `false` to disable. */
|
|
159
|
-
burst?: BurstOptions | false;
|
|
160
|
-
/**
|
|
161
|
-
* LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).
|
|
162
|
-
* Enabled with the per-call ceiling by default; pass `false` to disable.
|
|
163
|
-
*/
|
|
164
|
-
llm?: LlmSignalOptions | false;
|
|
165
|
-
/** Emit `autotel.security.*` metrics. Default true. */
|
|
166
|
-
metrics?: boolean;
|
|
167
|
-
/** Called whenever a signal fires. Keep it fast and non-throwing. */
|
|
168
|
-
onSignal?: (signal: SecuritySignal) => void;
|
|
169
|
-
/** Clock override for tests. */
|
|
170
|
-
now?: () => number;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Conservative request-target patterns. Tuned for scanner/probe traffic —
|
|
175
|
-
* high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.
|
|
176
|
-
*/
|
|
177
|
-
export const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {
|
|
178
|
-
path_traversal: /(\.\.[/\\]|%2e%2e(%2f|%5c|\/)|\.\.%2f|%252e%252e)/i,
|
|
179
|
-
sensitive_file_probe:
|
|
180
|
-
/(\/\.env\b|\/\.git\b|\/etc\/passwd|\/wp-admin\b|\/\.aws\b|\/id_rsa\b)/i,
|
|
181
|
-
sqli_probe:
|
|
182
|
-
/(\bunion\b[\s+%20]+(all[\s+%20]+)?select\b|'[\s+%20]*or[\s+%20]*'?1'?[\s+%20]*=[\s+%20]*'?1)/i,
|
|
183
|
-
xss_probe: /(<script\b|%3cscript)/i,
|
|
184
|
-
null_byte: /%00/,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const TARGET_ATTRIBUTES = [
|
|
188
|
-
'url.path',
|
|
189
|
-
'url.full',
|
|
190
|
-
'http.target',
|
|
191
|
-
'http.url',
|
|
192
|
-
] as const;
|
|
193
|
-
|
|
194
|
-
function readAttribute(
|
|
195
|
-
attributes: Record<string, AttributeValue | undefined>,
|
|
196
|
-
keys: readonly string[],
|
|
197
|
-
): AttributeValue | undefined {
|
|
198
|
-
for (const key of keys) {
|
|
199
|
-
const value = attributes[key];
|
|
200
|
-
if (value !== undefined) return value;
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Weighted sliding-window counter with bounded key cardinality.
|
|
207
|
-
* Weight 1 per hit counts occurrences; token counts as weights sum usage.
|
|
208
|
-
*/
|
|
209
|
-
class SlidingWindow {
|
|
210
|
-
private readonly hits = new Map<string, Array<[number, number]>>();
|
|
211
|
-
|
|
212
|
-
constructor(
|
|
213
|
-
private readonly windowMs: number,
|
|
214
|
-
private readonly maxKeys: number,
|
|
215
|
-
) {}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Record a hit; returns the totals inside the window before and after it,
|
|
219
|
-
* so callers can signal exactly once on a threshold crossing.
|
|
220
|
-
*/
|
|
221
|
-
record(key: string, now: number, weight = 1): { before: number; after: number } {
|
|
222
|
-
let entries = this.hits.get(key);
|
|
223
|
-
if (!entries) {
|
|
224
|
-
// Bound memory: random client addresses must not grow the map forever.
|
|
225
|
-
if (this.hits.size >= this.maxKeys) {
|
|
226
|
-
const oldest = this.hits.keys().next().value;
|
|
227
|
-
if (oldest !== undefined) this.hits.delete(oldest);
|
|
228
|
-
}
|
|
229
|
-
entries = [];
|
|
230
|
-
this.hits.set(key, entries);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const cutoff = now - this.windowMs;
|
|
234
|
-
while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {
|
|
235
|
-
entries.shift();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let before = 0;
|
|
239
|
-
for (const [, w] of entries) before += w;
|
|
240
|
-
entries.push([now, weight]);
|
|
241
|
-
return { before, after: before + weight };
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Burst detection options with defaults applied and the window attached. */
|
|
246
|
-
interface BurstConfig {
|
|
247
|
-
statuses: Set<number>;
|
|
248
|
-
threshold: number;
|
|
249
|
-
windowMs: number;
|
|
250
|
-
keyAttribute: string;
|
|
251
|
-
window: SlidingWindow;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function resolveBurstConfig(
|
|
255
|
-
option: BurstOptions | false | undefined,
|
|
256
|
-
): BurstConfig | undefined {
|
|
257
|
-
if (option === false) return undefined;
|
|
258
|
-
const opts = option ?? {};
|
|
259
|
-
const windowMs = opts.windowMs ?? 60_000;
|
|
260
|
-
return {
|
|
261
|
-
statuses: new Set(opts.statuses ?? [401, 403]),
|
|
262
|
-
threshold: opts.threshold ?? 10,
|
|
263
|
-
windowMs,
|
|
264
|
-
keyAttribute: opts.keyAttribute ?? 'client.address',
|
|
265
|
-
window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** LLM consumption options with defaults applied and windows attached. */
|
|
270
|
-
interface LlmConfig {
|
|
271
|
-
maxTokensPerCall?: number;
|
|
272
|
-
budget?: {
|
|
273
|
-
budget: number;
|
|
274
|
-
windowMs: number;
|
|
275
|
-
keyAttribute: string;
|
|
276
|
-
window: SlidingWindow;
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function resolveLlmConfig(
|
|
281
|
-
option: LlmSignalOptions | false | undefined,
|
|
282
|
-
): LlmConfig | undefined {
|
|
283
|
-
if (option === false) return undefined;
|
|
284
|
-
const opts = option ?? {};
|
|
285
|
-
const tokenBudget = opts.tokenBudget;
|
|
286
|
-
const windowMs = tokenBudget?.windowMs ?? 300_000;
|
|
287
|
-
return {
|
|
288
|
-
maxTokensPerCall:
|
|
289
|
-
opts.maxTokensPerCall === false
|
|
290
|
-
? undefined
|
|
291
|
-
: (opts.maxTokensPerCall ?? 100_000),
|
|
292
|
-
budget: tokenBudget && {
|
|
293
|
-
budget: tokenBudget.budget,
|
|
294
|
-
windowMs,
|
|
295
|
-
keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',
|
|
296
|
-
window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export function createSecuritySignalProcessor(
|
|
302
|
-
options: SecuritySignalProcessorOptions = {},
|
|
303
|
-
): SecuritySignalProcessor {
|
|
304
|
-
const detect = options.detectSuspiciousRequests !== false;
|
|
305
|
-
const forceKeep = options.forceKeepSuspicious !== false;
|
|
306
|
-
const metricsEnabled = options.metrics !== false;
|
|
307
|
-
const deniedStatuses = new Set(
|
|
308
|
-
options.deniedStatuses ?? SECURITY_DENIED_STATUSES,
|
|
309
|
-
);
|
|
310
|
-
const now = options.now ?? Date.now;
|
|
311
|
-
|
|
312
|
-
const patterns: Record<string, RegExp> = {
|
|
313
|
-
...SUSPICIOUS_REQUEST_PATTERNS,
|
|
314
|
-
...options.extraPatterns,
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const burst = resolveBurstConfig(options.burst);
|
|
318
|
-
const llm = resolveLlmConfig(options.llm);
|
|
319
|
-
|
|
320
|
-
const counters = {
|
|
321
|
-
suspicious: lazyCounter(
|
|
322
|
-
SECURITY_METRICS.httpSuspicious,
|
|
323
|
-
'Requests matching suspicious-path patterns',
|
|
324
|
-
),
|
|
325
|
-
denied: lazyCounter(
|
|
326
|
-
SECURITY_METRICS.httpDenied,
|
|
327
|
-
'HTTP responses with denied status codes (401/403/429)',
|
|
328
|
-
),
|
|
329
|
-
anomaly: lazyCounter(
|
|
330
|
-
SECURITY_METRICS.anomaly,
|
|
331
|
-
'Security anomaly signals (e.g. auth-failure bursts)',
|
|
332
|
-
),
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
function count(
|
|
336
|
-
which: keyof typeof counters,
|
|
337
|
-
attributes: Record<string, string | number>,
|
|
338
|
-
): void {
|
|
339
|
-
if (!metricsEnabled) return;
|
|
340
|
-
counters[which].add(1, attributes);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function emit(signal: SecuritySignal): void {
|
|
344
|
-
try {
|
|
345
|
-
options.onSignal?.(signal);
|
|
346
|
-
} catch {
|
|
347
|
-
// Callbacks must never break the span pipeline.
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function checkDeniedResponse(span: ReadableSpanLike): void {
|
|
352
|
-
const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);
|
|
353
|
-
if (typeof status !== 'number' || !deniedStatuses.has(status)) return;
|
|
354
|
-
|
|
355
|
-
count('denied', { status });
|
|
356
|
-
|
|
357
|
-
if (!burst || !burst.statuses.has(status)) return;
|
|
358
|
-
|
|
359
|
-
const key = readAttribute(span.attributes, [
|
|
360
|
-
burst.keyAttribute,
|
|
361
|
-
'http.client_ip',
|
|
362
|
-
]);
|
|
363
|
-
if (typeof key !== 'string' || key.length === 0) return;
|
|
364
|
-
|
|
365
|
-
const { before, after } = burst.window.record(key, now());
|
|
366
|
-
// Signal once per window on the exact crossing, not on every
|
|
367
|
-
// subsequent hit — keeps anomaly volume bounded under attack.
|
|
368
|
-
if (before < burst.threshold && after >= burst.threshold) {
|
|
369
|
-
count('anomaly', { signal: 'auth_failure_burst', status });
|
|
370
|
-
emit({
|
|
371
|
-
signal: 'auth_failure_burst',
|
|
372
|
-
key,
|
|
373
|
-
count: after,
|
|
374
|
-
windowMs: burst.windowMs,
|
|
375
|
-
status,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function checkLlmConsumption(span: ReadableSpanLike): void {
|
|
381
|
-
if (!llm) return;
|
|
382
|
-
|
|
383
|
-
const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);
|
|
384
|
-
let tokens: number | undefined;
|
|
385
|
-
if (typeof total === 'number') {
|
|
386
|
-
tokens = total;
|
|
387
|
-
} else {
|
|
388
|
-
const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);
|
|
389
|
-
const output = readAttribute(span.attributes, [
|
|
390
|
-
'gen_ai.usage.output_tokens',
|
|
391
|
-
]);
|
|
392
|
-
if (typeof input === 'number' || typeof output === 'number') {
|
|
393
|
-
tokens =
|
|
394
|
-
(typeof input === 'number' ? input : 0) +
|
|
395
|
-
(typeof output === 'number' ? output : 0);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (tokens === undefined || tokens <= 0) return;
|
|
399
|
-
|
|
400
|
-
if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {
|
|
401
|
-
const model = readAttribute(span.attributes, [
|
|
402
|
-
'gen_ai.response.model',
|
|
403
|
-
'gen_ai.request.model',
|
|
404
|
-
]);
|
|
405
|
-
count('anomaly', { signal: 'llm_excessive_tokens' });
|
|
406
|
-
emit({
|
|
407
|
-
signal: 'llm_excessive_tokens',
|
|
408
|
-
tokens,
|
|
409
|
-
maxTokens: llm.maxTokensPerCall,
|
|
410
|
-
...(typeof model === 'string' && { model }),
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const budget = llm.budget;
|
|
415
|
-
if (!budget) return;
|
|
416
|
-
|
|
417
|
-
const key = readAttribute(span.attributes, [
|
|
418
|
-
budget.keyAttribute,
|
|
419
|
-
'client.address',
|
|
420
|
-
]);
|
|
421
|
-
if (typeof key !== 'string' || key.length === 0) return;
|
|
422
|
-
|
|
423
|
-
const { before, after } = budget.window.record(key, now(), tokens);
|
|
424
|
-
if (before < budget.budget && after >= budget.budget) {
|
|
425
|
-
count('anomaly', { signal: 'llm_token_budget_exceeded' });
|
|
426
|
-
emit({
|
|
427
|
-
signal: 'llm_token_budget_exceeded',
|
|
428
|
-
key,
|
|
429
|
-
tokens: after,
|
|
430
|
-
budget: budget.budget,
|
|
431
|
-
windowMs: budget.windowMs,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
onStart(span) {
|
|
438
|
-
if (!detect) return;
|
|
439
|
-
|
|
440
|
-
const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);
|
|
441
|
-
if (typeof target !== 'string' || target.length === 0) return;
|
|
442
|
-
|
|
443
|
-
for (const [name, pattern] of Object.entries(patterns)) {
|
|
444
|
-
if (!pattern.test(target)) continue;
|
|
445
|
-
|
|
446
|
-
span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);
|
|
447
|
-
span.setAttribute(SECURITY_ATTR.signal, name);
|
|
448
|
-
if (forceKeep) {
|
|
449
|
-
span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
450
|
-
span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
count('suspicious', { pattern: name });
|
|
454
|
-
emit({ signal: 'suspicious_request', pattern: name, target });
|
|
455
|
-
return; // first match wins — one signal per span
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
onEnd(span) {
|
|
460
|
-
checkDeniedResponse(span);
|
|
461
|
-
checkLlmConsumption(span);
|
|
462
|
-
},
|
|
463
|
-
|
|
464
|
-
shutdown() {
|
|
465
|
-
return Promise.resolve();
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
forceFlush() {
|
|
469
|
-
return Promise.resolve();
|
|
470
|
-
},
|
|
471
|
-
};
|
|
472
|
-
}
|