@knowttl/checkpoint-axi 0.0.0-stage → 0.1.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -2
  3. package/axi-credential.schema.json +35 -0
  4. package/bin/checkpoint-axi-hook.js +9 -0
  5. package/bin/checkpoint-axi.js +8 -0
  6. package/bin/version.js +3 -0
  7. package/dist/src/catalogue.d.ts +317 -0
  8. package/dist/src/catalogue.js +103 -0
  9. package/dist/src/cli.d.ts +9 -0
  10. package/dist/src/cli.js +22 -0
  11. package/dist/src/doctor.d.ts +3 -0
  12. package/dist/src/doctor.js +37 -0
  13. package/dist/src/downloads.d.ts +3 -0
  14. package/dist/src/downloads.js +52 -0
  15. package/dist/src/exceptions.d.ts +37 -0
  16. package/dist/src/exceptions.js +81 -0
  17. package/dist/src/local-commands.d.ts +3 -0
  18. package/dist/src/local-commands.js +123 -0
  19. package/dist/src/private-file.d.ts +10 -0
  20. package/dist/src/private-file.js +65 -0
  21. package/dist/src/profiles.d.ts +122 -0
  22. package/dist/src/profiles.js +137 -0
  23. package/dist/src/reads.d.ts +3 -0
  24. package/dist/src/reads.js +169 -0
  25. package/dist/src/redact.d.ts +6 -0
  26. package/dist/src/redact.js +25 -0
  27. package/dist/src/regions.d.ts +9 -0
  28. package/dist/src/regions.js +10 -0
  29. package/dist/src/transport.d.ts +86 -0
  30. package/dist/src/transport.js +306 -0
  31. package/dist/src/write-exceptions.d.ts +4 -0
  32. package/dist/src/write-exceptions.js +97 -0
  33. package/dist/src/write-sectools.d.ts +5 -0
  34. package/dist/src/write-sectools.js +107 -0
  35. package/dist/src/writes.d.ts +30 -0
  36. package/dist/src/writes.js +295 -0
  37. package/docs/commands.md +461 -0
  38. package/inventory/capabilities.json +217 -0
  39. package/inventory/specs.json +12 -0
  40. package/package.json +54 -4
  41. package/skills/checkpoint-axi/SKILL.md +102 -0
@@ -0,0 +1,169 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { createHash } from "node:crypto";
3
+ import { z } from "zod";
4
+ import { REGIONS } from "./regions.js";
5
+ import { gateway, READ_LIMITS } from "./transport.js";
6
+ const day = 86_400_000;
7
+ const timestamp = z.iso.datetime({ offset: true });
8
+ const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
9
+ saasAttrOp: z.enum(["is", "contains", "startsWith", "isEmpty", "isNot", "notContains", "isNotEmpty", "greaterThan", "lessThan"]),
10
+ saasAttrValue: z.string() }));
11
+ const continuation = z.strictObject({ version: z.literal(1), binding: z.string(), start: timestamp, end: timestamp,
12
+ scroll: z.string().optional(), offset: z.number().int().nonnegative(), replayRisk: z.literal(true).optional() });
13
+ // Entity schemas and SaaS-specific payloads: guide pages 11-15 and 21-24.
14
+ const entity = z.object({ entityInfo: z.object({ entityId: z.string().min(1), entityActionState: z.string().optional() }).passthrough(),
15
+ entityPayload: z.object({ subject: z.string().optional() }).passthrough(),
16
+ entitySecurityResults: z.object({ combinedVerdict: z.unknown().optional() }).passthrough().optional(),
17
+ }).passthrough();
18
+ // Event and task fields: guide pages 31-32, 35-36 and 40-41.
19
+ const event = z.object({ eventId: z.string().min(1), type: z.string(), state: z.string(), severity: z.string() }).passthrough();
20
+ const task = z.object({ id: z.union([z.string().min(1), z.number().int().nonnegative()]),
21
+ status: z.enum(["init", "inprogress", "completed", "failed", "stopped", "paused"]),
22
+ actions: z.array(z.object({ action_status: z.string() }).passthrough()),
23
+ progress: z.number().nonnegative(), total: z.number().nonnegative(),
24
+ }).passthrough();
25
+ const invalid = (message) => { throw new AxiError(message, "VALIDATION_ERROR", ["Run checkpoint-axi <email search|email show|event list|event show|task show> --help"]); };
26
+ export async function readCommand(name, id, flags, store, transport) {
27
+ const isEvent = name.startsWith("event ");
28
+ if (name.endsWith(" show") && (!id || !/^[A-Za-z0-9_-]+$/.test(id)))
29
+ invalid(`${name} requires one ID containing letters, digits, underscores or hyphens`);
30
+ const limitText = flags.get("--limit") ?? "50";
31
+ if (!/^[1-9][0-9]*$/.test(limitText) || Number(limitText) > READ_LIMITS.rows)
32
+ invalid("--limit must be an integer from 1 to 1000");
33
+ if (flags.has("--all") && flags.has("--limit"))
34
+ invalid("Use either --all or --limit");
35
+ const limit = flags.has("--all") ? READ_LIMITS.rows : Number(limitText);
36
+ let extended;
37
+ if (flags.has("--filter")) {
38
+ let raw;
39
+ try {
40
+ raw = JSON.parse(flags.get("--filter"));
41
+ }
42
+ catch {
43
+ invalid("--filter must be a JSON array of attribute filters");
44
+ }
45
+ const parsed = filters.safeParse(raw);
46
+ if (!parsed.success)
47
+ invalid("--filter must contain saasAttrName, a documented saasAttrOp and saasAttrValue");
48
+ extended = parsed.data;
49
+ }
50
+ let cursor;
51
+ if (flags.has("--cursor")) {
52
+ try {
53
+ const encoded = flags.get("--cursor");
54
+ if (!/^[A-Za-z0-9_-]+$/.test(encoded))
55
+ invalid("Invalid continuation cursor");
56
+ cursor = continuation.parse(JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")));
57
+ }
58
+ catch {
59
+ invalid("Invalid continuation cursor");
60
+ }
61
+ }
62
+ const end = flags.get("--end-date") ?? cursor?.end ?? new Date().toISOString();
63
+ if (!timestamp.safeParse(end).success)
64
+ invalid("--end-date must be an ISO timestamp with timezone");
65
+ const start = flags.get("--start-date") ?? cursor?.start ?? new Date(Date.parse(end) - day).toISOString();
66
+ if (!timestamp.safeParse(start).success)
67
+ invalid("--start-date must be an ISO timestamp with timezone");
68
+ const window = Date.parse(end) - Date.parse(start);
69
+ if (window <= 0 || window > 31 * day)
70
+ invalid("Search window must be positive and at most 31 days");
71
+ for (const flag of ["--saas", "--saas-entity"])
72
+ if (flags.has(flag) && !flags.get(flag).trim())
73
+ invalid(`${flag} must not be blank`);
74
+ const eventSelectors = {};
75
+ for (const [flag, key] of [["event-types", "eventTypes"], ["event-states", "eventStates"], ["severities", "severities"], ["event-ids", "eventIds"], ["saas", "saas"]]) {
76
+ if (!isEvent || !flags.has(`--${flag}`))
77
+ continue;
78
+ const values = flags.get(`--${flag}`).split(",").map((value) => value.trim());
79
+ if (values.some((value) => !value))
80
+ invalid(`--${flag} must contain nonempty comma-separated values`);
81
+ eventSelectors[key] = values;
82
+ }
83
+ for (const [flag, key] of [["confidence-indicator", "confidenceIndicator"], ["description", "description"]]) {
84
+ if (!flags.has(`--${flag}`))
85
+ continue;
86
+ const value = flags.get(`--${flag}`).trim();
87
+ if (!value)
88
+ invalid(`--${flag} must not be blank`);
89
+ eventSelectors[key] = value;
90
+ }
91
+ store.readOnly();
92
+ const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
93
+ const saas = flags.get("--saas")?.trim() ?? profile.saas;
94
+ const entityFilter = { saas, ...(flags.has("--saas-entity") ? { saasEntity: flags.get("--saas-entity").trim() } : {}), startDate: start, endDate: end };
95
+ const selectors = { ...eventSelectors, saas: eventSelectors.saas ?? [saas], startDate: start, endDate: end };
96
+ const binding = createHash("sha256").update(JSON.stringify({ profile: profile.name, client: profile.clientId,
97
+ origin: REGIONS[profile.region], operation: name, ...(isEvent ? { selectors } : { entityFilter, extended }) })).digest("hex");
98
+ if (cursor && cursor.binding !== binding)
99
+ invalid("Cursor does not match this profile, region or search selectors; repeat the original selectors");
100
+ const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
101
+ if (name.endsWith(" show")) {
102
+ const kind = name.split(" ")[0];
103
+ const page = await api.read({ path: name === "task show" ? `/task/${id}` : isEvent ? `/event/${id}` : `/search/entity/${id}` });
104
+ if (page.records.length === 0)
105
+ throw new AxiError(`${kind} was not found`, "NOT_FOUND", []);
106
+ 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", []);
109
+ const record = page.records[0];
110
+ const returnedId = name === "task show" ? String(record.id) : isEvent ? record.eventId : record.entityInfo?.entityId;
111
+ if (returnedId !== id)
112
+ throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
113
+ return { [kind]: parsed.data };
114
+ }
115
+ const results = [];
116
+ let scroll = cursor?.scroll;
117
+ let offset = cursor?.offset ?? 0;
118
+ let replayRisk = cursor?.replayRisk ?? offset > 0;
119
+ let total;
120
+ let exhausted = false;
121
+ while (results.length < limit) {
122
+ let page;
123
+ try {
124
+ page = await api.read(isEvent ? { path: "/event/query", body: { ...selectors, ...(scroll === undefined ? {} : { scrollId: scroll }) } }
125
+ : { path: "/search/query", body: { entityFilter,
126
+ ...(extended === undefined ? {} : { entityExtendedFilter: extended }), ...(scroll === undefined ? {} : { scrollId: scroll }) } });
127
+ }
128
+ catch (error) {
129
+ if (results.length && error instanceof AxiError && ["REQUEST_BUDGET", "TIME_BUDGET", "RESPONSE_TOO_LARGE"].includes(error.code))
130
+ break;
131
+ throw error;
132
+ }
133
+ 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", []);
136
+ // Never wrap an echoed credential in a reversible continuation cursor.
137
+ if (page.scrollId && store.redactor.text(page.scrollId) !== page.scrollId)
138
+ throw new AxiError("Unsafe API scrollId", "RESPONSE_INVALID", []);
139
+ total = page.total ?? total;
140
+ const rows = parsed.data.slice(offset, offset + limit - results.length);
141
+ results.push(...rows.map((record) => {
142
+ if (isEvent) {
143
+ const value = event.parse(record);
144
+ return { id: value.eventId, type: value.type, state: value.state, severity: value.severity };
145
+ }
146
+ const value = entity.parse(record);
147
+ return { id: value.entityInfo.entityId, subject: value.entityPayload.subject ?? "",
148
+ state: value.entityInfo.entityActionState ?? "", verdict: value.entitySecurityResults?.combinedVerdict ?? null };
149
+ }));
150
+ offset += rows.length;
151
+ if (offset < page.records.length) {
152
+ replayRisk = true;
153
+ break;
154
+ }
155
+ if (!page.scrollId) {
156
+ exhausted = true;
157
+ break;
158
+ }
159
+ scroll = page.scrollId;
160
+ offset = 0;
161
+ }
162
+ const next = exhausted ? undefined : Buffer.from(JSON.stringify({ version: 1, binding, start, end, scroll, offset,
163
+ ...(replayRisk ? { replayRisk: true } : {}) })).toString("base64url");
164
+ return { [isEvent ? "events" : "emails"]: results, count: results.length, ...(total === undefined ? {} : { total }), complete: exhausted && !replayRisk,
165
+ ...(replayRisk ? { reason: "replay-may-skip-or-repeat" } : {}), ...(next ? { cursor: next } : {}),
166
+ help: [...(results.length ? [isEvent ? "Run checkpoint-axi event show <id> for event details and available actions" : "Run checkpoint-axi email show <id> for verdicts, actions and available actions"] : [`No ${isEvent ? "events" : "emails"} matched this search window and selectors`]),
167
+ ...(next ? ["Repeat this search with --cursor <cursor> and the original selectors to continue"] : []),
168
+ ...(replayRisk ? ["Results changed between calls may be skipped or repeated during page replay"] : [])] };
169
+ }
@@ -0,0 +1,6 @@
1
+ export declare class SecretRedactor {
2
+ private readonly secrets;
3
+ add(secret: string | undefined): void;
4
+ text(value: string): string;
5
+ value(value: unknown): unknown;
6
+ }
@@ -0,0 +1,25 @@
1
+ export class SecretRedactor {
2
+ secrets = new Set();
3
+ add(secret) {
4
+ if (!secret)
5
+ return;
6
+ this.secrets.add(secret);
7
+ this.secrets.add(JSON.stringify(secret).slice(1, -1));
8
+ if (secret.isWellFormed())
9
+ this.secrets.add(encodeURIComponent(secret));
10
+ }
11
+ text(value) {
12
+ for (const secret of [...this.secrets].sort((a, b) => b.length - a.length))
13
+ value = value.split(secret).join("***redacted***");
14
+ return value;
15
+ }
16
+ value(value) {
17
+ if (typeof value === "string")
18
+ return this.text(value);
19
+ if (Array.isArray(value))
20
+ return value.map((child) => this.value(child));
21
+ if (value && typeof value === "object")
22
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [this.text(key), this.value(child)]));
23
+ return value;
24
+ }
25
+ }
@@ -0,0 +1,9 @@
1
+ export declare const REGIONS: {
2
+ readonly us: "https://cloudinfra-gw-us.portal.checkpoint.com";
3
+ readonly eu: "https://cloudinfra-gw.portal.checkpoint.com";
4
+ readonly ca: "https://cloudinfra-gw.ca.portal.checkpoint.com";
5
+ readonly au: "https://cloudinfra-gw.ap.portal.checkpoint.com";
6
+ readonly uk: "https://cloudinfra-gw.uk.portal.checkpoint.com";
7
+ readonly uae: "https://cloudinfra-gw.me.portal.checkpoint.com";
8
+ readonly in: "https://cloudinfra-gw.in.portal.checkpoint.com";
9
+ };
@@ -0,0 +1,10 @@
1
+ // Email Security API Reference Guide, 23 October 2026, page 8.
2
+ export const REGIONS = {
3
+ us: "https://cloudinfra-gw-us.portal.checkpoint.com",
4
+ eu: "https://cloudinfra-gw.portal.checkpoint.com",
5
+ ca: "https://cloudinfra-gw.ca.portal.checkpoint.com",
6
+ au: "https://cloudinfra-gw.ap.portal.checkpoint.com",
7
+ uk: "https://cloudinfra-gw.uk.portal.checkpoint.com",
8
+ uae: "https://cloudinfra-gw.me.portal.checkpoint.com",
9
+ in: "https://cloudinfra-gw.in.portal.checkpoint.com",
10
+ };
@@ -0,0 +1,86 @@
1
+ import { type Agent } from "node:https";
2
+ import type { SelectedProfile } from "./profiles.js";
3
+ import type { SecretRedactor } from "./redact.js";
4
+ import { type WriteAuthorization } from "./writes.js";
5
+ export declare const READ_LIMITS: {
6
+ readonly requests: 10;
7
+ readonly rows: 1000;
8
+ readonly milliseconds: 60000;
9
+ readonly bytes: number;
10
+ };
11
+ export declare const DOWNLOAD_LIMIT: number;
12
+ export type DownloadRequest = {
13
+ entityId: string;
14
+ attachmentId?: string;
15
+ all?: boolean;
16
+ original?: boolean;
17
+ scope?: string;
18
+ };
19
+ export type ExceptionPath = `/exceptions/${"whitelist" | "blacklist" | "spam_whitelist"}${"" | `/${string}`}` | `/sectools/${"anomaly" | "click_time_protection"}/exceptions${"" | `/${string}`}` | `/sectool-exceptions/${"checkpoint2" | "avanan_url" | "avanan_dlp"}/exceptions/${string}`;
20
+ export type ReadRequest = {
21
+ path: "/search/query";
22
+ body: {
23
+ entityFilter: {
24
+ saas: string;
25
+ saasEntity?: string;
26
+ startDate: string;
27
+ endDate: string;
28
+ };
29
+ entityExtendedFilter?: {
30
+ saasAttrName: string;
31
+ saasAttrOp: string;
32
+ saasAttrValue: string;
33
+ }[];
34
+ scrollId?: string;
35
+ };
36
+ } | {
37
+ path: `/search/entity/${string}`;
38
+ body?: never;
39
+ } | {
40
+ path: "/event/query";
41
+ body: {
42
+ startDate?: string;
43
+ endDate?: string;
44
+ saas?: string[];
45
+ eventTypes?: string[];
46
+ eventStates?: string[];
47
+ severities?: string[];
48
+ eventIds?: string[];
49
+ confidenceIndicator?: string;
50
+ description?: string;
51
+ scrollId?: string;
52
+ };
53
+ } | {
54
+ path: `/event/${string}` | `/task/${string}`;
55
+ body?: never;
56
+ } | {
57
+ path: ExceptionPath;
58
+ body?: never;
59
+ };
60
+ export type Page = {
61
+ records: unknown[];
62
+ scrollId?: string;
63
+ total?: number;
64
+ };
65
+ type Dependencies = {
66
+ agent?: Agent;
67
+ now?: () => number;
68
+ wait?: (milliseconds: number) => Promise<unknown>;
69
+ };
70
+ export declare function decodePage(value: unknown, unverified?: boolean): Page;
71
+ export declare function gateway(profile: SelectedProfile, credentials: {
72
+ clientId: string;
73
+ clientSecret: string;
74
+ }, redactor: SecretRedactor, dependencies?: Dependencies, mutation?: boolean): {
75
+ authenticate: () => Promise<void>;
76
+ read: (query: ReadRequest) => Promise<Page>;
77
+ download: (query: DownloadRequest) => Promise<{
78
+ content: Buffer;
79
+ sha256: string;
80
+ }>;
81
+ emailAction: (authorization: WriteAuthorization, action: "quarantine" | "restore", targets: readonly string[]) => Promise<Page>;
82
+ eventAction: (authorization: WriteAuthorization, action: string, targets: readonly string[]) => Promise<Page>;
83
+ exceptionWrite: (authorization: WriteAuthorization, kind: "phishing-allow" | "phishing-block" | "spam-allow", action: "create" | "update" | "remove", target: string, checkScopeBeforeSend: () => void, data?: Record<string, unknown>) => Promise<void>;
84
+ sectoolsWrite: (authorization: WriteAuthorization, kind: "anomaly" | "click-time" | "malware" | "url" | "dlp", action: "create" | "update" | "remove" | "remove-items", targets: readonly string[], checkScopeBeforeSend: () => void, data?: Record<string, unknown>, specific?: boolean) => Promise<unknown>;
85
+ };
86
+ export {};
@@ -0,0 +1,306 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { request } from "node:https";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { REGIONS } from "./regions.js";
6
+ import { consumeWriteAuthorization } from "./writes.js";
7
+ export const READ_LIMITS = { requests: 10, rows: 1_000, milliseconds: 60_000, bytes: 8 * 1024 * 1024 };
8
+ export const DOWNLOAD_LIMIT = 50 * 1024 * 1024;
9
+ const failure = (code, message) => new AxiError(message, code, ["Check the selected region and credentials; run checkpoint-axi doctor"]);
10
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
11
+ // Object/array responseData inconsistency is documented on guide pages 35-37.
12
+ export function decodePage(value, unverified = false) {
13
+ // These exception families publish no response schema (guide pages 83-123).
14
+ if (unverified && (!object(value) || !Object.hasOwn(value, "responseEnvelope"))) {
15
+ return { records: Array.isArray(value) ? value : [value] };
16
+ }
17
+ if (!object(value) || !object(value.responseEnvelope))
18
+ throw failure("RESPONSE_INVALID", "Missing API response envelope");
19
+ const envelope = value.responseEnvelope;
20
+ if (envelope.responseCode !== 0 && envelope.responseCode !== "0" && envelope.responseCode !== 200 && envelope.responseCode !== "200") {
21
+ throw failure("API_ERROR", "API response envelope did not report success");
22
+ }
23
+ const data = value.responseData;
24
+ if ((!unverified && !Array.isArray(data) && !object(data)) || data === undefined)
25
+ throw failure("RESPONSE_INVALID", "Expected responseData");
26
+ if (envelope.scrollId !== undefined && envelope.scrollId !== null && typeof envelope.scrollId !== "string") {
27
+ throw failure("RESPONSE_INVALID", "Invalid API scrollId");
28
+ }
29
+ if (envelope.totalRecordsNumber !== undefined && (!Number.isSafeInteger(envelope.totalRecordsNumber) || Number(envelope.totalRecordsNumber) < 0)) {
30
+ throw failure("RESPONSE_INVALID", "Invalid API totalRecordsNumber");
31
+ }
32
+ return { records: Array.isArray(data) ? data : [data], scrollId: typeof envelope.scrollId === "string" && envelope.scrollId ? envelope.scrollId : undefined,
33
+ total: envelope.totalRecordsNumber };
34
+ }
35
+ // No field name or lifetime is commissioned yet (guide page 8).
36
+ function tokenFrom(value, redactor) {
37
+ const register = (child) => {
38
+ if (typeof child === "string")
39
+ redactor.add(child);
40
+ else if (Array.isArray(child))
41
+ child.forEach(register);
42
+ else if (object(child))
43
+ Object.values(child).forEach(register);
44
+ };
45
+ register(value);
46
+ const tokens = new Set();
47
+ let invalid = false;
48
+ if (object(value))
49
+ for (const container of [value, value.data, value.responseData]) {
50
+ if (!object(container))
51
+ continue;
52
+ for (const key of ["token", "access_token", "accessToken"]) {
53
+ const candidate = container[key];
54
+ if (candidate === undefined)
55
+ continue;
56
+ if (typeof candidate === "string" && candidate.length > 0 && /^[\x21-\x7E]+$/.test(candidate))
57
+ tokens.add(candidate);
58
+ else
59
+ invalid = true;
60
+ }
61
+ }
62
+ if (invalid || tokens.size !== 1)
63
+ throw failure("AUTH_SHAPE_UNVERIFIED", "Unverified auth token response shape; owner commissioning is required (API guide page 8 does not specify the shape)");
64
+ return [...tokens][0];
65
+ }
66
+ export function gateway(profile, credentials, redactor, dependencies = {}, mutation = false) {
67
+ const now = dependencies.now ?? Date.now;
68
+ const wait = dependencies.wait ?? delay;
69
+ const deadline = now() + READ_LIMITS.milliseconds;
70
+ let bytes = 0;
71
+ let requests = 0;
72
+ let token;
73
+ let refreshed = false;
74
+ const nextException = new Map();
75
+ redactor.add(credentials.clientSecret);
76
+ redactor.add(JSON.stringify({ clientId: credentials.clientId, accessKey: credentials.clientSecret }));
77
+ async function pause(milliseconds) {
78
+ if (!Number.isFinite(milliseconds) || milliseconds < 0 || now() + milliseconds >= deadline)
79
+ throw failure("TIME_BUDGET", "Read time budget exhausted");
80
+ await wait(milliseconds);
81
+ }
82
+ async function send(path, body, bearer, method = body === undefined ? "GET" : "POST", allowEmpty = false, binary = false, scopes) {
83
+ const remaining = deadline - now();
84
+ if (remaining <= 0)
85
+ throw failure("TIME_BUDGET", "Read time budget exhausted");
86
+ if (++requests > (mutation ? 44 : READ_LIMITS.requests))
87
+ throw failure("REQUEST_BUDGET", "Request budget exhausted");
88
+ const payload = body === undefined ? undefined : JSON.stringify(body);
89
+ if (bearer)
90
+ redactor.add(`Bearer ${bearer}`);
91
+ return new Promise((resolve, reject) => {
92
+ const req = request(new URL(path, REGIONS[profile.region]), {
93
+ method, agent: dependencies.agent,
94
+ headers: { "x-av-req-id": randomUUID(), accept: "application/json", ...(bearer ? { authorization: `Bearer ${bearer}` } : {}),
95
+ ...(scopes === undefined ? {} : { scopes }),
96
+ ...(payload === undefined ? {} : { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }) },
97
+ }, (response) => {
98
+ const chunks = [];
99
+ let received = 0;
100
+ response.on("data", (chunk) => {
101
+ received += chunk.length;
102
+ if (!binary)
103
+ bytes += chunk.length;
104
+ if (binary ? received > DOWNLOAD_LIMIT : bytes > READ_LIMITS.bytes)
105
+ req.destroy(failure("RESPONSE_TOO_LARGE", "Response byte budget exhausted"));
106
+ else
107
+ chunks.push(chunk);
108
+ });
109
+ response.on("error", () => {
110
+ clearTimeout(timer);
111
+ reject(failure("NETWORK_ERROR", "API response stream failed"));
112
+ });
113
+ response.on("end", () => {
114
+ clearTimeout(timer);
115
+ const status = response.statusCode ?? 0;
116
+ // Error bodies are untrusted and may echo credentials. Never render them.
117
+ if (status < 200 || status >= 300)
118
+ return resolve({ status, retryAfter: response.headers["retry-after"], value: undefined });
119
+ if (binary)
120
+ return resolve({ status, value: Buffer.concat(chunks), sha256: typeof response.headers["x-av-sha256"] === "string" ? response.headers["x-av-sha256"] : undefined });
121
+ const json = Buffer.concat(chunks).toString("utf8");
122
+ if (allowEmpty && status === 204 && !json)
123
+ return resolve({ status, value: null });
124
+ try {
125
+ resolve({ status, value: JSON.parse(json) });
126
+ }
127
+ catch {
128
+ reject(failure("RESPONSE_INVALID", "API returned invalid JSON"));
129
+ }
130
+ });
131
+ });
132
+ const timer = setTimeout(() => req.destroy(failure("TIME_BUDGET", "Read time budget exhausted")), remaining);
133
+ req.on("error", (error) => {
134
+ clearTimeout(timer);
135
+ reject(error instanceof AxiError ? error : failure("NETWORK_ERROR", "Cannot reach the regional API with verified TLS"));
136
+ });
137
+ req.end(payload);
138
+ });
139
+ }
140
+ async function authenticate() {
141
+ if (token)
142
+ return;
143
+ const response = await send("/auth/external", { clientId: credentials.clientId, accessKey: credentials.clientSecret });
144
+ if (response.status < 200 || response.status >= 300)
145
+ throw failure("AUTH_FAILED", "Regional API authentication failed");
146
+ token = tokenFrom(response.value, redactor);
147
+ }
148
+ async function spaceException(path) {
149
+ const group = /^\/exceptions\/(whitelist|blacklist)(\/|$)/.test(path) ? "phishing"
150
+ : /^\/(exceptions|sectools|sectool-exceptions)\//.test(path) ? "other" : undefined;
151
+ if (group) {
152
+ const remaining = (nextException.get(group) ?? 0) - now();
153
+ if (remaining > 0)
154
+ await pause(remaining);
155
+ nextException.set(group, now() + (group === "phishing" ? 1_000 : 100));
156
+ }
157
+ }
158
+ async function read(query) {
159
+ await authenticate();
160
+ for (let retries = 0;;) {
161
+ await spaceException(query.path);
162
+ const response = await send(`/app/hec-api/v1.0${query.path}`, query.body === undefined ? undefined : { requestData: query.body }, token);
163
+ if (response.status === 401 && !refreshed) {
164
+ refreshed = true;
165
+ token = undefined;
166
+ await authenticate();
167
+ continue;
168
+ }
169
+ if (response.status === 429 && retries++ < 2) {
170
+ const header = response.retryAfter;
171
+ const milliseconds = header === undefined ? 1_000 : /^\d+$/.test(header) ? Number(header) * 1_000 : Math.max(0, Date.parse(header) - now());
172
+ await pause(Number.isFinite(milliseconds) ? milliseconds : 1_000);
173
+ continue;
174
+ }
175
+ if (response.status === 401)
176
+ throw failure("AUTH_FAILED", "API rejected authentication after one re-exchange");
177
+ if (response.status === 429)
178
+ throw failure("RATE_LIMITED", "API rate limit remains after two read retries");
179
+ if (response.status < 200 || response.status >= 300)
180
+ throw failure("HTTP_ERROR", `Regional API returned HTTP ${response.status}`);
181
+ return decodePage(response.value, /^\/(sectools|sectool-exceptions)\//.test(query.path));
182
+ }
183
+ }
184
+ async function download(query) {
185
+ if (!profile.downloads.allow)
186
+ throw failure("DOWNLOADS_DISABLED", "Enable downloads.allow in the selected file profile");
187
+ const validId = (value) => /^[A-Za-z0-9_-]{1,256}$/.test(value);
188
+ if (!validId(query.entityId) || query.attachmentId !== undefined && !validId(query.attachmentId)
189
+ || query.all && query.attachmentId !== undefined || (query.all || query.attachmentId !== undefined) && query.original !== undefined
190
+ || query.scope !== undefined && !/^[\x21-\x7E]+$/.test(query.scope)
191
+ || profile.scope !== undefined && !/^[\x21-\x7E]+$/.test(profile.scope))
192
+ throw failure("VALIDATION_ERROR", "Invalid download selectors or scopes");
193
+ const attachment = query.all || query.attachmentId !== undefined;
194
+ if (!attachment && !profile.scope)
195
+ throw failure("DOWNLOAD_SCOPE_REQUIRED", "Email downloads require the profile scope for the scopes header");
196
+ const params = new URLSearchParams();
197
+ if (attachment && query.scope)
198
+ params.set("scope", query.scope);
199
+ if (!attachment)
200
+ params.set("original", String(query.original ?? false));
201
+ const path = `/app/hec-api/v1.0/download/entity/${encodeURIComponent(query.entityId)}${query.all ? "/attachments" : query.attachmentId ? `/attachment/${encodeURIComponent(query.attachmentId)}` : ""}${params.size ? `?${params}` : ""}`;
202
+ await authenticate();
203
+ for (let retries = 0;;) {
204
+ const response = await send(path, undefined, token, "GET", false, true, attachment ? undefined : profile.scope);
205
+ if (response.status === 401 && !refreshed) {
206
+ refreshed = true;
207
+ token = undefined;
208
+ await authenticate();
209
+ continue;
210
+ }
211
+ if (response.status === 429 && retries++ < 2) {
212
+ const header = response.retryAfter;
213
+ const milliseconds = header === undefined ? 1_000 : /^\d+$/.test(header) ? Number(header) * 1_000 : Math.max(0, Date.parse(header) - now());
214
+ await pause(Number.isFinite(milliseconds) ? milliseconds : 1_000);
215
+ continue;
216
+ }
217
+ if (response.status === 401)
218
+ throw failure("AUTH_FAILED", "API rejected authentication after one re-exchange");
219
+ if (response.status === 429)
220
+ throw failure("RATE_LIMITED", "API rate limit remains after two download retries");
221
+ if (response.status !== 200)
222
+ throw failure("HTTP_ERROR", `Regional API returned HTTP ${response.status}`);
223
+ if (!response.sha256 || !/^[a-fA-F0-9]{64}$/.test(response.sha256))
224
+ throw failure("DOWNLOAD_HASH_INVALID", "Download SHA-256 header is missing or invalid; commissioning may be required");
225
+ return { content: response.value, sha256: response.sha256.toLowerCase() };
226
+ }
227
+ }
228
+ // Closed entity-action route; mutations never enter the read retry loop.
229
+ async function emailAction(authorization, action, targets) {
230
+ if (!mutation || !consumeWriteAuthorization(authorization, `email.${action}`, targets, profile.name, profile.region)) {
231
+ throw failure("WRITES_DISABLED", "A one-use coordinator authorization is required");
232
+ }
233
+ await authenticate();
234
+ try {
235
+ const response = await send("/app/hec-api/v1.0/action/entity", { requestData: {
236
+ entityIds: targets, entityActionName: [action], entityActionParam: [""],
237
+ } }, token);
238
+ if (response.status < 200 || response.status >= 300)
239
+ throw new Error("Action response requires reconciliation");
240
+ return decodePage(response.value);
241
+ }
242
+ catch {
243
+ throw new AxiError("Email action outcome is unknown; never replay automatically", "OUTCOME_UNKNOWN", [
244
+ "Inspect the write journal and read back every target before deciding what to do next",
245
+ ]);
246
+ }
247
+ }
248
+ // Closed event-action route; authorization cannot be reused or supplied by argv.
249
+ async function eventAction(authorization, action, targets) {
250
+ if (!mutation || !consumeWriteAuthorization(authorization, `event.action.${action}`, targets, profile.name, profile.region)) {
251
+ throw failure("WRITES_DISABLED", "A one-use coordinator authorization is required");
252
+ }
253
+ await authenticate();
254
+ const response = await send("/app/hec-api/v1.0/action/event", { requestData: {
255
+ eventIds: targets, eventActionName: [action], eventActionParam: [""],
256
+ } }, token);
257
+ if (response.status < 200 || response.status >= 300)
258
+ throw failure("OUTCOME_UNKNOWN", "Event action outcome requires reconciliation; never replay automatically");
259
+ return decodePage(response.value);
260
+ }
261
+ // Guide pp.61-69, 75-82: closed C8 routes; deletion is a bodyless POST.
262
+ async function exceptionWrite(authorization, kind, action, target, checkScopeBeforeSend, data) {
263
+ const routes = { "phishing-allow": "whitelist", "phishing-block": "blacklist", "spam-allow": "spam_whitelist" };
264
+ if (!mutation || !Object.hasOwn(routes, kind) || !["create", "update", "remove"].includes(action)
265
+ || !consumeWriteAuthorization(authorization, `exception.${kind}.${action}`, [target], profile.name, profile.region)) {
266
+ throw failure("WRITES_DISABLED", "A one-use coordinator authorization is required");
267
+ }
268
+ await authenticate();
269
+ const path = `/exceptions/${routes[kind]}${action === "create" ? "" : action === "remove" ? `/delete/${encodeURIComponent(target)}` : `/${encodeURIComponent(target)}`}`;
270
+ await spaceException(path);
271
+ checkScopeBeforeSend();
272
+ const response = await send(`/app/hec-api/v1.0${path}`, action === "remove" ? undefined : { requestData: data }, token, action === "update" ? "PUT" : "POST");
273
+ if (response.status < 200 || response.status >= 300 || !object(response.value) || !object(response.value.responseEnvelope)
274
+ || ![0, "0", 200, "200", ...(action === "remove" ? [204, "204"] : [])].includes(response.value.responseEnvelope.responseCode)) {
275
+ throw failure("OUTCOME_UNKNOWN", "Exception write acknowledgement requires reconciliation; never replay automatically");
276
+ }
277
+ }
278
+ // Guide pp.84-99,102-127: closed routes; request bodies distinguish shared POSTs.
279
+ // Response schemas are unpublished; preserve JSON, interpreting only HTTP/envelope status.
280
+ async function sectoolsWrite(authorization, kind, action, targets, checkScopeBeforeSend, data, specific = false) {
281
+ const sectools = { malware: "checkpoint2", url: "avanan_url", dlp: "avanan_dlp" };
282
+ const sectool = Object.hasOwn(sectools, kind);
283
+ if (sectool && action !== "create" && action !== "update")
284
+ throw failure("ENDPOINT_UNVERIFIED", "Sectool deletes require commissioning; nothing was sent");
285
+ if (!mutation || !["anomaly", "click-time", "malware", "url", "dlp"].includes(kind) || !["create", "update", "remove", "remove-items"].includes(action)
286
+ || kind === "anomaly" && action !== "create" && action !== "remove"
287
+ || specific && (!sectool || action !== "create")
288
+ || action !== "remove-items" && targets.length !== 1
289
+ || !consumeWriteAuthorization(authorization, `exception.${kind}.${action}`, targets, profile.name, profile.region)) {
290
+ throw failure("WRITES_DISABLED", "A one-use coordinator authorization is required");
291
+ }
292
+ const path = sectool ? `/sectool-exceptions/${sectools[kind]}${specific ? "/exceptions" : ""}`
293
+ : kind === "anomaly" ? "/sectools/anomaly/exceptions"
294
+ : `/sectools/click_time_protection/exceptions/items${action === "update" || action === "remove" ? `/${encodeURIComponent(targets[0])}` : ""}`;
295
+ await authenticate();
296
+ await spaceException(path);
297
+ checkScopeBeforeSend();
298
+ const response = await send(`/app/hec-api/v1.0${path}`, kind === "click-time" && action === "remove" ? undefined : { requestData: data }, token, action === "update" ? "PUT" : "POST", true);
299
+ if (response.status < 200 || response.status >= 300 || object(response.value) && Object.hasOwn(response.value, "responseEnvelope")
300
+ && (!object(response.value.responseEnvelope) || ![0, "0", 200, "200", 204, "204"].includes(response.value.responseEnvelope.responseCode))) {
301
+ throw failure("OUTCOME_UNKNOWN", "Exception write status requires reconciliation; never replay automatically");
302
+ }
303
+ return response.value;
304
+ }
305
+ return { authenticate, read, download, emailAction, eventAction, exceptionWrite, sectoolsWrite };
306
+ }
@@ -0,0 +1,4 @@
1
+ import type { credentialStore } from "./profiles.js";
2
+ import { gateway } from "./transport.js";
3
+ import { type SectoolsAction } from "./write-sectools.js";
4
+ export declare function exceptionWriteCommand(name: `exception ${SectoolsAction}`, kind: string | undefined, id: string | undefined, flags: Map<string, string>, store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;