@p0security/cli 0.3.0

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 (103) hide show
  1. package/CONTRIBUTING.md +23 -0
  2. package/LICENSE.md +675 -0
  3. package/README.md +201 -0
  4. package/dist/commands/__tests__/login.test.d.ts +1 -0
  5. package/dist/commands/__tests__/login.test.js +75 -0
  6. package/dist/commands/__tests__/ls.test.d.ts +1 -0
  7. package/dist/commands/__tests__/ls.test.js +84 -0
  8. package/dist/commands/__tests__/request.test.d.ts +1 -0
  9. package/dist/commands/__tests__/request.test.js +94 -0
  10. package/dist/commands/__tests__/ssh.test.d.ts +1 -0
  11. package/dist/commands/__tests__/ssh.test.js +107 -0
  12. package/dist/commands/aws/__tests__/__input__/saml-response.d.ts +11 -0
  13. package/dist/commands/aws/__tests__/__input__/saml-response.js +18 -0
  14. package/dist/commands/aws/__tests__/__input__/sts-response.d.ts +11 -0
  15. package/dist/commands/aws/__tests__/__input__/sts-response.js +37 -0
  16. package/dist/commands/aws/__tests__/role.test.d.ts +1 -0
  17. package/dist/commands/aws/__tests__/role.test.js +98 -0
  18. package/dist/commands/aws/index.d.ts +4 -0
  19. package/dist/commands/aws/index.js +26 -0
  20. package/dist/commands/aws/role.d.ts +27 -0
  21. package/dist/commands/aws/role.js +123 -0
  22. package/dist/commands/index.d.ts +2 -0
  23. package/dist/commands/index.js +49 -0
  24. package/dist/commands/login.d.ts +14 -0
  25. package/dist/commands/login.js +93 -0
  26. package/dist/commands/ls.d.ts +4 -0
  27. package/dist/commands/ls.js +78 -0
  28. package/dist/commands/request.d.ts +12 -0
  29. package/dist/commands/request.js +116 -0
  30. package/dist/commands/ssh.d.ts +11 -0
  31. package/dist/commands/ssh.js +154 -0
  32. package/dist/common/auth/oidc.d.ts +4 -0
  33. package/dist/common/auth/oidc.js +18 -0
  34. package/dist/common/auth/server.d.ts +15 -0
  35. package/dist/common/auth/server.js +73 -0
  36. package/dist/common/fetch.d.ts +16 -0
  37. package/dist/common/fetch.js +39 -0
  38. package/dist/common/mime.d.ts +14 -0
  39. package/dist/common/mime.js +17 -0
  40. package/dist/common/xml.d.ts +21 -0
  41. package/dist/common/xml.js +52 -0
  42. package/dist/drivers/__mocks__/auth.d.ts +30 -0
  43. package/dist/drivers/__mocks__/auth.js +46 -0
  44. package/dist/drivers/api.d.ts +3 -0
  45. package/dist/drivers/api.js +69 -0
  46. package/dist/drivers/auth.d.ts +11 -0
  47. package/dist/drivers/auth.js +122 -0
  48. package/dist/drivers/env.d.ts +15 -0
  49. package/dist/drivers/env.js +38 -0
  50. package/dist/drivers/firestore.d.ts +10 -0
  51. package/dist/drivers/firestore.js +53 -0
  52. package/dist/drivers/stdio.d.ts +25 -0
  53. package/dist/drivers/stdio.js +44 -0
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +23 -0
  56. package/dist/middlewares/version.d.ts +8 -0
  57. package/dist/middlewares/version.js +77 -0
  58. package/dist/plugins/__mocks__/login.d.ts +14 -0
  59. package/dist/plugins/__mocks__/login.js +24 -0
  60. package/dist/plugins/aws/__mocks__/assumeRole.d.ts +12 -0
  61. package/dist/plugins/aws/__mocks__/assumeRole.js +20 -0
  62. package/dist/plugins/aws/api.d.ts +12 -0
  63. package/dist/plugins/aws/api.js +16 -0
  64. package/dist/plugins/aws/assumeRole.d.ts +14 -0
  65. package/dist/plugins/aws/assumeRole.js +55 -0
  66. package/dist/plugins/aws/config.d.ts +5 -0
  67. package/dist/plugins/aws/config.js +38 -0
  68. package/dist/plugins/aws/ssm/index.d.ts +18 -0
  69. package/dist/plugins/aws/ssm/index.js +274 -0
  70. package/dist/plugins/aws/ssm/install.d.ts +7 -0
  71. package/dist/plugins/aws/ssm/install.js +133 -0
  72. package/dist/plugins/aws/types.d.ts +54 -0
  73. package/dist/plugins/aws/types.js +2 -0
  74. package/dist/plugins/google/login.d.ts +2 -0
  75. package/dist/plugins/google/login.js +76 -0
  76. package/dist/plugins/login.d.ts +13 -0
  77. package/dist/plugins/login.js +19 -0
  78. package/dist/plugins/okta/aws.d.ts +5 -0
  79. package/dist/plugins/okta/aws.js +42 -0
  80. package/dist/plugins/okta/login.d.ts +8 -0
  81. package/dist/plugins/okta/login.js +165 -0
  82. package/dist/plugins/ssh/types.d.ts +22 -0
  83. package/dist/plugins/ssh/types.js +2 -0
  84. package/dist/public/favicon.ico +0 -0
  85. package/dist/public/redirect-landing.html +40 -0
  86. package/dist/testing/firestore.d.ts +2 -0
  87. package/dist/testing/firestore.js +16 -0
  88. package/dist/testing/yargs.d.ts +12 -0
  89. package/dist/testing/yargs.js +23 -0
  90. package/dist/types/identity.d.ts +23 -0
  91. package/dist/types/identity.js +2 -0
  92. package/dist/types/index.d.ts +11 -0
  93. package/dist/types/index.js +15 -0
  94. package/dist/types/oidc.d.ts +41 -0
  95. package/dist/types/oidc.js +2 -0
  96. package/dist/types/org.d.ts +20 -0
  97. package/dist/types/org.js +2 -0
  98. package/dist/types/request.d.ts +35 -0
  99. package/dist/types/request.js +20 -0
  100. package/dist/util.d.ts +42 -0
  101. package/dist/util.js +87 -0
  102. package/p0 +16 -0
  103. package/package.json +70 -0
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.assumeRoleWithSaml = void 0;
13
+ /** Copyright © 2024-present P0 Security
14
+
15
+ This file is part of @p0security/cli
16
+
17
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
18
+
19
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
22
+ **/
23
+ const fetch_1 = require("../../common/fetch");
24
+ const xml_1 = require("../../common/xml");
25
+ const api_1 = require("./api");
26
+ const api_2 = require("./api");
27
+ const roleArn = (args) => `${(0, api_1.arnPrefix)(args.account)}:role/${args.role}`;
28
+ const stsAssume = (params) => __awaiter(void 0, void 0, void 0, function* () {
29
+ const url = `https://sts.amazonaws.com?${(0, fetch_1.urlEncode)(params)}`;
30
+ const response = yield fetch(url, {
31
+ method: "GET",
32
+ });
33
+ yield (0, fetch_1.validateResponse)(response);
34
+ const stsXml = yield response.text();
35
+ const stsObject = (0, xml_1.parseXml)(stsXml);
36
+ const stsCredentials = stsObject.AssumeRoleWithSAMLResponse.AssumeRoleWithSAMLResult.Credentials;
37
+ return {
38
+ AWS_ACCESS_KEY_ID: stsCredentials.AccessKeyId,
39
+ AWS_SECRET_ACCESS_KEY: stsCredentials.SecretAccessKey,
40
+ AWS_SESSION_TOKEN: stsCredentials.SessionToken,
41
+ };
42
+ });
43
+ /** Assumes an AWS role via SAML login */
44
+ const assumeRoleWithSaml = (args) => __awaiter(void 0, void 0, void 0, function* () {
45
+ const params = {
46
+ Version: api_2.AWS_API_VERSION,
47
+ Action: "AssumeRoleWithSAML",
48
+ RoleArn: roleArn(args),
49
+ PrincipalArn: `${(0, api_1.arnPrefix)(args.account)}:saml-provider/${args.saml.providerName}`,
50
+ // Note that, despite the name, AWS actually expects a SAML Response
51
+ SAMLAssertion: args.saml.response,
52
+ };
53
+ return yield stsAssume(params);
54
+ });
55
+ exports.assumeRoleWithSaml = assumeRoleWithSaml;
@@ -0,0 +1,5 @@
1
+ import { Authn } from "../../types/identity";
2
+ export declare const getAwsConfig: (authn: Authn, account: string | undefined) => Promise<{
3
+ identity: import("../../types/identity").Identity;
4
+ config: import("./types").AwsItemConfig;
5
+ }>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getAwsConfig = void 0;
13
+ /** Copyright © 2024-present P0 Security
14
+
15
+ This file is part of @p0security/cli
16
+
17
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
18
+
19
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
22
+ **/
23
+ const firestore_1 = require("../../drivers/firestore");
24
+ const firestore_2 = require("firebase/firestore");
25
+ const getAwsConfig = (authn, account) => __awaiter(void 0, void 0, void 0, function* () {
26
+ var _a, _b;
27
+ const { identity } = authn;
28
+ const snapshot = yield (0, firestore_2.getDoc)((0, firestore_1.doc)(`o/${identity.org.tenantId}/integrations/aws`));
29
+ const config = snapshot.data();
30
+ // TODO: Support alias lookup
31
+ const item = account
32
+ ? (_a = config === null || config === void 0 ? void 0 : config.workflows) === null || _a === void 0 ? void 0 : _a.items.find((i) => i.state === "installed" && i.account.id === account)
33
+ : (_b = config === null || config === void 0 ? void 0 : config.workflows) === null || _b === void 0 ? void 0 : _b.items[0];
34
+ if (!item)
35
+ throw `P0 is not installed on AWS account ${account}`;
36
+ return { identity, config: item };
37
+ });
38
+ exports.getAwsConfig = getAwsConfig;
@@ -0,0 +1,18 @@
1
+ /** Copyright © 2024-present P0 Security
2
+
3
+ This file is part of @p0security/cli
4
+
5
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
6
+
7
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8
+
9
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
10
+ **/
11
+ import { SshCommandArgs } from "../../../commands/ssh";
12
+ import { Authn } from "../../../types/identity";
13
+ import { Request } from "../../../types/request";
14
+ import { AwsSsh } from "../types";
15
+ /** Connect to an SSH backend using AWS Systems Manager (SSM) */
16
+ export declare const ssm: (authn: Authn, request: Request<AwsSsh> & {
17
+ id: string;
18
+ }, args: SshCommandArgs) => Promise<void>;
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ssm = void 0;
16
+ const stdio_1 = require("../../../drivers/stdio");
17
+ const aws_1 = require("../../okta/aws");
18
+ const install_1 = require("./install");
19
+ const node_child_process_1 = require("node:child_process");
20
+ const node_stream_1 = require("node:stream");
21
+ const ps_tree_1 = __importDefault(require("ps-tree"));
22
+ const STARTING_SESSION_MESSAGE = /Starting session with SessionId: (.*)/;
23
+ /** Matches the error message that AWS SSM print1 when access is not propagated */
24
+ // Note that the resource will randomly be either the SSM document or the EC2 instance
25
+ const UNPROVISIONED_ACCESS_MESSAGE = /An error occurred \(AccessDeniedException\) when calling the StartSession operation: User: arn:aws:sts::.*:assumed-role\/P0GrantsRole.* is not authorized to perform: ssm:StartSession on resource: arn:aws:.*:.*:.* because no identity-based policy allows the ssm:StartSession action/;
26
+ /** Maximum amount of time after AWS SSM process starts to check for {@link UNPROVISIONED_ACCESS_MESSAGE}
27
+ * in the process's stderr
28
+ */
29
+ const UNPROVISIONED_ACCESS_VALIDATION_WINDOW_MS = 5e3;
30
+ /** Maximum number of attempts to start an SSM session
31
+ *
32
+ * Note that each attempt consumes ~ 1 s.
33
+ */
34
+ const MAX_SSM_RETRIES = 30;
35
+ const INSTANCE_ARN_PATTERN = /^arn:aws:ssm:([^:]+):([^:]+):managed-instance\/([^:]+)$/;
36
+ /** The name of the SessionManager port forwarding document. This document is managed by AWS. */
37
+ const LOCAL_PORT_FORWARDING_DOCUMENT_NAME = "AWS-StartPortForwardingSession";
38
+ /** Checks if access has propagated through AWS to the SSM agent
39
+ *
40
+ * AWS takes about 8 minutes to fully resolve access after it is granted. During
41
+ * this time, calls to `aws ssm start-session` will fail randomly with an
42
+ * access denied exception.
43
+ *
44
+ * This function checks AWS to see if this exception is print1d to the SSM
45
+ * error output within the first 5 seconds of startup. If it is, the returned
46
+ * `isAccessPropagated()` function will return false. When this occurs, the
47
+ * consumer of this function should retry the AWS SSM session.
48
+ *
49
+ * Note that this function requires interception of the AWS SSM stderr stream.
50
+ * This works because AWS SSM wraps the session in a single-stream pty, so we
51
+ * do not capture stderr emitted from the wrapped shell session.
52
+ */
53
+ const accessPropagationGuard = (child) => {
54
+ let isEphemeralAccessDeniedException = false;
55
+ const beforeStart = Date.now();
56
+ child.stderr.on("data", (chunk) => {
57
+ const chunkString = chunk.toString("utf-8");
58
+ const match = chunkString.match(UNPROVISIONED_ACCESS_MESSAGE);
59
+ if (match &&
60
+ Date.now() <= beforeStart + UNPROVISIONED_ACCESS_VALIDATION_WINDOW_MS) {
61
+ isEphemeralAccessDeniedException = true;
62
+ return;
63
+ }
64
+ (0, stdio_1.print2)(chunkString);
65
+ });
66
+ return {
67
+ isAccessPropagated: () => !isEphemeralAccessDeniedException,
68
+ };
69
+ };
70
+ const createBaseSsmCommand = (args) => {
71
+ return [
72
+ "aws",
73
+ "ssm",
74
+ "start-session",
75
+ "--region",
76
+ args.region,
77
+ "--target",
78
+ args.instance,
79
+ ];
80
+ };
81
+ const createInteractiveShellCommand = (args) => {
82
+ var _a;
83
+ const ssmCommand = [
84
+ ...createBaseSsmCommand(args),
85
+ "--document-name",
86
+ args.documentName,
87
+ ];
88
+ const command = (_a = args.command) === null || _a === void 0 ? void 0 : _a.trim();
89
+ if (command) {
90
+ ssmCommand.push("--parameters", `command='${command}'`);
91
+ }
92
+ return ssmCommand;
93
+ };
94
+ const createPortForwardingCommand = (args) => {
95
+ const [localPort, remotePort] = args.forwardPortAddress
96
+ .split(":")
97
+ .map(Number);
98
+ return [
99
+ ...createBaseSsmCommand(args),
100
+ "--document-name",
101
+ // Port forwarding is a special case that uses an AWS-managed document, not the user-generated document we use for an SSH session
102
+ LOCAL_PORT_FORWARDING_DOCUMENT_NAME,
103
+ "--parameters",
104
+ `localPortNumber=${localPort},portNumber=${remotePort}`,
105
+ ];
106
+ };
107
+ const createSsmCommands = (args) => {
108
+ const interactiveShellCommand = createInteractiveShellCommand(args);
109
+ const forwardPortAddress = args.forwardPortAddress;
110
+ if (!forwardPortAddress) {
111
+ return { shellCommand: interactiveShellCommand };
112
+ }
113
+ const portForwardingCommand = createPortForwardingCommand(Object.assign(Object.assign({}, args), { forwardPortAddress }));
114
+ if (args.noRemoteCommands) {
115
+ return { shellCommand: portForwardingCommand };
116
+ }
117
+ return {
118
+ shellCommand: interactiveShellCommand,
119
+ subCommand: portForwardingCommand,
120
+ };
121
+ };
122
+ function spawnChildProcess(credential, command, stdio) {
123
+ return (0, node_child_process_1.spawn)("/usr/bin/env", command, {
124
+ env: Object.assign(Object.assign({}, process.env), credential),
125
+ stdio,
126
+ });
127
+ }
128
+ /** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess
129
+ *
130
+ * Requires `aws ssm` to be installed on the client machine.
131
+ */
132
+ const spawnSsmNode = (options) => __awaiter(void 0, void 0, void 0, function* () {
133
+ return new Promise((resolve, reject) => {
134
+ const child = spawnChildProcess(options.credential, options.command, [
135
+ "inherit",
136
+ "inherit",
137
+ "pipe",
138
+ ]);
139
+ const { isAccessPropagated } = accessPropagationGuard(child);
140
+ const exitListener = child.on("exit", (code) => {
141
+ var _a;
142
+ exitListener.unref();
143
+ // In the case of ephemeral AccessDenied exceptions due to unpropagated
144
+ // permissions, continually retry access until success
145
+ if (!isAccessPropagated()) {
146
+ const attemptsRemaining = (_a = options === null || options === void 0 ? void 0 : options.attemptsRemaining) !== null && _a !== void 0 ? _a : MAX_SSM_RETRIES;
147
+ if (attemptsRemaining <= 0) {
148
+ reject("Access did not propagate through AWS before max retry attempts were exceeded. Please contact support@p0.dev for assistance.");
149
+ return;
150
+ }
151
+ spawnSsmNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
152
+ .then((code) => resolve(code))
153
+ .catch(reject);
154
+ return;
155
+ }
156
+ options.abortController.abort(code);
157
+ (0, stdio_1.print2)(`SSH session terminated`);
158
+ resolve(code);
159
+ });
160
+ });
161
+ });
162
+ /**
163
+ * A subprocess SSM session redirects its output through a proxy that filters certain messages reducing the verbosity of the output.
164
+ * The subprocess also makes sure to terminate any grandchild processes that might spawn during the session.
165
+ *
166
+ * This process should be used when multiple SSM sessions need to be spawned in parallel.
167
+ */
168
+ const spawnSubprocessSsmNode = (options) => __awaiter(void 0, void 0, void 0, function* () {
169
+ return new Promise((resolve, reject) => {
170
+ const child = spawnChildProcess(options.credential, options.command, [
171
+ "ignore",
172
+ "pipe",
173
+ "pipe",
174
+ ]);
175
+ // Captures the starting session message and filters it from the output
176
+ const proxyStream = new node_stream_1.Transform({
177
+ transform(chunk, _, end) {
178
+ const message = chunk.toString("utf-8");
179
+ const match = message.match(STARTING_SESSION_MESSAGE);
180
+ if (!match) {
181
+ this.push(chunk);
182
+ }
183
+ end();
184
+ },
185
+ });
186
+ // Ensures that content from the child process is printed to the terminal and the proxy stream
187
+ child.stdout.pipe(proxyStream).pipe(process.stdout);
188
+ const { isAccessPropagated } = accessPropagationGuard(child);
189
+ const abortListener = (code) => {
190
+ options.abortController.signal.removeEventListener("abort", abortListener);
191
+ // AWS CLI typically will spawn a grandchild process for the SSM session. Using `ps-tree` will allow us
192
+ // to identify and terminate the grandchild process as well.
193
+ (0, ps_tree_1.default)(child.pid, function (_, children) {
194
+ // kill the original child process first so that messages from grandchildren are not printed to stdout
195
+ child.kill();
196
+ // Send a SIGTERM because other signals (e.g. SIGKILL) will not propagate to the grandchildren
197
+ (0, node_child_process_1.exec)(`kill -15 ${children.map((p) => p.PID).join(" ")}`);
198
+ });
199
+ resolve(code);
200
+ };
201
+ child.on("spawn", () => {
202
+ options.abortController.signal.addEventListener("abort", abortListener);
203
+ });
204
+ const exitListener = child.on("exit", (code) => {
205
+ var _a;
206
+ exitListener.unref();
207
+ // In the case of ephemeral AccessDenied exceptions due to unpropagated
208
+ // permissions, continually retry access until success
209
+ if (!isAccessPropagated()) {
210
+ options.abortController.signal.removeEventListener("abort", abortListener);
211
+ const attemptsRemaining = (_a = options === null || options === void 0 ? void 0 : options.attemptsRemaining) !== null && _a !== void 0 ? _a : MAX_SSM_RETRIES;
212
+ if (attemptsRemaining <= 0) {
213
+ reject("Access did not propagate through AWS before max retry attempts were exceeded. Please contact support@p0.dev for assistance.");
214
+ return;
215
+ }
216
+ spawnSubprocessSsmNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
217
+ .then((code) => resolve(code))
218
+ .catch(reject);
219
+ return;
220
+ }
221
+ options.abortController.abort(code);
222
+ });
223
+ });
224
+ });
225
+ /** Convert an SshCommandArgs into an SSM document "command" parameter */
226
+ const commandParameter = (args) => args.command
227
+ ? `${args.command} ${args.arguments
228
+ .map((argument) =>
229
+ // escape all double quotes (") in commands such as `p0 ssh <instance>> echo 'hello; "world"'` because we
230
+ // need to encapsulate command arguments in double quotes as we pass them along to the remote shell
231
+ `"${String(argument).replace(/"/g, '\\"')}"`)
232
+ .join(" ")}`.trim()
233
+ : undefined;
234
+ /** Connect to an SSH backend using AWS Systems Manager (SSM) */
235
+ const ssm = (authn, request, args) => __awaiter(void 0, void 0, void 0, function* () {
236
+ const isInstalled = yield (0, install_1.ensureSsmInstall)();
237
+ if (!isInstalled)
238
+ throw "Please try again after installing the required AWS utilities";
239
+ const match = request.permission.spec.arn.match(INSTANCE_ARN_PATTERN);
240
+ if (!match)
241
+ throw "Did not receive a properly formatted instance identifier";
242
+ const [, region, account, instance] = match;
243
+ const credential = yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
244
+ account,
245
+ role: request.generatedRoles[0].role,
246
+ });
247
+ const ssmArgs = {
248
+ instance: instance,
249
+ region: region,
250
+ documentName: request.generated.documentName,
251
+ requestId: request.id,
252
+ forwardPortAddress: args.L,
253
+ noRemoteCommands: args.N,
254
+ command: commandParameter(args),
255
+ };
256
+ const ssmCommands = createSsmCommands(ssmArgs);
257
+ yield startSsmProcesses(credential, ssmCommands);
258
+ });
259
+ exports.ssm = ssm;
260
+ /**
261
+ * Starts the SSM session and any additional processes that are requested for the session to function properly.
262
+ */
263
+ const startSsmProcesses = (credential, commands) => __awaiter(void 0, void 0, void 0, function* () {
264
+ /** The AbortController is responsible for sending a shared signal to all spawned processes ({@link spawnSsmNode}) when the parent process is terminated unexpectedly. This is necessary because the spawned processes are detached and would otherwise continue running after the parent process is terminated. */
265
+ const abortController = new AbortController();
266
+ const args = { credential, abortController };
267
+ const processes = [
268
+ spawnSsmNode(Object.assign(Object.assign({}, args), { command: commands.shellCommand })),
269
+ ];
270
+ if (commands.subCommand) {
271
+ processes.push(spawnSubprocessSsmNode(Object.assign(Object.assign({}, args), { command: commands.subCommand })));
272
+ }
273
+ yield Promise.all(processes);
274
+ });
@@ -0,0 +1,7 @@
1
+ /** Ensures that AWS CLI and SSM plugin are installed on the user environment
2
+ *
3
+ * If they are not, and the session is a TTY, prompt the user to auto-install. If
4
+ * the user declines, or if not a TTY, the installation commands are printed to
5
+ * stdout.
6
+ */
7
+ export declare const ensureSsmInstall: () => Promise<boolean>;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ensureSsmInstall = void 0;
16
+ /** Copyright © 2024-present P0 Security
17
+
18
+ This file is part of @p0security/cli
19
+
20
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
21
+
22
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
23
+
24
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
25
+ **/
26
+ const stdio_1 = require("../../../drivers/stdio");
27
+ const types_1 = require("../../../types");
28
+ const lodash_1 = require("lodash");
29
+ const node_child_process_1 = require("node:child_process");
30
+ const node_os_1 = __importDefault(require("node:os"));
31
+ const typescript_1 = require("typescript");
32
+ const which_1 = __importDefault(require("which"));
33
+ const SupportedPlatforms = ["darwin"];
34
+ const AwsItems = ["aws", "session-manager-plugin"];
35
+ const AwsInstall = {
36
+ aws: {
37
+ label: "AWS CLI v2",
38
+ commands: {
39
+ darwin: [
40
+ 'curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"',
41
+ "sudo installer -pkg AWSCLIV2.pkg -target /",
42
+ 'rm "AWSCLIV2.pkg"',
43
+ ],
44
+ },
45
+ },
46
+ "session-manager-plugin": {
47
+ label: "the AWS CLI Session Manager plugin",
48
+ commands: {
49
+ darwin: [
50
+ 'curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac/session-manager-plugin.pkg" -o "session-manager-plugin.pkg"',
51
+ "sudo installer -pkg session-manager-plugin.pkg -target /",
52
+ "sudo ln -s /usr/local/sessionmanagerplugin/bin/session-manager-plugin /usr/local/bin/session-manager-plugin",
53
+ 'rm "session-manager-plugin.pkg"',
54
+ ],
55
+ },
56
+ },
57
+ };
58
+ const printToInstall = (toInstall) => {
59
+ (0, stdio_1.print2)("The following items must be installed on your system to continue:");
60
+ for (const item of toInstall) {
61
+ (0, stdio_1.print2)(` - ${AwsInstall[item].label}`);
62
+ }
63
+ (0, stdio_1.print2)("");
64
+ };
65
+ const queryInteractive = () => __awaiter(void 0, void 0, void 0, function* () {
66
+ const inquirer = (yield import("inquirer")).default;
67
+ const { isGuided } = yield inquirer.prompt([
68
+ {
69
+ type: "confirm",
70
+ name: "isGuided",
71
+ message: "Do you want P0 to install these for you (sudo access required)?",
72
+ },
73
+ ]);
74
+ (0, stdio_1.print2)("");
75
+ return isGuided;
76
+ });
77
+ const requiredInstalls = () => __awaiter(void 0, void 0, void 0, function* () {
78
+ return (0, lodash_1.compact)(yield Promise.all(AwsItems.map((item) => __awaiter(void 0, void 0, void 0, function* () { return (yield (0, which_1.default)(item, { nothrow: true })) === null ? item : undefined; }))));
79
+ });
80
+ const printInstallCommands = (platform, item) => {
81
+ const { label, commands } = AwsInstall[item];
82
+ (0, stdio_1.print2)(`To install ${label}, run the following commands:\n`);
83
+ for (const command of commands[platform]) {
84
+ (0, stdio_1.print1)(` ${command}`);
85
+ }
86
+ (0, stdio_1.print1)(""); // Newline is useful for reading command output in a script, so send to /fd/1
87
+ };
88
+ const guidedInstall = (platform, item) => __awaiter(void 0, void 0, void 0, function* () {
89
+ const commands = AwsInstall[item].commands[platform];
90
+ const combined = commands.join(" && \\\n");
91
+ (0, stdio_1.print2)(`Executing:\n${combined}`);
92
+ (0, stdio_1.print2)("");
93
+ yield new Promise((resolve, reject) => {
94
+ const child = (0, node_child_process_1.spawn)("bash", ["-c", combined], { stdio: "inherit" });
95
+ child.on("exit", (code) => {
96
+ if (code === 0)
97
+ resolve();
98
+ else
99
+ reject(`Shell exited with code ${code}`);
100
+ });
101
+ });
102
+ (0, stdio_1.print2)("");
103
+ });
104
+ /** Ensures that AWS CLI and SSM plugin are installed on the user environment
105
+ *
106
+ * If they are not, and the session is a TTY, prompt the user to auto-install. If
107
+ * the user declines, or if not a TTY, the installation commands are printed to
108
+ * stdout.
109
+ */
110
+ const ensureSsmInstall = () => __awaiter(void 0, void 0, void 0, function* () {
111
+ var _a;
112
+ const platform = node_os_1.default.platform();
113
+ if (!(0, types_1.isa)(SupportedPlatforms)(platform))
114
+ throw "SSH to AWS managed instances is only available on MacOS";
115
+ const toInstall = yield requiredInstalls();
116
+ if (toInstall.length === 0)
117
+ return true;
118
+ printToInstall(toInstall);
119
+ const interactive = !!((_a = typescript_1.sys.writeOutputIsTTY) === null || _a === void 0 ? void 0 : _a.call(typescript_1.sys)) && (yield queryInteractive());
120
+ for (const item of toInstall) {
121
+ if (interactive)
122
+ yield guidedInstall(platform, item);
123
+ else
124
+ printInstallCommands(platform, item);
125
+ }
126
+ const remaining = yield requiredInstalls();
127
+ if (remaining.length === 0) {
128
+ (0, stdio_1.print2)("All packages successfully installed");
129
+ return true;
130
+ }
131
+ return false;
132
+ });
133
+ exports.ensureSsmInstall = ensureSsmInstall;
@@ -0,0 +1,54 @@
1
+ /** Copyright © 2024-present P0 Security
2
+
3
+ This file is part of @p0security/cli
4
+
5
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
6
+
7
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8
+
9
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
10
+ **/
11
+ export declare type AwsCredentials = {
12
+ AWS_ACCESS_KEY_ID: string;
13
+ AWS_SECRET_ACCESS_KEY: string;
14
+ AWS_SESSION_TOKEN: string;
15
+ };
16
+ export declare type AwsOktaSamlUidLocation = {
17
+ id: "okta_saml_sso";
18
+ samlProviderName: string;
19
+ appId: string;
20
+ };
21
+ declare type AwsUidLocation = AwsOktaSamlUidLocation | {
22
+ id: "idc";
23
+ parentId: string;
24
+ } | {
25
+ id: "user_tag";
26
+ tagName: string;
27
+ } | {
28
+ id: "username";
29
+ };
30
+ export declare type AwsItemConfig = {
31
+ account: {
32
+ id: string;
33
+ description?: string;
34
+ };
35
+ state: string;
36
+ uidLocation?: AwsUidLocation;
37
+ };
38
+ export declare type AwsConfig = {
39
+ workflows?: {
40
+ items: AwsItemConfig[];
41
+ };
42
+ };
43
+ export declare type AwsSsh = {
44
+ permission: {
45
+ spec: {
46
+ arn: string;
47
+ };
48
+ type: "session";
49
+ };
50
+ generated: {
51
+ documentName: string;
52
+ };
53
+ };
54
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import { TokenResponse } from "../../types/oidc";
2
+ export declare const googleLogin: () => Promise<TokenResponse>;