@cdot65/prisma-airs-cli 3.1.0 → 3.3.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/README.md +13 -2
- package/dist/{chunk-DSNQSBLE.js → chunk-2VIUZRPB.js} +498 -40
- package/dist/cli/index.js +1665 -352
- package/dist/index.d.ts +170 -17
- package/dist/index.js +3 -1
- package/package.json +2 -2
|
@@ -1012,6 +1012,52 @@ function normalizeTargetDetail(raw) {
|
|
|
1012
1012
|
metadata: sanitizeTargetMetadata(raw.target_metadata)
|
|
1013
1013
|
};
|
|
1014
1014
|
}
|
|
1015
|
+
function normalizeAdapterListItem(raw) {
|
|
1016
|
+
return {
|
|
1017
|
+
uuid: raw.uuid,
|
|
1018
|
+
name: raw.name,
|
|
1019
|
+
status: raw.status,
|
|
1020
|
+
createdAt: raw.created_at,
|
|
1021
|
+
updatedAt: raw.updated_at,
|
|
1022
|
+
createdByUserId: raw.created_by_user_id,
|
|
1023
|
+
targetCount: raw.target_count
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function normalizeAdapterVar(raw) {
|
|
1027
|
+
return {
|
|
1028
|
+
key: raw.key,
|
|
1029
|
+
value: raw.value,
|
|
1030
|
+
type: raw.type,
|
|
1031
|
+
isRedacted: raw.is_redacted
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function normalizeAdapterDetail(raw) {
|
|
1035
|
+
return {
|
|
1036
|
+
uuid: raw.uuid,
|
|
1037
|
+
tsgId: raw.tsg_id,
|
|
1038
|
+
name: raw.name,
|
|
1039
|
+
scriptB64: raw.script_b64,
|
|
1040
|
+
status: raw.status,
|
|
1041
|
+
description: raw.description,
|
|
1042
|
+
networkBrokerChannelUuid: raw.network_broker_channel_uuid,
|
|
1043
|
+
variables: (raw.variables ?? []).map(normalizeAdapterVar),
|
|
1044
|
+
targetCount: raw.target_count,
|
|
1045
|
+
createdAt: raw.created_at,
|
|
1046
|
+
updatedAt: raw.updated_at,
|
|
1047
|
+
createdByUserId: raw.created_by_user_id,
|
|
1048
|
+
updatedByUserId: raw.updated_by_user_id
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function preserveVariablesForUpdate(variables) {
|
|
1052
|
+
return variables.map((v) => ({
|
|
1053
|
+
key: v.key,
|
|
1054
|
+
value: v.isRedacted ? null : v.value ?? null,
|
|
1055
|
+
type: v.type
|
|
1056
|
+
}));
|
|
1057
|
+
}
|
|
1058
|
+
function toWireVariables(variables) {
|
|
1059
|
+
return variables.map((v) => ({ key: v.key, value: v.value, type: v.type }));
|
|
1060
|
+
}
|
|
1015
1061
|
var SdkRedTeamService = class {
|
|
1016
1062
|
client;
|
|
1017
1063
|
constructor(opts) {
|
|
@@ -1180,7 +1226,12 @@ var SdkRedTeamService = class {
|
|
|
1180
1226
|
}
|
|
1181
1227
|
async createScan(request) {
|
|
1182
1228
|
let jobMetadata = {};
|
|
1183
|
-
if (request.jobType === "STATIC"
|
|
1229
|
+
if (request.jobType === "STATIC") {
|
|
1230
|
+
if (!request.categories) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
"STATIC scans require categories. Pass categories explicitly or use the CLI default."
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1184
1235
|
jobMetadata = { categories: request.categories };
|
|
1185
1236
|
} else if (request.jobType === "CUSTOM" && request.customPromptSets) {
|
|
1186
1237
|
jobMetadata = {
|
|
@@ -1408,21 +1459,224 @@ var SdkRedTeamService = class {
|
|
|
1408
1459
|
totalItems: pagination?.total_items
|
|
1409
1460
|
};
|
|
1410
1461
|
}
|
|
1462
|
+
// -------------------------------------------------------------------------
|
|
1463
|
+
// Custom target adapters (SDK 0.16.0)
|
|
1464
|
+
// -------------------------------------------------------------------------
|
|
1465
|
+
async listAdapters(opts) {
|
|
1466
|
+
const sdkOpts = {};
|
|
1467
|
+
if (opts?.limit != null) sdkOpts.limit = opts.limit;
|
|
1468
|
+
if (opts?.offset != null) sdkOpts.skip = opts.offset;
|
|
1469
|
+
if (opts?.search) sdkOpts.search = opts.search;
|
|
1470
|
+
const raw = await this.client.adapters.list(sdkOpts);
|
|
1471
|
+
const pagination = raw.pagination;
|
|
1472
|
+
return {
|
|
1473
|
+
adapters: (raw.data ?? []).map(normalizeAdapterListItem),
|
|
1474
|
+
totalItems: pagination?.total_items
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
async getAdapter(uuid) {
|
|
1478
|
+
const raw = await this.client.adapters.get(uuid);
|
|
1479
|
+
return normalizeAdapterDetail(raw);
|
|
1480
|
+
}
|
|
1481
|
+
async createAdapter(request, validate) {
|
|
1482
|
+
const body = {
|
|
1483
|
+
name: request.name,
|
|
1484
|
+
script_b64: request.scriptB64,
|
|
1485
|
+
prompt: request.prompt
|
|
1486
|
+
};
|
|
1487
|
+
if (request.description !== void 0) body.description = request.description;
|
|
1488
|
+
if (request.networkBrokerChannelUuid !== void 0) {
|
|
1489
|
+
body.network_broker_channel_uuid = request.networkBrokerChannelUuid;
|
|
1490
|
+
}
|
|
1491
|
+
if (request.variables !== void 0) body.variables = toWireVariables(request.variables);
|
|
1492
|
+
const raw = await this.client.adapters.create(
|
|
1493
|
+
// biome-ignore lint/suspicious/noExplicitAny: body is assembled dynamically; SDK validates the shape
|
|
1494
|
+
body,
|
|
1495
|
+
validate === void 0 ? void 0 : { validate }
|
|
1496
|
+
);
|
|
1497
|
+
return normalizeAdapterDetail(raw);
|
|
1498
|
+
}
|
|
1499
|
+
async updateAdapter(uuid, overrides, validate) {
|
|
1500
|
+
const current = await this.getAdapter(uuid);
|
|
1501
|
+
const body = {
|
|
1502
|
+
name: overrides.name ?? current.name,
|
|
1503
|
+
script_b64: overrides.scriptB64 ?? current.scriptB64,
|
|
1504
|
+
prompt: overrides.prompt,
|
|
1505
|
+
variables: overrides.variables ? toWireVariables(overrides.variables) : preserveVariablesForUpdate(current.variables)
|
|
1506
|
+
};
|
|
1507
|
+
const description = overrides.description ?? current.description;
|
|
1508
|
+
if (description != null) body.description = description;
|
|
1509
|
+
const channel = overrides.networkBrokerChannelUuid ?? current.networkBrokerChannelUuid;
|
|
1510
|
+
if (channel != null) body.network_broker_channel_uuid = channel;
|
|
1511
|
+
const raw = await this.client.adapters.update(
|
|
1512
|
+
uuid,
|
|
1513
|
+
// biome-ignore lint/suspicious/noExplicitAny: body is assembled dynamically; SDK validates the shape
|
|
1514
|
+
body,
|
|
1515
|
+
validate === void 0 ? void 0 : { validate }
|
|
1516
|
+
);
|
|
1517
|
+
return normalizeAdapterDetail(raw);
|
|
1518
|
+
}
|
|
1519
|
+
async deleteAdapter(uuid) {
|
|
1520
|
+
await this.client.adapters.delete(uuid);
|
|
1521
|
+
}
|
|
1522
|
+
async validateAdapter(request) {
|
|
1523
|
+
let variables = request.variables ? toWireVariables(request.variables) : void 0;
|
|
1524
|
+
if (variables === void 0 && request.adapterUuid) {
|
|
1525
|
+
const adapter = await this.getAdapter(request.adapterUuid);
|
|
1526
|
+
variables = preserveVariablesForUpdate(adapter.variables);
|
|
1527
|
+
}
|
|
1528
|
+
const body = {
|
|
1529
|
+
script_b64: request.scriptB64,
|
|
1530
|
+
network_broker_channel_uuid: request.networkBrokerChannelUuid,
|
|
1531
|
+
prompt: request.prompt
|
|
1532
|
+
};
|
|
1533
|
+
if (variables !== void 0) body.variables = variables;
|
|
1534
|
+
if (request.adapterUuid !== void 0) body.adapter_uuid = request.adapterUuid;
|
|
1535
|
+
const raw = await this.client.adapters.validate(body);
|
|
1536
|
+
return {
|
|
1537
|
+
validated: raw.validated,
|
|
1538
|
+
stdout: raw.stdout,
|
|
1539
|
+
stderr: raw.stderr,
|
|
1540
|
+
traceback: raw.traceback
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1411
1543
|
};
|
|
1412
1544
|
|
|
1413
1545
|
// src/airs/runtime.ts
|
|
1414
|
-
import {
|
|
1415
|
-
|
|
1546
|
+
import {
|
|
1547
|
+
Content,
|
|
1548
|
+
init,
|
|
1549
|
+
MAX_NUMBER_OF_BATCH_SCAN_OBJECTS,
|
|
1550
|
+
Scanner
|
|
1551
|
+
} from "@cdot65/prisma-airs-sdk";
|
|
1552
|
+
var SDK_ASYNC_BATCH_SIZE = MAX_NUMBER_OF_BATCH_SCAN_OBJECTS;
|
|
1416
1553
|
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
1417
1554
|
var DEFAULT_MAX_RETRIES = 5;
|
|
1418
1555
|
var DEFAULT_BASE_DELAY_MS = 1e4;
|
|
1556
|
+
var DEFAULT_MAX_NO_PROGRESS_POLLS = 120;
|
|
1557
|
+
var NO_SDK_RETRIES = { numRetries: 0 };
|
|
1558
|
+
var RUNTIME_DETECTION_KEYS = [
|
|
1559
|
+
"topic_violation",
|
|
1560
|
+
"injection",
|
|
1561
|
+
"toxic_content",
|
|
1562
|
+
"dlp",
|
|
1563
|
+
"url_cats",
|
|
1564
|
+
"malicious_code",
|
|
1565
|
+
"source_code",
|
|
1566
|
+
"agent"
|
|
1567
|
+
];
|
|
1568
|
+
var REPORT_DETECTION_KEYS = {
|
|
1569
|
+
topic_guardrails: "topic_violation",
|
|
1570
|
+
topic_violation: "topic_violation",
|
|
1571
|
+
pi: "injection",
|
|
1572
|
+
prompt_injection: "injection",
|
|
1573
|
+
injection: "injection",
|
|
1574
|
+
tc: "toxic_content",
|
|
1575
|
+
toxic_content: "toxic_content",
|
|
1576
|
+
dlp: "dlp",
|
|
1577
|
+
uf: "url_cats",
|
|
1578
|
+
url_filtering: "url_cats",
|
|
1579
|
+
url_cats: "url_cats",
|
|
1580
|
+
mc: "malicious_code",
|
|
1581
|
+
malicious_code: "malicious_code",
|
|
1582
|
+
source_code: "source_code",
|
|
1583
|
+
agent: "agent"
|
|
1584
|
+
};
|
|
1419
1585
|
function isRateLimitError(err) {
|
|
1586
|
+
if (err?.statusCode === 429) return true;
|
|
1420
1587
|
if (err instanceof Error) {
|
|
1421
1588
|
const msg = err.message.toLowerCase();
|
|
1422
1589
|
return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("429");
|
|
1423
1590
|
}
|
|
1424
1591
|
return false;
|
|
1425
1592
|
}
|
|
1593
|
+
function isDefiniteRateLimitError(err) {
|
|
1594
|
+
const metadata = err;
|
|
1595
|
+
return metadata?.failureKind === "http" && metadata.statusCode === 429;
|
|
1596
|
+
}
|
|
1597
|
+
function runtimeDetections(value) {
|
|
1598
|
+
const source = value ?? {};
|
|
1599
|
+
return Object.fromEntries(
|
|
1600
|
+
RUNTIME_DETECTION_KEYS.filter((key) => typeof source[key] === "boolean").map((key) => [
|
|
1601
|
+
key,
|
|
1602
|
+
source[key]
|
|
1603
|
+
])
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
function runtimeAction(value) {
|
|
1607
|
+
const normalized = typeof value === "string" ? value.toLowerCase() : "";
|
|
1608
|
+
if (normalized === "allow" || normalized === "block") return normalized;
|
|
1609
|
+
return "failed";
|
|
1610
|
+
}
|
|
1611
|
+
function scanResponseToResult(response, prompt) {
|
|
1612
|
+
const detections = runtimeDetections(response.prompt_detected);
|
|
1613
|
+
const action = runtimeAction(response.action);
|
|
1614
|
+
const failed = response.error === true || response.timeout === true || action === "failed";
|
|
1615
|
+
const errors = Array.isArray(response.errors) ? response.errors.map((entry) => {
|
|
1616
|
+
const detail = entry;
|
|
1617
|
+
return [detail.feature, detail.status, detail.content_type].filter(Boolean).join(": ");
|
|
1618
|
+
}) : [];
|
|
1619
|
+
return {
|
|
1620
|
+
prompt,
|
|
1621
|
+
response: void 0,
|
|
1622
|
+
scanId: response.scan_id ?? "",
|
|
1623
|
+
reportId: response.report_id ?? "",
|
|
1624
|
+
action: failed ? "failed" : action,
|
|
1625
|
+
category: failed ? "error" : response.category ?? "unknown",
|
|
1626
|
+
triggered: RUNTIME_DETECTION_KEYS.some((key) => detections[key] === true),
|
|
1627
|
+
detections,
|
|
1628
|
+
...failed ? {
|
|
1629
|
+
error: errors.filter(Boolean).join("; ") || (response.timeout === true ? "AIRS scan timed out" : response.error === true ? "AIRS scan failed" : `Unknown AIRS action: ${String(response.action ?? "missing")}`)
|
|
1630
|
+
} : {}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function threatReportToResult(report, entry) {
|
|
1634
|
+
const detections = {};
|
|
1635
|
+
let sawBlock = false;
|
|
1636
|
+
let unexpectedAction;
|
|
1637
|
+
const detectionResults = Array.isArray(report.detection_results) ? report.detection_results : [];
|
|
1638
|
+
for (const detection of detectionResults) {
|
|
1639
|
+
const service = String(detection.detection_service ?? "").toLowerCase();
|
|
1640
|
+
const detectorAction = String(detection.action ?? "").toLowerCase();
|
|
1641
|
+
const verdict = String(detection.verdict ?? "").toLowerCase();
|
|
1642
|
+
const fired = detectorAction === "block" || ["malicious", "unsafe", "violation", "detected"].includes(verdict);
|
|
1643
|
+
const namedKey = REPORT_DETECTION_KEYS[service] ?? service;
|
|
1644
|
+
const key = namedKey || (fired ? "unknown" : "");
|
|
1645
|
+
if (key) detections[key] = detections[key] === true || fired;
|
|
1646
|
+
if (detectorAction === "block") sawBlock = true;
|
|
1647
|
+
else if (detectorAction && detectorAction !== "allow") unexpectedAction = detectorAction;
|
|
1648
|
+
}
|
|
1649
|
+
const triggered = Object.values(detections).some(Boolean);
|
|
1650
|
+
const action = sawBlock ? "block" : unexpectedAction ? "failed" : "allow";
|
|
1651
|
+
return {
|
|
1652
|
+
index: entry.index,
|
|
1653
|
+
reqId: entry.reqId,
|
|
1654
|
+
prompt: entry.prompt,
|
|
1655
|
+
response: void 0,
|
|
1656
|
+
scanId: entry.scanId,
|
|
1657
|
+
reportId: report.report_id ?? "",
|
|
1658
|
+
action,
|
|
1659
|
+
category: action === "failed" ? "error" : triggered ? "malicious" : "benign",
|
|
1660
|
+
triggered,
|
|
1661
|
+
detections,
|
|
1662
|
+
...action === "failed" ? { error: `Unknown AIRS action in threat report: ${unexpectedAction}` } : {}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function failedBulkResult(entry, error = "AIRS async scan failed") {
|
|
1666
|
+
return {
|
|
1667
|
+
index: entry.index,
|
|
1668
|
+
reqId: entry.reqId,
|
|
1669
|
+
prompt: entry.prompt,
|
|
1670
|
+
response: void 0,
|
|
1671
|
+
scanId: entry.scanId,
|
|
1672
|
+
reportId: "",
|
|
1673
|
+
action: "failed",
|
|
1674
|
+
category: "error",
|
|
1675
|
+
triggered: false,
|
|
1676
|
+
detections: {},
|
|
1677
|
+
error
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1426
1680
|
var SdkRuntimeService = class {
|
|
1427
1681
|
scanner;
|
|
1428
1682
|
constructor(opts) {
|
|
@@ -1434,23 +1688,208 @@ var SdkRuntimeService = class {
|
|
|
1434
1688
|
if (response) contentOpts.response = response;
|
|
1435
1689
|
const content = new Content(contentOpts);
|
|
1436
1690
|
const res = await this.scanner.syncScan({ profile_name: profileName }, content, void 0);
|
|
1437
|
-
const
|
|
1438
|
-
const triggered = !!(detected.topic_violation || detected.injection || detected.toxic_content || detected.dlp || detected.url_cats || detected.malicious_code);
|
|
1691
|
+
const normalized = scanResponseToResult(res, prompt);
|
|
1439
1692
|
return {
|
|
1440
|
-
|
|
1693
|
+
...normalized,
|
|
1441
1694
|
response,
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1695
|
+
action: normalized.action === "block" ? "block" : "allow"
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
async submitBatch(profileName, prompts, sessionId, retryOpts) {
|
|
1699
|
+
if (prompts.length < 1 || prompts.length > SDK_ASYNC_BATCH_SIZE) {
|
|
1700
|
+
throw new Error(`submitBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} prompts`);
|
|
1701
|
+
}
|
|
1702
|
+
const promptIndices = /* @__PURE__ */ new Set();
|
|
1703
|
+
for (const prompt of prompts) {
|
|
1704
|
+
if (!Number.isSafeInteger(prompt.index) || prompt.index < 0 || promptIndices.has(prompt.index)) {
|
|
1705
|
+
throw new Error("submitBatch requires a unique nonnegative safe index for every prompt");
|
|
1706
|
+
}
|
|
1707
|
+
promptIndices.add(prompt.index);
|
|
1708
|
+
}
|
|
1709
|
+
const scanObjects = prompts.map(({ index, prompt }) => ({
|
|
1710
|
+
req_id: index,
|
|
1711
|
+
scan_req: {
|
|
1712
|
+
ai_profile: { profile_name: profileName },
|
|
1713
|
+
contents: [{ prompt }],
|
|
1714
|
+
...sessionId ? { session_id: sessionId } : {}
|
|
1715
|
+
}
|
|
1716
|
+
}));
|
|
1717
|
+
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1718
|
+
const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1719
|
+
let retryAttempt = 0;
|
|
1720
|
+
let receipt;
|
|
1721
|
+
while (true) {
|
|
1722
|
+
try {
|
|
1723
|
+
receipt = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
|
|
1724
|
+
break;
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
if (!isDefiniteRateLimitError(error) || retryAttempt >= maxRetries) throw error;
|
|
1727
|
+
retryAttempt++;
|
|
1728
|
+
const retryAfterMs = error.retryAfterMs;
|
|
1729
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryAttempt - 1);
|
|
1730
|
+
retryOpts?.onRetry?.(retryAttempt, delayMs);
|
|
1731
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
scanId: receipt.scan_id,
|
|
1736
|
+
reportId: receipt.report_id,
|
|
1737
|
+
entries: prompts.map(({ index, prompt }) => ({
|
|
1738
|
+
scanId: receipt.scan_id,
|
|
1739
|
+
reqId: index,
|
|
1740
|
+
index,
|
|
1741
|
+
prompt
|
|
1742
|
+
}))
|
|
1448
1743
|
};
|
|
1449
1744
|
}
|
|
1745
|
+
async pollBatch(batch, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
|
|
1746
|
+
if (batch.entries.length < 1 || batch.entries.length > SDK_ASYNC_BATCH_SIZE) {
|
|
1747
|
+
throw new Error(`pollBatch requires between 1 and ${SDK_ASYNC_BATCH_SIZE} receipt entries`);
|
|
1748
|
+
}
|
|
1749
|
+
const receiptIds = /* @__PURE__ */ new Set();
|
|
1750
|
+
for (const entry of batch.entries) {
|
|
1751
|
+
if (entry.scanId !== batch.scanId) {
|
|
1752
|
+
throw new Error(`Receipt entry scan ID ${entry.scanId} does not match ${batch.scanId}`);
|
|
1753
|
+
}
|
|
1754
|
+
if (!Number.isSafeInteger(entry.reqId) || entry.reqId < 0 || entry.reqId !== entry.index || receiptIds.has(entry.reqId)) {
|
|
1755
|
+
throw new Error("pollBatch requires a unique request ID matching each prompt index");
|
|
1756
|
+
}
|
|
1757
|
+
receiptIds.add(entry.reqId);
|
|
1758
|
+
}
|
|
1759
|
+
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1760
|
+
const baseDelay = retryOpts?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1761
|
+
const maxNoProgressPolls = retryOpts?.maxNoProgressPolls ?? DEFAULT_MAX_NO_PROGRESS_POLLS;
|
|
1762
|
+
const entries = new Map(batch.entries.map((entry) => [entry.reqId, entry]));
|
|
1763
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1764
|
+
let retryLevel = 0;
|
|
1765
|
+
let noProgressPolls = 0;
|
|
1766
|
+
while (resolved.size < batch.entries.length) {
|
|
1767
|
+
let rows;
|
|
1768
|
+
try {
|
|
1769
|
+
rows = await this.scanner.queryByScanIds([batch.scanId], NO_SDK_RETRIES);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
if (isRateLimitError(err) && retryLevel < maxRetries) {
|
|
1772
|
+
retryLevel++;
|
|
1773
|
+
const retryAfterMs = err.retryAfterMs;
|
|
1774
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1775
|
+
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1776
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
throw err;
|
|
1780
|
+
}
|
|
1781
|
+
const resolvedBeforePoll = resolved.size;
|
|
1782
|
+
const fallbackReports = /* @__PURE__ */ new Map();
|
|
1783
|
+
for (const row of rows) {
|
|
1784
|
+
const scanId = row.scan_id ?? batch.scanId;
|
|
1785
|
+
const reqId = row.req_id;
|
|
1786
|
+
const status = (row.status ?? "").toLowerCase();
|
|
1787
|
+
const terminal = status === "failed" || status === "complete" || status === "completed";
|
|
1788
|
+
if (reqId === void 0 && terminal && scanId === batch.scanId) {
|
|
1789
|
+
if (status === "failed") {
|
|
1790
|
+
const newlyFailed = [];
|
|
1791
|
+
for (const entry2 of batch.entries) {
|
|
1792
|
+
if (!resolved.has(entry2.reqId)) {
|
|
1793
|
+
const result = failedBulkResult(entry2);
|
|
1794
|
+
resolved.set(entry2.reqId, result);
|
|
1795
|
+
newlyFailed.push(result);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (newlyFailed.length > 0) await retryOpts?.onProgress?.(newlyFailed);
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
const reportId = row.result?.report_id ?? row.report_id ?? batch.reportId;
|
|
1802
|
+
if (!reportId) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`AIRS result correlation failed for scan ${scanId}: terminal row has no request or report ID`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
fallbackReports.set(scanId, reportId);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const entry = reqId === void 0 || scanId !== batch.scanId ? void 0 : entries.get(reqId);
|
|
1811
|
+
if (!entry || resolved.has(entry.reqId)) continue;
|
|
1812
|
+
if (status === "failed") {
|
|
1813
|
+
const result = failedBulkResult(entry);
|
|
1814
|
+
resolved.set(entry.reqId, result);
|
|
1815
|
+
await retryOpts?.onProgress?.([result]);
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if ((status === "complete" || status === "completed") && row.result) {
|
|
1819
|
+
const nestedResult = row.result;
|
|
1820
|
+
const nestedScanId = nestedResult.scan_id;
|
|
1821
|
+
if (nestedScanId && nestedScanId !== scanId) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${scanId}`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
const result = {
|
|
1827
|
+
...scanResponseToResult(nestedResult, entry.prompt),
|
|
1828
|
+
scanId,
|
|
1829
|
+
index: entry.index,
|
|
1830
|
+
reqId: entry.reqId
|
|
1831
|
+
};
|
|
1832
|
+
resolved.set(entry.reqId, result);
|
|
1833
|
+
await retryOpts?.onProgress?.([result]);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (fallbackReports.size > 0) {
|
|
1837
|
+
let reports;
|
|
1838
|
+
try {
|
|
1839
|
+
reports = await this.scanner.queryByReportIds(
|
|
1840
|
+
[...new Set(fallbackReports.values())],
|
|
1841
|
+
NO_SDK_RETRIES
|
|
1842
|
+
);
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
if (isRateLimitError(err) && retryLevel < maxRetries) {
|
|
1845
|
+
retryLevel++;
|
|
1846
|
+
const retryAfterMs = err.retryAfterMs;
|
|
1847
|
+
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1848
|
+
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1849
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
throw err;
|
|
1853
|
+
}
|
|
1854
|
+
for (const report of reports) {
|
|
1855
|
+
const reqId = report.req_id;
|
|
1856
|
+
const reportScanId = report.scan_id ?? batch.scanId;
|
|
1857
|
+
const expectedReportId = fallbackReports.get(reportScanId);
|
|
1858
|
+
const reportId = report.report_id;
|
|
1859
|
+
if (reportScanId !== batch.scanId || reportId && reportId !== expectedReportId) {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
`AIRS report correlation mismatch for scan ${batch.scanId}, request ${String(reqId)}`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
const entry = reqId === void 0 ? void 0 : entries.get(reqId);
|
|
1865
|
+
if (!entry || resolved.has(entry.reqId)) continue;
|
|
1866
|
+
const result = threatReportToResult(report, entry);
|
|
1867
|
+
resolved.set(entry.reqId, result);
|
|
1868
|
+
await retryOpts?.onProgress?.([result]);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (retryLevel > 0) retryLevel--;
|
|
1872
|
+
if (resolved.size === resolvedBeforePoll) {
|
|
1873
|
+
noProgressPolls++;
|
|
1874
|
+
} else {
|
|
1875
|
+
noProgressPolls = 0;
|
|
1876
|
+
}
|
|
1877
|
+
if (noProgressPolls >= maxNoProgressPolls) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
`AIRS polling made no progress after ${noProgressPolls} polls for scan ${batch.scanId}`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
if (resolved.size < batch.entries.length) {
|
|
1883
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return [...resolved.values()].sort((left, right) => left.index - right.index);
|
|
1887
|
+
}
|
|
1888
|
+
/** @deprecated Use submitBatch to preserve per-prompt request correlation. */
|
|
1450
1889
|
async submitBulkScan(profileName, prompts, sessionId) {
|
|
1451
1890
|
const scanIds = [];
|
|
1452
|
-
for (let i = 0; i < prompts.length; i +=
|
|
1453
|
-
const batch = prompts.slice(i, i +
|
|
1891
|
+
for (let i = 0; i < prompts.length; i += SDK_ASYNC_BATCH_SIZE) {
|
|
1892
|
+
const batch = prompts.slice(i, i + SDK_ASYNC_BATCH_SIZE);
|
|
1454
1893
|
const scanObjects = batch.map((prompt, idx) => ({
|
|
1455
1894
|
req_id: i + idx,
|
|
1456
1895
|
scan_req: {
|
|
@@ -1459,18 +1898,16 @@ var SdkRuntimeService = class {
|
|
|
1459
1898
|
...sessionId ? { session_id: sessionId } : {}
|
|
1460
1899
|
}
|
|
1461
1900
|
}));
|
|
1462
|
-
const res = await this.scanner.asyncScan(scanObjects);
|
|
1901
|
+
const res = await this.scanner.asyncScan(scanObjects, NO_SDK_RETRIES);
|
|
1463
1902
|
scanIds.push(res.scan_id);
|
|
1464
1903
|
}
|
|
1465
1904
|
return scanIds;
|
|
1466
1905
|
}
|
|
1467
1906
|
/**
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* defaults (`''`, `undefined`, `false`, `{}`) in the returned results.
|
|
1473
|
-
* Use `scanPrompt()` (sync API) when these fields are needed.
|
|
1907
|
+
* Compatibility poller for callers that retained only batch scan IDs.
|
|
1908
|
+
* Nested detection data is preserved, but prompt text and per-request fan-out
|
|
1909
|
+
* cannot be reconstructed from scan IDs alone.
|
|
1910
|
+
* @deprecated Use pollBatch to preserve `(scan_id, req_id)` correlation and prompt text.
|
|
1474
1911
|
*/
|
|
1475
1912
|
async pollResults(scanIds, intervalMs = DEFAULT_POLL_INTERVAL_MS, retryOpts) {
|
|
1476
1913
|
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
@@ -1519,19 +1956,16 @@ var SdkRuntimeService = class {
|
|
|
1519
1956
|
const status = (r.status ?? "").toLowerCase();
|
|
1520
1957
|
if ((status === "complete" || status === "completed") && r.result) {
|
|
1521
1958
|
const result = r.result;
|
|
1959
|
+
const nestedScanId = result.scan_id;
|
|
1960
|
+
if (nestedScanId && nestedScanId !== id) {
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
`AIRS result correlation mismatch: nested scan ID ${nestedScanId} does not match ${id}`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1522
1965
|
completed.set(id, {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
// not available from async API
|
|
1527
|
-
scanId: result.scan_id ?? id,
|
|
1528
|
-
reportId: result.report_id ?? "",
|
|
1529
|
-
action: result.action === "block" ? "block" : "allow",
|
|
1530
|
-
category: result.category ?? "unknown",
|
|
1531
|
-
triggered: false,
|
|
1532
|
-
// not available from async API — always false
|
|
1533
|
-
detections: {}
|
|
1534
|
-
// not available from async API
|
|
1966
|
+
...scanResponseToResult(result, ""),
|
|
1967
|
+
scanId: id,
|
|
1968
|
+
action: result.action === "block" ? "block" : "allow"
|
|
1535
1969
|
});
|
|
1536
1970
|
pending.delete(id);
|
|
1537
1971
|
} else if (status === "failed") {
|
|
@@ -1543,22 +1977,38 @@ var SdkRuntimeService = class {
|
|
|
1543
1977
|
scanId: id,
|
|
1544
1978
|
reportId: "",
|
|
1545
1979
|
action: "allow",
|
|
1546
|
-
// safe default for failed scans
|
|
1547
1980
|
category: "error",
|
|
1548
1981
|
triggered: false,
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
// not available from async API
|
|
1982
|
+
detections: {},
|
|
1983
|
+
error: "AIRS async scan failed"
|
|
1552
1984
|
});
|
|
1553
1985
|
pending.delete(id);
|
|
1554
1986
|
}
|
|
1555
1987
|
}
|
|
1556
1988
|
}
|
|
1557
1989
|
static formatResultsCsv(results) {
|
|
1558
|
-
const header =
|
|
1990
|
+
const header = [
|
|
1991
|
+
"prompt",
|
|
1992
|
+
"action",
|
|
1993
|
+
"category",
|
|
1994
|
+
"triggered",
|
|
1995
|
+
...RUNTIME_DETECTION_KEYS,
|
|
1996
|
+
"scan_id",
|
|
1997
|
+
"report_id",
|
|
1998
|
+
"error"
|
|
1999
|
+
].join(",");
|
|
1559
2000
|
const rows = results.map((r) => {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
2001
|
+
const fields = [
|
|
2002
|
+
r.prompt,
|
|
2003
|
+
r.action,
|
|
2004
|
+
r.category,
|
|
2005
|
+
String(r.triggered),
|
|
2006
|
+
...RUNTIME_DETECTION_KEYS.map((key) => String(r.detections[key] === true)),
|
|
2007
|
+
r.scanId,
|
|
2008
|
+
r.reportId,
|
|
2009
|
+
r.error ?? ""
|
|
2010
|
+
];
|
|
2011
|
+
return fields.map((field) => `"${field.replace(/"/g, '""')}"`).join(",");
|
|
1562
2012
|
});
|
|
1563
2013
|
return [header, ...rows].join("\n");
|
|
1564
2014
|
}
|
|
@@ -1712,6 +2162,10 @@ var ConfigSchema = z.object({
|
|
|
1712
2162
|
modelSecDataEndpoint: z.string().optional(),
|
|
1713
2163
|
modelSecMgmtEndpoint: z.string().optional(),
|
|
1714
2164
|
modelSecTokenEndpoint: z.string().optional(),
|
|
2165
|
+
// AI Gateway (endpoints only; creds shared with mgmt*)
|
|
2166
|
+
aiGwDataEndpoint: z.string().optional(),
|
|
2167
|
+
aiGwAdminEndpoint: z.string().optional(),
|
|
2168
|
+
aiGwTokenEndpoint: z.string().optional(),
|
|
1715
2169
|
// Tuning
|
|
1716
2170
|
scanConcurrency: z.coerce.number().int().min(1).max(20).default(5),
|
|
1717
2171
|
// Persistence
|
|
@@ -1742,6 +2196,9 @@ function fromEnv() {
|
|
|
1742
2196
|
modelSecDataEndpoint: env.PANW_MODEL_SEC_DATA_ENDPOINT,
|
|
1743
2197
|
modelSecMgmtEndpoint: env.PANW_MODEL_SEC_MGMT_ENDPOINT,
|
|
1744
2198
|
modelSecTokenEndpoint: env.PANW_MODEL_SEC_TOKEN_ENDPOINT,
|
|
2199
|
+
aiGwDataEndpoint: env.PANW_AI_GW_DATA_ENDPOINT,
|
|
2200
|
+
aiGwAdminEndpoint: env.PANW_AI_GW_ADMIN_ENDPOINT,
|
|
2201
|
+
aiGwTokenEndpoint: env.PANW_AI_GW_TOKEN_ENDPOINT,
|
|
1745
2202
|
scanConcurrency: env.SCAN_CONCURRENCY,
|
|
1746
2203
|
dataDir: env.DATA_DIR
|
|
1747
2204
|
};
|
|
@@ -1931,6 +2388,7 @@ export {
|
|
|
1931
2388
|
SdkModelSecurityService,
|
|
1932
2389
|
SdkPromptSetService,
|
|
1933
2390
|
SdkRedTeamService,
|
|
2391
|
+
SDK_ASYNC_BATCH_SIZE,
|
|
1934
2392
|
SdkRuntimeService,
|
|
1935
2393
|
AirsScanService,
|
|
1936
2394
|
RateLimitedScanService,
|