@p0security/cli 0.7.1 → 0.8.1

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.
@@ -32,12 +32,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
32
32
  });
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.baseFetch = exports.fetchExerciseGrant = exports.fetchCommand = void 0;
35
+ exports.baseFetch = exports.fetchCommand = void 0;
36
+ /** Copyright © 2024-present P0 Security
37
+
38
+ This file is part of @p0security/cli
39
+
40
+ @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.
41
+
42
+ @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.
43
+
44
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
45
+ **/
36
46
  const env_1 = require("../drivers/env");
37
47
  const path = __importStar(require("node:path"));
38
48
  const tenantUrl = (tenant) => `${env_1.config.appUrl}/o/${tenant}`;
39
49
  const commandUrl = (tenant) => `${tenantUrl(tenant)}/command/`;
40
- const exerciseGrantUrl = (tenant) => `${tenantUrl(tenant)}/exercise-grant/`;
41
50
  const fetchCommand = (authn, args, argv) => __awaiter(void 0, void 0, void 0, function* () {
42
51
  return (0, exports.baseFetch)(authn, commandUrl(authn.identity.org.slug), "POST", JSON.stringify({
43
52
  argv,
@@ -45,10 +54,6 @@ const fetchCommand = (authn, args, argv) => __awaiter(void 0, void 0, void 0, fu
45
54
  }));
46
55
  });
47
56
  exports.fetchCommand = fetchCommand;
48
- const fetchExerciseGrant = (authn, args) => __awaiter(void 0, void 0, void 0, function* () {
49
- return (0, exports.baseFetch)(authn, exerciseGrantUrl(authn.identity.org.slug), "POST", JSON.stringify(args));
50
- });
51
- exports.fetchExerciseGrant = fetchExerciseGrant;
52
57
  const baseFetch = (authn, url, method, body) => __awaiter(void 0, void 0, void 0, function* () {
53
58
  const token = yield authn.userCredential.user.getIdToken();
54
59
  const response = yield fetch(url, {
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
- export declare const TERMINATION_CONTROLLER: AbortController;
2
1
  export declare const main: () => void;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.main = exports.TERMINATION_CONTROLLER = void 0;
3
+ exports.main = void 0;
4
4
  /** Copyright © 2024-present P0 Security
5
5
 
6
6
  This file is part of @p0security/cli
@@ -13,17 +13,6 @@ You should have received a copy of the GNU General Public License along with @p0
13
13
  **/
14
14
  const commands_1 = require("./commands");
15
15
  const lodash_1 = require("lodash");
16
- const node_os_1 = require("node:os");
17
- // Subscribing to this global abort controller allows handling process termination signals anywhere in the application
18
- exports.TERMINATION_CONTROLLER = new AbortController();
19
- const terminationHandler = (code) => () => {
20
- exports.TERMINATION_CONTROLLER.abort(code);
21
- process.exit(128 + code); // by convention the exit code is the signal code + 128
22
- };
23
- process.on("SIGHUP", terminationHandler(node_os_1.constants.signals.SIGHUP));
24
- process.on("SIGINT", terminationHandler(node_os_1.constants.signals.SIGINT));
25
- process.on("SIGQUIT", terminationHandler(node_os_1.constants.signals.SIGQUIT));
26
- process.on("SIGTERM", terminationHandler(node_os_1.constants.signals.SIGTERM));
27
16
  const main = () => {
28
17
  // We can suppress output here, as .fail() already print1 errors
29
18
  void commands_1.cli.parse().catch(lodash_1.noop);
@@ -26,9 +26,10 @@ const api_1 = require("./api");
26
26
  const api_2 = require("./api");
27
27
  const roleArn = (args) => `${(0, api_1.arnPrefix)(args.account)}:role/${args.role}`;
28
28
  const stsAssume = (params) => __awaiter(void 0, void 0, void 0, function* () {
29
- const url = `https://sts.amazonaws.com?${(0, fetch_1.urlEncode)(params)}`;
29
+ const url = `https://sts.amazonaws.com`;
30
30
  const response = yield fetch(url, {
31
- method: "GET",
31
+ method: "POST",
32
+ body: new URLSearchParams(params),
32
33
  });
33
34
  yield (0, fetch_1.validateResponse)(response);
34
35
  const stsXml = yield response.text();
@@ -0,0 +1,13 @@
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 { SshRequest } from "../../commands/shared";
12
+ import { AwsSsh } from "./types";
13
+ export declare const awsRequestToSsh: (request: AwsSsh) => SshRequest;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.awsRequestToSsh = void 0;
4
+ const awsRequestToSsh = (request) => {
5
+ return {
6
+ id: request.permission.spec.instanceId,
7
+ accountId: request.permission.spec.accountId,
8
+ region: request.permission.spec.region,
9
+ role: request.generated.name,
10
+ linuxUserName: request.generated.ssh.linuxUserName,
11
+ type: "aws",
12
+ };
13
+ };
14
+ exports.awsRequestToSsh = awsRequestToSsh;
@@ -8,6 +8,8 @@ This file is part of @p0security/cli
8
8
 
9
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
10
  **/
11
+ import { CliPermissionSpec, PermissionSpec } from "../../types/request";
12
+ import { CommonSshPermissionSpec } from "../ssh/types";
11
13
  export declare type AwsCredentials = {
12
14
  AWS_ACCESS_KEY_ID: string;
13
15
  AWS_SECRET_ACCESS_KEY: string;
@@ -50,19 +52,21 @@ export declare type AwsItem = {
50
52
  export declare type AwsConfig = {
51
53
  "iam-write": Record<string, AwsItemConfig>;
52
54
  };
53
- export declare type AwsSsh = {
54
- permission: {
55
- spec: {
56
- awsResourcePermission: {
57
- resource: {
58
- arn: string;
59
- };
60
- };
61
- };
62
- type: "session";
55
+ export declare type AwsSshPermission = {
56
+ spec: CommonSshPermissionSpec & {
57
+ instanceId: string;
58
+ accountId: string;
59
+ region: string;
60
+ type: "aws";
63
61
  };
64
- generated: {
65
- documentName: string;
62
+ type: "session";
63
+ };
64
+ export declare type AwsSshGenerated = {
65
+ name: string;
66
+ ssh: {
67
+ linuxUserName: string;
66
68
  };
67
69
  };
70
+ export declare type AwsPermissionSpec = PermissionSpec<"ssh", AwsSshPermission, AwsSshGenerated>;
71
+ export declare type AwsSsh = CliPermissionSpec<AwsPermissionSpec>;
68
72
  export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Adds an ssh public key to the user object's sshPublicKeys array in Google Workspace.
3
+ * GCP OS Login uses these public keys to authenticate the user.
4
+ * Importing the same public key multiple times is idempotent.
5
+ *
6
+ * The user account and the access token is retrieved from the gcloud CLI.
7
+ *
8
+ * Returns the posix account to use for SSH access.
9
+ *
10
+ * See https://cloud.google.com/compute/docs/oslogin/rest/v1/users/importSshPublicKey
11
+ */
12
+ export declare const importSshKey: (publicKey: string, options?: {
13
+ debug?: boolean;
14
+ }) => Promise<string>;
@@ -0,0 +1,80 @@
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.importSshKey = 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 subprocess_1 = require("../../common/subprocess");
24
+ const stdio_1 = require("../../drivers/stdio");
25
+ /**
26
+ * Adds an ssh public key to the user object's sshPublicKeys array in Google Workspace.
27
+ * GCP OS Login uses these public keys to authenticate the user.
28
+ * Importing the same public key multiple times is idempotent.
29
+ *
30
+ * The user account and the access token is retrieved from the gcloud CLI.
31
+ *
32
+ * Returns the posix account to use for SSH access.
33
+ *
34
+ * See https://cloud.google.com/compute/docs/oslogin/rest/v1/users/importSshPublicKey
35
+ */
36
+ const importSshKey = (publicKey, options) => __awaiter(void 0, void 0, void 0, function* () {
37
+ var _a;
38
+ const debug = (_a = options === null || options === void 0 ? void 0 : options.debug) !== null && _a !== void 0 ? _a : false;
39
+ // Force debug=false otherwise it prints the access token
40
+ const accessToken = yield (0, subprocess_1.asyncSpawn)({ debug: false }, "gcloud", [
41
+ "auth",
42
+ "print-access-token",
43
+ ]);
44
+ const account = yield (0, subprocess_1.asyncSpawn)({ debug }, "gcloud", [
45
+ "config",
46
+ "get-value",
47
+ "account",
48
+ ]);
49
+ if (debug) {
50
+ (0, stdio_1.print2)(`Retrieved access token ${accessToken.slice(0, 10)}... for account ${account}`);
51
+ }
52
+ const url = `https://oslogin.googleapis.com/v1/users/${account}:importSshPublicKey`;
53
+ const response = yield fetch(url, {
54
+ method: "POST",
55
+ body: JSON.stringify({
56
+ key: publicKey,
57
+ }),
58
+ headers: {
59
+ Authorization: `Bearer ${accessToken}`,
60
+ "Content-Type": "application/json",
61
+ },
62
+ });
63
+ const data = yield response.json();
64
+ if (debug) {
65
+ (0, stdio_1.print2)(`Login profile for user after importing public key: ${JSON.stringify(data)}`);
66
+ }
67
+ const { loginProfile } = data;
68
+ // Find the primary POSIX account for the user, or the first in the array
69
+ const linuxAccounts = loginProfile.posixAccounts.filter((account) => account.operatingSystemType === "LINUX");
70
+ const posixAccount = linuxAccounts.find((account) => account.primary) ||
71
+ loginProfile.posixAccounts[0];
72
+ if (debug) {
73
+ (0, stdio_1.print2)(`Picked linux user name: ${posixAccount === null || posixAccount === void 0 ? void 0 : posixAccount.username}`);
74
+ }
75
+ if (!posixAccount) {
76
+ throw "No POSIX accounts configured for the user. Ask your Google Workspace administrator to configure the user's POSIX account.";
77
+ }
78
+ return posixAccount.username;
79
+ });
80
+ exports.importSshKey = importSshKey;
@@ -0,0 +1,13 @@
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 { SshRequest } from "../../commands/shared";
12
+ import { GcpSsh } from "./types";
13
+ export declare const gcpRequestToSsh: (request: GcpSsh) => SshRequest;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gcpRequestToSsh = void 0;
4
+ const gcpRequestToSsh = (request) => {
5
+ return {
6
+ id: request.permission.spec.instanceName,
7
+ projectId: request.permission.spec.projectId,
8
+ zone: request.permission.spec.zone,
9
+ linuxUserName: request.cliLocalData.linuxUserName,
10
+ type: "gcloud",
11
+ };
12
+ };
13
+ exports.gcpRequestToSsh = gcpRequestToSsh;
@@ -0,0 +1,49 @@
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 { CliPermissionSpec, PermissionSpec } from "../../types/request";
12
+ import { CommonSshPermissionSpec } from "../ssh/types";
13
+ export declare type GcpSshPermission = {
14
+ spec: CommonSshPermissionSpec & {
15
+ instanceName: string;
16
+ projectId: string;
17
+ zone: string;
18
+ type: "gcloud";
19
+ };
20
+ type: "session";
21
+ };
22
+ export declare type GcpPermissionSpec = PermissionSpec<"ssh", GcpSshPermission>;
23
+ export declare type GcpSsh = CliPermissionSpec<GcpPermissionSpec, {
24
+ linuxUserName: string;
25
+ }>;
26
+ declare type PosixAccount = {
27
+ username: string;
28
+ uid: string;
29
+ gid: string;
30
+ operatingSystemType: "LINUX" | "OPERATING_SYSTEM_TYPE_UNSPECIFIED" | "WINDOWS";
31
+ homeDirectory?: string;
32
+ primary?: boolean;
33
+ };
34
+ declare type SshPublicKey = {
35
+ key: string;
36
+ fingerprint?: string;
37
+ expirationTimeUsec?: number;
38
+ };
39
+ declare type LoginProfile = {
40
+ name: string;
41
+ posixAccounts: PosixAccount[];
42
+ sshPublicKeys: {
43
+ [fingerprint: string]: SshPublicKey;
44
+ };
45
+ };
46
+ export declare type ImportSshPublicKeyResponse = {
47
+ loginProfile: LoginProfile;
48
+ };
49
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -8,6 +8,6 @@ This file is part of @p0security/cli
8
8
 
9
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
10
  **/
11
- import { ExerciseGrantResponse, ScpCommandArgs, SshCommandArgs } from "../../../commands/shared";
12
- import { Authn } from "../../../types/identity";
13
- export declare const sshOrScp: (authn: Authn, data: ExerciseGrantResponse, cmdArgs: ScpCommandArgs | SshCommandArgs, privateKey: string) => Promise<number | null>;
11
+ import { ScpCommandArgs, SshCommandArgs, SshRequest } from "../../commands/shared";
12
+ import { Authn } from "../../types/identity";
13
+ export declare const sshOrScp: (authn: Authn, data: SshRequest, cmdArgs: ScpCommandArgs | SshCommandArgs, privateKey: string) => Promise<number | null>;
@@ -0,0 +1,302 @@
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.sshOrScp = void 0;
13
+ const keys_1 = require("../../common/keys");
14
+ const stdio_1 = require("../../drivers/stdio");
15
+ const util_1 = require("../../util");
16
+ const install_1 = require("../aws/ssm/install");
17
+ const aws_1 = require("../okta/aws");
18
+ const ssh_agent_1 = require("../ssh-agent");
19
+ const node_child_process_1 = require("node:child_process");
20
+ /** Matches the error message that AWS SSM print1 when access is not propagated */
21
+ // Note that the resource will randomly be either the SSM document or the EC2 instance
22
+ const UNAUTHORIZED_START_SESSION_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/;
23
+ /**
24
+ * Matches the following error messages that AWS SSM print1 when ssh authorized
25
+ * key access hasn't propagated to the instance yet.
26
+ * - Connection closed by UNKNOWN port 65535
27
+ * - scp: Connection closed
28
+ * - kex_exchange_identification: Connection closed by remote host
29
+ */
30
+ const CONNECTION_CLOSED_MESSAGE = /\bConnection closed\b.*\b(?:by UNKNOWN port \d+|by remote host)?/;
31
+ const PUBLIC_KEY_DENIED_MESSAGE = /Permission denied \(publickey\)/;
32
+ const UNAUTHORIZED_TUNNEL_USER_MESSAGE = /Error while connecting \[4033: 'not authorized'\]/;
33
+ const UNAUTHORIZED_INSTANCES_GET_MESSAGE = /Required 'compute\.instances\.get' permission/;
34
+ const DESTINATION_READ_ERROR = /Error while connecting \[4010: 'destination read failed'\]/;
35
+ const GOOGLE_LOGIN_MESSAGE = /You do not currently have an active account selected/;
36
+ /** Maximum amount of time after SSH subprocess starts to check for {@link UNPROVISIONED_ACCESS_MESSAGES}
37
+ * in the process's stderr
38
+ */
39
+ const DEFAULT_VALIDATION_WINDOW_MS = 5e3;
40
+ /** Maximum number of attempts to start an SSH session
41
+ *
42
+ * Note that each attempt consumes ~ 1 s.
43
+ */
44
+ const DEFAULT_MAX_SSH_RETRIES = 30;
45
+ const GCP_MAX_SSH_RETRIES = 120; // GCP requires more time to propagate access
46
+ /** The name of the SessionManager port forwarding document. This document is managed by AWS. */
47
+ const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
48
+ /**
49
+ * AWS
50
+ * There are 2 cases of unprovisioned access in AWS
51
+ * 1. SSM:StartSession action is missing either on the SSM document (AWS-StartSSHSession) or the EC2 instance
52
+ * 2. Temporary error when issuing an SCP command
53
+ *
54
+ * 1: results in UNAUTHORIZED_START_SESSION_MESSAGE
55
+ * 2: results in CONNECTION_CLOSED_MESSAGE
56
+ *
57
+ * Google Cloud
58
+ * There are 5 cases of unprovisioned access in Google Cloud.
59
+ * These are all potentially subject to propagation delays.
60
+ * 1. The linux user name is not present in the user's Google Workspace profile `posixAccounts` attribute
61
+ * 2. The public key is not present in the user's Google Workspace profile `sshPublicKeys` attribute
62
+ * 3. The user cannot act as the service account of the compute instance
63
+ * 4. The user cannot tunnel through the IAP tunnel to the instance
64
+ * 5. The user doesn't have osLogin or osAdminLogin role to the instance
65
+ * 5.a. compute.instances.get permission is missing
66
+ * 5.b. compute.instances.osLogin permission is missing
67
+ * 6: Rare occurrence, the exact conditions so far undetermined (together with CONNECTION_CLOSED_MESSAGE)
68
+ *
69
+ * 1, 2, 3 (yes!), 5b: result in PUBLIC_KEY_DENIED_MESSAGE
70
+ * 4: results in UNAUTHORIZED_TUNNEL_USER_MESSAGE and also CONNECTION_CLOSED_MESSAGE
71
+ * 5a: results in UNAUTHORIZED_INSTANCES_GET_MESSAGE
72
+ * 6: results in DESTINATION_READ_ERROR and also CONNECTION_CLOSED_MESSAGE
73
+ */
74
+ const UNPROVISIONED_ACCESS_MESSAGES = [
75
+ { pattern: UNAUTHORIZED_START_SESSION_MESSAGE },
76
+ { pattern: CONNECTION_CLOSED_MESSAGE },
77
+ { pattern: PUBLIC_KEY_DENIED_MESSAGE },
78
+ { pattern: UNAUTHORIZED_TUNNEL_USER_MESSAGE },
79
+ { pattern: UNAUTHORIZED_INSTANCES_GET_MESSAGE, validationWindowMs: 30e3 },
80
+ { pattern: DESTINATION_READ_ERROR },
81
+ ];
82
+ /** Checks if access has propagated through AWS to the SSM agent
83
+ *
84
+ * AWS takes about 8 minutes, GCP takes under 1 minute
85
+ * to fully resolve access after it is granted.
86
+ * During this time, calls to `aws ssm start-session` / `gcloud compute start-iap-tunnel`
87
+ * will fail randomly with an various error messages.
88
+ *
89
+ * This function checks the subprocess output to see if any of the error messages
90
+ * are printed to the error output within the first 5 seconds of startup.
91
+ * If they are, the returned `isAccessPropagated()` function will return false.
92
+ * When this occurs, the consumer of this function should retry the `aws` / `gcloud` command.
93
+ *
94
+ * Note that this function requires interception of the subprocess stderr stream.
95
+ * This works because AWS SSM wraps the session in a single-stream pty, so we
96
+ * do not capture stderr emitted from the wrapped shell session.
97
+ */
98
+ const accessPropagationGuard = (child, debug) => {
99
+ let isEphemeralAccessDeniedException = false;
100
+ let isGoogleLoginException = false;
101
+ const beforeStart = Date.now();
102
+ child.stderr.on("data", (chunk) => {
103
+ const chunkString = chunk.toString("utf-8");
104
+ if (debug)
105
+ (0, stdio_1.print2)(chunkString);
106
+ const match = UNPROVISIONED_ACCESS_MESSAGES.find((message) => chunkString.match(message.pattern));
107
+ if (match &&
108
+ Date.now() <=
109
+ beforeStart + (match.validationWindowMs || DEFAULT_VALIDATION_WINDOW_MS)) {
110
+ isEphemeralAccessDeniedException = true;
111
+ }
112
+ const googleLoginMatch = chunkString.match(GOOGLE_LOGIN_MESSAGE);
113
+ isGoogleLoginException = isGoogleLoginException || !!googleLoginMatch; // once true, always true
114
+ if (isGoogleLoginException) {
115
+ isEphemeralAccessDeniedException = false; // always overwrite to false so we don't retry the access
116
+ }
117
+ });
118
+ return {
119
+ isAccessPropagated: () => !isEphemeralAccessDeniedException,
120
+ isGoogleLoginException: () => isGoogleLoginException,
121
+ };
122
+ };
123
+ const spawnChildProcess = (credential, command, args, stdio) => (0, node_child_process_1.spawn)(command, args, {
124
+ env: Object.assign(Object.assign({}, process.env), credential),
125
+ stdio,
126
+ shell: false,
127
+ });
128
+ const friendlyProvider = (provider) => provider === "aws"
129
+ ? "AWS"
130
+ : provider === "gcloud"
131
+ ? "Google Cloud"
132
+ : (0, util_1.throwAssertNever)(provider);
133
+ /** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess
134
+ *
135
+ * Requires `aws ssm` to be installed on the client machine.
136
+ */
137
+ function spawnSshNode(options) {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ return new Promise((resolve, reject) => {
140
+ const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio);
141
+ // TODO ENG-2284 support login with Google Cloud: currently return a boolean to indicate if the exception was a Google login error.
142
+ const { isAccessPropagated, isGoogleLoginException } = accessPropagationGuard(child, options.debug);
143
+ const exitListener = child.on("exit", (code) => {
144
+ var _a;
145
+ exitListener.unref();
146
+ // In the case of ephemeral AccessDenied exceptions due to unpropagated
147
+ // permissions, continually retry access until success
148
+ if (!isAccessPropagated()) {
149
+ const attemptsRemaining = options.attemptsRemaining;
150
+ if (options.debug) {
151
+ (0, stdio_1.print2)(`Waiting for access to propagate. Retrying SSH session... (remaining attempts: ${attemptsRemaining})`);
152
+ }
153
+ if (attemptsRemaining <= 0) {
154
+ reject(`Access did not propagate through ${friendlyProvider(options.provider)} before max retry attempts were exceeded. Please contact support@p0.dev for assistance.`);
155
+ return;
156
+ }
157
+ spawnSshNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
158
+ .then((code) => resolve(code))
159
+ .catch(reject);
160
+ return;
161
+ }
162
+ else if (isGoogleLoginException()) {
163
+ reject(`Please login to Google Cloud CLI with 'gcloud auth login'`);
164
+ return;
165
+ }
166
+ (_a = options.abortController) === null || _a === void 0 ? void 0 : _a.abort(code);
167
+ (0, stdio_1.print2)(`SSH session terminated`);
168
+ resolve(code);
169
+ });
170
+ });
171
+ });
172
+ }
173
+ const createProxyCommands = (data, args, debug) => {
174
+ let proxyCommand;
175
+ if (data.type === "aws") {
176
+ proxyCommand = [
177
+ "aws",
178
+ "ssm",
179
+ "start-session",
180
+ "--region",
181
+ data.region,
182
+ "--target",
183
+ "%h",
184
+ "--document-name",
185
+ START_SSH_SESSION_DOCUMENT_NAME,
186
+ "--parameters",
187
+ '"portNumber=%p"',
188
+ ];
189
+ }
190
+ else if (data.type === "gcloud") {
191
+ proxyCommand = [
192
+ "gcloud",
193
+ "compute",
194
+ "start-iap-tunnel",
195
+ data.id,
196
+ "%p",
197
+ // --listen-on-stdin flag is required for interactive SSH session.
198
+ // It is undocumented on page https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel
199
+ // but mention on page https://cloud.google.com/iap/docs/tcp-by-host
200
+ // and also found in `gcloud ssh --dry-run` output
201
+ "--listen-on-stdin",
202
+ `--zone=${data.zone}`,
203
+ `--project=${data.projectId}`,
204
+ ];
205
+ }
206
+ else {
207
+ throw (0, util_1.assertNever)(data);
208
+ }
209
+ const commonArgs = [
210
+ ...(debug ? ["-v"] : []),
211
+ "-o",
212
+ `ProxyCommand=${proxyCommand.join(" ")}`,
213
+ ];
214
+ if ("source" in args) {
215
+ return {
216
+ command: "scp",
217
+ args: [
218
+ ...commonArgs,
219
+ // if a response is not received after three 5 minute attempts,
220
+ // the connection will be closed.
221
+ "-o",
222
+ "ServerAliveCountMax=3",
223
+ `-o`,
224
+ "ServerAliveInterval=300",
225
+ ...(args.recursive ? ["-r"] : []),
226
+ args.source,
227
+ args.destination,
228
+ ],
229
+ };
230
+ }
231
+ return {
232
+ command: "ssh",
233
+ args: [
234
+ ...commonArgs,
235
+ ...(args.A ? ["-A"] : []),
236
+ ...(args.L ? ["-L", args.L] : []),
237
+ ...(args.N ? ["-N"] : []),
238
+ `${data.linuxUserName}@${data.id}`,
239
+ ...(args.command ? [args.command] : []),
240
+ ...args.arguments.map((argument) =>
241
+ // escape all double quotes (") in commands such as `p0 ssh <instance>> echo 'hello; "world"'` because we
242
+ // need to encapsulate command arguments in double quotes as we pass them along to the remote shell
243
+ `"${String(argument).replace(/"/g, '\\"')}"`),
244
+ ],
245
+ };
246
+ };
247
+ /** Converts arguments for manual execution - arguments may have to be quoted or certain characters escaped when executing the commands from a shell */
248
+ const transformForShell = (args) => {
249
+ return args.map((arg) => {
250
+ // The ProxyCommand option must be surrounded by single quotes
251
+ if (arg.startsWith("ProxyCommand=")) {
252
+ const [name, ...value] = arg.split("="); // contains the '=' character in the parameters option: ProxyCommand=aws ssm start-session ... --parameters "portNumber=%p"
253
+ return `${name}='${value.join("=")}'`;
254
+ }
255
+ return arg;
256
+ });
257
+ };
258
+ const awsLogin = (authn, data) => __awaiter(void 0, void 0, void 0, function* () {
259
+ if (!(yield (0, install_1.ensureSsmInstall)())) {
260
+ throw "Please try again after installing the required AWS utilities";
261
+ }
262
+ return yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
263
+ account: data.accountId,
264
+ role: data.role,
265
+ });
266
+ });
267
+ const sshOrScp = (authn, data, cmdArgs, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
268
+ if (!privateKey) {
269
+ throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
270
+ }
271
+ // TODO ENG-2284 support login with Google Cloud
272
+ const credential = data.type === "aws" ? yield awsLogin(authn, data) : undefined;
273
+ return (0, ssh_agent_1.withSshAgent)(cmdArgs, () => __awaiter(void 0, void 0, void 0, function* () {
274
+ const { command, args } = createProxyCommands(data, cmdArgs, cmdArgs.debug);
275
+ if (cmdArgs.debug) {
276
+ const reproCommands = [
277
+ `eval $(ssh-agent)`,
278
+ `ssh-add "${keys_1.PRIVATE_KEY_PATH}"`,
279
+ // TODO ENG-2284 support login with Google Cloud
280
+ ...(data.type === "aws"
281
+ ? [
282
+ `eval $(p0 aws role assume ${data.role} --account ${data.accountId})`,
283
+ ]
284
+ : []),
285
+ `${command} ${transformForShell(args).join(" ")}`,
286
+ ];
287
+ (0, stdio_1.print2)(`Execute the following commands to create a similar SSH/SCP session:\n*** COMMANDS BEGIN ***\n${reproCommands.join("\n")}\n*** COMMANDS END ***"\n`);
288
+ }
289
+ const maxRetries = data.type === "gcloud" ? GCP_MAX_SSH_RETRIES : DEFAULT_MAX_SSH_RETRIES;
290
+ return spawnSshNode({
291
+ credential,
292
+ abortController: new AbortController(),
293
+ command,
294
+ args,
295
+ stdio: ["inherit", "inherit", "pipe"],
296
+ debug: cmdArgs.debug,
297
+ provider: data.type,
298
+ attemptsRemaining: maxRetries,
299
+ });
300
+ }));
301
+ });
302
+ exports.sshOrScp = sshOrScp;
@@ -15,4 +15,8 @@ declare type SshItemConfig = {
15
15
  export declare type SshConfig = {
16
16
  "iam-write": Record<string, SshItemConfig>;
17
17
  };
18
+ export declare type CommonSshPermissionSpec = {
19
+ publicKey: string;
20
+ sudo?: boolean;
21
+ };
18
22
  export {};