@knowttl/falcon-axi 0.0.0-stage → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.typescript-build/native/checksums.json +4 -0
  2. package/.typescript-build/src/auth.js +233 -0
  3. package/.typescript-build/src/axi-credential.schema.json +64 -0
  4. package/.typescript-build/src/bin.js +9 -0
  5. package/.typescript-build/src/cli.js +345 -0
  6. package/.typescript-build/src/core.js +31 -0
  7. package/.typescript-build/src/cql.js +159 -0
  8. package/.typescript-build/src/credentials.js +235 -0
  9. package/.typescript-build/src/cursor.js +65 -0
  10. package/.typescript-build/src/domain.js +119 -0
  11. package/.typescript-build/src/executable.js +45 -0
  12. package/.typescript-build/src/exposure.js +533 -0
  13. package/.typescript-build/src/help.json +91 -0
  14. package/.typescript-build/src/hook.js +19 -0
  15. package/.typescript-build/src/identity.js +313 -0
  16. package/.typescript-build/src/inspection-commands.json +539 -0
  17. package/.typescript-build/src/inspection.js +651 -0
  18. package/.typescript-build/src/inventory.js +356 -0
  19. package/.typescript-build/src/investigation.js +893 -0
  20. package/.typescript-build/src/malquery.js +145 -0
  21. package/.typescript-build/src/metadata.js +559 -0
  22. package/.typescript-build/src/origin.js +47 -0
  23. package/.typescript-build/src/parser.js +297 -0
  24. package/.typescript-build/src/render.js +92 -0
  25. package/.typescript-build/src/response.js +65 -0
  26. package/.typescript-build/src/rtr-filesystem.js +50 -0
  27. package/.typescript-build/src/rtr-policy.js +100 -0
  28. package/.typescript-build/src/rtr.js +409 -0
  29. package/.typescript-build/src/search-commands.json +184 -0
  30. package/.typescript-build/src/search-help.json +15 -0
  31. package/.typescript-build/src/search.js +237 -0
  32. package/.typescript-build/src/setup.js +173 -0
  33. package/.typescript-build/src/skill.js +87 -0
  34. package/.typescript-build/src/transport/descriptors.json +1598 -0
  35. package/.typescript-build/src/transport/dns-worker.js +8 -0
  36. package/.typescript-build/src/transport/documents.json +72 -0
  37. package/.typescript-build/src/transport/https.js +530 -0
  38. package/.typescript-build/src/transport/operations.js +55 -0
  39. package/.typescript-build/src/transport/resolver.js +89 -0
  40. package/.typescript-build/src/transport/search-authority.js +112 -0
  41. package/.typescript-build/src/version.js +1 -0
  42. package/.typescript-build/src/windows-acl.js +37 -0
  43. package/LICENSE +21 -0
  44. package/README.md +192 -2
  45. package/artium-invocations.json +1484 -0
  46. package/native/prebuilds/linux-x64.node +0 -0
  47. package/native/prebuilds/win32-x64.node +0 -0
  48. package/package.json +58 -4
  49. package/skills/falcon-axi/SKILL.md +104 -0
  50. package/skills/falcon-axi/references/account-list.md +25 -0
  51. package/skills/falcon-axi/references/account-show.md +14 -0
  52. package/skills/falcon-axi/references/actor-list.md +19 -0
  53. package/skills/falcon-axi/references/actor-show.md +18 -0
  54. package/skills/falcon-axi/references/agent-guide.md +93 -0
  55. package/skills/falcon-axi/references/application-list.md +30 -0
  56. package/skills/falcon-axi/references/asset-list.md +19 -0
  57. package/skills/falcon-axi/references/asset-show.md +12 -0
  58. package/skills/falcon-axi/references/auth-status.md +10 -0
  59. package/skills/falcon-axi/references/case-list.md +15 -0
  60. package/skills/falcon-axi/references/case-show.md +12 -0
  61. package/skills/falcon-axi/references/classification-list.md +18 -0
  62. package/skills/falcon-axi/references/classification-show.md +11 -0
  63. package/skills/falcon-axi/references/cloud-asset-list.md +19 -0
  64. package/skills/falcon-axi/references/cloud-asset-show.md +13 -0
  65. package/skills/falcon-axi/references/cloud-risk-list.md +19 -0
  66. package/skills/falcon-axi/references/config-path.md +9 -0
  67. package/skills/falcon-axi/references/container-list.md +18 -0
  68. package/skills/falcon-axi/references/correlation-rule-list.md +15 -0
  69. package/skills/falcon-axi/references/cve-list.md +23 -0
  70. package/skills/falcon-axi/references/cve-show.md +22 -0
  71. package/skills/falcon-axi/references/data-policy-list.md +20 -0
  72. package/skills/falcon-axi/references/data-policy-show.md +12 -0
  73. package/skills/falcon-axi/references/detection-list.md +29 -0
  74. package/skills/falcon-axi/references/detection-process.md +19 -0
  75. package/skills/falcon-axi/references/detection-show.md +13 -0
  76. package/skills/falcon-axi/references/detection-summary.md +21 -0
  77. package/skills/falcon-axi/references/exclusion-list.md +21 -0
  78. package/skills/falcon-axi/references/exclusion-show.md +13 -0
  79. package/skills/falcon-axi/references/file-change-list.md +18 -0
  80. package/skills/falcon-axi/references/file-change-show.md +14 -0
  81. package/skills/falcon-axi/references/firewall-group-list.md +15 -0
  82. package/skills/falcon-axi/references/firewall-rule-list.md +15 -0
  83. package/skills/falcon-axi/references/firewall-rule-show.md +12 -0
  84. package/skills/falcon-axi/references/host-group-list.md +17 -0
  85. package/skills/falcon-axi/references/host-group-members.md +17 -0
  86. package/skills/falcon-axi/references/host-list.md +27 -0
  87. package/skills/falcon-axi/references/host-logins.md +22 -0
  88. package/skills/falcon-axi/references/host-network.md +17 -0
  89. package/skills/falcon-axi/references/host-show.md +13 -0
  90. package/skills/falcon-axi/references/host-usb.md +22 -0
  91. package/skills/falcon-axi/references/identity-activity.md +21 -0
  92. package/skills/falcon-axi/references/identity-assessment.md +13 -0
  93. package/skills/falcon-axi/references/identity-list.md +24 -0
  94. package/skills/falcon-axi/references/identity-rule-list.md +18 -0
  95. package/skills/falcon-axi/references/identity-rule-show.md +12 -0
  96. package/skills/falcon-axi/references/identity-sensor-list.md +14 -0
  97. package/skills/falcon-axi/references/identity-sensor-show.md +11 -0
  98. package/skills/falcon-axi/references/identity-show.md +18 -0
  99. package/skills/falcon-axi/references/identity-timeline.md +21 -0
  100. package/skills/falcon-axi/references/image-list.md +18 -0
  101. package/skills/falcon-axi/references/image-vulnerabilities.md +18 -0
  102. package/skills/falcon-axi/references/indicator-list.md +20 -0
  103. package/skills/falcon-axi/references/indicator-show.md +13 -0
  104. package/skills/falcon-axi/references/ioa-group-list.md +15 -0
  105. package/skills/falcon-axi/references/ioc-hosts.md +15 -0
  106. package/skills/falcon-axi/references/ioc-list.md +17 -0
  107. package/skills/falcon-axi/references/ioc-processes.md +16 -0
  108. package/skills/falcon-axi/references/ioc-show.md +11 -0
  109. package/skills/falcon-axi/references/iom-list.md +19 -0
  110. package/skills/falcon-axi/references/iom-show.md +13 -0
  111. package/skills/falcon-axi/references/iot-list.md +17 -0
  112. package/skills/falcon-axi/references/login-list.md +18 -0
  113. package/skills/falcon-axi/references/malquery-quota.md +16 -0
  114. package/skills/falcon-axi/references/malquery-search.md +21 -0
  115. package/skills/falcon-axi/references/malquery-show.md +17 -0
  116. package/skills/falcon-axi/references/malquery-status.md +17 -0
  117. package/skills/falcon-axi/references/malware-list.md +17 -0
  118. package/skills/falcon-axi/references/malware-show.md +13 -0
  119. package/skills/falcon-axi/references/policy-list.md +19 -0
  120. package/skills/falcon-axi/references/policy-members.md +19 -0
  121. package/skills/falcon-axi/references/policy-show.md +16 -0
  122. package/skills/falcon-axi/references/process-show.md +19 -0
  123. package/skills/falcon-axi/references/process-tree.md +20 -0
  124. package/skills/falcon-axi/references/profile-list.md +9 -0
  125. package/skills/falcon-axi/references/quarantine-list.md +16 -0
  126. package/skills/falcon-axi/references/quarantine-show.md +11 -0
  127. package/skills/falcon-axi/references/recon-notification-list.md +16 -0
  128. package/skills/falcon-axi/references/recon-notification-show.md +12 -0
  129. package/skills/falcon-axi/references/recon-record-list.md +16 -0
  130. package/skills/falcon-axi/references/recon-record-show.md +12 -0
  131. package/skills/falcon-axi/references/recon-rule-list.md +16 -0
  132. package/skills/falcon-axi/references/recon-rule-show.md +12 -0
  133. package/skills/falcon-axi/references/report-list.md +19 -0
  134. package/skills/falcon-axi/references/report-show.md +17 -0
  135. package/skills/falcon-axi/references/rtr-audit-list.md +15 -0
  136. package/skills/falcon-axi/references/rtr-run.md +24 -0
  137. package/skills/falcon-axi/references/rtr-session-list.md +15 -0
  138. package/skills/falcon-axi/references/rtr-session-show.md +12 -0
  139. package/skills/falcon-axi/references/sandbox-list.md +21 -0
  140. package/skills/falcon-axi/references/sandbox-show.md +16 -0
  141. package/skills/falcon-axi/references/scopes.md +10 -0
  142. package/skills/falcon-axi/references/search-start.md +26 -0
  143. package/skills/falcon-axi/references/search-status.md +22 -0
  144. package/skills/falcon-axi/references/search-stop.md +16 -0
  145. package/skills/falcon-axi/references/sensor-usage-list.md +15 -0
  146. package/skills/falcon-axi/references/serverless-list.md +19 -0
  147. package/skills/falcon-axi/references/setup-hooks.md +14 -0
  148. package/skills/falcon-axi/references/setup.md +13 -0
  149. package/skills/falcon-axi/references/shield-list.md +19 -0
  150. package/skills/falcon-axi/references/shield-show.md +16 -0
  151. package/skills/falcon-axi/references/vuln-list.md +30 -0
  152. package/skills/falcon-axi/references/vuln-show.md +16 -0
@@ -0,0 +1,31 @@
1
+ export class CliError extends Error {
2
+ code;
3
+ help;
4
+ details;
5
+ constructor(code, message, help = [], details = {}) {
6
+ super(message);
7
+ this.code = code;
8
+ this.help = help;
9
+ this.details = details;
10
+ }
11
+ get exitCode() {
12
+ return ["VALIDATION_ERROR", "FQL_INVALID", "CONFIG_INVALID"].includes(this.code)
13
+ ? 2
14
+ : 1;
15
+ }
16
+ }
17
+ export function object(value) {
18
+ return value !== null && typeof value === "object" && !Array.isArray(value);
19
+ }
20
+ // Preserve numeric lexemes outside Number's exact integer domain before rounding.
21
+ export function parseResponse(text) {
22
+ return JSON.parse(text, (_key, value, context) => {
23
+ if (typeof value !== "number" ||
24
+ Math.abs(value) <= Number.MAX_SAFE_INTEGER ||
25
+ !context?.source)
26
+ return value;
27
+ return /^-?\d+$/.test(context.source)
28
+ ? BigInt(context.source)
29
+ : context.source;
30
+ });
31
+ }
@@ -0,0 +1,159 @@
1
+ import { CliError } from "./core.js";
2
+ // Refuse certain structural errors; ambiguous CQL remains Falcon's decision.
3
+ export function validateQuery(query) {
4
+ const open = {
5
+ "(": ")",
6
+ "[": "]",
7
+ "{": "}",
8
+ };
9
+ const classes = {
10
+ "(": "parenthesis",
11
+ ")": "parenthesis",
12
+ "[": "square bracket",
13
+ "]": "square bracket",
14
+ "{": "brace",
15
+ "}": "brace",
16
+ };
17
+ const fail = (offset, message, expected = null) => {
18
+ const before = Array.from(query.slice(0, offset));
19
+ const line = before.filter((char) => char === "\n").length + 1;
20
+ const column = before.length - before.lastIndexOf("\n");
21
+ throw new CliError("VALIDATION_ERROR", `CQL syntax error at line ${line}, column ${column}: ${message}`, [
22
+ expected
23
+ ? `Expected ${expected}; correct the CQL structure before retrying`
24
+ : "Remove the unexpected closing delimiter",
25
+ ], { diagnostics: [{ line, column, message, expected }] });
26
+ };
27
+ if (!query.trim())
28
+ fail(0, "missing query", "a non-empty CQL query in --query");
29
+ const stack = [];
30
+ let content = false;
31
+ let pipe;
32
+ let assignment;
33
+ let previous = "";
34
+ let i = 0;
35
+ while (i < query.length) {
36
+ const char = query[i];
37
+ if (/\s/.test(char)) {
38
+ i++;
39
+ continue;
40
+ }
41
+ if (query.startsWith("/*", i)) {
42
+ const end = query.indexOf("*/", i + 2);
43
+ if (end < 0)
44
+ fail(i, "unterminated block comment", "*/");
45
+ i = end + 2;
46
+ continue;
47
+ }
48
+ if (query.startsWith("//", i)) {
49
+ const end = query.indexOf("\n", i);
50
+ i = end < 0 ? query.length : end;
51
+ continue;
52
+ }
53
+ if (assignment !== undefined) {
54
+ if ("|)]};,".includes(char))
55
+ fail(assignment, "dangling assignment", "an expression after ':='");
56
+ assignment = undefined;
57
+ }
58
+ const regex = char === "/";
59
+ if (regex &&
60
+ ![
61
+ "",
62
+ "=",
63
+ "(",
64
+ ",",
65
+ "|",
66
+ "[",
67
+ "{",
68
+ ";",
69
+ "=>",
70
+ "AND",
71
+ "OR",
72
+ "NOT",
73
+ ].includes(previous))
74
+ return;
75
+ if (char === '"' || regex) {
76
+ let end = i + 1;
77
+ while (end < query.length && query[end] !== char) {
78
+ if (/[\r\n]/.test(query[end]))
79
+ break;
80
+ end +=
81
+ query[end] === "\\" &&
82
+ end + 1 < query.length &&
83
+ !/[\r\n]/.test(query[end + 1])
84
+ ? 2
85
+ : 1;
86
+ }
87
+ if (end >= query.length || query[end] !== char)
88
+ fail(i, regex ? "unterminated regex literal" : "unterminated string", char);
89
+ i = end + 1;
90
+ while (regex && i < query.length && "dim".includes(query[i]))
91
+ i++;
92
+ content = true;
93
+ previous = "value";
94
+ continue;
95
+ }
96
+ if (Object.hasOwn(open, char)) {
97
+ stack.push({ char, offset: i, pipe });
98
+ content = false;
99
+ pipe = undefined;
100
+ previous = char;
101
+ i++;
102
+ continue;
103
+ }
104
+ else if (")]}".includes(char)) {
105
+ if (!stack.length || open[stack.at(-1).char] !== char)
106
+ fail(i, `unexpected closing ${classes[char]}`, stack.length ? open[stack.at(-1).char] : null);
107
+ if (!content && pipe !== undefined)
108
+ fail(pipe, "query ends with an empty pipeline stage", "a function after the last '|'");
109
+ pipe = stack.pop().pipe;
110
+ }
111
+ else if (char === "|") {
112
+ if (!content)
113
+ fail(i, "empty pipeline stage", "a filter or function before '|'");
114
+ content = false;
115
+ pipe = i;
116
+ previous = char;
117
+ i++;
118
+ continue;
119
+ }
120
+ else if (";,".includes(char) || query.startsWith("=>", i)) {
121
+ if (!content && pipe !== undefined)
122
+ fail(pipe, "query ends with an empty pipeline stage", "a function after the last '|'");
123
+ content = false;
124
+ pipe = undefined;
125
+ previous = query.startsWith("=>", i) ? "=>" : char;
126
+ i += previous.length;
127
+ continue;
128
+ }
129
+ else if (query.startsWith(":=", i)) {
130
+ assignment = i;
131
+ previous = "=";
132
+ i += 2;
133
+ continue;
134
+ }
135
+ else if (!"=!<>/".includes(char)) {
136
+ let end = i + 1;
137
+ while (end < query.length && !/\s|[()[\]{}"=,!<>;|]/.test(query[end])) {
138
+ if (query.startsWith(":=", end))
139
+ break;
140
+ end++;
141
+ }
142
+ previous = query.slice(i, end);
143
+ content = true;
144
+ i = end;
145
+ continue;
146
+ }
147
+ content = true;
148
+ previous = char;
149
+ i++;
150
+ }
151
+ if (assignment !== undefined)
152
+ fail(assignment, "dangling assignment", "an expression after ':='");
153
+ if (stack.length) {
154
+ const last = stack.at(-1);
155
+ fail(last.offset, `unclosed ${classes[last.char]}`, open[last.char]);
156
+ }
157
+ if (!content && pipe !== undefined)
158
+ fail(pipe, "query ends with an empty pipeline stage", "a function after the last '|'");
159
+ }
@@ -0,0 +1,235 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, win32 } from "node:path";
5
+ import { Ajv2020 } from "ajv/dist/2020.js";
6
+ import schema from "./axi-credential.schema.json" with { type: "json" };
7
+ import { CliError, object } from "./core.js";
8
+ import { registerSecret, registerMemberCid } from "./render.js";
9
+ import { verifyWindowsFile } from "./windows-acl.js";
10
+ import { rtrPolicy } from "./rtr-policy.js";
11
+ const aliases = {
12
+ FALCON_CLIENT_ID: "FALCON_AXI_CLIENT_ID",
13
+ FALCON_CLIENT_SECRET: "FALCON_AXI_CLIENT_SECRET",
14
+ FALCON_MEMBER_CID: "FALCON_AXI_MEMBER_CID",
15
+ FALCON_BASE_URL: "FALCON_AXI_REGION",
16
+ FALCON_PROXY_URL: "FALCON_AXI_PROXY_URL",
17
+ };
18
+ const sourceAliases = new Map();
19
+ export function normalizeEnvironment() {
20
+ const warnings = [];
21
+ for (const [old, current] of Object.entries(aliases)) {
22
+ if (process.env[old] !== undefined) {
23
+ warnings.push(`${old} is deprecated; use ${current}`);
24
+ if (process.env[current] === undefined) {
25
+ sourceAliases.set(current, old);
26
+ process.env[current] = process.env[old];
27
+ }
28
+ }
29
+ }
30
+ return warnings;
31
+ }
32
+ export function directory(kind) {
33
+ const env = process.env;
34
+ if (process.platform === "win32")
35
+ return join(env[kind === "config" ? "APPDATA" : "LOCALAPPDATA"] ||
36
+ join(homedir(), "AppData", kind === "config" ? "Roaming" : "Local"), "falcon-axi");
37
+ const root = env[`XDG_${kind.toUpperCase()}_HOME`];
38
+ return join(root && isAbsolute(root)
39
+ ? root
40
+ : join(homedir(), kind === "config" ? ".config" : ".local/state"), "falcon-axi");
41
+ }
42
+ export function configPath(flag) {
43
+ return (flag ||
44
+ process.env.FALCON_AXI_CONFIG ||
45
+ join(directory("config"), "config.json"));
46
+ }
47
+ export function writeLogPath() {
48
+ return (process.env.FALCON_AXI_WRITE_LOG || join(directory("state"), "writes.log"));
49
+ }
50
+ async function checkLegacy(path) {
51
+ const exists = async (candidate) => {
52
+ try {
53
+ await lstat(candidate);
54
+ return true;
55
+ }
56
+ catch (error) {
57
+ if (object(error) && error.code === "ENOENT")
58
+ return false;
59
+ throw new CliError("CONFIG_INSECURE", `cannot inspect ${candidate}`);
60
+ }
61
+ };
62
+ const legacy = join(homedir(), ".config/falcon-axi/credentials");
63
+ if (!(await exists(path)) && (await exists(legacy)))
64
+ throw new CliError("CONFIG_MOVED", `legacy credentials at ${legacy} must be converted to ${path}`, [
65
+ "Use version 1 config.json profiles with clientId and clientSecret: {env: NAME} or {file: /absolute/path}",
66
+ "See https://github.com/knowttl/artium-axi/blob/main/docs/credential-standard.md",
67
+ ]);
68
+ }
69
+ export async function loadProfiles(config, diagnostic = false) {
70
+ const path = configPath(config);
71
+ await checkLegacy(path);
72
+ const contents = await readPrivate(path, false, diagnostic);
73
+ if (contents === undefined)
74
+ return { profiles: {} };
75
+ let document;
76
+ try {
77
+ document = JSON.parse(contents);
78
+ }
79
+ catch {
80
+ throw new CliError("CONFIG_INVALID", `invalid JSON in ${path}`);
81
+ }
82
+ if (!object(document) ||
83
+ document.version !== 1 ||
84
+ !object(document.profiles) ||
85
+ Object.keys(document).some((key) => !["version", "defaultProfile", "profiles"].includes(key)))
86
+ throw new CliError("CONFIG_INVALID", "config.json requires version 1 and a profiles object");
87
+ const validate = new Ajv2020({ strict: false }).compile(schema);
88
+ for (const [name, entry] of Object.entries(document.profiles)) {
89
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name))
90
+ throw new CliError("CONFIG_INVALID", "invalid profile name");
91
+ if (!validate(entry) || !object(entry))
92
+ throw new CliError("CONFIG_INVALID", `profile ${name} does not match the profile schema`);
93
+ rtrPolicy(entry.rtr);
94
+ for (const field of ["clientId", "region", "memberCid"])
95
+ if (typeof entry[field] === "string" && !entry[field].trim())
96
+ throw new CliError("CONFIG_INVALID", `profile ${name} requires a nonempty string for ${field}`);
97
+ if (object(entry.clientSecret) &&
98
+ typeof entry.clientSecret.file === "string") {
99
+ const file = entry.clientSecret.file;
100
+ if (process.platform === "win32"
101
+ ? !/^[A-Za-z]:$/.test(win32.parse(file).root.slice(0, 2)) ||
102
+ !win32.isAbsolute(file)
103
+ : !isAbsolute(file))
104
+ throw new CliError("CONFIG_INVALID", "clientSecret must reference an absolute local file");
105
+ }
106
+ }
107
+ const defaultProfile = document.defaultProfile;
108
+ if (defaultProfile !== undefined &&
109
+ defaultProfile !== null &&
110
+ (typeof defaultProfile !== "string" ||
111
+ !Object.hasOwn(document.profiles, defaultProfile)))
112
+ throw new CliError("CONFIG_INVALID", "defaultProfile must name an existing profile");
113
+ return {
114
+ profiles: document.profiles,
115
+ ...(typeof defaultProfile === "string" ? { defaultProfile } : {}),
116
+ };
117
+ }
118
+ export async function readPrivate(path, secret = false, diagnostic = false) {
119
+ let info;
120
+ try {
121
+ info = await lstat(path);
122
+ }
123
+ catch (error) {
124
+ if (object(error) && error.code === "ENOENT")
125
+ return undefined;
126
+ throw new CliError("CONFIG_INSECURE", "cannot inspect the selected credential file");
127
+ }
128
+ if (!info.isFile() ||
129
+ info.isSymbolicLink() ||
130
+ (process.platform === "win32" &&
131
+ !(await verifyWindowsFile(path, diagnostic))) ||
132
+ (process.platform !== "win32" &&
133
+ (info.uid !== process.getuid() ||
134
+ (info.mode & (secret ? 0o077 : 0o022)) !== 0)))
135
+ throw new CliError("CONFIG_INSECURE", "the selected credential file is not protected");
136
+ let file;
137
+ try {
138
+ file = await open(path, constants.O_RDONLY |
139
+ (constants.O_NOFOLLOW ?? 0) |
140
+ (constants.O_NONBLOCK ?? 0));
141
+ }
142
+ catch {
143
+ throw new CliError("CONFIG_INVALID", `cannot read UTF-8 from ${path}`);
144
+ }
145
+ try {
146
+ const opened = await file.stat();
147
+ if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino)
148
+ throw new CliError("CONFIG_INSECURE", "the credential file changed during its read check");
149
+ try {
150
+ return new TextDecoder("utf-8", { fatal: true }).decode(await file.readFile());
151
+ }
152
+ catch {
153
+ throw new CliError("CONFIG_INVALID", `cannot read UTF-8 from ${path}`);
154
+ }
155
+ }
156
+ finally {
157
+ await file.close();
158
+ }
159
+ }
160
+ export async function resolveCredential(config, profile, diagnostic = false) {
161
+ const env = process.env;
162
+ const selected = profile ?? env.FALCON_AXI_PROFILE;
163
+ const path = configPath(config);
164
+ await checkLegacy(path);
165
+ let credential;
166
+ if (selected === undefined &&
167
+ (env.FALCON_AXI_CLIENT_ID || env.FALCON_AXI_CLIENT_SECRET)) {
168
+ if (!env.FALCON_AXI_CLIENT_ID || !env.FALCON_AXI_CLIENT_SECRET)
169
+ throw new CliError("CONFIG_INVALID", `env profile is incomplete; set ${env.FALCON_AXI_CLIENT_ID ? "FALCON_AXI_CLIENT_SECRET" : "FALCON_AXI_CLIENT_ID"}`);
170
+ credential = {
171
+ clientId: env.FALCON_AXI_CLIENT_ID,
172
+ clientSecret: env.FALCON_AXI_CLIENT_SECRET,
173
+ channel: "environment",
174
+ profile: "env",
175
+ secretSource: `env:${sourceAliases.get("FALCON_AXI_CLIENT_SECRET") ?? "FALCON_AXI_CLIENT_SECRET"}`,
176
+ ...(env.FALCON_AXI_REGION ? { region: env.FALCON_AXI_REGION } : {}),
177
+ ...(env.FALCON_AXI_MEMBER_CID
178
+ ? { memberCid: env.FALCON_AXI_MEMBER_CID }
179
+ : {}),
180
+ };
181
+ }
182
+ else {
183
+ const document = await loadProfiles(config, diagnostic);
184
+ const profiles = document.profiles;
185
+ const names = Object.keys(profiles);
186
+ const name = selected ??
187
+ document.defaultProfile ??
188
+ (names.length === 1 ? names[0] : undefined);
189
+ if (typeof name !== "string")
190
+ throw new CliError(names.length ? "PROFILE_AMBIGUOUS" : "PROFILE_REQUIRED", names.length
191
+ ? "select a profile with --profile or FALCON_AXI_PROFILE"
192
+ : "no Falcon profile configured", names.length
193
+ ? []
194
+ : [
195
+ "Set FALCON_AXI_CLIENT_ID and FALCON_AXI_CLIENT_SECRET in the environment",
196
+ `Or configure a profile with a clientSecret SecretRef in ${path}`,
197
+ "Provision an API client with read scopes; only `search start`, `search stop`, and `identity activity` need NGSIEM:write",
198
+ ]);
199
+ if (!Object.hasOwn(profiles, name))
200
+ throw new CliError("PROFILE_NOT_FOUND", "selected profile does not exist", ["Run `falcon-axi profile list`"]);
201
+ const settings = profiles[name];
202
+ const ref = settings.clientSecret ?? { env: "FALCON_AXI_CLIENT_SECRET" };
203
+ if (!object(ref))
204
+ throw new CliError("CONFIG_INVALID", "invalid clientSecret SecretRef");
205
+ let secret;
206
+ if (typeof ref.env === "string")
207
+ secret = env[ref.env];
208
+ else if (typeof ref.file === "string" && isAbsolute(ref.file))
209
+ secret = (await readPrivate(ref.file, true, diagnostic))?.trimEnd();
210
+ if (!secret)
211
+ throw new CliError("AUTH_REQUIRED", "clientSecret SecretRef resolved to no value");
212
+ credential = {
213
+ clientId: settings.clientId,
214
+ clientSecret: secret,
215
+ channel: "file",
216
+ rtr: rtrPolicy(settings.rtr),
217
+ profile: name,
218
+ secretSource: typeof ref.env === "string"
219
+ ? `env:${sourceAliases.get(ref.env) ?? ref.env}`
220
+ : `file:${String(ref.file)}`,
221
+ ...(typeof ref.file === "string" ? { path: ref.file } : {}),
222
+ ...(typeof settings.region === "string"
223
+ ? { region: settings.region }
224
+ : {}),
225
+ ...(typeof settings.memberCid === "string"
226
+ ? { memberCid: settings.memberCid }
227
+ : {}),
228
+ };
229
+ }
230
+ registerSecret(credential.clientId);
231
+ registerSecret(credential.clientSecret);
232
+ if (credential.memberCid)
233
+ registerMemberCid(credential.memberCid);
234
+ return credential;
235
+ }
@@ -0,0 +1,65 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { CliError, object } from "./core.js";
3
+ export class Cursor {
4
+ credential;
5
+ origin;
6
+ operation;
7
+ query;
8
+ model;
9
+ sort;
10
+ constructor(credential, origin, operation, query, model = "offset", sort = null) {
11
+ this.credential = credential;
12
+ this.origin = origin;
13
+ this.operation = operation;
14
+ this.query = query;
15
+ this.model = model;
16
+ this.sort = sort;
17
+ }
18
+ fingerprint(position) {
19
+ return createHmac("sha256", this.credential.clientSecret)
20
+ .update(JSON.stringify({
21
+ client_id: this.credential.clientId,
22
+ member_cid: this.credential.memberCid ?? null,
23
+ model: this.model,
24
+ operation: this.operation,
25
+ origin: this.origin,
26
+ position,
27
+ query: this.query ?? null,
28
+ sort: this.sort,
29
+ }))
30
+ .digest("hex");
31
+ }
32
+ encode(position) {
33
+ return Buffer.from(JSON.stringify({
34
+ operation: this.operation,
35
+ model: this.model,
36
+ position,
37
+ fingerprint: this.fingerprint(position),
38
+ })).toString("base64url");
39
+ }
40
+ decode(token) {
41
+ try {
42
+ const payload = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
43
+ if (!object(payload) ||
44
+ payload.operation !== this.operation ||
45
+ payload.model !== this.model ||
46
+ (this.model === "offset" ||
47
+ (this.model === "offset-or-token" &&
48
+ typeof payload.position === "number")
49
+ ? !Number.isSafeInteger(payload.position) ||
50
+ payload.position < 0
51
+ : typeof payload.position !== "string" || !payload.position) ||
52
+ typeof payload.fingerprint !== "string" ||
53
+ !/^[a-f0-9]{64}$/.test(payload.fingerprint) ||
54
+ !timingSafeEqual(Buffer.from(payload.fingerprint), Buffer.from(this.fingerprint(payload.position))))
55
+ throw new Error("cursor");
56
+ return payload.position;
57
+ }
58
+ catch {
59
+ throw new CliError("VALIDATION_ERROR", "this cursor does not continue the current query", [
60
+ "A cursor is bound to its operation, filter, sort, region, tenant selection, and credential",
61
+ "Re-run the command without `--cursor` to start a fresh read",
62
+ ]);
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,119 @@
1
+ import { CliError, object } from "./core.js";
2
+ import { responseFailure } from "./auth.js";
3
+ import { requestRead } from "./transport/https.js";
4
+ import { malformed } from "./response.js";
5
+ import { Cursor } from "./cursor.js";
6
+ import { Raw } from "./render.js";
7
+ export const text = (value) => typeof value === "string" && value ? value : undefined;
8
+ export const quote = (value) => /^[\w@%+=:,./-]+$/.test(value)
9
+ ? value
10
+ : `'${value.replaceAll("'", `'"'"'`)}'`;
11
+ export const project = (record, fields) => Object.fromEntries(fields.map((field) => [field, record[field] ?? "unknown"]));
12
+ export class Domain {
13
+ invocation;
14
+ credential;
15
+ session;
16
+ allowUnknown;
17
+ signal;
18
+ suggestion;
19
+ constructor(invocation, credential, session, allowUnknown, signal) {
20
+ this.invocation = invocation;
21
+ this.credential = credential;
22
+ this.session = session;
23
+ this.allowUnknown = allowUnknown;
24
+ this.signal = signal;
25
+ const { command, flags, id } = invocation;
26
+ const values = ["identity list", "identity timeline"].includes(command.name)
27
+ ? ["since", "limit", "name", "email", "domain", "type", "category"]
28
+ : /^(exclusion|policy) /.test(command.name)
29
+ ? [...command.values.filter((value) => value !== "type"), "type"]
30
+ : command.values;
31
+ this.suggestion = `falcon-axi ${command.name}${id ? ` ${quote(id)}` : ""}${flags.has("--all") ? " --all" : ""}${values
32
+ .filter((key) => key !== "cursor" && flags.has(`--${key}`))
33
+ .map((key) => ` --${key} ${quote(key === "filter" && /\b(?:cid|member_cid)\s*:/i.test(flags.get(`--${key}`)) ? "<original-filter>" : flags.get(`--${key}`))}`)
34
+ .join("")}`;
35
+ }
36
+ async response(operation, subject, query = {}, body, missing = false) {
37
+ const response = await requestRead(operation, this.session.base, this.session.token, query, body, this.allowUnknown, this.signal);
38
+ if (response.status === 404 && !missing) {
39
+ const noun = this.invocation.command.name.startsWith("identity sensor ")
40
+ ? "identity sensor"
41
+ : this.invocation.command.name.split(" ")[0];
42
+ throw new CliError("NOT_FOUND", noun === "host-group"
43
+ ? "no host group matched that identifier"
44
+ : `no ${subject} matched that identifier`, [
45
+ `Run \`falcon-axi ${noun} list${noun === "policy" ? ` --type ${this.invocation.flags.get("--type")}` : ""}\`${noun === "host-group" ? "" : " to see current identifiers"}`,
46
+ ]);
47
+ }
48
+ if (response.status === 403 &&
49
+ operation === "GetVulnerabilities" &&
50
+ this.invocation.command.name === "cve show") {
51
+ try {
52
+ responseFailure(response, subject, false, false, operation);
53
+ }
54
+ catch (error) {
55
+ if (error instanceof CliError && error.code === "SCOPE_DENIED")
56
+ throw new CliError(error.code, error.message, [
57
+ ...error.help,
58
+ "This scope requires a Falcon Intelligence subscription; Spotlight's Vulnerabilities:read does not satisfy it",
59
+ "If the scope is missing from the API client picker, ask your Falcon administrator about the license",
60
+ ], error.details);
61
+ throw error;
62
+ }
63
+ }
64
+ if (response.status !== 200 && !(missing && response.status === 404))
65
+ responseFailure(response, subject, false, false, operation);
66
+ return response;
67
+ }
68
+ async read(operation, subject, query = {}, body, missing = false) {
69
+ const response = await this.response(operation, subject, query, body, missing);
70
+ if (missing && response.status === 404)
71
+ return {
72
+ rows: [],
73
+ total: undefined,
74
+ pagination: {},
75
+ meta: {},
76
+ };
77
+ const envelope = object(response.body) ? response.body : {};
78
+ const rows = operation === "GetSensorUsageWeekly" && Array.isArray(response.body)
79
+ ? response.body
80
+ : envelope.resources;
81
+ if (!Array.isArray(rows) ||
82
+ (Array.isArray(envelope.errors) && envelope.errors.length))
83
+ malformed(response, "Falcon returned a malformed resources array");
84
+ const meta = object(envelope.meta) ? envelope.meta : {};
85
+ const pagination = object(meta.pagination) ? meta.pagination : {};
86
+ const total = typeof pagination.total === "number" &&
87
+ Number.isSafeInteger(pagination.total) &&
88
+ pagination.total >= 0
89
+ ? pagination.total
90
+ : undefined;
91
+ return { rows: rows, total, pagination, meta };
92
+ }
93
+ async hydrate(operation, subject, ids, key = "id", body = false) {
94
+ const records = new Map();
95
+ const chunk = operation === "GetSensorDetails" ? 200 : 100;
96
+ for (let start = 0; start < ids.length; start += chunk) {
97
+ const batch = ids.slice(start, start + chunk);
98
+ const result = await this.read(operation, subject, body ? {} : { ids: batch }, body ? { ids: batch } : undefined, ["get_accounts", "get_iot_hosts", "GetVulnerabilities"].includes(operation));
99
+ for (const row of result.rows)
100
+ if (object(row) && text(row[key]))
101
+ records.set(String(row[key]), row);
102
+ }
103
+ return ids.flatMap((id) => (records.has(id) ? [records.get(id)] : []));
104
+ }
105
+ cursor(operation, filter, model = "offset", sort = null) {
106
+ return new Cursor(this.credential, this.session.base, operation, filter, model, sort);
107
+ }
108
+ continuation(output, context, next) {
109
+ if (next === undefined)
110
+ return;
111
+ const token = context.encode(next);
112
+ output.value.continuation_cursor = token;
113
+ output.help.push(`Run \`${this.suggestion}${this.invocation.flags.has("--full") ? " --full" : ""} --cursor ${token}\` for the next page`);
114
+ }
115
+ }
116
+ export const count = (rows, total) => new Raw(`${rows.length} of ${total ?? "unknown"} total`);
117
+ export const ids = (rows) => rows.filter((row) => typeof row === "string");
118
+ export const more = (length, offset, total, limit) => length > 0 &&
119
+ (total === undefined ? length >= limit : offset + length < total);
@@ -0,0 +1,45 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { CliError } from "./core.js";
4
+ export function shellArgument(value) {
5
+ return process.platform === "win32"
6
+ ? `"${value}"`
7
+ : `'${value.replaceAll("'", `'"'"'`)}'`;
8
+ }
9
+ export function executableCommand(hook = false) {
10
+ const entry = realpathSync(resolve(process.argv[1]));
11
+ const target = hook ? join(dirname(entry), "hook.js") : entry;
12
+ const name = hook ? "falcon-axi-hook" : "falcon-axi";
13
+ if (!statSync(target).isFile())
14
+ throw new CliError("CONFIG_INVALID", `cannot locate ${name}; reinstall falcon-axi`);
15
+ return [process.execPath, target];
16
+ }
17
+ export function durableInstallation() {
18
+ const entry = resolve(process.argv[1]);
19
+ const hook = join(dirname(realpathSync(entry)), "hook.js");
20
+ const temporaryRoots = process.platform === "win32" ? [] : ["/tmp", realpathSync("/tmp")];
21
+ for (const path of [
22
+ entry,
23
+ realpathSync(entry),
24
+ hook,
25
+ realpathSync(hook),
26
+ process.execPath,
27
+ ]) {
28
+ const parts = path.split(sep);
29
+ if (temporaryRoots.some((root) => path === root || path.startsWith(root + sep)) ||
30
+ parts.some((part) => [".treehouse", "worktrees", ".no-mistakes"].includes(part)))
31
+ throw new CliError("CONFIG_INVALID", "session hooks require a durable installation outside temporary directories and git worktrees", [
32
+ "Install with `npm install -g @knowttl/falcon-axi@next`, then run `falcon-axi setup hooks`",
33
+ ]);
34
+ for (let parent = dirname(path); parent !== dirname(parent); parent = dirname(parent)) {
35
+ try {
36
+ if (statSync(join(parent, ".git")).isFile())
37
+ throw new CliError("CONFIG_INVALID", "session hooks require a durable installation outside temporary directories and git worktrees");
38
+ }
39
+ catch (error) {
40
+ if (error.code !== "ENOENT")
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+ }