@cdot65/prisma-airs 0.2.2 → 0.2.3

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,56 @@ export interface ScanRequest {
21
53
  appName?: string;
22
54
  appUser?: string;
23
55
  aiModel?: string;
56
+ toolEvents?: ToolEventInput[];
24
57
  }
25
58
 
26
59
  export interface PromptDetected {
27
60
  injection: boolean;
28
61
  dlp: boolean;
29
62
  urlCats: boolean;
63
+ toxicContent: boolean;
64
+ maliciousCode: boolean;
65
+ agent: boolean;
66
+ topicViolation: boolean;
30
67
  }
31
68
 
32
69
  export interface ResponseDetected {
33
70
  dlp: boolean;
34
71
  urlCats: boolean;
72
+ dbSecurity: boolean;
73
+ toxicContent: boolean;
74
+ maliciousCode: boolean;
75
+ agent: boolean;
76
+ ungrounded: boolean;
77
+ topicViolation: boolean;
78
+ }
79
+
80
+ export interface TopicGuardrails {
81
+ allowedTopics: string[];
82
+ blockedTopics: string[];
83
+ }
84
+
85
+ export interface DetectionDetails {
86
+ topicGuardrailsDetails?: TopicGuardrails;
87
+ }
88
+
89
+ export interface PatternDetection {
90
+ pattern: string;
91
+ locations: number[][];
92
+ }
93
+
94
+ export interface MaskedData {
95
+ data?: string;
96
+ patternDetections: PatternDetection[];
97
+ }
98
+
99
+ export type ContentErrorType = "prompt" | "response";
100
+ export type ErrorStatus = "error" | "timeout";
101
+
102
+ export interface ContentError {
103
+ contentType: ContentErrorType;
104
+ feature: string;
105
+ status: ErrorStatus;
35
106
  }
36
107
 
37
108
  export interface ScanResult {
@@ -47,12 +118,63 @@ export interface ScanResult {
47
118
  trId?: string;
48
119
  latencyMs: number;
49
120
  error?: string;
121
+ promptDetectionDetails?: DetectionDetails;
122
+ responseDetectionDetails?: DetectionDetails;
123
+ promptMaskedData?: MaskedData;
124
+ responseMaskedData?: MaskedData;
125
+ timeout: boolean;
126
+ hasError: boolean;
127
+ contentErrors: ContentError[];
128
+ toolDetected?: ToolDetected;
129
+ source?: string;
130
+ profileId?: string;
131
+ createdAt?: string;
132
+ completedAt?: string;
133
+ }
134
+
135
+ /** Default prompt detection flags (all false) */
136
+ export function defaultPromptDetected(): PromptDetected {
137
+ return {
138
+ injection: false,
139
+ dlp: false,
140
+ urlCats: false,
141
+ toxicContent: false,
142
+ maliciousCode: false,
143
+ agent: false,
144
+ topicViolation: false,
145
+ };
146
+ }
147
+
148
+ /** Default response detection flags (all false) */
149
+ export function defaultResponseDetected(): ResponseDetected {
150
+ return {
151
+ dlp: false,
152
+ urlCats: false,
153
+ dbSecurity: false,
154
+ toxicContent: false,
155
+ maliciousCode: false,
156
+ agent: false,
157
+ ungrounded: false,
158
+ topicViolation: false,
159
+ };
50
160
  }
51
161
 
52
162
  // AIRS API request/response types (per OpenAPI spec)
53
163
  interface AIRSContentItem {
54
164
  prompt?: string;
55
165
  response?: string;
166
+ tool_calls?: AIRSToolEvent[];
167
+ }
168
+
169
+ interface AIRSToolEvent {
170
+ metadata: {
171
+ ecosystem: string;
172
+ method: string;
173
+ server_name: string;
174
+ tool_invoked?: string;
175
+ };
176
+ input?: string;
177
+ output?: string;
56
178
  }
57
179
 
58
180
  interface AIRSRequest {
@@ -74,11 +196,70 @@ interface AIRSPromptDetected {
74
196
  injection?: boolean;
75
197
  dlp?: boolean;
76
198
  url_cats?: boolean;
199
+ toxic_content?: boolean;
200
+ malicious_code?: boolean;
201
+ agent?: boolean;
202
+ topic_violation?: boolean;
77
203
  }
78
204
 
79
205
  interface AIRSResponseDetected {
80
206
  dlp?: boolean;
81
207
  url_cats?: boolean;
208
+ db_security?: boolean;
209
+ toxic_content?: boolean;
210
+ malicious_code?: boolean;
211
+ agent?: boolean;
212
+ ungrounded?: boolean;
213
+ topic_violation?: boolean;
214
+ }
215
+
216
+ interface AIRSTopicGuardrails {
217
+ allowed_topics?: string[];
218
+ blocked_topics?: string[];
219
+ }
220
+
221
+ interface AIRSDetectionDetails {
222
+ topic_guardrails_details?: AIRSTopicGuardrails;
223
+ }
224
+
225
+ interface AIRSPatternDetection {
226
+ pattern?: string;
227
+ locations?: number[][];
228
+ }
229
+
230
+ interface AIRSMaskedData {
231
+ data?: string;
232
+ pattern_detections?: AIRSPatternDetection[];
233
+ }
234
+
235
+ interface AIRSContentError {
236
+ content_type?: string;
237
+ feature?: string;
238
+ status?: string;
239
+ }
240
+
241
+ interface AIRSToolDetectionFlags {
242
+ injection?: boolean;
243
+ url_cats?: boolean;
244
+ dlp?: boolean;
245
+ db_security?: boolean;
246
+ toxic_content?: boolean;
247
+ malicious_code?: boolean;
248
+ agent?: boolean;
249
+ topic_violation?: boolean;
250
+ }
251
+
252
+ interface AIRSToolDetected {
253
+ verdict?: string;
254
+ metadata?: {
255
+ ecosystem?: string;
256
+ method?: string;
257
+ server_name?: string;
258
+ tool_invoked?: string;
259
+ };
260
+ summary?: string;
261
+ input_detected?: AIRSToolDetectionFlags;
262
+ output_detected?: AIRSToolDetectionFlags;
82
263
  }
83
264
 
84
265
  interface AIRSResponse {
@@ -89,7 +270,19 @@ interface AIRSResponse {
89
270
  action?: string;
90
271
  prompt_detected?: AIRSPromptDetected;
91
272
  response_detected?: AIRSResponseDetected;
273
+ prompt_detection_details?: AIRSDetectionDetails;
274
+ response_detection_details?: AIRSDetectionDetails;
275
+ prompt_masked_data?: AIRSMaskedData;
276
+ response_masked_data?: AIRSMaskedData;
92
277
  tr_id?: string;
278
+ timeout?: boolean;
279
+ error?: boolean;
280
+ errors?: AIRSContentError[];
281
+ tool_detected?: AIRSToolDetected;
282
+ source?: string;
283
+ profile_id?: string;
284
+ created_at?: string;
285
+ completed_at?: string;
93
286
  }
94
287
 
95
288
  /**
@@ -108,9 +301,12 @@ 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,
307
+ timeout: false,
308
+ hasError: false,
309
+ contentErrors: [],
114
310
  error: "PANW_AI_SEC_API_KEY not set",
115
311
  };
116
312
  }
@@ -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,7 +524,83 @@ 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
  /**