@cdot65/prisma-airs-cli 5.6.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/dist/cli/index.js +340 -57
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -522,6 +522,10 @@ ${INDENT}${chalk3.bold(label)}
|
|
|
522
522
|
warn(msg) {
|
|
523
523
|
console.log(`${INDENT}${COLORS.warn(`${GLYPHS.warn} ${msg}`)}`);
|
|
524
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
|
+
},
|
|
525
529
|
/** Neutral informational callout. Quiet-suppressed. */
|
|
526
530
|
info(msg) {
|
|
527
531
|
if (quietMode) return;
|
|
@@ -9551,6 +9555,102 @@ import {
|
|
|
9551
9555
|
SecurityProfileSchema
|
|
9552
9556
|
} from "@cdot65/prisma-airs-sdk";
|
|
9553
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
|
|
9554
9654
|
var RuntimeProfilesBackupSchema = z5.object({
|
|
9555
9655
|
version: z5.literal(1),
|
|
9556
9656
|
resourceType: z5.literal("runtime-security-profiles"),
|
|
@@ -9559,7 +9659,7 @@ var RuntimeProfilesBackupSchema = z5.object({
|
|
|
9559
9659
|
profiles: z5.array(SecurityProfileSchema).min(1).max(1e4),
|
|
9560
9660
|
topics: z5.array(CustomTopicSchema).max(1e4)
|
|
9561
9661
|
}).strict();
|
|
9562
|
-
function
|
|
9662
|
+
function object2(value) {
|
|
9563
9663
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9564
9664
|
}
|
|
9565
9665
|
function pagesLimit(value) {
|
|
@@ -9626,7 +9726,7 @@ function topicReferences(policy) {
|
|
|
9626
9726
|
value.forEach(visit);
|
|
9627
9727
|
return;
|
|
9628
9728
|
}
|
|
9629
|
-
if (!
|
|
9729
|
+
if (!object2(value)) return;
|
|
9630
9730
|
if ("topic_id" in value) {
|
|
9631
9731
|
if (typeof value.topic_id !== "string" || typeof value.topic_name !== "string" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0)
|
|
9632
9732
|
throw new Error("Invalid topic identity in profile policy");
|
|
@@ -9679,26 +9779,48 @@ function sameTopic(a, b) {
|
|
|
9679
9779
|
return a.description === b.description && JSON.stringify(a.examples) === JSON.stringify(b.examples) && (a.active ?? true) === (b.active ?? true);
|
|
9680
9780
|
}
|
|
9681
9781
|
function dlpReferences(policy) {
|
|
9682
|
-
if (!
|
|
9782
|
+
if (!object2(policy)) return [];
|
|
9683
9783
|
const refs = [];
|
|
9684
9784
|
for (const entry of Array.isArray(policy["dlp-data-profiles"]) ? policy["dlp-data-profiles"] : []) {
|
|
9685
|
-
if (!
|
|
9785
|
+
if (!object2(entry) || typeof entry.name !== "string")
|
|
9686
9786
|
throw new Error("Invalid embedded DLP reference");
|
|
9687
9787
|
refs.push({ value: entry, name: entry.name, kind: "embedded" });
|
|
9688
9788
|
}
|
|
9689
9789
|
for (const entry of Array.isArray(policy["ai-security-profiles"]) ? policy["ai-security-profiles"] : []) {
|
|
9690
|
-
if (!
|
|
9790
|
+
if (!object2(entry) || !object2(entry["model-configuration"])) continue;
|
|
9691
9791
|
const protection = entry["model-configuration"]["data-protection"];
|
|
9692
|
-
if (!
|
|
9792
|
+
if (!object2(protection) || !object2(protection["data-leak-detection"])) continue;
|
|
9693
9793
|
const members = protection["data-leak-detection"].member;
|
|
9694
9794
|
if (!Array.isArray(members)) continue;
|
|
9695
9795
|
for (const member of members) {
|
|
9696
|
-
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;
|
|
9697
9799
|
refs.push({ value: member, name: member.text, kind: "member" });
|
|
9698
9800
|
}
|
|
9699
9801
|
}
|
|
9700
9802
|
return refs;
|
|
9701
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
|
+
}
|
|
9702
9824
|
function assertKnownPolicyReferences(policy) {
|
|
9703
9825
|
const known = new Map([
|
|
9704
9826
|
...topicReferences(policy).map((ref) => [ref, ["topic_id"]]),
|
|
@@ -9714,7 +9836,7 @@ function assertKnownPolicyReferences(policy) {
|
|
|
9714
9836
|
});
|
|
9715
9837
|
return;
|
|
9716
9838
|
}
|
|
9717
|
-
if (!
|
|
9839
|
+
if (!object2(value)) return;
|
|
9718
9840
|
for (const [key, child] of Object.entries(value)) {
|
|
9719
9841
|
if (child !== null && child !== "" && /(?:^|[-_])(id|uuid)$|(?:Id|ID|Uuid|UUID)$/.test(key) && !known.get(value)?.includes(key))
|
|
9720
9842
|
throw new Error(`Unsupported cross-tenant policy reference at ${path3}.${key}`);
|
|
@@ -9744,7 +9866,10 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9744
9866
|
if (prefix.length > 64 || [...prefix].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
9745
9867
|
throw new Error("Invalid --name-prefix");
|
|
9746
9868
|
const conflict = options.onConflict ?? "error";
|
|
9747
|
-
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");
|
|
9748
9873
|
const selected = latestProfiles(backup.profiles);
|
|
9749
9874
|
assertTenant(selected, backup.source.tsgId);
|
|
9750
9875
|
if (selected.length !== backup.profiles.length)
|
|
@@ -9766,14 +9891,14 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9766
9891
|
const match = existing.find((p) => p.profile_name === name);
|
|
9767
9892
|
if (match && conflict === "error")
|
|
9768
9893
|
throw new Error(
|
|
9769
|
-
`Destination profile already exists: ${name}; choose --on-conflict skip or update`
|
|
9894
|
+
`Destination profile already exists: ${name}; choose --on-conflict verify, skip or update`
|
|
9770
9895
|
);
|
|
9771
9896
|
if (match && !match.profile_id) throw new Error("Destination profile has no identity");
|
|
9772
9897
|
return {
|
|
9773
9898
|
source: structuredClone(source),
|
|
9774
9899
|
name,
|
|
9775
9900
|
existing: match,
|
|
9776
|
-
action: match ? conflict === "skip" ? "skip" : "update" : "create"
|
|
9901
|
+
action: match ? conflict === "skip" ? "skip" : conflict === "verify" ? "verify" : "update" : "create"
|
|
9777
9902
|
};
|
|
9778
9903
|
});
|
|
9779
9904
|
const changing = profilePlans.filter((p) => p.action !== "skip");
|
|
@@ -9807,46 +9932,102 @@ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options
|
|
|
9807
9932
|
);
|
|
9808
9933
|
topicPlans.set(key, { key, name, source, existing: match });
|
|
9809
9934
|
}
|
|
9810
|
-
const dlpRefs = crossTenant ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9935
|
+
const dlpRefs = crossTenant || missingDlp === "basic" ? changing.flatMap((p) => dlpReferences(p.source.policy)) : [];
|
|
9811
9936
|
const names = [...new Set(dlpRefs.map((ref) => ref.name))];
|
|
9812
|
-
|
|
9813
|
-
|
|
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")
|
|
9814
9941
|
throw new Error(
|
|
9815
|
-
`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.`
|
|
9816
9943
|
);
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
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
|
+
}
|
|
9821
9958
|
if (matches.length !== 1 || !matches[0].id)
|
|
9822
9959
|
throw new Error(`Missing or ambiguous destination DLP profile: ${target}`);
|
|
9823
9960
|
if (dlpRefs.some((ref) => ref.name === name && "version" in ref.value) && matches[0].version == null)
|
|
9824
9961
|
throw new Error(`Destination DLP version is unknown: ${target}`);
|
|
9825
|
-
|
|
9962
|
+
resolved.push({
|
|
9826
9963
|
source: name,
|
|
9827
9964
|
destination: String(target),
|
|
9828
9965
|
id: matches[0].id,
|
|
9829
9966
|
version: matches[0].version ?? void 0
|
|
9830
|
-
};
|
|
9831
|
-
}
|
|
9832
|
-
|
|
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 = {
|
|
9833
9990
|
sourceTsgId: backup.source.tsgId,
|
|
9834
9991
|
destinationTsgId,
|
|
9835
9992
|
profiles: profilePlans,
|
|
9836
9993
|
topics: [...topicPlans.values()],
|
|
9837
9994
|
dlpMappings,
|
|
9995
|
+
dlpFallbacks,
|
|
9996
|
+
serverDefaults: [],
|
|
9838
9997
|
maxPages: maxPages2
|
|
9839
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;
|
|
9840
10013
|
}
|
|
9841
10014
|
function canonical(value) {
|
|
9842
|
-
const order = (v) => Array.isArray(v) ? v.map(order) :
|
|
10015
|
+
const order = (v) => Array.isArray(v) ? v.map(order) : object2(v) ? Object.fromEntries(
|
|
9843
10016
|
Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, child]) => [k, order(child)])
|
|
9844
10017
|
) : v;
|
|
9845
10018
|
return JSON.stringify(order(value));
|
|
9846
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
|
+
}
|
|
9847
10028
|
function unchanged(current, prior) {
|
|
9848
10029
|
return Boolean(
|
|
9849
|
-
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)
|
|
9850
10031
|
);
|
|
9851
10032
|
}
|
|
9852
10033
|
async function restoreRuntimeProfiles(api, plan) {
|
|
@@ -9854,6 +10035,8 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9854
10035
|
sourceTsgId: plan.sourceTsgId,
|
|
9855
10036
|
destinationTsgId: plan.destinationTsgId,
|
|
9856
10037
|
complete: false,
|
|
10038
|
+
dlpFallbacks: structuredClone(plan.dlpFallbacks ?? []),
|
|
10039
|
+
serverDefaults: [],
|
|
9857
10040
|
topics: [],
|
|
9858
10041
|
profiles: []
|
|
9859
10042
|
};
|
|
@@ -9864,7 +10047,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9864
10047
|
const catalog = await dataProfiles(api, plan.maxPages);
|
|
9865
10048
|
for (const mapping of plan.dlpMappings) {
|
|
9866
10049
|
const matches = catalog.filter(
|
|
9867
|
-
(p) => p.name === mapping.destination && p.profile_status !== "deleted"
|
|
10050
|
+
(p) => p.name === mapping.destination && p.profile_status !== "deleted" && p.profile_status !== "disabled"
|
|
9868
10051
|
);
|
|
9869
10052
|
if (matches.length !== 1 || matches[0].id !== mapping.id || (matches[0].version ?? void 0) !== mapping.version)
|
|
9870
10053
|
throw new Error(`Destination DLP profile changed after planning: ${mapping.destination}`);
|
|
@@ -9910,27 +10093,7 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9910
10093
|
result.profiles.push({ name: item.name, action: "skipped", id: item.existing?.profile_id });
|
|
9911
10094
|
continue;
|
|
9912
10095
|
}
|
|
9913
|
-
const policy =
|
|
9914
|
-
for (const ref of topicReferences(policy)) {
|
|
9915
|
-
const topic = mappedTopics.get(topicKey(ref));
|
|
9916
|
-
if (!topic?.topic_id) throw new Error("Missing planned topic mapping");
|
|
9917
|
-
ref.topic_id = topic.topic_id;
|
|
9918
|
-
ref.topic_name = topic.topic_name;
|
|
9919
|
-
ref.revision = topic.revision;
|
|
9920
|
-
}
|
|
9921
|
-
for (const ref of dlpReferences(policy)) {
|
|
9922
|
-
const mapping = plan.dlpMappings.find((m) => m.source === ref.name);
|
|
9923
|
-
if (!mapping) continue;
|
|
9924
|
-
if (ref.kind === "embedded") {
|
|
9925
|
-
ref.value.name = mapping.destination;
|
|
9926
|
-
ref.value.uuid = mapping.id;
|
|
9927
|
-
} else ref.value.text = mapping.destination;
|
|
9928
|
-
if ("id" in ref.value) ref.value.id = mapping.id;
|
|
9929
|
-
if ("version" in ref.value) {
|
|
9930
|
-
if (mapping.version === void 0) throw new Error("Destination DLP version is unknown");
|
|
9931
|
-
ref.value.version = String(mapping.version);
|
|
9932
|
-
}
|
|
9933
|
-
}
|
|
10096
|
+
const policy = mappedPolicy(item, plan, mappedTopics);
|
|
9934
10097
|
const body = CreateSecurityProfileRequestSchema.parse({
|
|
9935
10098
|
profile_name: item.name,
|
|
9936
10099
|
active: item.source.active,
|
|
@@ -9941,6 +10104,14 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9941
10104
|
const current = inventory2.find((p) => p.profile_name === item.name);
|
|
9942
10105
|
if (item.existing ? !unchanged(current, item.existing) : Boolean(current))
|
|
9943
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
|
+
}
|
|
9944
10115
|
const response = item.action === "update" && item.existing?.profile_id ? await api.profiles.update(item.existing.profile_id, body) : await api.profiles.create(body);
|
|
9945
10116
|
result.profiles.push({
|
|
9946
10117
|
name: item.name,
|
|
@@ -9951,8 +10122,11 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9951
10122
|
(p) => p.profile_name === item.name
|
|
9952
10123
|
);
|
|
9953
10124
|
if (verified) assertTenant([verified], plan.destinationTsgId);
|
|
9954
|
-
|
|
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)
|
|
9955
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 });
|
|
9956
10130
|
}
|
|
9957
10131
|
result.complete = true;
|
|
9958
10132
|
} catch (error) {
|
|
@@ -9963,10 +10137,97 @@ async function restoreRuntimeProfiles(api, plan) {
|
|
|
9963
10137
|
}
|
|
9964
10138
|
return result;
|
|
9965
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
|
+
}
|
|
9966
10167
|
|
|
9967
10168
|
// src/cli/commands/profile-transfer.ts
|
|
9968
10169
|
var MAX_BACKUP_BYTES = 20 * 1024 * 1024;
|
|
9969
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
|
+
}
|
|
9970
10231
|
function maxPages(value) {
|
|
9971
10232
|
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1e3)
|
|
9972
10233
|
usageError("--max-pages must be an integer from 1 to 1000");
|
|
@@ -10079,7 +10340,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10079
10340
|
});
|
|
10080
10341
|
const restore = profiles2.command("restore <file>").description(
|
|
10081
10342
|
"Restore profile policies into the selected tenant; validate all dependencies before changes"
|
|
10082
|
-
).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(
|
|
10083
10352
|
"--dlp-map <source=destination>",
|
|
10084
10353
|
"Explicit cross-tenant binding to an existing DLP data profile (repeatable)",
|
|
10085
10354
|
(value, prior) => [...prior, value],
|
|
@@ -10096,12 +10365,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10096
10365
|
"after",
|
|
10097
10366
|
examples(
|
|
10098
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",
|
|
10099
10369
|
"airs runtime profiles restore ./profiles.json --name-prefix migrated- --expect-tsg <destination-tsg> --force"
|
|
10100
10370
|
)
|
|
10101
10371
|
).action(async (file, opts) => {
|
|
10102
10372
|
try {
|
|
10103
|
-
if (!["error", "skip", "update"].includes(opts.onConflict))
|
|
10104
|
-
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");
|
|
10105
10377
|
if (opts.force && !opts.expectTsg)
|
|
10106
10378
|
throw new CliUsageError("--force requires --expect-tsg <destination-tsg>");
|
|
10107
10379
|
const mappings = dlpMap(opts.dlpMap);
|
|
@@ -10131,7 +10403,8 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10131
10403
|
namePrefix: opts.namePrefix,
|
|
10132
10404
|
onConflict: opts.onConflict,
|
|
10133
10405
|
maxPages: opts.maxPages,
|
|
10134
|
-
dlpMap: mappings
|
|
10406
|
+
dlpMap: mappings,
|
|
10407
|
+
onMissingDlp: opts.onMissingDlp
|
|
10135
10408
|
});
|
|
10136
10409
|
const summary = {
|
|
10137
10410
|
sourceTsgId: plan.sourceTsgId,
|
|
@@ -10145,7 +10418,9 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10145
10418
|
dlpMappings: plan.dlpMappings.map((m) => ({
|
|
10146
10419
|
source: m.source,
|
|
10147
10420
|
destination: m.destination
|
|
10148
|
-
}))
|
|
10421
|
+
})),
|
|
10422
|
+
dlpFallbacks: plan.dlpFallbacks,
|
|
10423
|
+
serverDefaults: plan.serverDefaults
|
|
10149
10424
|
};
|
|
10150
10425
|
const columns = [
|
|
10151
10426
|
{ key: "sourceTsgId", label: "Source TSG" },
|
|
@@ -10153,22 +10428,30 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10153
10428
|
{ key: "profiles", label: "Profiles" },
|
|
10154
10429
|
{ key: "topics", label: "Topics" },
|
|
10155
10430
|
{ key: "dlpMappings", label: "DLP mappings" },
|
|
10431
|
+
{ key: "dlpFallbacks", label: "Basic DLP fallbacks (custom protection lost)" },
|
|
10432
|
+
{ key: "serverDefaults", label: "Verified server-added defaults" },
|
|
10156
10433
|
{ key: "dryRun", label: "Dry run" },
|
|
10157
10434
|
{ key: "complete", label: "Complete" }
|
|
10158
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
|
+
);
|
|
10159
10440
|
if (opts.dryRun) {
|
|
10160
|
-
console.log(
|
|
10441
|
+
console.log(renderRestore(summary, columns, format));
|
|
10161
10442
|
return;
|
|
10162
10443
|
}
|
|
10163
|
-
const changing = plan.profiles.filter(
|
|
10444
|
+
const changing = plan.profiles.filter(
|
|
10445
|
+
(p) => p.action === "create" || p.action === "update"
|
|
10446
|
+
);
|
|
10164
10447
|
if (changing.length)
|
|
10165
10448
|
await confirmOrAbort(
|
|
10166
|
-
`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)?`,
|
|
10167
10450
|
Boolean(opts.force),
|
|
10168
10451
|
{ action: `restore profiles into TSG ${tsgId}` }
|
|
10169
10452
|
);
|
|
10170
10453
|
const result = await restoreRuntimeProfiles(api, plan);
|
|
10171
|
-
console.log(
|
|
10454
|
+
console.log(renderRestore({ ...result }, columns, format));
|
|
10172
10455
|
if (!result.complete) {
|
|
10173
10456
|
ui.error(result.error ?? "Restore incomplete");
|
|
10174
10457
|
process.exitCode = 1;
|
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",
|