@knowttl/falcon-axi 1.0.0-rc.2 → 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/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/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 +16 -16
- package/.typescript-build/src/version.js +1 -1
- package/artium-invocations.json +1 -1
- package/package.json +1 -1
|
@@ -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,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);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "1.0.0-rc.
|
|
1
|
+
export const VERSION = "1.0.0-rc.3"; // x-release-please-version
|
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",
|