@p0security/cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/commands/__tests__/ssh.test.js +11 -10
  2. package/dist/commands/scp.d.ts +1 -1
  3. package/dist/commands/scp.js +11 -5
  4. package/dist/commands/shared/index.d.ts +4 -0
  5. package/dist/commands/shared/index.js +67 -0
  6. package/dist/commands/{shared.d.ts → shared/ssh.d.ts} +7 -14
  7. package/dist/commands/{shared.js → shared/ssh.js} +30 -64
  8. package/dist/commands/ssh.d.ts +1 -1
  9. package/dist/commands/ssh.js +10 -4
  10. package/dist/common/retry.d.ts +9 -0
  11. package/dist/common/retry.js +50 -0
  12. package/dist/common/subprocess.d.ts +10 -0
  13. package/dist/common/subprocess.js +72 -0
  14. package/dist/drivers/auth.d.ts +1 -1
  15. package/dist/drivers/auth.js +7 -3
  16. package/dist/plugins/aws/config.d.ts +1 -1
  17. package/dist/plugins/aws/idc/index.d.ts +16 -0
  18. package/dist/plugins/aws/idc/index.js +150 -0
  19. package/dist/plugins/aws/ssh.d.ts +13 -0
  20. package/dist/plugins/aws/ssh.js +24 -0
  21. package/dist/plugins/aws/types.d.ts +42 -16
  22. package/dist/plugins/google/ssh-key.d.ts +14 -0
  23. package/dist/plugins/google/ssh-key.js +80 -0
  24. package/dist/plugins/google/ssh.d.ts +15 -0
  25. package/dist/plugins/google/ssh.js +29 -0
  26. package/dist/plugins/google/types.d.ts +57 -0
  27. package/dist/plugins/google/types.js +2 -0
  28. package/dist/plugins/login.d.ts +3 -0
  29. package/dist/plugins/login.js +10 -0
  30. package/dist/plugins/oidc/login.d.ts +33 -2
  31. package/dist/plugins/oidc/login.js +100 -60
  32. package/dist/plugins/okta/aws.d.ts +1 -1
  33. package/dist/plugins/okta/aws.js +2 -2
  34. package/dist/plugins/okta/login.js +11 -1
  35. package/dist/plugins/ping/login.d.ts +2 -1
  36. package/dist/plugins/ping/login.js +11 -1
  37. package/dist/plugins/{aws/ssm → ssh}/index.d.ts +3 -2
  38. package/dist/plugins/ssh/index.js +311 -0
  39. package/dist/plugins/ssh/types.d.ts +4 -0
  40. package/dist/plugins/ssh-agent/index.js +5 -39
  41. package/dist/types/aws/oidc.d.ts +36 -0
  42. package/dist/types/aws/oidc.js +12 -0
  43. package/dist/types/oidc.d.ts +21 -0
  44. package/dist/types/request.d.ts +9 -11
  45. package/dist/types/request.js +0 -10
  46. package/dist/types/ssh.d.ts +27 -0
  47. package/dist/types/ssh.js +5 -0
  48. package/package.json +1 -1
  49. package/dist/plugins/aws/ssm/index.js +0 -213
@@ -25,20 +25,20 @@ You should have received a copy of the GNU General Public License along with @p0
25
25
  const keys_1 = require("../../common/__mocks__/keys");
26
26
  const api_1 = require("../../drivers/api");
27
27
  const stdio_1 = require("../../drivers/stdio");
28
- const ssm_1 = require("../../plugins/aws/ssm");
28
+ const ssh_1 = require("../../plugins/ssh");
29
29
  const firestore_1 = require("../../testing/firestore");
30
30
  const util_1 = require("../../util");
31
- const ssh_1 = require("../ssh");
31
+ const ssh_2 = require("../ssh");
32
32
  const firestore_2 = require("firebase/firestore");
33
33
  const lodash_1 = require("lodash");
34
34
  const yargs_1 = __importDefault(require("yargs"));
35
35
  jest.mock("../../drivers/api");
36
36
  jest.mock("../../drivers/auth");
37
37
  jest.mock("../../drivers/stdio");
38
- jest.mock("../../plugins/aws/ssm");
38
+ jest.mock("../../plugins/ssh");
39
39
  jest.mock("../../common/keys");
40
40
  const mockFetchCommand = api_1.fetchCommand;
41
- const mockSshOrScp = ssm_1.sshOrScp;
41
+ const mockSshOrScp = ssh_1.sshOrScp;
42
42
  const mockPrint1 = stdio_1.print1;
43
43
  const mockPrint2 = stdio_1.print2;
44
44
  const MOCK_REQUEST = {
@@ -47,7 +47,6 @@ const MOCK_REQUEST = {
47
47
  name: "name",
48
48
  ssh: {
49
49
  linuxUserName: "linuxUserName",
50
- publicKey: keys_1.TEST_PUBLIC_KEY,
51
50
  },
52
51
  },
53
52
  permission: {
@@ -55,6 +54,8 @@ const MOCK_REQUEST = {
55
54
  instanceId: "instanceId",
56
55
  accountId: "accountId",
57
56
  region: "region",
57
+ publicKey: keys_1.TEST_PUBLIC_KEY,
58
+ type: "aws",
58
59
  },
59
60
  },
60
61
  };
@@ -91,19 +92,19 @@ describe("ssh", () => {
91
92
  });
92
93
  });
93
94
  it("should call p0 request with reason arg", () => __awaiter(void 0, void 0, void 0, function* () {
94
- void (0, ssh_1.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance --reason reason`);
95
+ void (0, ssh_2.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance --reason reason --provider aws`);
95
96
  yield (0, util_1.sleep)(100);
96
97
  const hiddenFilenameRequestArgs = (0, lodash_1.omit)(mockFetchCommand.mock.calls[0][1], "$0");
97
98
  expect(hiddenFilenameRequestArgs).toMatchSnapshot("args");
98
99
  }));
99
100
  it("should wait for access grant", () => __awaiter(void 0, void 0, void 0, function* () {
100
- const promise = (0, ssh_1.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
101
+ const promise = (0, ssh_2.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
101
102
  const wait = (0, util_1.sleep)(100);
102
103
  yield Promise.race([wait, promise]);
103
104
  yield expect(wait).resolves.toBeUndefined();
104
105
  }));
105
106
  it("should wait for provisioning", () => __awaiter(void 0, void 0, void 0, function* () {
106
- const promise = (0, ssh_1.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
107
+ const promise = (0, ssh_2.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
107
108
  yield (0, util_1.sleep)(100); // Need to wait for listen before trigger in tests
108
109
  firestore_2.onSnapshot.trigger({
109
110
  status: "APPROVED",
@@ -113,7 +114,7 @@ describe("ssh", () => {
113
114
  yield expect(wait).resolves.toBeUndefined();
114
115
  }));
115
116
  it("should call sshOrScp with non-interactive command", () => __awaiter(void 0, void 0, void 0, function* () {
116
- const promise = (0, ssh_1.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance do something`);
117
+ const promise = (0, ssh_2.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance do something`);
117
118
  yield (0, util_1.sleep)(100); // Need to wait for listen before trigger in tests
118
119
  firestore_2.onSnapshot.trigger({
119
120
  status: "APPROVED",
@@ -126,7 +127,7 @@ describe("ssh", () => {
126
127
  expect(mockSshOrScp).toHaveBeenCalled();
127
128
  }));
128
129
  it("should call sshOrScp with interactive session", () => __awaiter(void 0, void 0, void 0, function* () {
129
- const promise = (0, ssh_1.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
130
+ const promise = (0, ssh_2.sshCommand)((0, yargs_1.default)()).parse(`ssh some-instance`);
130
131
  yield (0, util_1.sleep)(100); // Need to wait for listen before trigger in tests
131
132
  firestore_2.onSnapshot.trigger({
132
133
  status: "APPROVED",
@@ -1,3 +1,3 @@
1
- import { ScpCommandArgs } from "./shared";
1
+ import { ScpCommandArgs } from "./shared/ssh";
2
2
  import yargs from "yargs";
3
3
  export declare const scpCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<ScpCommandArgs>;
@@ -22,8 +22,9 @@ You should have received a copy of the GNU General Public License along with @p0
22
22
  **/
23
23
  const auth_1 = require("../drivers/auth");
24
24
  const firestore_1 = require("../drivers/firestore");
25
- const ssm_1 = require("../plugins/aws/ssm");
26
- const shared_1 = require("./shared");
25
+ const ssh_1 = require("../plugins/ssh");
26
+ const ssh_2 = require("../types/ssh");
27
+ const ssh_3 = require("./shared/ssh");
27
28
  const scpCommand = (yargs) => yargs.command("scp <source> <destination>",
28
29
  // TODO (ENG-1930): support scp across multiple remote hosts.
29
30
  "SCP copies files between a local and remote host.", (yargs) => yargs
@@ -49,6 +50,11 @@ const scpCommand = (yargs) => yargs.command("scp <source> <destination>",
49
50
  .option("account", {
50
51
  type: "string",
51
52
  describe: "The account on which the instance is located",
53
+ })
54
+ .option("provider", {
55
+ type: "string",
56
+ describe: "The cloud provider where the instance is hosted",
57
+ choices: ssh_2.SupportedSshProviders,
52
58
  })
53
59
  .option("sudo", {
54
60
  type: "boolean",
@@ -69,15 +75,15 @@ const scpAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
69
75
  if (!host) {
70
76
  throw "Could not determine host identifier from source or destination";
71
77
  }
72
- const result = yield (0, shared_1.provisionRequest)(authn, args, host);
78
+ const result = yield (0, ssh_3.provisionRequest)(authn, args, host);
73
79
  if (!result) {
74
80
  throw "Server did not return a request id. Please contact support@p0.dev for assistance.";
75
81
  }
76
82
  const { request, privateKey } = result;
77
- const data = (0, shared_1.requestToSsh)(request);
83
+ const data = (0, ssh_3.requestToSsh)(request);
78
84
  // replace the host with the linuxUserName@instanceId
79
85
  const { source, destination } = replaceHostWithInstance(data, args);
80
- yield (0, ssm_1.sshOrScp)(authn, data, Object.assign(Object.assign({}, args), { source,
86
+ yield (0, ssh_1.sshOrScp)(authn, data, Object.assign(Object.assign({}, args), { source,
81
87
  destination }), privateKey);
82
88
  });
83
89
  /** If a path is not explicitly local, use this pattern to determine if it's remote */
@@ -0,0 +1,4 @@
1
+ import { Authn } from "../../types/identity";
2
+ import { Request } from "../../types/request";
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>>;
@@ -0,0 +1,67 @@
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.waitForProvisioning = void 0;
13
+ /** Copyright © 2024-present P0 Security
14
+
15
+ This file is part of @p0security/cli
16
+
17
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
18
+
19
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
22
+ **/
23
+ const firestore_1 = require("../../drivers/firestore");
24
+ const request_1 = require("../../types/request");
25
+ const firestore_2 = require("firebase/firestore");
26
+ /** Maximum amount of time to wait after access is approved to wait for access
27
+ * to be configured
28
+ */
29
+ const GRANT_TIMEOUT_MILLIS = 60e3;
30
+ /** Waits until P0 grants access for a request */
31
+ const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void 0, function* () {
32
+ let cancel = undefined;
33
+ const result = yield new Promise((resolve, reject) => {
34
+ let isResolved = false;
35
+ const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/permission-requests/${requestId}`), (snap) => {
36
+ const data = snap.data();
37
+ if (!data)
38
+ return;
39
+ if (request_1.DONE_STATUSES.includes(data.status)) {
40
+ resolve(data);
41
+ }
42
+ else if (request_1.DENIED_STATUSES.includes(data.status)) {
43
+ reject("Your access request was denied");
44
+ }
45
+ else if (request_1.ERROR_STATUSES.includes(data.status)) {
46
+ reject("Your access request encountered an error (see Slack for details)");
47
+ }
48
+ else {
49
+ return;
50
+ }
51
+ isResolved = true;
52
+ unsubscribe();
53
+ });
54
+ // Skip timeout in test; it holds a ref longer than the test lasts
55
+ if (process.env.NODE_ENV === "test")
56
+ return;
57
+ cancel = setTimeout(() => {
58
+ if (!isResolved) {
59
+ unsubscribe();
60
+ reject("Timeout awaiting SSH access grant");
61
+ }
62
+ }, GRANT_TIMEOUT_MILLIS);
63
+ });
64
+ clearTimeout(cancel);
65
+ return result;
66
+ });
67
+ exports.waitForProvisioning = waitForProvisioning;
@@ -1,24 +1,18 @@
1
- import { AwsSsh } from "../plugins/aws/types";
2
- import { Authn } from "../types/identity";
3
- import { Request } from "../types/request";
1
+ import { Authn } from "../../types/identity";
2
+ import { Request } from "../../types/request";
3
+ import { CliSshRequest, SshRequest, SupportedSshProvider } from "../../types/ssh";
4
4
  import yargs from "yargs";
5
- export declare type SshRequest = {
6
- linuxUserName: string;
7
- role: string;
8
- accountId: string;
9
- region: string;
10
- id: string;
11
- };
12
5
  export declare type BaseSshCommandArgs = {
13
6
  sudo?: boolean;
14
7
  reason?: string;
15
8
  account?: string;
9
+ provider?: SupportedSshProvider;
10
+ debug?: boolean;
16
11
  };
17
12
  export declare type ScpCommandArgs = BaseSshCommandArgs & {
18
13
  source: string;
19
14
  destination: string;
20
15
  recursive?: boolean;
21
- debug?: boolean;
22
16
  };
23
17
  export declare type SshCommandArgs = BaseSshCommandArgs & {
24
18
  sudo?: boolean;
@@ -28,11 +22,10 @@ export declare type SshCommandArgs = BaseSshCommandArgs & {
28
22
  A?: boolean;
29
23
  arguments: string[];
30
24
  command?: string;
31
- debug?: boolean;
32
25
  };
33
26
  export declare const provisionRequest: (authn: Authn, args: yargs.ArgumentsCamelCase<BaseSshCommandArgs>, destination: string) => Promise<{
34
- request: Request<AwsSsh>;
27
+ request: Request<CliSshRequest>;
35
28
  publicKey: string;
36
29
  privateKey: string;
37
30
  } | undefined>;
38
- export declare const requestToSsh: (request: AwsSsh) => SshRequest;
31
+ export declare const requestToSsh: (request: Request<CliSshRequest>) => SshRequest;
@@ -20,75 +20,46 @@ This file is part of @p0security/cli
20
20
 
21
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
22
  **/
23
- const keys_1 = require("../common/keys");
24
- const firestore_1 = require("../drivers/firestore");
25
- const stdio_1 = require("../drivers/stdio");
26
- const request_1 = require("../types/request");
27
- const request_2 = require("./request");
23
+ const _1 = require(".");
24
+ const keys_1 = require("../../common/keys");
25
+ const firestore_1 = require("../../drivers/firestore");
26
+ const stdio_1 = require("../../drivers/stdio");
27
+ const ssh_1 = require("../../plugins/aws/ssh");
28
+ const ssh_2 = require("../../plugins/google/ssh");
29
+ const ssh_3 = require("../../types/ssh");
30
+ const request_1 = require("../request");
28
31
  const firestore_2 = require("firebase/firestore");
29
32
  const lodash_1 = require("lodash");
30
- /** Maximum amount of time to wait after access is approved to wait for access
31
- * to be configured
32
- */
33
- const GRANT_TIMEOUT_MILLIS = 60e3;
34
- const validateSshInstall = (authn) => __awaiter(void 0, void 0, void 0, function* () {
33
+ const SSH_PROVIDERS = {
34
+ aws: ssh_1.awsSshProvider,
35
+ gcloud: ssh_2.gcpSshProvider,
36
+ };
37
+ const validateSshInstall = (authn, args) => __awaiter(void 0, void 0, void 0, function* () {
35
38
  var _a;
36
39
  const configDoc = yield (0, firestore_2.getDoc)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/integrations/ssh`));
37
40
  const configItems = (_a = configDoc.data()) === null || _a === void 0 ? void 0 : _a["iam-write"];
38
- const items = Object.entries(configItems !== null && configItems !== void 0 ? configItems : {}).filter(([key, value]) => value.state == "installed" && key.startsWith("aws"));
41
+ const providersToCheck = args.provider
42
+ ? [args.provider]
43
+ : ssh_3.SupportedSshProviders;
44
+ const items = Object.entries(configItems !== null && configItems !== void 0 ? configItems : {}).filter(([key, value]) => value.state == "installed" &&
45
+ providersToCheck.some((prefix) => key.startsWith(prefix)));
39
46
  if (items.length === 0) {
40
47
  throw "This organization is not configured for SSH access via the P0 CLI";
41
48
  }
42
49
  });
43
- /** Waits until P0 grants access for a request */
44
- const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void 0, function* () {
45
- let cancel = undefined;
46
- const result = yield new Promise((resolve, reject) => {
47
- let isResolved = false;
48
- const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${authn.identity.org.tenantId}/permission-requests/${requestId}`), (snap) => {
49
- const data = snap.data();
50
- if (!data)
51
- return;
52
- if (request_1.DONE_STATUSES.includes(data.status)) {
53
- resolve(data);
54
- }
55
- else if (request_1.DENIED_STATUSES.includes(data.status)) {
56
- reject("Your access request was denied");
57
- }
58
- else if (request_1.ERROR_STATUSES.includes(data.status)) {
59
- reject("Your access request encountered an error (see Slack for details)");
60
- }
61
- else {
62
- return;
63
- }
64
- isResolved = true;
65
- unsubscribe();
66
- });
67
- // Skip timeout in test; it holds a ref longer than the test lasts
68
- if (process.env.NODE_ENV === "test")
69
- return;
70
- cancel = setTimeout(() => {
71
- if (!isResolved) {
72
- unsubscribe();
73
- reject("Timeout awaiting SSH access grant");
74
- }
75
- }, GRANT_TIMEOUT_MILLIS);
76
- });
77
- clearTimeout(cancel);
78
- return result;
50
+ const pluginToCliRequest = (request, options) => __awaiter(void 0, void 0, void 0, function* () {
51
+ return yield SSH_PROVIDERS[request.permission.spec.type].toCliRequest(request, options);
79
52
  });
80
53
  const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0, void 0, function* () {
81
- yield validateSshInstall(authn);
54
+ yield validateSshInstall(authn, args);
82
55
  const { publicKey, privateKey } = yield (0, keys_1.createKeyPair)();
83
- const response = yield (0, request_2.request)(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
56
+ const response = yield (0, request_1.request)(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
84
57
  "ssh",
85
58
  "session",
86
59
  destination,
87
60
  "--public-key",
88
61
  publicKey,
89
- // Prefix is required because the backend uses it to determine that this is an AWS request
90
- "--provider",
91
- "aws",
62
+ ...(args.provider ? ["--provider", args.provider] : []),
92
63
  ...(args.sudo || args.command === "sudo" ? ["--sudo"] : []),
93
64
  ...(args.reason ? ["--reason", args.reason] : []),
94
65
  ...(args.account ? ["--account", args.account] : []),
@@ -100,20 +71,15 @@ const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0,
100
71
  const { id, isPreexisting } = response;
101
72
  if (!isPreexisting)
102
73
  (0, stdio_1.print2)("Waiting for access to be provisioned");
103
- const provisionedRequest = yield waitForProvisioning(authn, id);
104
- if (provisionedRequest.generated.ssh.publicKey !== publicKey) {
74
+ const provisionedRequest = yield (0, _1.waitForProvisioning)(authn, id);
75
+ if (provisionedRequest.permission.spec.publicKey !== publicKey) {
105
76
  throw "Public key mismatch. Please revoke the request and try again.";
106
77
  }
107
- return { request: provisionedRequest, publicKey, privateKey };
78
+ const cliRequest = yield pluginToCliRequest(provisionedRequest, {
79
+ debug: args.debug,
80
+ });
81
+ return { request: cliRequest, publicKey, privateKey };
108
82
  });
109
83
  exports.provisionRequest = provisionRequest;
110
- const requestToSsh = (request) => {
111
- return {
112
- id: request.permission.spec.instanceId,
113
- accountId: request.permission.spec.accountId,
114
- region: request.permission.spec.region,
115
- role: request.generated.name,
116
- linuxUserName: request.generated.ssh.linuxUserName,
117
- };
118
- };
84
+ const requestToSsh = (request) => SSH_PROVIDERS[request.permission.spec.type].requestToSsh(request);
119
85
  exports.requestToSsh = requestToSsh;
@@ -1,3 +1,3 @@
1
- import { SshCommandArgs } from "./shared";
1
+ import { SshCommandArgs } from "./shared/ssh";
2
2
  import yargs from "yargs";
3
3
  export declare const sshCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<SshCommandArgs>;
@@ -22,8 +22,8 @@ You should have received a copy of the GNU General Public License along with @p0
22
22
  **/
23
23
  const auth_1 = require("../drivers/auth");
24
24
  const firestore_1 = require("../drivers/firestore");
25
- const ssm_1 = require("../plugins/aws/ssm");
26
- const shared_1 = require("./shared");
25
+ const ssh_1 = require("../plugins/ssh");
26
+ const ssh_2 = require("./shared/ssh");
27
27
  const sshCommand = (yargs) => yargs.command("ssh <destination> [command [arguments..]]", "SSH into a virtual machine", (yargs) => yargs
28
28
  .positional("destination", {
29
29
  type: "string",
@@ -64,6 +64,11 @@ const sshCommand = (yargs) => yargs.command("ssh <destination> [command [argumen
64
64
  .option("account", {
65
65
  type: "string",
66
66
  describe: "The account on which the instance is located",
67
+ })
68
+ .option("provider", {
69
+ type: "string",
70
+ describe: "The cloud provider where the instance is hosted",
71
+ choices: ["aws", "gcloud"],
67
72
  })
68
73
  .option("debug", {
69
74
  type: "boolean",
@@ -81,9 +86,10 @@ const sshAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
81
86
  // Prefix is required because the backend uses it to determine that this is an AWS request
82
87
  const authn = yield (0, auth_1.authenticate)();
83
88
  const destination = args.destination;
84
- const result = yield (0, shared_1.provisionRequest)(authn, args, destination);
89
+ const result = yield (0, ssh_2.provisionRequest)(authn, args, destination);
85
90
  if (!result) {
86
91
  throw "Server did not return a request id. Please contact support@p0.dev for assistance.";
87
92
  }
88
- yield (0, ssm_1.sshOrScp)(authn, (0, shared_1.requestToSsh)(result.request), Object.assign(Object.assign({}, args), { destination }), result.privateKey);
93
+ const { request, privateKey } = result;
94
+ yield (0, ssh_1.sshOrScp)(authn, (0, ssh_2.requestToSsh)(request), Object.assign(Object.assign({}, args), { destination }), privateKey);
89
95
  });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Retries an operation with a delay between retries
3
+ * @param operation operation to retry
4
+ * @param shouldRetry predicate to evaluate on error; will retry only if this is true
5
+ * @param retries number of retries
6
+ * @param delay time to wait before retrying
7
+ * @returns
8
+ */
9
+ export declare function retryWithSleep<T>(operation: () => Promise<T>, shouldRetry: (error: unknown) => boolean, retries?: number, delayMs?: number): Promise<T>;
@@ -0,0 +1,50 @@
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.retryWithSleep = 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 util_1 = require("../util");
24
+ const MAX_RETRIES = 3;
25
+ const MAX_RETRY_BACK_OFF_TIME = 10000;
26
+ /**
27
+ * Retries an operation with a delay between retries
28
+ * @param operation operation to retry
29
+ * @param shouldRetry predicate to evaluate on error; will retry only if this is true
30
+ * @param retries number of retries
31
+ * @param delay time to wait before retrying
32
+ * @returns
33
+ */
34
+ function retryWithSleep(operation, shouldRetry, retries = MAX_RETRIES, delayMs = MAX_RETRY_BACK_OFF_TIME) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ try {
37
+ return yield operation();
38
+ }
39
+ catch (error) {
40
+ if (shouldRetry(error)) {
41
+ if (retries > 0) {
42
+ yield (0, util_1.sleep)(delayMs);
43
+ return yield retryWithSleep(operation, shouldRetry, retries - 1);
44
+ }
45
+ }
46
+ throw error;
47
+ }
48
+ });
49
+ }
50
+ exports.retryWithSleep = retryWithSleep;
@@ -0,0 +1,10 @@
1
+ import { AgentArgs } from "../plugins/ssh-agent/types";
2
+ import { SpawnOptionsWithoutStdio } from "node:child_process";
3
+ /** Spawns a subprocess with given command, args, and options.
4
+ * May write content to its standard input.
5
+ * Stdout and stderr of the subprocess is printed to stderr in debug mode.
6
+ * The returned promise resolves with stdout or rejects with stderr of the subprocess.
7
+ *
8
+ * The captured output is expected to be relatively small.
9
+ * For larger outputs we should implement this with streams. */
10
+ export declare const asyncSpawn: ({ debug }: AgentArgs, command: string, args?: ReadonlyArray<string>, options?: SpawnOptionsWithoutStdio, writeStdin?: string) => Promise<string>;
@@ -0,0 +1,72 @@
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.asyncSpawn = 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 stdio_1 = require("../drivers/stdio");
24
+ const node_child_process_1 = require("node:child_process");
25
+ /** Spawns a subprocess with given command, args, and options.
26
+ * May write content to its standard input.
27
+ * Stdout and stderr of the subprocess is printed to stderr in debug mode.
28
+ * The returned promise resolves with stdout or rejects with stderr of the subprocess.
29
+ *
30
+ * The captured output is expected to be relatively small.
31
+ * For larger outputs we should implement this with streams. */
32
+ const asyncSpawn = ({ debug }, command, args, options, writeStdin) => __awaiter(void 0, void 0, void 0, function* () {
33
+ return new Promise((resolve, reject) => {
34
+ var _a;
35
+ const child = (0, node_child_process_1.spawn)(command, args, options);
36
+ // Use streams for larger output
37
+ let stdout = "";
38
+ let stderr = "";
39
+ if (writeStdin) {
40
+ if (!child.stdin)
41
+ return reject("Child process has no stdin");
42
+ child.stdin.write(writeStdin);
43
+ }
44
+ child.stdout.on("data", (data) => {
45
+ const str = data.toString("utf-8");
46
+ stdout += str;
47
+ if (debug) {
48
+ (0, stdio_1.print2)(str);
49
+ }
50
+ });
51
+ child.stderr.on("data", (data) => {
52
+ const str = data.toString("utf-8");
53
+ stderr += str;
54
+ if (debug) {
55
+ (0, stdio_1.print2)(data.toString("utf-8"));
56
+ }
57
+ });
58
+ child.on("exit", (code) => {
59
+ if (debug) {
60
+ (0, stdio_1.print2)("Process exited with code " + code);
61
+ }
62
+ if (code !== 0) {
63
+ return reject(stderr);
64
+ }
65
+ resolve(stdout);
66
+ });
67
+ if (writeStdin) {
68
+ (_a = child.stdin) === null || _a === void 0 ? void 0 : _a.end();
69
+ }
70
+ });
71
+ });
72
+ exports.asyncSpawn = asyncSpawn;
@@ -2,7 +2,7 @@ import { Authn, Identity } from "../types/identity";
2
2
  export declare const IDENTITY_FILE_PATH: string;
3
3
  export declare const cached: <T>(name: string, loader: () => Promise<T>, options: {
4
4
  duration: number;
5
- }) => Promise<T>;
5
+ }, hasExpired?: ((data: T) => boolean) | undefined) => Promise<T>;
6
6
  export declare const loadCredentials: (options?: {
7
7
  noRefresh?: boolean;
8
8
  }) => Promise<Identity>;
@@ -51,7 +51,7 @@ const auth_1 = require("firebase/auth");
51
51
  const fs = __importStar(require("fs/promises"));
52
52
  const path = __importStar(require("path"));
53
53
  exports.IDENTITY_FILE_PATH = path.join(util_1.P0_PATH, "identity.json");
54
- const cached = (name, loader, options) => __awaiter(void 0, void 0, void 0, function* () {
54
+ const cached = (name, loader, options, hasExpired) => __awaiter(void 0, void 0, void 0, function* () {
55
55
  var _a;
56
56
  const cachePath = path.join(path.dirname(exports.IDENTITY_FILE_PATH), "cache");
57
57
  // Following lines sanitize input
@@ -74,8 +74,12 @@ const cached = (name, loader, options) => __awaiter(void 0, void 0, void 0, func
74
74
  yield fs.rm(loc);
75
75
  return yield loadCache();
76
76
  }
77
- const data = yield fs.readFile(loc);
78
- return JSON.parse(data.toString("utf-8"));
77
+ const data = JSON.parse((yield fs.readFile(loc)).toString("utf-8"));
78
+ if (hasExpired === null || hasExpired === void 0 ? void 0 : hasExpired(data)) {
79
+ yield fs.rm(loc);
80
+ return yield loadCache();
81
+ }
82
+ return data;
79
83
  }
80
84
  catch (error) {
81
85
  if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT")
@@ -4,7 +4,7 @@ export declare const getAwsConfig: (authn: Authn, account: string | undefined) =
4
4
  config: {
5
5
  label?: string | undefined;
6
6
  state: string;
7
- login?: (import("./types").AwsIamLogin | import("./types").AwsIdcLogin | import("./types").AwsFederatedLogin) | undefined;
7
+ login?: import("./types").AwsLogin | undefined;
8
8
  id: string;
9
9
  };
10
10
  }>;
@@ -0,0 +1,16 @@
1
+ import { AWSClientInformation } from "../../../types/aws/oidc";
2
+ import { AwsCredentials } from "../types";
3
+ export declare const registerClient: (region: string) => Promise<AWSClientInformation>;
4
+ /**
5
+ * Returns AWS credentials for the specified account and permission set for the authorized user
6
+ * @param args accountId, permissionSet and idc to assume role associated with the permission set
7
+ * @returns
8
+ */
9
+ export declare const assumeRoleWithIdc: (args: {
10
+ accountId?: string;
11
+ permissionSet: string;
12
+ idc: {
13
+ id: string;
14
+ region: string;
15
+ };
16
+ }) => Promise<AwsCredentials>;