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
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
createSecuritySignalProcessor,
|
|
4
|
-
SUSPICIOUS_REQUEST_PATTERNS,
|
|
5
|
-
type SecuritySignal,
|
|
6
|
-
} from './security-signals';
|
|
7
|
-
|
|
8
|
-
const counterAdd = vi.fn();
|
|
9
|
-
|
|
10
|
-
vi.mock('autotel', () => ({
|
|
11
|
-
createCounter: vi.fn(() => ({ add: counterAdd })),
|
|
12
|
-
AUTOTEL_SAMPLING_TAIL_EVALUATED: 'autotel.sampling.tail.evaluated',
|
|
13
|
-
AUTOTEL_SAMPLING_TAIL_KEEP: 'autotel.sampling.tail.keep',
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
function makeSpan(attributes: Record<string, unknown>) {
|
|
17
|
-
const span = {
|
|
18
|
-
attributes: attributes as never,
|
|
19
|
-
setAttribute: vi.fn((key: string, value: unknown) => {
|
|
20
|
-
(span.attributes as Record<string, unknown>)[key] = value;
|
|
21
|
-
}),
|
|
22
|
-
};
|
|
23
|
-
return span;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('createSecuritySignalProcessor — suspicious requests', () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it.each([
|
|
32
|
-
['path traversal', '/files/../../etc/passwd', 'path_traversal'],
|
|
33
|
-
['encoded traversal', '/files/%2e%2e%2fadmin', 'path_traversal'],
|
|
34
|
-
['.env probe', '/.env', 'sensitive_file_probe'],
|
|
35
|
-
['.git probe', '/.git/config', 'sensitive_file_probe'],
|
|
36
|
-
['sqli probe', "/search?q=' or '1'='1", 'sqli_probe'],
|
|
37
|
-
['union select', '/items?id=1 union select password', 'sqli_probe'],
|
|
38
|
-
['xss probe', '/comment?text=<script>alert(1)</script>', 'xss_probe'],
|
|
39
|
-
['null byte', '/download?file=report%00.pdf', 'null_byte'],
|
|
40
|
-
])('flags %s', (_label, target, expectedPattern) => {
|
|
41
|
-
const signals: SecuritySignal[] = [];
|
|
42
|
-
const processor = createSecuritySignalProcessor({
|
|
43
|
-
onSignal: (s) => signals.push(s),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const span = makeSpan({ 'url.path': target });
|
|
47
|
-
processor.onStart(span);
|
|
48
|
-
|
|
49
|
-
expect(span.setAttribute).toHaveBeenCalledWith(
|
|
50
|
-
'security.suspicious_request',
|
|
51
|
-
true,
|
|
52
|
-
);
|
|
53
|
-
expect(span.setAttribute).toHaveBeenCalledWith(
|
|
54
|
-
'security.signal',
|
|
55
|
-
expectedPattern,
|
|
56
|
-
);
|
|
57
|
-
expect(signals).toEqual([
|
|
58
|
-
{ signal: 'suspicious_request', pattern: expectedPattern, target },
|
|
59
|
-
]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('force-keeps flagged spans through tail sampling by default', () => {
|
|
63
|
-
const processor = createSecuritySignalProcessor();
|
|
64
|
-
const span = makeSpan({ 'url.path': '/.env' });
|
|
65
|
-
|
|
66
|
-
processor.onStart(span);
|
|
67
|
-
|
|
68
|
-
expect(span.setAttribute).toHaveBeenCalledWith(
|
|
69
|
-
'autotel.sampling.tail.evaluated',
|
|
70
|
-
true,
|
|
71
|
-
);
|
|
72
|
-
expect(span.setAttribute).toHaveBeenCalledWith(
|
|
73
|
-
'autotel.sampling.tail.keep',
|
|
74
|
-
true,
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('leaves normal requests untouched', () => {
|
|
79
|
-
const processor = createSecuritySignalProcessor();
|
|
80
|
-
const span = makeSpan({ 'url.path': '/api/users/123/orders' });
|
|
81
|
-
|
|
82
|
-
processor.onStart(span);
|
|
83
|
-
|
|
84
|
-
expect(span.setAttribute).not.toHaveBeenCalled();
|
|
85
|
-
expect(counterAdd).not.toHaveBeenCalled();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('reads legacy http.target attribute', () => {
|
|
89
|
-
const processor = createSecuritySignalProcessor();
|
|
90
|
-
const span = makeSpan({ 'http.target': '/wp-admin/setup.php' });
|
|
91
|
-
|
|
92
|
-
processor.onStart(span);
|
|
93
|
-
|
|
94
|
-
expect(span.setAttribute).toHaveBeenCalledWith(
|
|
95
|
-
'security.signal',
|
|
96
|
-
'sensitive_file_probe',
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('supports extra patterns', () => {
|
|
101
|
-
const signals: SecuritySignal[] = [];
|
|
102
|
-
const processor = createSecuritySignalProcessor({
|
|
103
|
-
extraPatterns: { graphql_introspection: /__schema/i },
|
|
104
|
-
onSignal: (s) => signals.push(s),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
processor.onStart(makeSpan({ 'url.path': '/graphql?query={__schema}' }));
|
|
108
|
-
|
|
109
|
-
expect(signals[0]).toMatchObject({ pattern: 'graphql_introspection' });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('emits the suspicious metric', () => {
|
|
113
|
-
const processor = createSecuritySignalProcessor();
|
|
114
|
-
processor.onStart(makeSpan({ 'url.path': '/.env' }));
|
|
115
|
-
|
|
116
|
-
expect(counterAdd).toHaveBeenCalledWith(1, {
|
|
117
|
-
pattern: 'sensitive_file_probe',
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('never throws when the onSignal callback throws', () => {
|
|
122
|
-
const processor = createSecuritySignalProcessor({
|
|
123
|
-
onSignal: () => {
|
|
124
|
-
throw new Error('subscriber bug');
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
expect(() =>
|
|
129
|
-
processor.onStart(makeSpan({ 'url.path': '/.env' })),
|
|
130
|
-
).not.toThrow();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('createSecuritySignalProcessor — denied responses and bursts', () => {
|
|
135
|
-
beforeEach(() => {
|
|
136
|
-
vi.clearAllMocks();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('counts denied statuses', () => {
|
|
140
|
-
const processor = createSecuritySignalProcessor();
|
|
141
|
-
|
|
142
|
-
processor.onEnd(makeSpan({ 'http.response.status_code': 401 }));
|
|
143
|
-
processor.onEnd(makeSpan({ 'http.status_code': 403 }));
|
|
144
|
-
processor.onEnd(makeSpan({ 'http.response.status_code': 200 }));
|
|
145
|
-
|
|
146
|
-
expect(counterAdd).toHaveBeenCalledWith(1, { status: 401 });
|
|
147
|
-
expect(counterAdd).toHaveBeenCalledWith(1, { status: 403 });
|
|
148
|
-
expect(counterAdd).toHaveBeenCalledTimes(2);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('signals an auth-failure burst exactly once per window crossing', () => {
|
|
152
|
-
let clock = 1_000_000;
|
|
153
|
-
const signals: SecuritySignal[] = [];
|
|
154
|
-
const processor = createSecuritySignalProcessor({
|
|
155
|
-
burst: { threshold: 3, windowMs: 60_000 },
|
|
156
|
-
onSignal: (s) => signals.push(s),
|
|
157
|
-
now: () => clock,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const failedLogin = () => {
|
|
161
|
-
clock += 1000;
|
|
162
|
-
processor.onEnd(
|
|
163
|
-
makeSpan({
|
|
164
|
-
'http.response.status_code': 401,
|
|
165
|
-
'client.address': '203.0.113.7',
|
|
166
|
-
}),
|
|
167
|
-
);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
failedLogin();
|
|
171
|
-
failedLogin();
|
|
172
|
-
expect(signals).toHaveLength(0);
|
|
173
|
-
|
|
174
|
-
failedLogin(); // 3rd in window → crossing
|
|
175
|
-
expect(signals).toEqual([
|
|
176
|
-
{
|
|
177
|
-
signal: 'auth_failure_burst',
|
|
178
|
-
key: '203.0.113.7',
|
|
179
|
-
count: 3,
|
|
180
|
-
windowMs: 60_000,
|
|
181
|
-
status: 401,
|
|
182
|
-
},
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
failedLogin(); // 4th — already past threshold, no duplicate signal
|
|
186
|
-
expect(signals).toHaveLength(1);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('expires hits outside the window', () => {
|
|
190
|
-
let clock = 1_000_000;
|
|
191
|
-
const signals: SecuritySignal[] = [];
|
|
192
|
-
const processor = createSecuritySignalProcessor({
|
|
193
|
-
burst: { threshold: 3, windowMs: 10_000 },
|
|
194
|
-
onSignal: (s) => signals.push(s),
|
|
195
|
-
now: () => clock,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const failAt = (t: number) => {
|
|
199
|
-
clock = t;
|
|
200
|
-
processor.onEnd(
|
|
201
|
-
makeSpan({
|
|
202
|
-
'http.response.status_code': 401,
|
|
203
|
-
'client.address': '203.0.113.7',
|
|
204
|
-
}),
|
|
205
|
-
);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
failAt(1_000_000);
|
|
209
|
-
failAt(1_005_000);
|
|
210
|
-
failAt(1_020_000); // first two expired — count back to 1, then 2, no signal
|
|
211
|
-
failAt(1_021_000);
|
|
212
|
-
expect(signals).toHaveLength(0);
|
|
213
|
-
|
|
214
|
-
failAt(1_022_000); // 3 inside the 10s window → signal
|
|
215
|
-
expect(signals).toHaveLength(1);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('tracks bursts per client independently', () => {
|
|
219
|
-
const signals: SecuritySignal[] = [];
|
|
220
|
-
const processor = createSecuritySignalProcessor({
|
|
221
|
-
burst: { threshold: 2, windowMs: 60_000 },
|
|
222
|
-
onSignal: (s) => signals.push(s),
|
|
223
|
-
now: () => 1_000_000,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const failFrom = (ip: string) =>
|
|
227
|
-
processor.onEnd(
|
|
228
|
-
makeSpan({
|
|
229
|
-
'http.response.status_code': 401,
|
|
230
|
-
'client.address': ip,
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
failFrom('203.0.113.1');
|
|
235
|
-
failFrom('203.0.113.2');
|
|
236
|
-
expect(signals).toHaveLength(0);
|
|
237
|
-
|
|
238
|
-
failFrom('203.0.113.1');
|
|
239
|
-
expect(signals).toEqual([
|
|
240
|
-
expect.objectContaining({ key: '203.0.113.1', count: 2 }),
|
|
241
|
-
]);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('429s count as denied but not toward auth bursts by default', () => {
|
|
245
|
-
const signals: SecuritySignal[] = [];
|
|
246
|
-
const processor = createSecuritySignalProcessor({
|
|
247
|
-
burst: { threshold: 1 },
|
|
248
|
-
onSignal: (s) => signals.push(s),
|
|
249
|
-
now: () => 1_000_000,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
processor.onEnd(
|
|
253
|
-
makeSpan({
|
|
254
|
-
'http.response.status_code': 429,
|
|
255
|
-
'client.address': '203.0.113.9',
|
|
256
|
-
}),
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
expect(counterAdd).toHaveBeenCalledWith(1, { status: 429 });
|
|
260
|
-
expect(signals).toHaveLength(0);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('can disable burst detection', () => {
|
|
264
|
-
const signals: SecuritySignal[] = [];
|
|
265
|
-
const processor = createSecuritySignalProcessor({
|
|
266
|
-
burst: false,
|
|
267
|
-
onSignal: (s) => signals.push(s),
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
for (let i = 0; i < 50; i++) {
|
|
271
|
-
processor.onEnd(
|
|
272
|
-
makeSpan({
|
|
273
|
-
'http.response.status_code': 401,
|
|
274
|
-
'client.address': '203.0.113.7',
|
|
275
|
-
}),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
expect(signals).toHaveLength(0);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('bounds tracked clients to maxKeys', () => {
|
|
283
|
-
const signals: SecuritySignal[] = [];
|
|
284
|
-
const processor = createSecuritySignalProcessor({
|
|
285
|
-
burst: { threshold: 2, maxKeys: 2 },
|
|
286
|
-
onSignal: (s) => signals.push(s),
|
|
287
|
-
now: () => 1_000_000,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const failFrom = (ip: string) =>
|
|
291
|
-
processor.onEnd(
|
|
292
|
-
makeSpan({
|
|
293
|
-
'http.response.status_code': 401,
|
|
294
|
-
'client.address': ip,
|
|
295
|
-
}),
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
failFrom('a'); // tracked: a
|
|
299
|
-
failFrom('b'); // tracked: a, b
|
|
300
|
-
failFrom('c'); // evicts a; tracked: b, c
|
|
301
|
-
failFrom('a'); // re-tracked fresh — count 1, not 2
|
|
302
|
-
expect(signals).toHaveLength(0);
|
|
303
|
-
|
|
304
|
-
failFrom('a'); // now 2 → signal
|
|
305
|
-
expect(signals).toEqual([expect.objectContaining({ key: 'a', count: 2 })]);
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe('createSecuritySignalProcessor — LLM consumption', () => {
|
|
310
|
-
beforeEach(() => {
|
|
311
|
-
vi.clearAllMocks();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('flags a single call above the token ceiling', () => {
|
|
315
|
-
const signals: SecuritySignal[] = [];
|
|
316
|
-
const processor = createSecuritySignalProcessor({
|
|
317
|
-
llm: { maxTokensPerCall: 10_000 },
|
|
318
|
-
onSignal: (s) => signals.push(s),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
processor.onEnd(
|
|
322
|
-
makeSpan({
|
|
323
|
-
'gen_ai.usage.total_tokens': 50_000,
|
|
324
|
-
'gen_ai.response.model': 'claude-opus-4-8',
|
|
325
|
-
}),
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
expect(signals).toEqual([
|
|
329
|
-
{
|
|
330
|
-
signal: 'llm_excessive_tokens',
|
|
331
|
-
tokens: 50_000,
|
|
332
|
-
maxTokens: 10_000,
|
|
333
|
-
model: 'claude-opus-4-8',
|
|
334
|
-
},
|
|
335
|
-
]);
|
|
336
|
-
expect(counterAdd).toHaveBeenCalledWith(1, {
|
|
337
|
-
signal: 'llm_excessive_tokens',
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('sums input+output when total_tokens is absent', () => {
|
|
342
|
-
const signals: SecuritySignal[] = [];
|
|
343
|
-
const processor = createSecuritySignalProcessor({
|
|
344
|
-
llm: { maxTokensPerCall: 1000 },
|
|
345
|
-
onSignal: (s) => signals.push(s),
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
processor.onEnd(
|
|
349
|
-
makeSpan({
|
|
350
|
-
'gen_ai.usage.input_tokens': 800,
|
|
351
|
-
'gen_ai.usage.output_tokens': 700,
|
|
352
|
-
}),
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
expect(signals[0]).toMatchObject({
|
|
356
|
-
signal: 'llm_excessive_tokens',
|
|
357
|
-
tokens: 1500,
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('stays quiet for calls under the ceiling', () => {
|
|
362
|
-
const signals: SecuritySignal[] = [];
|
|
363
|
-
const processor = createSecuritySignalProcessor({
|
|
364
|
-
onSignal: (s) => signals.push(s),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
processor.onEnd(makeSpan({ 'gen_ai.usage.total_tokens': 5000 }));
|
|
368
|
-
|
|
369
|
-
expect(signals).toHaveLength(0);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('signals a per-user token budget crossing exactly once', () => {
|
|
373
|
-
let clock = 1_000_000;
|
|
374
|
-
const signals: SecuritySignal[] = [];
|
|
375
|
-
const processor = createSecuritySignalProcessor({
|
|
376
|
-
llm: {
|
|
377
|
-
maxTokensPerCall: false,
|
|
378
|
-
tokenBudget: { budget: 10_000, windowMs: 60_000 },
|
|
379
|
-
},
|
|
380
|
-
onSignal: (s) => signals.push(s),
|
|
381
|
-
now: () => clock,
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const llmCall = (tokens: number) => {
|
|
385
|
-
clock += 1000;
|
|
386
|
-
processor.onEnd(
|
|
387
|
-
makeSpan({
|
|
388
|
-
'gen_ai.usage.total_tokens': tokens,
|
|
389
|
-
'enduser.id': 'user-7',
|
|
390
|
-
}),
|
|
391
|
-
);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
llmCall(4000);
|
|
395
|
-
llmCall(4000);
|
|
396
|
-
expect(signals).toHaveLength(0);
|
|
397
|
-
|
|
398
|
-
llmCall(4000); // 12k in window → crossing
|
|
399
|
-
expect(signals).toEqual([
|
|
400
|
-
{
|
|
401
|
-
signal: 'llm_token_budget_exceeded',
|
|
402
|
-
key: 'user-7',
|
|
403
|
-
tokens: 12_000,
|
|
404
|
-
budget: 10_000,
|
|
405
|
-
windowMs: 60_000,
|
|
406
|
-
},
|
|
407
|
-
]);
|
|
408
|
-
|
|
409
|
-
llmCall(4000); // still over — no duplicate signal
|
|
410
|
-
expect(signals).toHaveLength(1);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('budget tracking is per user', () => {
|
|
414
|
-
const signals: SecuritySignal[] = [];
|
|
415
|
-
const processor = createSecuritySignalProcessor({
|
|
416
|
-
llm: {
|
|
417
|
-
maxTokensPerCall: false,
|
|
418
|
-
tokenBudget: { budget: 5000 },
|
|
419
|
-
},
|
|
420
|
-
onSignal: (s) => signals.push(s),
|
|
421
|
-
now: () => 1_000_000,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const llmCallBy = (userId: string, tokens: number) =>
|
|
425
|
-
processor.onEnd(
|
|
426
|
-
makeSpan({
|
|
427
|
-
'gen_ai.usage.total_tokens': tokens,
|
|
428
|
-
'enduser.id': userId,
|
|
429
|
-
}),
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
llmCallBy('user-1', 3000);
|
|
433
|
-
llmCallBy('user-2', 3000);
|
|
434
|
-
expect(signals).toHaveLength(0);
|
|
435
|
-
|
|
436
|
-
llmCallBy('user-1', 3000);
|
|
437
|
-
expect(signals).toEqual([
|
|
438
|
-
expect.objectContaining({
|
|
439
|
-
signal: 'llm_token_budget_exceeded',
|
|
440
|
-
key: 'user-1',
|
|
441
|
-
tokens: 6000,
|
|
442
|
-
}),
|
|
443
|
-
]);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('can disable LLM signals entirely', () => {
|
|
447
|
-
const signals: SecuritySignal[] = [];
|
|
448
|
-
const processor = createSecuritySignalProcessor({
|
|
449
|
-
llm: false,
|
|
450
|
-
onSignal: (s) => signals.push(s),
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
processor.onEnd(makeSpan({ 'gen_ai.usage.total_tokens': 999_999 }));
|
|
454
|
-
|
|
455
|
-
expect(signals).toHaveLength(0);
|
|
456
|
-
expect(counterAdd).not.toHaveBeenCalled();
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('ignores non-LLM spans', () => {
|
|
460
|
-
const signals: SecuritySignal[] = [];
|
|
461
|
-
const processor = createSecuritySignalProcessor({
|
|
462
|
-
onSignal: (s) => signals.push(s),
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
processor.onEnd(makeSpan({ 'http.response.status_code': 200 }));
|
|
466
|
-
|
|
467
|
-
expect(signals).toHaveLength(0);
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe('SUSPICIOUS_REQUEST_PATTERNS', () => {
|
|
472
|
-
it('does not flag common legitimate paths', () => {
|
|
473
|
-
const legitimate = [
|
|
474
|
-
'/api/users/123',
|
|
475
|
-
'/search?q=union+station+select+board',
|
|
476
|
-
'/files/report-2026.pdf',
|
|
477
|
-
'/blog/scripting-languages-compared',
|
|
478
|
-
'/env/production/status',
|
|
479
|
-
'/git-tutorial/intro',
|
|
480
|
-
];
|
|
481
|
-
|
|
482
|
-
for (const path of legitimate) {
|
|
483
|
-
for (const [name, pattern] of Object.entries(
|
|
484
|
-
SUSPICIOUS_REQUEST_PATTERNS,
|
|
485
|
-
)) {
|
|
486
|
-
expect(pattern.test(path), `${name} flagged ${path}`).toBe(false);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
});
|