@knowttl/checkpoint-axi 0.1.0 → 0.1.2

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.
@@ -6,29 +6,48 @@
6
6
  "additionalProperties": false,
7
7
  "required": ["region", "clientId"],
8
8
  "properties": {
9
- "region": { "type": "string", "enum": ["us", "eu", "ca", "au", "uk", "uae", "in"] },
10
- "clientId": { "type": "string", "minLength": 1, "pattern": "\\S" },
11
- "clientSecret": { "$ref": "#/$defs/secretRef", "x-secret": true, "default": { "env": "CHECKPOINT_AXI_CLIENT_SECRET" } },
12
- "saas": { "type": "string", "minLength": 1, "pattern": "\\S", "default": "office365_emails" },
13
- "scope": { "type": "string", "minLength": 1, "pattern": "\\S" },
9
+ "region": {
10
+ "title": "Region", "description": "Required region code for your tenant's regional gateway: us, eu, ca, au, uk, uae or in. Obtain it from your tenant administrator. There is no default; URLs are not accepted.",
11
+ "type": "string", "enum": ["us", "eu", "ca", "au", "uk", "uae", "in"]
12
+ },
13
+ "clientId": {
14
+ "title": "Client ID", "description": "Required non-blank API client ID from your tenant's API credentials. There is no default.",
15
+ "type": "string", "minLength": 1, "pattern": "\\S"
16
+ },
17
+ "clientSecret": {
18
+ "title": "Client secret reference", "description": "Reference the environment variable or absolute private file containing your API client secret, never the secret itself. If omitted, reads CHECKPOINT_AXI_CLIENT_SECRET; a missing or blank secret prevents authentication.",
19
+ "$ref": "#/$defs/secretRef", "x-secret": true, "default": { "env": "CHECKPOINT_AXI_CLIENT_SECRET" }
20
+ },
21
+ "saas": {
22
+ "title": "SaaS application name",
23
+ "description": "One non-blank SaaS application name for the tenant, not a gateway, host or authentication URL; slash (/) is not allowed. Obtain the exact API application name from your tenant administrator or Check Point support. If omitted, defaults to office365_emails, selecting Microsoft 365 email; supply the appropriate name for another application. A profile takes one value, while --saas on event list accepts a comma-separated list.",
24
+ "type": "string", "minLength": 1, "pattern": "\\S", "not": { "pattern": "/" }, "default": "office365_emails"
25
+ },
26
+ "scope": {
27
+ "title": "Download scope",
28
+ "description": "Download scope value supplied by your tenant administrator or Check Point support for the intended download. Use printable ASCII with no spaces (characters ! through ~); comma-separated values are sent verbatim. There is no default: omitted means unset, so email download fails because it requires the profile scope as its scopes header. Attachment download uses this only as the default scope query; --scope overrides it, and neither supplied means no scope query. Tenant scope authorization and the relationship between these selectors remain unverified; do not guess a value.",
29
+ "type": "string", "minLength": 1, "pattern": "^[!-~]+$"
30
+ },
14
31
  "downloads": {
32
+ "title": "Download permission", "description": "Separate file-profile download opt-in. If omitted, downloads are disabled. Environment profiles cannot enable downloads.",
15
33
  "type": "object", "additionalProperties": false, "required": ["allow"], "default": { "allow": false },
16
- "properties": { "allow": { "type": "boolean" } }
34
+ "properties": { "allow": { "title": "Allow downloads", "description": "Set true to permit download commands; each command still requires a new private output file.", "type": "boolean" } }
17
35
  },
18
36
  "writes": {
37
+ "title": "Write permissions", "description": "File-profile write opt-in and exact operation allow-list. If omitted, writes are disabled with no allowed operations. Forced read-only always wins.",
19
38
  "type": "object", "additionalProperties": false, "required": ["allowWrites", "operations"],
20
39
  "default": { "allowWrites": false, "operations": [] },
21
40
  "properties": {
22
- "allowWrites": { "type": "boolean" },
23
- "operations": { "type": "array", "items": { "type": "string", "pattern": "^[a-z][A-Za-z0-9_.-]*$" } }
41
+ "allowWrites": { "title": "Allow writes", "description": "Set true to permit listed operations, subject to preview, exact confirmation and durable redacted intent.", "type": "boolean" },
42
+ "operations": { "title": "Allowed write operations", "description": "Exact operation names to allow, such as email.quarantine. An empty list permits no writes.", "type": "array", "items": { "type": "string", "pattern": "^[a-z][A-Za-z0-9_.-]*$" } }
24
43
  }
25
44
  }
26
45
  },
27
46
  "$defs": {
28
47
  "secretRef": {
29
48
  "oneOf": [
30
- { "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
31
- { "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/])" } } }
49
+ { "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "title": "Secret environment variable", "description": "Name of the environment variable holding the secret, such as CHECKPOINT_AXI_CLIENT_SECRET. Use letters, digits and underscores, starting with a letter or underscore.", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
50
+ { "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "title": "Secret file path", "description": "Absolute path to a private UTF-8 file holding the secret. Symlinks and insecure permissions are rejected; a leading BOM and trailing whitespace are removed from its content.", "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/])" } } }
32
51
  ]
33
52
  }
34
53
  }
@@ -1,5 +1,7 @@
1
1
  export declare const DESCRIPTION = "Inspect Check Point Email Security through compact, guarded AXI commands";
2
2
  export declare const SETUP_GUIDANCE = "Run checkpoint-axi config path; configure a region, clientId and clientSecret env/file SecretRef";
3
+ export declare const EMAIL_PAYLOAD_ATTRIBUTES: string[];
4
+ export declare const EMAIL_FILTER_GUIDANCE: string;
3
5
  export declare const catalogue: {
4
6
  readonly "email download": {
5
7
  readonly description: "Download an email to a new private file with profile opt-in, a 50 MiB cap and mandatory SHA-256 verification; scopes and email hash availability are unverified (guide pages 131-132)";
@@ -230,7 +232,7 @@ export declare const catalogue: {
230
232
  readonly "email search": {
231
233
  readonly description: "Search emails with a bounded date window and scroll continuation (API guide pages 18-27)";
232
234
  readonly flags: {
233
- readonly filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)";
235
+ readonly filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${string}`;
234
236
  readonly "start-date": string;
235
237
  readonly "end-date": string;
236
238
  readonly limit: string;
@@ -1,6 +1,11 @@
1
1
  import { exceptionFamilies } from "./exceptions.js";
2
2
  export const DESCRIPTION = "Inspect Check Point Email Security through compact, guarded AXI commands";
3
3
  export const SETUP_GUIDANCE = "Run checkpoint-axi config path; configure a region, clientId and clientSecret env/file SecretRef";
4
+ export const EMAIL_PAYLOAD_ATTRIBUTES = ["subject", "fromEmail", "attachmentCount", "internetMessageId", "received", "size", "emailLinks", "attachments", "mode", "recipients", "fromDomain", "fromUser", "fromName", "to", "toUser", "cc", "ccUser", "bcc", "bccUser", "replyToEmail", "replyToNickname", "isRead", "isDeleted", "isIncoming", "isInternal", "isOutgoing", "isQuarantined", "isQuarantineNotification", "isRestored", "isRestoreRequested", "isRestoreDeclined", "saasSpamVerdict", "SpfResult"].map((name) => `entityPayload.${name}`);
5
+ export const EMAIL_FILTER_GUIDANCE = "saasAttrName is a field path, not a closed enum (guide pages 19-20). Documented examples: "
6
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(0, 3).join(", ") + ". Other documented email payload paths (pages 15-16, 24-25): "
7
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(3).join(", ")
8
+ + ". saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan. The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan. saasAttrValue is a string.";
4
9
  const common = {
5
10
  config: "Config path; default CHECKPOINT_AXI_CONFIG or platform path",
6
11
  profile: "Profile name; default CHECKPOINT_AXI_PROFILE, env profile, config default, or sole profile",
@@ -82,7 +87,7 @@ export const catalogue = {
82
87
  saas: "SaaS override; default selected profile saas or office365_emails",
83
88
  "saas-entity": "Optional SaaS entity type selector",
84
89
  ...paging,
85
- filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)",
90
+ filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${EMAIL_FILTER_GUIDANCE}`,
86
91
  }, examples: ["checkpoint-axi email search", "checkpoint-axi email search --all --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"] },
87
92
  "email show": { description: "Show one email with verdicts, actions and available actions (API guide pages 10-18)", flags: common,
88
93
  examples: ["checkpoint-axi email show <id>", "checkpoint-axi email show <id> --full"] },
@@ -8,6 +8,7 @@ import { SecretRedactor } from "./redact.js";
8
8
  const profileName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/);
9
9
  const region = z.enum(Object.keys(REGIONS));
10
10
  const nonempty = z.string().trim().min(1);
11
+ const saasName = nonempty.refine((value) => !value.includes("/"));
11
12
  function schema(platform) {
12
13
  const secretRef = z.union([
13
14
  z.strictObject({ env: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/) }),
@@ -17,7 +18,7 @@ function schema(platform) {
17
18
  region,
18
19
  clientId: nonempty,
19
20
  clientSecret: secretRef.default({ env: "CHECKPOINT_AXI_CLIENT_SECRET" }),
20
- saas: nonempty.default("office365_emails"),
21
+ saas: saasName.default("office365_emails"),
21
22
  scope: nonempty.optional(),
22
23
  downloads: z.strictObject({ allow: z.boolean() }).default({ allow: false }),
23
24
  writes: z.strictObject({ allowWrites: z.boolean(), operations: z.array(z.string().regex(/^[a-z][A-Za-z0-9_.-]*$/)) })
@@ -78,7 +79,7 @@ export function credentialStore(options = {}, redactor = new SecretRedactor()) {
78
79
  }
79
80
  const parsed = schema(platform).safeParse(raw);
80
81
  if (!parsed.success)
81
- return invalid("Invalid configuration: check profile names, region, defaultProfile, unknown fields and clientSecret SecretRef");
82
+ return invalid("Invalid configuration: check profile names, region, defaultProfile, unknown fields, clientSecret SecretRef and saas (use a SaaS application name, not a URL)");
82
83
  return { path, config: parsed.data };
83
84
  }
84
85
  function select(config, flag) {
@@ -87,8 +88,11 @@ export function credentialStore(options = {}, redactor = new SecretRedactor()) {
87
88
  invalid("Invalid selected profile name");
88
89
  const envFields = ["REGION", "CLIENT_ID", "SAAS", "SCOPE"];
89
90
  if (selected === undefined && envFields.some((field) => env[`CHECKPOINT_AXI_${field}`] !== undefined)) {
91
+ if (env.CHECKPOINT_AXI_SAAS !== undefined && !saasName.safeParse(env.CHECKPOINT_AXI_SAAS).success) {
92
+ return invalid("saas must be a SaaS application name, not a URL");
93
+ }
90
94
  if (!region.safeParse(env.CHECKPOINT_AXI_REGION).success || !env.CHECKPOINT_AXI_CLIENT_ID?.trim() || !env.CHECKPOINT_AXI_CLIENT_SECRET?.trim()
91
- || (env.CHECKPOINT_AXI_SAAS !== undefined && !env.CHECKPOINT_AXI_SAAS.trim()) || (env.CHECKPOINT_AXI_SCOPE !== undefined && !env.CHECKPOINT_AXI_SCOPE.trim())) {
95
+ || (env.CHECKPOINT_AXI_SCOPE !== undefined && !env.CHECKPOINT_AXI_SCOPE.trim())) {
92
96
  return invalid("Incomplete or invalid env profile; set REGION, CLIENT_ID and CLIENT_SECRET together");
93
97
  }
94
98
  return { region: env.CHECKPOINT_AXI_REGION, clientId: env.CHECKPOINT_AXI_CLIENT_ID.trim(),
package/dist/src/reads.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { REGIONS } from "./regions.js";
5
5
  import { gateway, READ_LIMITS } from "./transport.js";
6
+ import { responseInvalid } from "./response-invalid.js";
6
7
  const day = 86_400_000;
7
8
  const timestamp = z.iso.datetime({ offset: true });
8
9
  const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
@@ -10,10 +11,12 @@ const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
10
11
  saasAttrValue: z.string() }));
11
12
  const continuation = z.strictObject({ version: z.literal(1), binding: z.string(), start: timestamp, end: timestamp,
12
13
  scroll: z.string().optional(), offset: z.number().int().nonnegative(), replayRisk: z.literal(true).optional() });
14
+ // The guide's prose/table (pp.11,14) says security-result array; its JSON examples use an object.
15
+ const securityResults = z.object({ combinedVerdict: z.unknown().optional() }).passthrough();
13
16
  // Entity schemas and SaaS-specific payloads: guide pages 11-15 and 21-24.
14
17
  const entity = z.object({ entityInfo: z.object({ entityId: z.string().min(1), entityActionState: z.string().optional() }).passthrough(),
15
18
  entityPayload: z.object({ subject: z.string().optional() }).passthrough(),
16
- entitySecurityResults: z.object({ combinedVerdict: z.unknown().optional() }).passthrough().optional(),
19
+ entitySecurityResults: z.union([securityResults, z.array(z.object({}).passthrough())]).optional(),
17
20
  }).passthrough();
18
21
  // Event and task fields: guide pages 31-32, 35-36 and 40-41.
19
22
  const event = z.object({ eventId: z.string().min(1), type: z.string(), state: z.string(), severity: z.string() }).passthrough();
@@ -101,15 +104,19 @@ export async function readCommand(name, id, flags, store, transport) {
101
104
  if (name.endsWith(" show")) {
102
105
  const kind = name.split(" ")[0];
103
106
  const page = await api.read({ path: name === "task show" ? `/task/${id}` : isEvent ? `/event/${id}` : `/search/entity/${id}` });
107
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
108
+ const recordPath = page.dataIsArray ? ["responseData", 0] : ["responseData"];
104
109
  if (page.records.length === 0)
105
110
  throw new AxiError(`${kind} was not found`, "NOT_FOUND", []);
106
111
  const parsed = (name === "task show" ? task : isEvent ? event : entity).safeParse(page.records[0]);
107
- if (page.records.length !== 1 || !parsed.success)
108
- throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
112
+ if (page.records.length !== 1)
113
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [["responseData"]]);
114
+ if (!parsed.success)
115
+ throw responseInvalid(`Invalid ${kind} detail response`, response, parsed.error.issues.map((issue) => [...recordPath, ...issue.path]));
109
116
  const record = page.records[0];
110
117
  const returnedId = name === "task show" ? String(record.id) : isEvent ? record.eventId : record.entityInfo?.entityId;
111
118
  if (returnedId !== id)
112
- throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
119
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [[...recordPath, ...(name === "task show" ? ["id"] : isEvent ? ["eventId"] : ["entityInfo", "entityId"])]]);
113
120
  return { [kind]: parsed.data };
114
121
  }
115
122
  const results = [];
@@ -131,11 +138,14 @@ export async function readCommand(name, id, flags, store, transport) {
131
138
  throw error;
132
139
  }
133
140
  const parsed = z.array(isEvent ? event : entity).safeParse(page.records);
134
- if (!parsed.success || offset > page.records.length)
135
- throw new AxiError("Invalid search response", "RESPONSE_INVALID", []);
141
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
142
+ if (!parsed.success)
143
+ throw responseInvalid("Invalid search response", response, parsed.error.issues.map((issue) => ["responseData", ...(page.dataIsArray ? issue.path : issue.path.slice(1))]));
144
+ if (offset > page.records.length)
145
+ throw responseInvalid("Invalid search response", response, [["responseData"]]);
136
146
  // Never wrap an echoed credential in a reversible continuation cursor.
137
147
  if (page.scrollId && store.redactor.text(page.scrollId) !== page.scrollId)
138
- throw new AxiError("Unsafe API scrollId", "RESPONSE_INVALID", []);
148
+ throw responseInvalid("Unsafe API scrollId", { responseEnvelope: { scrollId: page.scrollId } }, [["responseEnvelope", "scrollId"]]);
139
149
  total = page.total ?? total;
140
150
  const rows = parsed.data.slice(offset, offset + limit - results.length);
141
151
  results.push(...rows.map((record) => {
@@ -145,7 +155,7 @@ export async function readCommand(name, id, flags, store, transport) {
145
155
  }
146
156
  const value = entity.parse(record);
147
157
  return { id: value.entityInfo.entityId, subject: value.entityPayload.subject ?? "",
148
- state: value.entityInfo.entityActionState ?? "", verdict: value.entitySecurityResults?.combinedVerdict ?? null };
158
+ state: value.entityInfo.entityActionState ?? "", verdict: Array.isArray(value.entitySecurityResults) ? null : value.entitySecurityResults?.combinedVerdict ?? null };
149
159
  }));
150
160
  offset += rows.length;
151
161
  if (offset < page.records.length) {
@@ -0,0 +1,2 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ export declare function responseInvalid(message: string, value: unknown, paths: readonly (readonly PropertyKey[])[]): AxiError;
@@ -0,0 +1,17 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // Never format validator messages or received values: both may contain tenant data.
3
+ export function responseInvalid(message, value, paths) {
4
+ const failures = paths.map((path) => {
5
+ let received = value;
6
+ let location = "$";
7
+ for (const key of path) {
8
+ location += typeof key === "number" ? `[${key}]`
9
+ : `.${typeof key === "string" && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : "<key>"}`;
10
+ received = received !== null && typeof received === "object" && Object.hasOwn(received, key)
11
+ ? received[key] : undefined;
12
+ }
13
+ const type = received === undefined ? "missing" : received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
14
+ return `${location}: ${type}`;
15
+ });
16
+ return new AxiError(`${message} (${[...new Set(failures)].join("; ")})`, "RESPONSE_INVALID", []);
17
+ }
@@ -59,6 +59,7 @@ export type ReadRequest = {
59
59
  };
60
60
  export type Page = {
61
61
  records: unknown[];
62
+ dataIsArray: boolean;
62
63
  scrollId?: string;
63
64
  total?: number;
64
65
  };
@@ -4,6 +4,7 @@ import { request } from "node:https";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
5
  import { REGIONS } from "./regions.js";
6
6
  import { consumeWriteAuthorization } from "./writes.js";
7
+ import { responseInvalid } from "./response-invalid.js";
7
8
  export const READ_LIMITS = { requests: 10, rows: 1_000, milliseconds: 60_000, bytes: 8 * 1024 * 1024 };
8
9
  export const DOWNLOAD_LIMIT = 50 * 1024 * 1024;
9
10
  const failure = (code, message) => new AxiError(message, code, ["Check the selected region and credentials; run checkpoint-axi doctor"]);
@@ -12,24 +13,26 @@ const object = (value) => value !== null && typeof value === "object" && !Array.
12
13
  export function decodePage(value, unverified = false) {
13
14
  // These exception families publish no response schema (guide pages 83-123).
14
15
  if (unverified && (!object(value) || !Object.hasOwn(value, "responseEnvelope"))) {
15
- return { records: Array.isArray(value) ? value : [value] };
16
+ return { records: Array.isArray(value) ? value : [value], dataIsArray: Array.isArray(value) };
16
17
  }
17
- if (!object(value) || !object(value.responseEnvelope))
18
- throw failure("RESPONSE_INVALID", "Missing API response envelope");
18
+ if (!object(value))
19
+ throw responseInvalid("Missing API response envelope", value, [[]]);
20
+ if (!object(value.responseEnvelope))
21
+ throw responseInvalid("Missing API response envelope", value, [["responseEnvelope"]]);
19
22
  const envelope = value.responseEnvelope;
20
23
  if (envelope.responseCode !== 0 && envelope.responseCode !== "0" && envelope.responseCode !== 200 && envelope.responseCode !== "200") {
21
24
  throw failure("API_ERROR", "API response envelope did not report success");
22
25
  }
23
26
  const data = value.responseData;
24
27
  if ((!unverified && !Array.isArray(data) && !object(data)) || data === undefined)
25
- throw failure("RESPONSE_INVALID", "Expected responseData");
28
+ throw responseInvalid("Expected responseData", value, [["responseData"]]);
26
29
  if (envelope.scrollId !== undefined && envelope.scrollId !== null && typeof envelope.scrollId !== "string") {
27
- throw failure("RESPONSE_INVALID", "Invalid API scrollId");
30
+ throw responseInvalid("Invalid API scrollId", value, [["responseEnvelope", "scrollId"]]);
28
31
  }
29
32
  if (envelope.totalRecordsNumber !== undefined && (!Number.isSafeInteger(envelope.totalRecordsNumber) || Number(envelope.totalRecordsNumber) < 0)) {
30
- throw failure("RESPONSE_INVALID", "Invalid API totalRecordsNumber");
33
+ throw responseInvalid("Invalid API totalRecordsNumber", value, [["responseEnvelope", "totalRecordsNumber"]]);
31
34
  }
32
- return { records: Array.isArray(data) ? data : [data], scrollId: typeof envelope.scrollId === "string" && envelope.scrollId ? envelope.scrollId : undefined,
35
+ return { records: Array.isArray(data) ? data : [data], dataIsArray: Array.isArray(data), scrollId: typeof envelope.scrollId === "string" && envelope.scrollId ? envelope.scrollId : undefined,
33
36
  total: envelope.totalRecordsNumber };
34
37
  }
35
38
  // No field name or lifetime is commissioned yet (guide page 8).
@@ -125,7 +128,7 @@ export function gateway(profile, credentials, redactor, dependencies = {}, mutat
125
128
  resolve({ status, value: JSON.parse(json) });
126
129
  }
127
130
  catch {
128
- reject(failure("RESPONSE_INVALID", "API returned invalid JSON"));
131
+ reject(responseInvalid("API returned invalid JSON", json, [[]]));
129
132
  }
130
133
  });
131
134
  });
@@ -0,0 +1,114 @@
1
+ <!-- Generated by scripts/generate-docs.mjs from src/catalogue.ts. Do not hand-edit. -->
2
+
3
+ # Investigation agent guide
4
+
5
+ Start with these reads; use command help only to recover from a failed command or inspect an unfamiliar selector.
6
+ Examples assume an installed checkpoint-axi and an operator-provisioned read-only profile (the repository tests use synthetic credentials and a fake gateway).
7
+ Without a global install, replace checkpoint-axi with npx -y @knowttl/checkpoint-axi.
8
+ Never pass credentials in argv; use environment/file SecretRefs as described in [configuration](configuration.md).
9
+ Force read-only with CHECKPOINT_AXI_READ_ONLY=1; this overrides profile write permissions.
10
+ Live API contracts remain uncommissioned; fixture success does not establish live compatibility.
11
+
12
+ ## Read commands and examples
13
+
14
+ Local readiness checks make no API calls:
15
+
16
+ ```sh
17
+ checkpoint-axi
18
+ checkpoint-axi config path
19
+ checkpoint-axi profile list
20
+ checkpoint-axi auth status
21
+ ```
22
+
23
+ The home view shows local profile context, config path shows effective paths, profile list lists file profiles, and auth status validates the selected secret source locally.
24
+ Add --profile <name> or --config <absolute-path> to select provisioned configuration; profile list ignores --profile.
25
+ Doctor authenticates and checks one search page without printing email data:
26
+
27
+ ```sh
28
+ checkpoint-axi doctor
29
+ ```
30
+
31
+ Search emails, then inspect a returned entity ID for payload, verdicts, actions taken and available actions:
32
+
33
+ ```sh
34
+ checkpoint-axi email search --limit 5 --fields emails,count,total,complete,reason,cursor
35
+ checkpoint-axi email show synthetic-email-1 --fields email
36
+ ```
37
+
38
+ Search flags: --saas (default profile SaaS or office365_emails), --saas-entity, and --filter (JSON array of {saasAttrName, saasAttrOp, saasAttrValue}).
39
+ saasAttrName is a field path, not a closed enum (guide pages 19-20).
40
+ Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount.
41
+ Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult.
42
+ saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan.
43
+ The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan.
44
+ saasAttrValue is a string.
45
+ Event selectors include --event-types, --event-states, --severities, --event-ids (comma-separated), --confidence-indicator and --description; --saas accepts comma-separated SaaS values here.
46
+ Event detail includes available actions; task detail reads status and per-action results once, without waiting or polling:
47
+
48
+ ```sh
49
+ checkpoint-axi event list --all --event-types phishing --fields events,count,total,complete,reason,cursor
50
+ checkpoint-axi event show synthetic-event-1 --fields event
51
+ checkpoint-axi task show synthetic-task-1 --fields task
52
+ ```
53
+
54
+ Inspect exception rules with exception list <kind> or exception show <kind> <id-or-value>:
55
+
56
+ ```sh
57
+ checkpoint-axi exception list phishing-block --limit 5 --fields exceptions,count,total,complete,reason
58
+ checkpoint-axi exception show phishing-block synthetic-exception-1 --fields exception
59
+ ```
60
+
61
+ Kinds: phishing-allow, phishing-block, spam-allow, anomaly, click-time, malware, url, dlp.
62
+ For malware, --type is required: hash, micro_hash, file_type, ppat_sender_name, allow_file_type or sender_email.
63
+ For url, --type is allow-url, allow-domain, block-url or block-domain; for dlp, hash, text_content, sender_email or recipient_email.
64
+ Click-time --items selects item routes instead of list routes.
65
+ Anomaly show is refused with ENDPOINT_UNVERIFIED; use anomaly list.
66
+ Undocumented response shapes carry responseSchema: unverified; do not infer identities from them.
67
+
68
+ ## One-call forms and output bounds
69
+
70
+ Email search and event list --all follow scroll pages in one invocation, capped at 1000 rows, 10 HTTP requests, 60 seconds and 8 MiB of JSON, including authentication and read retries.
71
+ Both default to 50 rows over the past 24 hours; --limit accepts 1-1000 and cannot combine with --all.
72
+ Use --start-date and --end-date with ISO timestamps including timezones; the positive window cannot exceed 31 days.
73
+ Inspect complete, reason and cursor even on exit 0; limits and page replay can leave results incomplete.
74
+ Resume with --cursor <returned-cursor> and the original selectors/profile; see [continuation limitations](reads.md#email-reads).
75
+ Exception --all bounds returned rows at 1000 but cannot fetch additional pages: no continuation parameter is documented.
76
+ Email/event detail already includes available actions, avoiding a separate action lookup.
77
+ There is no bulk detail expansion, cross-resource pivot or task wait flag; use returned IDs for individual reads and never replay an action to obtain its status.
78
+
79
+ --fields selects top-level output keys, not nested row columns: request emails or events together with count,total,complete,reason,cursor for lists, and email,event,task or exception for details.
80
+ Email rows contain id,subject,state,verdict; event rows contain id,type,state,severity; documented exception rows contain id,sender,subject,comment.
81
+ TOON strings preview at 500 characters; --full expands strings after redaction, while cursors are never truncated.
82
+
83
+ ## Help and safety
84
+
85
+ Top-level --help returns the full command catalogue, common flags, version flags and home examples.
86
+ Group --help (such as email --help) is unsupported and returns VALIDATION_ERROR with the valid command list; select a leaf command directly.
87
+ Leaf command --help returns its description, accepted flags with defaults, and examples; positional arguments appear in those examples.
88
+ Use the failing leaf's help to correct syntax; do not run a help sweep before investigating.
89
+
90
+ Reviewed read allowlist: home (also no arguments), config path, profile list, auth status, doctor, email search, email show, event list, event show, task show, exception list, exception show (except unsupported anomaly detail).
91
+ These commands never mutate remote state or save downloaded files; API authentication exchanges tokens held only in memory.
92
+ Exclude email quarantine/restore, event action and every exception create/update/remove/remove-items/clear command, including previews and --specific creates, from an investigation read allowlist.
93
+ Writes require exact profile operation scope, preview, --execute, exact --confirm and durable redacted intent; forced read-only always wins.
94
+ Email download and attachment download/download-all write local files and require separate download opt-in; they are outside the read allowlist.
95
+ See [write safety](writes.md) and [download safety](downloads.md) before requesting them.
96
+
97
+ ## Failure recovery
98
+
99
+ Exit 0 means success, preview or no-op; 2 means invalid syntax (VALIDATION_ERROR); 1 means configuration, authentication, transport or operation failure.
100
+ Structured errors and suggestions are on stdout; follow the code and suggestion instead of retrying blindly.
101
+
102
+ | Code | Meaning and next action |
103
+ |---|---|
104
+ | VALIDATION_ERROR | Invalid flags, IDs, date window or mismatched cursor selectors; correct the failed command. |
105
+ | PROFILE_REQUIRED / CONFIG_INVALID | Missing or invalid configuration; select a complete provisioned profile. |
106
+ | CONFIG_INSECURE | Fix private-file permissions using the configuration guide. |
107
+ | AUTH_FAILED | Check region and secret source; the CLI already attempts one token re-exchange after API rejection. |
108
+ | AUTH_SHAPE_UNVERIFIED / ENDPOINT_UNVERIFIED | Contract is undocumented; request owner commissioning, never guess a route or token shape. |
109
+ | NOT_FOUND | No matching detail record; verify the ID and profile using a list read. |
110
+ | RESPONSE_INVALID / API_ERROR / HTTP_ERROR | Unexpected response or upstream failure; inspect the redacted error and verify service/profile before retrying. |
111
+ | RATE_LIMITED | Two read retries exhausted; defer work instead of immediate repeated calls. |
112
+ | REQUEST_BUDGET / TIME_BUDGET / RESPONSE_TOO_LARGE | Reduce date window or limit; inspect incomplete results and continuation before another bounded read. |
113
+ | WRITES_DISABLED / DOWNLOADS_DISABLED | Operation is outside profile permissions; do not loosen permissions during investigation. |
114
+ | OUTCOME_UNKNOWN | A mutation may have happened; reconcile with reads and the journal, never replay automatically. |
package/docs/commands.md CHANGED
@@ -6,6 +6,34 @@ Inspect Check Point Email Security through compact, guarded AXI commands.
6
6
  Email, event, task and exception reads use the regional API; doctor checks authentication and one bounded search, while discovery commands stay local.
7
7
  Run each command with `--help` for concise help.
8
8
 
9
+ [attachment download](#attachment-download) | [attachment download-all](#attachment-download-all) | [auth status](#auth-status) | [config path](#config-path) | [doctor](#doctor) | [email download](#email-download) | [email quarantine](#email-quarantine) | [email restore](#email-restore) | [email search](#email-search) | [email show](#email-show) | [event action](#event-action) | [event list](#event-list) | [event show](#event-show) | [exception clear](#exception-clear) | [exception create](#exception-create) | [exception list](#exception-list) | [exception remove](#exception-remove) | [exception remove-items](#exception-remove-items) | [exception show](#exception-show) | [exception update](#exception-update) | [home](#home) | [profile list](#profile-list) | [task show](#task-show)
10
+
11
+ | Command | Purpose |
12
+ |---|---|
13
+ | `checkpoint-axi attachment download` | Download one attachment to a new private file with profile opt-in, a 50 MiB cap and verified x-av-sha256; scope behavior is unverified (guide pages 133-135) |
14
+ | `checkpoint-axi attachment download-all` | Download available attachments as a ZIP to a new private file with opt-in, a 50 MiB cap and mandatory SHA-256 verification; ZIP hash availability and scopes are unverified; HTTP 200 may contain only some attachments (guide pages 135-137) |
15
+ | `checkpoint-axi auth status` | Validate the selected local profile and secret source without API calls |
16
+ | `checkpoint-axi config path` | Show config and state paths without reading files; --profile does not affect paths |
17
+ | `checkpoint-axi doctor` | Check regional API authentication and one bounded email search without printing tenant data |
18
+ | `checkpoint-axi email download` | Download an email to a new private file with profile opt-in, a 50 MiB cap and mandatory SHA-256 verification; scopes and email hash availability are unverified (guide pages 131-132) |
19
+ | `checkpoint-axi email quarantine` | Preview or request asynchronous email quarantine through the gated write coordinator (guide pages 28-30) |
20
+ | `checkpoint-axi email restore` | Preview or request asynchronous email restore through the gated write coordinator (guide pages 28-30) |
21
+ | `checkpoint-axi email search` | Search emails with a bounded date window and scroll continuation (API guide pages 18-27) |
22
+ | `checkpoint-axi email show` | Show one email with verdicts, actions and available actions (API guide pages 10-18) |
23
+ | `checkpoint-axi event action` | Preview or request a name-only event action; parameterized actions refuse ENDPOINT_UNVERIFIED pending commissioning (guide pages 35-37, 42-45) |
24
+ | `checkpoint-axi event list` | List security events with bounded scroll continuation (API guide pages 37-42) |
25
+ | `checkpoint-axi event show` | Show one event, actions taken and available actions (API guide pages 34-37) |
26
+ | `checkpoint-axi exception clear` | Refuse click-time/malware/url/dlp clear with ENDPOINT_UNVERIFIED until commissioning establishes counts and delete contracts (guide pages 87-88, 96-97, 107-110, 117-120, 128-131) |
27
+ | `checkpoint-axi exception create` | Preview or create one exception of any kind; anomaly/click-time/malware/url/dlp response identities are unverified (guide pages 61-64, 75-78, 84-85, 91-93, 102-105, 113-116, 123-126) |
28
+ | `checkpoint-axi exception list` | List bounded exceptions for phishing-allow, phishing-block, spam-allow, anomaly, click-time, malware, url or dlp (guide pages 49-123) |
29
+ | `checkpoint-axi exception remove` | Preview or remove one phishing/spam/anomaly/click-time exception by ID; malware/url/dlp refuse ENDPOINT_UNVERIFIED before authentication (guide pages 67-69, 81-82, 85-87, 98-99, 108-110, 119-120, 129-131) |
30
+ | `checkpoint-axi exception remove-items` | Preview or remove 1-20 click-time items by caller-supplied IDs; existence is unverified (guide pages 97-98) |
31
+ | `checkpoint-axi exception show` | Show an exception by kind and ID/value; anomaly refuses ENDPOINT_UNVERIFIED pending commissioning (guide pages 52-123) |
32
+ | `checkpoint-axi exception update` | Preview or replace one exception; phishing/spam/click-time require an ID; malware/url/dlp identify it in --input without an ID; anomaly unsupported (guide pages 64-67, 78-81, 94-96, 105-107, 116-117, 126-127) |
33
+ | `checkpoint-axi home` | Show local profile context without resolving secrets or making API calls |
34
+ | `checkpoint-axi profile list` | List file profiles without resolving secrets; --profile does not filter this list |
35
+ | `checkpoint-axi task show` | Show asynchronous task status and per-action details (API guide pages 30-33) |
36
+
9
37
  ## attachment download
10
38
 
11
39
  Download one attachment to a new private file with profile opt-in, a 50 MiB cap and verified x-av-sha256; scope behavior is unverified (guide pages 133-135)
@@ -169,7 +197,7 @@ Search emails with a bounded date window and scroll continuation (API guide page
169
197
  | `--limit` | Maximum rows; default 50, range 1-1000; cannot combine with --all |
170
198
  | `--all` | Read up to 1000 rows / 10 HTTP requests / 60 seconds / 8 MiB; default false |
171
199
  | `--cursor` | Opaque continuation bound to profile, region, operation and selectors; repeat original selectors |
172
- | `--filter` | JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20) |
200
+ | `--filter` | JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. saasAttrName is a field path, not a closed enum (guide pages 19-20). Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount. Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult. saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan. The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan. saasAttrValue is a string. |
173
201
 
174
202
  ```sh
175
203
  checkpoint-axi email search
@@ -0,0 +1,77 @@
1
+ # Configuration
2
+
3
+ See [environment variables and platform defaults](../README.md#configuration) for the complete variable table.
4
+
5
+ `config path` reports platform paths without reading files.
6
+ Config defaults to `$XDG_CONFIG_HOME/checkpoint-axi/config.json` (or `~/.config/checkpoint-axi/config.json`) on Linux/macOS, and `%APPDATA%\checkpoint-axi\config.json` on Windows.
7
+ State defaults to `$XDG_STATE_HOME/checkpoint-axi` (or `~/.local/state/checkpoint-axi`), and `%LOCALAPPDATA%\checkpoint-axi` on Windows.
8
+ Windows ignores XDG variables.
9
+ XDG and Windows base-directory overrides must be absolute; relative values use the platform defaults.
10
+ If APPDATA or LOCALAPPDATA is absent or relative, the Windows defaults are `~/AppData/Roaming` and `~/AppData/Local` respectively.
11
+ No command discovers config from the current directory or writes configuration.
12
+
13
+ Create a config file with this synthetic shape and protect it from group/other writes:
14
+
15
+ ```json
16
+ {
17
+ "version": 1,
18
+ "defaultProfile": "example",
19
+ "profiles": {
20
+ "example": {
21
+ "region": "eu",
22
+ "clientId": "synthetic-client",
23
+ "clientSecret": { "env": "CHECKPOINT_AXI_CLIENT_SECRET" },
24
+ "saas": "office365_emails",
25
+ "downloads": { "allow": false },
26
+ "writes": { "allowWrites": false, "operations": [] }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ Supply the secret through your environment or an absolute private file SecretRef, never chat, argv or an inline config value.
33
+ Omitting `clientSecret` defaults its reference to `CHECKPOINT_AXI_CLIENT_SECRET`.
34
+ `auth status` resolves the selected reference locally and reports only its kind (`env` or `file`); it never prints the value, client ID, variable name or secret file path.
35
+ It confirms local readiness, not API authentication.
36
+ `profile list` and home never resolve secret files.
37
+ Supported regions are `us`, `eu`, `ca`, `au`, `uk`, `uae` and `in`; there is no origin override.
38
+ The host-facing [credential schema](../axi-credential.schema.json) describes one strict profile.
39
+
40
+ Config precedence is `--config`, then `CHECKPOINT_AXI_CONFIG`, then the platform path.
41
+ Profile precedence is `--profile`, `CHECKPOINT_AXI_PROFILE`, complete env profile, config default, sole file profile, then an explicit missing/ambiguous error.
42
+ An env profile requires `CHECKPOINT_AXI_REGION`, `CHECKPOINT_AXI_CLIENT_ID` and `CHECKPOINT_AXI_CLIENT_SECRET` together; optional `CHECKPOINT_AXI_SAAS` and `CHECKPOINT_AXI_SCOPE` cannot stand alone.
43
+ File-selected metadata never mixes with env-profile fields, though an explicit SecretRef can name an environment variable.
44
+ Environment profiles always disable writes and downloads.
45
+ `CHECKPOINT_AXI_READ_ONLY` accepts `1`/`true` or `0`/`false` case-insensitively; any other value fails closed.
46
+ `CHECKPOINT_AXI_WRITE_LOG` overrides the write journal, defaulting to `writes.log` in the platform state directory.
47
+ An empty override uses that default.
48
+
49
+ POSIX reads reject symlinks, foreign ownership, group/other-writable config and any group/other access to secret files.
50
+ Secret file content is UTF-8 with a leading BOM and trailing whitespace removed.
51
+ On Windows, regular files and reparse points are checked without interpreting mode bits; `auth status`, `doctor` and email, event, task and exception API commands also verify a protected DACL for the current user, SYSTEM and Administrators.
52
+
53
+ ## SaaS and scope
54
+
55
+ `saas` is one non-blank SaaS application name, not a gateway, host or authentication URL.
56
+ Obtain the exact API application name for your tenant from its administrator or Check Point support.
57
+ If omitted, it defaults to `office365_emails`, selecting Microsoft 365 email; choose the appropriate name when targeting another application.
58
+ `office365_emails` is valid; `https://example.test/auth` and `example.test/auth` are invalid because slash (`/`) is not allowed.
59
+ A profile takes one value, not a comma-separated list.
60
+ `doctor` and `email search` accept `--saas <name>` for a single-call override; `event list --saas <names>` accepts a comma-separated list.
61
+
62
+ `scope` is a download scope value supplied by your tenant administrator or Check Point support for the intended download.
63
+ Use printable ASCII with no spaces (characters `!` through `~`); comma-separated values are sent verbatim.
64
+ For example, `synthetic:scope` has a valid form, while `synthetic scope` does not; the example is not a tenant-authorized value.
65
+ There is no default: omitted means unset.
66
+ Do not guess a tenant-authorized value.
67
+ See [download requirements](downloads.md) for how email and attachment commands use scope, the remaining verification limits, and the separate download opt-in and output checks.
68
+
69
+ ## Write and download permissions
70
+
71
+ File profiles are read-only by default.
72
+ Set `writes.allowWrites` to true and list each exact operation in the `writes` object's `operations` array only when that permission is intended.
73
+ Environment profiles cannot enable writes or downloads.
74
+ `CHECKPOINT_AXI_READ_ONLY=1` overrides all configured write permission.
75
+ Downloads require the separate `downloads.allow: true` file-profile opt-in.
76
+ See [operation gates](writes.md) and [download requirements](downloads.md); profile permission alone does not execute an operation.
77
+