@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.
@@ -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;
@@ -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 import("../../types/ssh").PluginSshRequest>(authn: Authn, requestId: string) => Promise<Request<P>>;
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 SSH access grant");
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;
@@ -22,4 +22,5 @@ export declare function print2(message: any): void;
22
22
  export declare const Ansi: {
23
23
  readonly Reset: string;
24
24
  readonly Dim: string;
25
+ readonly Yellow: string;
25
26
  };
@@ -40,5 +40,6 @@ exports.print2 = print2;
40
40
  const AnsiCodes = {
41
41
  Reset: "00",
42
42
  Dim: "02",
43
+ Yellow: "33",
43
44
  };
44
45
  exports.Ansi = (0, lodash_1.mapValues)(AnsiCodes, (v) => `\u001b[${v}m`);
@@ -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 stdio_1 = require("../../../drivers/stdio");
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 typescript_1 = require("typescript");
32
- const which_1 = __importDefault(require("which"));
33
- const SupportedPlatforms = ["darwin"];
34
- const AwsItems = ["aws", "session-manager-plugin"];
35
- const AwsInstall = {
36
- aws: {
37
- label: "AWS CLI v2",
38
- commands: {
39
- darwin: [
40
- 'curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"',
41
- "sudo installer -pkg AWSCLIV2.pkg -target /",
42
- 'rm "AWSCLIV2.pkg"',
43
- ],
44
- },
45
- },
46
- "session-manager-plugin": {
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
- if (!(0, types_1.isa)(SupportedPlatforms)(platform))
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
- const toInstall = yield requiredInstalls();
116
- if (toInstall.length === 0)
117
- return true;
118
- printToInstall(toInstall);
119
- const interactive = !!((_a = typescript_1.sys.writeOutputIsTTY) === null || _a === void 0 ? void 0 : _a.call(typescript_1.sys)) && (yield queryInteractive());
120
- for (const item of toInstall) {
121
- if (interactive)
122
- yield guidedInstall(platform, item);
123
- else
124
- printInstallCommands(platform, item);
125
- }
126
- const remaining = yield requiredInstalls();
127
- if (remaining.length === 0) {
128
- (0, stdio_1.print2)("All packages successfully installed");
129
- return true;
130
- }
131
- return false;
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 {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.8.3",
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"