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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,469 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Audit Log Streaming (v2.1)
5
+ *
6
+ * Stream security audit events to external logging/SIEM systems.
7
+ * Supports Splunk HEC, Elasticsearch, file-based logging, and custom transports.
8
+ *
9
+ * Zero dependencies — uses Node.js built-in http/https/fs modules.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const http = require('http');
15
+ const https = require('https');
16
+
17
+ // =========================================================================
18
+ // TRANSPORT INTERFACE
19
+ // =========================================================================
20
+
21
+ /**
22
+ * Base transport class. Extend for custom destinations.
23
+ */
24
+ class AuditTransport {
25
+ /**
26
+ * Send an event to the transport.
27
+ * @param {object} event - Audit event.
28
+ * @returns {Promise<void>}
29
+ */
30
+ async send(event) { throw new Error('Not implemented'); }
31
+
32
+ /**
33
+ * Send multiple events in a batch.
34
+ * @param {Array<object>} events
35
+ * @returns {Promise<void>}
36
+ */
37
+ async sendBatch(events) {
38
+ for (const event of events) {
39
+ await this.send(event);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Flush any buffered events.
45
+ * @returns {Promise<void>}
46
+ */
47
+ async flush() {}
48
+
49
+ /**
50
+ * Close the transport.
51
+ * @returns {Promise<void>}
52
+ */
53
+ async close() {}
54
+ }
55
+
56
+ // =========================================================================
57
+ // FILE TRANSPORT
58
+ // =========================================================================
59
+
60
+ /**
61
+ * Writes audit events to a local file (JSONL format).
62
+ */
63
+ class FileTransport extends AuditTransport {
64
+ /**
65
+ * @param {object} [options]
66
+ * @param {string} [options.filePath='./agent-shield-audit.log'] - Log file path.
67
+ * @param {number} [options.maxSizeMB=100] - Max file size before rotation.
68
+ * @param {number} [options.maxFiles=5] - Number of rotated files to keep.
69
+ */
70
+ constructor(options = {}) {
71
+ super();
72
+ this.filePath = options.filePath || './agent-shield-audit.log';
73
+ this.maxSizeBytes = (options.maxSizeMB || 100) * 1024 * 1024;
74
+ this.maxFiles = options.maxFiles || 5;
75
+ this._buffer = [];
76
+ this._bufferSize = 0;
77
+ this._flushInterval = setInterval(() => this.flush(), 5000);
78
+ }
79
+
80
+ async send(event) {
81
+ const line = JSON.stringify(event) + '\n';
82
+ this._buffer.push(line);
83
+ this._bufferSize += line.length;
84
+
85
+ if (this._bufferSize >= 64 * 1024) {
86
+ await this.flush();
87
+ }
88
+ }
89
+
90
+ async flush() {
91
+ if (this._buffer.length === 0) return;
92
+
93
+ const data = this._buffer.join('');
94
+ this._buffer = [];
95
+ this._bufferSize = 0;
96
+
97
+ try {
98
+ // Check rotation
99
+ if (fs.existsSync(this.filePath)) {
100
+ const stat = fs.statSync(this.filePath);
101
+ if (stat.size + data.length > this.maxSizeBytes) {
102
+ this._rotate();
103
+ }
104
+ }
105
+
106
+ fs.appendFileSync(this.filePath, data);
107
+ } catch (e) {
108
+ console.warn('[Agent Shield] FileTransport write error:', e.message);
109
+ }
110
+ }
111
+
112
+ async close() {
113
+ clearInterval(this._flushInterval);
114
+ await this.flush();
115
+ }
116
+
117
+ /** @private */
118
+ _rotate() {
119
+ for (let i = this.maxFiles - 1; i >= 1; i--) {
120
+ const from = `${this.filePath}.${i}`;
121
+ const to = `${this.filePath}.${i + 1}`;
122
+ if (fs.existsSync(from)) {
123
+ if (i + 1 > this.maxFiles) fs.unlinkSync(from);
124
+ else fs.renameSync(from, to);
125
+ }
126
+ }
127
+ if (fs.existsSync(this.filePath)) {
128
+ fs.renameSync(this.filePath, `${this.filePath}.1`);
129
+ }
130
+ }
131
+ }
132
+
133
+ // =========================================================================
134
+ // SPLUNK HEC TRANSPORT
135
+ // =========================================================================
136
+
137
+ /**
138
+ * Sends audit events to Splunk via HTTP Event Collector (HEC).
139
+ */
140
+ class SplunkTransport extends AuditTransport {
141
+ /**
142
+ * @param {object} options
143
+ * @param {string} options.url - Splunk HEC URL (e.g., https://splunk:8088/services/collector/event).
144
+ * @param {string} options.token - HEC token.
145
+ * @param {string} [options.index='main'] - Splunk index.
146
+ * @param {string} [options.source='agent-shield'] - Event source.
147
+ * @param {string} [options.sourcetype='_json'] - Source type.
148
+ * @param {number} [options.batchSize=50] - Events per batch.
149
+ * @param {number} [options.flushIntervalMs=5000] - Auto-flush interval.
150
+ */
151
+ constructor(options = {}) {
152
+ super();
153
+ this.url = options.url;
154
+ this.token = options.token;
155
+ this.index = options.index || 'main';
156
+ this.source = options.source || 'agent-shield';
157
+ this.sourcetype = options.sourcetype || '_json';
158
+ this.batchSize = options.batchSize || 50;
159
+ this._buffer = [];
160
+ this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
161
+ this._stats = { sent: 0, errors: 0 };
162
+
163
+ if (!this.url || !this.token) {
164
+ console.warn('[Agent Shield] SplunkTransport: url and token are required.');
165
+ }
166
+ }
167
+
168
+ async send(event) {
169
+ this._buffer.push({
170
+ time: event.timestamp ? event.timestamp / 1000 : Date.now() / 1000,
171
+ source: this.source,
172
+ sourcetype: this.sourcetype,
173
+ index: this.index,
174
+ event
175
+ });
176
+
177
+ if (this._buffer.length >= this.batchSize) {
178
+ await this.flush();
179
+ }
180
+ }
181
+
182
+ async flush() {
183
+ if (this._buffer.length === 0 || !this.url || !this.token) return;
184
+
185
+ const events = this._buffer.splice(0, this.batchSize);
186
+ const payload = events.map(e => JSON.stringify(e)).join('\n');
187
+
188
+ try {
189
+ await this._post(this.url, payload, {
190
+ 'Authorization': `Splunk ${this.token}`,
191
+ 'Content-Type': 'application/json'
192
+ });
193
+ this._stats.sent += events.length;
194
+ } catch (e) {
195
+ this._stats.errors += events.length;
196
+ console.warn('[Agent Shield] SplunkTransport error:', e.message);
197
+ // Re-queue failed events
198
+ this._buffer.unshift(...events);
199
+ }
200
+ }
201
+
202
+ async close() {
203
+ clearInterval(this._flushInterval);
204
+ await this.flush();
205
+ }
206
+
207
+ getStats() { return { ...this._stats, buffered: this._buffer.length }; }
208
+
209
+ /** @private */
210
+ _post(url, body, headers) {
211
+ return new Promise((resolve, reject) => {
212
+ const parsed = new URL(url);
213
+ const lib = parsed.protocol === 'https:' ? https : http;
214
+ const req = lib.request({
215
+ hostname: parsed.hostname, port: parsed.port,
216
+ path: parsed.pathname, method: 'POST',
217
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
218
+ rejectUnauthorized: false, timeout: 10000
219
+ }, (res) => {
220
+ let data = '';
221
+ res.on('data', c => data += c);
222
+ res.on('end', () => resolve(data));
223
+ });
224
+ req.on('error', reject);
225
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
226
+ req.write(body);
227
+ req.end();
228
+ });
229
+ }
230
+ }
231
+
232
+ // =========================================================================
233
+ // ELASTICSEARCH TRANSPORT
234
+ // =========================================================================
235
+
236
+ /**
237
+ * Sends audit events to Elasticsearch.
238
+ */
239
+ class ElasticsearchTransport extends AuditTransport {
240
+ /**
241
+ * @param {object} options
242
+ * @param {string} options.url - Elasticsearch URL (e.g., http://localhost:9200).
243
+ * @param {string} [options.index='agent-shield-audit'] - Index name.
244
+ * @param {string} [options.apiKey] - API key for authentication.
245
+ * @param {number} [options.batchSize=100] - Events per bulk request.
246
+ * @param {number} [options.flushIntervalMs=5000]
247
+ */
248
+ constructor(options = {}) {
249
+ super();
250
+ this.url = options.url;
251
+ this.index = options.index || 'agent-shield-audit';
252
+ this.apiKey = options.apiKey || null;
253
+ this.batchSize = options.batchSize || 100;
254
+ this._buffer = [];
255
+ this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
256
+ this._stats = { sent: 0, errors: 0 };
257
+
258
+ if (!this.url) {
259
+ console.warn('[Agent Shield] ElasticsearchTransport: url is required.');
260
+ }
261
+ }
262
+
263
+ async send(event) {
264
+ this._buffer.push(event);
265
+ if (this._buffer.length >= this.batchSize) {
266
+ await this.flush();
267
+ }
268
+ }
269
+
270
+ async flush() {
271
+ if (this._buffer.length === 0 || !this.url) return;
272
+
273
+ const events = this._buffer.splice(0, this.batchSize);
274
+ const dateStr = new Date().toISOString().split('T')[0].replace(/-/g, '.');
275
+ const indexName = `${this.index}-${dateStr}`;
276
+
277
+ // Build NDJSON bulk payload
278
+ const lines = [];
279
+ for (const event of events) {
280
+ lines.push(JSON.stringify({ index: { _index: indexName } }));
281
+ lines.push(JSON.stringify({ ...event, '@timestamp': event.timestamp || Date.now() }));
282
+ }
283
+ const payload = lines.join('\n') + '\n';
284
+
285
+ const headers = { 'Content-Type': 'application/x-ndjson' };
286
+ if (this.apiKey) headers['Authorization'] = `ApiKey ${this.apiKey}`;
287
+
288
+ try {
289
+ await this._post(`${this.url}/_bulk`, payload, headers);
290
+ this._stats.sent += events.length;
291
+ } catch (e) {
292
+ this._stats.errors += events.length;
293
+ console.warn('[Agent Shield] ElasticsearchTransport error:', e.message);
294
+ this._buffer.unshift(...events);
295
+ }
296
+ }
297
+
298
+ async close() {
299
+ clearInterval(this._flushInterval);
300
+ await this.flush();
301
+ }
302
+
303
+ getStats() { return { ...this._stats, buffered: this._buffer.length }; }
304
+
305
+ /** @private */
306
+ _post(url, body, headers) {
307
+ return new Promise((resolve, reject) => {
308
+ const parsed = new URL(url);
309
+ const lib = parsed.protocol === 'https:' ? https : http;
310
+ const req = lib.request({
311
+ hostname: parsed.hostname, port: parsed.port,
312
+ path: parsed.pathname, method: 'POST',
313
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
314
+ timeout: 10000
315
+ }, (res) => {
316
+ let data = '';
317
+ res.on('data', c => data += c);
318
+ res.on('end', () => resolve(data));
319
+ });
320
+ req.on('error', reject);
321
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
322
+ req.write(body);
323
+ req.end();
324
+ });
325
+ }
326
+ }
327
+
328
+ // =========================================================================
329
+ // AUDIT STREAM MANAGER
330
+ // =========================================================================
331
+
332
+ /**
333
+ * Manages multiple audit transports and routes events to all of them.
334
+ */
335
+ class AuditStreamManager {
336
+ /**
337
+ * @param {object} [options]
338
+ * @param {Array<AuditTransport>} [options.transports] - Initial transports.
339
+ * @param {boolean} [options.includeMetadata=true] - Add instance metadata to events.
340
+ * @param {string} [options.environment='production'] - Environment label.
341
+ */
342
+ constructor(options = {}) {
343
+ this._transports = options.transports || [];
344
+ this.includeMetadata = options.includeMetadata !== false;
345
+ this.environment = options.environment || 'production';
346
+ this._eventCount = 0;
347
+
348
+ console.log('[Agent Shield] AuditStreamManager initialized (%d transports)', this._transports.length);
349
+ }
350
+
351
+ /**
352
+ * Add a transport.
353
+ * @param {AuditTransport} transport
354
+ */
355
+ addTransport(transport) {
356
+ this._transports.push(transport);
357
+ }
358
+
359
+ /**
360
+ * Emit an audit event to all transports.
361
+ * @param {string} type - Event type (e.g., 'scan', 'threat', 'block', 'config_change').
362
+ * @param {object} data - Event data.
363
+ * @returns {Promise<void>}
364
+ */
365
+ async emit(type, data = {}) {
366
+ this._eventCount++;
367
+
368
+ const event = {
369
+ type,
370
+ ...data,
371
+ timestamp: Date.now(),
372
+ eventId: `evt_${this._eventCount}_${Date.now()}`
373
+ };
374
+
375
+ if (this.includeMetadata) {
376
+ event.environment = this.environment;
377
+ event.agentShieldVersion = '2.1.0';
378
+ }
379
+
380
+ const promises = this._transports.map(t =>
381
+ t.send(event).catch(e => console.warn('[Agent Shield] Transport error:', e.message))
382
+ );
383
+
384
+ await Promise.all(promises);
385
+ }
386
+
387
+ /**
388
+ * Emit a scan event.
389
+ * @param {object} scanResult - Result from scanText/AgentShield.
390
+ * @param {object} [context] - Additional context.
391
+ */
392
+ async emitScan(scanResult, context = {}) {
393
+ await this.emit('scan', {
394
+ status: scanResult.status,
395
+ threatCount: scanResult.threats ? scanResult.threats.length : 0,
396
+ categories: scanResult.threats ? [...new Set(scanResult.threats.map(t => t.category))] : [],
397
+ scanTimeMs: scanResult.stats ? scanResult.stats.scanTimeMs : 0,
398
+ ...context
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Emit a threat event.
404
+ * @param {object} threat - Individual threat object.
405
+ * @param {object} [context]
406
+ */
407
+ async emitThreat(threat, context = {}) {
408
+ await this.emit('threat', {
409
+ severity: threat.severity,
410
+ category: threat.category,
411
+ description: threat.description,
412
+ confidence: threat.confidence,
413
+ ...context
414
+ });
415
+ }
416
+
417
+ /**
418
+ * Emit a block event.
419
+ * @param {string} reason
420
+ * @param {object} [context]
421
+ */
422
+ async emitBlock(reason, context = {}) {
423
+ await this.emit('block', { reason, ...context });
424
+ }
425
+
426
+ /**
427
+ * Flush all transports.
428
+ * @returns {Promise<void>}
429
+ */
430
+ async flush() {
431
+ await Promise.all(this._transports.map(t => t.flush()));
432
+ }
433
+
434
+ /**
435
+ * Close all transports.
436
+ * @returns {Promise<void>}
437
+ */
438
+ async close() {
439
+ await Promise.all(this._transports.map(t => t.close()));
440
+ }
441
+
442
+ /**
443
+ * Get streaming statistics.
444
+ * @returns {object}
445
+ */
446
+ getStats() {
447
+ return {
448
+ eventCount: this._eventCount,
449
+ transports: this._transports.length,
450
+ transportStats: this._transports.map((t, i) => ({
451
+ index: i,
452
+ type: t.constructor.name,
453
+ stats: typeof t.getStats === 'function' ? t.getStats() : null
454
+ }))
455
+ };
456
+ }
457
+ }
458
+
459
+ // =========================================================================
460
+ // EXPORTS
461
+ // =========================================================================
462
+
463
+ module.exports = {
464
+ AuditStreamManager,
465
+ AuditTransport,
466
+ FileTransport,
467
+ SplunkTransport,
468
+ ElasticsearchTransport
469
+ };
package/src/badges.js ADDED
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Integration Badges & GitHub Action Support
5
+ *
6
+ * Generate SVG badges for READMEs and CI/CD pipelines.
7
+ */
8
+
9
+ // =========================================================================
10
+ // Badge Generator
11
+ // =========================================================================
12
+
13
+ class BadgeGenerator {
14
+ /**
15
+ * Generate a Shield Score badge in SVG.
16
+ */
17
+ static shieldScore(score) {
18
+ const color = score >= 90 ? '22c55e' : score >= 70 ? 'eab308' : score >= 50 ? 'f97316' : 'ef4444';
19
+ const grade = score >= 95 ? 'A+' : score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 50 ? 'D' : 'F';
20
+ return BadgeGenerator.generateSVG('shield score', `${score} (${grade})`, color);
21
+ }
22
+
23
+ /**
24
+ * Generate a protection status badge.
25
+ */
26
+ static protectionStatus(enabled = true) {
27
+ return BadgeGenerator.generateSVG(
28
+ 'agent shield',
29
+ enabled ? 'protected' : 'unprotected',
30
+ enabled ? '3b82f6' : 'ef4444'
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Generate a detection rate badge.
36
+ */
37
+ static detectionRate(rate) {
38
+ const num = parseFloat(rate);
39
+ const color = num >= 90 ? '22c55e' : num >= 70 ? 'eab308' : 'ef4444';
40
+ return BadgeGenerator.generateSVG('detection rate', `${num}%`, color);
41
+ }
42
+
43
+ /**
44
+ * Generate a scan count badge.
45
+ */
46
+ static scanCount(count) {
47
+ return BadgeGenerator.generateSVG('scans', count.toLocaleString(), '06b6d4');
48
+ }
49
+
50
+ /**
51
+ * Generate a compliance badge.
52
+ */
53
+ static compliance(framework, rate) {
54
+ const num = parseFloat(rate);
55
+ const color = num >= 80 ? '22c55e' : num >= 50 ? 'eab308' : 'ef4444';
56
+ return BadgeGenerator.generateSVG(framework, `${num}%`, color);
57
+ }
58
+
59
+ /**
60
+ * Generate a custom badge.
61
+ */
62
+ static custom(label, value, color = '3b82f6') {
63
+ return BadgeGenerator.generateSVG(label, value, color);
64
+ }
65
+
66
+ /**
67
+ * Generate Markdown badge links for README.
68
+ */
69
+ static markdownBadges(options = {}) {
70
+ const lines = [];
71
+
72
+ if (options.score !== undefined) {
73
+ const color = options.score >= 90 ? 'brightgreen' : options.score >= 70 ? 'yellow' : options.score >= 50 ? 'orange' : 'red';
74
+ lines.push(`![Shield Score](https://img.shields.io/badge/shield_score-${options.score}-${color}?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiLz48L3N2Zz4=)`);
75
+ }
76
+
77
+ lines.push(`![Agent Shield](https://img.shields.io/badge/protected_by-agent_shield-blue?style=flat-square)`);
78
+
79
+ if (options.detectionRate) {
80
+ const color = parseFloat(options.detectionRate) >= 90 ? 'brightgreen' : 'yellow';
81
+ lines.push(`![Detection Rate](https://img.shields.io/badge/detection_rate-${options.detectionRate}%25-${color}?style=flat-square)`);
82
+ }
83
+
84
+ return lines.join('\n');
85
+ }
86
+
87
+ /**
88
+ * Core SVG badge generator.
89
+ */
90
+ static generateSVG(label, value, color) {
91
+ const labelWidth = label.length * 7 + 12;
92
+ const valueWidth = String(value).length * 7 + 12;
93
+ const totalWidth = labelWidth + valueWidth;
94
+
95
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img">
96
+ <title>${label}: ${value}</title>
97
+ <linearGradient id="s" x2="0" y2="100%">
98
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
99
+ <stop offset="1" stop-opacity=".1"/>
100
+ </linearGradient>
101
+ <clipPath id="r"><rect width="${totalWidth}" height="20" rx="3" fill="#fff"/></clipPath>
102
+ <g clip-path="url(#r)">
103
+ <rect width="${labelWidth}" height="20" fill="#555"/>
104
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="#${color}"/>
105
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
106
+ </g>
107
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
108
+ <text x="${labelWidth / 2}" y="14" fill="#010101" fill-opacity=".3">${label}</text>
109
+ <text x="${labelWidth / 2}" y="13">${label}</text>
110
+ <text x="${labelWidth + valueWidth / 2}" y="14" fill="#010101" fill-opacity=".3">${value}</text>
111
+ <text x="${labelWidth + valueWidth / 2}" y="13">${value}</text>
112
+ </g>
113
+ </svg>`;
114
+ }
115
+ }
116
+
117
+ // =========================================================================
118
+ // GitHub Action Output
119
+ // =========================================================================
120
+
121
+ class GitHubActionReporter {
122
+ constructor() {
123
+ this.annotations = [];
124
+ }
125
+
126
+ /**
127
+ * Report scan results as GitHub Action annotations.
128
+ */
129
+ reportScan(result, file = '', line = 0) {
130
+ if (!result.threats || result.threats.length === 0) return;
131
+
132
+ for (const threat of result.threats) {
133
+ const level = threat.severity === 'critical' || threat.severity === 'high' ? 'error' : 'warning';
134
+ const msg = `[Agent Shield] ${threat.description} (${threat.severity})`;
135
+
136
+ // GitHub Actions annotation format
137
+ if (file) {
138
+ console.log(`::${level} file=${file},line=${line}::${msg}`);
139
+ } else {
140
+ console.log(`::${level}::${msg}`);
141
+ }
142
+
143
+ this.annotations.push({ level, file, line, message: msg });
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Set GitHub Action output variables.
149
+ */
150
+ setOutputs(results) {
151
+ const total = results.threats ? results.threats.length : 0;
152
+ const blocked = results.blocked || false;
153
+ const status = results.status || 'unknown';
154
+
155
+ // Use GITHUB_OUTPUT env file (modern) with fallback to deprecated ::set-output
156
+ const githubOutput = process.env.GITHUB_OUTPUT;
157
+ if (githubOutput) {
158
+ const fs = require('fs');
159
+ fs.appendFileSync(githubOutput, `threat_count=${total}\n`);
160
+ fs.appendFileSync(githubOutput, `status=${status}\n`);
161
+ fs.appendFileSync(githubOutput, `blocked=${blocked}\n`);
162
+ } else {
163
+ console.log(`::set-output name=threat_count::${total}`);
164
+ console.log(`::set-output name=status::${status}`);
165
+ console.log(`::set-output name=blocked::${blocked}`);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Create a summary for GitHub Actions.
171
+ */
172
+ createSummary(shieldScore, scanResults) {
173
+ const lines = [];
174
+ lines.push('## Agent Shield Scan Results\n');
175
+ lines.push(`| Metric | Value |`);
176
+ lines.push(`|--------|-------|`);
177
+
178
+ if (shieldScore) {
179
+ lines.push(`| Shield Score | ${shieldScore.score}/100 (${shieldScore.grade}) |`);
180
+ }
181
+
182
+ if (scanResults) {
183
+ lines.push(`| Status | ${scanResults.status} |`);
184
+ lines.push(`| Threats | ${scanResults.threats ? scanResults.threats.length : 0} |`);
185
+ lines.push(`| Blocked | ${scanResults.blocked ? 'Yes' : 'No'} |`);
186
+ }
187
+
188
+ lines.push('');
189
+ return lines.join('\n');
190
+ }
191
+ }
192
+
193
+ module.exports = {
194
+ BadgeGenerator,
195
+ GitHubActionReporter
196
+ };