agentshield-sdk 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
package/src/otel.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — OpenTelemetry-Compatible Metrics & Tracing
|
|
5
|
+
*
|
|
6
|
+
* Emits OTel-compatible data formats without requiring the OTel SDK dependency.
|
|
7
|
+
* Features:
|
|
8
|
+
* - ShieldMetrics: counters, histograms in OTel/Prometheus format
|
|
9
|
+
* - ShieldTracer: spans and traces in OTLP-compatible JSON
|
|
10
|
+
* - MetricsDashboard: human-readable summaries and percentiles
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
// =========================================================================
|
|
16
|
+
// Shield Metrics
|
|
17
|
+
// =========================================================================
|
|
18
|
+
|
|
19
|
+
class ShieldMetrics {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} [options]
|
|
22
|
+
* @param {string} [options.serviceName='agent-shield'] - Service name for metric labels.
|
|
23
|
+
* @param {number} [options.interval=60000] - Metric flush interval in ms.
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.serviceName = options.serviceName || 'agent-shield';
|
|
27
|
+
this.interval = options.interval || 60000;
|
|
28
|
+
|
|
29
|
+
this._scans = { total: 0, blocked: 0, latencies: [] };
|
|
30
|
+
this._threats = {};
|
|
31
|
+
this._blocks = { total: 0, contexts: [] };
|
|
32
|
+
this._windows = [];
|
|
33
|
+
this._startTime = Date.now();
|
|
34
|
+
this._lastFlush = Date.now();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Record a scan event with timing information.
|
|
39
|
+
* @param {Object} result - Scan result.
|
|
40
|
+
* @param {number} [result.latencyMs] - Scan latency in milliseconds.
|
|
41
|
+
* @param {boolean} [result.blocked] - Whether the scan resulted in a block.
|
|
42
|
+
* @param {number} [result.threatCount] - Number of threats detected.
|
|
43
|
+
*/
|
|
44
|
+
recordScan(result) {
|
|
45
|
+
this._scans.total++;
|
|
46
|
+
|
|
47
|
+
if (result.latencyMs !== undefined) {
|
|
48
|
+
this._scans.latencies.push(result.latencyMs);
|
|
49
|
+
// Keep latencies bounded
|
|
50
|
+
if (this._scans.latencies.length > 10000) {
|
|
51
|
+
this._scans.latencies = this._scans.latencies.slice(-5000);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (result.blocked) {
|
|
56
|
+
this._scans.blocked++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Record window snapshot for throughput tracking
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (now - this._lastFlush >= this.interval) {
|
|
62
|
+
this._windows.push({
|
|
63
|
+
timestamp: now,
|
|
64
|
+
scans: this._scans.total,
|
|
65
|
+
blocked: this._scans.blocked
|
|
66
|
+
});
|
|
67
|
+
this._lastFlush = now;
|
|
68
|
+
// Keep max 1440 windows (~24h at 1-min intervals)
|
|
69
|
+
if (this._windows.length > 1440) {
|
|
70
|
+
this._windows = this._windows.slice(-1440);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Record a threat detection event.
|
|
77
|
+
* @param {Object} threat - Threat information.
|
|
78
|
+
* @param {string} [threat.category] - Threat category.
|
|
79
|
+
* @param {string} [threat.severity] - Threat severity level.
|
|
80
|
+
*/
|
|
81
|
+
recordThreat(threat) {
|
|
82
|
+
const category = threat.category || 'unknown';
|
|
83
|
+
const severity = threat.severity || 'medium';
|
|
84
|
+
|
|
85
|
+
if (!this._threats[category]) {
|
|
86
|
+
this._threats[category] = { count: 0, severities: {} };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._threats[category].count++;
|
|
90
|
+
|
|
91
|
+
if (!this._threats[category].severities[severity]) {
|
|
92
|
+
this._threats[category].severities[severity] = 0;
|
|
93
|
+
}
|
|
94
|
+
this._threats[category].severities[severity]++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Record a block event.
|
|
99
|
+
* @param {Object} context - Block context.
|
|
100
|
+
* @param {string} [context.reason] - Reason for blocking.
|
|
101
|
+
* @param {string} [context.category] - Threat category that triggered the block.
|
|
102
|
+
*/
|
|
103
|
+
recordBlock(context) {
|
|
104
|
+
this._blocks.total++;
|
|
105
|
+
this._blocks.contexts.push({
|
|
106
|
+
reason: context.reason || 'unknown',
|
|
107
|
+
category: context.category || 'unknown',
|
|
108
|
+
timestamp: Date.now()
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Keep bounded
|
|
112
|
+
if (this._blocks.contexts.length > 1000) {
|
|
113
|
+
this._blocks.contexts = this._blocks.contexts.slice(-500);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Return OTel-compatible metric objects.
|
|
119
|
+
* @returns {Array<Object>} Array of metric objects {name, type, value, labels, timestamp}.
|
|
120
|
+
*/
|
|
121
|
+
getMetrics() {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const metrics = [];
|
|
124
|
+
|
|
125
|
+
metrics.push({
|
|
126
|
+
name: 'agent_shield_scans_total',
|
|
127
|
+
type: 'counter',
|
|
128
|
+
value: this._scans.total,
|
|
129
|
+
labels: { service: this.serviceName },
|
|
130
|
+
timestamp: now
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
metrics.push({
|
|
134
|
+
name: 'agent_shield_blocks_total',
|
|
135
|
+
type: 'counter',
|
|
136
|
+
value: this._blocks.total,
|
|
137
|
+
labels: { service: this.serviceName },
|
|
138
|
+
timestamp: now
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
metrics.push({
|
|
142
|
+
name: 'agent_shield_scans_blocked_total',
|
|
143
|
+
type: 'counter',
|
|
144
|
+
value: this._scans.blocked,
|
|
145
|
+
labels: { service: this.serviceName },
|
|
146
|
+
timestamp: now
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Threat counters per category
|
|
150
|
+
for (const [category, data] of Object.entries(this._threats)) {
|
|
151
|
+
metrics.push({
|
|
152
|
+
name: 'agent_shield_threats_total',
|
|
153
|
+
type: 'counter',
|
|
154
|
+
value: data.count,
|
|
155
|
+
labels: { service: this.serviceName, category },
|
|
156
|
+
timestamp: now
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Latency histogram summary
|
|
161
|
+
if (this._scans.latencies.length > 0) {
|
|
162
|
+
const sorted = [...this._scans.latencies].sort((a, b) => a - b);
|
|
163
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
164
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
165
|
+
const p99 = sorted[Math.floor(sorted.length * 0.99)];
|
|
166
|
+
|
|
167
|
+
metrics.push({
|
|
168
|
+
name: 'agent_shield_scan_latency_ms',
|
|
169
|
+
type: 'histogram',
|
|
170
|
+
value: { p50, p95, p99, count: sorted.length, sum: sorted.reduce((a, b) => a + b, 0) },
|
|
171
|
+
labels: { service: this.serviceName },
|
|
172
|
+
timestamp: now
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return metrics;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Export metrics in Prometheus text exposition format.
|
|
181
|
+
* @returns {string} Prometheus-formatted metrics.
|
|
182
|
+
*/
|
|
183
|
+
toPrometheus() {
|
|
184
|
+
const lines = [];
|
|
185
|
+
const metrics = this.getMetrics();
|
|
186
|
+
|
|
187
|
+
for (const metric of metrics) {
|
|
188
|
+
const labelStr = Object.entries(metric.labels)
|
|
189
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
190
|
+
.join(',');
|
|
191
|
+
|
|
192
|
+
if (metric.type === 'histogram' && typeof metric.value === 'object') {
|
|
193
|
+
lines.push(`# HELP ${metric.name} Agent Shield scan latency histogram`);
|
|
194
|
+
lines.push(`# TYPE ${metric.name} summary`);
|
|
195
|
+
lines.push(`${metric.name}{${labelStr},quantile="0.5"} ${metric.value.p50}`);
|
|
196
|
+
lines.push(`${metric.name}{${labelStr},quantile="0.95"} ${metric.value.p95}`);
|
|
197
|
+
lines.push(`${metric.name}{${labelStr},quantile="0.99"} ${metric.value.p99}`);
|
|
198
|
+
lines.push(`${metric.name}_count{${labelStr}} ${metric.value.count}`);
|
|
199
|
+
lines.push(`${metric.name}_sum{${labelStr}} ${metric.value.sum}`);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(`# HELP ${metric.name} Agent Shield metric`);
|
|
202
|
+
lines.push(`# TYPE ${metric.name} ${metric.type}`);
|
|
203
|
+
lines.push(`${metric.name}{${labelStr}} ${metric.value}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines.join('\n') + '\n';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Export metrics as JSON (OTLP/JSON compatible).
|
|
212
|
+
* @returns {Object} JSON metrics payload.
|
|
213
|
+
*/
|
|
214
|
+
toJSON() {
|
|
215
|
+
return {
|
|
216
|
+
resourceMetrics: [{
|
|
217
|
+
resource: {
|
|
218
|
+
attributes: [
|
|
219
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } }
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
scopeMetrics: [{
|
|
223
|
+
scope: { name: 'agent-shield', version: '1.0.0' },
|
|
224
|
+
metrics: this.getMetrics().map(m => ({
|
|
225
|
+
name: m.name,
|
|
226
|
+
description: `Agent Shield: ${m.name}`,
|
|
227
|
+
unit: m.name.includes('latency') ? 'ms' : '1',
|
|
228
|
+
[m.type]: {
|
|
229
|
+
dataPoints: [{
|
|
230
|
+
asDouble: typeof m.value === 'object' ? m.value.sum : m.value,
|
|
231
|
+
timeUnixNano: String(m.timestamp * 1000000),
|
|
232
|
+
attributes: Object.entries(m.labels).map(([k, v]) => ({
|
|
233
|
+
key: k,
|
|
234
|
+
value: { stringValue: v }
|
|
235
|
+
}))
|
|
236
|
+
}]
|
|
237
|
+
}
|
|
238
|
+
}))
|
|
239
|
+
}]
|
|
240
|
+
}]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reset all metrics.
|
|
246
|
+
*/
|
|
247
|
+
reset() {
|
|
248
|
+
this._scans = { total: 0, blocked: 0, latencies: [] };
|
|
249
|
+
this._threats = {};
|
|
250
|
+
this._blocks = { total: 0, contexts: [] };
|
|
251
|
+
this._windows = [];
|
|
252
|
+
this._lastFlush = Date.now();
|
|
253
|
+
console.log('[Agent Shield] ShieldMetrics reset.');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// =========================================================================
|
|
258
|
+
// Shield Tracer
|
|
259
|
+
// =========================================================================
|
|
260
|
+
|
|
261
|
+
class ShieldTracer {
|
|
262
|
+
/**
|
|
263
|
+
* @param {Object} [options]
|
|
264
|
+
* @param {string} [options.serviceName='agent-shield'] - Service name for traces.
|
|
265
|
+
* @param {number} [options.sampleRate=1.0] - Sampling rate (0.0 to 1.0).
|
|
266
|
+
*/
|
|
267
|
+
constructor(options = {}) {
|
|
268
|
+
this.serviceName = options.serviceName || 'agent-shield';
|
|
269
|
+
this.sampleRate = options.sampleRate !== undefined ? options.sampleRate : 1.0;
|
|
270
|
+
this._traces = [];
|
|
271
|
+
this._activeSpans = new Map();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Generate a random hex ID of a given byte length.
|
|
276
|
+
* @private
|
|
277
|
+
* @param {number} bytes - Number of bytes.
|
|
278
|
+
* @returns {string} Hex string.
|
|
279
|
+
*/
|
|
280
|
+
_generateId(bytes) {
|
|
281
|
+
return crypto.randomBytes(bytes).toString('hex');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Start a new span.
|
|
286
|
+
* @param {string} name - Span name (e.g., 'shield.scan', 'shield.detect').
|
|
287
|
+
* @param {Object} [attributes={}] - Span attributes.
|
|
288
|
+
* @returns {Object|null} Span object with {traceId, spanId, name, startTime, attributes, events, end()}, or null if not sampled.
|
|
289
|
+
*/
|
|
290
|
+
startSpan(name, attributes = {}) {
|
|
291
|
+
if (Math.random() > this.sampleRate) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const span = {
|
|
296
|
+
traceId: this._generateId(16),
|
|
297
|
+
spanId: this._generateId(8),
|
|
298
|
+
name,
|
|
299
|
+
startTime: Date.now(),
|
|
300
|
+
endTime: null,
|
|
301
|
+
attributes: { ...attributes, 'service.name': this.serviceName },
|
|
302
|
+
events: [],
|
|
303
|
+
status: 'OK',
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Add an event to this span.
|
|
307
|
+
* @param {string} eventName - Event name.
|
|
308
|
+
* @param {Object} [eventAttributes={}] - Event attributes.
|
|
309
|
+
*/
|
|
310
|
+
addEvent: (eventName, eventAttributes = {}) => {
|
|
311
|
+
span.events.push({
|
|
312
|
+
name: eventName,
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
attributes: eventAttributes
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* End this span.
|
|
320
|
+
* @param {string} [status='OK'] - Final status ('OK', 'ERROR').
|
|
321
|
+
*/
|
|
322
|
+
end: (status) => {
|
|
323
|
+
span.endTime = Date.now();
|
|
324
|
+
span.status = status || 'OK';
|
|
325
|
+
span.durationMs = span.endTime - span.startTime;
|
|
326
|
+
this._activeSpans.delete(span.spanId);
|
|
327
|
+
this._traces.push(span);
|
|
328
|
+
|
|
329
|
+
// Keep traces bounded
|
|
330
|
+
if (this._traces.length > 10000) {
|
|
331
|
+
this._traces = this._traces.slice(-5000);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
this._activeSpans.set(span.spanId, span);
|
|
337
|
+
return span;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Wrap a function call in a span, auto-ending on completion.
|
|
342
|
+
* @param {string} name - Span name.
|
|
343
|
+
* @param {Function} fn - Function to execute.
|
|
344
|
+
* @returns {*} The return value of fn.
|
|
345
|
+
*/
|
|
346
|
+
withSpan(name, fn) {
|
|
347
|
+
const span = this.startSpan(name);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const result = fn(span);
|
|
351
|
+
|
|
352
|
+
// Handle async functions
|
|
353
|
+
if (result && typeof result.then === 'function') {
|
|
354
|
+
return result
|
|
355
|
+
.then(val => {
|
|
356
|
+
if (span) span.end('OK');
|
|
357
|
+
return val;
|
|
358
|
+
})
|
|
359
|
+
.catch(err => {
|
|
360
|
+
if (span) {
|
|
361
|
+
span.addEvent('exception', { message: err.message });
|
|
362
|
+
span.end('ERROR');
|
|
363
|
+
}
|
|
364
|
+
throw err;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (span) span.end('OK');
|
|
369
|
+
return result;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (span) {
|
|
372
|
+
span.addEvent('exception', { message: err.message });
|
|
373
|
+
span.end('ERROR');
|
|
374
|
+
}
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get all completed traces.
|
|
381
|
+
* @returns {Array<Object>} Array of completed span objects.
|
|
382
|
+
*/
|
|
383
|
+
getTraces() {
|
|
384
|
+
return [...this._traces];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Export traces in OTLP-compatible JSON format.
|
|
389
|
+
* @returns {Object} OTLP trace payload.
|
|
390
|
+
*/
|
|
391
|
+
toOTLP() {
|
|
392
|
+
return {
|
|
393
|
+
resourceSpans: [{
|
|
394
|
+
resource: {
|
|
395
|
+
attributes: [
|
|
396
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } }
|
|
397
|
+
]
|
|
398
|
+
},
|
|
399
|
+
scopeSpans: [{
|
|
400
|
+
scope: { name: 'agent-shield', version: '1.0.0' },
|
|
401
|
+
spans: this._traces.map(span => ({
|
|
402
|
+
traceId: span.traceId,
|
|
403
|
+
spanId: span.spanId,
|
|
404
|
+
name: span.name,
|
|
405
|
+
kind: 1, // INTERNAL
|
|
406
|
+
startTimeUnixNano: String(span.startTime * 1000000),
|
|
407
|
+
endTimeUnixNano: span.endTime ? String(span.endTime * 1000000) : undefined,
|
|
408
|
+
attributes: Object.entries(span.attributes).map(([k, v]) => ({
|
|
409
|
+
key: k,
|
|
410
|
+
value: { stringValue: String(v) }
|
|
411
|
+
})),
|
|
412
|
+
events: span.events.map(e => ({
|
|
413
|
+
name: e.name,
|
|
414
|
+
timeUnixNano: String(e.timestamp * 1000000),
|
|
415
|
+
attributes: Object.entries(e.attributes).map(([k, v]) => ({
|
|
416
|
+
key: k,
|
|
417
|
+
value: { stringValue: String(v) }
|
|
418
|
+
}))
|
|
419
|
+
})),
|
|
420
|
+
status: { code: span.status === 'OK' ? 1 : 2 }
|
|
421
|
+
}))
|
|
422
|
+
}]
|
|
423
|
+
}]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// =========================================================================
|
|
429
|
+
// Metrics Dashboard
|
|
430
|
+
// =========================================================================
|
|
431
|
+
|
|
432
|
+
class MetricsDashboard {
|
|
433
|
+
/**
|
|
434
|
+
* @param {ShieldMetrics} metrics - ShieldMetrics instance to read from.
|
|
435
|
+
*/
|
|
436
|
+
constructor(metrics) {
|
|
437
|
+
this.metrics = metrics;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Return a formatted text summary of current metrics.
|
|
442
|
+
* @returns {string} Human-readable metrics summary.
|
|
443
|
+
*/
|
|
444
|
+
summary() {
|
|
445
|
+
const m = this.metrics;
|
|
446
|
+
const uptime = ((Date.now() - m._startTime) / 1000).toFixed(0);
|
|
447
|
+
const blockRate = m._scans.total > 0
|
|
448
|
+
? ((m._scans.blocked / m._scans.total) * 100).toFixed(1)
|
|
449
|
+
: '0.0';
|
|
450
|
+
|
|
451
|
+
const threatCount = Object.values(m._threats).reduce((sum, t) => sum + t.count, 0);
|
|
452
|
+
const percentiles = this.latencyPercentiles();
|
|
453
|
+
|
|
454
|
+
const lines = [
|
|
455
|
+
'=== Agent Shield Metrics Summary ===',
|
|
456
|
+
`Uptime: ${uptime}s`,
|
|
457
|
+
`Total Scans: ${m._scans.total}`,
|
|
458
|
+
`Blocked: ${m._scans.blocked} (${blockRate}%)`,
|
|
459
|
+
`Threats Detected: ${threatCount}`,
|
|
460
|
+
`Block Events: ${m._blocks.total}`,
|
|
461
|
+
''
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
if (percentiles) {
|
|
465
|
+
lines.push(`Latency p50: ${percentiles.p50.toFixed(2)}ms`);
|
|
466
|
+
lines.push(`Latency p95: ${percentiles.p95.toFixed(2)}ms`);
|
|
467
|
+
lines.push(`Latency p99: ${percentiles.p99.toFixed(2)}ms`);
|
|
468
|
+
lines.push('');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const top = this.topThreats(5);
|
|
472
|
+
if (top.length > 0) {
|
|
473
|
+
lines.push('Top Threats:');
|
|
474
|
+
for (const t of top) {
|
|
475
|
+
lines.push(` ${t.category}: ${t.count}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return lines.join('\n');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Return the top N threat categories by count.
|
|
484
|
+
* @param {number} [n=10] - Number of top threats to return.
|
|
485
|
+
* @returns {Array<Object>} Sorted array [{category, count, severities}].
|
|
486
|
+
*/
|
|
487
|
+
topThreats(n = 10) {
|
|
488
|
+
const threats = Object.entries(this.metrics._threats).map(([category, data]) => ({
|
|
489
|
+
category,
|
|
490
|
+
count: data.count,
|
|
491
|
+
severities: { ...data.severities }
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
threats.sort((a, b) => b.count - a.count);
|
|
495
|
+
return threats.slice(0, n);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Return scans-per-second over recorded time windows.
|
|
500
|
+
* @returns {Array<Object>} Array of [{timestamp, scansPerSec}].
|
|
501
|
+
*/
|
|
502
|
+
throughputHistory() {
|
|
503
|
+
const windows = this.metrics._windows;
|
|
504
|
+
if (windows.length < 2) return [];
|
|
505
|
+
|
|
506
|
+
const result = [];
|
|
507
|
+
for (let i = 1; i < windows.length; i++) {
|
|
508
|
+
const dt = (windows[i].timestamp - windows[i - 1].timestamp) / 1000;
|
|
509
|
+
const dScans = windows[i].scans - windows[i - 1].scans;
|
|
510
|
+
result.push({
|
|
511
|
+
timestamp: windows[i].timestamp,
|
|
512
|
+
scansPerSec: dt > 0 ? dScans / dt : 0
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Return p50, p95, p99 latency percentiles.
|
|
521
|
+
* @returns {Object|null} {p50, p95, p99} in ms, or null if no data.
|
|
522
|
+
*/
|
|
523
|
+
latencyPercentiles() {
|
|
524
|
+
const latencies = this.metrics._scans.latencies;
|
|
525
|
+
if (latencies.length === 0) return null;
|
|
526
|
+
|
|
527
|
+
const sorted = [...latencies].sort((a, b) => a - b);
|
|
528
|
+
return {
|
|
529
|
+
p50: sorted[Math.floor(sorted.length * 0.5)],
|
|
530
|
+
p95: sorted[Math.floor(sorted.length * 0.95)],
|
|
531
|
+
p99: sorted[Math.floor(sorted.length * 0.99)]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// =========================================================================
|
|
537
|
+
// Exports
|
|
538
|
+
// =========================================================================
|
|
539
|
+
|
|
540
|
+
module.exports = {
|
|
541
|
+
ShieldMetrics,
|
|
542
|
+
ShieldTracer,
|
|
543
|
+
MetricsDashboard
|
|
544
|
+
};
|