@cdot65/prisma-airs 0.2.2 → 0.2.4

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/src/scanner.ts CHANGED
@@ -12,6 +12,38 @@ const AIRS_SCAN_ENDPOINT = `${AIRS_API_BASE}/v1/scan/sync/request`;
12
12
  export type Action = "allow" | "warn" | "block";
13
13
  export type Severity = "SAFE" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
14
14
 
15
+ export interface ToolEventMetadata {
16
+ ecosystem: string;
17
+ method: string;
18
+ serverName: string;
19
+ toolInvoked?: string;
20
+ }
21
+
22
+ export interface ToolEventInput {
23
+ metadata: ToolEventMetadata;
24
+ input?: string;
25
+ output?: string;
26
+ }
27
+
28
+ export interface ToolDetectionFlags {
29
+ injection?: boolean;
30
+ urlCats?: boolean;
31
+ dlp?: boolean;
32
+ dbSecurity?: boolean;
33
+ toxicContent?: boolean;
34
+ maliciousCode?: boolean;
35
+ agent?: boolean;
36
+ topicViolation?: boolean;
37
+ }
38
+
39
+ export interface ToolDetected {
40
+ verdict: string;
41
+ metadata: ToolEventMetadata;
42
+ summary: string;
43
+ inputDetected?: ToolDetectionFlags;
44
+ outputDetected?: ToolDetectionFlags;
45
+ }
46
+
15
47
  export interface ScanRequest {
16
48
  prompt?: string;
17
49
  response?: string;
@@ -21,17 +53,57 @@ export interface ScanRequest {
21
53
  appName?: string;
22
54
  appUser?: string;
23
55
  aiModel?: string;
56
+ apiKey?: string;
57
+ toolEvents?: ToolEventInput[];
24
58
  }
25
59
 
26
60
  export interface PromptDetected {
27
61
  injection: boolean;
28
62
  dlp: boolean;
29
63
  urlCats: boolean;
64
+ toxicContent: boolean;
65
+ maliciousCode: boolean;
66
+ agent: boolean;
67
+ topicViolation: boolean;
30
68
  }
31
69
 
32
70
  export interface ResponseDetected {
33
71
  dlp: boolean;
34
72
  urlCats: boolean;
73
+ dbSecurity: boolean;
74
+ toxicContent: boolean;
75
+ maliciousCode: boolean;
76
+ agent: boolean;
77
+ ungrounded: boolean;
78
+ topicViolation: boolean;
79
+ }
80
+
81
+ export interface TopicGuardrails {
82
+ allowedTopics: string[];
83
+ blockedTopics: string[];
84
+ }
85
+
86
+ export interface DetectionDetails {
87
+ topicGuardrailsDetails?: TopicGuardrails;
88
+ }
89
+
90
+ export interface PatternDetection {
91
+ pattern: string;
92
+ locations: number[][];
93
+ }
94
+
95
+ export interface MaskedData {
96
+ data?: string;
97
+ patternDetections: PatternDetection[];
98
+ }
99
+
100
+ export type ContentErrorType = "prompt" | "response";
101
+ export type ErrorStatus = "error" | "timeout";
102
+
103
+ export interface ContentError {
104
+ contentType: ContentErrorType;
105
+ feature: string;
106
+ status: ErrorStatus;
35
107
  }
36
108
 
37
109
  export interface ScanResult {
@@ -47,12 +119,63 @@ export interface ScanResult {
47
119
  trId?: string;
48
120
  latencyMs: number;
49
121
  error?: string;
122
+ promptDetectionDetails?: DetectionDetails;
123
+ responseDetectionDetails?: DetectionDetails;
124
+ promptMaskedData?: MaskedData;
125
+ responseMaskedData?: MaskedData;
126
+ timeout: boolean;
127
+ hasError: boolean;
128
+ contentErrors: ContentError[];
129
+ toolDetected?: ToolDetected;
130
+ source?: string;
131
+ profileId?: string;
132
+ createdAt?: string;
133
+ completedAt?: string;
134
+ }
135
+
136
+ /** Default prompt detection flags (all false) */
137
+ export function defaultPromptDetected(): PromptDetected {
138
+ return {
139
+ injection: false,
140
+ dlp: false,
141
+ urlCats: false,
142
+ toxicContent: false,
143
+ maliciousCode: false,
144
+ agent: false,
145
+ topicViolation: false,
146
+ };
147
+ }
148
+
149
+ /** Default response detection flags (all false) */
150
+ export function defaultResponseDetected(): ResponseDetected {
151
+ return {
152
+ dlp: false,
153
+ urlCats: false,
154
+ dbSecurity: false,
155
+ toxicContent: false,
156
+ maliciousCode: false,
157
+ agent: false,
158
+ ungrounded: false,
159
+ topicViolation: false,
160
+ };
50
161
  }
51
162
 
52
163
  // AIRS API request/response types (per OpenAPI spec)
53
164
  interface AIRSContentItem {
54
165
  prompt?: string;
55
166
  response?: string;
167
+ tool_calls?: AIRSToolEvent[];
168
+ }
169
+
170
+ interface AIRSToolEvent {
171
+ metadata: {
172
+ ecosystem: string;
173
+ method: string;
174
+ server_name: string;
175
+ tool_invoked?: string;
176
+ };
177
+ input?: string;
178
+ output?: string;
56
179
  }
57
180
 
58
181
  interface AIRSRequest {
@@ -74,11 +197,70 @@ interface AIRSPromptDetected {
74
197
  injection?: boolean;
75
198
  dlp?: boolean;
76
199
  url_cats?: boolean;
200
+ toxic_content?: boolean;
201
+ malicious_code?: boolean;
202
+ agent?: boolean;
203
+ topic_violation?: boolean;
77
204
  }
78
205
 
79
206
  interface AIRSResponseDetected {
80
207
  dlp?: boolean;
81
208
  url_cats?: boolean;
209
+ db_security?: boolean;
210
+ toxic_content?: boolean;
211
+ malicious_code?: boolean;
212
+ agent?: boolean;
213
+ ungrounded?: boolean;
214
+ topic_violation?: boolean;
215
+ }
216
+
217
+ interface AIRSTopicGuardrails {
218
+ allowed_topics?: string[];
219
+ blocked_topics?: string[];
220
+ }
221
+
222
+ interface AIRSDetectionDetails {
223
+ topic_guardrails_details?: AIRSTopicGuardrails;
224
+ }
225
+
226
+ interface AIRSPatternDetection {
227
+ pattern?: string;
228
+ locations?: number[][];
229
+ }
230
+
231
+ interface AIRSMaskedData {
232
+ data?: string;
233
+ pattern_detections?: AIRSPatternDetection[];
234
+ }
235
+
236
+ interface AIRSContentError {
237
+ content_type?: string;
238
+ feature?: string;
239
+ status?: string;
240
+ }
241
+
242
+ interface AIRSToolDetectionFlags {
243
+ injection?: boolean;
244
+ url_cats?: boolean;
245
+ dlp?: boolean;
246
+ db_security?: boolean;
247
+ toxic_content?: boolean;
248
+ malicious_code?: boolean;
249
+ agent?: boolean;
250
+ topic_violation?: boolean;
251
+ }
252
+
253
+ interface AIRSToolDetected {
254
+ verdict?: string;
255
+ metadata?: {
256
+ ecosystem?: string;
257
+ method?: string;
258
+ server_name?: string;
259
+ tool_invoked?: string;
260
+ };
261
+ summary?: string;
262
+ input_detected?: AIRSToolDetectionFlags;
263
+ output_detected?: AIRSToolDetectionFlags;
82
264
  }
83
265
 
84
266
  interface AIRSResponse {
@@ -89,16 +271,27 @@ interface AIRSResponse {
89
271
  action?: string;
90
272
  prompt_detected?: AIRSPromptDetected;
91
273
  response_detected?: AIRSResponseDetected;
274
+ prompt_detection_details?: AIRSDetectionDetails;
275
+ response_detection_details?: AIRSDetectionDetails;
276
+ prompt_masked_data?: AIRSMaskedData;
277
+ response_masked_data?: AIRSMaskedData;
92
278
  tr_id?: string;
279
+ timeout?: boolean;
280
+ error?: boolean;
281
+ errors?: AIRSContentError[];
282
+ tool_detected?: AIRSToolDetected;
283
+ source?: string;
284
+ profile_id?: string;
285
+ created_at?: string;
286
+ completed_at?: string;
93
287
  }
94
288
 
95
289
  /**
96
290
  * Scan content through Prisma AIRS API
97
291
  */
98
292
  export async function scan(request: ScanRequest): Promise<ScanResult> {
99
- const apiKey = process.env.PANW_AI_SEC_API_KEY;
100
- // Profile name: request param > env var > default
101
- const profileName = request.profileName ?? process.env.PANW_AI_SEC_PROFILE_NAME ?? "default";
293
+ const apiKey = request.apiKey;
294
+ const profileName = request.profileName ?? "default";
102
295
 
103
296
  if (!apiKey) {
104
297
  return {
@@ -108,10 +301,13 @@ export async function scan(request: ScanRequest): Promise<ScanResult> {
108
301
  scanId: "",
109
302
  reportId: "",
110
303
  profileName,
111
- promptDetected: { injection: false, dlp: false, urlCats: false },
112
- responseDetected: { dlp: false, urlCats: false },
304
+ promptDetected: defaultPromptDetected(),
305
+ responseDetected: defaultResponseDetected(),
113
306
  latencyMs: 0,
114
- error: "PANW_AI_SEC_API_KEY not set",
307
+ timeout: false,
308
+ hasError: false,
309
+ contentErrors: [],
310
+ error: "API key not configured. Set it in plugin config.",
115
311
  };
116
312
  }
117
313
 
@@ -122,6 +318,20 @@ export async function scan(request: ScanRequest): Promise<ScanResult> {
122
318
  if (request.prompt) contentItem.prompt = request.prompt;
123
319
  if (request.response) contentItem.response = request.response;
124
320
 
321
+ // Map tool events into contents
322
+ if (request.toolEvents && request.toolEvents.length > 0) {
323
+ contentItem.tool_calls = request.toolEvents.map((te) => ({
324
+ metadata: {
325
+ ecosystem: te.metadata.ecosystem,
326
+ method: te.metadata.method,
327
+ server_name: te.metadata.serverName,
328
+ ...(te.metadata.toolInvoked ? { tool_invoked: te.metadata.toolInvoked } : {}),
329
+ },
330
+ ...(te.input ? { input: te.input } : {}),
331
+ ...(te.output ? { output: te.output } : {}),
332
+ }));
333
+ }
334
+
125
335
  // Build request body (per OpenAPI spec)
126
336
  const body: AIRSRequest = {
127
337
  ai_profile: {
@@ -164,9 +374,12 @@ export async function scan(request: ScanRequest): Promise<ScanResult> {
164
374
  scanId: "",
165
375
  reportId: "",
166
376
  profileName,
167
- promptDetected: { injection: false, dlp: false, urlCats: false },
168
- responseDetected: { dlp: false, urlCats: false },
377
+ promptDetected: defaultPromptDetected(),
378
+ responseDetected: defaultResponseDetected(),
169
379
  latencyMs,
380
+ timeout: false,
381
+ hasError: true,
382
+ contentErrors: [],
170
383
  error: `API error ${resp.status}: ${errorText}`,
171
384
  };
172
385
  }
@@ -182,9 +395,12 @@ export async function scan(request: ScanRequest): Promise<ScanResult> {
182
395
  scanId: "",
183
396
  reportId: "",
184
397
  profileName,
185
- promptDetected: { injection: false, dlp: false, urlCats: false },
186
- responseDetected: { dlp: false, urlCats: false },
398
+ promptDetected: defaultPromptDetected(),
399
+ responseDetected: defaultResponseDetected(),
187
400
  latencyMs,
401
+ timeout: false,
402
+ hasError: true,
403
+ contentErrors: [],
188
404
  error: err instanceof Error ? err.message : String(err),
189
405
  };
190
406
  }
@@ -210,38 +426,56 @@ function parseResponse(
210
426
  injection: data.prompt_detected?.injection ?? false,
211
427
  dlp: data.prompt_detected?.dlp ?? false,
212
428
  urlCats: data.prompt_detected?.url_cats ?? false,
429
+ toxicContent: data.prompt_detected?.toxic_content ?? false,
430
+ maliciousCode: data.prompt_detected?.malicious_code ?? false,
431
+ agent: data.prompt_detected?.agent ?? false,
432
+ topicViolation: data.prompt_detected?.topic_violation ?? false,
213
433
  };
214
434
 
215
435
  const responseDetected: ResponseDetected = {
216
436
  dlp: data.response_detected?.dlp ?? false,
217
437
  urlCats: data.response_detected?.url_cats ?? false,
438
+ dbSecurity: data.response_detected?.db_security ?? false,
439
+ toxicContent: data.response_detected?.toxic_content ?? false,
440
+ maliciousCode: data.response_detected?.malicious_code ?? false,
441
+ agent: data.response_detected?.agent ?? false,
442
+ ungrounded: data.response_detected?.ungrounded ?? false,
443
+ topicViolation: data.response_detected?.topic_violation ?? false,
218
444
  };
219
445
 
220
446
  // Build categories list
221
447
  const categories: string[] = [];
448
+ // Prompt detections
222
449
  if (promptDetected.injection) categories.push("prompt_injection");
223
450
  if (promptDetected.dlp) categories.push("dlp_prompt");
224
451
  if (promptDetected.urlCats) categories.push("url_filtering_prompt");
452
+ if (promptDetected.toxicContent) categories.push("toxic_content_prompt");
453
+ if (promptDetected.maliciousCode) categories.push("malicious_code_prompt");
454
+ if (promptDetected.agent) categories.push("agent_threat_prompt");
455
+ if (promptDetected.topicViolation) categories.push("topic_violation_prompt");
456
+ // Response detections
225
457
  if (responseDetected.dlp) categories.push("dlp_response");
226
458
  if (responseDetected.urlCats) categories.push("url_filtering_response");
459
+ if (responseDetected.dbSecurity) categories.push("db_security_response");
460
+ if (responseDetected.toxicContent) categories.push("toxic_content_response");
461
+ if (responseDetected.maliciousCode) categories.push("malicious_code_response");
462
+ if (responseDetected.agent) categories.push("agent_threat_response");
463
+ if (responseDetected.ungrounded) categories.push("ungrounded_response");
464
+ if (responseDetected.topicViolation) categories.push("topic_violation_response");
227
465
 
228
466
  if (categories.length === 0) {
229
467
  categories.push(category === "benign" ? "safe" : category);
230
468
  }
231
469
 
232
470
  // Determine severity
471
+ const anyDetected =
472
+ Object.values(promptDetected).some(Boolean) || Object.values(responseDetected).some(Boolean);
233
473
  let severity: Severity;
234
474
  if (category === "malicious" || actionStr === "block") {
235
475
  severity = "CRITICAL";
236
476
  } else if (category === "suspicious") {
237
477
  severity = "HIGH";
238
- } else if (
239
- promptDetected.injection ||
240
- promptDetected.dlp ||
241
- promptDetected.urlCats ||
242
- responseDetected.dlp ||
243
- responseDetected.urlCats
244
- ) {
478
+ } else if (anyDetected) {
245
479
  severity = "MEDIUM";
246
480
  } else {
247
481
  severity = "SAFE";
@@ -257,7 +491,28 @@ function parseResponse(
257
491
  action = "allow";
258
492
  }
259
493
 
260
- return {
494
+ // Extract detection details (optional)
495
+ const promptDetectionDetails = parseDetectionDetails(data.prompt_detection_details);
496
+ const responseDetectionDetails = parseDetectionDetails(data.response_detection_details);
497
+
498
+ // Extract masked data (optional)
499
+ const promptMaskedData = parseMaskedData(data.prompt_masked_data);
500
+ const responseMaskedData = parseMaskedData(data.response_masked_data);
501
+
502
+ // Extract timeout/error info
503
+ const isTimeout = data.timeout === true;
504
+ const hasError = data.error === true;
505
+ const contentErrors: ContentError[] = (data.errors ?? []).map((e) => ({
506
+ contentType: (e.content_type === "prompt" ? "prompt" : "response") as ContentErrorType,
507
+ feature: e.feature ?? "",
508
+ status: (e.status === "timeout" ? "timeout" : "error") as ErrorStatus,
509
+ }));
510
+
511
+ if (isTimeout && !categories.includes("partial_scan")) {
512
+ categories.push("partial_scan");
513
+ }
514
+
515
+ const result: ScanResult = {
261
516
  action,
262
517
  severity,
263
518
  categories,
@@ -269,12 +524,88 @@ function parseResponse(
269
524
  sessionId: request.sessionId,
270
525
  trId: data.tr_id ?? request.trId,
271
526
  latencyMs,
527
+ timeout: isTimeout,
528
+ hasError,
529
+ contentErrors,
530
+ };
531
+
532
+ if (promptDetectionDetails) result.promptDetectionDetails = promptDetectionDetails;
533
+ if (responseDetectionDetails) result.responseDetectionDetails = responseDetectionDetails;
534
+ if (promptMaskedData) result.promptMaskedData = promptMaskedData;
535
+ if (responseMaskedData) result.responseMaskedData = responseMaskedData;
536
+
537
+ // Extract tool detection (optional)
538
+ const toolDetected = parseToolDetected(data.tool_detected);
539
+ if (toolDetected) result.toolDetected = toolDetected;
540
+
541
+ // Extract timestamps and metadata (optional)
542
+ if (data.source) result.source = data.source;
543
+ if (data.profile_id) result.profileId = data.profile_id;
544
+ if (data.created_at) result.createdAt = data.created_at;
545
+ if (data.completed_at) result.completedAt = data.completed_at;
546
+
547
+ return result;
548
+ }
549
+
550
+ function parseDetectionDetails(raw?: AIRSDetectionDetails): DetectionDetails | undefined {
551
+ if (!raw) return undefined;
552
+ const details: DetectionDetails = {};
553
+ if (raw.topic_guardrails_details) {
554
+ details.topicGuardrailsDetails = {
555
+ allowedTopics: raw.topic_guardrails_details.allowed_topics ?? [],
556
+ blockedTopics: raw.topic_guardrails_details.blocked_topics ?? [],
557
+ };
558
+ }
559
+ return Object.keys(details).length > 0 ? details : undefined;
560
+ }
561
+
562
+ function parseMaskedData(raw?: AIRSMaskedData): MaskedData | undefined {
563
+ if (!raw) return undefined;
564
+ return {
565
+ data: raw.data,
566
+ patternDetections: (raw.pattern_detections ?? []).map((p) => ({
567
+ pattern: p.pattern ?? "",
568
+ locations: p.locations ?? [],
569
+ })),
570
+ };
571
+ }
572
+
573
+ function parseToolDetectionFlags(raw?: AIRSToolDetectionFlags): ToolDetectionFlags | undefined {
574
+ if (!raw) return undefined;
575
+ const flags: ToolDetectionFlags = {};
576
+ if (raw.injection != null) flags.injection = raw.injection;
577
+ if (raw.url_cats != null) flags.urlCats = raw.url_cats;
578
+ if (raw.dlp != null) flags.dlp = raw.dlp;
579
+ if (raw.db_security != null) flags.dbSecurity = raw.db_security;
580
+ if (raw.toxic_content != null) flags.toxicContent = raw.toxic_content;
581
+ if (raw.malicious_code != null) flags.maliciousCode = raw.malicious_code;
582
+ if (raw.agent != null) flags.agent = raw.agent;
583
+ if (raw.topic_violation != null) flags.topicViolation = raw.topic_violation;
584
+ return Object.keys(flags).length > 0 ? flags : undefined;
585
+ }
586
+
587
+ function parseToolDetected(raw?: AIRSToolDetected): ToolDetected | undefined {
588
+ if (!raw || !raw.metadata) return undefined;
589
+ const result: ToolDetected = {
590
+ verdict: raw.verdict ?? "",
591
+ metadata: {
592
+ ecosystem: raw.metadata.ecosystem ?? "",
593
+ method: raw.metadata.method ?? "",
594
+ serverName: raw.metadata.server_name ?? "",
595
+ toolInvoked: raw.metadata.tool_invoked,
596
+ },
597
+ summary: raw.summary ?? "",
272
598
  };
599
+ const inputDetected = parseToolDetectionFlags(raw.input_detected);
600
+ const outputDetected = parseToolDetectionFlags(raw.output_detected);
601
+ if (inputDetected) result.inputDetected = inputDetected;
602
+ if (outputDetected) result.outputDetected = outputDetected;
603
+ return result;
273
604
  }
274
605
 
275
606
  /**
276
607
  * Check if API key is configured
277
608
  */
278
- export function isConfigured(): boolean {
279
- return !!process.env.PANW_AI_SEC_API_KEY;
609
+ export function isConfigured(apiKey?: string): boolean {
610
+ return !!apiKey;
280
611
  }