@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,103 @@
|
|
|
1
|
+
import { exceptionFamilies } from "./exceptions.js";
|
|
2
|
+
export const DESCRIPTION = "Inspect Check Point Email Security through compact, guarded AXI commands";
|
|
3
|
+
export const SETUP_GUIDANCE = "Run checkpoint-axi config path; configure a region, clientId and clientSecret env/file SecretRef";
|
|
4
|
+
const common = {
|
|
5
|
+
config: "Config path; default CHECKPOINT_AXI_CONFIG or platform path",
|
|
6
|
+
profile: "Profile name; default CHECKPOINT_AXI_PROFILE, env profile, config default, or sole profile",
|
|
7
|
+
fields: "Comma-separated output fields; default all",
|
|
8
|
+
full: "Show full strings instead of 500-character previews; default false",
|
|
9
|
+
help: "Show concise command help; default false",
|
|
10
|
+
};
|
|
11
|
+
const paging = {
|
|
12
|
+
"start-date": "ISO timestamp with timezone; default 24 hours before end-date; maximum window 31 days",
|
|
13
|
+
"end-date": "ISO timestamp with timezone; default now",
|
|
14
|
+
limit: "Maximum rows; default 50, range 1-1000; cannot combine with --all",
|
|
15
|
+
all: "Read up to 1000 rows / 10 HTTP requests / 60 seconds / 8 MiB; default false",
|
|
16
|
+
cursor: "Opaque continuation bound to profile, region, operation and selectors; repeat original selectors",
|
|
17
|
+
};
|
|
18
|
+
const exceptionType = `Required for ${["malware", "url", "dlp"].map((kind) => `${kind}: ${exceptionFamilies[kind].types.join(",")}`).join("; ")}`;
|
|
19
|
+
const writeFlags = { ...common,
|
|
20
|
+
ids: "Required comma-separated unique entity IDs; maximum 20",
|
|
21
|
+
execute: "Send once after all write gates; default false (preview)",
|
|
22
|
+
"dry-run": "Explicit preview; cannot combine with --execute",
|
|
23
|
+
confirm: "Exact operation followed by a space and the comma-separated IDs in supplied order",
|
|
24
|
+
};
|
|
25
|
+
const exceptionWriteFlags = { ...common, execute: writeFlags.execute, "dry-run": writeFlags["dry-run"],
|
|
26
|
+
confirm: "Exact operation followed by a space and every target from preview (payload sha256 for create and malware/url/dlp update, exception ID or comma-separated item IDs otherwise)" };
|
|
27
|
+
const input = "Required absolute private JSON file containing documented requestData fields directly; fields depend on kind; update sends a replacement PUT";
|
|
28
|
+
// Only shipped commands belong here. Inventory records are evidence, never dispatch.
|
|
29
|
+
export const catalogue = {
|
|
30
|
+
"email download": { 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)",
|
|
31
|
+
flags: { config: common.config, profile: common.profile, help: common.help, out: "Required absolute new output file; mode 0600, never stdout or overwrite", original: "Download original email without processing modifications; default false" },
|
|
32
|
+
examples: ["checkpoint-axi email download <entity-id> --out /absolute/message", "checkpoint-axi email download <entity-id> --original --out /absolute/original"] },
|
|
33
|
+
"attachment download": { description: "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)",
|
|
34
|
+
flags: { config: common.config, profile: common.profile, help: common.help, out: "Required absolute new output file; mode 0600, never stdout or overwrite", scope: "Optional authorized scope query; default profile scope when configured; live behavior unverified" },
|
|
35
|
+
examples: ["checkpoint-axi attachment download <entity-id> <attachment-id> --out /absolute/attachment", "checkpoint-axi attachment download <entity-id> <attachment-id> --scope <scope> --out /absolute/attachment"] },
|
|
36
|
+
"attachment download-all": { description: "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)",
|
|
37
|
+
flags: { config: common.config, profile: common.profile, help: common.help, out: "Required absolute new output file; mode 0600, never stdout or overwrite", scope: "Optional authorized scope query; default profile scope when configured; live behavior unverified" },
|
|
38
|
+
examples: ["checkpoint-axi attachment download-all <entity-id> --out /absolute/attachments", "checkpoint-axi attachment download-all <entity-id> --scope <scope> --out /absolute/attachments"] },
|
|
39
|
+
"exception create": { description: "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)",
|
|
40
|
+
flags: { ...exceptionWriteFlags, input, specific: "Malware/url/dlp only: use the documented create-specific /exceptions route; default false; included in confirmation digest" },
|
|
41
|
+
examples: ["checkpoint-axi exception create phishing-allow --input /absolute/rule.json", "checkpoint-axi exception create spam-allow --input /absolute/rule.json --execute --confirm 'exception.spam-allow.create sha256:<preview-digest>'"] },
|
|
42
|
+
"exception update": { description: "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)",
|
|
43
|
+
flags: { ...exceptionWriteFlags, input },
|
|
44
|
+
examples: ["checkpoint-axi exception update phishing-block <id> --input /absolute/rule.json", "checkpoint-axi exception update spam-allow <id> --input /absolute/rule.json --execute --confirm 'exception.spam-allow.update <id>'"] },
|
|
45
|
+
"exception remove": { description: "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)",
|
|
46
|
+
flags: exceptionWriteFlags,
|
|
47
|
+
examples: ["checkpoint-axi exception remove phishing-allow <id>", "checkpoint-axi exception remove spam-allow <id> --execute --confirm 'exception.spam-allow.remove <id>'"] },
|
|
48
|
+
"exception remove-items": { description: "Preview or remove 1-20 click-time items by caller-supplied IDs; existence is unverified (guide pages 97-98)",
|
|
49
|
+
flags: { ...exceptionWriteFlags, ids: "Required 1-20 unique comma-separated click-time item IDs" },
|
|
50
|
+
examples: ["checkpoint-axi exception remove-items click-time --ids <id1>,<id2>", "checkpoint-axi exception remove-items click-time --ids <id1>,<id2> --execute --confirm 'exception.click-time.remove-items <id1>,<id2>'"] },
|
|
51
|
+
"exception clear": { description: "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)",
|
|
52
|
+
flags: exceptionWriteFlags,
|
|
53
|
+
examples: ["checkpoint-axi exception clear click-time", "checkpoint-axi exception clear click-time --dry-run"] },
|
|
54
|
+
"event action": { description: "Preview or request a name-only event action; parameterized actions refuse ENDPOINT_UNVERIFIED pending commissioning (guide pages 35-37, 42-45)",
|
|
55
|
+
flags: { ...writeFlags, ids: "Required comma-separated unique event IDs; maximum 20", name: "Required available action name; nonempty action parameters are unverified" },
|
|
56
|
+
examples: ["checkpoint-axi event action --ids <id> --name dismiss", "checkpoint-axi event action --ids <id> --name dismiss --execute --confirm 'event.action.dismiss <id>'"] },
|
|
57
|
+
"email quarantine": { description: "Preview or request asynchronous email quarantine through the gated write coordinator (guide pages 28-30)", flags: writeFlags,
|
|
58
|
+
examples: ["checkpoint-axi email quarantine --ids <id>", "checkpoint-axi email quarantine --ids <id> --execute --confirm 'email.quarantine <id>'"] },
|
|
59
|
+
"email restore": { description: "Preview or request asynchronous email restore through the gated write coordinator (guide pages 28-30)", flags: writeFlags,
|
|
60
|
+
examples: ["checkpoint-axi email restore --ids <id>", "checkpoint-axi email restore --ids <id> --execute --confirm 'email.restore <id>'"] },
|
|
61
|
+
"exception list": { description: "List bounded exceptions for phishing-allow, phishing-block, spam-allow, anomaly, click-time, malware, url or dlp (guide pages 49-123)",
|
|
62
|
+
flags: { ...common, type: exceptionType,
|
|
63
|
+
items: "Click-time item routes instead of list routes; default false", limit: paging.limit, all: "Return up to 1000 rows within transport budgets; no server continuation parameter is documented" },
|
|
64
|
+
examples: ["checkpoint-axi exception list phishing-allow", "checkpoint-axi exception list malware --type hash", "checkpoint-axi exception list click-time --items --all"] },
|
|
65
|
+
"exception show": { description: "Show an exception by kind and ID/value; anomaly refuses ENDPOINT_UNVERIFIED pending commissioning (guide pages 52-123)",
|
|
66
|
+
flags: { ...common, type: exceptionType, items: "Click-time item route instead of list route; default false" },
|
|
67
|
+
examples: ["checkpoint-axi exception show phishing-block <id>", "checkpoint-axi exception show url <value> --type allow-url", "checkpoint-axi exception show click-time <id> --items --full"] },
|
|
68
|
+
"event list": { description: "List security events with bounded scroll continuation (API guide pages 37-42)", flags: { ...common, ...paging,
|
|
69
|
+
saas: "Comma-separated SaaS overrides; default selected profile saas or office365_emails",
|
|
70
|
+
"event-types": "Comma-separated event types; default unfiltered",
|
|
71
|
+
"event-states": "Comma-separated event states; default unfiltered",
|
|
72
|
+
severities: "Comma-separated severities; default unfiltered",
|
|
73
|
+
"event-ids": "Comma-separated event IDs; default unfiltered",
|
|
74
|
+
"confidence-indicator": "Confidence indicator selector; default unfiltered",
|
|
75
|
+
description: "Description selector; default unfiltered",
|
|
76
|
+
}, examples: ["checkpoint-axi event list", "checkpoint-axi event list --all --event-types phishing", "checkpoint-axi event list --cursor <cursor>"] },
|
|
77
|
+
"event show": { description: "Show one event, actions taken and available actions (API guide pages 34-37)", flags: common,
|
|
78
|
+
examples: ["checkpoint-axi event show <id>", "checkpoint-axi event show <id> --full"] },
|
|
79
|
+
"task show": { description: "Show asynchronous task status and per-action details (API guide pages 30-33)", flags: common,
|
|
80
|
+
examples: ["checkpoint-axi task show <id>", "checkpoint-axi task show <id> --full"] },
|
|
81
|
+
"email search": { description: "Search emails with a bounded date window and scroll continuation (API guide pages 18-27)", flags: { ...common,
|
|
82
|
+
saas: "SaaS override; default selected profile saas or office365_emails",
|
|
83
|
+
"saas-entity": "Optional SaaS entity type selector",
|
|
84
|
+
...paging,
|
|
85
|
+
filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)",
|
|
86
|
+
}, examples: ["checkpoint-axi email search", "checkpoint-axi email search --all --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"] },
|
|
87
|
+
"email show": { description: "Show one email with verdicts, actions and available actions (API guide pages 10-18)", flags: common,
|
|
88
|
+
examples: ["checkpoint-axi email show <id>", "checkpoint-axi email show <id> --full"] },
|
|
89
|
+
doctor: { description: "Check regional API authentication and one bounded email search without printing tenant data", flags: { ...common, saas: "SaaS override; default selected profile saas or office365_emails" },
|
|
90
|
+
examples: ["checkpoint-axi doctor", "checkpoint-axi doctor --profile example --saas office365_emails"] },
|
|
91
|
+
home: { description: "Show local profile context without resolving secrets or making API calls", flags: common,
|
|
92
|
+
examples: ["checkpoint-axi", "checkpoint-axi home --profile example"] },
|
|
93
|
+
"auth status": { description: "Validate the selected local profile and secret source without API calls", flags: common,
|
|
94
|
+
examples: ["checkpoint-axi auth status", "checkpoint-axi auth status --profile example"] },
|
|
95
|
+
"profile list": { description: "List file profiles without resolving secrets; --profile does not filter this list", flags: common,
|
|
96
|
+
examples: ["checkpoint-axi profile list", "checkpoint-axi profile list --config /example/config.json"] },
|
|
97
|
+
"config path": { description: "Show config and state paths without reading files; --profile does not affect paths", flags: common,
|
|
98
|
+
examples: ["checkpoint-axi config path", "checkpoint-axi config path --fields config"] },
|
|
99
|
+
};
|
|
100
|
+
export function help() {
|
|
101
|
+
return { description: DESCRIPTION, commands: Object.fromEntries(Object.entries(catalogue).map(([name, entry]) => [name, entry.description])),
|
|
102
|
+
flags: common, version: "-v, -V, --version: print the bare version", examples: catalogue.home.examples };
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CredentialContext } from "./profiles.js";
|
|
2
|
+
import type { gateway } from "./transport.js";
|
|
3
|
+
export declare function main(argv?: string[], options?: {
|
|
4
|
+
credentials?: CredentialContext;
|
|
5
|
+
transport?: Parameters<typeof gateway>[3];
|
|
6
|
+
stdout?: {
|
|
7
|
+
write: (chunk: string) => unknown;
|
|
8
|
+
};
|
|
9
|
+
}): Promise<void>;
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AxiError, runAxiCli } from "axi-sdk-js";
|
|
2
|
+
import { encode } from "@toon-format/toon";
|
|
3
|
+
import { DESCRIPTION } from "./catalogue.js";
|
|
4
|
+
import { credentialStore } from "./profiles.js";
|
|
5
|
+
import { localCommand } from "./local-commands.js";
|
|
6
|
+
export async function main(argv = process.argv.slice(2), options = {}) {
|
|
7
|
+
const store = credentialStore(options.credentials);
|
|
8
|
+
await runAxiCli({
|
|
9
|
+
description: DESCRIPTION,
|
|
10
|
+
argv: ["local"],
|
|
11
|
+
topLevelHelp: "",
|
|
12
|
+
home: () => localCommand([], store),
|
|
13
|
+
stdout: { write: (chunk) => (options.stdout ?? process.stdout).write(store.redactor.text(chunk)) },
|
|
14
|
+
formatError: (error) => ({
|
|
15
|
+
output: `${encode(store.redactor.value(error instanceof AxiError
|
|
16
|
+
? { error: { code: error.code, message: error.message }, help: error.suggestions }
|
|
17
|
+
: { error: { code: "CONFIG_INVALID", message: "Command could not complete" }, help: ["Check local configuration; run checkpoint-axi config path"] }))}\n`,
|
|
18
|
+
exitCode: error instanceof AxiError && error.code === "VALIDATION_ERROR" ? 2 : 1,
|
|
19
|
+
}),
|
|
20
|
+
commands: { local: async () => store.redactor.value(await localCommand(argv, store, options.transport)) },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { credentialStore } from "./profiles.js";
|
|
2
|
+
import { gateway } from "./transport.js";
|
|
3
|
+
export declare function doctor(store: ReturnType<typeof credentialStore>, config?: string, selected?: string, override?: string, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { gateway } from "./transport.js";
|
|
3
|
+
export async function doctor(store, config, selected, override, transport) {
|
|
4
|
+
store.readOnly();
|
|
5
|
+
const profile = store.select(store.load(config, true).config, selected);
|
|
6
|
+
const saas = override?.trim() ?? profile.saas;
|
|
7
|
+
if (!saas)
|
|
8
|
+
throw new AxiError("--saas must not be blank", "VALIDATION_ERROR", ["Run checkpoint-axi doctor --help"]);
|
|
9
|
+
const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
|
|
10
|
+
const checks = [];
|
|
11
|
+
for (const check of ["auth", "read"]) {
|
|
12
|
+
try {
|
|
13
|
+
if (check === "auth")
|
|
14
|
+
await api.authenticate();
|
|
15
|
+
else {
|
|
16
|
+
const end = new Date();
|
|
17
|
+
// One page of the default 24-hour window, not an --all scan (guide pages 18-21).
|
|
18
|
+
await api.read({ path: "/search/query", body: { entityFilter: { saas,
|
|
19
|
+
startDate: new Date(end.getTime() - 86_400_000).toISOString(), endDate: end.toISOString() } } });
|
|
20
|
+
}
|
|
21
|
+
checks.push({ check, pass: true });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
checks.push({ check, pass: false, code: error instanceof AxiError ? error.code : "API_ERROR" });
|
|
25
|
+
if (check === "auth")
|
|
26
|
+
checks.push({ check: "read", pass: false, code: "AUTH_NOT_PASSED" });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const pass = checks.every((check) => check.pass);
|
|
31
|
+
if (!pass)
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return { region: profile.region, saas, checks, pass, ...(!pass ? { help: [
|
|
34
|
+
"Check the regional credentials and API access; AUTH_SHAPE_UNVERIFIED requires owner commissioning of the undocumented token response (guide page 8)",
|
|
35
|
+
"Run checkpoint-axi auth status to check local credential readiness; never share credentials or raw API responses",
|
|
36
|
+
] } : {}) };
|
|
37
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { credentialStore } from "./profiles.js";
|
|
2
|
+
import { gateway } from "./transport.js";
|
|
3
|
+
export declare function downloadCommand(name: string, entityId: string | undefined, attachmentId: string | undefined, flags: Map<string, string>, store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, fchmodSync, fsyncSync, openSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { protectWindowsFile } from "./private-file.js";
|
|
6
|
+
import { gateway } from "./transport.js";
|
|
7
|
+
export async function downloadCommand(name, entityId, attachmentId, flags, store, transport) {
|
|
8
|
+
const out = flags.get("--out");
|
|
9
|
+
if (!entityId || !/^[A-Za-z0-9_-]{1,256}$/.test(entityId) || name === "attachment download" && (!attachmentId || !/^[A-Za-z0-9_-]{1,256}$/.test(attachmentId))
|
|
10
|
+
|| !out || !isAbsolute(out) || out === "-" || out.includes("\0")) {
|
|
11
|
+
throw new AxiError("Supply download IDs and an absolute new --out file path", "VALIDATION_ERROR", ["Run checkpoint-axi " + name + " --help"]);
|
|
12
|
+
}
|
|
13
|
+
store.readOnly();
|
|
14
|
+
const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
|
|
15
|
+
if (!profile.downloads.allow)
|
|
16
|
+
throw new AxiError("Downloads are disabled", "DOWNLOADS_DISABLED", ["Enable downloads.allow in the selected file profile"]);
|
|
17
|
+
const path = resolve(out);
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW), 0o600);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new AxiError("Cannot create a new output file", "DOWNLOAD_OUTPUT_INVALID", ["Choose a new --out path in an existing directory; existing files are never overwritten"]);
|
|
24
|
+
}
|
|
25
|
+
let complete = false;
|
|
26
|
+
try {
|
|
27
|
+
if (process.platform === "win32")
|
|
28
|
+
protectWindowsFile(path);
|
|
29
|
+
else
|
|
30
|
+
fchmodSync(fd, 0o600);
|
|
31
|
+
const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
|
|
32
|
+
const result = await api.download({ entityId, attachmentId, all: name === "attachment download-all", original: name === "email download" ? flags.has("--original") : undefined,
|
|
33
|
+
scope: flags.get("--scope") ?? (name === "email download" ? undefined : profile.scope) });
|
|
34
|
+
const sha256 = createHash("sha256").update(result.content).digest("hex");
|
|
35
|
+
if (sha256 !== result.sha256)
|
|
36
|
+
throw new AxiError("Download SHA-256 does not match", "DOWNLOAD_HASH_MISMATCH", ["Discard the failed download and investigate before trying again"]);
|
|
37
|
+
writeFileSync(fd, result.content);
|
|
38
|
+
fsyncSync(fd);
|
|
39
|
+
complete = true;
|
|
40
|
+
return { path, size: result.content.length, sha256 };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof AxiError)
|
|
44
|
+
throw error;
|
|
45
|
+
throw new AxiError("Download could not complete", "DOWNLOAD_FAILED", ["Check the output directory and selected profile"]);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
if (!complete)
|
|
50
|
+
unlinkSync(path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { credentialStore } from "./profiles.js";
|
|
2
|
+
import { gateway } from "./transport.js";
|
|
3
|
+
export declare const exceptionFamilies: {
|
|
4
|
+
readonly "phishing-allow": {
|
|
5
|
+
readonly path: "/exceptions/whitelist";
|
|
6
|
+
readonly types: readonly [];
|
|
7
|
+
};
|
|
8
|
+
readonly "phishing-block": {
|
|
9
|
+
readonly path: "/exceptions/blacklist";
|
|
10
|
+
readonly types: readonly [];
|
|
11
|
+
};
|
|
12
|
+
readonly "spam-allow": {
|
|
13
|
+
readonly path: "/exceptions/spam_whitelist";
|
|
14
|
+
readonly types: readonly [];
|
|
15
|
+
};
|
|
16
|
+
readonly anomaly: {
|
|
17
|
+
readonly path: "/sectools/anomaly/exceptions";
|
|
18
|
+
readonly types: readonly [];
|
|
19
|
+
};
|
|
20
|
+
readonly "click-time": {
|
|
21
|
+
readonly path: "/sectools/click_time_protection/exceptions";
|
|
22
|
+
readonly types: readonly [];
|
|
23
|
+
};
|
|
24
|
+
readonly malware: {
|
|
25
|
+
readonly path: "/sectool-exceptions/checkpoint2/exceptions";
|
|
26
|
+
readonly types: readonly ["hash", "micro_hash", "file_type", "ppat_sender_name", "allow_file_type", "sender_email"];
|
|
27
|
+
};
|
|
28
|
+
readonly url: {
|
|
29
|
+
readonly path: "/sectool-exceptions/avanan_url/exceptions";
|
|
30
|
+
readonly types: readonly ["allow-url", "allow-domain", "block-url", "block-domain"];
|
|
31
|
+
};
|
|
32
|
+
readonly dlp: {
|
|
33
|
+
readonly path: "/sectool-exceptions/avanan_dlp/exceptions";
|
|
34
|
+
readonly types: readonly ["hash", "text_content", "sender_email", "recipient_email"];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export declare function exceptionCommand(name: "exception list" | "exception show", kind: string | undefined, value: string | undefined, flags: Map<string, string>, store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { gateway, READ_LIMITS } from "./transport.js";
|
|
4
|
+
// Read routes and selectors: guide pages 49-60, 69-74, 83, 87-91, 100-102, 110-112 and 120-123.
|
|
5
|
+
export const exceptionFamilies = {
|
|
6
|
+
"phishing-allow": { path: "/exceptions/whitelist", types: [] },
|
|
7
|
+
"phishing-block": { path: "/exceptions/blacklist", types: [] },
|
|
8
|
+
"spam-allow": { path: "/exceptions/spam_whitelist", types: [] },
|
|
9
|
+
anomaly: { path: "/sectools/anomaly/exceptions", types: [] },
|
|
10
|
+
"click-time": { path: "/sectools/click_time_protection/exceptions", types: [] },
|
|
11
|
+
malware: { path: "/sectool-exceptions/checkpoint2/exceptions", types: ["hash", "micro_hash", "file_type", "ppat_sender_name", "allow_file_type", "sender_email"] },
|
|
12
|
+
url: { path: "/sectool-exceptions/avanan_url/exceptions", types: ["allow-url", "allow-domain", "block-url", "block-domain"] },
|
|
13
|
+
dlp: { path: "/sectool-exceptions/avanan_dlp/exceptions", types: ["hash", "text_content", "sender_email", "recipient_email"] },
|
|
14
|
+
};
|
|
15
|
+
// Only these families have published response fields (pages 50, 56-60 and 70-74).
|
|
16
|
+
const exception = z.object({ entityId: z.string().min(1), senderEmail: z.string().nullish(),
|
|
17
|
+
subject: z.string().nullish(), comment: z.string().nullish() }).passthrough();
|
|
18
|
+
export async function exceptionCommand(name, kind, value, flags, store, transport) {
|
|
19
|
+
const invalid = (message) => { throw new AxiError(message, "VALIDATION_ERROR", [`Run checkpoint-axi ${name} --help`]); };
|
|
20
|
+
if (!kind || !Object.hasOwn(exceptionFamilies, kind))
|
|
21
|
+
invalid(`Choose an exception kind: ${Object.keys(exceptionFamilies).join(", ")}`);
|
|
22
|
+
const family = exceptionFamilies[kind];
|
|
23
|
+
const show = name === "exception show";
|
|
24
|
+
const documented = family.path.startsWith("/exceptions/");
|
|
25
|
+
const type = flags.get("--type");
|
|
26
|
+
if (family.types.length ? !type || !family.types.includes(type) : type !== undefined) {
|
|
27
|
+
invalid(family.types.length ? `--type must be one of: ${family.types.join(", ")}` : "--type is only valid for malware, url and dlp");
|
|
28
|
+
}
|
|
29
|
+
if (flags.has("--items") && kind !== "click-time")
|
|
30
|
+
invalid("--items is only valid for click-time");
|
|
31
|
+
if (show && (!value?.trim() || /[\u0000-\u001f\u007f]/.test(value) || value === "." || value === ".."))
|
|
32
|
+
invalid("show requires one nonempty exception ID or value");
|
|
33
|
+
if (show && documented && !/^[A-Za-z0-9_-]+$/.test(value))
|
|
34
|
+
invalid("Exception ID must contain letters, digits, underscores or hyphens");
|
|
35
|
+
const limitText = flags.get("--limit") ?? "50";
|
|
36
|
+
if (!/^[1-9][0-9]*$/.test(limitText) || Number(limitText) > READ_LIMITS.rows)
|
|
37
|
+
invalid("--limit must be an integer from 1 to 1000");
|
|
38
|
+
if (flags.has("--all") && flags.has("--limit"))
|
|
39
|
+
invalid("Use either --all or --limit");
|
|
40
|
+
if (show && kind === "anomaly")
|
|
41
|
+
throw new AxiError("Anomaly detail endpoint and response identity are undocumented (guide pages 83-84)", "ENDPOINT_UNVERIFIED", ["Use checkpoint-axi exception list anomaly; owner commissioning happens later through Artium"]);
|
|
42
|
+
let path = family.path;
|
|
43
|
+
if (type)
|
|
44
|
+
path += `/${type}`;
|
|
45
|
+
if (flags.has("--items"))
|
|
46
|
+
path += "/items";
|
|
47
|
+
if (show) {
|
|
48
|
+
try {
|
|
49
|
+
path += `/${encodeURIComponent(value)}`;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
invalid("Exception value must be valid Unicode");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
store.readOnly();
|
|
56
|
+
const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
|
|
57
|
+
const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
|
|
58
|
+
const page = await api.read({ path: path });
|
|
59
|
+
if (documented && !z.array(exception).safeParse(page.records).success)
|
|
60
|
+
throw new AxiError("Invalid exception response", "RESPONSE_INVALID", []);
|
|
61
|
+
const unverified = documented ? {} : { responseSchema: "unverified" };
|
|
62
|
+
if (show) {
|
|
63
|
+
if (!page.records.length)
|
|
64
|
+
throw new AxiError("Exception was not found", "NOT_FOUND", []);
|
|
65
|
+
if (documented && (page.records.length !== 1 || exception.parse(page.records[0]).entityId !== value)) {
|
|
66
|
+
throw new AxiError("Invalid exception detail response", "RESPONSE_INVALID", []);
|
|
67
|
+
}
|
|
68
|
+
return { exception: documented ? page.records[0] : page.records, ...unverified };
|
|
69
|
+
}
|
|
70
|
+
const limit = flags.has("--all") ? READ_LIMITS.rows : Number(limitText);
|
|
71
|
+
const rows = page.records.slice(0, limit);
|
|
72
|
+
const complete = rows.length === page.records.length && !page.scrollId && (page.total === undefined || page.total <= rows.length);
|
|
73
|
+
return { exceptions: documented ? rows.map((row) => {
|
|
74
|
+
const record = exception.parse(row);
|
|
75
|
+
return { id: record.entityId, sender: record.senderEmail ?? "", subject: record.subject ?? "", comment: record.comment ?? "" };
|
|
76
|
+
}) : rows, count: rows.length, ...(page.total === undefined ? {} : { total: page.total }), complete, ...unverified,
|
|
77
|
+
...(!complete ? { reason: "No continuation request parameter is documented for this exception route" } : {}),
|
|
78
|
+
help: [rows.length ? kind === "anomaly" ? "Anomaly detail remains unavailable pending response identity commissioning"
|
|
79
|
+
: `Run checkpoint-axi exception show ${kind} <id-or-value>${type ? ` --type ${type}` : ""}${flags.has("--items") ? " --items" : ""} for details`
|
|
80
|
+
: `No ${kind} exceptions found`, ...(!complete && limit < READ_LIMITS.rows ? ["Repeat with --all for up to 1000 rows; server-side continuation remains undocumented"] : [])] };
|
|
81
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { catalogue, DESCRIPTION, help, SETUP_GUIDANCE } from "./catalogue.js";
|
|
5
|
+
import { doctor } from "./doctor.js";
|
|
6
|
+
import { readCommand } from "./reads.js";
|
|
7
|
+
import { exceptionCommand } from "./exceptions.js";
|
|
8
|
+
import { emailWriteCommand, eventWriteCommand } from "./writes.js";
|
|
9
|
+
import { exceptionWriteCommand } from "./write-exceptions.js";
|
|
10
|
+
import { downloadCommand } from "./downloads.js";
|
|
11
|
+
export async function localCommand(argv, store, transport) {
|
|
12
|
+
if (argv.length === 1 && argv[0] === "--help")
|
|
13
|
+
return help();
|
|
14
|
+
const name = !argv.length || argv[0]?.startsWith("--") ? "home" : argv[0] === "home" || argv[0] === "doctor" ? argv[0] : argv.slice(0, 2).join(" ");
|
|
15
|
+
const entry = Object.hasOwn(catalogue, name) ? catalogue[name] : undefined;
|
|
16
|
+
const usage = (message = "Unknown command, argument, duplicate or invalid flag") => {
|
|
17
|
+
throw new AxiError(message, "VALIDATION_ERROR", [
|
|
18
|
+
`Valid commands: ${Object.keys(catalogue).join(", ")}; valid flags: ${entry ? Object.keys(entry.flags).map((flag) => `--${flag}`).join(", ") : "run checkpoint-axi --help"}`,
|
|
19
|
+
]);
|
|
20
|
+
};
|
|
21
|
+
if (!entry)
|
|
22
|
+
return usage();
|
|
23
|
+
const flags = new Map();
|
|
24
|
+
const isExceptionWrite = name === "exception create" || name === "exception update" || name === "exception remove" || name === "exception remove-items" || name === "exception clear";
|
|
25
|
+
const isException = name === "exception list" || name === "exception show" || isExceptionWrite;
|
|
26
|
+
const isDownload = name === "email download" || name === "attachment download" || name === "attachment download-all";
|
|
27
|
+
const kind = isException && !argv[2]?.startsWith("--") ? argv[2] : undefined;
|
|
28
|
+
const idIndex = isException ? 3 : 2;
|
|
29
|
+
const entityId = (isDownload || name.endsWith(" show") || name === "exception update" || name === "exception remove") && !argv[idIndex]?.startsWith("--") ? argv[idIndex] : undefined;
|
|
30
|
+
const attachmentId = name === "attachment download" && entityId !== undefined && !argv[3]?.startsWith("--") ? argv[3] : undefined;
|
|
31
|
+
const start = name === "home" ? (argv[0] === "home" ? 1 : 0) : name === "doctor" ? 1
|
|
32
|
+
: isException ? 2 + Number(kind !== undefined) + Number(entityId !== undefined) : entityId === undefined ? 2 : 3 + Number(attachmentId !== undefined);
|
|
33
|
+
for (let index = start; index < argv.length; index++) {
|
|
34
|
+
const flag = argv[index];
|
|
35
|
+
if (!flag.startsWith("--"))
|
|
36
|
+
usage();
|
|
37
|
+
if (!Object.hasOwn(entry.flags, flag.slice(2)) || flags.has(flag)) {
|
|
38
|
+
const label = flag.split("=")[0];
|
|
39
|
+
usage(/^--[A-Za-z][A-Za-z0-9-]*$/.test(label) ? `Unknown or duplicate flag ${label}` : undefined);
|
|
40
|
+
}
|
|
41
|
+
if (["--help", "--full", "--all", "--items", "--specific", "--original", "--execute", "--dry-run"].includes(flag))
|
|
42
|
+
flags.set(flag, "true");
|
|
43
|
+
else {
|
|
44
|
+
const value = argv[++index];
|
|
45
|
+
if (!value || value.startsWith("--"))
|
|
46
|
+
usage();
|
|
47
|
+
flags.set(flag, value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (flags.has("--help"))
|
|
51
|
+
return { command: name, ...entry };
|
|
52
|
+
const available = isException ? name === "exception list" ? ["exceptions", "count", "total", "complete", "reason", "responseSchema", "help"] : ["exception", "responseSchema"]
|
|
53
|
+
: name === "email search" || name === "event list" ? [name === "event list" ? "events" : "emails", "count", "total", "complete", "reason", "cursor", "help"] : name.endsWith(" show") ? [name.split(" ")[0]]
|
|
54
|
+
: name === "doctor" ? ["region", "saas", "checks", "pass", "help"] : name === "auth status" ? ["profile", "source", "region", "origin", "saas", "clientSecret", "readOnly"]
|
|
55
|
+
: name === "profile list" ? ["profiles", "count", "config"] : name === "config path" ? ["config", "state"]
|
|
56
|
+
: ["bin", "description", "status", "profiles", "count", "help"];
|
|
57
|
+
const isWrite = name === "email quarantine" || name === "email restore" || name === "event action" || isExceptionWrite;
|
|
58
|
+
if (isWrite)
|
|
59
|
+
available.splice(0, available.length, "outcome", "preview", "auditId", "tasks", "response", "responseSchema", "help");
|
|
60
|
+
if (isDownload)
|
|
61
|
+
available.splice(0, available.length, "path", "size", "sha256");
|
|
62
|
+
const fields = flags.get("--fields")?.split(",") ?? available;
|
|
63
|
+
if (fields.some((field) => !available.includes(field)) || new Set(fields).size !== fields.length)
|
|
64
|
+
usage();
|
|
65
|
+
const config = flags.get("--config");
|
|
66
|
+
let result;
|
|
67
|
+
if (isDownload)
|
|
68
|
+
result = await downloadCommand(name, entityId, attachmentId, flags, store, transport);
|
|
69
|
+
else if (name === "exception create" || name === "exception update" || name === "exception remove" || name === "exception remove-items" || name === "exception clear")
|
|
70
|
+
result = await exceptionWriteCommand(name, kind, entityId, flags, store, transport);
|
|
71
|
+
else if (name === "event action")
|
|
72
|
+
result = await eventWriteCommand(flags, store, transport);
|
|
73
|
+
else if (name === "email quarantine" || name === "email restore")
|
|
74
|
+
result = await emailWriteCommand(name, flags, store, transport);
|
|
75
|
+
else if (name === "exception list" || name === "exception show")
|
|
76
|
+
result = await exceptionCommand(name, kind, entityId, flags, store, transport);
|
|
77
|
+
else if (name === "email search" || name === "email show" || name === "event list" || name === "event show" || name === "task show")
|
|
78
|
+
result = await readCommand(name, entityId, flags, store, transport);
|
|
79
|
+
else if (name === "doctor")
|
|
80
|
+
result = await doctor(store, config, flags.get("--profile"), flags.get("--saas"), transport);
|
|
81
|
+
else if (name === "auth status")
|
|
82
|
+
result = store.status(config, flags.get("--profile"));
|
|
83
|
+
else if (name === "profile list")
|
|
84
|
+
result = store.list(config);
|
|
85
|
+
else if (name === "config path")
|
|
86
|
+
result = { config: store.configPath(config), state: store.statePath() };
|
|
87
|
+
else {
|
|
88
|
+
const listed = store.list(config);
|
|
89
|
+
// Selection errors matter when context exists, but a new installation has a useful empty home.
|
|
90
|
+
const loaded = store.load(config);
|
|
91
|
+
let status = "not configured";
|
|
92
|
+
try {
|
|
93
|
+
const selected = store.select(loaded.config, flags.get("--profile"));
|
|
94
|
+
status = `configured: ${selected.name} (${selected.region}); not verified with the API`;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof AxiError) || error.code !== "PROFILE_REQUIRED")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const executable = resolve(process.argv[1] ?? "bin/checkpoint-axi.js");
|
|
101
|
+
const home = homedir();
|
|
102
|
+
result = { bin: executable.startsWith(`${home}/`) ? `~${executable.slice(home.length)}` : executable,
|
|
103
|
+
description: DESCRIPTION, status, profiles: listed.profiles, count: listed.count,
|
|
104
|
+
help: [SETUP_GUIDANCE, "Run checkpoint-axi auth status to validate local credentials", "Run checkpoint-axi profile list to inspect configured profiles"] };
|
|
105
|
+
}
|
|
106
|
+
let truncated = false;
|
|
107
|
+
const preview = (value) => {
|
|
108
|
+
if (typeof value === "string" && value.length > 500 && !flags.has("--full")) {
|
|
109
|
+
truncated = true;
|
|
110
|
+
return `${value.slice(0, 500)}... (truncated, ${value.length} chars total)`;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(value))
|
|
113
|
+
return value.map(preview);
|
|
114
|
+
if (value && typeof value === "object")
|
|
115
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, preview(child)]));
|
|
116
|
+
return value;
|
|
117
|
+
};
|
|
118
|
+
const projected = Object.fromEntries(fields.filter((field) => result[field] !== undefined).map((field) => [field,
|
|
119
|
+
field === "cursor" || isDownload ? store.redactor.value(result[field]) : preview(store.redactor.value(result[field]))]));
|
|
120
|
+
if (truncated && !isWrite)
|
|
121
|
+
projected.help = [...(Array.isArray(projected.help) ? projected.help : []), "Repeat the command with --full to see complete strings"];
|
|
122
|
+
return projected;
|
|
123
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type WindowsAcl = (path: string, verify: boolean) => void;
|
|
2
|
+
export declare const windowsAcl: WindowsAcl;
|
|
3
|
+
export declare function protectWindowsFile(path: string): void;
|
|
4
|
+
export declare function toolDirectory(kind: "config" | "state", env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, home?: string): string;
|
|
5
|
+
export type PrivateFileOptions = {
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
acl?: WindowsAcl;
|
|
8
|
+
uid?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function readPrivateFile(path: string, secret: boolean, verifyAcl: boolean, options?: PrivateFileOptions): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, win32 } from "node:path";
|
|
5
|
+
import { AxiError } from "axi-sdk-js";
|
|
6
|
+
export const windowsAcl = (path, verify) => {
|
|
7
|
+
const quoted = `'${path.replaceAll("'", "''")}'`;
|
|
8
|
+
const script = `$ErrorActionPreference='Stop'; $p=${quoted}; $item=Get-Item -LiteralPath $p -Force; `
|
|
9
|
+
+ `if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point' }; `
|
|
10
|
+
+ (verify ? `$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $allowed=@($sid.Value,'S-1-5-18','S-1-5-32-544'); `
|
|
11
|
+
+ `$acl=Get-Acl -LiteralPath $p; if (!$acl.AreAccessRulesProtected -or ($acl.Owner -ne $sid.Translate([Security.Principal.NTAccount]).Value -and $acl.Owner -ne $sid.Value)) { throw 'Unprotected ACL' }; `
|
|
12
|
+
+ `$seen=@(); foreach ($r in $acl.Access) { $s=$r.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value; if ($s -notin $allowed -or $r.IsInherited -or $r.AccessControlType -ne 'Allow' -or $r.FileSystemRights -ne [Security.AccessControl.FileSystemRights]::FullControl) { throw 'Unexpected ACL' }; $seen+=$s }; foreach ($s in $allowed) { if ($s -notin $seen) { throw 'Missing ACL' } }` : "");
|
|
13
|
+
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "pipe" });
|
|
14
|
+
};
|
|
15
|
+
export function protectWindowsFile(path) {
|
|
16
|
+
const quoted = `'${path.replaceAll("'", "''")}'`;
|
|
17
|
+
const script = `$ErrorActionPreference='Stop'; $p=${quoted}; $item=Get-Item -LiteralPath $p -Force; `
|
|
18
|
+
+ `if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point' }; `
|
|
19
|
+
+ `$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $acl=Get-Acl -LiteralPath $p; `
|
|
20
|
+
+ `$acl.SetAccessRuleProtection($true,$false); foreach ($r in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($r) }; $acl.SetOwner($sid); `
|
|
21
|
+
+ `foreach ($s in @($sid.Value,'S-1-5-18','S-1-5-32-544')) { $id=New-Object Security.Principal.SecurityIdentifier($s); `
|
|
22
|
+
+ `$rule=New-Object Security.AccessControl.FileSystemAccessRule($id,'FullControl','Allow'); $acl.AddAccessRule($rule) }; Set-Acl -LiteralPath $p -AclObject $acl`;
|
|
23
|
+
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "pipe" });
|
|
24
|
+
windowsAcl(path, true);
|
|
25
|
+
}
|
|
26
|
+
export function toolDirectory(kind, env = process.env, platform = process.platform, home = homedir()) {
|
|
27
|
+
if (platform === "win32") {
|
|
28
|
+
const base = env[kind === "config" ? "APPDATA" : "LOCALAPPDATA"];
|
|
29
|
+
return win32.join(base && win32.isAbsolute(base) ? base : win32.join(home, "AppData", kind === "config" ? "Roaming" : "Local"), "checkpoint-axi");
|
|
30
|
+
}
|
|
31
|
+
const xdg = env[kind === "config" ? "XDG_CONFIG_HOME" : "XDG_STATE_HOME"];
|
|
32
|
+
return join(xdg && isAbsolute(xdg) ? xdg : join(home, kind === "config" ? ".config" : ".local/state"), "checkpoint-axi");
|
|
33
|
+
}
|
|
34
|
+
export function readPrivateFile(path, secret, verifyAcl, options = {}) {
|
|
35
|
+
const platform = options.platform ?? process.platform;
|
|
36
|
+
const insecure = () => {
|
|
37
|
+
throw new AxiError("Insecure private file", "CONFIG_INSECURE", [
|
|
38
|
+
platform === "win32" ? "Apply a protected DACL granting FullControl only to the current user, SYSTEM and Administrators"
|
|
39
|
+
: "Ensure the file is regular, owned by the current user and has mode 600",
|
|
40
|
+
]);
|
|
41
|
+
};
|
|
42
|
+
function validate(stat) {
|
|
43
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
44
|
+
insecure();
|
|
45
|
+
if (platform !== "win32" && (stat.uid !== (options.uid ?? process.getuid()) || (stat.mode & (secret ? 0o077 : 0o022)) !== 0))
|
|
46
|
+
insecure();
|
|
47
|
+
}
|
|
48
|
+
validate(lstatSync(path));
|
|
49
|
+
if (platform === "win32") {
|
|
50
|
+
try {
|
|
51
|
+
(options.acl ?? windowsAcl)(path, verifyAcl);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
insecure();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const fd = openSync(path, constants.O_RDONLY | (platform === "win32" ? 0 : constants.O_NOFOLLOW));
|
|
58
|
+
try {
|
|
59
|
+
validate(fstatSync(fd));
|
|
60
|
+
return readFileSync(fd, "utf8");
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
}
|
|
65
|
+
}
|