@knowttl/falcon-axi 1.0.0-rc.1 → 1.0.0-rc.3
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/.typescript-build/src/auth.js +18 -4
- package/.typescript-build/src/cli.js +2 -0
- package/.typescript-build/src/core.js +24 -7
- package/.typescript-build/src/cursor.js +7 -0
- package/.typescript-build/src/domain.js +7 -5
- package/.typescript-build/src/executable.js +2 -19
- package/.typescript-build/src/help.json +1 -1
- package/.typescript-build/src/identity.js +3 -2
- package/.typescript-build/src/inspection.js +5 -3
- package/.typescript-build/src/inventory.js +2 -1
- package/.typescript-build/src/investigation.js +10 -14
- package/.typescript-build/src/malquery.js +10 -1
- package/.typescript-build/src/metadata.js +29 -0
- package/.typescript-build/src/parser.js +20 -16
- package/.typescript-build/src/rtr.js +1 -1
- package/.typescript-build/src/search.js +1 -1
- package/.typescript-build/src/version.js +1 -1
- package/README.md +1 -1
- package/artium-invocations.json +1 -1
- package/package.json +1 -1
- package/skills/falcon-axi/references/agent-guide.md +1 -1
- package/skills/falcon-axi/references/host-show.md +1 -1
|
@@ -9,6 +9,18 @@ export function responseFailure(response, subject, token = false, member = false
|
|
|
9
9
|
const envelope = object(response.body) ? response.body : {};
|
|
10
10
|
const meta = object(envelope.meta) ? envelope.meta : {};
|
|
11
11
|
const details = typeof meta.trace_id === "string" ? { trace_id: meta.trace_id } : {};
|
|
12
|
+
const requestHelp = operationId === "RTRAuditSessions"
|
|
13
|
+
? [
|
|
14
|
+
`Check that the API client has ${scopes.join(" and ")} in the Falcon console under Support and resources > API clients and keys; run \`falcon-axi scopes\` for the matrix`,
|
|
15
|
+
"This rejection does not confirm a missing scope; if the scope is granted, report the request command and trace_id to CrowdStrike support",
|
|
16
|
+
]
|
|
17
|
+
: operationId === "GetSearchStatusV1"
|
|
18
|
+
? [
|
|
19
|
+
"A stopped or expired job may no longer be queryable; this rejection does not confirm that the job was stopped",
|
|
20
|
+
"Check the job identifier and pass the same `--repository` the job was started against",
|
|
21
|
+
"Run `falcon-axi search start --query '<cql>'` to start a new job if needed",
|
|
22
|
+
]
|
|
23
|
+
: undefined;
|
|
12
24
|
if (response.status === 400) {
|
|
13
25
|
if (operationId === "api_preempt_proxy_post_graphql")
|
|
14
26
|
throw new CliError("UPSTREAM_ERROR", `Falcon rejected falcon-axi's fixed Identity Protection query for ${subject}`, [
|
|
@@ -86,15 +98,16 @@ export function responseFailure(response, subject, token = false, member = false
|
|
|
86
98
|
? ["host list", "hostname:'WIN-*'"]
|
|
87
99
|
: ["detection list", "severity_name:'High'+status:'new'"]);
|
|
88
100
|
const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
if (operationId !== "GetSearchStatusV1" &&
|
|
102
|
+
errors.some((error) => object(error) &&
|
|
103
|
+
typeof error.message === "string" &&
|
|
104
|
+
/\b(fql|filter|query)\b/i.test(error.message)))
|
|
92
105
|
throw new CliError("FQL_INVALID", `the filter for ${subject} was rejected`, [
|
|
93
106
|
"FQL uses + for AND, `,` for OR, and values must be quoted",
|
|
94
107
|
`Example: \`--filter "${example}"\``,
|
|
95
108
|
`Run \`falcon-axi ${command} --help\` for the filterable fields`,
|
|
96
109
|
]);
|
|
97
|
-
throw new CliError("UPSTREAM_ERROR", `Falcon rejected the request for ${subject}`, ["Check the flags for this command with `--help`"], details);
|
|
110
|
+
throw new CliError("UPSTREAM_ERROR", `Falcon rejected the request for ${subject}`, requestHelp ?? ["Check the flags for this command with `--help`"], details);
|
|
98
111
|
}
|
|
99
112
|
if (response.status === 401)
|
|
100
113
|
throw new CliError("AUTH_FAILED", "the Falcon credential was rejected", [
|
|
@@ -162,6 +175,7 @@ export function responseFailure(response, subject, token = false, member = false
|
|
|
162
175
|
throw new CliError("UPSTREAM_ERROR", `Falcon could not answer the request for ${subject}`, [
|
|
163
176
|
"Retry shortly",
|
|
164
177
|
"Quote the trace_id when opening a CrowdStrike support case",
|
|
178
|
+
...(requestHelp ?? []),
|
|
165
179
|
], details);
|
|
166
180
|
}
|
|
167
181
|
export function observeRate(session, response) {
|
|
@@ -62,6 +62,8 @@ export async function main(argv, bin) {
|
|
|
62
62
|
.map((name) => ` --${name}`)
|
|
63
63
|
.join("");
|
|
64
64
|
const result = help.map((item) => item.replace(/`(falcon-axi .*)`/s, (_, command) => `\`${command}${suffix}\``));
|
|
65
|
+
if (help.some((item) => item.includes("<original-filter>")))
|
|
66
|
+
result.push("Replace <original-filter> with your original --filter value when continuing; CID filters are not echoed");
|
|
65
67
|
if (memberCid && help.some((item) => /`falcon-axi /.test(item)))
|
|
66
68
|
result.push("Supply the same tenant selection used for this invocation when following these suggestions");
|
|
67
69
|
return result;
|
|
@@ -17,15 +17,32 @@ export class CliError extends Error {
|
|
|
17
17
|
export function object(value) {
|
|
18
18
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
19
|
}
|
|
20
|
-
// Preserve numeric lexemes outside Number's exact integer domain
|
|
20
|
+
// Preserve numeric lexemes outside Number's exact integer domain, judged from the
|
|
21
|
+
// original text because JSON.parse has already rounded the value it hands us.
|
|
21
22
|
export function parseResponse(text) {
|
|
22
23
|
return JSON.parse(text, (_key, value, context) => {
|
|
23
|
-
if (typeof value !== "number" ||
|
|
24
|
-
Math.abs(value) <= Number.MAX_SAFE_INTEGER ||
|
|
25
|
-
!context?.source)
|
|
24
|
+
if (typeof value !== "number" || !context?.source)
|
|
26
25
|
return value;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const { source } = context;
|
|
27
|
+
if (/^-?\d+$/.test(source)) {
|
|
28
|
+
const integer = BigInt(source);
|
|
29
|
+
return (integer < 0n ? -integer : integer) >
|
|
30
|
+
BigInt(Number.MAX_SAFE_INTEGER)
|
|
31
|
+
? integer
|
|
32
|
+
: value;
|
|
33
|
+
}
|
|
34
|
+
const [mantissa, exponent = "0"] = source.replace(/^-/, "").split(/[eE]/);
|
|
35
|
+
const [whole, fraction = ""] = mantissa.split(".");
|
|
36
|
+
const coefficient = whole + fraction;
|
|
37
|
+
const significant = coefficient.replace(/^0+/, "");
|
|
38
|
+
if (!significant)
|
|
39
|
+
return value;
|
|
40
|
+
const position = BigInt(whole.length - (coefficient.length - significant.length)) +
|
|
41
|
+
BigInt(exponent);
|
|
42
|
+
const digits = significant.replace(/0+$/, "");
|
|
43
|
+
return position > 16n ||
|
|
44
|
+
(position === 16n && digits.padEnd(16, "0") > "9007199254740991")
|
|
45
|
+
? source
|
|
46
|
+
: value;
|
|
30
47
|
});
|
|
31
48
|
}
|
|
@@ -63,3 +63,10 @@ export class Cursor {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
// Serializes a cursor query context exactly as Python's json.dumps did: ASCII-only
|
|
67
|
+
// output with either compact or default separators, so existing tokens stay valid.
|
|
68
|
+
export function pythonJson(value, compact = false) {
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return `[${value.map((item) => pythonJson(item, compact)).join(compact ? "," : ", ")}]`;
|
|
71
|
+
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
72
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
|
+
import { recoveryHelp } from "./metadata.js";
|
|
2
3
|
import { responseFailure } from "./auth.js";
|
|
3
4
|
import { requestRead } from "./transport/https.js";
|
|
4
5
|
import { malformed } from "./response.js";
|
|
@@ -36,13 +37,14 @@ export class Domain {
|
|
|
36
37
|
async response(operation, subject, query = {}, body, missing = false) {
|
|
37
38
|
const response = await requestRead(operation, this.session.base, this.session.token, query, body, this.allowUnknown, this.signal);
|
|
38
39
|
if (response.status === 404 && !missing) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw new CliError("NOT_FOUND", noun === "host-group"
|
|
40
|
+
const { command, flags } = this.invocation;
|
|
41
|
+
const hostGroup = command.name.startsWith("host-group ");
|
|
42
|
+
throw new CliError("NOT_FOUND", hostGroup
|
|
43
43
|
? "no host group matched that identifier"
|
|
44
44
|
: `no ${subject} matched that identifier`, [
|
|
45
|
-
|
|
45
|
+
hostGroup
|
|
46
|
+
? "Run `falcon-axi host-group list`"
|
|
47
|
+
: recoveryHelp(command.name, flags),
|
|
46
48
|
]);
|
|
47
49
|
}
|
|
48
50
|
if (response.status === 403 &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { realpathSync, statSync
|
|
2
|
-
import { dirname,
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { CliError } from "./core.js";
|
|
4
4
|
export function shellArgument(value) {
|
|
5
5
|
return process.platform === "win32"
|
|
@@ -12,23 +12,6 @@ export function executableCommand(hook = false) {
|
|
|
12
12
|
const name = hook ? "falcon-axi-hook" : "falcon-axi";
|
|
13
13
|
if (!statSync(target).isFile())
|
|
14
14
|
throw new CliError("CONFIG_INVALID", `cannot locate ${name}; reinstall falcon-axi`);
|
|
15
|
-
// npm's Windows .cmd wrapper is a shell script, so use Node plus its entry point.
|
|
16
|
-
if (hook && process.platform !== "win32")
|
|
17
|
-
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
18
|
-
const candidate = join(directory || ".", name);
|
|
19
|
-
try {
|
|
20
|
-
accessSync(candidate, constants.X_OK);
|
|
21
|
-
if (!statSync(candidate).isFile())
|
|
22
|
-
continue;
|
|
23
|
-
if (isAbsolute(directory) && realpathSync(candidate) === target)
|
|
24
|
-
return [name];
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
if (!["ENOENT", "ENOTDIR", "EACCES", "ELOOP"].includes(error.code ?? ""))
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
15
|
return [process.execPath, target];
|
|
33
16
|
}
|
|
34
17
|
export function durableInstallation() {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"detection show": "falcon-axi detection show <composite id> [--full]\n\n--full print long fields such as the command line without truncation\n\nExample: falcon-axi detection show \"ldt:aid:1234\"\n\nThis command is read-only and requires only Alerts:read.",
|
|
11
11
|
"detection process": "falcon-axi detection process <composite id> [--full]\n\n--full print command lines without the default 800-character truncation\n\nShow the triggering process, parent, and grandparent recorded on the alert.\nRows contain relation, filename, filepath, cmdline, sha256, and process_id.\ntriggering_process_graph_id is the alert's graph pivot; this view does not fetch a graph.\nAbsent ancestors and alerts without a process are stated explicitly.\nExamples:\n falcon-axi detection process 'ldt:synthetic-agent:1001'\n falcon-axi detection process 'ldt:synthetic-agent:1001' --full\n\nThis command is read-only and requires only Alerts:read.",
|
|
12
12
|
"host list": "falcon-axi host list [--hostname <name>] [--platform <name>] [--status <name>] [--since <window>] [--filter <FQL>] [--limit N] [--cursor <token>] [--all] [--max-rows N] [--fields <names>]\n\n--hostname hostname to match; Hosts filters accept wildcards, so WIN-* works\n--platform one of windows, mac, linux\n--status one of normal, containment_pending, contained, lift_containment_pending\n--since last_seen within a relative window such as 24h or 7d\n--filter raw FQL; + is AND, `,` is OR, values are single-quoted, relative dates are lowercase\n--limit rows in this call (default 20, ceiling 5000)\n--cursor opaque continuation token from a previous call\n--all walk pages; page size is independent of --limit\n--max-rows row budget (default 1000; requires --all)\n--fields extra columns, comma-separated: os_version, agent_version, status, first_seen, machine_domain\n\nFilterable fields include hostname, platform_name, os_version, agent_version, status,\nlast_seen, first_seen, machine_domain, and local_ip.\nExamples:\n falcon-axi host list --platform windows --since 24h\n falcon-axi host list --filter \"hostname:'WIN-*'+platform_name:'Windows'\"\n\nThis command is read-only and requires only Hosts:read. It reports containment status as\ndata and cannot change it.",
|
|
13
|
-
"host show": "falcon-axi host show <device id>\n\nThe device id is the agent id Falcon calls the AID, as printed by `falcon-axi host list`.\n\nExample: falcon-axi host show
|
|
13
|
+
"host show": "falcon-axi host show <device id>\n\nThe device id is the agent id Falcon calls the AID, as printed by `falcon-axi host list`.\n\nExample: falcon-axi host show 0123456789abcdef0123456789abcdef\n\nThis command is read-only and requires only Hosts:read.",
|
|
14
14
|
"host logins": "falcon-axi host logins <device id> [--since <window>] [--limit N]\nfalcon-axi host logins --hostname <pattern> [--since <window>] [--limit N]\n\nThe device id is the agent id Falcon calls the AID, as printed by `falcon-axi host list`.\n--hostname match a hostname (wildcards allowed), instead of a device id; requires exactly one match\n--since window to read, such as 24h or 7d (default 7d, Falcon's own default)\n--limit logins per host in this call (default 20, ceiling 100)\n\nReports recent interactive logins from the Host Timeline as device_id, user_name, and\nlogin_time. It is a window, not a full audit trail: a host that returns its limit may have more.\nExample: falcon-axi host logins abc123 --since 24h\nExample: falcon-axi host logins --hostname 'synthetic-host-*' --since 24h --limit 2\nHostname form: one lookup capped at 2 IDs, then one history read; 60s total deadline.\nAmbiguous or incomplete lookups fail without reading history; upstream partial errors are disclosed.\n\nThis command is read-only and requires only Hosts:read.",
|
|
15
15
|
"host network": "falcon-axi host network <device id>\nfalcon-axi host network --hostname <pattern>\n\nRetrieves the IP and MAC address history Falcon retains for one sensor host.\nThe device id is the AID printed by `falcon-axi host list`.\n--hostname match a hostname (wildcards allowed), instead of a device id; requires exactly one match\nExample: falcon-axi host network abc123\nExample: falcon-axi host network --hostname 'synthetic-host-*'\nHostname form: one lookup capped at 2 IDs, then one history read; 60s total deadline.\nAmbiguous or incomplete lookups fail without reading history; upstream partial errors are disclosed.\nRead-only; requires Hosts:read. This endpoint has no pagination parameters.",
|
|
16
16
|
"ioc list": "falcon-axi ioc list [--filter <FQL>] [--limit N] [--cursor <token>]\n\n--filter FQL on type, value, action, source, severity_number, expired, created_on, modified_on\n--limit rows per call (default 20, ceiling 500)\n--cursor opaque continuation token\n\nExamples:\n falcon-axi ioc list --filter \"type:'domain'+expired:false\"\n falcon-axi ioc list --limit 100\n\nRead-only; requires IOC Management:read.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
|
+
import { pythonJson } from "./cursor.js";
|
|
2
3
|
import { Domain, text } from "./domain.js";
|
|
3
4
|
import { Raw } from "./render.js";
|
|
4
5
|
import { seconds } from "./search.js";
|
|
@@ -208,12 +209,12 @@ export async function identity(domain) {
|
|
|
208
209
|
const type = flags.get("--type")?.toLowerCase();
|
|
209
210
|
const fingerprint = timeline
|
|
210
211
|
? [entityId(id).toLowerCase(), since.toLowerCase(), chosen.join(",")].join("|")
|
|
211
|
-
:
|
|
212
|
+
: pythonJson([
|
|
212
213
|
type ?? null,
|
|
213
214
|
flags.get("--name") ?? null,
|
|
214
215
|
flags.get("--email") ?? null,
|
|
215
216
|
flags.get("--domain") ?? null,
|
|
216
|
-
]);
|
|
217
|
+
], true);
|
|
217
218
|
const context = domain.cursor("api_preempt_proxy_post_graphql", fingerprint, "token");
|
|
218
219
|
const variables = { first: limit };
|
|
219
220
|
let start = new Date(Date.now() - seconds(since) * 1000)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
|
+
import { pythonJson } from "./cursor.js";
|
|
3
|
+
import { recoveryHelp } from "./metadata.js";
|
|
2
4
|
import { Domain, count, more, project, text } from "./domain.js";
|
|
3
5
|
import { Raw, redact, truncate } from "./render.js";
|
|
4
6
|
import { operation } from "./transport/operations.js";
|
|
@@ -330,7 +332,7 @@ export async function inspection(domain, filter) {
|
|
|
330
332
|
]
|
|
331
333
|
: ["classification", "data-policy"].includes(noun)
|
|
332
334
|
? [`Run \`falcon-axi ${noun} list --help\` to find identifiers`]
|
|
333
|
-
: []);
|
|
335
|
+
: [recoveryHelp(command.name, flags)]);
|
|
334
336
|
const record = records[0];
|
|
335
337
|
if (recon || collection)
|
|
336
338
|
return detail(domain, recon ? "record" : collectionNoun.replaceAll(" ", "_"), record, true, recon ? "for untruncated content" : "for untruncated detail");
|
|
@@ -378,9 +380,9 @@ export async function inspection(domain, filter) {
|
|
|
378
380
|
filter = sandboxFilter(flags);
|
|
379
381
|
const sort = noun === "indicator" ? "_marker.asc" : flags.get("--sort");
|
|
380
382
|
const contextFilter = data
|
|
381
|
-
?
|
|
383
|
+
? pythonJson([filter ?? null, flags.get("--platform") ?? null], true)
|
|
382
384
|
: noun === "policy"
|
|
383
|
-
?
|
|
385
|
+
? pythonJson([filter ?? null, members ? id : null], true)
|
|
384
386
|
: filter;
|
|
385
387
|
const context = domain.cursor(op, contextFilter, noun === "indicator" ? "marker" : (collection?.model ?? "offset"), sort ?? null);
|
|
386
388
|
const position = supplied
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
|
+
import { pythonJson } from "./cursor.js";
|
|
2
3
|
import { Domain, count, ids, more, project, quote, text } from "./domain.js";
|
|
3
4
|
import { Raw, redact, truncate } from "./render.js";
|
|
4
5
|
export const applicationFields = [
|
|
@@ -167,7 +168,7 @@ export async function inventory(domain, filter) {
|
|
|
167
168
|
if (noun === "host-group") {
|
|
168
169
|
const members = name.endsWith("members");
|
|
169
170
|
const sort = members ? null : "name.asc";
|
|
170
|
-
const context = domain.cursor(operation,
|
|
171
|
+
const context = domain.cursor(operation, pythonJson([id ? [id] : [], filter ?? null]), "offset", sort);
|
|
171
172
|
const position = flags.has("--cursor")
|
|
172
173
|
? Number(context.decode(flags.get("--cursor")))
|
|
173
174
|
: 0;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
|
-
import { Cursor } from "./cursor.js";
|
|
2
|
+
import { Cursor, pythonJson } from "./cursor.js";
|
|
3
3
|
import { Raw, redact, truncate } from "./render.js";
|
|
4
4
|
import { requestRead } from "./transport/https.js";
|
|
5
5
|
import { responseFailure } from "./auth.js";
|
|
6
6
|
import { malformed } from "./response.js";
|
|
7
7
|
import { text, quote, project } from "./domain.js";
|
|
8
|
+
import { recoveryHelp } from "./metadata.js";
|
|
8
9
|
const invalid = (message, details = {}) => {
|
|
9
10
|
throw new CliError("UPSTREAM_ERROR", message, [], details);
|
|
10
11
|
};
|
|
@@ -94,21 +95,18 @@ export async function investigate(invocation, credential, session, filter, allow
|
|
|
94
95
|
]);
|
|
95
96
|
if (name.startsWith("ioc "))
|
|
96
97
|
throw new CliError("NOT_FOUND", "no IOC record matched that identifier", ["Run `falcon-axi ioc list`"]);
|
|
97
|
-
const command = operation === "QueryGetNetworkAddressHistoryV1"
|
|
98
|
-
? "host list"
|
|
99
|
-
: name.startsWith("file-change ")
|
|
100
|
-
? "file-change list"
|
|
101
|
-
: name.startsWith("rtr session ")
|
|
102
|
-
? "rtr session list"
|
|
103
|
-
: name.startsWith("rtr audit ")
|
|
104
|
-
? "rtr audit list"
|
|
105
|
-
: "detection list";
|
|
106
98
|
throw new CliError("NOT_FOUND", `no ${subject} matched that identifier`, [
|
|
107
|
-
|
|
99
|
+
recoveryHelp(name === "home" ? "detection list" : name, flags),
|
|
108
100
|
]);
|
|
109
101
|
}
|
|
110
102
|
if (response.status !== 200)
|
|
111
103
|
responseFailure(response, subject, false, false, operation);
|
|
104
|
+
if ((name === "host logins" || name === "host network") &&
|
|
105
|
+
Array.isArray(envelope.errors) &&
|
|
106
|
+
envelope.errors.length)
|
|
107
|
+
invalid(operation === "QueryDevicesByFilter"
|
|
108
|
+
? "Falcon returned a partial hostname lookup; history was not read"
|
|
109
|
+
: `Falcon returned partial host ${name === "host logins" ? "login" : "network"} history`, { partial: true });
|
|
112
110
|
const rows = Array.isArray(response.body)
|
|
113
111
|
? response.body
|
|
114
112
|
: envelope.resources;
|
|
@@ -272,8 +270,6 @@ export async function investigate(invocation, credential, session, filter, allow
|
|
|
272
270
|
if (more && offset < wall) {
|
|
273
271
|
value.continuation_cursor = context.encode(offset);
|
|
274
272
|
help.push(`Run \`${suggestion} --cursor ${String(value.continuation_cursor)}\` for the next page`);
|
|
275
|
-
if (filter && /\b(?:cid|member_cid)\s*:/i.test(filter))
|
|
276
|
-
help.push("Replace <original-filter> with your original --filter value when continuing; CID filters are not echoed");
|
|
277
273
|
}
|
|
278
274
|
else if (more)
|
|
279
275
|
help.push(wallHelp[0], hosts
|
|
@@ -662,7 +658,7 @@ export async function investigate(invocation, credential, session, filter, allow
|
|
|
662
658
|
const type = id.slice(0, id.indexOf(":"));
|
|
663
659
|
const indicator = id.slice(id.indexOf(":") + 1);
|
|
664
660
|
const host = flags.get("--host");
|
|
665
|
-
const context = cursor(operation,
|
|
661
|
+
const context = cursor(operation, pythonJson([type, indicator, host ?? null]));
|
|
666
662
|
const offset = flags.has("--cursor")
|
|
667
663
|
? context.decode(flags.get("--cursor"))
|
|
668
664
|
: 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError, object } from "./core.js";
|
|
2
2
|
import { Domain, project } from "./domain.js";
|
|
3
|
+
import { recoveryHelp } from "./metadata.js";
|
|
3
4
|
import { Raw } from "./render.js";
|
|
4
5
|
import { searchBody } from "./transport/search-authority.js";
|
|
5
6
|
import { requestException, requestRead } from "./transport/https.js";
|
|
@@ -37,7 +38,15 @@ export async function malquery(domain) {
|
|
|
37
38
|
"Check `falcon-axi malquery search --help` for the supported literal pattern grammar",
|
|
38
39
|
]);
|
|
39
40
|
if (response.status === 404)
|
|
40
|
-
throw new CliError("NOT_FOUND", `no ${metadata ? "MalQuery metadata" : status || submit ? "MalQuery search" : "MalQuery quota"} matched that identifier`,
|
|
41
|
+
throw new CliError("NOT_FOUND", `no ${metadata ? "MalQuery metadata" : status || submit ? "MalQuery search" : "MalQuery quota"} matched that identifier`, metadata || status || submit
|
|
42
|
+
? [
|
|
43
|
+
recoveryHelp(metadata
|
|
44
|
+
? "malquery show"
|
|
45
|
+
: status
|
|
46
|
+
? "malquery status"
|
|
47
|
+
: "malquery search", new Map()),
|
|
48
|
+
]
|
|
49
|
+
: []);
|
|
41
50
|
if (response.status !== 200)
|
|
42
51
|
responseFailure(response, metadata
|
|
43
52
|
? "MalQuery metadata"
|
|
@@ -550,6 +550,35 @@ export const boolFlags = [
|
|
|
550
550
|
"--no-member-cid",
|
|
551
551
|
];
|
|
552
552
|
export const commandNames = commands.map((command) => command.name);
|
|
553
|
+
// Recovery guidance for an identifier or collection that Falcon did not find.
|
|
554
|
+
// Commands whose list needs a selector the failed command may not carry point
|
|
555
|
+
// to that list's help instead of an invalid invocation.
|
|
556
|
+
export const recoveryHelp = (name, flags, kind) => {
|
|
557
|
+
if (name.startsWith("process "))
|
|
558
|
+
return "Run `falcon-axi ioc processes <type:value> --host <device_id>` to find process identifiers";
|
|
559
|
+
if (name === "malquery search")
|
|
560
|
+
return "Run `falcon-axi malquery search --help` for the supported literal pattern grammar";
|
|
561
|
+
if (name === "malquery status")
|
|
562
|
+
return "Run `falcon-axi malquery search --help` to start a search and obtain a request identifier";
|
|
563
|
+
if (name === "malquery show")
|
|
564
|
+
return "Run `falcon-axi malquery show --help` for the SHA-256 argument it expects";
|
|
565
|
+
const words = name.split(" ");
|
|
566
|
+
const collection = name === "rtr run"
|
|
567
|
+
? "host list"
|
|
568
|
+
: words.at(-1) === "list"
|
|
569
|
+
? name
|
|
570
|
+
: words.length === 3
|
|
571
|
+
? `${words[0]} ${words[1]} list`
|
|
572
|
+
: `${words[0]} list`;
|
|
573
|
+
const selector = { exclusion: "type", policy: "type", "data-policy": "platform" }[words[0]];
|
|
574
|
+
const value = selector ? flags.get(`--${selector}`) : undefined;
|
|
575
|
+
const command = value !== undefined && /^[A-Za-z0-9_-]+$/.test(value)
|
|
576
|
+
? `${collection} --${selector} ${value}`
|
|
577
|
+
: selector || ["sandbox", "serverless", "vuln"].includes(words[0])
|
|
578
|
+
? `${collection} --help`
|
|
579
|
+
: collection;
|
|
580
|
+
return `Run \`falcon-axi ${command}\` to ${command.endsWith(" --help") ? "see the selectors it requires" : kind === "policy" ? "find policy ids" : `see current ${kind ? `${kind} ` : ""}identifiers`}`;
|
|
581
|
+
};
|
|
553
582
|
for (const flag of [
|
|
554
583
|
...valueFlags,
|
|
555
584
|
...boolFlags,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./core.js";
|
|
2
|
-
import { commands, valueFlags, boolFlags } from "./metadata.js";
|
|
2
|
+
import { commands, valueFlags, boolFlags, recoveryHelp } from "./metadata.js";
|
|
3
3
|
import { applicationFields } from "./inventory.js";
|
|
4
4
|
import { shieldFilters } from "./exposure.js";
|
|
5
5
|
import { validateInspection } from "./inspection.js";
|
|
@@ -98,21 +98,21 @@ export function parse(argv) {
|
|
|
98
98
|
if (command.positional &&
|
|
99
99
|
(hostname ? positionals.length !== 0 : positionals.length !== 1))
|
|
100
100
|
throw new CliError("VALIDATION_ERROR", `${command.name} requires exactly one ${command.positional}`, [
|
|
101
|
-
command.name === "
|
|
102
|
-
? "
|
|
103
|
-
: command.name === "
|
|
104
|
-
? "
|
|
105
|
-
: command.name
|
|
106
|
-
? "
|
|
107
|
-
: command.name
|
|
108
|
-
? "
|
|
109
|
-
:
|
|
110
|
-
? "Run `falcon-axi
|
|
111
|
-
: command.name === "identity
|
|
112
|
-
? "Run `falcon-axi
|
|
113
|
-
: command.name
|
|
114
|
-
? "
|
|
115
|
-
:
|
|
101
|
+
command.name === "cve show"
|
|
102
|
+
? "A CVE identifier matches CVE-<year>-<number>, for example CVE-2021-44228"
|
|
103
|
+
: command.name === "vuln show"
|
|
104
|
+
? "Run `falcon-axi vuln list --status open` to find Spotlight record identifiers"
|
|
105
|
+
: command.name.startsWith("search ")
|
|
106
|
+
? "A search identifier comes from `falcon-axi search start --query '<cql>'`"
|
|
107
|
+
: ["identity show", "identity timeline"].includes(command.name)
|
|
108
|
+
? "Run `falcon-axi identity list` to see current identity ids"
|
|
109
|
+
: command.name === "identity activity"
|
|
110
|
+
? "Run `falcon-axi detection show <id>` for an identity detection's account_id"
|
|
111
|
+
: command.name === "identity sensor show"
|
|
112
|
+
? "Run `falcon-axi identity sensor list` to see current sensor identifiers"
|
|
113
|
+
: recoveryHelp(command.name, flags, command.name.startsWith("host ")
|
|
114
|
+
? "device"
|
|
115
|
+
: command.name.split(" ")[0]),
|
|
116
116
|
]);
|
|
117
117
|
if (!command.positional && positionals.length)
|
|
118
118
|
throw new CliError("VALIDATION_ERROR", `\`${command.name}\` accepts no positional arguments`, valid);
|
|
@@ -150,6 +150,10 @@ export function parse(argv) {
|
|
|
150
150
|
throw new CliError("VALIDATION_ERROR", "--max-rows must be a positive integer");
|
|
151
151
|
}
|
|
152
152
|
const id = positionals[0];
|
|
153
|
+
if (command.name === "host show" &&
|
|
154
|
+
id &&
|
|
155
|
+
(!/^[a-fA-F0-9]{32}$/.test(id) || /^0{32}$/.test(id)))
|
|
156
|
+
throw new CliError("VALIDATION_ERROR", "host show requires a nonzero 32-character hexadecimal device id (AID)", ["Run `falcon-axi host list` to see current device identifiers"]);
|
|
153
157
|
if (!flags.has("--help"))
|
|
154
158
|
validateInspection(command.name, flags, limit);
|
|
155
159
|
if (command.name === "cve show" && id && !/^CVE-[0-9]{4}-[0-9]{4,}$/.test(id))
|
|
@@ -95,7 +95,7 @@ export async function rtrRun(invocation, credential, base, cancelled) {
|
|
|
95
95
|
const policy = credential.rtr ?? rtrPolicy(undefined);
|
|
96
96
|
const commandBase = parseRtr(command, policy.commands);
|
|
97
97
|
if (executing && (credential.channel !== "file" || !policy.enabled))
|
|
98
|
-
throw new CliError("READ_ONLY_VIOLATION", "RTR execution requires explicit profile opt-in");
|
|
98
|
+
throw new CliError("READ_ONLY_VIOLATION", "RTR execution requires explicit profile opt-in", ['Set "rtr": {"enabled": true} on the selected file profile']);
|
|
99
99
|
if (executing && (process.platform !== "linux" || process.arch !== "x64"))
|
|
100
100
|
throw new CliError("CONFIG_INSECURE", "RTR execution requires a supported POSIX controller");
|
|
101
101
|
process.stdout.write(render({
|
|
@@ -116,7 +116,7 @@ export async function search(domain) {
|
|
|
116
116
|
return {
|
|
117
117
|
value: { stopped: id, repository },
|
|
118
118
|
help: [
|
|
119
|
-
|
|
119
|
+
"A stopped job may no longer be queryable with `falcon-axi search status`",
|
|
120
120
|
"Run `falcon-axi search start --query '<cql>'` to run a narrower search",
|
|
121
121
|
],
|
|
122
122
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "1.0.0-rc.
|
|
1
|
+
export const VERSION = "1.0.0-rc.3"; // x-release-please-version
|
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Run `falcon-axi config path` to inspect config and journal paths.
|
|
|
131
131
|
| `FALCON_AXI_WRITE_LOG` | Mandatory RTR execution journal | State directory `writes.log` |
|
|
132
132
|
| `XDG_CONFIG_HOME`, `XDG_STATE_HOME` | Absolute POSIX config / state roots | `~/.config`, `~/.local/state` |
|
|
133
133
|
| `APPDATA`, `LOCALAPPDATA` | Windows config / state roots | `~/AppData/Roaming`, `~/AppData/Local` |
|
|
134
|
-
| `HOME
|
|
134
|
+
| `HOME` | Home directory for session integrations | OS home directory |
|
|
135
135
|
| `CLAUDECODE`, `CODEX_THREAD_ID`, `CODEX_SESSION_ID`, `PI_CODING_AGENT`, `OPENCODE` | Detect the host for skill-install hints | Unset |
|
|
136
136
|
| `CLAUDE_CONFIG_DIR`, `PI_CODING_AGENT_DIR` | Host skill-discovery roots | `~/.claude`, `~/.pi/agent` |
|
|
137
137
|
| `FALCON_CLIENT_ID`, `FALCON_CLIENT_SECRET`, `FALCON_MEMBER_CID`, `FALCON_BASE_URL`, `FALCON_PROXY_URL` | Deprecated aliases; replacements win | Unset |
|
package/artium-invocations.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"parser": {
|
|
10
10
|
"path": ".typescript-build/src/parser.js",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "1a348bb4a13e0034d5919b218e02ddf7ed0036b51d78d06b488bda4fcb78f8eb"
|
|
12
12
|
},
|
|
13
13
|
"skill": {
|
|
14
14
|
"path": "skills/falcon-axi/SKILL.md",
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ Do not allow a whole noun such as `host` or `search`: other subcommands can crea
|
|
|
18
18
|
| Alert detail: `detection show <id>` | `falcon-axi detection show 'ldt:synthetic-agent-01:1001' --full` | `--full` removes long-field previews |
|
|
19
19
|
| Alert process lineage: `detection process <id>` | `falcon-axi detection process 'ldt:synthetic-agent:1001'` | Trigger, parent, grandparent, hashes and graph pivot in one call; `--full` removes 800-character command-line previews |
|
|
20
20
|
| Find host: `host list` | `falcon-axi host list --hostname 'synthetic-host-*' --limit 2 --fields status,os_version,agent_version` | `--platform windows`, `--since 24h`, `--filter <FQL>`; returns device IDs (AIDs) |
|
|
21
|
-
| Host detail: `host show <device-id>` | `falcon-axi host show
|
|
21
|
+
| Host detail: `host show <device-id>` | `falcon-axi host show 00000000000000000000000000000001` | Full host record; no `--full` flag |
|
|
22
22
|
| Host address history: `host network <device-id>` | `falcon-axi host network synthetic-device-01` | Retained IP/MAC history; no pagination or time flag |
|
|
23
23
|
| Interactive logins: `host logins <device-id>` | `falcon-axi host logins synthetic-device-01 --since 24h --limit 2` | Default window 7d; default 20, maximum 100 logins; a filled window may omit older logins |
|
|
24
24
|
| Graph lineage: `process tree <pid-id>` | `falcon-axi process tree pid:synthetic-device:1` | Up to five ancestors and one child level, 100 child edges per page; `--cursor <token>` continues children; `--full` for complete command lines |
|
|
@@ -7,7 +7,7 @@ npx -y @knowttl/falcon-axi@next host show <device id>
|
|
|
7
7
|
|
|
8
8
|
The device id is the agent id Falcon calls the AID, as printed by `npx -y @knowttl/falcon-axi@next host list`.
|
|
9
9
|
|
|
10
|
-
Example: npx -y @knowttl/falcon-axi@next host show
|
|
10
|
+
Example: npx -y @knowttl/falcon-axi@next host show 0123456789abcdef0123456789abcdef
|
|
11
11
|
|
|
12
12
|
This command is read-only and requires only Hosts:read.
|
|
13
13
|
```
|