@nimbus-dev/connectors 0.2.1 → 0.2.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/README.md +19 -7
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +153 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +160 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +21 -40
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- package/standalone/src/launcher.ts +19 -7
package/shared/run-cli-json.ts
CHANGED
|
@@ -2,23 +2,36 @@ import { nimbusSpawn } from "./nimbus-spawn.ts";
|
|
|
2
2
|
|
|
3
3
|
export type RunCliJsonResult = { ok: true; data: unknown } | { ok: false; message: string };
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Spawn `command`. An empty command, or a non-zero exit, becomes the one failure message every
|
|
7
|
+
* caller reports: `<binary> exited <code>: <first 500 chars of stderr>`.
|
|
8
|
+
*/
|
|
9
|
+
async function spawnChecked(
|
|
6
10
|
command: readonly string[],
|
|
7
11
|
env: Record<string, string | undefined>,
|
|
8
|
-
): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
12
|
+
): Promise<{ ok: true; stdout: string } | { ok: false; message: string }> {
|
|
9
13
|
if (command.length === 0) {
|
|
10
14
|
return { ok: false, message: "empty command" };
|
|
11
15
|
}
|
|
12
|
-
const { code, stderr
|
|
16
|
+
const { code, stdout, stderr } = await nimbusSpawn(command, env);
|
|
13
17
|
if (code !== 0) {
|
|
14
18
|
return {
|
|
15
19
|
ok: false,
|
|
16
|
-
message: `${command[0] ?? "cli"} exited ${String(code)}: ${
|
|
20
|
+
message: `${command[0] ?? "cli"} exited ${String(code)}: ${stderr.slice(0, 500)}`,
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
|
-
return { ok: true };
|
|
23
|
+
return { ok: true, stdout };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runCliOk(
|
|
27
|
+
command: readonly string[],
|
|
28
|
+
env: Record<string, string | undefined>,
|
|
29
|
+
): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
30
|
+
const r = await spawnChecked(command, env);
|
|
31
|
+
return r.ok ? { ok: true } : r;
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
/** {@link runCliOk}, throwing its failure message instead of returning it. */
|
|
22
35
|
export async function runCliOkThrowing(
|
|
23
36
|
command: readonly string[],
|
|
24
37
|
env: Record<string, string | undefined>,
|
|
@@ -33,23 +46,32 @@ export async function runCliJson(
|
|
|
33
46
|
command: readonly string[],
|
|
34
47
|
env: Record<string, string | undefined>,
|
|
35
48
|
): Promise<RunCliJsonResult> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { code, stdout: out, stderr: err } = await nimbusSpawn(command, env);
|
|
40
|
-
if (code !== 0) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
message: `${command[0] ?? "cli"} exited ${String(code)}: ${err.slice(0, 500)}`,
|
|
44
|
-
};
|
|
49
|
+
const r = await spawnChecked(command, env);
|
|
50
|
+
if (!r.ok) {
|
|
51
|
+
return r;
|
|
45
52
|
}
|
|
46
|
-
const trimmed =
|
|
53
|
+
const trimmed = r.stdout.trim();
|
|
47
54
|
if (trimmed === "") {
|
|
48
55
|
return { ok: true, data: null };
|
|
49
56
|
}
|
|
50
57
|
try {
|
|
51
58
|
return { ok: true, data: JSON.parse(trimmed) as unknown };
|
|
52
59
|
} catch {
|
|
53
|
-
return { ok: false, message: `invalid JSON from CLI: ${
|
|
60
|
+
return { ok: false, message: `invalid JSON from CLI: ${r.stdout.slice(0, 200)}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* {@link runCliJson}, throwing its failure message instead of returning it. Resolves to the parsed
|
|
66
|
+
* JSON, or `null` when the command printed nothing.
|
|
67
|
+
*/
|
|
68
|
+
export async function runCliJsonThrowing(
|
|
69
|
+
command: readonly string[],
|
|
70
|
+
env: Record<string, string | undefined>,
|
|
71
|
+
): Promise<unknown> {
|
|
72
|
+
const r = await runCliJson(command, env);
|
|
73
|
+
if (!r.ok) {
|
|
74
|
+
throw new Error(r.message);
|
|
54
75
|
}
|
|
76
|
+
return r.data;
|
|
55
77
|
}
|
package/shared/safe-cli-arg.ts
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard a tool-supplied value before it is passed as an argument to a spawned CLI
|
|
3
|
-
* (`gcloud` / `aws` / `
|
|
3
|
+
* (`gcloud` / `aws` / `az` / `kubectl` / …).
|
|
4
4
|
*
|
|
5
5
|
* Spawns use an argv array (no shell), so there is no classic shell injection — but
|
|
6
6
|
* a value that begins with `-` would be parsed by the CLI as a FLAG rather than a
|
|
7
7
|
* positional / flag-value. This "argv flag smuggling" lets tool input inject
|
|
8
|
-
* arbitrary CLI flags (e.g. a `sinkName` of `--project=attacker
|
|
8
|
+
* arbitrary CLI flags (e.g. a `sinkName` of `--project=attacker`, or a pod name of
|
|
9
|
+
* `--kubeconfig=<path>`, whose exec credential plugin runs a command). Reject
|
|
9
10
|
* any value that is empty, over-long, starts with `-`, or contains control
|
|
10
11
|
* characters.
|
|
11
12
|
*
|
|
12
13
|
* Resource-id charsets vary per service (sink ids are `[A-Za-z0-9_.-]`, CloudWatch
|
|
13
14
|
* log-group names contain `/`, BigQuery refs contain `.`), so this is the UNIVERSAL
|
|
14
15
|
* guard against flag smuggling; callers may layer a stricter per-resource regex on
|
|
15
|
-
* top.
|
|
16
|
+
* top. Two CLIs need one: `aws` and `az` read some argument values from a file
|
|
17
|
+
* instead of taking them as written — see {@link awsCliArgProblem} and
|
|
18
|
+
* {@link azCliArgProblem}. A third rule is not per value at all: an argument to a CLI
|
|
19
|
+
* that Windows runs as a batch file is checked at the spawn itself, in
|
|
20
|
+
* `windows-batch-args.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Returns the value unchanged so it can be used inline:
|
|
16
23
|
* `cli(["describe", assertSafeCliArg(p.id, "id")])`.
|
|
17
24
|
*
|
|
18
25
|
* @throws {Error} when the value is unsafe to pass as a CLI argument.
|
|
19
26
|
*/
|
|
20
27
|
export function assertSafeCliArg(value: string, label = "argument"): string {
|
|
28
|
+
const problem = cliArgProblem(value);
|
|
29
|
+
if (problem !== undefined) {
|
|
30
|
+
throw new Error(`Invalid ${label}: ${problem}`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The longest value accepted as one CLI argument. */
|
|
36
|
+
const MAX_CLI_ARG_LENGTH = 1024;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Why `value` must not be passed to a spawned CLI as one argument, or `undefined` when it may.
|
|
40
|
+
*
|
|
41
|
+
* The reason is the whole of the message a refusal carries, so it says what to change. It is the
|
|
42
|
+
* rule {@link assertSafeCliArg} throws on and {@link isSafeCliArg} tests, written once so that the
|
|
43
|
+
* two can never disagree.
|
|
44
|
+
*/
|
|
45
|
+
export function cliArgProblem(value: unknown): string | undefined {
|
|
21
46
|
if (typeof value !== "string" || value.length === 0) {
|
|
22
|
-
|
|
47
|
+
return "must be a non-empty string";
|
|
23
48
|
}
|
|
24
|
-
if (value.length >
|
|
25
|
-
|
|
49
|
+
if (value.length > MAX_CLI_ARG_LENGTH) {
|
|
50
|
+
return `exceeds ${String(MAX_CLI_ARG_LENGTH)} characters`;
|
|
26
51
|
}
|
|
27
52
|
if (value.startsWith("-")) {
|
|
28
|
-
|
|
29
|
-
`Invalid ${label}: must not start with "-" (argv flag smuggling is not allowed)`,
|
|
30
|
-
);
|
|
53
|
+
return 'must not start with "-" (argv flag smuggling is not allowed)';
|
|
31
54
|
}
|
|
55
|
+
if (hasControlCharacter(value)) {
|
|
56
|
+
return "must not contain control characters";
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasControlCharacter(value: string): boolean {
|
|
32
62
|
for (let i = 0; i < value.length; i += 1) {
|
|
33
63
|
if ((value.codePointAt(i) ?? 0) < 0x20) {
|
|
34
|
-
|
|
64
|
+
return true;
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
|
-
return
|
|
67
|
+
return false;
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
/**
|
|
@@ -43,13 +73,105 @@ export function assertSafeCliArg(value: string, label = "argument"): string {
|
|
|
43
73
|
* schema boundary before the handler ever shells out.
|
|
44
74
|
*/
|
|
45
75
|
export function isSafeCliArg(value: unknown): value is string {
|
|
76
|
+
return cliArgProblem(value) === undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// aws
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The prefixes with which the AWS CLI REPLACES a parameter's value by what it reads from
|
|
85
|
+
* somewhere else. Matched here case-insensitively and after leading whitespace, though the CLI
|
|
86
|
+
* matches them exactly: refusing a value the CLI would have taken as written costs nothing.
|
|
87
|
+
*
|
|
88
|
+
* `file://` and `fileb://` read a local file — for nearly every parameter, not only the ones
|
|
89
|
+
* documented as taking a document; the CLI exempts a short list whose value is itself a URL — and
|
|
90
|
+
* v1 of the CLI fetches an `http://` or `https://` value too, unless `cli_follow_urlparam` is
|
|
91
|
+
* turned off. The CLI then sends what it read to AWS as the value, and an
|
|
92
|
+
* error can quote it back — AWS's own validation errors name the value they reject — so a tool
|
|
93
|
+
* argument of `file://<path>` could hand the model the contents of a file on the user's machine.
|
|
94
|
+
*/
|
|
95
|
+
export const AWS_CLI_LOADING_PREFIXES = ["file://", "fileb://", "http://", "https://"] as const;
|
|
96
|
+
|
|
97
|
+
function awsPrefixProblem(value: string): string | undefined {
|
|
98
|
+
const head = value.trimStart().toLowerCase();
|
|
99
|
+
const prefix = AWS_CLI_LOADING_PREFIXES.find((p) => head.startsWith(p));
|
|
100
|
+
return prefix === undefined
|
|
101
|
+
? undefined
|
|
102
|
+
: `must not start with "${prefix}" (the aws CLI would read the value from that location)`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `@=` is the AWS CLI v2 shorthand-syntax operator that loads a file into one key of a structure
|
|
107
|
+
* parameter, as in `TemplateBody@=file://<path>`. No tool passes a structure parameter today, so
|
|
108
|
+
* this is refused for the next tool that does rather than for any value that reaches the CLI now.
|
|
109
|
+
*/
|
|
110
|
+
function awsShorthandProblem(value: string): string | undefined {
|
|
111
|
+
return value.includes("@=")
|
|
112
|
+
? 'must not contain "@=" (aws shorthand syntax would read a file into the value)'
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Why `value` must not be passed to the AWS CLI as one argument, or `undefined` when it may: the
|
|
118
|
+
* universal rule, then a loading prefix ({@link AWS_CLI_LOADING_PREFIXES}), then `@=`.
|
|
119
|
+
*/
|
|
120
|
+
export function awsCliArgProblem(value: unknown): string | undefined {
|
|
46
121
|
if (typeof value !== "string") {
|
|
47
|
-
return
|
|
122
|
+
return cliArgProblem(value);
|
|
123
|
+
}
|
|
124
|
+
return cliArgProblem(value) ?? awsPrefixProblem(value) ?? awsShorthandProblem(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why `value` must not be passed to the AWS CLI as a DOCUMENT — a CloudFormation template body —
|
|
129
|
+
* or `undefined` when it may.
|
|
130
|
+
*
|
|
131
|
+
* A document legitimately spans lines and runs past the 1024 characters an argument is held to,
|
|
132
|
+
* so of the argument rules only the two about where the CLI looks remain: a leading `-`, which the
|
|
133
|
+
* CLI would parse as an option, and a loading prefix, which would make it read the "document" from
|
|
134
|
+
* a file. `@=` is not among them: shorthand syntax applies to structure parameters, and a template
|
|
135
|
+
* body is a string.
|
|
136
|
+
*/
|
|
137
|
+
export function awsCliDocumentProblem(value: unknown): string | undefined {
|
|
138
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
139
|
+
return "must be a non-empty string";
|
|
140
|
+
}
|
|
141
|
+
if (value.startsWith("-")) {
|
|
142
|
+
return 'must not start with "-" (argv flag smuggling is not allowed)';
|
|
143
|
+
}
|
|
144
|
+
return awsPrefixProblem(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// az
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
function azFileProblem(value: string): string | undefined {
|
|
152
|
+
if (value.startsWith("@")) {
|
|
153
|
+
return 'must not start with "@" (az would read the value from a file)';
|
|
154
|
+
}
|
|
155
|
+
if (value.includes("=@")) {
|
|
156
|
+
return 'must not contain "=@" (az would read what follows from a file)';
|
|
48
157
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Why `value` must not be passed to the Azure CLI as one argument, or `undefined` when it may: the
|
|
163
|
+
* universal rule, then a leading `@`, then `=@`.
|
|
164
|
+
*
|
|
165
|
+
* Before it parses anything, `az` replaces every argument that starts with `@` by the contents of
|
|
166
|
+
* the file named after it — with `~` expanded, and `@-` read from stdin — and does the same to
|
|
167
|
+
* whatever follows the first `=` in an argument. A resource group of
|
|
168
|
+
* `@~/.azure/msal_token_cache.json` would be sent to Azure as that cache, which a "could not be
|
|
169
|
+
* found" error can quote back. An `@` anywhere else, as in an email address, is left alone by `az`
|
|
170
|
+
* and is accepted here.
|
|
171
|
+
*/
|
|
172
|
+
export function azCliArgProblem(value: unknown): string | undefined {
|
|
173
|
+
if (typeof value !== "string") {
|
|
174
|
+
return cliArgProblem(value);
|
|
54
175
|
}
|
|
176
|
+
return cliArgProblem(value) ?? azFileProblem(value);
|
|
55
177
|
}
|
package/shared/search-filter.ts
CHANGED
|
@@ -43,21 +43,35 @@ export function tagText(row: Record<string, unknown>): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* The non-empty string entries of the array at `row[key]`, space-joined; `""` when it is absent or
|
|
47
|
+
* not an array (Airflow `owners`, Prefect `tags`). Unlike {@link tagText}, an empty-string entry is
|
|
48
|
+
* skipped rather than joined — which shows in the haystack's spacing, so it is a separate rule.
|
|
48
49
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const
|
|
51
|
-
if (!Array.isArray(
|
|
50
|
+
export function nonEmptyStringsText(row: Record<string, unknown>, key: string): string {
|
|
51
|
+
const list = row[key];
|
|
52
|
+
if (!Array.isArray(list)) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
return list.filter((v): v is string => typeof v === "string" && v !== "").join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The non-empty string `name` of each objectish entry of the array at `row[key]`, space-joined
|
|
60
|
+
* (Wiz `projects`). Returns "" when `row[key]` is absent, not an array, or holds no object entry
|
|
61
|
+
* with a non-empty string `name`.
|
|
62
|
+
*/
|
|
63
|
+
export function objectNamesText(row: Record<string, unknown>, key: string): string {
|
|
64
|
+
const list = row[key];
|
|
65
|
+
if (!Array.isArray(list)) {
|
|
52
66
|
return "";
|
|
53
67
|
}
|
|
54
68
|
const names: string[] = [];
|
|
55
|
-
for (const
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
69
|
+
for (const entry of list) {
|
|
70
|
+
const obj = asObjectish(entry);
|
|
71
|
+
if (obj === undefined) {
|
|
58
72
|
continue;
|
|
59
73
|
}
|
|
60
|
-
const name =
|
|
74
|
+
const name = obj["name"];
|
|
61
75
|
if (typeof name === "string" && name !== "") {
|
|
62
76
|
names.push(name);
|
|
63
77
|
}
|
|
@@ -65,6 +79,14 @@ export function tagNamesFromObjects(row: Record<string, unknown>): string {
|
|
|
65
79
|
return names.join(" ");
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Extract tag names from an array of `{name: string}` tag objects (e.g. Airflow, DependencyTrack).
|
|
84
|
+
* Returns "" when `tags` is absent, not an array, or contains no object entries with a string `name`.
|
|
85
|
+
*/
|
|
86
|
+
export function tagNamesFromObjects(row: Record<string, unknown>): string {
|
|
87
|
+
return objectNamesText(row, "tags");
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
export function filterByQuery<T>(items: readonly T[], options: FilterByQueryOptions<T>): T[] {
|
|
69
91
|
const needle = options.query.toLowerCase();
|
|
70
92
|
const cap = options.limit ?? 50;
|
|
@@ -88,28 +110,46 @@ export function filterByQuery<T>(items: readonly T[], options: FilterByQueryOpti
|
|
|
88
110
|
|
|
89
111
|
export type FieldExtractor = (item: unknown) => readonly (string | null | undefined)[] | null;
|
|
90
112
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* boilerplate `fieldsOf` body shared by the simpler connectors.
|
|
95
|
-
*/
|
|
96
|
-
export function fieldsFromKeys(
|
|
113
|
+
/** The one body behind {@link fieldsFromKeys} and {@link recordFieldsFromKeys}. */
|
|
114
|
+
function keyedFields(
|
|
115
|
+
toRow: (item: unknown) => Record<string, unknown> | undefined,
|
|
97
116
|
keys: readonly string[],
|
|
98
|
-
|
|
117
|
+
tags: boolean,
|
|
99
118
|
): FieldExtractor {
|
|
100
119
|
return (item: unknown) => {
|
|
101
|
-
const row =
|
|
120
|
+
const row = toRow(item);
|
|
102
121
|
if (row === undefined) {
|
|
103
122
|
return null;
|
|
104
123
|
}
|
|
105
124
|
const parts = keys.map((key) => stringField(row, key));
|
|
106
|
-
if (
|
|
125
|
+
if (tags) {
|
|
107
126
|
parts.push(tagText(row));
|
|
108
127
|
}
|
|
109
128
|
return parts;
|
|
110
129
|
};
|
|
111
130
|
}
|
|
112
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Build a {@link FieldExtractor} that reads a fixed list of string keys off each
|
|
134
|
+
* objectish row, optionally appending the standard `tags` text. Collapses the
|
|
135
|
+
* boilerplate `fieldsOf` body shared by the simpler connectors.
|
|
136
|
+
*/
|
|
137
|
+
export function fieldsFromKeys(
|
|
138
|
+
keys: readonly string[],
|
|
139
|
+
opts?: { readonly tags?: boolean },
|
|
140
|
+
): FieldExtractor {
|
|
141
|
+
return keyedFields(asObjectish, keys, opts?.tags === true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* {@link fieldsFromKeys} for rows that must be plain objects: an ARRAY row is skipped
|
|
146
|
+
* ({@link asRecord}) instead of being read as an object that has none of the keys — which a
|
|
147
|
+
* query made only of spaces would still match. The BI connectors' filters use this rule.
|
|
148
|
+
*/
|
|
149
|
+
export function recordFieldsFromKeys(keys: readonly string[]): FieldExtractor {
|
|
150
|
+
return keyedFields(asRecord, keys, false);
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
/**
|
|
114
154
|
* Read a nested string field by key path off an objectish row, returning `""`
|
|
115
155
|
* when any path segment is missing or the leaf is not a string. Shared by the
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* walk-files — the bounded recursive directory walk the local-filesystem
|
|
3
|
+
* connectors share, and the path-traversal guard that goes with it.
|
|
4
|
+
*
|
|
5
|
+
* `great-expectations`, `localdb` and `dataprofile` each scan a configured
|
|
6
|
+
* directory tree for the files they care about (`*.json`, `*.sql`, and a set of
|
|
7
|
+
* tabular extensions), and each had written out the same walk: recurse, stop at
|
|
8
|
+
* a depth limit, stop at a file-count limit, swallow an unreadable directory
|
|
9
|
+
* rather than aborting the scan.
|
|
10
|
+
*
|
|
11
|
+
* Those two limits are why this is worth sharing rather than tolerating. They
|
|
12
|
+
* are the guard against a connector being pointed — by configuration or by a
|
|
13
|
+
* symlink — at a tree deep or wide enough to exhaust the process, and a guard
|
|
14
|
+
* maintained in three places is a guard that can be tightened in one and left
|
|
15
|
+
* alone in the other two. They were already inconsistent: 1000 files for
|
|
16
|
+
* great-expectations against 2000 for the others. That difference is real (a
|
|
17
|
+
* GX run directory is small) so it stays a per-caller argument rather than
|
|
18
|
+
* being averaged away — but it is now visibly an argument rather than three
|
|
19
|
+
* independent constants that happen to differ.
|
|
20
|
+
*
|
|
21
|
+
* The same three connectors each carried a copy of {@link assertWithinDir} too,
|
|
22
|
+
* identical but for the directory named in the error — a second security
|
|
23
|
+
* control in three hand-written copies, shared for the same reason.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Dirent } from "node:fs";
|
|
27
|
+
import { readdir } from "node:fs/promises";
|
|
28
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
29
|
+
|
|
30
|
+
export interface WalkFilesOptions<T> {
|
|
31
|
+
/** Hard cap on results. The walk stops as soon as it is reached. */
|
|
32
|
+
readonly maxFiles: number;
|
|
33
|
+
/** Hard cap on recursion depth, counted from `root` at 0. */
|
|
34
|
+
readonly maxDepth: number;
|
|
35
|
+
/**
|
|
36
|
+
* What to keep for one file entry, or `undefined` to skip it. Receives the
|
|
37
|
+
* directory entry and its full path, so a caller can select on the name, the
|
|
38
|
+
* extension, or anything else it can decide without reading the file.
|
|
39
|
+
*/
|
|
40
|
+
readonly select: (entry: Dirent, fullPath: string) => T | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Collect what `select` keeps, walking `root` depth-first within the limits.
|
|
45
|
+
*
|
|
46
|
+
* An unreadable directory is SKIPPED rather than fatal: a scan of a
|
|
47
|
+
* user-configured tree meets permission-denied subdirectories routinely, and
|
|
48
|
+
* failing the whole tool call for one of them would make the connector useless
|
|
49
|
+
* on a real machine. A caller that needs to know a directory was unreadable
|
|
50
|
+
* should stat it itself.
|
|
51
|
+
*
|
|
52
|
+
* Symlinks are not followed — `readdir` reports a link as neither a file nor a
|
|
53
|
+
* directory here, so a cycle cannot make the walk diverge.
|
|
54
|
+
*/
|
|
55
|
+
export async function walkFiles<T>(root: string, options: WalkFilesOptions<T>): Promise<T[]> {
|
|
56
|
+
const found: T[] = [];
|
|
57
|
+
|
|
58
|
+
async function walk(dir: string, depth: number): Promise<void> {
|
|
59
|
+
if (depth > options.maxDepth || found.length >= options.maxFiles) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let entries: Dirent[];
|
|
63
|
+
try {
|
|
64
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (found.length >= options.maxFiles) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const full = join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
await walk(full, depth + 1); // NOSONAR S9382: depth-first under one shared maxFiles cap — a subtree must finish before the next starts or the cap stops at a nondeterministic point, and concurrent readdirs over a wide tree are the exhaustion this walk exists to bound.
|
|
75
|
+
} else if (entry.isFile()) {
|
|
76
|
+
const kept = options.select(entry, full);
|
|
77
|
+
if (kept !== undefined) {
|
|
78
|
+
found.push(kept);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await walk(root, 0);
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A {@link WalkFilesOptions.select} that keeps the full path of every file with
|
|
90
|
+
* one of `extensions`, compared case-insensitively.
|
|
91
|
+
*/
|
|
92
|
+
export function byExtension(
|
|
93
|
+
...extensions: readonly string[]
|
|
94
|
+
): (e: Dirent, full: string) => string | undefined {
|
|
95
|
+
const wanted = extensions.map((x) => x.toLowerCase());
|
|
96
|
+
return (entry: Dirent, full: string): string | undefined =>
|
|
97
|
+
wanted.some((x) => entry.name.toLowerCase().endsWith(x)) ? full : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The path-traversal guard: throws `"path escapes <what>"` unless `candidate` (an
|
|
102
|
+
* absolute, resolved path) is `root` itself or a descendant of it. `what` names the
|
|
103
|
+
* directory for the message, e.g. `"the configured local DB scripts dir"`.
|
|
104
|
+
*
|
|
105
|
+
* A `relative()` that starts with `..` climbed out of `root` (this also covers
|
|
106
|
+
* `..<sep>`); one that is absolute could not be expressed relative to it at all — a
|
|
107
|
+
* different drive on Windows. It fails closed: a child whose own name begins with
|
|
108
|
+
* `..` (such as `..cache`) is refused too.
|
|
109
|
+
*/
|
|
110
|
+
export function assertWithinDir(candidate: string, root: string, what: string): void {
|
|
111
|
+
const rel = relative(root, candidate);
|
|
112
|
+
if (rel === "") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
116
|
+
throw new Error(`path escapes ${what}`);
|
|
117
|
+
}
|
|
118
|
+
}
|