@cdot65/prisma-airs-cli 5.5.0 → 5.7.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 +1 -1
- package/dist/{chunk-NKAVUBKI.js → chunk-XQOICLSH.js} +7 -1
- package/dist/cli/index.js +562 -109
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- **Unified automation output** — resource reads support `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout; environment deliverables use HTML or Markdown
|
|
26
26
|
- **Complete pagination** — consistent `--limit`, `--offset`, and `--all` traversal with a configurable safety cap
|
|
27
27
|
- **`airs doctor`** — one-command diagnostics for environment, credentials, and API connectivity
|
|
28
|
-
- **Tenant
|
|
28
|
+
- **Tenant configuration** — `airs tenant create|set|switch|list|read|delete` supports guided setup with hidden secret prompts, individual setting updates, and existing-file registration without changing read-only mounts
|
|
29
29
|
- **Profile migration** — `airs runtime profiles backup|restore` exports private JSON/YAML, previews cross-tenant restores, remaps topics and explicit DLP dependencies, and verifies restored policies ([guide](https://cdot65.github.io/prisma-airs-cli/runtime/profile-transfer/))
|
|
30
30
|
- **`airs config`** — manage the selected config file from the CLI (`list`, `get`, `set`, `unset`, `path`)
|
|
31
31
|
|
|
@@ -2402,11 +2402,14 @@ async function changeStore(change) {
|
|
|
2402
2402
|
await unlink(lockPath);
|
|
2403
2403
|
}
|
|
2404
2404
|
}
|
|
2405
|
-
|
|
2405
|
+
function validateTenantName(name) {
|
|
2406
2406
|
if (!NameSchema.safeParse(name).success)
|
|
2407
2407
|
throw new Error(
|
|
2408
2408
|
"Tenant name must be 1\u201364 letters, digits, hyphens or underscores, start with a letter/digit, and not be default"
|
|
2409
2409
|
);
|
|
2410
|
+
}
|
|
2411
|
+
async function createTenant(name, configPath) {
|
|
2412
|
+
validateTenantName(name);
|
|
2410
2413
|
let path2;
|
|
2411
2414
|
try {
|
|
2412
2415
|
path2 = realpathSync(resolve(expandConfigPath(configPath)));
|
|
@@ -4186,10 +4189,13 @@ export {
|
|
|
4186
4189
|
readBackupFile,
|
|
4187
4190
|
readBackupDir,
|
|
4188
4191
|
ConfigSchema,
|
|
4192
|
+
tenantStorePath,
|
|
4189
4193
|
expandConfigPath,
|
|
4190
4194
|
defaultTenantConfigPath,
|
|
4191
4195
|
readTenantStore,
|
|
4192
4196
|
readTenantConfig,
|
|
4197
|
+
readTenantConfigFile,
|
|
4198
|
+
validateTenantName,
|
|
4193
4199
|
createTenant,
|
|
4194
4200
|
switchTenant,
|
|
4195
4201
|
deleteTenant,
|
package/dist/cli/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
readBackupDir,
|
|
24
24
|
readBackupFile,
|
|
25
25
|
readTenantConfig,
|
|
26
|
+
readTenantConfigFile,
|
|
26
27
|
readTenantStore,
|
|
27
28
|
renderEnvironmentReportHtml,
|
|
28
29
|
renderEnvironmentReportMarkdown,
|
|
@@ -34,10 +35,12 @@ import {
|
|
|
34
35
|
resolveOutputDir,
|
|
35
36
|
sanitizeFilename,
|
|
36
37
|
switchTenant,
|
|
38
|
+
tenantStorePath,
|
|
39
|
+
validateTenantName,
|
|
37
40
|
validateTopic,
|
|
38
41
|
writeBackupFile,
|
|
39
42
|
writeReportFile
|
|
40
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-XQOICLSH.js";
|
|
41
44
|
|
|
42
45
|
// src/cli/index.ts
|
|
43
46
|
import "dotenv/config";
|
|
@@ -56,9 +59,9 @@ function installProcessGuards() {
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// src/cli/program.ts
|
|
59
|
-
import { randomUUID as
|
|
62
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
60
63
|
import { readFileSync as readFileSync4 } from "fs";
|
|
61
|
-
import { dirname as
|
|
64
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
62
65
|
import { fileURLToPath } from "url";
|
|
63
66
|
import { Command } from "commander";
|
|
64
67
|
|
|
@@ -519,6 +522,10 @@ ${INDENT}${chalk3.bold(label)}
|
|
|
519
522
|
warn(msg) {
|
|
520
523
|
console.log(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
|
|
521
524
|
},
|
|
525
|
+
/** Safety warnings remain visible in quiet mode without corrupting structured stdout. */
|
|
526
|
+
warning(msg) {
|
|
527
|
+
console.error(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
|
|
528
|
+
},
|
|
522
529
|
/** Neutral informational callout. Quiet-suppressed. */
|
|
523
530
|
info(msg) {
|
|
524
531
|
if (quietMode) return;
|
|
@@ -3684,19 +3691,19 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3684
3691
|
mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
|
|
3685
3692
|
writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
3686
3693
|
const originalFetch = globalThis.fetch;
|
|
3687
|
-
globalThis.fetch = async function debugFetch(
|
|
3688
|
-
const url = typeof
|
|
3694
|
+
globalThis.fetch = async function debugFetch(input2, init2) {
|
|
3695
|
+
const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
|
|
3689
3696
|
const rawHeaders = headersToRecord(
|
|
3690
|
-
init2?.headers ?? (
|
|
3697
|
+
init2?.headers ?? (input2 instanceof Request ? input2.headers : void 0)
|
|
3691
3698
|
);
|
|
3692
3699
|
const inference = Object.keys(rawHeaders).some(
|
|
3693
3700
|
(name) => name.toLowerCase() === "x-portkey-api-key"
|
|
3694
3701
|
);
|
|
3695
3702
|
if (!isAirsUrl(url) && !inference) {
|
|
3696
|
-
return originalFetch(
|
|
3703
|
+
return originalFetch(input2, init2);
|
|
3697
3704
|
}
|
|
3698
3705
|
const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
|
|
3699
|
-
const method = init2?.method ?? (
|
|
3706
|
+
const method = init2?.method ?? (input2 instanceof Request ? input2.method : "GET");
|
|
3700
3707
|
const reqHeaders = redactHeaders(rawHeaders);
|
|
3701
3708
|
const loggedUrl = redactUrl(url);
|
|
3702
3709
|
let reqBody;
|
|
@@ -3715,7 +3722,7 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3715
3722
|
let resBody;
|
|
3716
3723
|
let error;
|
|
3717
3724
|
try {
|
|
3718
|
-
response = await originalFetch(
|
|
3725
|
+
response = await originalFetch(input2, init2);
|
|
3719
3726
|
} catch (err) {
|
|
3720
3727
|
error = inference ? "Runtime request failed" : "Request failed";
|
|
3721
3728
|
const entry2 = JSON.stringify({
|
|
@@ -6955,8 +6962,8 @@ function buildDefaultCategories(categories) {
|
|
|
6955
6962
|
])
|
|
6956
6963
|
);
|
|
6957
6964
|
}
|
|
6958
|
-
function parseAttackGoals(
|
|
6959
|
-
const trimmed =
|
|
6965
|
+
function parseAttackGoals(input2) {
|
|
6966
|
+
const trimmed = input2.trim();
|
|
6960
6967
|
const raw = trimmed.startsWith("[") ? trimmed : fs2.readFileSync(trimmed, "utf-8");
|
|
6961
6968
|
let parsed;
|
|
6962
6969
|
try {
|
|
@@ -6978,10 +6985,10 @@ function sliceClientSide(items, opts) {
|
|
|
6978
6985
|
const limit = opts.limit !== void 0 ? Number.parseInt(String(opts.limit), 10) : void 0;
|
|
6979
6986
|
return items.slice(offset, limit === void 0 ? void 0 : offset + limit);
|
|
6980
6987
|
}
|
|
6981
|
-
function parsePositiveInt(
|
|
6982
|
-
const n = Number.parseInt(
|
|
6988
|
+
function parsePositiveInt(input2, flag) {
|
|
6989
|
+
const n = Number.parseInt(input2, 10);
|
|
6983
6990
|
if (!Number.isFinite(n) || n <= 0) {
|
|
6984
|
-
throw new Error(`${flag}: expected a positive integer, got "${
|
|
6991
|
+
throw new Error(`${flag}: expected a positive integer, got "${input2}"`);
|
|
6985
6992
|
}
|
|
6986
6993
|
return n;
|
|
6987
6994
|
}
|
|
@@ -7067,10 +7074,10 @@ function resolveScriptB64(opts) {
|
|
|
7067
7074
|
}
|
|
7068
7075
|
throw new Error("one of --script-file or --script-b64 is required");
|
|
7069
7076
|
}
|
|
7070
|
-
function parseAdapterVariables(
|
|
7077
|
+
function parseAdapterVariables(input2) {
|
|
7071
7078
|
let parsed;
|
|
7072
7079
|
try {
|
|
7073
|
-
parsed = JSON.parse(
|
|
7080
|
+
parsed = JSON.parse(input2);
|
|
7074
7081
|
} catch (err) {
|
|
7075
7082
|
throw new Error(`--variables: invalid JSON (${err instanceof Error ? err.message : err})`);
|
|
7076
7083
|
}
|
|
@@ -9548,6 +9555,102 @@ import {
|
|
|
9548
9555
|
SecurityProfileSchema
|
|
9549
9556
|
} from "@cdot65/prisma-airs-sdk";
|
|
9550
9557
|
import { z as z5 } from "zod";
|
|
9558
|
+
|
|
9559
|
+
// src/backup/runtime-policy.ts
|
|
9560
|
+
function object(value) {
|
|
9561
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9562
|
+
}
|
|
9563
|
+
function compareRuntimePolicies(expected, actual) {
|
|
9564
|
+
const serverDefaults = [];
|
|
9565
|
+
const differences = [];
|
|
9566
|
+
const visit = (source, target, path3, insideToxicCategories = false, insideTopicGuardrails = false) => {
|
|
9567
|
+
if (source === target) return;
|
|
9568
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
9569
|
+
if (source.length !== target.length) differences.push(`${path3}.length`);
|
|
9570
|
+
source.forEach((item, index) => {
|
|
9571
|
+
visit(
|
|
9572
|
+
item,
|
|
9573
|
+
target[index],
|
|
9574
|
+
`${path3}[${index}]`,
|
|
9575
|
+
insideToxicCategories,
|
|
9576
|
+
insideTopicGuardrails
|
|
9577
|
+
);
|
|
9578
|
+
});
|
|
9579
|
+
return;
|
|
9580
|
+
}
|
|
9581
|
+
if (object(source) && object(target)) {
|
|
9582
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(target)])) {
|
|
9583
|
+
const field = `${path3}.${key}`;
|
|
9584
|
+
if (!Object.hasOwn(source, key) && Object.hasOwn(target, key)) {
|
|
9585
|
+
const model = /^policy\.ai-security-profiles\[\d+\]\.model-configuration/;
|
|
9586
|
+
const relative = path3.replace(model, "model");
|
|
9587
|
+
let allowed;
|
|
9588
|
+
if (relative === "model.data-protection" && key === "database-security") allowed = null;
|
|
9589
|
+
if (key === "severity") {
|
|
9590
|
+
if (/^model\.data-protection\.database-security\[\d+\]$/.test(relative)) {
|
|
9591
|
+
const values = {
|
|
9592
|
+
"database-security-create": "medium",
|
|
9593
|
+
"database-security-read": "low",
|
|
9594
|
+
"database-security-update": "medium",
|
|
9595
|
+
"database-security-delete": "high"
|
|
9596
|
+
};
|
|
9597
|
+
if (source.name === target.name && typeof source.name === "string")
|
|
9598
|
+
allowed = Object.hasOwn(values, source.name) ? values[source.name] : void 0;
|
|
9599
|
+
}
|
|
9600
|
+
if (relative === "model.data-protection.source-code-detection") allowed = "high";
|
|
9601
|
+
if (relative === "model.app-protection.malicious-code-protection" && source.name === "malicious-code" && target.name === source.name)
|
|
9602
|
+
allowed = "high";
|
|
9603
|
+
if (/^model\.model-protection\[\d+\]$/.test(relative) && (source.name === "prompt-injection" || source.name === "topic-guardrails" || source.name === "contextual-grounding" && source.action === "block") && target.name === source.name)
|
|
9604
|
+
allowed = "medium";
|
|
9605
|
+
if (insideTopicGuardrails && /^model\.model-protection\[\d+\]\.topic-list\[\d+\]\.topic\[\d+\]$/.test(relative) && typeof source.topic_id === "string" && source.topic_id.length > 0 && source.topic_id === target.topic_id && source.topic_name === target.topic_name && Number.isInteger(source.revision) && source.revision === target.revision)
|
|
9606
|
+
allowed = "medium";
|
|
9607
|
+
if (/^model\.agent-protection\[\d+\]$/.test(relative) && source.name === "agent-security" && target.name === source.name)
|
|
9608
|
+
allowed = "medium";
|
|
9609
|
+
}
|
|
9610
|
+
if (relative === "model.app-protection" && key === "url-detected-severity")
|
|
9611
|
+
allowed = "low";
|
|
9612
|
+
if (relative === "model.app-protection" && key === "default-url-category" && object(target[key])) {
|
|
9613
|
+
const value = target[key];
|
|
9614
|
+
if (Object.keys(value).length === 1 && value.member === null) {
|
|
9615
|
+
serverDefaults.push(field);
|
|
9616
|
+
continue;
|
|
9617
|
+
}
|
|
9618
|
+
}
|
|
9619
|
+
if ((/^model\.model-protection\[\d+\]$/.test(relative) && source.name === "toxic-content" && target.name === source.name || insideToxicCategories && /^model\.model-protection\[\d+\]\.toxic-category-list\[\d+\]$/.test(relative) && typeof source.category === "string" && source.category.length > 0 && source.category === target.category) && key === "severity-by-confidence" && object(target[key])) {
|
|
9620
|
+
const value = target[key];
|
|
9621
|
+
if (Object.keys(value).length === 2 && value.high === "medium" && value.moderate === "low") {
|
|
9622
|
+
serverDefaults.push(field);
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
if (allowed !== void 0 && target[key] === allowed) {
|
|
9627
|
+
serverDefaults.push(field);
|
|
9628
|
+
continue;
|
|
9629
|
+
}
|
|
9630
|
+
differences.push(field);
|
|
9631
|
+
} else if (!Object.hasOwn(target, key)) differences.push(field);
|
|
9632
|
+
else
|
|
9633
|
+
visit(
|
|
9634
|
+
source[key],
|
|
9635
|
+
target[key],
|
|
9636
|
+
field,
|
|
9637
|
+
key === "toxic-category-list" && source.name === "toxic-content" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
|
|
9638
|
+
path3
|
|
9639
|
+
),
|
|
9640
|
+
key === "topic-list" && source.name === "topic-guardrails" && target.name === source.name && /^policy\.ai-security-profiles\[\d+\]\.model-configuration\.model-protection\[\d+\]$/.test(
|
|
9641
|
+
path3
|
|
9642
|
+
) || insideTopicGuardrails && key === "topic" && source.action === "block" && target.action === "block"
|
|
9643
|
+
);
|
|
9644
|
+
}
|
|
9645
|
+
return;
|
|
9646
|
+
}
|
|
9647
|
+
differences.push(path3);
|
|
9648
|
+
};
|
|
9649
|
+
visit(expected, actual, "policy");
|
|
9650
|
+
return { matches: differences.length === 0, serverDefaults, differences };
|
|
9651
|
+
}
|
|
9652
|
+
|
|
9653
|
+
// src/backup/runtime-profiles.ts
|
|
9551
9654
|
var RuntimeProfilesBackupSchema = z5.object({
|
|
9552
9655
|
version: z5.literal(1),
|
|
9553
9656
|
resourceType: z5.literal("runtime-security-profiles"),
|
|
@@ -9556,7 +9659,7 @@ var RuntimeProfilesBackupSchema = z5.object({
|
|
|
9556
9659
|
profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
|
|
9557
9660
|
topics: z5.array(CustomTopicSchema).max(1e4)
|
|
9558
9661
|
}).strict();
|
|
9559
|
-
function
|
|
9662
|
+
function object2(value) {
|
|
9560
9663
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9561
9664
|
}
|
|
9562
9665
|
function pagesLimit(value) {
|
|
@@ -9623,7 +9726,7 @@ function topicReferences(policy) {
|
|
|
9623
9726
|
value.forEach(visit);
|
|
9624
9727
|
return;
|
|
9625
9728
|
}
|
|
9626
|
-
if (!
|
|
9729
|
+
if (!object2(value)) return;
|
|
9627
9730
|
if ("topic_id" in value) {
|
|
9628
9731
|
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9629
9732
|
throw new Error("Invalid topic identity in profile policy");
|
|
@@ -9676,26 +9779,48 @@ function sameTopic(a, b) {
|
|
|
9676
9779
|
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9677
9780
|
}
|
|
9678
9781
|
function dlpReferences(policy) {
|
|
9679
|
-
if (!
|
|
9782
|
+
if (!object2(policy)) return [];
|
|
9680
9783
|
const refs = [];
|
|
9681
9784
|
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9682
|
-
if (!
|
|
9785
|
+
if (!object2(entry) || typeof entry.name !== "string")
|
|
9683
9786
|
throw new Error("Invalid embedded DLP reference");
|
|
9684
9787
|
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9685
9788
|
}
|
|
9686
9789
|
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9687
|
-
if (!
|
|
9790
|
+
if (!object2(entry) || !object2(entry["model-configuration"])) continue;
|
|
9688
9791
|
const protection = entry["model-configuration"]["data-protection"];
|
|
9689
|
-
if (!
|
|
9792
|
+
if (!object2(protection) || !object2(protection["data-leak-detection"])) continue;
|
|
9690
9793
|
const members = protection["data-leak-detection"].member;
|
|
9691
9794
|
if (!Array.isArray(members)) continue;
|
|
9692
9795
|
for (const member of members) {
|
|
9693
|
-
if (!
|
|
9796
|
+
if (!object2(member) || typeof member.text !== "string") throw new Error("Invalid DLP member");
|
|
9797
|
+
if (member.text.toLowerCase() === "sensitive content" && (member.id === "" || member.id === void 0) && (member.version === "2" || member.version === void 0) && !refs.some((ref) => ref.kind === "embedded" && ref.name === member.text))
|
|
9798
|
+
continue;
|
|
9694
9799
|
refs.push({ value: member, name: member.text, kind: "member" });
|
|
9695
9800
|
}
|
|
9696
9801
|
}
|
|
9697
9802
|
return refs;
|
|
9698
9803
|
}
|
|
9804
|
+
function basicFallback(policy) {
|
|
9805
|
+
const result = structuredClone(policy);
|
|
9806
|
+
const customNames = new Set(dlpReferences(result).map((ref) => ref.name));
|
|
9807
|
+
let converted = false;
|
|
9808
|
+
for (const entry of result?.["ai-security-profiles"] ?? []) {
|
|
9809
|
+
const detection = entry["model-configuration"]?.["data-protection"]?.["data-leak-detection"];
|
|
9810
|
+
if (!detection?.member?.some((member) => customNames.has(member.text))) continue;
|
|
9811
|
+
if (!["block", "allow", ""].includes(detection.action))
|
|
9812
|
+
throw new Error("Cannot safely fall back to Basic: unsupported DLP action");
|
|
9813
|
+
if (detection["mask-data-inline"] && detection.action !== "block")
|
|
9814
|
+
throw new Error("Cannot safely fall back to Basic: inline masking requires a block action");
|
|
9815
|
+
detection.member = detection.action === "" ? null : [{ text: "sensitive content", id: "", version: "2" }];
|
|
9816
|
+
converted = true;
|
|
9817
|
+
}
|
|
9818
|
+
if (!converted)
|
|
9819
|
+
throw new Error("Cannot safely fall back to Basic: no matching DLP detection configuration");
|
|
9820
|
+
if (result && "dlp-data-profiles" in result) result["dlp-data-profiles"] = [];
|
|
9821
|
+
PolicySchema.parse(result);
|
|
9822
|
+
return result;
|
|
9823
|
+
}
|
|
9699
9824
|
function assertKnownPolicyReferences(policy) {
|
|
9700
9825
|
const known = new Map([
|
|
9701
9826
|
...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
|
|
@@ -9711,7 +9836,7 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9711
9836
|
});
|
|
9712
9837
|
return;
|
|
9713
9838
|
}
|
|
9714
|
-
if (!
|
|
9839
|
+
if (!object2(value)) return;
|
|
9715
9840
|
for (const [key, child] of Object.entries(value)) {
|
|
9716
9841
|
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9717
9842
|
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
@@ -9732,8 +9857,8 @@ async function dataProfiles(api, maxPages2) {
|
|
|
9732
9857
|
}
|
|
9733
9858
|
throw new Error("DLP inventory is incomplete; increase --max-pages");
|
|
9734
9859
|
}
|
|
9735
|
-
async function planRuntimeProfilesRestore(api,
|
|
9736
|
-
const backup = RuntimeProfilesBackupSchema.parse(
|
|
9860
|
+
async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9861
|
+
const backup = RuntimeProfilesBackupSchema.parse(input2);
|
|
9737
9862
|
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9738
9863
|
const maxPages2 = options.maxPages ?? 100;
|
|
9739
9864
|
pagesLimit(maxPages2);
|
|
@@ -9741,7 +9866,10 @@ async function planRuntimeProfilesRestore(api, input, destinationTsgId, options
|
|
|
9741
9866
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9742
9867
|
throw new Error("Invalid --name-prefix");
|
|
9743
9868
|
const conflict = options.onConflict ?? "error";
|
|
9744
|
-
if (!["error", "skip", "update"].includes(conflict))
|
|
9869
|
+
if (!["error", "skip", "update", "verify"].includes(conflict))
|
|
9870
|
+
throw new Error("Invalid conflict policy");
|
|
9871
|
+
const missingDlp = options.onMissingDlp ?? "error";
|
|
9872
|
+
if (!["error", "basic"].includes(missingDlp)) throw new Error("Invalid missing DLP policy");
|
|
9745
9873
|
const selected = latestProfiles(backup.profiles);
|
|
9746
9874
|
assertTenant(selected, backup.source.tsgId);
|
|
9747
9875
|
if (selected.length !== backup.profiles.length)
|
|
@@ -9763,14 +9891,14 @@ async function planRuntimeProfilesRestore(api, input, destinationTsgId, options
|
|
|
9763
9891
|
const match = existing.find((p) => p.profile_name === name);
|
|
9764
9892
|
if (match && conflict === "error")
|
|
9765
9893
|
throw new Error(
|
|
9766
|
-
`Destination profile already exists: ${name}; choose --on-conflict skip or update`
|
|
9894
|
+
`Destination profile already exists: ${name}; choose --on-conflict verify, skip or update`
|
|
9767
9895
|
);
|
|
9768
9896
|
if (match && !match.profile_id) throw new Error("Destination profile has no identity");
|
|
9769
9897
|
return {
|
|
9770
9898
|
source: structuredClone(source),
|
|
9771
9899
|
name,
|
|
9772
9900
|
existing: match,
|
|
9773
|
-
action: match ? conflict === "skip" ? "skip" : "update" : "create"
|
|
9901
|
+
action: match ? conflict === "skip" ? "skip" : conflict === "verify" ? "verify" : "update" : "create"
|
|
9774
9902
|
};
|
|
9775
9903
|
});
|
|
9776
9904
|
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
@@ -9804,46 +9932,102 @@ async function planRuntimeProfilesRestore(api, input, destinationTsgId, options
|
|
|
9804
9932
|
);
|
|
9805
9933
|
topicPlans.set(key, { key, name, source, existing: match });
|
|
9806
9934
|
}
|
|
9807
|
-
const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9935
|
+
const dlpRefs = crossTenant || missingDlp === "basic" ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9808
9936
|
const names = [...new Set(dlpRefs.map((ref) => ref.name))];
|
|
9809
|
-
|
|
9810
|
-
|
|
9937
|
+
const targetFor = (name) => Object.hasOwn(options.dlpMap ?? {}, name) ? options.dlpMap?.[name] : crossTenant ? void 0 : name;
|
|
9938
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
9939
|
+
for (const name of names) {
|
|
9940
|
+
if (!targetFor(name) && missingDlp === "error")
|
|
9811
9941
|
throw new Error(
|
|
9812
|
-
`Cross-tenant DLP reference requires explicit --dlp-map ${name}=<destination-name
|
|
9942
|
+
`Cross-tenant DLP reference requires explicit --dlp-map "${name}=<destination-name>"; DLP resources are not cloned. Inspect destination resources with airs runtime dlp profiles list --all --output json, or explicitly accept --on-missing-dlp basic.`
|
|
9813
9943
|
);
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9944
|
+
if (!targetFor(name)) unresolved.add(name);
|
|
9945
|
+
}
|
|
9946
|
+
const catalog = names.some((name) => targetFor(name)) ? await dataProfiles(api, maxPages2) : [];
|
|
9947
|
+
const resolved = [];
|
|
9948
|
+
for (const name of names) {
|
|
9949
|
+
const target = targetFor(name);
|
|
9950
|
+
if (!target) continue;
|
|
9951
|
+
const matches = catalog.filter(
|
|
9952
|
+
(p) => p.name === target && p.profile_status !== "deleted" && p.profile_status !== "disabled"
|
|
9953
|
+
);
|
|
9954
|
+
if (!matches.length && missingDlp === "basic") {
|
|
9955
|
+
unresolved.add(name);
|
|
9956
|
+
continue;
|
|
9957
|
+
}
|
|
9818
9958
|
if (matches.length !== 1 || !matches[0].id)
|
|
9819
9959
|
throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
|
|
9820
9960
|
if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
|
|
9821
9961
|
throw new Error(`Destination DLP version is unknown: ${target}`);
|
|
9822
|
-
|
|
9962
|
+
resolved.push({
|
|
9823
9963
|
source: name,
|
|
9824
9964
|
destination: String(target),
|
|
9825
9965
|
id: matches[0].id,
|
|
9826
9966
|
version: matches[0].version ?? void 0
|
|
9827
|
-
};
|
|
9828
|
-
}
|
|
9829
|
-
|
|
9967
|
+
});
|
|
9968
|
+
}
|
|
9969
|
+
const dlpFallbacks = [];
|
|
9970
|
+
for (const item of changing) {
|
|
9971
|
+
const replaced = [...new Set(dlpReferences(item.source.policy).map((ref) => ref.name))];
|
|
9972
|
+
const missing = replaced.filter((name) => unresolved.has(name));
|
|
9973
|
+
if (!missing.length) continue;
|
|
9974
|
+
item.fallbackPolicy = basicFallback(item.source.policy);
|
|
9975
|
+
dlpFallbacks.push({
|
|
9976
|
+
profile: item.name,
|
|
9977
|
+
unresolved: missing,
|
|
9978
|
+
replaced,
|
|
9979
|
+
reason: "unmapped-or-missing",
|
|
9980
|
+
protection: "basic"
|
|
9981
|
+
});
|
|
9982
|
+
}
|
|
9983
|
+
const remaining = new Set(
|
|
9984
|
+
changing.flatMap(
|
|
9985
|
+
(p) => dlpReferences(p.fallbackPolicy ?? p.source.policy).map((ref) => ref.name)
|
|
9986
|
+
)
|
|
9987
|
+
);
|
|
9988
|
+
const dlpMappings = resolved.filter((mapping) => remaining.has(mapping.source));
|
|
9989
|
+
const plan = {
|
|
9830
9990
|
sourceTsgId: backup.source.tsgId,
|
|
9831
9991
|
destinationTsgId,
|
|
9832
9992
|
profiles: profilePlans,
|
|
9833
9993
|
topics: [...topicPlans.values()],
|
|
9834
9994
|
dlpMappings,
|
|
9995
|
+
dlpFallbacks,
|
|
9996
|
+
serverDefaults: [],
|
|
9835
9997
|
maxPages: maxPages2
|
|
9836
9998
|
};
|
|
9999
|
+
const existingTopics = new Map(
|
|
10000
|
+
plan.topics.filter((t) => t.existing).map((t) => [t.key, t.existing])
|
|
10001
|
+
);
|
|
10002
|
+
for (const item of profilePlans.filter((p) => p.action === "verify")) {
|
|
10003
|
+
const expected = mappedPolicy(item, plan, existingTopics);
|
|
10004
|
+
const comparison = compareRuntimePolicies(expected, item.existing?.policy);
|
|
10005
|
+
if (!comparison.matches || item.source.active !== void 0 && item.existing?.active !== item.source.active)
|
|
10006
|
+
throw new Error(
|
|
10007
|
+
`Destination profile does not match restore plan: ${item.name}; verification made no changes`
|
|
10008
|
+
);
|
|
10009
|
+
if (comparison.serverDefaults.length)
|
|
10010
|
+
plan.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
|
|
10011
|
+
}
|
|
10012
|
+
return plan;
|
|
9837
10013
|
}
|
|
9838
10014
|
function canonical(value) {
|
|
9839
|
-
const order = (v) => Array.isArray(v) ? v.map(order) :
|
|
10015
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object2(v) ? Object.fromEntries(
|
|
9840
10016
|
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9841
10017
|
) : v;
|
|
9842
10018
|
return JSON.stringify(order(value));
|
|
9843
10019
|
}
|
|
10020
|
+
function canonicalPolicy(policy) {
|
|
10021
|
+
const value = structuredClone(policy);
|
|
10022
|
+
for (const entry of value?.["ai-security-profiles"] ?? []) {
|
|
10023
|
+
const protection = entry["model-configuration"]?.["data-protection"];
|
|
10024
|
+
if (protection && !("database-security" in protection)) protection["database-security"] = null;
|
|
10025
|
+
}
|
|
10026
|
+
return canonical(value);
|
|
10027
|
+
}
|
|
9844
10028
|
function unchanged(current, prior) {
|
|
9845
10029
|
return Boolean(
|
|
9846
|
-
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active &&
|
|
10030
|
+
current && current.profile_id === prior.profile_id && current.revision === prior.revision && current.active === prior.active && canonicalPolicy(current.policy) === canonicalPolicy(prior.policy)
|
|
9847
10031
|
);
|
|
9848
10032
|
}
|
|
9849
10033
|
async function restoreRuntimeProfiles(api, plan) {
|
|
@@ -9851,6 +10035,8 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9851
10035
|
sourceTsgId: plan.sourceTsgId,
|
|
9852
10036
|
destinationTsgId: plan.destinationTsgId,
|
|
9853
10037
|
complete: false,
|
|
10038
|
+
dlpFallbacks: structuredClone(plan.dlpFallbacks ?? []),
|
|
10039
|
+
serverDefaults: [],
|
|
9854
10040
|
topics: [],
|
|
9855
10041
|
profiles: []
|
|
9856
10042
|
};
|
|
@@ -9861,7 +10047,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9861
10047
|
const catalog = await dataProfiles(api, plan.maxPages);
|
|
9862
10048
|
for (const mapping of plan.dlpMappings) {
|
|
9863
10049
|
const matches = catalog.filter(
|
|
9864
|
-
(p) => p.name === mapping.destination && p.profile_status !== "deleted"
|
|
10050
|
+
(p) => p.name === mapping.destination && p.profile_status !== "deleted" && p.profile_status !== "disabled"
|
|
9865
10051
|
);
|
|
9866
10052
|
if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
|
|
9867
10053
|
throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
|
|
@@ -9907,27 +10093,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9907
10093
|
result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
|
|
9908
10094
|
continue;
|
|
9909
10095
|
}
|
|
9910
|
-
const policy =
|
|
9911
|
-
for (const ref of topicReferences(policy)) {
|
|
9912
|
-
const topic = mappedTopics.get(topicKey(ref));
|
|
9913
|
-
if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
|
|
9914
|
-
ref.topic_id = topic.topic_id;
|
|
9915
|
-
ref.topic_name = topic.topic_name;
|
|
9916
|
-
ref.revision = topic.revision;
|
|
9917
|
-
}
|
|
9918
|
-
for (const ref of dlpReferences(policy)) {
|
|
9919
|
-
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
9920
|
-
if (!mapping) continue;
|
|
9921
|
-
if (ref.kind === "embedded") {
|
|
9922
|
-
ref.value.name = mapping.destination;
|
|
9923
|
-
ref.value.uuid = mapping.id;
|
|
9924
|
-
} else ref.value.text = mapping.destination;
|
|
9925
|
-
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
9926
|
-
if ("version" in ref.value) {
|
|
9927
|
-
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
9928
|
-
ref.value.version = String(mapping.version);
|
|
9929
|
-
}
|
|
9930
|
-
}
|
|
10096
|
+
const policy = mappedPolicy(item, plan, mappedTopics);
|
|
9931
10097
|
const body = CreateSecurityProfileRequestSchema.parse({
|
|
9932
10098
|
profile_name: item.name,
|
|
9933
10099
|
active: item.source.active,
|
|
@@ -9938,6 +10104,14 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9938
10104
|
const current = inventory2.find((p) => p.profile_name === item.name);
|
|
9939
10105
|
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9940
10106
|
throw new Error(`Destination profile changed after planning: ${item.name}`);
|
|
10107
|
+
if (item.action === "verify") {
|
|
10108
|
+
const comparison2 = compareRuntimePolicies(policy, current?.policy);
|
|
10109
|
+
if (!comparison2.matches) throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
10110
|
+
result.profiles.push({ name: item.name, action: "verified", id: current?.profile_id });
|
|
10111
|
+
if (comparison2.serverDefaults.length)
|
|
10112
|
+
result.serverDefaults.push({ profile: item.name, fields: comparison2.serverDefaults });
|
|
10113
|
+
continue;
|
|
10114
|
+
}
|
|
9941
10115
|
const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
|
|
9942
10116
|
result.profiles.push({
|
|
9943
10117
|
name: item.name,
|
|
@@ -9948,8 +10122,11 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9948
10122
|
(p) => p.profile_name === item.name
|
|
9949
10123
|
);
|
|
9950
10124
|
if (verified) assertTenant([verified], plan.destinationTsgId);
|
|
9951
|
-
|
|
10125
|
+
const comparison = compareRuntimePolicies(policy, verified?.policy);
|
|
10126
|
+
if (!verified?.profile_id || response.profile_id && response.profile_id !== verified.profile_id || item.source.active !== void 0 && verified.active !== item.source.active || !comparison.matches)
|
|
9952
10127
|
throw new Error(`Restored profile did not verify: ${item.name}`);
|
|
10128
|
+
if (comparison.serverDefaults.length)
|
|
10129
|
+
result.serverDefaults.push({ profile: item.name, fields: comparison.serverDefaults });
|
|
9953
10130
|
}
|
|
9954
10131
|
result.complete = true;
|
|
9955
10132
|
} catch (error) {
|
|
@@ -9960,10 +10137,97 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9960
10137
|
}
|
|
9961
10138
|
return result;
|
|
9962
10139
|
}
|
|
10140
|
+
function mappedPolicy(item, plan, mappedTopics) {
|
|
10141
|
+
const policy = structuredClone(item.fallbackPolicy ?? item.source.policy);
|
|
10142
|
+
for (const ref of topicReferences(policy)) {
|
|
10143
|
+
const topic = mappedTopics.get(topicKey(ref));
|
|
10144
|
+
if (!topic?.topic_id)
|
|
10145
|
+
throw new Error(
|
|
10146
|
+
"Missing planned topic mapping; an existing profile cannot be verified without its destination dependencies"
|
|
10147
|
+
);
|
|
10148
|
+
ref.topic_id = topic.topic_id;
|
|
10149
|
+
ref.topic_name = topic.topic_name;
|
|
10150
|
+
ref.revision = topic.revision;
|
|
10151
|
+
}
|
|
10152
|
+
for (const ref of dlpReferences(policy)) {
|
|
10153
|
+
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
10154
|
+
if (!mapping) continue;
|
|
10155
|
+
if (ref.kind === "embedded") {
|
|
10156
|
+
ref.value.name = mapping.destination;
|
|
10157
|
+
ref.value.uuid = mapping.id;
|
|
10158
|
+
} else ref.value.text = mapping.destination;
|
|
10159
|
+
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
10160
|
+
if ("version" in ref.value) {
|
|
10161
|
+
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
10162
|
+
ref.value.version = String(mapping.version);
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
return policy;
|
|
10166
|
+
}
|
|
9963
10167
|
|
|
9964
10168
|
// src/cli/commands/profile-transfer.ts
|
|
9965
10169
|
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
9966
10170
|
var FORMATS = "Output format: pretty, table, markdown, csv, json, yaml";
|
|
10171
|
+
function renderRestore(value, columns, format) {
|
|
10172
|
+
if (format !== "pretty" && format !== "table" && format !== "markdown")
|
|
10173
|
+
return formatOutput([value], columns, format);
|
|
10174
|
+
const markdown = format === "markdown";
|
|
10175
|
+
const rows = (key) => Array.isArray(value[key]) ? value[key] : [];
|
|
10176
|
+
const heading = (text2) => markdown ? `### ${text2}
|
|
10177
|
+
` : text2;
|
|
10178
|
+
const blocks = [
|
|
10179
|
+
heading(
|
|
10180
|
+
value.dryRun ? "Restore preview \u2014 no changes made" : value.complete ? "Restore complete" : "Restore incomplete \u2014 completed writes remain"
|
|
10181
|
+
),
|
|
10182
|
+
`Source TSG: ${value.sourceTsgId}
|
|
10183
|
+
${markdown ? "\n" : ""}Destination TSG: ${value.destinationTsgId}`
|
|
10184
|
+
];
|
|
10185
|
+
for (const [key, title] of [
|
|
10186
|
+
["profiles", "Profiles"],
|
|
10187
|
+
["topics", "Topics"]
|
|
10188
|
+
]) {
|
|
10189
|
+
blocks.push(heading(`${title} (${rows(key).length})`));
|
|
10190
|
+
blocks.push(
|
|
10191
|
+
formatOutput(
|
|
10192
|
+
rows(key),
|
|
10193
|
+
[
|
|
10194
|
+
{ key: "action", label: "Action" },
|
|
10195
|
+
{ key: "name", label: "Name" }
|
|
10196
|
+
],
|
|
10197
|
+
markdown ? "markdown" : "table"
|
|
10198
|
+
) || "(none)"
|
|
10199
|
+
);
|
|
10200
|
+
}
|
|
10201
|
+
if (rows("dlpMappings").length) {
|
|
10202
|
+
blocks.push(heading("DLP mappings"));
|
|
10203
|
+
blocks.push(
|
|
10204
|
+
formatOutput(
|
|
10205
|
+
rows("dlpMappings"),
|
|
10206
|
+
[
|
|
10207
|
+
{ key: "source", label: "Source" },
|
|
10208
|
+
{ key: "destination", label: "Destination" }
|
|
10209
|
+
],
|
|
10210
|
+
markdown ? "markdown" : "table"
|
|
10211
|
+
)
|
|
10212
|
+
);
|
|
10213
|
+
}
|
|
10214
|
+
if (rows("dlpFallbacks").length) {
|
|
10215
|
+
blocks.push(heading("Basic DLP fallbacks \u2014 custom protection lost"));
|
|
10216
|
+
for (const fallback of rows("dlpFallbacks"))
|
|
10217
|
+
blocks.push(
|
|
10218
|
+
`${fallback.profile}: ${Array.isArray(fallback.replaced) ? fallback.replaced.join(", ") : ""}`
|
|
10219
|
+
);
|
|
10220
|
+
}
|
|
10221
|
+
if (rows("serverDefaults").length) {
|
|
10222
|
+
blocks.push(heading("Verified server-added defaults (source omitted these fields)"));
|
|
10223
|
+
for (const item of rows("serverDefaults"))
|
|
10224
|
+
blocks.push(
|
|
10225
|
+
`${item.profile}: ${Array.isArray(item.fields) ? item.fields.length : 0} fields; use --output json for paths`
|
|
10226
|
+
);
|
|
10227
|
+
}
|
|
10228
|
+
if (value.error) blocks.push(`Error: ${value.error}`);
|
|
10229
|
+
return blocks.join("\n\n");
|
|
10230
|
+
}
|
|
9967
10231
|
function maxPages(value) {
|
|
9968
10232
|
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
9969
10233
|
usageError("--max-pages must be an integer from 1 to 1000");
|
|
@@ -10076,7 +10340,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10076
10340
|
});
|
|
10077
10341
|
const restore = profiles2.command("restore <file>").description(
|
|
10078
10342
|
"Restore profile policies into the selected tenant; validate all dependencies before changes"
|
|
10079
|
-
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option(
|
|
10343
|
+
).option("--dry-run", "Read and validate a restore plan without changing destination resources").option("--name-prefix <prefix>", "Prefix restored profile and topic names to avoid collisions").option(
|
|
10344
|
+
"--on-conflict <policy>",
|
|
10345
|
+
"Existing profile names: error, verify (resume without writes), skip or update",
|
|
10346
|
+
"error"
|
|
10347
|
+
).option(
|
|
10348
|
+
"--on-missing-dlp <policy>",
|
|
10349
|
+
"Unresolved custom DLP: error or basic (explicitly accepts loss of custom detection)",
|
|
10350
|
+
"error"
|
|
10351
|
+
).option(
|
|
10080
10352
|
"--dlp-map <source=destination>",
|
|
10081
10353
|
"Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
|
|
10082
10354
|
(value, prior) => [...prior, value],
|
|
@@ -10093,12 +10365,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10093
10365
|
"after",
|
|
10094
10366
|
examples(
|
|
10095
10367
|
"airs runtime profiles restore ./profiles.json --dry-run --output json",
|
|
10368
|
+
"airs runtime profiles restore ./profiles.json --on-missing-dlp basic --dry-run --output json",
|
|
10096
10369
|
"airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10097
10370
|
)
|
|
10098
10371
|
).action(async (file, opts) => {
|
|
10099
10372
|
try {
|
|
10100
|
-
if (!["error", "skip", "update"].includes(opts.onConflict))
|
|
10101
|
-
throw new CliUsageError("--on-conflict must be error, skip or update");
|
|
10373
|
+
if (!["error", "skip", "update", "verify"].includes(opts.onConflict))
|
|
10374
|
+
throw new CliUsageError("--on-conflict must be error, verify, skip or update");
|
|
10375
|
+
if (!["error", "basic"].includes(opts.onMissingDlp))
|
|
10376
|
+
throw new CliUsageError("--on-missing-dlp must be error or basic");
|
|
10102
10377
|
if (opts.force && !opts.expectTsg)
|
|
10103
10378
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10104
10379
|
const mappings = dlpMap(opts.dlpMap);
|
|
@@ -10109,15 +10384,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10109
10384
|
const info = await stat(path3);
|
|
10110
10385
|
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10111
10386
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10112
|
-
let
|
|
10387
|
+
let input2;
|
|
10113
10388
|
try {
|
|
10114
10389
|
const text2 = await readFile9(path3, "utf8");
|
|
10115
10390
|
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10116
|
-
|
|
10391
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10117
10392
|
} catch {
|
|
10118
10393
|
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10119
10394
|
}
|
|
10120
|
-
const data = RuntimeProfilesBackupSchema.parse(
|
|
10395
|
+
const data = RuntimeProfilesBackupSchema.parse(input2);
|
|
10121
10396
|
const format = await resolveOutput(restore, opts);
|
|
10122
10397
|
const { api, tsgId } = await context();
|
|
10123
10398
|
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
@@ -10128,7 +10403,8 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10128
10403
|
namePrefix: opts.namePrefix,
|
|
10129
10404
|
onConflict: opts.onConflict,
|
|
10130
10405
|
maxPages: opts.maxPages,
|
|
10131
|
-
dlpMap: mappings
|
|
10406
|
+
dlpMap: mappings,
|
|
10407
|
+
onMissingDlp: opts.onMissingDlp
|
|
10132
10408
|
});
|
|
10133
10409
|
const summary = {
|
|
10134
10410
|
sourceTsgId: plan.sourceTsgId,
|
|
@@ -10142,7 +10418,9 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10142
10418
|
dlpMappings: plan.dlpMappings.map((m) => ({
|
|
10143
10419
|
source: m.source,
|
|
10144
10420
|
destination: m.destination
|
|
10145
|
-
}))
|
|
10421
|
+
})),
|
|
10422
|
+
dlpFallbacks: plan.dlpFallbacks,
|
|
10423
|
+
serverDefaults: plan.serverDefaults
|
|
10146
10424
|
};
|
|
10147
10425
|
const columns = [
|
|
10148
10426
|
{ key: "sourceTsgId", label: "Source TSG" },
|
|
@@ -10150,22 +10428,30 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10150
10428
|
{ key: "profiles", label: "Profiles" },
|
|
10151
10429
|
{ key: "topics", label: "Topics" },
|
|
10152
10430
|
{ key: "dlpMappings", label: "DLP mappings" },
|
|
10431
|
+
{ key: "dlpFallbacks", label: "Basic DLP fallbacks (custom protection lost)" },
|
|
10432
|
+
{ key: "serverDefaults", label: "Verified server-added defaults" },
|
|
10153
10433
|
{ key: "dryRun", label: "Dry run" },
|
|
10154
10434
|
{ key: "complete", label: "Complete" }
|
|
10155
10435
|
];
|
|
10436
|
+
for (const fallback of plan.dlpFallbacks)
|
|
10437
|
+
ui.warning(
|
|
10438
|
+
`DLP protection change: ${fallback.profile} will use Basic detection instead of ${fallback.replaced.join(", ")}. Custom rules are not preserved.`
|
|
10439
|
+
);
|
|
10156
10440
|
if (opts.dryRun) {
|
|
10157
|
-
console.log(
|
|
10441
|
+
console.log(renderRestore(summary, columns, format));
|
|
10158
10442
|
return;
|
|
10159
10443
|
}
|
|
10160
|
-
const changing = plan.profiles.filter(
|
|
10444
|
+
const changing = plan.profiles.filter(
|
|
10445
|
+
(p) => p.action === "create" || p.action === "update"
|
|
10446
|
+
);
|
|
10161
10447
|
if (changing.length)
|
|
10162
10448
|
await confirmOrAbort(
|
|
10163
|
-
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates)?`,
|
|
10449
|
+
`Restore ${changing.length} profiles from TSG ${plan.sourceTsgId} to TSG ${tsgId} (${changing.filter((p) => p.action === "update").length} updates, ${plan.dlpFallbacks.length} Basic DLP fallbacks losing custom detection)?`,
|
|
10164
10450
|
Boolean(opts.force),
|
|
10165
10451
|
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10166
10452
|
);
|
|
10167
10453
|
const result = await restoreRuntimeProfiles(api, plan);
|
|
10168
|
-
console.log(
|
|
10454
|
+
console.log(renderRestore({ ...result }, columns, format));
|
|
10169
10455
|
if (!result.complete) {
|
|
10170
10456
|
ui.error(result.error ?? "Restore incomplete");
|
|
10171
10457
|
process.exitCode = 1;
|
|
@@ -10608,26 +10894,26 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10608
10894
|
}
|
|
10609
10895
|
|
|
10610
10896
|
// src/cli/commands/topics-apply.ts
|
|
10611
|
-
async function applyTopicToProfile(mgmt,
|
|
10897
|
+
async function applyTopicToProfile(mgmt, input2) {
|
|
10612
10898
|
const allTopics = await mgmt.listTopics();
|
|
10613
|
-
const match = allTopics.find((t) => t.topic_name ===
|
|
10899
|
+
const match = allTopics.find((t) => t.topic_name === input2.topicName);
|
|
10614
10900
|
if (!match || !match.topic_id) {
|
|
10615
|
-
throw new Error(`Topic "${
|
|
10901
|
+
throw new Error(`Topic "${input2.topicName}" not found. Create it first with "topics create".`);
|
|
10616
10902
|
}
|
|
10617
|
-
const existing = await mgmt.getProfileTopics(
|
|
10618
|
-
const merged = existing.filter((t) => t.topicName !==
|
|
10903
|
+
const existing = await mgmt.getProfileTopics(input2.profileName);
|
|
10904
|
+
const merged = existing.filter((t) => t.topicName !== input2.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
|
|
10619
10905
|
merged.push({
|
|
10620
10906
|
topicId: match.topic_id,
|
|
10621
10907
|
topicName: match.topic_name,
|
|
10622
|
-
action:
|
|
10908
|
+
action: input2.intent
|
|
10623
10909
|
});
|
|
10624
|
-
const guardrailAction =
|
|
10625
|
-
await mgmt.assignTopicsToProfile(
|
|
10910
|
+
const guardrailAction = input2.intent === "block" ? "allow" : "block";
|
|
10911
|
+
await mgmt.assignTopicsToProfile(input2.profileName, merged, guardrailAction);
|
|
10626
10912
|
return {
|
|
10627
10913
|
topicId: match.topic_id,
|
|
10628
10914
|
topicName: match.topic_name,
|
|
10629
|
-
profileName:
|
|
10630
|
-
intent:
|
|
10915
|
+
profileName: input2.profileName,
|
|
10916
|
+
intent: input2.intent
|
|
10631
10917
|
};
|
|
10632
10918
|
}
|
|
10633
10919
|
function registerApplyCommand(parent) {
|
|
@@ -10672,21 +10958,21 @@ function registerApplyCommand(parent) {
|
|
|
10672
10958
|
}
|
|
10673
10959
|
|
|
10674
10960
|
// src/cli/commands/topics-create.ts
|
|
10675
|
-
async function createOrUpdateTopic(mgmt,
|
|
10676
|
-
const topic = { name:
|
|
10961
|
+
async function createOrUpdateTopic(mgmt, input2) {
|
|
10962
|
+
const topic = { name: input2.name, description: input2.description, examples: input2.examples };
|
|
10677
10963
|
const errors = validateTopic(topic);
|
|
10678
10964
|
if (errors.length > 0) {
|
|
10679
10965
|
throw new Error(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
|
|
10680
10966
|
}
|
|
10681
10967
|
const existing = await mgmt.listTopics();
|
|
10682
|
-
const match = existing.find((t) => t.topic_name ===
|
|
10968
|
+
const match = existing.find((t) => t.topic_name === input2.name);
|
|
10683
10969
|
const request = {
|
|
10684
|
-
topic_name:
|
|
10685
|
-
description:
|
|
10686
|
-
examples:
|
|
10970
|
+
topic_name: input2.name,
|
|
10971
|
+
description: input2.description,
|
|
10972
|
+
examples: input2.examples
|
|
10687
10973
|
};
|
|
10688
10974
|
if (match && !match.topic_id) {
|
|
10689
|
-
throw new Error(`Existing topic '${
|
|
10975
|
+
throw new Error(`Existing topic '${input2.name}' has no topic_id`);
|
|
10690
10976
|
}
|
|
10691
10977
|
const result = match ? await mgmt.updateTopic(match.topic_id, request) : await mgmt.createTopic(request);
|
|
10692
10978
|
if (!result.topic_id) {
|
|
@@ -11843,7 +12129,139 @@ function registerRuntimeCommand(program) {
|
|
|
11843
12129
|
registerDlpCommands(runtime);
|
|
11844
12130
|
}
|
|
11845
12131
|
|
|
12132
|
+
// src/config/tenant-settings.ts
|
|
12133
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
12134
|
+
import { constants } from "fs";
|
|
12135
|
+
import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
12136
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
12137
|
+
var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
12138
|
+
function validateTenantSettingKey(key) {
|
|
12139
|
+
if (!Object.hasOwn(ConfigSchema.shape, key))
|
|
12140
|
+
throw new Error(`Unknown configuration key. Supported keys: ${TENANT_CONFIG_KEYS.join(", ")}`);
|
|
12141
|
+
}
|
|
12142
|
+
function isTenantSecret(key) {
|
|
12143
|
+
return /(?:key|secret|token|password)$/i.test(key);
|
|
12144
|
+
}
|
|
12145
|
+
function validateCredentials(config) {
|
|
12146
|
+
for (const key of ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"]) {
|
|
12147
|
+
if (typeof config[key] !== "string" || !config[key].trim())
|
|
12148
|
+
throw new Error(
|
|
12149
|
+
"Tenant config requires nonempty mgmtTsgId, mgmtClientId and mgmtClientSecret"
|
|
12150
|
+
);
|
|
12151
|
+
}
|
|
12152
|
+
}
|
|
12153
|
+
async function createManagedTenant(name, config) {
|
|
12154
|
+
validateTenantName(name);
|
|
12155
|
+
for (const key of Object.keys(config)) validateTenantSettingKey(key);
|
|
12156
|
+
const parsed = ConfigSchema.safeParse(config);
|
|
12157
|
+
if (!parsed.success) throw new Error("Invalid tenant configuration");
|
|
12158
|
+
validateCredentials(config);
|
|
12159
|
+
if (readTenantStore().tenants.some((entry) => entry.name === name))
|
|
12160
|
+
throw new Error("Tenant name already exists");
|
|
12161
|
+
const directory = join4(dirname4(tenantStorePath()), "configs");
|
|
12162
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
12163
|
+
const path3 = join4(directory, `${name}-${randomUUID10()}.json`);
|
|
12164
|
+
const file = await open2(path3, "wx", 384);
|
|
12165
|
+
try {
|
|
12166
|
+
const data = Object.fromEntries(
|
|
12167
|
+
Object.keys(config).map((key) => [key, Reflect.get(parsed.data, key)])
|
|
12168
|
+
);
|
|
12169
|
+
await file.writeFile(`${JSON.stringify(data, null, 2)}
|
|
12170
|
+
`);
|
|
12171
|
+
await file.sync();
|
|
12172
|
+
await file.close();
|
|
12173
|
+
return await createTenant(name, path3);
|
|
12174
|
+
} catch {
|
|
12175
|
+
await file.close();
|
|
12176
|
+
await unlink2(path3);
|
|
12177
|
+
throw new Error("Could not create tenant configuration; registration was not completed");
|
|
12178
|
+
}
|
|
12179
|
+
}
|
|
12180
|
+
async function setTenantSetting(name, key, value) {
|
|
12181
|
+
validateTenantSettingKey(key);
|
|
12182
|
+
const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
|
|
12183
|
+
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
12184
|
+
if (key === "mgmtTsgId" && value !== entry.tsgId)
|
|
12185
|
+
throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
|
|
12186
|
+
const path3 = entry.configPath;
|
|
12187
|
+
const lockPath = `${path3}.lock`;
|
|
12188
|
+
const lock = await open2(lockPath, "wx", 384).catch(() => {
|
|
12189
|
+
throw new Error(
|
|
12190
|
+
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
12191
|
+
);
|
|
12192
|
+
});
|
|
12193
|
+
const temporary = join4(dirname4(path3), `.airs-config-${randomUUID10()}.tmp`);
|
|
12194
|
+
let created = false;
|
|
12195
|
+
try {
|
|
12196
|
+
const stat2 = await lstat6(path3);
|
|
12197
|
+
if (!stat2.isFile() || (stat2.mode & 146) === 0)
|
|
12198
|
+
throw new Error("Tenant config is not a writable regular file");
|
|
12199
|
+
await access(path3, constants.W_OK);
|
|
12200
|
+
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
12201
|
+
const result = ConfigSchema.safeParse({ ...current, [key]: value });
|
|
12202
|
+
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
12203
|
+
const next = { ...current, [key]: Reflect.get(result.data, key) };
|
|
12204
|
+
validateCredentials(next);
|
|
12205
|
+
const file = await open2(temporary, "wx", 384);
|
|
12206
|
+
created = true;
|
|
12207
|
+
try {
|
|
12208
|
+
await file.writeFile(`${JSON.stringify(next, null, 2)}
|
|
12209
|
+
`);
|
|
12210
|
+
await file.sync();
|
|
12211
|
+
} finally {
|
|
12212
|
+
await file.close();
|
|
12213
|
+
}
|
|
12214
|
+
await rename2(temporary, path3);
|
|
12215
|
+
created = false;
|
|
12216
|
+
} finally {
|
|
12217
|
+
if (created) await unlink2(temporary).catch(() => {
|
|
12218
|
+
});
|
|
12219
|
+
await lock.close();
|
|
12220
|
+
await unlink2(lockPath);
|
|
12221
|
+
}
|
|
12222
|
+
}
|
|
12223
|
+
|
|
12224
|
+
// src/cli/tenant-input.ts
|
|
12225
|
+
import { input, password } from "@inquirer/prompts";
|
|
12226
|
+
async function promptTenantValue(message, secret = false) {
|
|
12227
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
12228
|
+
throw new Error(
|
|
12229
|
+
"Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
|
|
12230
|
+
);
|
|
12231
|
+
const prompt = secret ? password : input;
|
|
12232
|
+
return prompt(
|
|
12233
|
+
{
|
|
12234
|
+
message,
|
|
12235
|
+
validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
|
|
12236
|
+
},
|
|
12237
|
+
{ input: process.stdin, output: process.stderr }
|
|
12238
|
+
);
|
|
12239
|
+
}
|
|
12240
|
+
async function readTenantStdin() {
|
|
12241
|
+
if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
|
|
12242
|
+
const chunks = [];
|
|
12243
|
+
let size = 0;
|
|
12244
|
+
for await (const chunk of process.stdin) {
|
|
12245
|
+
const buffer = Buffer.from(chunk);
|
|
12246
|
+
size += buffer.length;
|
|
12247
|
+
if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
|
|
12248
|
+
chunks.push(buffer);
|
|
12249
|
+
}
|
|
12250
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
12251
|
+
if (!value.trim() || /[\r\n\0]/.test(value))
|
|
12252
|
+
throw new Error("Provide one nonempty configuration value on stdin");
|
|
12253
|
+
return value;
|
|
12254
|
+
}
|
|
12255
|
+
|
|
11846
12256
|
// src/cli/commands/tenant.ts
|
|
12257
|
+
function tenantInputFailure(error) {
|
|
12258
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
12259
|
+
ui.status("Cancelled; no configuration saved.");
|
|
12260
|
+
process.exitCode = 130;
|
|
12261
|
+
return;
|
|
12262
|
+
}
|
|
12263
|
+
fail(error);
|
|
12264
|
+
}
|
|
11847
12265
|
var COLUMNS2 = [
|
|
11848
12266
|
{ key: "name", label: "Tenant" },
|
|
11849
12267
|
{ key: "active", label: "Selected" },
|
|
@@ -11851,25 +12269,60 @@ var COLUMNS2 = [
|
|
|
11851
12269
|
{ key: "configPath", label: "Config file" }
|
|
11852
12270
|
];
|
|
11853
12271
|
function registerTenantCommand(program) {
|
|
11854
|
-
const tenant = program.command("tenant").description("
|
|
12272
|
+
const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
|
|
11855
12273
|
"after",
|
|
11856
12274
|
examples(
|
|
12275
|
+
"airs tenant create development",
|
|
12276
|
+
"airs tenant set development defaultOutput yaml",
|
|
11857
12277
|
"airs tenant create production --config /secure/production.json",
|
|
11858
12278
|
"airs tenant switch production",
|
|
11859
12279
|
"airs tenant read",
|
|
11860
12280
|
"airs tenant switch default"
|
|
11861
12281
|
)
|
|
11862
12282
|
);
|
|
11863
|
-
tenant.command("create <name>").description(
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
12283
|
+
tenant.command("create <name>").description(
|
|
12284
|
+
"Create a config with guided prompts, or register an existing JSON file; does not activate it"
|
|
12285
|
+
).option("--config <path>", "Existing Prisma AIRS JSON config file (no copying or editing)").option("--tsg-id <id>", "Tenant service group ID for a new config").option("--client-id <id>", "OAuth client ID for a new config").option("--client-secret-stdin", "Read the new config OAuth secret from piped stdin").action(
|
|
12286
|
+
async (name, opts) => {
|
|
12287
|
+
try {
|
|
12288
|
+
validateTenantName(name);
|
|
12289
|
+
if (opts.config && (opts.tsgId !== void 0 || opts.clientId !== void 0 || opts.clientSecretStdin))
|
|
12290
|
+
throw new Error("--config cannot be combined with new-config options");
|
|
12291
|
+
if (!opts.config && readTenantStore().tenants.some((entry2) => entry2.name === name))
|
|
12292
|
+
throw new Error("Tenant name already exists");
|
|
12293
|
+
const entry = opts.config ? await createTenant(name, opts.config) : await createManagedTenant(name, {
|
|
12294
|
+
mgmtTsgId: opts.tsgId ?? await promptTenantValue("Tenant service group ID (mgmtTsgId):"),
|
|
12295
|
+
mgmtClientId: opts.clientId ?? await promptTenantValue("OAuth client ID (mgmtClientId):"),
|
|
12296
|
+
mgmtClientSecret: opts.clientSecretStdin ? await readTenantStdin() : await promptTenantValue("OAuth client secret (mgmtClientSecret):", true)
|
|
12297
|
+
});
|
|
12298
|
+
ui.success(
|
|
12299
|
+
`Registered ${entry.name} (TSG ${entry.tsgId}); ${opts.config ? "config file unchanged" : "private config created"}. Use airs tenant switch ${entry.name}.`
|
|
12300
|
+
);
|
|
12301
|
+
} catch (error) {
|
|
12302
|
+
tenantInputFailure(error);
|
|
12303
|
+
}
|
|
11871
12304
|
}
|
|
11872
|
-
|
|
12305
|
+
);
|
|
12306
|
+
tenant.command("set <name> <key> [value]").description("Update one named tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
|
|
12307
|
+
async (name, key, value, opts) => {
|
|
12308
|
+
try {
|
|
12309
|
+
validateTenantSettingKey(key);
|
|
12310
|
+
if (!readTenantStore().tenants.some((entry) => entry.name === name))
|
|
12311
|
+
throw new Error("Tenant not found; register a named tenant first");
|
|
12312
|
+
if (opts.stdin && value !== void 0)
|
|
12313
|
+
throw new Error("Use a value or --stdin, not both");
|
|
12314
|
+
if (isTenantSecret(key) && value !== void 0)
|
|
12315
|
+
throw new Error(
|
|
12316
|
+
"Do not pass credentials as arguments. Omit the value for a hidden prompt, or use --stdin."
|
|
12317
|
+
);
|
|
12318
|
+
const setting = opts.stdin ? await readTenantStdin() : value ?? await promptTenantValue(`${key}:`, isTenantSecret(key));
|
|
12319
|
+
await setTenantSetting(name, key, setting);
|
|
12320
|
+
ui.success(`Updated ${key} for tenant ${name}; selection unchanged.`);
|
|
12321
|
+
} catch (error) {
|
|
12322
|
+
tenantInputFailure(error);
|
|
12323
|
+
}
|
|
12324
|
+
}
|
|
12325
|
+
);
|
|
11873
12326
|
tenant.command("switch <name>").description(
|
|
11874
12327
|
"Persist the selected tenant; default restores legacy config/environment resolution"
|
|
11875
12328
|
).action(async (name) => {
|
|
@@ -12005,8 +12458,8 @@ function applySortedHelp(cmd) {
|
|
|
12005
12458
|
for (const sub of cmd.commands) applySortedHelp(sub);
|
|
12006
12459
|
}
|
|
12007
12460
|
function buildProgram() {
|
|
12008
|
-
const here =
|
|
12009
|
-
const pkg = JSON.parse(readFileSync4(
|
|
12461
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
12462
|
+
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
12010
12463
|
const program = new Command();
|
|
12011
12464
|
program.name("airs").description(
|
|
12012
12465
|
"CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
|
|
@@ -12035,9 +12488,9 @@ function buildProgram() {
|
|
|
12035
12488
|
}
|
|
12036
12489
|
}
|
|
12037
12490
|
if (root.debug) {
|
|
12038
|
-
const logPath =
|
|
12491
|
+
const logPath = join5(
|
|
12039
12492
|
process.cwd(),
|
|
12040
|
-
`debug-api-${Date.now()}-${
|
|
12493
|
+
`debug-api-${Date.now()}-${randomUUID11().slice(0, 8)}.jsonl`
|
|
12041
12494
|
);
|
|
12042
12495
|
try {
|
|
12043
12496
|
let ancestor = actionCommand;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs-cli",
|
|
3
3
|
"packageManager": "pnpm@10.6.5",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.7.0",
|
|
5
5
|
"description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"license": "MIT",
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@cdot65/prisma-airs-sdk": "0.
|
|
62
|
+
"@cdot65/prisma-airs-sdk": "0.30.0",
|
|
63
63
|
"@inquirer/prompts": "^8.3.0",
|
|
64
64
|
"chalk": "^5.6.2",
|
|
65
65
|
"commander": "^14.0.3",
|