@cdot65/prisma-airs-cli 5.8.4 → 5.9.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +78 -20
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -8870,6 +8870,17 @@ async function parseBody(opts) {
8870
8870
  }
8871
8871
  }
8872
8872
 
8873
+ // src/cli/commands/dlp/visibility.ts
8874
+ function predefinedFlag(cmd) {
8875
+ return cmd.option(
8876
+ "--include-predefined",
8877
+ "Include predefined (PANW-shipped) records; listings show only tenant-created records by default"
8878
+ );
8879
+ }
8880
+ function visibleRecords(records, includePredefined) {
8881
+ return includePredefined ? records : records.filter((record) => record.type !== "predefined");
8882
+ }
8883
+
8873
8884
  // src/cli/commands/dlp/dictionaries.ts
8874
8885
  async function buildMetadata(opts) {
8875
8886
  if (opts.metadataFile) {
@@ -8894,6 +8905,7 @@ function register(dlp) {
8894
8905
  "Starting offset \u2014 rounds down to a page boundary",
8895
8906
  (v) => Number.parseInt(v, 10)
8896
8907
  ).option("--sort <field,dir>", "(repeatable)", (v, p = []) => [...p, v]).option("--keywords", "Include keyword list in response").option("--include-keywords", "Alias for --keywords").option("--output <fmt>", "Output format: pretty, table, markdown, csv, json, yaml");
8908
+ predefinedFlag(listCmd);
8897
8909
  registerPageAliases(listCmd, { sizeFlag: "--size", sizeKey: "size" });
8898
8910
  listCmd.action(async (opts) => {
8899
8911
  try {
@@ -8905,9 +8917,10 @@ function register(dlp) {
8905
8917
  sort: opts.sort,
8906
8918
  keywords: includeKeywords ? true : void 0
8907
8919
  };
8908
- const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : void 0;
8920
+ const all = opts.all ? await svc.listAll({ ...params, max: Number(opts.max) }) : (await svc.list({ ...params, page })).content;
8921
+ const visible = visibleRecords(all, opts.includePredefined);
8909
8922
  dlpDictionaries.renderList(
8910
- all ? { content: all, totalElements: all.length } : await svc.list({ ...params, page }),
8923
+ { content: visible, totalElements: visible.length },
8911
8924
  await resolveOutput(listCmd, opts)
8912
8925
  );
8913
8926
  } catch (err) {
@@ -9321,14 +9334,17 @@ async function resolveWriteBody(opts) {
9321
9334
  }
9322
9335
  function register4(dlp) {
9323
9336
  const group = dlp.command("patterns").description("DLP data patterns (full CRUD)");
9324
- const listCmd = listFlags2(group.command("list").description("List data patterns"));
9337
+ const listCmd = predefinedFlag(
9338
+ listFlags2(group.command("list").description("List data patterns (tenant-created by default)"))
9339
+ );
9325
9340
  listCmd.action(async (opts) => {
9326
9341
  try {
9327
9342
  const { page, size } = resolvePageParams(listCmd, opts);
9328
9343
  const svc = new SdkDataPatternsService(await loadDlpClientOptions());
9329
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9344
+ const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
9345
+ const visible = visibleRecords(result, opts.includePredefined);
9330
9346
  dlpPatterns.renderList(
9331
- result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
9347
+ { content: visible, totalElements: visible.length },
9332
9348
  await resolveOutput(listCmd, opts)
9333
9349
  );
9334
9350
  } catch (err) {
@@ -9467,14 +9483,17 @@ function register5(dlp) {
9467
9483
  const group = dlp.command("profiles").description(
9468
9484
  "DLP data profiles. No supported DELETE; status-based retirement is not live-verified."
9469
9485
  );
9470
- const listCmd = listFlags3(group.command("list").description("List data profiles"));
9486
+ const listCmd = predefinedFlag(
9487
+ listFlags3(group.command("list").description("List data profiles (tenant-created by default)"))
9488
+ );
9471
9489
  listCmd.action(async (opts) => {
9472
9490
  try {
9473
9491
  const { page, size } = resolvePageParams(listCmd, opts);
9474
9492
  const svc = new SdkDataProfilesService(await loadDlpClientOptions());
9475
- const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : void 0;
9493
+ const result = opts.all ? await svc.listAll({ size, sort: opts.sort, max: Number(opts.max) }) : (await svc.list({ page, size, sort: opts.sort })).content;
9494
+ const visible = visibleRecords(result, opts.includePredefined);
9476
9495
  dlpProfiles.renderList(
9477
- result ? { content: result, totalElements: result.length } : await svc.list({ page, size, sort: opts.sort }),
9496
+ { content: visible, totalElements: visible.length },
9478
9497
  await resolveOutput(listCmd, opts)
9479
9498
  );
9480
9499
  } catch (err) {
@@ -9719,11 +9738,19 @@ async function backupDlpResources(api, tsgId, options = {}) {
9719
9738
  if (!record.id) throw new Error("Inventory dictionary has no id");
9720
9739
  if (record.type !== "predefined" && !Array.isArray(record.keywords))
9721
9740
  throw new Error(`Dictionary keywords were not returned by the region: ${record.name}`);
9722
- dictionaries.set(record.id, structuredClone(record));
9741
+ const stored = record.type === "predefined" ? { id: record.id, name: record.name, type: record.type } : record;
9742
+ dictionaries.set(record.id, structuredClone(stored));
9723
9743
  };
9724
9744
  const addPattern = (record) => {
9725
9745
  if (!record.id) throw new Error("Inventory data pattern has no id");
9726
- patterns.set(record.id, structuredClone(record));
9746
+ const stored = record.type === "predefined" ? {
9747
+ id: record.id,
9748
+ name: record.name,
9749
+ type: record.type,
9750
+ ...record.version != null ? { version: record.version } : {},
9751
+ ...record.detection_config?.technique ? { detection_config: { technique: record.detection_config.technique } } : {}
9752
+ } : record;
9753
+ patterns.set(record.id, structuredClone(stored));
9727
9754
  };
9728
9755
  if (kinds.includes("dictionaries")) {
9729
9756
  for (const record of inventory2.dictionaries)
@@ -9744,6 +9771,10 @@ async function backupDlpResources(api, tsgId, options = {}) {
9744
9771
  if (RETIRED_PROFILE.has(profile.profile_status ?? "active")) continue;
9745
9772
  const name = profile.name ?? "(unnamed)";
9746
9773
  try {
9774
+ if (!profile.detection_rules?.length)
9775
+ throw new Error(
9776
+ "has no exported detection rules (the API does not expose basic data profile content)"
9777
+ );
9747
9778
  for (const leaf of ruleLeaves(profile)) {
9748
9779
  const id = leaf.value.id;
9749
9780
  if (id == null || id === "") {
@@ -9761,6 +9792,8 @@ async function backupDlpResources(api, tsgId, options = {}) {
9761
9792
  } else {
9762
9793
  const record = patternById.get(id);
9763
9794
  if (!record) throw new Error(`references an unknown data pattern: ${id}`);
9795
+ if (RETIRED_PATTERN.has(record.status ?? "active"))
9796
+ throw new Error(`references a retired data pattern: ${record.name ?? id}`);
9764
9797
  if (leaf.value.version != null && record.version !== leaf.value.version)
9765
9798
  throw new Error(
9766
9799
  `references data pattern version ${leaf.value.version}, but the catalog holds version ${record.version ?? "unknown"}: ${id}`
@@ -9814,6 +9847,10 @@ function compareEcho(request, readBack) {
9814
9847
  differences.push(path3);
9815
9848
  return;
9816
9849
  }
9850
+ if (path3.endsWith(".supported_confidence_levels") && Array.isArray(a) && Array.isArray(b) && a.every((level) => b.includes(level))) {
9851
+ if (a.length !== b.length) serverAdded.push(path3);
9852
+ return;
9853
+ }
9817
9854
  if (Array.isArray(a) || Array.isArray(b)) {
9818
9855
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
9819
9856
  differences.push(path3);
@@ -9885,14 +9922,15 @@ function normalizedName(value) {
9885
9922
  }
9886
9923
  function predefinedCandidates(source, records) {
9887
9924
  const wanted = normalizedName(source.name ?? "");
9888
- const wantedTokens = new Set(wanted.split(" "));
9889
- return records.filter(
9890
- (record) => record.type === "predefined" && record.name != null && !RETIRED_PATTERN.has(record.status ?? "active") && record.detection_config?.technique === source.detection_config?.technique
9891
- ).map((record) => {
9925
+ const scored = /* @__PURE__ */ new Map();
9926
+ for (const record of records) {
9927
+ if (record.type !== "predefined" || record.name == null || RETIRED_PATTERN.has(record.status ?? "active") || record.detection_config?.technique !== source.detection_config?.technique)
9928
+ continue;
9892
9929
  const name = normalizedName(record.name);
9893
- const score = name === wanted ? 3 : name.includes(wanted) || wanted.includes(name) ? 2 : name.split(" ").some((token) => wantedTokens.has(token)) ? 1 : 0;
9894
- return { name: record.name, score };
9895
- }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 3).map((item) => item.name);
9930
+ const score = name === wanted ? 2 : name.includes(wanted) || wanted.includes(name) ? 1 : 0;
9931
+ if (score > 0) scored.set(record.name, Math.max(scored.get(record.name) ?? 0, score));
9932
+ }
9933
+ return [...scored.entries()].sort(([aName, aScore], [bName, bScore]) => bScore - aScore || aName.localeCompare(bName)).slice(0, 3).map(([name]) => name);
9896
9934
  }
9897
9935
  function matchByName(records, name, retired, label) {
9898
9936
  const matches = records.filter((record) => record.name === name && !retired(record));
@@ -10040,6 +10078,20 @@ async function planDlpResourcesRestore(api, input2, destinationTsgId, options =
10040
10078
  `Data pattern uses a tenant-bound detection technique: ${source.name}; provision it in the destination and bind it with --pattern-map "${source.name}=<destination-name>"`
10041
10079
  );
10042
10080
  }
10081
+ if (["regex", "weighted_regex"].includes(source.detection_config?.technique ?? "") && !(Array.isArray(source.matching_rules?.regexes) && source.matching_rules.regexes.length)) {
10082
+ if (options.skipUnresolved) {
10083
+ unresolved.push({
10084
+ kind: "pattern",
10085
+ sourceId: source.id,
10086
+ name: source.name,
10087
+ reason: "detection content is not exported by the API (copy of a predefined pattern)"
10088
+ });
10089
+ continue;
10090
+ }
10091
+ throw new Error(
10092
+ `Data pattern has no exported detection content (a copy of a predefined pattern): ${source.name}; bind an equivalent with --pattern-map "${source.name}=<destination-name>" or exclude dependents with --skip-unresolved`
10093
+ );
10094
+ }
10043
10095
  const name = `${prefix}${source.name}`;
10044
10096
  assertName(name);
10045
10097
  const body = patternRequest(source, name);
@@ -10262,7 +10314,9 @@ async function restoreDlpResources(api, plan, options = {}) {
10262
10314
  const readBack = await api.dictionaries.get(created.id, { includeKeywords: true });
10263
10315
  const echo = dictionaryEcho(item.metadata, readBack, item.keywords);
10264
10316
  if (!echo.matches || readBack.name !== item.name)
10265
- throw new Error(`Restored dictionary did not verify: ${item.name}`);
10317
+ throw new Error(
10318
+ `Restored dictionary did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10319
+ );
10266
10320
  if (echo.serverAdded.length)
10267
10321
  result.serverAdded.push({ resource: `dictionary ${item.name}`, fields: echo.serverAdded });
10268
10322
  bindings.set(item.sourceId, { id: created.id, name: item.name });
@@ -10297,7 +10351,9 @@ async function restoreDlpResources(api, plan, options = {}) {
10297
10351
  const readBack = await api.patterns.get(created.id);
10298
10352
  const echo = compareEcho(item.body, readBack);
10299
10353
  if (!echo.matches || readBack.name !== item.name || RETIRED_PATTERN.has(readBack.status ?? "active"))
10300
- throw new Error(`Restored data pattern did not verify: ${item.name}`);
10354
+ throw new Error(
10355
+ `Restored data pattern did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10356
+ );
10301
10357
  if (echo.serverAdded.length)
10302
10358
  result.serverAdded.push({
10303
10359
  resource: `data pattern ${item.name}`,
@@ -10348,7 +10404,9 @@ async function restoreDlpResources(api, plan, options = {}) {
10348
10404
  const readBack = await api.profiles.get(created.id);
10349
10405
  const echo = compareEcho(body, readBack);
10350
10406
  if (!echo.matches || readBack.name !== item.name || RETIRED_PROFILE.has(readBack.profile_status ?? "active"))
10351
- throw new Error(`Restored data profile did not verify: ${item.name}`);
10407
+ throw new Error(
10408
+ `Restored data profile did not verify: ${item.name} (created id ${created.id}; inspect it before retrying)`
10409
+ );
10352
10410
  if (echo.serverAdded.length)
10353
10411
  result.serverAdded.push({
10354
10412
  resource: `data profile ${item.name}`,
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.8.4",
4
+ "version": "5.9.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",