@cdot65/prisma-airs-cli 5.5.0 → 5.6.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 +222 -52
- package/dist/index.js +1 -1
- package/package.json +1 -1
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
|
|
|
@@ -3684,19 +3687,19 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3684
3687
|
mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
|
|
3685
3688
|
writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
3686
3689
|
const originalFetch = globalThis.fetch;
|
|
3687
|
-
globalThis.fetch = async function debugFetch(
|
|
3688
|
-
const url = typeof
|
|
3690
|
+
globalThis.fetch = async function debugFetch(input2, init2) {
|
|
3691
|
+
const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
|
|
3689
3692
|
const rawHeaders = headersToRecord(
|
|
3690
|
-
init2?.headers ?? (
|
|
3693
|
+
init2?.headers ?? (input2 instanceof Request ? input2.headers : void 0)
|
|
3691
3694
|
);
|
|
3692
3695
|
const inference = Object.keys(rawHeaders).some(
|
|
3693
3696
|
(name) => name.toLowerCase() === "x-portkey-api-key"
|
|
3694
3697
|
);
|
|
3695
3698
|
if (!isAirsUrl(url) && !inference) {
|
|
3696
|
-
return originalFetch(
|
|
3699
|
+
return originalFetch(input2, init2);
|
|
3697
3700
|
}
|
|
3698
3701
|
const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
|
|
3699
|
-
const method = init2?.method ?? (
|
|
3702
|
+
const method = init2?.method ?? (input2 instanceof Request ? input2.method : "GET");
|
|
3700
3703
|
const reqHeaders = redactHeaders(rawHeaders);
|
|
3701
3704
|
const loggedUrl = redactUrl(url);
|
|
3702
3705
|
let reqBody;
|
|
@@ -3715,7 +3718,7 @@ function installDebugLogger(logPath, options = {}) {
|
|
|
3715
3718
|
let resBody;
|
|
3716
3719
|
let error;
|
|
3717
3720
|
try {
|
|
3718
|
-
response = await originalFetch(
|
|
3721
|
+
response = await originalFetch(input2, init2);
|
|
3719
3722
|
} catch (err) {
|
|
3720
3723
|
error = inference ? "Runtime request failed" : "Request failed";
|
|
3721
3724
|
const entry2 = JSON.stringify({
|
|
@@ -6955,8 +6958,8 @@ function buildDefaultCategories(categories) {
|
|
|
6955
6958
|
])
|
|
6956
6959
|
);
|
|
6957
6960
|
}
|
|
6958
|
-
function parseAttackGoals(
|
|
6959
|
-
const trimmed =
|
|
6961
|
+
function parseAttackGoals(input2) {
|
|
6962
|
+
const trimmed = input2.trim();
|
|
6960
6963
|
const raw = trimmed.startsWith("[") ? trimmed : fs2.readFileSync(trimmed, "utf-8");
|
|
6961
6964
|
let parsed;
|
|
6962
6965
|
try {
|
|
@@ -6978,10 +6981,10 @@ function sliceClientSide(items, opts) {
|
|
|
6978
6981
|
const limit = opts.limit !== void 0 ? Number.parseInt(String(opts.limit), 10) : void 0;
|
|
6979
6982
|
return items.slice(offset, limit === void 0 ? void 0 : offset + limit);
|
|
6980
6983
|
}
|
|
6981
|
-
function parsePositiveInt(
|
|
6982
|
-
const n = Number.parseInt(
|
|
6984
|
+
function parsePositiveInt(input2, flag) {
|
|
6985
|
+
const n = Number.parseInt(input2, 10);
|
|
6983
6986
|
if (!Number.isFinite(n) || n <= 0) {
|
|
6984
|
-
throw new Error(`${flag}: expected a positive integer, got "${
|
|
6987
|
+
throw new Error(`${flag}: expected a positive integer, got "${input2}"`);
|
|
6985
6988
|
}
|
|
6986
6989
|
return n;
|
|
6987
6990
|
}
|
|
@@ -7067,10 +7070,10 @@ function resolveScriptB64(opts) {
|
|
|
7067
7070
|
}
|
|
7068
7071
|
throw new Error("one of --script-file or --script-b64 is required");
|
|
7069
7072
|
}
|
|
7070
|
-
function parseAdapterVariables(
|
|
7073
|
+
function parseAdapterVariables(input2) {
|
|
7071
7074
|
let parsed;
|
|
7072
7075
|
try {
|
|
7073
|
-
parsed = JSON.parse(
|
|
7076
|
+
parsed = JSON.parse(input2);
|
|
7074
7077
|
} catch (err) {
|
|
7075
7078
|
throw new Error(`--variables: invalid JSON (${err instanceof Error ? err.message : err})`);
|
|
7076
7079
|
}
|
|
@@ -9732,8 +9735,8 @@ async function dataProfiles(api, maxPages2) {
|
|
|
9732
9735
|
}
|
|
9733
9736
|
throw new Error("DLP inventory is incomplete; increase --max-pages");
|
|
9734
9737
|
}
|
|
9735
|
-
async function planRuntimeProfilesRestore(api,
|
|
9736
|
-
const backup = RuntimeProfilesBackupSchema.parse(
|
|
9738
|
+
async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
|
|
9739
|
+
const backup = RuntimeProfilesBackupSchema.parse(input2);
|
|
9737
9740
|
if (!destinationTsgId) throw new Error("Destination TSG ID is required");
|
|
9738
9741
|
const maxPages2 = options.maxPages ?? 100;
|
|
9739
9742
|
pagesLimit(maxPages2);
|
|
@@ -10109,15 +10112,15 @@ function registerProfileTransferCommands(profiles2) {
|
|
|
10109
10112
|
const info = await stat(path3);
|
|
10110
10113
|
if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
|
|
10111
10114
|
throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
|
|
10112
|
-
let
|
|
10115
|
+
let input2;
|
|
10113
10116
|
try {
|
|
10114
10117
|
const text2 = await readFile9(path3, "utf8");
|
|
10115
10118
|
if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
|
|
10116
|
-
|
|
10119
|
+
input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
|
|
10117
10120
|
} catch {
|
|
10118
10121
|
throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
|
|
10119
10122
|
}
|
|
10120
|
-
const data = RuntimeProfilesBackupSchema.parse(
|
|
10123
|
+
const data = RuntimeProfilesBackupSchema.parse(input2);
|
|
10121
10124
|
const format = await resolveOutput(restore, opts);
|
|
10122
10125
|
const { api, tsgId } = await context();
|
|
10123
10126
|
if (opts.expectTsg && opts.expectTsg !== tsgId)
|
|
@@ -10608,26 +10611,26 @@ function registerRuntimeReportCommand(runtime) {
|
|
|
10608
10611
|
}
|
|
10609
10612
|
|
|
10610
10613
|
// src/cli/commands/topics-apply.ts
|
|
10611
|
-
async function applyTopicToProfile(mgmt,
|
|
10614
|
+
async function applyTopicToProfile(mgmt, input2) {
|
|
10612
10615
|
const allTopics = await mgmt.listTopics();
|
|
10613
|
-
const match = allTopics.find((t) => t.topic_name ===
|
|
10616
|
+
const match = allTopics.find((t) => t.topic_name === input2.topicName);
|
|
10614
10617
|
if (!match || !match.topic_id) {
|
|
10615
|
-
throw new Error(`Topic "${
|
|
10618
|
+
throw new Error(`Topic "${input2.topicName}" not found. Create it first with "topics create".`);
|
|
10616
10619
|
}
|
|
10617
|
-
const existing = await mgmt.getProfileTopics(
|
|
10618
|
-
const merged = existing.filter((t) => t.topicName !==
|
|
10620
|
+
const existing = await mgmt.getProfileTopics(input2.profileName);
|
|
10621
|
+
const merged = existing.filter((t) => t.topicName !== input2.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
|
|
10619
10622
|
merged.push({
|
|
10620
10623
|
topicId: match.topic_id,
|
|
10621
10624
|
topicName: match.topic_name,
|
|
10622
|
-
action:
|
|
10625
|
+
action: input2.intent
|
|
10623
10626
|
});
|
|
10624
|
-
const guardrailAction =
|
|
10625
|
-
await mgmt.assignTopicsToProfile(
|
|
10627
|
+
const guardrailAction = input2.intent === "block" ? "allow" : "block";
|
|
10628
|
+
await mgmt.assignTopicsToProfile(input2.profileName, merged, guardrailAction);
|
|
10626
10629
|
return {
|
|
10627
10630
|
topicId: match.topic_id,
|
|
10628
10631
|
topicName: match.topic_name,
|
|
10629
|
-
profileName:
|
|
10630
|
-
intent:
|
|
10632
|
+
profileName: input2.profileName,
|
|
10633
|
+
intent: input2.intent
|
|
10631
10634
|
};
|
|
10632
10635
|
}
|
|
10633
10636
|
function registerApplyCommand(parent) {
|
|
@@ -10672,21 +10675,21 @@ function registerApplyCommand(parent) {
|
|
|
10672
10675
|
}
|
|
10673
10676
|
|
|
10674
10677
|
// src/cli/commands/topics-create.ts
|
|
10675
|
-
async function createOrUpdateTopic(mgmt,
|
|
10676
|
-
const topic = { name:
|
|
10678
|
+
async function createOrUpdateTopic(mgmt, input2) {
|
|
10679
|
+
const topic = { name: input2.name, description: input2.description, examples: input2.examples };
|
|
10677
10680
|
const errors = validateTopic(topic);
|
|
10678
10681
|
if (errors.length > 0) {
|
|
10679
10682
|
throw new Error(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
|
|
10680
10683
|
}
|
|
10681
10684
|
const existing = await mgmt.listTopics();
|
|
10682
|
-
const match = existing.find((t) => t.topic_name ===
|
|
10685
|
+
const match = existing.find((t) => t.topic_name === input2.name);
|
|
10683
10686
|
const request = {
|
|
10684
|
-
topic_name:
|
|
10685
|
-
description:
|
|
10686
|
-
examples:
|
|
10687
|
+
topic_name: input2.name,
|
|
10688
|
+
description: input2.description,
|
|
10689
|
+
examples: input2.examples
|
|
10687
10690
|
};
|
|
10688
10691
|
if (match && !match.topic_id) {
|
|
10689
|
-
throw new Error(`Existing topic '${
|
|
10692
|
+
throw new Error(`Existing topic '${input2.name}' has no topic_id`);
|
|
10690
10693
|
}
|
|
10691
10694
|
const result = match ? await mgmt.updateTopic(match.topic_id, request) : await mgmt.createTopic(request);
|
|
10692
10695
|
if (!result.topic_id) {
|
|
@@ -11843,7 +11846,139 @@ function registerRuntimeCommand(program) {
|
|
|
11843
11846
|
registerDlpCommands(runtime);
|
|
11844
11847
|
}
|
|
11845
11848
|
|
|
11849
|
+
// src/config/tenant-settings.ts
|
|
11850
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
11851
|
+
import { constants } from "fs";
|
|
11852
|
+
import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
11853
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
11854
|
+
var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
|
|
11855
|
+
function validateTenantSettingKey(key) {
|
|
11856
|
+
if (!Object.hasOwn(ConfigSchema.shape, key))
|
|
11857
|
+
throw new Error(`Unknown configuration key. Supported keys: ${TENANT_CONFIG_KEYS.join(", ")}`);
|
|
11858
|
+
}
|
|
11859
|
+
function isTenantSecret(key) {
|
|
11860
|
+
return /(?:key|secret|token|password)$/i.test(key);
|
|
11861
|
+
}
|
|
11862
|
+
function validateCredentials(config) {
|
|
11863
|
+
for (const key of ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"]) {
|
|
11864
|
+
if (typeof config[key] !== "string" || !config[key].trim())
|
|
11865
|
+
throw new Error(
|
|
11866
|
+
"Tenant config requires nonempty mgmtTsgId, mgmtClientId and mgmtClientSecret"
|
|
11867
|
+
);
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11870
|
+
async function createManagedTenant(name, config) {
|
|
11871
|
+
validateTenantName(name);
|
|
11872
|
+
for (const key of Object.keys(config)) validateTenantSettingKey(key);
|
|
11873
|
+
const parsed = ConfigSchema.safeParse(config);
|
|
11874
|
+
if (!parsed.success) throw new Error("Invalid tenant configuration");
|
|
11875
|
+
validateCredentials(config);
|
|
11876
|
+
if (readTenantStore().tenants.some((entry) => entry.name === name))
|
|
11877
|
+
throw new Error("Tenant name already exists");
|
|
11878
|
+
const directory = join4(dirname4(tenantStorePath()), "configs");
|
|
11879
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
11880
|
+
const path3 = join4(directory, `${name}-${randomUUID10()}.json`);
|
|
11881
|
+
const file = await open2(path3, "wx", 384);
|
|
11882
|
+
try {
|
|
11883
|
+
const data = Object.fromEntries(
|
|
11884
|
+
Object.keys(config).map((key) => [key, Reflect.get(parsed.data, key)])
|
|
11885
|
+
);
|
|
11886
|
+
await file.writeFile(`${JSON.stringify(data, null, 2)}
|
|
11887
|
+
`);
|
|
11888
|
+
await file.sync();
|
|
11889
|
+
await file.close();
|
|
11890
|
+
return await createTenant(name, path3);
|
|
11891
|
+
} catch {
|
|
11892
|
+
await file.close();
|
|
11893
|
+
await unlink2(path3);
|
|
11894
|
+
throw new Error("Could not create tenant configuration; registration was not completed");
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11897
|
+
async function setTenantSetting(name, key, value) {
|
|
11898
|
+
validateTenantSettingKey(key);
|
|
11899
|
+
const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
|
|
11900
|
+
if (!entry) throw new Error("Tenant not found; register a named tenant first");
|
|
11901
|
+
if (key === "mgmtTsgId" && value !== entry.tsgId)
|
|
11902
|
+
throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
|
|
11903
|
+
const path3 = entry.configPath;
|
|
11904
|
+
const lockPath = `${path3}.lock`;
|
|
11905
|
+
const lock = await open2(lockPath, "wx", 384).catch(() => {
|
|
11906
|
+
throw new Error(
|
|
11907
|
+
"Cannot lock tenant config; it may be read-only or another edit is in progress"
|
|
11908
|
+
);
|
|
11909
|
+
});
|
|
11910
|
+
const temporary = join4(dirname4(path3), `.airs-config-${randomUUID10()}.tmp`);
|
|
11911
|
+
let created = false;
|
|
11912
|
+
try {
|
|
11913
|
+
const stat2 = await lstat6(path3);
|
|
11914
|
+
if (!stat2.isFile() || (stat2.mode & 146) === 0)
|
|
11915
|
+
throw new Error("Tenant config is not a writable regular file");
|
|
11916
|
+
await access(path3, constants.W_OK);
|
|
11917
|
+
const current = readTenantConfigFile(path3, entry.tsgId);
|
|
11918
|
+
const result = ConfigSchema.safeParse({ ...current, [key]: value });
|
|
11919
|
+
if (!result.success) throw new Error("Invalid value for configuration setting");
|
|
11920
|
+
const next = { ...current, [key]: Reflect.get(result.data, key) };
|
|
11921
|
+
validateCredentials(next);
|
|
11922
|
+
const file = await open2(temporary, "wx", 384);
|
|
11923
|
+
created = true;
|
|
11924
|
+
try {
|
|
11925
|
+
await file.writeFile(`${JSON.stringify(next, null, 2)}
|
|
11926
|
+
`);
|
|
11927
|
+
await file.sync();
|
|
11928
|
+
} finally {
|
|
11929
|
+
await file.close();
|
|
11930
|
+
}
|
|
11931
|
+
await rename2(temporary, path3);
|
|
11932
|
+
created = false;
|
|
11933
|
+
} finally {
|
|
11934
|
+
if (created) await unlink2(temporary).catch(() => {
|
|
11935
|
+
});
|
|
11936
|
+
await lock.close();
|
|
11937
|
+
await unlink2(lockPath);
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
|
|
11941
|
+
// src/cli/tenant-input.ts
|
|
11942
|
+
import { input, password } from "@inquirer/prompts";
|
|
11943
|
+
async function promptTenantValue(message, secret = false) {
|
|
11944
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY)
|
|
11945
|
+
throw new Error(
|
|
11946
|
+
"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."
|
|
11947
|
+
);
|
|
11948
|
+
const prompt = secret ? password : input;
|
|
11949
|
+
return prompt(
|
|
11950
|
+
{
|
|
11951
|
+
message,
|
|
11952
|
+
validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
|
|
11953
|
+
},
|
|
11954
|
+
{ input: process.stdin, output: process.stderr }
|
|
11955
|
+
);
|
|
11956
|
+
}
|
|
11957
|
+
async function readTenantStdin() {
|
|
11958
|
+
if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
|
|
11959
|
+
const chunks = [];
|
|
11960
|
+
let size = 0;
|
|
11961
|
+
for await (const chunk of process.stdin) {
|
|
11962
|
+
const buffer = Buffer.from(chunk);
|
|
11963
|
+
size += buffer.length;
|
|
11964
|
+
if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
|
|
11965
|
+
chunks.push(buffer);
|
|
11966
|
+
}
|
|
11967
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
11968
|
+
if (!value.trim() || /[\r\n\0]/.test(value))
|
|
11969
|
+
throw new Error("Provide one nonempty configuration value on stdin");
|
|
11970
|
+
return value;
|
|
11971
|
+
}
|
|
11972
|
+
|
|
11846
11973
|
// src/cli/commands/tenant.ts
|
|
11974
|
+
function tenantInputFailure(error) {
|
|
11975
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
11976
|
+
ui.status("Cancelled; no configuration saved.");
|
|
11977
|
+
process.exitCode = 130;
|
|
11978
|
+
return;
|
|
11979
|
+
}
|
|
11980
|
+
fail(error);
|
|
11981
|
+
}
|
|
11847
11982
|
var COLUMNS2 = [
|
|
11848
11983
|
{ key: "name", label: "Tenant" },
|
|
11849
11984
|
{ key: "active", label: "Selected" },
|
|
@@ -11851,25 +11986,60 @@ var COLUMNS2 = [
|
|
|
11851
11986
|
{ key: "configPath", label: "Config file" }
|
|
11852
11987
|
];
|
|
11853
11988
|
function registerTenantCommand(program) {
|
|
11854
|
-
const tenant = program.command("tenant").description("
|
|
11989
|
+
const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
|
|
11855
11990
|
"after",
|
|
11856
11991
|
examples(
|
|
11992
|
+
"airs tenant create development",
|
|
11993
|
+
"airs tenant set development defaultOutput yaml",
|
|
11857
11994
|
"airs tenant create production --config /secure/production.json",
|
|
11858
11995
|
"airs tenant switch production",
|
|
11859
11996
|
"airs tenant read",
|
|
11860
11997
|
"airs tenant switch default"
|
|
11861
11998
|
)
|
|
11862
11999
|
);
|
|
11863
|
-
tenant.command("create <name>").description(
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
12000
|
+
tenant.command("create <name>").description(
|
|
12001
|
+
"Create a config with guided prompts, or register an existing JSON file; does not activate it"
|
|
12002
|
+
).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(
|
|
12003
|
+
async (name, opts) => {
|
|
12004
|
+
try {
|
|
12005
|
+
validateTenantName(name);
|
|
12006
|
+
if (opts.config && (opts.tsgId !== void 0 || opts.clientId !== void 0 || opts.clientSecretStdin))
|
|
12007
|
+
throw new Error("--config cannot be combined with new-config options");
|
|
12008
|
+
if (!opts.config && readTenantStore().tenants.some((entry2) => entry2.name === name))
|
|
12009
|
+
throw new Error("Tenant name already exists");
|
|
12010
|
+
const entry = opts.config ? await createTenant(name, opts.config) : await createManagedTenant(name, {
|
|
12011
|
+
mgmtTsgId: opts.tsgId ?? await promptTenantValue("Tenant service group ID (mgmtTsgId):"),
|
|
12012
|
+
mgmtClientId: opts.clientId ?? await promptTenantValue("OAuth client ID (mgmtClientId):"),
|
|
12013
|
+
mgmtClientSecret: opts.clientSecretStdin ? await readTenantStdin() : await promptTenantValue("OAuth client secret (mgmtClientSecret):", true)
|
|
12014
|
+
});
|
|
12015
|
+
ui.success(
|
|
12016
|
+
`Registered ${entry.name} (TSG ${entry.tsgId}); ${opts.config ? "config file unchanged" : "private config created"}. Use airs tenant switch ${entry.name}.`
|
|
12017
|
+
);
|
|
12018
|
+
} catch (error) {
|
|
12019
|
+
tenantInputFailure(error);
|
|
12020
|
+
}
|
|
11871
12021
|
}
|
|
11872
|
-
|
|
12022
|
+
);
|
|
12023
|
+
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(
|
|
12024
|
+
async (name, key, value, opts) => {
|
|
12025
|
+
try {
|
|
12026
|
+
validateTenantSettingKey(key);
|
|
12027
|
+
if (!readTenantStore().tenants.some((entry) => entry.name === name))
|
|
12028
|
+
throw new Error("Tenant not found; register a named tenant first");
|
|
12029
|
+
if (opts.stdin && value !== void 0)
|
|
12030
|
+
throw new Error("Use a value or --stdin, not both");
|
|
12031
|
+
if (isTenantSecret(key) && value !== void 0)
|
|
12032
|
+
throw new Error(
|
|
12033
|
+
"Do not pass credentials as arguments. Omit the value for a hidden prompt, or use --stdin."
|
|
12034
|
+
);
|
|
12035
|
+
const setting = opts.stdin ? await readTenantStdin() : value ?? await promptTenantValue(`${key}:`, isTenantSecret(key));
|
|
12036
|
+
await setTenantSetting(name, key, setting);
|
|
12037
|
+
ui.success(`Updated ${key} for tenant ${name}; selection unchanged.`);
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
tenantInputFailure(error);
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
);
|
|
11873
12043
|
tenant.command("switch <name>").description(
|
|
11874
12044
|
"Persist the selected tenant; default restores legacy config/environment resolution"
|
|
11875
12045
|
).action(async (name) => {
|
|
@@ -12005,8 +12175,8 @@ function applySortedHelp(cmd) {
|
|
|
12005
12175
|
for (const sub of cmd.commands) applySortedHelp(sub);
|
|
12006
12176
|
}
|
|
12007
12177
|
function buildProgram() {
|
|
12008
|
-
const here =
|
|
12009
|
-
const pkg = JSON.parse(readFileSync4(
|
|
12178
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
12179
|
+
const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
|
|
12010
12180
|
const program = new Command();
|
|
12011
12181
|
program.name("airs").description(
|
|
12012
12182
|
"CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
|
|
@@ -12035,9 +12205,9 @@ function buildProgram() {
|
|
|
12035
12205
|
}
|
|
12036
12206
|
}
|
|
12037
12207
|
if (root.debug) {
|
|
12038
|
-
const logPath =
|
|
12208
|
+
const logPath = join5(
|
|
12039
12209
|
process.cwd(),
|
|
12040
|
-
`debug-api-${Date.now()}-${
|
|
12210
|
+
`debug-api-${Date.now()}-${randomUUID11().slice(0, 8)}.jsonl`
|
|
12041
12211
|
);
|
|
12042
12212
|
try {
|
|
12043
12213
|
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.6.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",
|