agentshield-sdk 7.4.0 → 10.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 (57) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -21
  3. package/README.md +30 -37
  4. package/bin/agentshield-audit +51 -0
  5. package/package.json +7 -9
  6. package/src/adaptive.js +330 -330
  7. package/src/agent-intent.js +807 -0
  8. package/src/alert-tuning.js +480 -480
  9. package/src/audit-streaming.js +1 -1
  10. package/src/badges.js +196 -196
  11. package/src/behavioral-dna.js +12 -0
  12. package/src/canary.js +2 -3
  13. package/src/certification.js +563 -563
  14. package/src/circuit-breaker.js +2 -2
  15. package/src/confused-deputy.js +4 -0
  16. package/src/conversation.js +494 -494
  17. package/src/cross-turn.js +649 -0
  18. package/src/ctf.js +462 -462
  19. package/src/detector-core.js +71 -152
  20. package/src/document-scanner.js +795 -795
  21. package/src/drift-monitor.js +344 -0
  22. package/src/encoding.js +429 -429
  23. package/src/ensemble.js +523 -0
  24. package/src/enterprise.js +405 -405
  25. package/src/flight-recorder.js +2 -0
  26. package/src/i18n-patterns.js +523 -523
  27. package/src/index.js +19 -0
  28. package/src/main.js +79 -6
  29. package/src/mcp-guard.js +974 -0
  30. package/src/micro-model.js +762 -0
  31. package/src/ml-detector.js +316 -0
  32. package/src/model-finetuning.js +884 -884
  33. package/src/multimodal.js +296 -296
  34. package/src/nist-mapping.js +2 -2
  35. package/src/observability.js +330 -330
  36. package/src/openclaw.js +450 -450
  37. package/src/otel.js +544 -544
  38. package/src/owasp-2025.js +1 -1
  39. package/src/owasp-agentic.js +420 -0
  40. package/src/persistent-learning.js +677 -0
  41. package/src/plugin-marketplace.js +628 -628
  42. package/src/plugin-system.js +349 -349
  43. package/src/policy-extended.js +635 -635
  44. package/src/policy.js +443 -443
  45. package/src/prompt-leakage.js +2 -2
  46. package/src/real-attack-datasets.js +2 -2
  47. package/src/redteam-cli.js +439 -0
  48. package/src/self-training.js +772 -0
  49. package/src/smart-config.js +812 -0
  50. package/src/supply-chain-scanner.js +691 -0
  51. package/src/testing.js +5 -1
  52. package/src/threat-encyclopedia.js +629 -629
  53. package/src/threat-intel-network.js +1017 -1017
  54. package/src/token-analysis.js +467 -467
  55. package/src/tool-output-validator.js +354 -354
  56. package/src/watermark.js +1 -2
  57. package/types/index.d.ts +660 -0
package/src/multimodal.js CHANGED
@@ -1,296 +1,296 @@
1
- 'use strict';
2
-
3
- /**
4
- * Agent Shield — Multi-Modal Scanning (v3.0)
5
- *
6
- * Scans non-text modalities for injection attacks:
7
- * - Image alt text and metadata
8
- * - Audio/video transcripts
9
- * - PDF extracted text
10
- * - Structured tool outputs (JSON, XML)
11
- * - Base64-encoded payloads in any field
12
- *
13
- * All processing runs locally — no data ever leaves your environment.
14
- */
15
-
16
- const { scanText } = require('./detector-core');
17
-
18
- // =========================================================================
19
- // MODALITY EXTRACTORS
20
- // =========================================================================
21
-
22
- /**
23
- * Extract scannable text from various data formats.
24
- */
25
- class ModalityExtractor {
26
- /**
27
- * Extract text from an image-like object (metadata, alt text, EXIF).
28
- * @param {object} imageData - { altText, title, caption, metadata, ocrText }
29
- * @returns {Array<{text: string, source: string}>}
30
- */
31
- extractFromImage(imageData) {
32
- const texts = [];
33
-
34
- if (imageData.altText) texts.push({ text: imageData.altText, source: 'image:alt_text' });
35
- if (imageData.title) texts.push({ text: imageData.title, source: 'image:title' });
36
- if (imageData.caption) texts.push({ text: imageData.caption, source: 'image:caption' });
37
- if (imageData.ocrText) texts.push({ text: imageData.ocrText, source: 'image:ocr' });
38
-
39
- // Check EXIF/metadata fields for hidden payloads
40
- if (imageData.metadata && typeof imageData.metadata === 'object') {
41
- for (const [key, value] of Object.entries(imageData.metadata)) {
42
- if (typeof value === 'string' && value.length > 10) {
43
- texts.push({ text: value, source: `image:metadata:${key}` });
44
- }
45
- }
46
- }
47
-
48
- // Check for base64 encoded content in any field
49
- if (imageData.base64) {
50
- try {
51
- const decoded = Buffer.from(imageData.base64.substring(0, 10000), 'base64').toString('utf-8');
52
- const printable = decoded.split('').filter(c => c.charCodeAt(0) >= 32 && c.charCodeAt(0) <= 126).length;
53
- if (printable / decoded.length > 0.7) {
54
- texts.push({ text: decoded, source: 'image:base64_decoded' });
55
- }
56
- } catch (e) {
57
- // Not valid base64
58
- }
59
- }
60
-
61
- return texts;
62
- }
63
-
64
- /**
65
- * Extract text from an audio/video transcript.
66
- * @param {object} audioData - { transcript, segments, metadata, speakers }
67
- * @returns {Array<{text: string, source: string}>}
68
- */
69
- extractFromAudio(audioData) {
70
- const texts = [];
71
-
72
- if (audioData.transcript) {
73
- texts.push({ text: audioData.transcript, source: 'audio:transcript' });
74
- }
75
-
76
- if (audioData.segments && Array.isArray(audioData.segments)) {
77
- for (let i = 0; i < audioData.segments.length; i++) {
78
- const seg = audioData.segments[i];
79
- if (seg.text && seg.text.length > 10) {
80
- texts.push({ text: seg.text, source: `audio:segment:${i}` });
81
- }
82
- }
83
- }
84
-
85
- if (audioData.speakers && typeof audioData.speakers === 'object') {
86
- for (const [speaker, content] of Object.entries(audioData.speakers)) {
87
- if (typeof content === 'string' && content.length > 10) {
88
- texts.push({ text: content, source: `audio:speaker:${speaker}` });
89
- }
90
- }
91
- }
92
-
93
- if (audioData.metadata && typeof audioData.metadata === 'object') {
94
- for (const [key, value] of Object.entries(audioData.metadata)) {
95
- if (typeof value === 'string' && value.length > 10) {
96
- texts.push({ text: value, source: `audio:metadata:${key}` });
97
- }
98
- }
99
- }
100
-
101
- return texts;
102
- }
103
-
104
- /**
105
- * Extract text from a PDF-like object.
106
- * @param {object} pdfData - { text, pages, metadata, annotations }
107
- * @returns {Array<{text: string, source: string}>}
108
- */
109
- extractFromPDF(pdfData) {
110
- const texts = [];
111
-
112
- if (pdfData.text) {
113
- texts.push({ text: pdfData.text, source: 'pdf:full_text' });
114
- }
115
-
116
- if (pdfData.pages && Array.isArray(pdfData.pages)) {
117
- for (let i = 0; i < pdfData.pages.length; i++) {
118
- const page = pdfData.pages[i];
119
- const pageText = typeof page === 'string' ? page : page.text;
120
- if (pageText && pageText.length > 10) {
121
- texts.push({ text: pageText, source: `pdf:page:${i + 1}` });
122
- }
123
- }
124
- }
125
-
126
- if (pdfData.annotations && Array.isArray(pdfData.annotations)) {
127
- for (let i = 0; i < pdfData.annotations.length; i++) {
128
- const ann = pdfData.annotations[i];
129
- const annText = typeof ann === 'string' ? ann : ann.text || ann.content;
130
- if (annText && annText.length > 10) {
131
- texts.push({ text: annText, source: `pdf:annotation:${i}` });
132
- }
133
- }
134
- }
135
-
136
- if (pdfData.metadata && typeof pdfData.metadata === 'object') {
137
- for (const [key, value] of Object.entries(pdfData.metadata)) {
138
- if (typeof value === 'string' && value.length > 10) {
139
- texts.push({ text: value, source: `pdf:metadata:${key}` });
140
- }
141
- }
142
- }
143
-
144
- return texts;
145
- }
146
-
147
- /**
148
- * Extract text from a tool call response.
149
- * @param {object} toolOutput - Any structured object from a tool call.
150
- * @param {string} [toolName='unknown'] - Name of the tool.
151
- * @returns {Array<{text: string, source: string}>}
152
- */
153
- extractFromToolOutput(toolOutput, toolName = 'unknown') {
154
- const texts = [];
155
- this._extractStrings(toolOutput, `tool:${toolName}`, texts, 0);
156
- return texts;
157
- }
158
-
159
- /** @private */
160
- _extractStrings(obj, prefix, results, depth) {
161
- if (depth > 8) return;
162
-
163
- if (typeof obj === 'string' && obj.length > 10) {
164
- results.push({ text: obj, source: prefix });
165
- } else if (Array.isArray(obj)) {
166
- for (let i = 0; i < Math.min(obj.length, 100); i++) {
167
- this._extractStrings(obj[i], `${prefix}[${i}]`, results, depth + 1);
168
- }
169
- } else if (obj && typeof obj === 'object') {
170
- for (const [key, value] of Object.entries(obj)) {
171
- this._extractStrings(value, `${prefix}.${key}`, results, depth + 1);
172
- }
173
- }
174
- }
175
- }
176
-
177
- // =========================================================================
178
- // MULTI-MODAL SCANNER
179
- // =========================================================================
180
-
181
- /**
182
- * Scans multi-modal inputs for injection attacks.
183
- */
184
- class MultiModalScanner {
185
- /**
186
- * @param {object} [options]
187
- * @param {string} [options.sensitivity='high'] - Scan sensitivity.
188
- * @param {Function} [options.onThreat] - Callback on threat detection.
189
- */
190
- constructor(options = {}) {
191
- this.sensitivity = options.sensitivity || 'high';
192
- this.onThreat = options.onThreat || null;
193
-
194
- this._extractor = new ModalityExtractor();
195
- this._stats = { scans: 0, threats: 0, modalities: {} };
196
-
197
- console.log('[Agent Shield] MultiModalScanner initialized (sensitivity: %s)', this.sensitivity);
198
- }
199
-
200
- /**
201
- * Scan an image for hidden injections.
202
- * @param {object} imageData - Image data with text fields.
203
- * @returns {object} { clean: boolean, threats: Array, modality: 'image' }
204
- */
205
- scanImage(imageData) {
206
- return this._scanModality('image', this._extractor.extractFromImage(imageData));
207
- }
208
-
209
- /**
210
- * Scan audio/video transcript for injections.
211
- * @param {object} audioData - Audio data with transcript/segments.
212
- * @returns {object}
213
- */
214
- scanAudio(audioData) {
215
- return this._scanModality('audio', this._extractor.extractFromAudio(audioData));
216
- }
217
-
218
- /**
219
- * Scan PDF content for injections.
220
- * @param {object} pdfData - PDF data with text/pages.
221
- * @returns {object}
222
- */
223
- scanPDF(pdfData) {
224
- return this._scanModality('pdf', this._extractor.extractFromPDF(pdfData));
225
- }
226
-
227
- /**
228
- * Scan a tool's output for injections.
229
- * @param {object} toolOutput - Tool output data.
230
- * @param {string} [toolName] - Tool name.
231
- * @returns {object}
232
- */
233
- scanToolOutput(toolOutput, toolName) {
234
- return this._scanModality('tool_output', this._extractor.extractFromToolOutput(toolOutput, toolName));
235
- }
236
-
237
- /**
238
- * Scan any modality by providing extracted texts directly.
239
- * @param {string} modality - Modality label.
240
- * @param {Array<{text: string, source: string}>} texts - Extracted text items.
241
- * @returns {object}
242
- */
243
- scanRaw(modality, texts) {
244
- return this._scanModality(modality, texts);
245
- }
246
-
247
- /**
248
- * Get scanning statistics.
249
- * @returns {object}
250
- */
251
- getStats() {
252
- return { ...this._stats };
253
- }
254
-
255
- /** @private */
256
- _scanModality(modality, texts) {
257
- this._stats.scans++;
258
- this._stats.modalities[modality] = (this._stats.modalities[modality] || 0) + 1;
259
-
260
- const allThreats = [];
261
-
262
- for (const { text, source } of texts) {
263
- const result = scanText(text, { source, sensitivity: this.sensitivity });
264
- if (result.threats.length > 0) {
265
- for (const threat of result.threats) {
266
- allThreats.push({
267
- ...threat,
268
- modality,
269
- source,
270
- description: `[${modality.toUpperCase()}] ${threat.description}`
271
- });
272
- }
273
- }
274
- }
275
-
276
- if (allThreats.length > 0) {
277
- this._stats.threats += allThreats.length;
278
- if (this.onThreat) {
279
- this.onThreat({ modality, threats: allThreats });
280
- }
281
- }
282
-
283
- return {
284
- clean: allThreats.length === 0,
285
- threats: allThreats,
286
- modality,
287
- textsScanned: texts.length
288
- };
289
- }
290
- }
291
-
292
- // =========================================================================
293
- // EXPORTS
294
- // =========================================================================
295
-
296
- module.exports = { MultiModalScanner, ModalityExtractor };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Multi-Modal Scanning (v3.0)
5
+ *
6
+ * Scans non-text modalities for injection attacks:
7
+ * - Image alt text and metadata
8
+ * - Audio/video transcripts
9
+ * - PDF extracted text
10
+ * - Structured tool outputs (JSON, XML)
11
+ * - Base64-encoded payloads in any field
12
+ *
13
+ * All processing runs locally — no data ever leaves your environment.
14
+ */
15
+
16
+ const { scanText } = require('./detector-core');
17
+
18
+ // =========================================================================
19
+ // MODALITY EXTRACTORS
20
+ // =========================================================================
21
+
22
+ /**
23
+ * Extract scannable text from various data formats.
24
+ */
25
+ class ModalityExtractor {
26
+ /**
27
+ * Extract text from an image-like object (metadata, alt text, EXIF).
28
+ * @param {object} imageData - { altText, title, caption, metadata, ocrText }
29
+ * @returns {Array<{text: string, source: string}>}
30
+ */
31
+ extractFromImage(imageData) {
32
+ const texts = [];
33
+
34
+ if (imageData.altText) texts.push({ text: imageData.altText, source: 'image:alt_text' });
35
+ if (imageData.title) texts.push({ text: imageData.title, source: 'image:title' });
36
+ if (imageData.caption) texts.push({ text: imageData.caption, source: 'image:caption' });
37
+ if (imageData.ocrText) texts.push({ text: imageData.ocrText, source: 'image:ocr' });
38
+
39
+ // Check EXIF/metadata fields for hidden payloads
40
+ if (imageData.metadata && typeof imageData.metadata === 'object') {
41
+ for (const [key, value] of Object.entries(imageData.metadata)) {
42
+ if (typeof value === 'string' && value.length > 10) {
43
+ texts.push({ text: value, source: `image:metadata:${key}` });
44
+ }
45
+ }
46
+ }
47
+
48
+ // Check for base64 encoded content in any field
49
+ if (imageData.base64) {
50
+ try {
51
+ const decoded = Buffer.from(imageData.base64.substring(0, 10000), 'base64').toString('utf-8');
52
+ const printable = decoded.split('').filter(c => c.charCodeAt(0) >= 32 && c.charCodeAt(0) <= 126).length;
53
+ if (printable / decoded.length > 0.7) {
54
+ texts.push({ text: decoded, source: 'image:base64_decoded' });
55
+ }
56
+ } catch (e) {
57
+ // Not valid base64
58
+ }
59
+ }
60
+
61
+ return texts;
62
+ }
63
+
64
+ /**
65
+ * Extract text from an audio/video transcript.
66
+ * @param {object} audioData - { transcript, segments, metadata, speakers }
67
+ * @returns {Array<{text: string, source: string}>}
68
+ */
69
+ extractFromAudio(audioData) {
70
+ const texts = [];
71
+
72
+ if (audioData.transcript) {
73
+ texts.push({ text: audioData.transcript, source: 'audio:transcript' });
74
+ }
75
+
76
+ if (audioData.segments && Array.isArray(audioData.segments)) {
77
+ for (let i = 0; i < audioData.segments.length; i++) {
78
+ const seg = audioData.segments[i];
79
+ if (seg.text && seg.text.length > 10) {
80
+ texts.push({ text: seg.text, source: `audio:segment:${i}` });
81
+ }
82
+ }
83
+ }
84
+
85
+ if (audioData.speakers && typeof audioData.speakers === 'object') {
86
+ for (const [speaker, content] of Object.entries(audioData.speakers)) {
87
+ if (typeof content === 'string' && content.length > 10) {
88
+ texts.push({ text: content, source: `audio:speaker:${speaker}` });
89
+ }
90
+ }
91
+ }
92
+
93
+ if (audioData.metadata && typeof audioData.metadata === 'object') {
94
+ for (const [key, value] of Object.entries(audioData.metadata)) {
95
+ if (typeof value === 'string' && value.length > 10) {
96
+ texts.push({ text: value, source: `audio:metadata:${key}` });
97
+ }
98
+ }
99
+ }
100
+
101
+ return texts;
102
+ }
103
+
104
+ /**
105
+ * Extract text from a PDF-like object.
106
+ * @param {object} pdfData - { text, pages, metadata, annotations }
107
+ * @returns {Array<{text: string, source: string}>}
108
+ */
109
+ extractFromPDF(pdfData) {
110
+ const texts = [];
111
+
112
+ if (pdfData.text) {
113
+ texts.push({ text: pdfData.text, source: 'pdf:full_text' });
114
+ }
115
+
116
+ if (pdfData.pages && Array.isArray(pdfData.pages)) {
117
+ for (let i = 0; i < pdfData.pages.length; i++) {
118
+ const page = pdfData.pages[i];
119
+ const pageText = typeof page === 'string' ? page : page.text;
120
+ if (pageText && pageText.length > 10) {
121
+ texts.push({ text: pageText, source: `pdf:page:${i + 1}` });
122
+ }
123
+ }
124
+ }
125
+
126
+ if (pdfData.annotations && Array.isArray(pdfData.annotations)) {
127
+ for (let i = 0; i < pdfData.annotations.length; i++) {
128
+ const ann = pdfData.annotations[i];
129
+ const annText = typeof ann === 'string' ? ann : ann.text || ann.content;
130
+ if (annText && annText.length > 10) {
131
+ texts.push({ text: annText, source: `pdf:annotation:${i}` });
132
+ }
133
+ }
134
+ }
135
+
136
+ if (pdfData.metadata && typeof pdfData.metadata === 'object') {
137
+ for (const [key, value] of Object.entries(pdfData.metadata)) {
138
+ if (typeof value === 'string' && value.length > 10) {
139
+ texts.push({ text: value, source: `pdf:metadata:${key}` });
140
+ }
141
+ }
142
+ }
143
+
144
+ return texts;
145
+ }
146
+
147
+ /**
148
+ * Extract text from a tool call response.
149
+ * @param {object} toolOutput - Any structured object from a tool call.
150
+ * @param {string} [toolName='unknown'] - Name of the tool.
151
+ * @returns {Array<{text: string, source: string}>}
152
+ */
153
+ extractFromToolOutput(toolOutput, toolName = 'unknown') {
154
+ const texts = [];
155
+ this._extractStrings(toolOutput, `tool:${toolName}`, texts, 0);
156
+ return texts;
157
+ }
158
+
159
+ /** @private */
160
+ _extractStrings(obj, prefix, results, depth) {
161
+ if (depth > 8) return;
162
+
163
+ if (typeof obj === 'string' && obj.length > 10) {
164
+ results.push({ text: obj, source: prefix });
165
+ } else if (Array.isArray(obj)) {
166
+ for (let i = 0; i < Math.min(obj.length, 100); i++) {
167
+ this._extractStrings(obj[i], `${prefix}[${i}]`, results, depth + 1);
168
+ }
169
+ } else if (obj && typeof obj === 'object') {
170
+ for (const [key, value] of Object.entries(obj)) {
171
+ this._extractStrings(value, `${prefix}.${key}`, results, depth + 1);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // =========================================================================
178
+ // MULTI-MODAL SCANNER
179
+ // =========================================================================
180
+
181
+ /**
182
+ * Scans multi-modal inputs for injection attacks.
183
+ */
184
+ class MultiModalScanner {
185
+ /**
186
+ * @param {object} [options]
187
+ * @param {string} [options.sensitivity='high'] - Scan sensitivity.
188
+ * @param {Function} [options.onThreat] - Callback on threat detection.
189
+ */
190
+ constructor(options = {}) {
191
+ this.sensitivity = options.sensitivity || 'high';
192
+ this.onThreat = options.onThreat || null;
193
+
194
+ this._extractor = new ModalityExtractor();
195
+ this._stats = { scans: 0, threats: 0, modalities: {} };
196
+
197
+ console.log('[Agent Shield] MultiModalScanner initialized (sensitivity: %s)', this.sensitivity);
198
+ }
199
+
200
+ /**
201
+ * Scan an image for hidden injections.
202
+ * @param {object} imageData - Image data with text fields.
203
+ * @returns {object} { clean: boolean, threats: Array, modality: 'image' }
204
+ */
205
+ scanImage(imageData) {
206
+ return this._scanModality('image', this._extractor.extractFromImage(imageData));
207
+ }
208
+
209
+ /**
210
+ * Scan audio/video transcript for injections.
211
+ * @param {object} audioData - Audio data with transcript/segments.
212
+ * @returns {object}
213
+ */
214
+ scanAudio(audioData) {
215
+ return this._scanModality('audio', this._extractor.extractFromAudio(audioData));
216
+ }
217
+
218
+ /**
219
+ * Scan PDF content for injections.
220
+ * @param {object} pdfData - PDF data with text/pages.
221
+ * @returns {object}
222
+ */
223
+ scanPDF(pdfData) {
224
+ return this._scanModality('pdf', this._extractor.extractFromPDF(pdfData));
225
+ }
226
+
227
+ /**
228
+ * Scan a tool's output for injections.
229
+ * @param {object} toolOutput - Tool output data.
230
+ * @param {string} [toolName] - Tool name.
231
+ * @returns {object}
232
+ */
233
+ scanToolOutput(toolOutput, toolName) {
234
+ return this._scanModality('tool_output', this._extractor.extractFromToolOutput(toolOutput, toolName));
235
+ }
236
+
237
+ /**
238
+ * Scan any modality by providing extracted texts directly.
239
+ * @param {string} modality - Modality label.
240
+ * @param {Array<{text: string, source: string}>} texts - Extracted text items.
241
+ * @returns {object}
242
+ */
243
+ scanRaw(modality, texts) {
244
+ return this._scanModality(modality, texts);
245
+ }
246
+
247
+ /**
248
+ * Get scanning statistics.
249
+ * @returns {object}
250
+ */
251
+ getStats() {
252
+ return { ...this._stats };
253
+ }
254
+
255
+ /** @private */
256
+ _scanModality(modality, texts) {
257
+ this._stats.scans++;
258
+ this._stats.modalities[modality] = (this._stats.modalities[modality] || 0) + 1;
259
+
260
+ const allThreats = [];
261
+
262
+ for (const { text, source } of texts) {
263
+ const result = scanText(text, { source, sensitivity: this.sensitivity });
264
+ if (result.threats.length > 0) {
265
+ for (const threat of result.threats) {
266
+ allThreats.push({
267
+ ...threat,
268
+ modality,
269
+ source,
270
+ description: `[${modality.toUpperCase()}] ${threat.description}`
271
+ });
272
+ }
273
+ }
274
+ }
275
+
276
+ if (allThreats.length > 0) {
277
+ this._stats.threats += allThreats.length;
278
+ if (this.onThreat) {
279
+ this.onThreat({ modality, threats: allThreats });
280
+ }
281
+ }
282
+
283
+ return {
284
+ clean: allThreats.length === 0,
285
+ threats: allThreats,
286
+ modality,
287
+ textsScanned: texts.length
288
+ };
289
+ }
290
+ }
291
+
292
+ // =========================================================================
293
+ // EXPORTS
294
+ // =========================================================================
295
+
296
+ module.exports = { MultiModalScanner, ModalityExtractor };
@@ -133,12 +133,12 @@ class NISTMapper {
133
133
  for (const [funcName, func] of Object.entries(NIST_AI_RMF_2025.functions)) {
134
134
  const funcCovered = func.categories.filter(c => c.agentShieldMapping.length > 0).length;
135
135
  const funcTotal = func.categories.length;
136
- byFunction[funcName] = { covered: funcCovered, total: funcTotal, percentage: Math.round((funcCovered / funcTotal) * 100) };
136
+ byFunction[funcName] = { covered: funcCovered, total: funcTotal, percentage: funcTotal > 0 ? Math.round((funcCovered / funcTotal) * 100) : 0 };
137
137
  covered += funcCovered;
138
138
  total += funcTotal;
139
139
  }
140
140
 
141
- return { percentage: Math.round((covered / total) * 100), covered, total, byFunction };
141
+ return { percentage: total > 0 ? Math.round((covered / total) * 100) : 0, covered, total, byFunction };
142
142
  }
143
143
 
144
144
  /**