@p0security/cli 0.8.3 → 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/aws/files.d.ts +41 -0
- package/dist/commands/aws/files.js +108 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/kubeconfig.d.ts +9 -0
- package/dist/commands/kubeconfig.js +190 -0
- package/dist/commands/shared/index.d.ts +2 -2
- package/dist/commands/shared/index.js +1 -1
- 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/ssm/install.js +7 -86
- 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/types/request.d.ts +2 -1
- package/dist/util.d.ts +11 -0
- package/dist/util.js +13 -1
- package/package.json +4 -1
|
@@ -0,0 +1,41 @@
|
|
|
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 { AwsCredentials } from "../../plugins/aws/types";
|
|
12
|
+
import * as ini from "ini";
|
|
13
|
+
export declare const AWS_CONFIG_FILE: string;
|
|
14
|
+
export declare const AWS_CREDENTIALS_FILE: string;
|
|
15
|
+
/**
|
|
16
|
+
* Reads in an AWS CLI configuration file, which is formatted as INI text, and
|
|
17
|
+
* returns an arbitrary object representing the contents.
|
|
18
|
+
*
|
|
19
|
+
* @param path Path of the file to read
|
|
20
|
+
* @returns Arbitrary object representing the contents of the file, or an empty
|
|
21
|
+
* object if the file is empty or does not exist
|
|
22
|
+
*/
|
|
23
|
+
export declare const readIniFile: (path: string) => Promise<{
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* This function writes an arbitrary object as INI-formatted text to a file
|
|
28
|
+
* atomically by first writing the data to a temporary file then moving the
|
|
29
|
+
* temporary file on top of the target file. This minimizes the chance that an
|
|
30
|
+
* exception, system crash, or other similar event will leave the file in a
|
|
31
|
+
* corrupted state; this is important since we're mucking around with the AWS
|
|
32
|
+
* CLI's configuration files.
|
|
33
|
+
*
|
|
34
|
+
* @param path Path of the (permanent) file to write to
|
|
35
|
+
* @param obj Arbitrary object to convert to INI-formatted text and write to the
|
|
36
|
+
* file
|
|
37
|
+
* @param iniEncodeOptions Options to pass to the INI encoding library
|
|
38
|
+
*/
|
|
39
|
+
export declare const atomicWriteIniFile: (path: string, obj: any, iniEncodeOptions?: ini.EncodeOptions) => Promise<void>;
|
|
40
|
+
export declare const writeAwsTempCredentials: (profileName: string, awsCredentials: AwsCredentials) => Promise<void>;
|
|
41
|
+
export declare const writeAwsConfigProfile: (profileName: string, profileConfig: any) => Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.writeAwsConfigProfile = exports.writeAwsTempCredentials = exports.atomicWriteIniFile = exports.readIniFile = exports.AWS_CREDENTIALS_FILE = exports.AWS_CONFIG_FILE = void 0;
|
|
39
|
+
const ini = __importStar(require("ini"));
|
|
40
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
+
const tmp_promise_1 = __importDefault(require("tmp-promise"));
|
|
44
|
+
const AWS_CONFIG_PATH = node_path_1.default.join(os.homedir(), ".aws");
|
|
45
|
+
exports.AWS_CONFIG_FILE = node_path_1.default.join(AWS_CONFIG_PATH, "config");
|
|
46
|
+
exports.AWS_CREDENTIALS_FILE = node_path_1.default.join(AWS_CONFIG_PATH, "credentials");
|
|
47
|
+
// Reference documentation: https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html
|
|
48
|
+
/**
|
|
49
|
+
* Reads in an AWS CLI configuration file, which is formatted as INI text, and
|
|
50
|
+
* returns an arbitrary object representing the contents.
|
|
51
|
+
*
|
|
52
|
+
* @param path Path of the file to read
|
|
53
|
+
* @returns Arbitrary object representing the contents of the file, or an empty
|
|
54
|
+
* object if the file is empty or does not exist
|
|
55
|
+
*/
|
|
56
|
+
const readIniFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
try {
|
|
58
|
+
const data = yield fs.readFile(path, { encoding: "utf-8" });
|
|
59
|
+
return data ? ini.parse(data) : {};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === "ENOENT") {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
exports.readIniFile = readIniFile;
|
|
69
|
+
/**
|
|
70
|
+
* This function writes an arbitrary object as INI-formatted text to a file
|
|
71
|
+
* atomically by first writing the data to a temporary file then moving the
|
|
72
|
+
* temporary file on top of the target file. This minimizes the chance that an
|
|
73
|
+
* exception, system crash, or other similar event will leave the file in a
|
|
74
|
+
* corrupted state; this is important since we're mucking around with the AWS
|
|
75
|
+
* CLI's configuration files.
|
|
76
|
+
*
|
|
77
|
+
* @param path Path of the (permanent) file to write to
|
|
78
|
+
* @param obj Arbitrary object to convert to INI-formatted text and write to the
|
|
79
|
+
* file
|
|
80
|
+
* @param iniEncodeOptions Options to pass to the INI encoding library
|
|
81
|
+
*/
|
|
82
|
+
const atomicWriteIniFile = (path, obj, iniEncodeOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
const data = ini.stringify(obj, iniEncodeOptions);
|
|
84
|
+
// Permissions will be moved along with the file
|
|
85
|
+
const { path: tmpPath } = yield tmp_promise_1.default.file({ mode: 0o600, prefix: "p0cli-" });
|
|
86
|
+
yield fs.writeFile(tmpPath, data, { encoding: "utf-8" });
|
|
87
|
+
yield fs.rename(tmpPath, path);
|
|
88
|
+
});
|
|
89
|
+
exports.atomicWriteIniFile = atomicWriteIniFile;
|
|
90
|
+
const writeAwsTempCredentials = (profileName, awsCredentials) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const credentials = yield (0, exports.readIniFile)(exports.AWS_CREDENTIALS_FILE);
|
|
92
|
+
credentials[profileName] = {
|
|
93
|
+
aws_access_key_id: awsCredentials.AWS_ACCESS_KEY_ID,
|
|
94
|
+
aws_secret_access_key: awsCredentials.AWS_SECRET_ACCESS_KEY,
|
|
95
|
+
aws_session_token: awsCredentials.AWS_SESSION_TOKEN,
|
|
96
|
+
};
|
|
97
|
+
// The credentials file is formatted with whitespace before and after the `=`
|
|
98
|
+
yield (0, exports.atomicWriteIniFile)(exports.AWS_CREDENTIALS_FILE, credentials, {
|
|
99
|
+
whitespace: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
exports.writeAwsTempCredentials = writeAwsTempCredentials;
|
|
103
|
+
const writeAwsConfigProfile = (profileName, profileConfig) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const config = yield (0, exports.readIniFile)(exports.AWS_CONFIG_FILE);
|
|
105
|
+
config[`profile ${profileName}`] = profileConfig;
|
|
106
|
+
yield (0, exports.atomicWriteIniFile)(exports.AWS_CONFIG_FILE, config);
|
|
107
|
+
});
|
|
108
|
+
exports.writeAwsConfigProfile = writeAwsConfigProfile;
|
package/dist/commands/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const version_1 = require("../middlewares/version");
|
|
|
19
19
|
const allow_1 = require("./allow");
|
|
20
20
|
const aws_1 = require("./aws");
|
|
21
21
|
const grant_1 = require("./grant");
|
|
22
|
+
const kubeconfig_1 = require("./kubeconfig");
|
|
22
23
|
const login_1 = require("./login");
|
|
23
24
|
const ls_1 = require("./ls");
|
|
24
25
|
const request_1 = require("./request");
|
|
@@ -36,6 +37,7 @@ const commands = [
|
|
|
36
37
|
allow_1.allowCommand,
|
|
37
38
|
ssh_1.sshCommand,
|
|
38
39
|
scp_1.scpCommand,
|
|
40
|
+
kubeconfig_1.kubeconfigCommand,
|
|
39
41
|
];
|
|
40
42
|
exports.cli = commands
|
|
41
43
|
.reduce((m, c) => c(m), (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import yargs from "yargs";
|
|
2
|
+
export declare type KubeconfigCommandArgs = {
|
|
3
|
+
cluster: string;
|
|
4
|
+
role: string;
|
|
5
|
+
resource?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
requestedDuration?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const kubeconfigCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<KubeconfigCommandArgs>;
|
|
@@ -0,0 +1,190 @@
|
|
|
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.kubeconfigCommand = 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 retry_1 = require("../common/retry");
|
|
24
|
+
const auth_1 = require("../drivers/auth");
|
|
25
|
+
const firestore_1 = require("../drivers/firestore");
|
|
26
|
+
const stdio_1 = require("../drivers/stdio");
|
|
27
|
+
const kubeconfig_1 = require("../plugins/kubeconfig");
|
|
28
|
+
const install_1 = require("../plugins/kubeconfig/install");
|
|
29
|
+
const util_1 = require("../util");
|
|
30
|
+
const files_1 = require("./aws/files");
|
|
31
|
+
const kubeconfigCommand = (yargs) => yargs.command("kubeconfig", "Request access to and automatically configure kubectl for a k8s cluster hosted by a cloud provider. Currently supports AWS EKS only.", (yargs) => yargs
|
|
32
|
+
.option("cluster", {
|
|
33
|
+
type: "string",
|
|
34
|
+
demandOption: true,
|
|
35
|
+
describe: "The ID of the k8s cluster as configured P0 Security",
|
|
36
|
+
})
|
|
37
|
+
.option("resource", {
|
|
38
|
+
type: "string",
|
|
39
|
+
describe: 'The resource or resource type (e.g., "Pod / *"), or omit for all',
|
|
40
|
+
})
|
|
41
|
+
.option("role", {
|
|
42
|
+
type: "string",
|
|
43
|
+
demandOption: true,
|
|
44
|
+
describe: 'The k8s role to request, e.g., "ClusterRole / cluster-admin"',
|
|
45
|
+
})
|
|
46
|
+
.option("reason", {
|
|
47
|
+
type: "string",
|
|
48
|
+
describe: "Reason access is needed",
|
|
49
|
+
})
|
|
50
|
+
.option("requested-duration", {
|
|
51
|
+
type: "string",
|
|
52
|
+
// Copied from the P0 backend
|
|
53
|
+
describe: "Requested duration for access (format like '10 minutes', '2 hours', '5 days', or '1 week')",
|
|
54
|
+
}), (0, firestore_1.guard)(kubeconfigAction));
|
|
55
|
+
exports.kubeconfigCommand = kubeconfigCommand;
|
|
56
|
+
const kubeconfigAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
const role = normalizeRoleArg(args.role);
|
|
58
|
+
if (args.resource) {
|
|
59
|
+
validateResourceArg(args.resource);
|
|
60
|
+
}
|
|
61
|
+
const authn = yield (0, auth_1.authenticate)();
|
|
62
|
+
const { clusterConfig, awsLoginType } = yield (0, kubeconfig_1.getAndValidateK8sIntegration)(authn, args.cluster);
|
|
63
|
+
const { clusterId, awsAccountId, awsClusterArn } = clusterConfig;
|
|
64
|
+
if (!(yield (0, install_1.ensureEksInstall)())) {
|
|
65
|
+
throw "Required dependencies are missing; please try again after installing them, or check that they are available on the PATH.";
|
|
66
|
+
}
|
|
67
|
+
const request = yield (0, kubeconfig_1.requestAccessToCluster)(authn, args, clusterId, role);
|
|
68
|
+
const awsAuth = yield (0, kubeconfig_1.awsCloudAuth)(authn, awsAccountId, request.generated, awsLoginType);
|
|
69
|
+
const profile = (0, kubeconfig_1.profileName)(clusterId);
|
|
70
|
+
// The `aws eks update-kubeconfig` command can't handle the ARN of the EKS cluster.
|
|
71
|
+
// So we must, with great annoyance, parse it to extract the cluster name and region.
|
|
72
|
+
const clusterInfo = extractClusterNameAndRegion(awsClusterArn);
|
|
73
|
+
const { clusterRegion, clusterName } = clusterInfo;
|
|
74
|
+
yield (0, files_1.writeAwsTempCredentials)(profile, awsAuth);
|
|
75
|
+
yield (0, files_1.writeAwsConfigProfile)(profile, { region: clusterRegion });
|
|
76
|
+
const updateKubeconfigArgs = [
|
|
77
|
+
"eks",
|
|
78
|
+
"update-kubeconfig",
|
|
79
|
+
"--name",
|
|
80
|
+
clusterName,
|
|
81
|
+
"--region",
|
|
82
|
+
clusterRegion,
|
|
83
|
+
"--profile",
|
|
84
|
+
profile,
|
|
85
|
+
];
|
|
86
|
+
try {
|
|
87
|
+
// Federated access especially sometimes takes some time to propagate, so
|
|
88
|
+
// retry for up to 20 seconds just in case it takes a while.
|
|
89
|
+
const awsResult = yield (0, retry_1.retryWithSleep)(() => __awaiter(void 0, void 0, void 0, function* () { return yield (0, util_1.exec)("aws", updateKubeconfigArgs, { check: true }); }), () => true, 8, 2500);
|
|
90
|
+
(0, stdio_1.print2)(awsResult.stdout);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
(0, stdio_1.print2)("Failed to invoke `aws eks update-kubeconfig`");
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
// `aws update-kubeconfig` will set the kubectl context if it made a change to the kubeconfig file.
|
|
97
|
+
// We'll set the context manually anyway, just in case. `aws update-kubeconfig` names the context
|
|
98
|
+
// with the EKS cluster's ARN.
|
|
99
|
+
try {
|
|
100
|
+
const kubectlResult = yield (0, util_1.exec)("kubectl", ["config", "use-context", awsClusterArn], { check: true });
|
|
101
|
+
(0, stdio_1.print2)(kubectlResult.stdout);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
(0, stdio_1.print2)("Failed to invoke `kubectl config use-context`");
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
(0, stdio_1.print2)("Access granted and kubectl configured successfully. Re-run this command to refresh access if credentials expire.");
|
|
108
|
+
if (process.env.AWS_ACCESS_KEY_ID) {
|
|
109
|
+
(0, stdio_1.print2)(`${stdio_1.Ansi.Yellow}Warning: AWS credentials were detected in your environment, which may cause kubectl errors. ` +
|
|
110
|
+
`To avoid issues, unset with \`unset AWS_ACCESS_KEY_ID\`.${stdio_1.Ansi.Reset}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Normalize the role argument to the format expected by the P0 backend,
|
|
115
|
+
* matching the way the Slack modal formats the role. Also validates that the
|
|
116
|
+
* role argument contains the components expected by the backend without having
|
|
117
|
+
* to make the request first.
|
|
118
|
+
*
|
|
119
|
+
* Currently, the P0 backend does not validate request arguments until after a
|
|
120
|
+
* request is approved; this function allows the validation to be done up-front
|
|
121
|
+
* pending a future backend change. TODO: ENG-2365.
|
|
122
|
+
*
|
|
123
|
+
* @param role The role argument to normalize
|
|
124
|
+
* @returns The normalized role value to pass to the backend
|
|
125
|
+
*/
|
|
126
|
+
const normalizeRoleArg = (role) => {
|
|
127
|
+
const SEPARATOR = "/";
|
|
128
|
+
const SYNTAX_HINT = "The role argument must be in one of the following formats:\n" +
|
|
129
|
+
"- ClusterRole/<roleName>\n" +
|
|
130
|
+
"- CuratedRole/<roleName>\n" +
|
|
131
|
+
"- Role/<namespace>/<roleName>";
|
|
132
|
+
const items = role.split(SEPARATOR).map((item) => item.trim());
|
|
133
|
+
if (items.length < 2 || items.length > 3) {
|
|
134
|
+
throw `Invalid format for role argument.\n${SYNTAX_HINT}`;
|
|
135
|
+
}
|
|
136
|
+
if (!items[0]) {
|
|
137
|
+
throw `Role kind must be specified.\n${SYNTAX_HINT}`;
|
|
138
|
+
}
|
|
139
|
+
if ((0, util_1.ciEquals)(items[0], "ClusterRole")) {
|
|
140
|
+
return `ClusterRole ${SEPARATOR} ${items[1]}`;
|
|
141
|
+
}
|
|
142
|
+
else if ((0, util_1.ciEquals)(items[0], "CuratedRole")) {
|
|
143
|
+
return `CuratedRole ${SEPARATOR} ${items[1]}`;
|
|
144
|
+
}
|
|
145
|
+
else if ((0, util_1.ciEquals)(items[0], "Role")) {
|
|
146
|
+
if (items.length !== 3) {
|
|
147
|
+
throw `Invalid format for role argument.\n${SYNTAX_HINT}`;
|
|
148
|
+
}
|
|
149
|
+
return `Role ${SEPARATOR} ${items[1]} ${SEPARATOR} ${items[2]}`;
|
|
150
|
+
}
|
|
151
|
+
throw `Invalid role kind ${items[0]}.\n${SYNTAX_HINT}`;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Validate that the resource argument is of the format expected by the P0
|
|
155
|
+
* backend, again matching the way the Slack modal formats the resource.
|
|
156
|
+
*
|
|
157
|
+
* Currently, the P0 backend does not validate request arguments until after a
|
|
158
|
+
* request is approved; this function allows the validation to be done up-front
|
|
159
|
+
* pending a future backend change. TODO: ENG-2365.
|
|
160
|
+
*
|
|
161
|
+
* @param resource The resource argument to validate
|
|
162
|
+
*/
|
|
163
|
+
const validateResourceArg = (resource) => {
|
|
164
|
+
const SEPARATOR = " / ";
|
|
165
|
+
const items = resource.split(SEPARATOR);
|
|
166
|
+
if (items.length < 2 || items.length > 3) {
|
|
167
|
+
throw ("Invalid format for resource argument.\n" +
|
|
168
|
+
"The resource argument must be in one of the following formats (spaces required):\n" +
|
|
169
|
+
"- <kind> / <namespace> / <name>\n" +
|
|
170
|
+
"- <kind> / <name>");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const extractClusterNameAndRegion = (clusterArn) => {
|
|
174
|
+
const INVALID_ARN_MSG = `Invalid EKS cluster ARN: ${clusterArn}`;
|
|
175
|
+
// Example EKS cluster ARN: arn:aws:eks:us-west-2:123456789012:cluster/my-testing-cluster
|
|
176
|
+
const parts = clusterArn.split(":");
|
|
177
|
+
if (parts.length < 6 || !parts[3] || !parts[5]) {
|
|
178
|
+
throw INVALID_ARN_MSG;
|
|
179
|
+
}
|
|
180
|
+
const clusterRegion = parts[3];
|
|
181
|
+
const resource = parts[5].split("/");
|
|
182
|
+
if (resource[0] !== "cluster") {
|
|
183
|
+
throw INVALID_ARN_MSG;
|
|
184
|
+
}
|
|
185
|
+
const clusterName = resource[1];
|
|
186
|
+
if (!clusterName) {
|
|
187
|
+
throw INVALID_ARN_MSG;
|
|
188
|
+
}
|
|
189
|
+
return { clusterRegion, clusterName };
|
|
190
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Authn } from "../../types/identity";
|
|
2
|
-
import { Request } from "../../types/request";
|
|
2
|
+
import { PluginRequest, Request } from "../../types/request";
|
|
3
3
|
/** Waits until P0 grants access for a request */
|
|
4
|
-
export declare const waitForProvisioning: <P extends
|
|
4
|
+
export declare const waitForProvisioning: <P extends PluginRequest>(authn: Authn, requestId: string) => Promise<Request<P>>;
|
|
@@ -57,7 +57,7 @@ const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void
|
|
|
57
57
|
cancel = setTimeout(() => {
|
|
58
58
|
if (!isResolved) {
|
|
59
59
|
unsubscribe();
|
|
60
|
-
reject("Timeout awaiting
|
|
60
|
+
reject("Timeout awaiting access grant. Please try again.");
|
|
61
61
|
}
|
|
62
62
|
}, GRANT_TIMEOUT_MILLIS);
|
|
63
63
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SupportedPlatforms: readonly ["darwin"];
|
|
2
|
+
export declare type SupportedPlatform = (typeof SupportedPlatforms)[number];
|
|
3
|
+
export declare const AwsItems: readonly ["aws"];
|
|
4
|
+
export declare type AwsItem = (typeof AwsItems)[number];
|
|
5
|
+
export declare type InstallMetadata = {
|
|
6
|
+
label: string;
|
|
7
|
+
commands: Record<SupportedPlatform, Readonly<string[]>>;
|
|
8
|
+
};
|
|
9
|
+
export declare const AwsInstall: Readonly<Record<AwsItem, InstallMetadata>>;
|
|
10
|
+
export declare const guidedInstall: <T extends string, U extends Readonly<Record<T, InstallMetadata>>>(platform: SupportedPlatform, item: T, installData: U) => Promise<void>;
|
|
11
|
+
export declare const ensureInstall: <T extends string, U extends Readonly<Record<T, InstallMetadata>>>(installItems: readonly T[], installData: U) => Promise<boolean>;
|
|
@@ -0,0 +1,120 @@
|
|
|
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.ensureInstall = exports.guidedInstall = exports.AwsInstall = exports.AwsItems = exports.SupportedPlatforms = 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
|
+
exports.SupportedPlatforms = ["darwin"];
|
|
34
|
+
exports.AwsItems = ["aws"];
|
|
35
|
+
exports.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
|
+
};
|
|
47
|
+
const printToInstall = (toInstall, installMetadata) => {
|
|
48
|
+
(0, stdio_1.print2)("The following items must be installed on your system to continue:");
|
|
49
|
+
for (const item of toInstall) {
|
|
50
|
+
(0, stdio_1.print2)(` - ${installMetadata[item].label} (${item})`);
|
|
51
|
+
}
|
|
52
|
+
(0, stdio_1.print2)("");
|
|
53
|
+
};
|
|
54
|
+
const queryInteractive = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
const inquirer = (yield import("inquirer")).default;
|
|
56
|
+
const { isGuided } = yield inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: "confirm",
|
|
59
|
+
name: "isGuided",
|
|
60
|
+
message: "Do you want P0 to install these for you (sudo access required)?",
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
(0, stdio_1.print2)("");
|
|
64
|
+
return isGuided;
|
|
65
|
+
});
|
|
66
|
+
const requiredInstalls = (installItems) => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
return (0, lodash_1.compact)(yield Promise.all(installItems.map((item) => __awaiter(void 0, void 0, void 0, function* () { return (yield (0, which_1.default)(item, { nothrow: true })) === null ? item : undefined; }))));
|
|
68
|
+
});
|
|
69
|
+
const printInstallCommands = (platform, item, installData) => {
|
|
70
|
+
const { label, commands } = installData[item];
|
|
71
|
+
(0, stdio_1.print2)(`To install ${label}, run the following commands:\n`);
|
|
72
|
+
for (const command of commands[platform]) {
|
|
73
|
+
(0, stdio_1.print1)(` ${command}`);
|
|
74
|
+
}
|
|
75
|
+
(0, stdio_1.print1)(""); // Newline is useful for reading command output in a script, so send to /fd/1
|
|
76
|
+
};
|
|
77
|
+
const guidedInstall = (platform, item, installData) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const commands = installData[item].commands[platform];
|
|
79
|
+
const combined = commands.join(" && \\\n");
|
|
80
|
+
(0, stdio_1.print2)(`Executing:\n${combined}`);
|
|
81
|
+
(0, stdio_1.print2)("");
|
|
82
|
+
yield new Promise((resolve, reject) => {
|
|
83
|
+
const child = (0, node_child_process_1.spawn)("bash", ["-c", combined], { stdio: "inherit" });
|
|
84
|
+
child.on("exit", (code) => {
|
|
85
|
+
if (code === 0)
|
|
86
|
+
resolve();
|
|
87
|
+
else
|
|
88
|
+
reject(`Shell exited with code ${code}`);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
(0, stdio_1.print2)("");
|
|
92
|
+
});
|
|
93
|
+
exports.guidedInstall = guidedInstall;
|
|
94
|
+
const ensureInstall = (installItems, installData) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
var _a;
|
|
96
|
+
const toInstall = yield requiredInstalls(installItems);
|
|
97
|
+
if (toInstall.length === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const platform = node_os_1.default.platform();
|
|
101
|
+
printToInstall(toInstall, installData);
|
|
102
|
+
if (!(0, types_1.isa)(exports.SupportedPlatforms)(platform)) {
|
|
103
|
+
throw (`Guided dependency installation is not available on platform ${platform}\n` +
|
|
104
|
+
"Please install the above dependencies manually, or ensure they are on your PATH.");
|
|
105
|
+
}
|
|
106
|
+
const interactive = !!((_a = typescript_1.sys.writeOutputIsTTY) === null || _a === void 0 ? void 0 : _a.call(typescript_1.sys)) && (yield queryInteractive());
|
|
107
|
+
for (const item of toInstall) {
|
|
108
|
+
if (interactive)
|
|
109
|
+
yield (0, exports.guidedInstall)(platform, item, installData);
|
|
110
|
+
else
|
|
111
|
+
printInstallCommands(platform, item, installData);
|
|
112
|
+
}
|
|
113
|
+
const remaining = yield requiredInstalls(installItems);
|
|
114
|
+
if (remaining.length === 0) {
|
|
115
|
+
(0, stdio_1.print2)("All packages successfully installed");
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
exports.ensureInstall = ensureInstall;
|
package/dist/drivers/stdio.d.ts
CHANGED
package/dist/drivers/stdio.js
CHANGED
|
@@ -23,27 +23,11 @@ This file is part of @p0security/cli
|
|
|
23
23
|
|
|
24
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
25
|
**/
|
|
26
|
-
const
|
|
26
|
+
const install_1 = require("../../../common/install");
|
|
27
27
|
const types_1 = require("../../../types");
|
|
28
|
-
const lodash_1 = require("lodash");
|
|
29
|
-
const node_child_process_1 = require("node:child_process");
|
|
30
28
|
const node_os_1 = __importDefault(require("node:os"));
|
|
31
|
-
const
|
|
32
|
-
const
|
|
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": {
|
|
29
|
+
const SsmItems = [...install_1.AwsItems, "session-manager-plugin"];
|
|
30
|
+
const SsmInstall = Object.assign(Object.assign({}, install_1.AwsInstall), { "session-manager-plugin": {
|
|
47
31
|
label: "the AWS CLI Session Manager plugin",
|
|
48
32
|
commands: {
|
|
49
33
|
darwin: [
|
|
@@ -53,54 +37,7 @@ const AwsInstall = {
|
|
|
53
37
|
'rm "session-manager-plugin.pkg"',
|
|
54
38
|
],
|
|
55
39
|
},
|
|
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
|
-
});
|
|
40
|
+
} });
|
|
104
41
|
/** Ensures that AWS CLI and SSM plugin are installed on the user environment
|
|
105
42
|
*
|
|
106
43
|
* If they are not, and the session is a TTY, prompt the user to auto-install. If
|
|
@@ -108,26 +45,10 @@ const guidedInstall = (platform, item) => __awaiter(void 0, void 0, void 0, func
|
|
|
108
45
|
* stdout.
|
|
109
46
|
*/
|
|
110
47
|
const ensureSsmInstall = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
-
var _a;
|
|
112
48
|
const platform = node_os_1.default.platform();
|
|
113
|
-
|
|
49
|
+
// Preserve existing behavior of a hard error on unsupported platforms
|
|
50
|
+
if (!(0, types_1.isa)(install_1.SupportedPlatforms)(platform))
|
|
114
51
|
throw "SSH to AWS managed instances is only available on MacOS";
|
|
115
|
-
|
|
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;
|
|
52
|
+
return yield (0, install_1.ensureInstall)(SsmItems, SsmInstall);
|
|
132
53
|
});
|
|
133
54
|
exports.ensureSsmInstall = ensureSsmInstall;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { KubeconfigCommandArgs } from "../../commands/kubeconfig";
|
|
12
|
+
import { Authn } from "../../types/identity";
|
|
13
|
+
import { Request } from "../../types/request";
|
|
14
|
+
import { AwsCredentials } from "../aws/types";
|
|
15
|
+
import { EksClusterConfig, K8sGenerated, K8sPermissionSpec } from "./types";
|
|
16
|
+
import yargs from "yargs";
|
|
17
|
+
export declare const getAndValidateK8sIntegration: (authn: Authn, clusterId: string) => Promise<{
|
|
18
|
+
clusterConfig: EksClusterConfig;
|
|
19
|
+
awsLoginType: "federated" | "idc";
|
|
20
|
+
}>;
|
|
21
|
+
export declare const requestAccessToCluster: (authn: Authn, args: yargs.ArgumentsCamelCase<KubeconfigCommandArgs>, clusterId: string, role: string) => Promise<Request<K8sPermissionSpec>>;
|
|
22
|
+
export declare const profileName: (eksCluterName: string) => string;
|
|
23
|
+
export declare const awsCloudAuth: (authn: Authn, awsAccountId: string, generated: K8sGenerated, loginType: "federated" | "idc") => Promise<AwsCredentials>;
|
|
@@ -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 {};
|
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/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,9 +21,11 @@
|
|
|
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
30
|
"jsdom": "^24.1.1",
|
|
29
31
|
"lodash": "^4.17.21",
|
|
@@ -32,6 +34,7 @@
|
|
|
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"
|