@p0security/cli 0.8.2 → 0.9.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.
- package/dist/commands/__tests__/grant.test.d.ts +1 -0
- package/dist/commands/__tests__/grant.test.js +55 -0
- package/dist/commands/aws/files.d.ts +41 -0
- package/dist/commands/aws/files.js +108 -0
- package/dist/commands/grant.d.ts +4 -0
- package/dist/commands/grant.js +17 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/kubeconfig.d.ts +9 -0
- package/dist/commands/kubeconfig.js +190 -0
- package/dist/commands/request.d.ts +0 -8
- package/dist/commands/request.js +3 -102
- package/dist/commands/shared/index.d.ts +2 -2
- package/dist/commands/shared/index.js +1 -1
- package/dist/commands/shared/request.d.ts +14 -0
- package/dist/commands/shared/request.js +115 -0
- package/dist/commands/shared/ssh.d.ts +7 -1
- package/dist/commands/shared/ssh.js +9 -7
- package/dist/common/install.d.ts +11 -0
- package/dist/common/install.js +120 -0
- package/dist/drivers/stdio.d.ts +1 -0
- package/dist/drivers/stdio.js +1 -0
- package/dist/plugins/aws/ssh.d.ts +2 -2
- package/dist/plugins/aws/ssh.js +54 -0
- package/dist/plugins/aws/ssm/install.js +7 -86
- package/dist/plugins/google/ssh.d.ts +0 -10
- package/dist/plugins/google/ssh.js +45 -0
- package/dist/plugins/kubeconfig/index.d.ts +23 -0
- package/dist/plugins/kubeconfig/index.js +98 -0
- package/dist/plugins/kubeconfig/install.d.ts +1 -0
- package/dist/plugins/kubeconfig/install.js +65 -0
- package/dist/plugins/kubeconfig/types.d.ts +57 -0
- package/dist/plugins/kubeconfig/types.js +2 -0
- package/dist/plugins/ssh/index.d.ts +2 -2
- package/dist/plugins/ssh/index.js +65 -93
- package/dist/types/request.d.ts +2 -1
- package/dist/types/ssh.d.ts +21 -1
- package/dist/util.d.ts +11 -0
- package/dist/util.js +13 -1
- package/package.json +7 -3
|
@@ -0,0 +1,98 @@
|
|
|
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.awsCloudAuth = exports.profileName = exports.requestAccessToCluster = exports.getAndValidateK8sIntegration = void 0;
|
|
13
|
+
const shared_1 = require("../../commands/shared");
|
|
14
|
+
const request_1 = require("../../commands/shared/request");
|
|
15
|
+
const firestore_1 = require("../../drivers/firestore");
|
|
16
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
17
|
+
const util_1 = require("../../util");
|
|
18
|
+
const config_1 = require("../aws/config");
|
|
19
|
+
const idc_1 = require("../aws/idc");
|
|
20
|
+
const aws_1 = require("../okta/aws");
|
|
21
|
+
const firestore_2 = require("firebase/firestore");
|
|
22
|
+
const lodash_1 = require("lodash");
|
|
23
|
+
const getAndValidateK8sIntegration = (authn, clusterId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
var _a;
|
|
25
|
+
const configDoc = yield (0, firestore_2.getDoc)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/integrations/k8s`));
|
|
26
|
+
// Validation done here in lieu of the backend, since the backend doesn't validate until approval. TODO: ENG-2365.
|
|
27
|
+
const clusterConfig = (_a = configDoc
|
|
28
|
+
.data()) === null || _a === void 0 ? void 0 : _a.workflows.items.find((c) => c.clusterId === clusterId && c.state === "installed");
|
|
29
|
+
if (!clusterConfig) {
|
|
30
|
+
throw `Cluster with ID ${clusterId} not found`;
|
|
31
|
+
}
|
|
32
|
+
const { awsAccountId, awsClusterArn } = clusterConfig;
|
|
33
|
+
if (!awsAccountId || !awsClusterArn) {
|
|
34
|
+
throw (`This command currently only supports AWS EKS clusters, and ${clusterId} is not configured as one.\n` +
|
|
35
|
+
"You can request access to the cluster using the `p0 request k8s` command.");
|
|
36
|
+
}
|
|
37
|
+
const { config: awsConfig } = yield (0, config_1.getAwsConfig)(authn, awsAccountId);
|
|
38
|
+
const { login: awsLogin } = awsConfig;
|
|
39
|
+
// Verify that the AWS auth type is supported before issuing the requests
|
|
40
|
+
if (!(awsLogin === null || awsLogin === void 0 ? void 0 : awsLogin.type) || (awsLogin === null || awsLogin === void 0 ? void 0 : awsLogin.type) === "iam") {
|
|
41
|
+
throw "This AWS account is not configured for kubectl access via the P0 CLI.\nYou can request access to the cluster using the `p0 request k8s` command.";
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
clusterConfig: Object.assign(Object.assign({}, clusterConfig), { awsAccountId,
|
|
45
|
+
awsClusterArn }),
|
|
46
|
+
awsLoginType: awsLogin.type,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
exports.getAndValidateK8sIntegration = getAndValidateK8sIntegration;
|
|
50
|
+
const requestAccessToCluster = (authn, args, clusterId, role) => __awaiter(void 0, void 0, void 0, function* () {
|
|
51
|
+
const response = yield (0, request_1.request)("request")(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
|
|
52
|
+
"k8s",
|
|
53
|
+
"resource",
|
|
54
|
+
"--cluster",
|
|
55
|
+
clusterId,
|
|
56
|
+
"--role",
|
|
57
|
+
role,
|
|
58
|
+
...(args.resource ? ["--locator", args.resource] : []),
|
|
59
|
+
...(args.reason ? ["--reason", args.reason] : []),
|
|
60
|
+
...(args.requestedDuration
|
|
61
|
+
? ["--requested-duration", args.requestedDuration]
|
|
62
|
+
: []),
|
|
63
|
+
], wait: true }), authn, { message: "approval-required" });
|
|
64
|
+
if (!response) {
|
|
65
|
+
throw "Did not receive access ID from server";
|
|
66
|
+
}
|
|
67
|
+
const { id, isPreexisting } = response;
|
|
68
|
+
if (!isPreexisting) {
|
|
69
|
+
(0, stdio_1.print2)("Waiting for access to be provisioned. This may take up to a minute.");
|
|
70
|
+
}
|
|
71
|
+
return yield (0, shared_1.waitForProvisioning)(authn, id);
|
|
72
|
+
});
|
|
73
|
+
exports.requestAccessToCluster = requestAccessToCluster;
|
|
74
|
+
const profileName = (eksCluterName) => `p0cli-managed-eks-${eksCluterName}`;
|
|
75
|
+
exports.profileName = profileName;
|
|
76
|
+
const awsCloudAuth = (authn, awsAccountId, generated, loginType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
const { eksGenerated } = generated;
|
|
78
|
+
const { name, idc } = eksGenerated;
|
|
79
|
+
switch (loginType) {
|
|
80
|
+
case "idc":
|
|
81
|
+
if (!idc) {
|
|
82
|
+
throw "AWS is configured to use Identity Center, but IDC information wasn't received in the request.";
|
|
83
|
+
}
|
|
84
|
+
return yield (0, idc_1.assumeRoleWithIdc)({
|
|
85
|
+
accountId: awsAccountId,
|
|
86
|
+
permissionSet: name,
|
|
87
|
+
idc,
|
|
88
|
+
});
|
|
89
|
+
case "federated":
|
|
90
|
+
return yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
|
|
91
|
+
accountId: awsAccountId,
|
|
92
|
+
role: name,
|
|
93
|
+
});
|
|
94
|
+
default:
|
|
95
|
+
throw (0, util_1.assertNever)(loginType);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
exports.awsCloudAuth = awsCloudAuth;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ensureEksInstall: () => Promise<boolean>;
|
|
@@ -0,0 +1,65 @@
|
|
|
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.ensureEksInstall = 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 install_1 = require("../../common/install");
|
|
24
|
+
const EksItems = [...install_1.AwsItems, "kubectl"];
|
|
25
|
+
/**
|
|
26
|
+
* Converts the current system architecture, as represented in TypeScript, to
|
|
27
|
+
* the value used in the kubectl download URL, or throw an exception if the
|
|
28
|
+
* current architecture is not one kubectl has an official build for.
|
|
29
|
+
*/
|
|
30
|
+
const kubectlDownloadArch = () => {
|
|
31
|
+
const arch = process.arch;
|
|
32
|
+
switch (arch) {
|
|
33
|
+
case "x64": // macOS, Linux, and Windows
|
|
34
|
+
return "amd64";
|
|
35
|
+
case "arm64": // macOS and Linux only
|
|
36
|
+
return arch;
|
|
37
|
+
default:
|
|
38
|
+
throw `Unsupported system architecture for kubectl: ${arch}. Please install kubectl manually, or check that it is available in your PATH.`;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const kubectlInstallCommandsDarwin = () => {
|
|
42
|
+
const arch = kubectlDownloadArch();
|
|
43
|
+
// The download is the kubectl binary itself
|
|
44
|
+
return [
|
|
45
|
+
`curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/darwin/${arch}/kubectl"`,
|
|
46
|
+
"chmod +x kubectl",
|
|
47
|
+
"sudo mkdir -p /usr/local/bin",
|
|
48
|
+
"sudo mv -i ./kubectl /usr/local/bin/kubectl",
|
|
49
|
+
"sudo chown root: /usr/local/bin/kubectl",
|
|
50
|
+
];
|
|
51
|
+
};
|
|
52
|
+
const EksInstall = Object.assign(Object.assign({}, install_1.AwsInstall), { kubectl: {
|
|
53
|
+
label: "Kubernetes command-line tool",
|
|
54
|
+
commands: {
|
|
55
|
+
get darwin() {
|
|
56
|
+
// Use a getter so that we only invoke kubectlInstallCommandsDarwin() if and when we
|
|
57
|
+
// need to generate the installation commands so that we only check the architecture as
|
|
58
|
+
// needed; if kubectl is already installed, doesn't really matter how it was installed
|
|
59
|
+
// or whether it's an officially-supported architecture.
|
|
60
|
+
return kubectlInstallCommandsDarwin();
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
} });
|
|
64
|
+
const ensureEksInstall = () => __awaiter(void 0, void 0, void 0, function* () { return yield (0, install_1.ensureInstall)(EksItems, EksInstall); });
|
|
65
|
+
exports.ensureEksInstall = ensureEksInstall;
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { PermissionSpec } from "../../types/request";
|
|
12
|
+
export declare type K8sConfig = {
|
|
13
|
+
workflows: {
|
|
14
|
+
items: K8sClusterConfig[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare type K8sClusterConfig = {
|
|
18
|
+
clusterId: string;
|
|
19
|
+
clusterServer: string;
|
|
20
|
+
clusterCertificate: string;
|
|
21
|
+
state: string;
|
|
22
|
+
awsAccountId?: string;
|
|
23
|
+
awsClusterArn?: string;
|
|
24
|
+
} & (KubernetesProxyComponentConfig | KubernetesPublicComponentConfig);
|
|
25
|
+
export declare type EksClusterConfig = K8sClusterConfig & {
|
|
26
|
+
awsAccountId: string;
|
|
27
|
+
awsClusterArn: string;
|
|
28
|
+
};
|
|
29
|
+
declare type KubernetesProxyComponentConfig = {
|
|
30
|
+
isProxy: true;
|
|
31
|
+
publicJwk: string;
|
|
32
|
+
};
|
|
33
|
+
export declare type KubernetesPublicComponentConfig = {
|
|
34
|
+
isProxy: false;
|
|
35
|
+
};
|
|
36
|
+
export declare type K8sPermissionSpec = PermissionSpec<"k8s", K8sResourcePermission, K8sGenerated>;
|
|
37
|
+
export declare type K8sResourcePermission = {
|
|
38
|
+
resource: {
|
|
39
|
+
name: string;
|
|
40
|
+
namespace: string;
|
|
41
|
+
kind: string;
|
|
42
|
+
};
|
|
43
|
+
role: string;
|
|
44
|
+
clusterId: string;
|
|
45
|
+
type: "resource";
|
|
46
|
+
};
|
|
47
|
+
export declare type K8sGenerated = {
|
|
48
|
+
eksGenerated: {
|
|
49
|
+
name: string;
|
|
50
|
+
idc?: {
|
|
51
|
+
id: string;
|
|
52
|
+
region: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
role: string;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
|
@@ -8,7 +8,7 @@ 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 {
|
|
11
|
+
import { CommandArgs } from "../../commands/shared/ssh";
|
|
12
12
|
import { Authn } from "../../types/identity";
|
|
13
13
|
import { SshRequest } from "../../types/ssh";
|
|
14
|
-
export declare const sshOrScp: (authn: Authn,
|
|
14
|
+
export declare const sshOrScp: (authn: Authn, request: SshRequest, cmdArgs: CommandArgs, privateKey: string) => Promise<number | null>;
|
|
@@ -10,13 +10,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.sshOrScp = 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 ssh_1 = require("../../commands/shared/ssh");
|
|
13
24
|
const keys_1 = require("../../common/keys");
|
|
14
25
|
const stdio_1 = require("../../drivers/stdio");
|
|
15
|
-
const util_1 = require("../../util");
|
|
16
|
-
const config_1 = require("../aws/config");
|
|
17
|
-
const idc_1 = require("../aws/idc");
|
|
18
|
-
const install_1 = require("../aws/ssm/install");
|
|
19
|
-
const aws_1 = require("../okta/aws");
|
|
20
26
|
const ssh_agent_1 = require("../ssh-agent");
|
|
21
27
|
const node_child_process_1 = require("node:child_process");
|
|
22
28
|
/** Matches the error message that AWS SSM print1 when access is not propagated */
|
|
@@ -35,18 +41,11 @@ const UNAUTHORIZED_TUNNEL_USER_MESSAGE = /Error while connecting \[4033: 'not au
|
|
|
35
41
|
const UNAUTHORIZED_INSTANCES_GET_MESSAGE = /Required 'compute\.instances\.get' permission/;
|
|
36
42
|
const DESTINATION_READ_ERROR = /Error while connecting \[4010: 'destination read failed'\]/;
|
|
37
43
|
const GOOGLE_LOGIN_MESSAGE = /You do not currently have an active account selected/;
|
|
44
|
+
const SUDO_MESSAGE = /Sorry, user .+ may not run sudo on .+/; // The output of `sudo -v` when the user is not allowed to run sudo
|
|
38
45
|
/** Maximum amount of time after SSH subprocess starts to check for {@link UNPROVISIONED_ACCESS_MESSAGES}
|
|
39
46
|
* in the process's stderr
|
|
40
47
|
*/
|
|
41
48
|
const DEFAULT_VALIDATION_WINDOW_MS = 5e3;
|
|
42
|
-
/** Maximum number of attempts to start an SSH session
|
|
43
|
-
*
|
|
44
|
-
* Note that each attempt consumes ~ 1 s.
|
|
45
|
-
*/
|
|
46
|
-
const DEFAULT_MAX_SSH_RETRIES = 30;
|
|
47
|
-
const GCP_MAX_SSH_RETRIES = 120; // GCP requires more time to propagate access
|
|
48
|
-
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
|
|
49
|
-
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
50
49
|
/**
|
|
51
50
|
* AWS
|
|
52
51
|
* There are 2 cases of unprovisioned access in AWS
|
|
@@ -57,7 +56,7 @@ const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
|
57
56
|
* 2: results in CONNECTION_CLOSED_MESSAGE
|
|
58
57
|
*
|
|
59
58
|
* Google Cloud
|
|
60
|
-
* There are
|
|
59
|
+
* There are 7 cases of unprovisioned access in Google Cloud.
|
|
61
60
|
* These are all potentially subject to propagation delays.
|
|
62
61
|
* 1. The linux user name is not present in the user's Google Workspace profile `posixAccounts` attribute
|
|
63
62
|
* 2. The public key is not present in the user's Google Workspace profile `sshPublicKeys` attribute
|
|
@@ -66,17 +65,20 @@ const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
|
66
65
|
* 5. The user doesn't have osLogin or osAdminLogin role to the instance
|
|
67
66
|
* 5.a. compute.instances.get permission is missing
|
|
68
67
|
* 5.b. compute.instances.osLogin permission is missing
|
|
69
|
-
* 6
|
|
68
|
+
* 6. compute.instances.osAdminLogin is not provisioned but compute.instances.osLogin is - happens when a user upgrades existing access to sudo
|
|
69
|
+
* 7: Rare occurrence, the exact conditions so far undetermined (together with CONNECTION_CLOSED_MESSAGE)
|
|
70
70
|
*
|
|
71
71
|
* 1, 2, 3 (yes!), 5b: result in PUBLIC_KEY_DENIED_MESSAGE
|
|
72
72
|
* 4: results in UNAUTHORIZED_TUNNEL_USER_MESSAGE and also CONNECTION_CLOSED_MESSAGE
|
|
73
73
|
* 5a: results in UNAUTHORIZED_INSTANCES_GET_MESSAGE
|
|
74
|
-
* 6: results in
|
|
74
|
+
* 6: results in SUDO_MESSAGE
|
|
75
|
+
* 7: results in DESTINATION_READ_ERROR and also CONNECTION_CLOSED_MESSAGE
|
|
75
76
|
*/
|
|
76
77
|
const UNPROVISIONED_ACCESS_MESSAGES = [
|
|
77
78
|
{ pattern: UNAUTHORIZED_START_SESSION_MESSAGE },
|
|
78
79
|
{ pattern: CONNECTION_CLOSED_MESSAGE },
|
|
79
80
|
{ pattern: PUBLIC_KEY_DENIED_MESSAGE },
|
|
81
|
+
{ pattern: SUDO_MESSAGE },
|
|
80
82
|
{ pattern: UNAUTHORIZED_TUNNEL_USER_MESSAGE },
|
|
81
83
|
{ pattern: UNAUTHORIZED_INSTANCES_GET_MESSAGE, validationWindowMs: 30e3 },
|
|
82
84
|
{ pattern: DESTINATION_READ_ERROR },
|
|
@@ -127,11 +129,6 @@ const spawnChildProcess = (credential, command, args, stdio) => (0, node_child_p
|
|
|
127
129
|
stdio,
|
|
128
130
|
shell: false,
|
|
129
131
|
});
|
|
130
|
-
const friendlyProvider = (provider) => provider === "aws"
|
|
131
|
-
? "AWS"
|
|
132
|
-
: provider === "gcloud"
|
|
133
|
-
? "Google Cloud"
|
|
134
|
-
: (0, util_1.throwAssertNever)(provider);
|
|
135
132
|
/** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess
|
|
136
133
|
*
|
|
137
134
|
* Requires `aws ssm` to be installed on the client machine.
|
|
@@ -139,6 +136,7 @@ const friendlyProvider = (provider) => provider === "aws"
|
|
|
139
136
|
function spawnSshNode(options) {
|
|
140
137
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
138
|
return new Promise((resolve, reject) => {
|
|
139
|
+
const provider = ssh_1.SSH_PROVIDERS[options.provider];
|
|
142
140
|
const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio);
|
|
143
141
|
// TODO ENG-2284 support login with Google Cloud: currently return a boolean to indicate if the exception was a Google login error.
|
|
144
142
|
const { isAccessPropagated, isGoogleLoginException } = accessPropagationGuard(child, options.debug);
|
|
@@ -153,7 +151,7 @@ function spawnSshNode(options) {
|
|
|
153
151
|
(0, stdio_1.print2)(`Waiting for access to propagate. Retrying SSH session... (remaining attempts: ${attemptsRemaining})`);
|
|
154
152
|
}
|
|
155
153
|
if (attemptsRemaining <= 0) {
|
|
156
|
-
reject(`Access did not propagate through ${
|
|
154
|
+
reject(`Access did not propagate through ${provider.friendlyName} before max retry attempts were exceeded. Please contact support@p0.dev for assistance.`);
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
159
157
|
spawnSshNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
|
|
@@ -166,50 +164,16 @@ function spawnSshNode(options) {
|
|
|
166
164
|
return;
|
|
167
165
|
}
|
|
168
166
|
(_a = options.abortController) === null || _a === void 0 ? void 0 : _a.abort(code);
|
|
169
|
-
(
|
|
167
|
+
if (!options.isAccessPropagationPreTest)
|
|
168
|
+
(0, stdio_1.print2)(`SSH session terminated`);
|
|
170
169
|
resolve(code);
|
|
171
170
|
});
|
|
172
171
|
});
|
|
173
172
|
});
|
|
174
173
|
}
|
|
175
|
-
const
|
|
176
|
-
let proxyCommand;
|
|
177
|
-
if (data.type === "aws") {
|
|
178
|
-
proxyCommand = [
|
|
179
|
-
"aws",
|
|
180
|
-
"ssm",
|
|
181
|
-
"start-session",
|
|
182
|
-
"--region",
|
|
183
|
-
data.region,
|
|
184
|
-
"--target",
|
|
185
|
-
"%h",
|
|
186
|
-
"--document-name",
|
|
187
|
-
START_SSH_SESSION_DOCUMENT_NAME,
|
|
188
|
-
"--parameters",
|
|
189
|
-
'"portNumber=%p"',
|
|
190
|
-
];
|
|
191
|
-
}
|
|
192
|
-
else if (data.type === "gcloud") {
|
|
193
|
-
proxyCommand = [
|
|
194
|
-
"gcloud",
|
|
195
|
-
"compute",
|
|
196
|
-
"start-iap-tunnel",
|
|
197
|
-
data.id,
|
|
198
|
-
"%p",
|
|
199
|
-
// --listen-on-stdin flag is required for interactive SSH session.
|
|
200
|
-
// It is undocumented on page https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel
|
|
201
|
-
// but mention on page https://cloud.google.com/iap/docs/tcp-by-host
|
|
202
|
-
// and also found in `gcloud ssh --dry-run` output
|
|
203
|
-
"--listen-on-stdin",
|
|
204
|
-
`--zone=${data.zone}`,
|
|
205
|
-
`--project=${data.projectId}`,
|
|
206
|
-
];
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
throw (0, util_1.assertNever)(data);
|
|
210
|
-
}
|
|
174
|
+
const createCommand = (data, args, proxyCommand) => {
|
|
211
175
|
const commonArgs = [
|
|
212
|
-
...(debug ? ["-v"] : []),
|
|
176
|
+
...(args.debug ? ["-v"] : []),
|
|
213
177
|
"-o",
|
|
214
178
|
`ProxyCommand=${proxyCommand.join(" ")}`,
|
|
215
179
|
];
|
|
@@ -257,45 +221,53 @@ const transformForShell = (args) => {
|
|
|
257
221
|
return arg;
|
|
258
222
|
});
|
|
259
223
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
224
|
+
/** Construct another command to use for testing access propagation prior to actually logging in the user to the ssh session */
|
|
225
|
+
const preTestAccessPropagationIfNeeded = (sshProvider, request, cmdArgs, proxyCommand, credential) => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
const testCmdArgs = sshProvider.preTestAccessPropagationArgs(cmdArgs);
|
|
227
|
+
// Pre-testing comes at a performance cost because we have to execute another ssh subprocess after
|
|
228
|
+
// a successful test. Only do when absolutely necessary.
|
|
229
|
+
if (testCmdArgs) {
|
|
230
|
+
const { command, args } = createCommand(request, testCmdArgs, proxyCommand);
|
|
231
|
+
// Assumes that this is a non-interactive ssh command that exits automatically
|
|
232
|
+
return spawnSshNode({
|
|
233
|
+
credential,
|
|
234
|
+
abortController: new AbortController(),
|
|
235
|
+
command,
|
|
236
|
+
args,
|
|
237
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
238
|
+
debug: cmdArgs.debug,
|
|
239
|
+
provider: request.type,
|
|
240
|
+
attemptsRemaining: sshProvider.maxRetries,
|
|
241
|
+
isAccessPropagationPreTest: true,
|
|
242
|
+
});
|
|
268
243
|
}
|
|
269
|
-
return
|
|
270
|
-
? yield (0, idc_1.assumeRoleWithIdc)(data)
|
|
271
|
-
: ((_d = config.login) === null || _d === void 0 ? void 0 : _d.type) === "federated"
|
|
272
|
-
? yield (0, aws_1.assumeRoleWithOktaSaml)(authn, data)
|
|
273
|
-
: (0, util_1.throwAssertNever)(config.login);
|
|
244
|
+
return null;
|
|
274
245
|
});
|
|
275
|
-
const sshOrScp = (authn,
|
|
246
|
+
const sshOrScp = (authn, request, cmdArgs, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
276
247
|
if (!privateKey) {
|
|
277
248
|
throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
|
|
278
249
|
}
|
|
279
|
-
|
|
280
|
-
const credential =
|
|
250
|
+
const sshProvider = ssh_1.SSH_PROVIDERS[request.type];
|
|
251
|
+
const credential = yield sshProvider.cloudProviderLogin(authn, request);
|
|
252
|
+
const proxyCommand = sshProvider.proxyCommand(request);
|
|
281
253
|
return (0, ssh_agent_1.withSshAgent)(cmdArgs, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
282
|
-
const { command, args } =
|
|
254
|
+
const { command, args } = createCommand(request, cmdArgs, proxyCommand);
|
|
283
255
|
if (cmdArgs.debug) {
|
|
284
|
-
const reproCommands =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
256
|
+
const reproCommands = sshProvider.reproCommands(request);
|
|
257
|
+
if (reproCommands) {
|
|
258
|
+
const repro = [
|
|
259
|
+
`eval $(ssh-agent)`,
|
|
260
|
+
`ssh-add "${keys_1.PRIVATE_KEY_PATH}"`,
|
|
261
|
+
...reproCommands,
|
|
262
|
+
`${command} ${transformForShell(args).join(" ")}`,
|
|
263
|
+
].join("\n");
|
|
264
|
+
(0, stdio_1.print2)(`Execute the following commands to create a similar SSH/SCP session:\n*** COMMANDS BEGIN ***\n${repro}\n*** COMMANDS END ***"\n`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const exitCode = yield preTestAccessPropagationIfNeeded(sshProvider, request, cmdArgs, proxyCommand, credential);
|
|
268
|
+
if (exitCode && exitCode !== 0) {
|
|
269
|
+
return exitCode; // Only exit if there was an error when pre-testing
|
|
297
270
|
}
|
|
298
|
-
const maxRetries = data.type === "gcloud" ? GCP_MAX_SSH_RETRIES : DEFAULT_MAX_SSH_RETRIES;
|
|
299
271
|
return spawnSshNode({
|
|
300
272
|
credential,
|
|
301
273
|
abortController: new AbortController(),
|
|
@@ -303,8 +275,8 @@ const sshOrScp = (authn, data, cmdArgs, privateKey) => __awaiter(void 0, void 0,
|
|
|
303
275
|
args,
|
|
304
276
|
stdio: ["inherit", "inherit", "pipe"],
|
|
305
277
|
debug: cmdArgs.debug,
|
|
306
|
-
provider:
|
|
307
|
-
attemptsRemaining: maxRetries,
|
|
278
|
+
provider: request.type,
|
|
279
|
+
attemptsRemaining: sshProvider.maxRetries,
|
|
308
280
|
});
|
|
309
281
|
}));
|
|
310
282
|
});
|
package/dist/types/request.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ 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 { K8sPermissionSpec } from "../plugins/kubeconfig/types";
|
|
11
12
|
import { PluginSshRequest } from "./ssh";
|
|
12
13
|
export declare const DONE_STATUSES: readonly ["DONE", "DONE_NOTIFIED"];
|
|
13
14
|
export declare const DENIED_STATUSES: readonly ["DENIED", "DENIED_NOTIFIED"];
|
|
@@ -19,7 +20,7 @@ export declare type PermissionSpec<K extends string, P extends {
|
|
|
19
20
|
permission: P;
|
|
20
21
|
generated: G;
|
|
21
22
|
};
|
|
22
|
-
export declare type PluginRequest = PluginSshRequest;
|
|
23
|
+
export declare type PluginRequest = K8sPermissionSpec | PluginSshRequest;
|
|
23
24
|
export declare type Request<P extends PluginRequest> = P & {
|
|
24
25
|
status: string;
|
|
25
26
|
principal: string;
|
package/dist/types/ssh.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ 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 { CommandArgs } from "../commands/shared/ssh";
|
|
11
12
|
import { AwsSsh, AwsSshPermissionSpec, AwsSshRequest } from "../plugins/aws/types";
|
|
12
13
|
import { GcpSsh, GcpSshPermissionSpec, GcpSshRequest } from "../plugins/google/types";
|
|
14
|
+
import { Authn } from "./identity";
|
|
13
15
|
import { Request } from "./request";
|
|
14
16
|
export declare type CliSshRequest = AwsSsh | GcpSsh;
|
|
15
17
|
export declare type PluginSshRequest = AwsSshPermissionSpec | GcpSshPermissionSpec;
|
|
@@ -18,10 +20,28 @@ export declare type CliPermissionSpec<P extends PluginSshRequest, C extends obje
|
|
|
18
20
|
};
|
|
19
21
|
export declare const SupportedSshProviders: readonly ["aws", "gcloud"];
|
|
20
22
|
export declare type SupportedSshProvider = (typeof SupportedSshProviders)[number];
|
|
21
|
-
export declare type SshProvider<PR extends PluginSshRequest = PluginSshRequest, O extends object | undefined = undefined, SR extends SshRequest = SshRequest> = {
|
|
23
|
+
export declare type SshProvider<PR extends PluginSshRequest = PluginSshRequest, O extends object | undefined = undefined, SR extends SshRequest = SshRequest, C extends object | undefined = undefined> = {
|
|
22
24
|
requestToSsh: (request: CliPermissionSpec<PR, O>) => SR;
|
|
25
|
+
/** Converts a backend request to a CLI request */
|
|
23
26
|
toCliRequest: (request: Request<PR>, options?: {
|
|
24
27
|
debug?: boolean;
|
|
25
28
|
}) => Promise<Request<CliSshRequest>>;
|
|
29
|
+
/** Logs in the user to the cloud provider */
|
|
30
|
+
cloudProviderLogin: (authn: Authn, request: SR) => Promise<C>;
|
|
31
|
+
/** Returns the command and its arguments that are going to be injected as the ssh ProxyCommand option */
|
|
32
|
+
proxyCommand: (request: SR) => string[];
|
|
33
|
+
/** Each element in the returned array is a command that can be run to reproduce the
|
|
34
|
+
* steps of logging in the user to the ssh session. */
|
|
35
|
+
reproCommands: (request: SR) => string[] | undefined;
|
|
36
|
+
/** Arguments for a pre-test command to verify access propagation prior
|
|
37
|
+
* to actually logging in the user to the ssh session.
|
|
38
|
+
* This must return arguments for a non-interactive command - meaning the `command`
|
|
39
|
+
* and potentially the `args` props must be specified in the returned scp/ssh command.
|
|
40
|
+
* If the return value is undefined then no pre-testing is done prior to executing
|
|
41
|
+
* the actual ssh/scp command.
|
|
42
|
+
*/
|
|
43
|
+
preTestAccessPropagationArgs: (cmdArgs: CommandArgs) => CommandArgs | undefined;
|
|
44
|
+
maxRetries: number;
|
|
45
|
+
friendlyName: string;
|
|
26
46
|
};
|
|
27
47
|
export declare type SshRequest = AwsSshRequest | GcpSshRequest;
|
package/dist/util.d.ts
CHANGED
|
@@ -43,3 +43,14 @@ export declare const exec: (command: string, args: string[], options?: child_pro
|
|
|
43
43
|
export declare const throwAssertNever: (value: never) => never;
|
|
44
44
|
export declare const assertNever: (value: never) => Error;
|
|
45
45
|
export declare const unexpectedValueError: (value: any) => Error;
|
|
46
|
+
/**
|
|
47
|
+
* Performs a case-insensitive comparison of two strings. This uses
|
|
48
|
+
* `localeCompare()`, which is safer than `toLowerCase()` or `toUpperCase()` for
|
|
49
|
+
* non-ASCII characters and is the generally-accepted best practice. See:
|
|
50
|
+
* https://stackoverflow.com/a/2140723
|
|
51
|
+
*
|
|
52
|
+
* @param a The first string to compare
|
|
53
|
+
* @param b The second string to compare
|
|
54
|
+
* @returns true if the strings are equal, ignoring case
|
|
55
|
+
*/
|
|
56
|
+
export declare const ciEquals: (a: string, b: string) => boolean;
|
package/dist/util.js
CHANGED
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.unexpectedValueError = exports.assertNever = exports.throwAssertNever = exports.exec = exports.timeout = exports.sleep = exports.P0_PATH = void 0;
|
|
15
|
+
exports.ciEquals = exports.unexpectedValueError = exports.assertNever = exports.throwAssertNever = exports.exec = exports.timeout = exports.sleep = exports.P0_PATH = void 0;
|
|
16
16
|
/** Copyright © 2024-present P0 Security
|
|
17
17
|
|
|
18
18
|
This file is part of @p0security/cli
|
|
@@ -95,3 +95,15 @@ const assertNever = (value) => {
|
|
|
95
95
|
exports.assertNever = assertNever;
|
|
96
96
|
const unexpectedValueError = (value) => new Error(`Unexpected code state: value ${value} had unexpected type`);
|
|
97
97
|
exports.unexpectedValueError = unexpectedValueError;
|
|
98
|
+
/**
|
|
99
|
+
* Performs a case-insensitive comparison of two strings. This uses
|
|
100
|
+
* `localeCompare()`, which is safer than `toLowerCase()` or `toUpperCase()` for
|
|
101
|
+
* non-ASCII characters and is the generally-accepted best practice. See:
|
|
102
|
+
* https://stackoverflow.com/a/2140723
|
|
103
|
+
*
|
|
104
|
+
* @param a The first string to compare
|
|
105
|
+
* @param b The second string to compare
|
|
106
|
+
* @returns true if the strings are equal, ignoring case
|
|
107
|
+
*/
|
|
108
|
+
const ciEquals = (a, b) => a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0;
|
|
109
|
+
exports.ciEquals = ciEquals;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@p0security/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Execute infra CLI commands with P0 grants",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -21,17 +21,20 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@rgrove/parse-xml": "^4.1.0",
|
|
24
|
+
"@types/ini": "^4.1.1",
|
|
24
25
|
"dotenv": "^16.4.1",
|
|
25
26
|
"express": "^4.18.2",
|
|
26
27
|
"firebase": "^10.7.2",
|
|
28
|
+
"ini": "^4.1.3",
|
|
27
29
|
"inquirer": "^9.2.15",
|
|
28
|
-
"jsdom": "^24.
|
|
30
|
+
"jsdom": "^24.1.1",
|
|
29
31
|
"lodash": "^4.17.21",
|
|
30
32
|
"node-forge": "^1.3.1",
|
|
31
33
|
"open": "^8.4.0",
|
|
32
34
|
"pkce-challenge": "^4.1.0",
|
|
33
35
|
"pluralize": "^8.0.0",
|
|
34
36
|
"semver": "^7.6.0",
|
|
37
|
+
"tmp-promise": "^3.0.3",
|
|
35
38
|
"typescript": "^4.8.4",
|
|
36
39
|
"which": "^4.0.0",
|
|
37
40
|
"yargs": "^17.6.0"
|
|
@@ -67,5 +70,6 @@
|
|
|
67
70
|
"lint": "yarn prettier --check . && yarn run eslint --max-warnings 0 .",
|
|
68
71
|
"p0": "node --no-deprecation ./p0",
|
|
69
72
|
"prepublishOnly": "npm run clean && npm run build"
|
|
70
|
-
}
|
|
73
|
+
},
|
|
74
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
71
75
|
}
|