@knowttl/checkpoint-axi 0.0.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +164 -3
- package/axi-credential.schema.json +35 -0
- package/bin/checkpoint-axi-hook.js +9 -0
- package/bin/checkpoint-axi.js +8 -0
- package/bin/version.js +3 -0
- package/dist/src/catalogue.d.ts +317 -0
- package/dist/src/catalogue.js +103 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +22 -0
- package/dist/src/doctor.d.ts +3 -0
- package/dist/src/doctor.js +37 -0
- package/dist/src/downloads.d.ts +3 -0
- package/dist/src/downloads.js +52 -0
- package/dist/src/exceptions.d.ts +37 -0
- package/dist/src/exceptions.js +81 -0
- package/dist/src/local-commands.d.ts +3 -0
- package/dist/src/local-commands.js +123 -0
- package/dist/src/private-file.d.ts +10 -0
- package/dist/src/private-file.js +65 -0
- package/dist/src/profiles.d.ts +122 -0
- package/dist/src/profiles.js +137 -0
- package/dist/src/reads.d.ts +3 -0
- package/dist/src/reads.js +169 -0
- package/dist/src/redact.d.ts +6 -0
- package/dist/src/redact.js +25 -0
- package/dist/src/regions.d.ts +9 -0
- package/dist/src/regions.js +10 -0
- package/dist/src/transport.d.ts +86 -0
- package/dist/src/transport.js +306 -0
- package/dist/src/write-exceptions.d.ts +4 -0
- package/dist/src/write-exceptions.js +97 -0
- package/dist/src/write-sectools.d.ts +5 -0
- package/dist/src/write-sectools.js +107 -0
- package/dist/src/writes.d.ts +30 -0
- package/dist/src/writes.js +295 -0
- package/docs/agent-guide.md +108 -0
- package/docs/commands.md +489 -0
- package/docs/configuration.md +63 -0
- package/docs/design.md +165 -0
- package/docs/downloads.md +30 -0
- package/docs/reads.md +84 -0
- package/docs/writes.md +200 -0
- package/inventory/capabilities.json +217 -0
- package/inventory/specs.json +12 -0
- package/package.json +48 -5
- package/skills/checkpoint-axi/SKILL.md +102 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { gateway } from "./transport.js";
|
|
7
|
+
import { protectWindowsFile, windowsAcl } from "./private-file.js";
|
|
8
|
+
const fail = (code, message, help = []) => { throw new AxiError(message, code, help); };
|
|
9
|
+
const authorizations = new WeakSet();
|
|
10
|
+
export function consumeWriteAuthorization(authorization, operation, targets, profile, region) {
|
|
11
|
+
if (!authorizations.delete(authorization))
|
|
12
|
+
return false;
|
|
13
|
+
return authorization.operation === operation && authorization.profile === profile && authorization.region === region
|
|
14
|
+
&& targets.length > 0 && targets.length <= 20 && new Set(targets).size === targets.length
|
|
15
|
+
&& targets.every((target) => authorization.targets.includes(target));
|
|
16
|
+
}
|
|
17
|
+
// Metadata only, never message payloads, headers, credentials or API bodies.
|
|
18
|
+
function journal(store) {
|
|
19
|
+
const path = store.writeLogPath();
|
|
20
|
+
function append(record) {
|
|
21
|
+
let lock;
|
|
22
|
+
let fd;
|
|
23
|
+
try {
|
|
24
|
+
const directory = dirname(path);
|
|
25
|
+
const created = mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
if (process.platform === "win32" && created !== undefined)
|
|
27
|
+
protectWindowsFile(directory);
|
|
28
|
+
const parent = lstatSync(directory);
|
|
29
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || process.platform !== "win32"
|
|
30
|
+
&& (parent.uid !== process.getuid() || (parent.mode & 0o077) !== 0))
|
|
31
|
+
throw new Error("Insecure journal directory");
|
|
32
|
+
if (process.platform === "win32")
|
|
33
|
+
windowsAcl(directory, true);
|
|
34
|
+
lock = openSync(`${path}.lock`, "wx", 0o600);
|
|
35
|
+
if (process.platform === "win32")
|
|
36
|
+
protectWindowsFile(`${path}.lock`);
|
|
37
|
+
try {
|
|
38
|
+
fd = openSync(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
39
|
+
if (process.platform === "win32")
|
|
40
|
+
protectWindowsFile(path);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code !== "EEXIST")
|
|
44
|
+
throw error;
|
|
45
|
+
const existing = lstatSync(path);
|
|
46
|
+
if (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1 || process.platform !== "win32"
|
|
47
|
+
&& (existing.uid !== process.getuid() || (existing.mode & 0o077) !== 0))
|
|
48
|
+
throw new Error("Insecure journal file");
|
|
49
|
+
if (process.platform === "win32")
|
|
50
|
+
windowsAcl(path, true);
|
|
51
|
+
fd = openSync(path, constants.O_RDWR | constants.O_APPEND | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW));
|
|
52
|
+
}
|
|
53
|
+
const stat = fstatSync(fd);
|
|
54
|
+
if (!stat.isFile() || stat.nlink !== 1 || process.platform !== "win32"
|
|
55
|
+
&& (stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0))
|
|
56
|
+
throw new Error("Insecure journal file");
|
|
57
|
+
if (process.platform === "win32")
|
|
58
|
+
windowsAcl(path, true);
|
|
59
|
+
const contents = readFileSync(fd, "utf8");
|
|
60
|
+
if (contents && !contents.endsWith("\n"))
|
|
61
|
+
throw new Error("Incomplete journal");
|
|
62
|
+
for (const line of contents.split("\n").filter(Boolean)) {
|
|
63
|
+
const entry = JSON.parse(line);
|
|
64
|
+
if (!entry || !["intent", "outcome"].includes(String(entry.kind)) || typeof entry.auditId !== "string")
|
|
65
|
+
throw new Error("Invalid journal");
|
|
66
|
+
if (record.kind === "intent" && entry.kind === "intent" && entry.auditId === record.auditId)
|
|
67
|
+
throw new Error("Duplicate intent");
|
|
68
|
+
}
|
|
69
|
+
const line = Buffer.from(JSON.stringify(store.redactor.value({ ...record, time: new Date().toISOString() })) + "\n");
|
|
70
|
+
let offset = 0;
|
|
71
|
+
while (offset < line.length) {
|
|
72
|
+
const written = writeSync(fd, line, offset, line.length - offset);
|
|
73
|
+
if (!written)
|
|
74
|
+
throw new Error("Incomplete journal write");
|
|
75
|
+
offset += written;
|
|
76
|
+
}
|
|
77
|
+
fsyncSync(fd);
|
|
78
|
+
// The directory entry must survive a crash too, before sending anything.
|
|
79
|
+
if (process.platform !== "win32") {
|
|
80
|
+
const stop = created === undefined ? directory : dirname(created);
|
|
81
|
+
let current = directory;
|
|
82
|
+
for (;;) {
|
|
83
|
+
const directoryFd = openSync(current, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
84
|
+
try {
|
|
85
|
+
fsyncSync(directoryFd);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
closeSync(directoryFd);
|
|
89
|
+
}
|
|
90
|
+
if (current === stop)
|
|
91
|
+
break;
|
|
92
|
+
current = dirname(current);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
fail(record.kind === "intent" ? "INTENT_NOT_RECORDED" : "OUTCOME_NOT_RECORDED", record.kind === "intent" ? "Durable write intent could not be recorded; nothing was sent" : "Write outcome could not be recorded; reconcile before any further action", [`Audit ID: ${String(record.auditId)}`, "Check CHECKPOINT_AXI_WRITE_LOG and its private directory permissions; never replay an ambiguous action"]);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (fd !== undefined)
|
|
101
|
+
closeSync(fd);
|
|
102
|
+
if (lock !== undefined) {
|
|
103
|
+
closeSync(lock);
|
|
104
|
+
unlinkSync(`${path}.lock`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return append;
|
|
109
|
+
}
|
|
110
|
+
// Domain callers bind the operation and single send; flags cannot supply routes.
|
|
111
|
+
export async function coordinateWrite(args) {
|
|
112
|
+
const { operation, targets, flags, store, checkScope, readState, send } = args;
|
|
113
|
+
if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length)
|
|
114
|
+
fail("VALIDATION_ERROR", "Supply 1-20 unique targets");
|
|
115
|
+
if (flags.has("--execute") && flags.has("--dry-run"))
|
|
116
|
+
fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
|
|
117
|
+
checkScope();
|
|
118
|
+
const current = await readState();
|
|
119
|
+
const confirmation = `${operation} ${targets.join(",")}`;
|
|
120
|
+
const preview = { operation, effect: "disruptive", targets, noop: current.noop, currentState: current.state, confirm: confirmation };
|
|
121
|
+
if (!flags.has("--execute"))
|
|
122
|
+
return { outcome: "preview", preview };
|
|
123
|
+
if (current.noop)
|
|
124
|
+
return { outcome: "noop", preview };
|
|
125
|
+
if (!flags.has("--confirm"))
|
|
126
|
+
fail("CONFIRM_REQUIRED", "Exact target confirmation is required", [`Repeat with --confirm '${confirmation}'`]);
|
|
127
|
+
if (flags.get("--confirm") !== confirmation)
|
|
128
|
+
fail("CONFIRM_MISMATCH", "Confirmation does not exactly match the operation and every target", [`Repeat with --confirm '${confirmation}'`]);
|
|
129
|
+
checkScope();
|
|
130
|
+
const auditId = randomUUID();
|
|
131
|
+
const record = journal(store);
|
|
132
|
+
const meta = { auditId, ...args.scope, operation, targets, effect: "disruptive" };
|
|
133
|
+
record({ ...meta, kind: "intent" });
|
|
134
|
+
let fresh;
|
|
135
|
+
try {
|
|
136
|
+
fresh = await readState();
|
|
137
|
+
checkScope();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (fresh.noop) {
|
|
144
|
+
record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
|
|
145
|
+
return { outcome: "noop", preview, auditId };
|
|
146
|
+
}
|
|
147
|
+
const authorization = Object.freeze({ operation, targets: Object.freeze([...targets]), ...args.scope });
|
|
148
|
+
authorizations.add(authorization);
|
|
149
|
+
let result;
|
|
150
|
+
let scopeRevoked = false;
|
|
151
|
+
try {
|
|
152
|
+
result = await send(authorization, () => {
|
|
153
|
+
try {
|
|
154
|
+
checkScope();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
scopeRevoked = true;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (scopeRevoked) {
|
|
164
|
+
record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
record({ ...meta, kind: "outcome", outcome: "OUTCOME_UNKNOWN" });
|
|
168
|
+
return fail("OUTCOME_UNKNOWN", "Action outcome is unknown; never replay automatically", [
|
|
169
|
+
`Audit ID: ${auditId}`, "Inspect the write journal and read back every target before deciding what to do next",
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
authorizations.delete(authorization);
|
|
174
|
+
}
|
|
175
|
+
record({ ...meta, kind: "outcome", outcome: "accepted" });
|
|
176
|
+
return { outcome: "accepted", auditId, ...result };
|
|
177
|
+
}
|
|
178
|
+
const entity = z.object({ entityInfo: z.object({ entityId: z.string(), saasEntityType: z.string() }),
|
|
179
|
+
entityPayload: z.object({ isQuarantined: z.boolean().optional(), isRestored: z.boolean().optional() }),
|
|
180
|
+
entityAvailableActions: z.array(z.object({ entityActionName: z.string() })) });
|
|
181
|
+
const task = z.object({ entityId: z.string(), taskId: z.union([z.string().regex(/^[A-Za-z0-9_-]+$/), z.number().int().nonnegative()]) });
|
|
182
|
+
const event = z.object({ eventId: z.string(), entityId: z.string(), state: z.string(),
|
|
183
|
+
availableEventActions: z.array(z.object({ actionName: z.string(), actionParameter: z.record(z.string(), z.unknown()) })) });
|
|
184
|
+
const eventTask = task.extend({ eventId: z.string() });
|
|
185
|
+
export async function eventWriteCommand(flags, store, transport) {
|
|
186
|
+
const targets = flags.get("--ids")?.split(",") ?? [];
|
|
187
|
+
const action = flags.get("--name");
|
|
188
|
+
if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length
|
|
189
|
+
|| targets.some((id) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(id))
|
|
190
|
+
|| !action || !/^[a-z][A-Za-z0-9_-]{0,127}$/.test(action)) {
|
|
191
|
+
return fail("VALIDATION_ERROR", "Supply --name <action> and --ids with 1-20 unique comma-separated event IDs");
|
|
192
|
+
}
|
|
193
|
+
if (flags.has("--execute") && flags.has("--dry-run"))
|
|
194
|
+
return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
|
|
195
|
+
store.readOnly();
|
|
196
|
+
const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
|
|
197
|
+
const operation = `event.action.${action}`;
|
|
198
|
+
const checkScope = () => {
|
|
199
|
+
if (store.readOnly() || !profile.writes.allowWrites)
|
|
200
|
+
fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
|
|
201
|
+
if (!profile.writes.operations.includes(operation))
|
|
202
|
+
fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
|
|
203
|
+
const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
|
|
204
|
+
if (JSON.stringify(fresh) !== JSON.stringify(profile))
|
|
205
|
+
fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
|
|
206
|
+
};
|
|
207
|
+
checkScope();
|
|
208
|
+
const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
|
|
209
|
+
const entities = new Map();
|
|
210
|
+
const readState = async () => {
|
|
211
|
+
const states = [];
|
|
212
|
+
for (const id of targets) {
|
|
213
|
+
const page = await session.read({ path: `/event/${encodeURIComponent(id)}` });
|
|
214
|
+
const parsed = event.safeParse(page.records[0]);
|
|
215
|
+
if (page.records.length !== 1 || !parsed.success || parsed.data.eventId !== id) {
|
|
216
|
+
return fail("RESPONSE_INVALID", "Event preflight returned a missing, mismatched or malformed event");
|
|
217
|
+
}
|
|
218
|
+
const value = parsed.data;
|
|
219
|
+
const offered = value.availableEventActions.filter((item) => item.actionName === action);
|
|
220
|
+
if (!offered.length)
|
|
221
|
+
fail("ACTION_UNAVAILABLE", "Requested action is unavailable for an event target");
|
|
222
|
+
// Guide pp.36-37 uses objects, but p.43 requires strings; never guess serialization.
|
|
223
|
+
if (offered.length !== 1 || Object.keys(offered[0].actionParameter).length) {
|
|
224
|
+
fail("ENDPOINT_UNVERIFIED", "Parameterized event actions require live commissioning through Artium", ["Run checkpoint-axi event show <id> to inspect available actions; parameter serialization is unverified (guide pages 36-37, 43)"]);
|
|
225
|
+
}
|
|
226
|
+
entities.set(id, value.entityId);
|
|
227
|
+
states.push({ id, state: value.state, action });
|
|
228
|
+
}
|
|
229
|
+
return { noop: false, state: states };
|
|
230
|
+
};
|
|
231
|
+
return coordinateWrite({ operation, targets, flags, store, scope: { profile: profile.name, region: profile.region }, checkScope, readState, send: async (authorization) => {
|
|
232
|
+
const page = await session.eventAction(authorization, action, targets);
|
|
233
|
+
const parsed = z.array(eventTask).safeParse(page.records);
|
|
234
|
+
if (!parsed.success || parsed.data.length !== targets.length || new Set(parsed.data.map((item) => item.eventId)).size !== targets.length
|
|
235
|
+
|| parsed.data.some((item) => !targets.includes(item.eventId) || entities.get(item.eventId) !== item.entityId
|
|
236
|
+
|| store.redactor.text(String(item.taskId)) !== String(item.taskId))) {
|
|
237
|
+
return fail("OUTCOME_UNKNOWN", "Action response did not identify a task for every event target");
|
|
238
|
+
}
|
|
239
|
+
return { tasks: parsed.data.map((item) => ({ ...item, outcome: "accepted" })), help: ["Run checkpoint-axi task show <taskId> to inspect asynchronous execution; accepted does not mean completed"] };
|
|
240
|
+
} });
|
|
241
|
+
}
|
|
242
|
+
export async function emailWriteCommand(name, flags, store, transport) {
|
|
243
|
+
const targets = flags.get("--ids")?.split(",") ?? [];
|
|
244
|
+
if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length
|
|
245
|
+
|| targets.some((id) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(id))) {
|
|
246
|
+
return fail("VALIDATION_ERROR", "--ids must name 1-20 unique comma-separated entity IDs");
|
|
247
|
+
}
|
|
248
|
+
if (flags.has("--execute") && flags.has("--dry-run"))
|
|
249
|
+
return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
|
|
250
|
+
store.readOnly();
|
|
251
|
+
const loaded = store.load(flags.get("--config"), true);
|
|
252
|
+
const profile = store.select(loaded.config, flags.get("--profile"));
|
|
253
|
+
const operation = name.replace(" ", ".");
|
|
254
|
+
const checkScope = () => {
|
|
255
|
+
if (store.readOnly() || !profile.writes.allowWrites)
|
|
256
|
+
fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
|
|
257
|
+
if (!profile.writes.operations.includes(operation))
|
|
258
|
+
fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
|
|
259
|
+
const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
|
|
260
|
+
if (JSON.stringify(fresh) !== JSON.stringify(profile))
|
|
261
|
+
fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
|
|
262
|
+
};
|
|
263
|
+
checkScope();
|
|
264
|
+
const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
|
|
265
|
+
const action = name === "email quarantine" ? "quarantine" : "restore";
|
|
266
|
+
let pending = targets;
|
|
267
|
+
const readState = async () => {
|
|
268
|
+
const states = [];
|
|
269
|
+
for (const id of targets) {
|
|
270
|
+
const page = await session.read({ path: `/search/entity/${encodeURIComponent(id)}` });
|
|
271
|
+
const parsed = entity.safeParse(page.records[0]);
|
|
272
|
+
if (page.records.length !== 1 || !parsed.success || parsed.data.entityInfo.entityId !== id
|
|
273
|
+
|| !["office365_emails_email", "google_mail_email"].includes(parsed.data.entityInfo.saasEntityType)) {
|
|
274
|
+
return fail("RESPONSE_INVALID", "Email preflight returned a missing, mismatched or malformed entity");
|
|
275
|
+
}
|
|
276
|
+
const value = parsed.data;
|
|
277
|
+
const noop = action === "quarantine" ? value.entityPayload.isQuarantined === true
|
|
278
|
+
: value.entityPayload.isRestored === true && value.entityPayload.isQuarantined === false;
|
|
279
|
+
if (!noop && !value.entityAvailableActions.some((item) => item.entityActionName === action))
|
|
280
|
+
fail("ACTION_UNAVAILABLE", "Requested action is unavailable for an email target");
|
|
281
|
+
states.push({ id, noop, isQuarantined: value.entityPayload.isQuarantined, isRestored: value.entityPayload.isRestored });
|
|
282
|
+
}
|
|
283
|
+
pending = states.filter((state) => !state.noop).map((state) => state.id);
|
|
284
|
+
return { noop: pending.length === 0, state: states };
|
|
285
|
+
};
|
|
286
|
+
return coordinateWrite({ operation, targets, flags, store, scope: { profile: profile.name, region: profile.region }, checkScope, readState, send: async (authorization) => {
|
|
287
|
+
const page = await session.emailAction(authorization, action, pending);
|
|
288
|
+
const parsed = z.array(task).safeParse(page.records);
|
|
289
|
+
if (!parsed.success || parsed.data.length !== pending.length || new Set(parsed.data.map((item) => item.entityId)).size !== pending.length
|
|
290
|
+
|| parsed.data.some((item) => !pending.includes(item.entityId) || store.redactor.text(String(item.taskId)) !== String(item.taskId))) {
|
|
291
|
+
return fail("OUTCOME_UNKNOWN", "Action response did not identify a task for every target");
|
|
292
|
+
}
|
|
293
|
+
return { tasks: parsed.data.map((item) => ({ ...item, outcome: "accepted" })), help: ["Run checkpoint-axi task show <taskId> to inspect asynchronous execution; accepted does not mean completed"] };
|
|
294
|
+
} });
|
|
295
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
Event selectors include --event-types, --event-states, --severities, --event-ids (comma-separated), --confidence-indicator and --description; --saas accepts comma-separated SaaS values here.
|
|
40
|
+
Event detail includes available actions; task detail reads status and per-action results once, without waiting or polling:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
checkpoint-axi event list --all --event-types phishing --fields events,count,total,complete,reason,cursor
|
|
44
|
+
checkpoint-axi event show synthetic-event-1 --fields event
|
|
45
|
+
checkpoint-axi task show synthetic-task-1 --fields task
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Inspect exception rules with exception list <kind> or exception show <kind> <id-or-value>:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
checkpoint-axi exception list phishing-block --limit 5 --fields exceptions,count,total,complete,reason
|
|
52
|
+
checkpoint-axi exception show phishing-block synthetic-exception-1 --fields exception
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Kinds: phishing-allow, phishing-block, spam-allow, anomaly, click-time, malware, url, dlp.
|
|
56
|
+
For malware, --type is required: hash, micro_hash, file_type, ppat_sender_name, allow_file_type or sender_email.
|
|
57
|
+
For url, --type is allow-url, allow-domain, block-url or block-domain; for dlp, hash, text_content, sender_email or recipient_email.
|
|
58
|
+
Click-time --items selects item routes instead of list routes.
|
|
59
|
+
Anomaly show is refused with ENDPOINT_UNVERIFIED; use anomaly list.
|
|
60
|
+
Undocumented response shapes carry responseSchema: unverified; do not infer identities from them.
|
|
61
|
+
|
|
62
|
+
## One-call forms and output bounds
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
Both default to 50 rows over the past 24 hours; --limit accepts 1-1000 and cannot combine with --all.
|
|
66
|
+
Use --start-date and --end-date with ISO timestamps including timezones; the positive window cannot exceed 31 days.
|
|
67
|
+
Inspect complete, reason and cursor even on exit 0; limits and page replay can leave results incomplete.
|
|
68
|
+
Resume with --cursor <returned-cursor> and the original selectors/profile; see [continuation limitations](reads.md#email-reads).
|
|
69
|
+
Exception --all bounds returned rows at 1000 but cannot fetch additional pages: no continuation parameter is documented.
|
|
70
|
+
Email/event detail already includes available actions, avoiding a separate action lookup.
|
|
71
|
+
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.
|
|
72
|
+
|
|
73
|
+
--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.
|
|
74
|
+
Email rows contain id,subject,state,verdict; event rows contain id,type,state,severity; documented exception rows contain id,sender,subject,comment.
|
|
75
|
+
TOON strings preview at 500 characters; --full expands strings after redaction, while cursors are never truncated.
|
|
76
|
+
|
|
77
|
+
## Help and safety
|
|
78
|
+
|
|
79
|
+
Top-level --help returns the full command catalogue, common flags, version flags and home examples.
|
|
80
|
+
Group --help (such as email --help) is unsupported and returns VALIDATION_ERROR with the valid command list; select a leaf command directly.
|
|
81
|
+
Leaf command --help returns its description, accepted flags with defaults, and examples; positional arguments appear in those examples.
|
|
82
|
+
Use the failing leaf's help to correct syntax; do not run a help sweep before investigating.
|
|
83
|
+
|
|
84
|
+
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).
|
|
85
|
+
These commands never mutate remote state or save downloaded files; API authentication exchanges tokens held only in memory.
|
|
86
|
+
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.
|
|
87
|
+
Writes require exact profile operation scope, preview, --execute, exact --confirm and durable redacted intent; forced read-only always wins.
|
|
88
|
+
Email download and attachment download/download-all write local files and require separate download opt-in; they are outside the read allowlist.
|
|
89
|
+
See [write safety](writes.md) and [download safety](downloads.md) before requesting them.
|
|
90
|
+
|
|
91
|
+
## Failure recovery
|
|
92
|
+
|
|
93
|
+
Exit 0 means success, preview or no-op; 2 means invalid syntax (VALIDATION_ERROR); 1 means configuration, authentication, transport or operation failure.
|
|
94
|
+
Structured errors and suggestions are on stdout; follow the code and suggestion instead of retrying blindly.
|
|
95
|
+
|
|
96
|
+
| Code | Meaning and next action |
|
|
97
|
+
|---|---|
|
|
98
|
+
| VALIDATION_ERROR | Invalid flags, IDs, date window or mismatched cursor selectors; correct the failed command. |
|
|
99
|
+
| PROFILE_REQUIRED / CONFIG_INVALID | Missing or invalid configuration; select a complete provisioned profile. |
|
|
100
|
+
| CONFIG_INSECURE | Fix private-file permissions using the configuration guide. |
|
|
101
|
+
| AUTH_FAILED | Check region and secret source; the CLI already attempts one token re-exchange after API rejection. |
|
|
102
|
+
| AUTH_SHAPE_UNVERIFIED / ENDPOINT_UNVERIFIED | Contract is undocumented; request owner commissioning, never guess a route or token shape. |
|
|
103
|
+
| NOT_FOUND | No matching detail record; verify the ID and profile using a list read. |
|
|
104
|
+
| RESPONSE_INVALID / API_ERROR / HTTP_ERROR | Unexpected response or upstream failure; inspect the redacted error and verify service/profile before retrying. |
|
|
105
|
+
| RATE_LIMITED | Two read retries exhausted; defer work instead of immediate repeated calls. |
|
|
106
|
+
| REQUEST_BUDGET / TIME_BUDGET / RESPONSE_TOO_LARGE | Reduce date window or limit; inspect incomplete results and continuation before another bounded read. |
|
|
107
|
+
| WRITES_DISABLED / DOWNLOADS_DISABLED | Operation is outside profile permissions; do not loosen permissions during investigation. |
|
|
108
|
+
| OUTCOME_UNKNOWN | A mutation may have happened; reconcile with reads and the journal, never replay automatically. |
|