@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.
Files changed (220) hide show
  1. package/README.md +19 -7
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +26 -43
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +199 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +134 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +19 -35
  29. package/connectors/cloudwatch/src/tools.ts +20 -38
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +153 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +160 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +21 -40
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +601 -34
  200. package/shared/cli-json-kit.ts +191 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/consent-kit.ts +81 -26
  204. package/shared/cursor-list-tool.ts +29 -0
  205. package/shared/env-json-api.ts +209 -0
  206. package/shared/fetch-json-text.ts +27 -0
  207. package/shared/github-rest.ts +62 -0
  208. package/shared/graphql-json.ts +60 -0
  209. package/shared/imap-mail-core.ts +91 -0
  210. package/shared/imap-tool-kit.ts +57 -23
  211. package/shared/imapflow-adapter.ts +417 -0
  212. package/shared/nimbus-spawn.ts +84 -27
  213. package/shared/rest-tool-kit.ts +77 -7
  214. package/shared/run-cli-json.ts +38 -16
  215. package/shared/safe-cli-arg.ts +139 -17
  216. package/shared/search-filter.ts +58 -18
  217. package/shared/walk-files.ts +118 -0
  218. package/shared/windows-batch-args.ts +273 -0
  219. package/shared/write-budget.ts +100 -0
  220. package/standalone/src/launcher.ts +19 -7
@@ -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
- export async function runCliOk(
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: err } = await nimbusSpawn(command, env);
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)}: ${err.slice(0, 500)}`,
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
- if (command.length === 0) {
37
- return { ok: false, message: "empty command" };
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 = out.trim();
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: ${out.slice(0, 200)}` };
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
  }
@@ -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` / `bq` / …).
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` or `-h`). Reject
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. Returns the value unchanged so it can be used inline:
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
- throw new Error(`Invalid ${label}: must be a non-empty string`);
47
+ return "must be a non-empty string";
23
48
  }
24
- if (value.length > 1024) {
25
- throw new Error(`Invalid ${label}: exceeds 1024 characters`);
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
- throw new Error(
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
- throw new Error(`Invalid ${label}: must not contain control characters`);
64
+ return true;
35
65
  }
36
66
  }
37
- return value;
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 false;
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
- try {
50
- assertSafeCliArg(value);
51
- return true;
52
- } catch {
53
- return false;
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
  }
@@ -43,21 +43,35 @@ export function tagText(row: Record<string, unknown>): string {
43
43
  }
44
44
 
45
45
  /**
46
- * Extract tag names from an array of `{name: string}` tag objects (e.g. Airflow, DependencyTrack).
47
- * Returns "" when `tags` is absent, not an array, or contains no object entries with a string `name`.
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 tagNamesFromObjects(row: Record<string, unknown>): string {
50
- const tags = row["tags"];
51
- if (!Array.isArray(tags)) {
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 t of tags) {
56
- const tag = asObjectish(t);
57
- if (tag === undefined) {
69
+ for (const entry of list) {
70
+ const obj = asObjectish(entry);
71
+ if (obj === undefined) {
58
72
  continue;
59
73
  }
60
- const name = tag["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
- * Build a {@link FieldExtractor} that reads a fixed list of string keys off each
93
- * objectish row, optionally appending the standard `tags` text. Collapses the
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
- opts?: { readonly tags?: boolean },
117
+ tags: boolean,
99
118
  ): FieldExtractor {
100
119
  return (item: unknown) => {
101
- const row = asObjectish(item);
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 (opts?.tags === true) {
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
+ }