@p0security/cli 0.7.1 → 0.8.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.
@@ -22,6 +22,7 @@ This file is part of @p0security/cli
22
22
 
23
23
  You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
24
24
  **/
25
+ const keys_1 = require("../../common/__mocks__/keys");
25
26
  const api_1 = require("../../drivers/api");
26
27
  const stdio_1 = require("../../drivers/stdio");
27
28
  const ssm_1 = require("../../plugins/aws/ssm");
@@ -35,10 +36,28 @@ jest.mock("../../drivers/api");
35
36
  jest.mock("../../drivers/auth");
36
37
  jest.mock("../../drivers/stdio");
37
38
  jest.mock("../../plugins/aws/ssm");
39
+ jest.mock("../../common/keys");
38
40
  const mockFetchCommand = api_1.fetchCommand;
39
41
  const mockSshOrScp = ssm_1.sshOrScp;
40
42
  const mockPrint1 = stdio_1.print1;
41
43
  const mockPrint2 = stdio_1.print2;
44
+ const MOCK_REQUEST = {
45
+ status: "DONE",
46
+ generated: {
47
+ name: "name",
48
+ ssh: {
49
+ linuxUserName: "linuxUserName",
50
+ publicKey: keys_1.TEST_PUBLIC_KEY,
51
+ },
52
+ },
53
+ permission: {
54
+ spec: {
55
+ instanceId: "instanceId",
56
+ accountId: "accountId",
57
+ region: "region",
58
+ },
59
+ },
60
+ };
42
61
  (0, firestore_1.mockGetDoc)({
43
62
  "iam-write": {
44
63
  ["aws:test-account"]: {
@@ -68,9 +87,6 @@ describe("ssh", () => {
68
87
  },
69
88
  },
70
89
  },
71
- generated: {
72
- documentName: "documentName",
73
- },
74
90
  },
75
91
  });
76
92
  });
@@ -103,9 +119,7 @@ describe("ssh", () => {
103
119
  status: "APPROVED",
104
120
  });
105
121
  yield (0, util_1.sleep)(100); // Need to wait for listen before trigger in tests
106
- firestore_2.onSnapshot.trigger({
107
- status: "DONE",
108
- });
122
+ firestore_2.onSnapshot.trigger(MOCK_REQUEST);
109
123
  yield expect(promise).resolves.toBeDefined();
110
124
  expect(mockPrint2.mock.calls).toMatchSnapshot("stderr");
111
125
  expect(mockPrint1).not.toHaveBeenCalled();
@@ -118,9 +132,7 @@ describe("ssh", () => {
118
132
  status: "APPROVED",
119
133
  });
120
134
  yield (0, util_1.sleep)(100); // Need to wait for listen before trigger in tests
121
- firestore_2.onSnapshot.trigger({
122
- status: "DONE",
123
- });
135
+ firestore_2.onSnapshot.trigger(MOCK_REQUEST);
124
136
  yield expect(promise).resolves.toBeDefined();
125
137
  expect(mockPrint2.mock.calls).toMatchSnapshot("stderr");
126
138
  expect(mockPrint1).not.toHaveBeenCalled();
@@ -20,7 +20,6 @@ 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 api_1 = require("../drivers/api");
24
23
  const auth_1 = require("../drivers/auth");
25
24
  const firestore_1 = require("../drivers/firestore");
26
25
  const ssm_1 = require("../plugins/aws/ssm");
@@ -70,19 +69,15 @@ const scpAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
70
69
  if (!host) {
71
70
  throw "Could not determine host identifier from source or destination";
72
71
  }
73
- const requestId = yield (0, shared_1.provisionRequest)(authn, args, host);
74
- if (!requestId) {
72
+ const result = yield (0, shared_1.provisionRequest)(authn, args, host);
73
+ if (!result) {
75
74
  throw "Server did not return a request id. Please contact support@p0.dev for assistance.";
76
75
  }
77
- const { publicKey, privateKey } = (0, shared_1.createKeyPair)();
78
- const result = yield (0, api_1.fetchExerciseGrant)(authn, {
79
- requestId,
80
- destination: host,
81
- publicKey,
82
- });
76
+ const { request, privateKey } = result;
77
+ const data = (0, shared_1.requestToSsh)(request);
83
78
  // replace the host with the linuxUserName@instanceId
84
- const { source, destination } = replaceHostWithInstance(result, args);
85
- yield (0, ssm_1.sshOrScp)(authn, result, Object.assign(Object.assign({}, args), { source,
79
+ const { source, destination } = replaceHostWithInstance(data, args);
80
+ yield (0, ssm_1.sshOrScp)(authn, data, Object.assign(Object.assign({}, args), { source,
86
81
  destination }), privateKey);
87
82
  });
88
83
  /** If a path is not explicitly local, use this pattern to determine if it's remote */
@@ -106,10 +101,10 @@ const replaceHostWithInstance = (result, args) => {
106
101
  let source = args.source;
107
102
  let destination = args.destination;
108
103
  if (isExplicitlyRemote(source)) {
109
- source = `${result.linuxUserName}@${result.instance.id}:${source.split(":")[1]}`;
104
+ source = `${result.linuxUserName}@${result.id}:${source.split(":")[1]}`;
110
105
  }
111
106
  if (isExplicitlyRemote(destination)) {
112
- destination = `${result.linuxUserName}@${result.instance.id}:${destination.split(":")[1]}`;
107
+ destination = `${result.linuxUserName}@${result.id}:${destination.split(":")[1]}`;
113
108
  }
114
109
  return { source, destination };
115
110
  };
@@ -1,17 +1,13 @@
1
+ import { AwsSsh } from "../plugins/aws/types";
1
2
  import { Authn } from "../types/identity";
3
+ import { Request } from "../types/request";
2
4
  import yargs from "yargs";
3
- export declare type ExerciseGrantResponse = {
4
- documentName: string;
5
+ export declare type SshRequest = {
5
6
  linuxUserName: string;
6
- ok: true;
7
7
  role: string;
8
- instance: {
9
- arn: string;
10
- accountId: string;
11
- region: string;
12
- id: string;
13
- name?: string;
14
- };
8
+ accountId: string;
9
+ region: string;
10
+ id: string;
15
11
  };
16
12
  export declare type BaseSshCommandArgs = {
17
13
  sudo?: boolean;
@@ -34,8 +30,9 @@ export declare type SshCommandArgs = BaseSshCommandArgs & {
34
30
  command?: string;
35
31
  debug?: boolean;
36
32
  };
37
- export declare const provisionRequest: (authn: Authn, args: yargs.ArgumentsCamelCase<BaseSshCommandArgs>, destination: string) => Promise<string | undefined>;
38
- export declare const createKeyPair: () => {
33
+ export declare const provisionRequest: (authn: Authn, args: yargs.ArgumentsCamelCase<BaseSshCommandArgs>, destination: string) => Promise<{
34
+ request: Request<AwsSsh>;
39
35
  publicKey: string;
40
36
  privateKey: string;
41
- };
37
+ } | undefined>;
38
+ export declare const requestToSsh: (request: AwsSsh) => SshRequest;
@@ -8,11 +8,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.createKeyPair = exports.provisionRequest = void 0;
12
+ exports.requestToSsh = exports.provisionRequest = void 0;
16
13
  /** Copyright © 2024-present P0 Security
17
14
 
18
15
  This file is part of @p0security/cli
@@ -23,13 +20,13 @@ This file is part of @p0security/cli
23
20
 
24
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/>.
25
22
  **/
23
+ const keys_1 = require("../common/keys");
26
24
  const firestore_1 = require("../drivers/firestore");
27
25
  const stdio_1 = require("../drivers/stdio");
28
26
  const request_1 = require("../types/request");
29
27
  const request_2 = require("./request");
30
28
  const firestore_2 = require("firebase/firestore");
31
29
  const lodash_1 = require("lodash");
32
- const node_forge_1 = __importDefault(require("node-forge"));
33
30
  /** Maximum amount of time to wait after access is approved to wait for access
34
31
  * to be configured
35
32
  */
@@ -82,10 +79,13 @@ const waitForProvisioning = (authn, requestId) => __awaiter(void 0, void 0, void
82
79
  });
83
80
  const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0, void 0, function* () {
84
81
  yield validateSshInstall(authn);
82
+ const { publicKey, privateKey } = yield (0, keys_1.createKeyPair)();
85
83
  const response = yield (0, request_2.request)(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
86
84
  "ssh",
87
85
  "session",
88
86
  destination,
87
+ "--public-key",
88
+ publicKey,
89
89
  // Prefix is required because the backend uses it to determine that this is an AWS request
90
90
  "--provider",
91
91
  "aws",
@@ -100,14 +100,20 @@ const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0,
100
100
  const { id, isPreexisting } = response;
101
101
  if (!isPreexisting)
102
102
  (0, stdio_1.print2)("Waiting for access to be provisioned");
103
- yield waitForProvisioning(authn, id);
104
- return id;
103
+ const provisionedRequest = yield waitForProvisioning(authn, id);
104
+ if (provisionedRequest.generated.ssh.publicKey !== publicKey) {
105
+ throw "Public key mismatch. Please revoke the request and try again.";
106
+ }
107
+ return { request: provisionedRequest, publicKey, privateKey };
105
108
  });
106
109
  exports.provisionRequest = provisionRequest;
107
- const createKeyPair = () => {
108
- const rsaKeyPair = node_forge_1.default.pki.rsa.generateKeyPair({ bits: 2048 });
109
- const privateKey = node_forge_1.default.pki.privateKeyToPem(rsaKeyPair.privateKey);
110
- const publicKey = node_forge_1.default.ssh.publicKeyToOpenSSH(rsaKeyPair.publicKey);
111
- return { publicKey, privateKey };
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
+ };
112
118
  };
113
- exports.createKeyPair = createKeyPair;
119
+ exports.requestToSsh = requestToSsh;
@@ -20,7 +20,6 @@ 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 api_1 = require("../drivers/api");
24
23
  const auth_1 = require("../drivers/auth");
25
24
  const firestore_1 = require("../drivers/firestore");
26
25
  const ssm_1 = require("../plugins/aws/ssm");
@@ -82,15 +81,9 @@ const sshAction = (args) => __awaiter(void 0, void 0, void 0, function* () {
82
81
  // Prefix is required because the backend uses it to determine that this is an AWS request
83
82
  const authn = yield (0, auth_1.authenticate)();
84
83
  const destination = args.destination;
85
- const requestId = yield (0, shared_1.provisionRequest)(authn, args, destination);
86
- if (!requestId) {
84
+ const result = yield (0, shared_1.provisionRequest)(authn, args, destination);
85
+ if (!result) {
87
86
  throw "Server did not return a request id. Please contact support@p0.dev for assistance.";
88
87
  }
89
- const { publicKey, privateKey } = (0, shared_1.createKeyPair)();
90
- const result = yield (0, api_1.fetchExerciseGrant)(authn, {
91
- requestId,
92
- destination,
93
- publicKey,
94
- });
95
- yield (0, ssm_1.sshOrScp)(authn, result, Object.assign(Object.assign({}, args), { destination }), privateKey);
88
+ yield (0, ssm_1.sshOrScp)(authn, (0, shared_1.requestToSsh)(result.request), Object.assign(Object.assign({}, args), { destination }), result.privateKey);
96
89
  });
@@ -0,0 +1,13 @@
1
+ /// <reference types="jest" />
2
+ /** Copyright © 2024-present P0 Security
3
+
4
+ This file is part of @p0security/cli
5
+
6
+ @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.
7
+
8
+ @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.
9
+
10
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
11
+ **/
12
+ export declare const TEST_PUBLIC_KEY = "test-public-key";
13
+ export declare const createKeyPair: jest.Mock<any, any, any>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createKeyPair = exports.TEST_PUBLIC_KEY = void 0;
4
+ /** Copyright © 2024-present P0 Security
5
+
6
+ This file is part of @p0security/cli
7
+
8
+ @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.
9
+
10
+ @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.
11
+
12
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
13
+ **/
14
+ exports.TEST_PUBLIC_KEY = "test-public-key";
15
+ exports.createKeyPair = jest.fn().mockImplementation(() => ({
16
+ publicKey: "test-public-key",
17
+ privateKey: "test-private-key",
18
+ }));
@@ -0,0 +1,9 @@
1
+ export declare const PUBLIC_KEY_PATH: string;
2
+ export declare const PRIVATE_KEY_PATH: string;
3
+ /**
4
+ * Search for a cached key pair, or create a new one if not found
5
+ */
6
+ export declare const createKeyPair: () => Promise<{
7
+ publicKey: string;
8
+ privateKey: string;
9
+ }>;
@@ -0,0 +1,84 @@
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.createKeyPair = exports.PRIVATE_KEY_PATH = exports.PUBLIC_KEY_PATH = void 0;
39
+ /** Copyright © 2024-present P0 Security
40
+
41
+ This file is part of @p0security/cli
42
+
43
+ @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.
44
+
45
+ @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.
46
+
47
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
48
+ **/
49
+ const util_1 = require("../util");
50
+ const fs = __importStar(require("fs/promises"));
51
+ const node_forge_1 = __importDefault(require("node-forge"));
52
+ const path = __importStar(require("path"));
53
+ exports.PUBLIC_KEY_PATH = path.join(util_1.P0_PATH, "ssh", "id_rsa.pub");
54
+ exports.PRIVATE_KEY_PATH = path.join(util_1.P0_PATH, "ssh", "id_rsa");
55
+ /**
56
+ * Search for a cached key pair, or create a new one if not found
57
+ */
58
+ const createKeyPair = () => __awaiter(void 0, void 0, void 0, function* () {
59
+ if ((yield fileExists(exports.PUBLIC_KEY_PATH)) &&
60
+ (yield fileExists(exports.PRIVATE_KEY_PATH))) {
61
+ const publicKey = yield fs.readFile(exports.PUBLIC_KEY_PATH, "utf8");
62
+ const privateKey = yield fs.readFile(exports.PRIVATE_KEY_PATH, "utf8");
63
+ return { publicKey, privateKey };
64
+ }
65
+ else {
66
+ const rsaKeyPair = node_forge_1.default.pki.rsa.generateKeyPair({ bits: 2048 });
67
+ const privateKey = node_forge_1.default.pki.privateKeyToPem(rsaKeyPair.privateKey);
68
+ const publicKey = node_forge_1.default.ssh.publicKeyToOpenSSH(rsaKeyPair.publicKey);
69
+ yield fs.mkdir(path.dirname(exports.PUBLIC_KEY_PATH), { recursive: true });
70
+ yield fs.writeFile(exports.PUBLIC_KEY_PATH, publicKey, { mode: 0o600 });
71
+ yield fs.writeFile(exports.PRIVATE_KEY_PATH, privateKey, { mode: 0o600 });
72
+ return { publicKey, privateKey };
73
+ }
74
+ });
75
+ exports.createKeyPair = createKeyPair;
76
+ const fileExists = (path) => __awaiter(void 0, void 0, void 0, function* () {
77
+ try {
78
+ yield fs.access(path);
79
+ return true;
80
+ }
81
+ catch (error) {
82
+ return false;
83
+ }
84
+ });
@@ -1,20 +1,4 @@
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 { ExerciseGrantResponse } from "../commands/shared";
12
1
  import { Authn } from "../types/identity";
13
2
  import yargs from "yargs";
14
3
  export declare const fetchCommand: <T>(authn: Authn, args: yargs.ArgumentsCamelCase, argv: string[]) => Promise<T>;
15
- export declare const fetchExerciseGrant: (authn: Authn, args: {
16
- requestId: string;
17
- destination: string;
18
- publicKey?: string;
19
- }) => Promise<ExerciseGrantResponse>;
20
4
  export declare const baseFetch: <T>(authn: Authn, url: string, method: string, body: string) => Promise<T>;
@@ -32,12 +32,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
32
32
  });
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.baseFetch = exports.fetchExerciseGrant = exports.fetchCommand = void 0;
35
+ exports.baseFetch = exports.fetchCommand = void 0;
36
+ /** Copyright © 2024-present P0 Security
37
+
38
+ This file is part of @p0security/cli
39
+
40
+ @p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
41
+
42
+ @p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
43
+
44
+ You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
45
+ **/
36
46
  const env_1 = require("../drivers/env");
37
47
  const path = __importStar(require("node:path"));
38
48
  const tenantUrl = (tenant) => `${env_1.config.appUrl}/o/${tenant}`;
39
49
  const commandUrl = (tenant) => `${tenantUrl(tenant)}/command/`;
40
- const exerciseGrantUrl = (tenant) => `${tenantUrl(tenant)}/exercise-grant/`;
41
50
  const fetchCommand = (authn, args, argv) => __awaiter(void 0, void 0, void 0, function* () {
42
51
  return (0, exports.baseFetch)(authn, commandUrl(authn.identity.org.slug), "POST", JSON.stringify({
43
52
  argv,
@@ -45,10 +54,6 @@ const fetchCommand = (authn, args, argv) => __awaiter(void 0, void 0, void 0, fu
45
54
  }));
46
55
  });
47
56
  exports.fetchCommand = fetchCommand;
48
- const fetchExerciseGrant = (authn, args) => __awaiter(void 0, void 0, void 0, function* () {
49
- return (0, exports.baseFetch)(authn, exerciseGrantUrl(authn.identity.org.slug), "POST", JSON.stringify(args));
50
- });
51
- exports.fetchExerciseGrant = fetchExerciseGrant;
52
57
  const baseFetch = (authn, url, method, body) => __awaiter(void 0, void 0, void 0, function* () {
53
58
  const token = yield authn.userCredential.user.getIdToken();
54
59
  const response = yield fetch(url, {
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
- export declare const TERMINATION_CONTROLLER: AbortController;
2
1
  export declare const main: () => void;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.main = exports.TERMINATION_CONTROLLER = void 0;
3
+ exports.main = void 0;
4
4
  /** Copyright © 2024-present P0 Security
5
5
 
6
6
  This file is part of @p0security/cli
@@ -13,17 +13,6 @@ You should have received a copy of the GNU General Public License along with @p0
13
13
  **/
14
14
  const commands_1 = require("./commands");
15
15
  const lodash_1 = require("lodash");
16
- const node_os_1 = require("node:os");
17
- // Subscribing to this global abort controller allows handling process termination signals anywhere in the application
18
- exports.TERMINATION_CONTROLLER = new AbortController();
19
- const terminationHandler = (code) => () => {
20
- exports.TERMINATION_CONTROLLER.abort(code);
21
- process.exit(128 + code); // by convention the exit code is the signal code + 128
22
- };
23
- process.on("SIGHUP", terminationHandler(node_os_1.constants.signals.SIGHUP));
24
- process.on("SIGINT", terminationHandler(node_os_1.constants.signals.SIGINT));
25
- process.on("SIGQUIT", terminationHandler(node_os_1.constants.signals.SIGQUIT));
26
- process.on("SIGTERM", terminationHandler(node_os_1.constants.signals.SIGTERM));
27
16
  const main = () => {
28
17
  // We can suppress output here, as .fail() already print1 errors
29
18
  void commands_1.cli.parse().catch(lodash_1.noop);
@@ -26,9 +26,10 @@ const api_1 = require("./api");
26
26
  const api_2 = require("./api");
27
27
  const roleArn = (args) => `${(0, api_1.arnPrefix)(args.account)}:role/${args.role}`;
28
28
  const stsAssume = (params) => __awaiter(void 0, void 0, void 0, function* () {
29
- const url = `https://sts.amazonaws.com?${(0, fetch_1.urlEncode)(params)}`;
29
+ const url = `https://sts.amazonaws.com`;
30
30
  const response = yield fetch(url, {
31
- method: "GET",
31
+ method: "POST",
32
+ body: new URLSearchParams(params),
32
33
  });
33
34
  yield (0, fetch_1.validateResponse)(response);
34
35
  const stsXml = yield response.text();
@@ -8,6 +8,6 @@ This file is part of @p0security/cli
8
8
 
9
9
  You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
10
10
  **/
11
- import { ExerciseGrantResponse, ScpCommandArgs, SshCommandArgs } from "../../../commands/shared";
11
+ import { ScpCommandArgs, SshCommandArgs, SshRequest } from "../../../commands/shared";
12
12
  import { Authn } from "../../../types/identity";
13
- export declare const sshOrScp: (authn: Authn, data: ExerciseGrantResponse, cmdArgs: ScpCommandArgs | SshCommandArgs, privateKey: string) => Promise<number | null>;
13
+ export declare const sshOrScp: (authn: Authn, data: SshRequest, cmdArgs: ScpCommandArgs | SshCommandArgs, privateKey: string) => Promise<number | null>;
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.sshOrScp = void 0;
13
+ const keys_1 = require("../../../common/keys");
13
14
  const stdio_1 = require("../../../drivers/stdio");
14
15
  const aws_1 = require("../../okta/aws");
15
16
  const ssh_agent_1 = require("../../ssh-agent");
@@ -81,8 +82,8 @@ const createBaseSsmCommand = (args) => {
81
82
  args.instance,
82
83
  ];
83
84
  };
84
- const spawnChildProcess = (credential, command, args, stdio, sshAgentEnv) => (0, node_child_process_1.spawn)(command, args, {
85
- env: Object.assign(Object.assign(Object.assign({}, process.env), credential), (sshAgentEnv || {})),
85
+ const spawnChildProcess = (credential, command, args, stdio) => (0, node_child_process_1.spawn)(command, args, {
86
+ env: Object.assign(Object.assign({}, process.env), credential),
86
87
  stdio,
87
88
  shell: false,
88
89
  });
@@ -93,7 +94,7 @@ const spawnChildProcess = (credential, command, args, stdio, sshAgentEnv) => (0,
93
94
  function spawnSsmNode(options) {
94
95
  return __awaiter(this, void 0, void 0, function* () {
95
96
  return new Promise((resolve, reject) => {
96
- const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio, options.sshAgentEnv);
97
+ const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio);
97
98
  const { isAccessPropagated } = accessPropagationGuard(child);
98
99
  const exitListener = child.on("exit", (code) => {
99
100
  var _a, _b;
@@ -118,10 +119,10 @@ function spawnSsmNode(options) {
118
119
  });
119
120
  });
120
121
  }
121
- const createProxyCommands = (data, args, sshAuthSock, debug) => {
122
+ const createProxyCommands = (data, args, debug) => {
122
123
  const ssmCommand = [
123
124
  ...createBaseSsmCommand({
124
- region: data.instance.region,
125
+ region: data.region,
125
126
  instance: "%h",
126
127
  }),
127
128
  "--document-name",
@@ -131,9 +132,6 @@ const createProxyCommands = (data, args, sshAuthSock, debug) => {
131
132
  ];
132
133
  const commonArgs = [
133
134
  ...(debug ? ["-v"] : []),
134
- // ignore any overrides in the user's config file, we only want to use the ssh-agent we've set up for the session
135
- "-o",
136
- `IdentityAgent=${sshAuthSock}`,
137
135
  "-o",
138
136
  `ProxyCommand=${ssmCommand.join(" ")}`,
139
137
  ];
@@ -161,7 +159,7 @@ const createProxyCommands = (data, args, sshAuthSock, debug) => {
161
159
  ...(args.A ? ["-A"] : []),
162
160
  ...(args.L ? ["-L", args.L] : []),
163
161
  ...(args.N ? ["-N"] : []),
164
- `${data.linuxUserName}@${data.instance.id}`,
162
+ `${data.linuxUserName}@${data.id}`,
165
163
  ...(args.command ? [args.command] : []),
166
164
  ...args.arguments.map((argument) =>
167
165
  // escape all double quotes (") in commands such as `p0 ssh <instance>> echo 'hello; "world"'` because we
@@ -189,28 +187,22 @@ const sshOrScp = (authn, data, cmdArgs, privateKey) => __awaiter(void 0, void 0,
189
187
  throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
190
188
  }
191
189
  const credential = yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
192
- account: data.instance.accountId,
190
+ account: data.accountId,
193
191
  role: data.role,
194
192
  });
195
- return (0, ssh_agent_1.withSshAgent)(cmdArgs, (sshAgentEnv) => __awaiter(void 0, void 0, void 0, function* () {
196
- yield (0, ssh_agent_1.sshAdd)(cmdArgs, sshAgentEnv, privateKey);
197
- if (cmdArgs.debug) {
198
- (0, stdio_1.print2)("SSH Agent Keys:");
199
- yield (0, ssh_agent_1.sshAddList)(cmdArgs, sshAgentEnv);
200
- }
201
- const { command, args } = createProxyCommands(data, cmdArgs, sshAgentEnv.SSH_AUTH_SOCK, cmdArgs.debug);
193
+ return (0, ssh_agent_1.withSshAgent)(cmdArgs, () => __awaiter(void 0, void 0, void 0, function* () {
194
+ const { command, args } = createProxyCommands(data, cmdArgs, cmdArgs.debug);
202
195
  if (cmdArgs.debug) {
203
196
  const reproCommands = [
204
- `eval $(p0 aws role assume ${data.role} --account ${data.instance.accountId})`,
205
- `export SSH_AUTH_SOCK=${sshAgentEnv.SSH_AUTH_SOCK}`,
206
- `export SSH_AGENT_PID=${sshAgentEnv.SSH_AGENT_PID}`,
197
+ `eval $(ssh-agent)`,
198
+ `ssh-add "${keys_1.PRIVATE_KEY_PATH}"`,
199
+ `eval $(p0 aws role assume ${data.role} --account ${data.accountId})`,
207
200
  `${command} ${transformForShell(args).join(" ")}`,
208
201
  ];
209
- (0, stdio_1.print2)(`Execute the following commands to create a similar SSH/SCP session:\n*** COMMANDS BEGIN ***\n${reproCommands.join("\n")}\n*** COMMANDS END ***\n\nTHE SSH AGENT PROCESS WILL NOT BE KILLED AUTOMATICALLY IN DEBUG MODE\nYou can kill it with "sudo kill ${sshAgentEnv.SSH_AGENT_PID}"\n`);
202
+ (0, stdio_1.print2)(`Execute the following commands to create a similar SSH/SCP session:\n*** COMMANDS BEGIN ***\n${reproCommands.join("\n")}\n*** COMMANDS END ***"\n`);
210
203
  }
211
204
  return spawnSsmNode({
212
205
  credential,
213
- sshAgentEnv,
214
206
  abortController: new AbortController(),
215
207
  command,
216
208
  args,
@@ -53,16 +53,18 @@ export declare type AwsConfig = {
53
53
  export declare type AwsSsh = {
54
54
  permission: {
55
55
  spec: {
56
- awsResourcePermission: {
57
- resource: {
58
- arn: string;
59
- };
60
- };
56
+ instanceId: string;
57
+ accountId: string;
58
+ region: string;
61
59
  };
62
60
  type: "session";
63
61
  };
64
62
  generated: {
65
- documentName: string;
63
+ name: string;
64
+ ssh: {
65
+ linuxUserName: string;
66
+ publicKey: string;
67
+ };
66
68
  };
67
69
  };
68
70
  export {};
@@ -1,10 +1,4 @@
1
- import { AgentArgs, SshAgentEnv } from "./types";
2
- /** Spawns a subprocess with the ssh-agent command.
3
- * Detects the auth socket and agent PID from stdout.
4
- * Stdout and stderr of the subprocess is printed to stderr in debug mode.
5
- * The returned promise resolves with an object that contains the auth socket and agent PID,
6
- * or rejects with the contents of stderr. */
7
- export declare const sshAgent: (cmdArgs: AgentArgs) => Promise<SshAgentEnv>;
8
- export declare const sshAdd: (args: AgentArgs, sshAgentEnv: SshAgentEnv, privateKey: string) => Promise<number>;
9
- export declare const sshAddList: (args: AgentArgs, sshAgentEnv: SshAgentEnv) => Promise<number>;
10
- export declare const withSshAgent: <T>(args: AgentArgs, fn: (sshAgentEnv: SshAgentEnv) => Promise<T>) => Promise<T>;
1
+ import { AgentArgs } from "./types";
2
+ export declare const privateKeyExists: (args: AgentArgs) => Promise<boolean>;
3
+ export declare const addPrivateKey: (args: AgentArgs) => Promise<boolean>;
4
+ export declare const withSshAgent: <T>(args: AgentArgs, fn: () => Promise<T>) => Promise<T>;
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.withSshAgent = exports.sshAddList = exports.sshAdd = exports.sshAgent = void 0;
12
+ exports.withSshAgent = exports.addPrivateKey = exports.privateKeyExists = void 0;
13
13
  /** Copyright © 2024-present P0 Security
14
14
 
15
15
  This file is part of @p0security/cli
@@ -20,11 +20,9 @@ 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 __1 = require("../..");
23
+ const keys_1 = require("../../common/keys");
24
24
  const stdio_1 = require("../../drivers/stdio");
25
25
  const node_child_process_1 = require("node:child_process");
26
- const AUTH_SOCK_MESSAGE = /SSH_AUTH_SOCK=(.+?);/;
27
- const AGENT_PID_MESSAGE = /SSH_AGENT_PID=(\d+?);/;
28
26
  /** Spawns a subprocess with given command, args, and options.
29
27
  * May write content to its standard input.
30
28
  * Stdout and stderr of the subprocess is printed to stderr in debug mode.
@@ -59,84 +57,80 @@ const asyncSpawn = ({ debug }, command, args, options, writeStdin) => __awaiter(
59
57
  }
60
58
  });
61
59
  });
62
- /** Spawns a subprocess with the ssh-agent command.
63
- * Detects the auth socket and agent PID from stdout.
64
- * Stdout and stderr of the subprocess is printed to stderr in debug mode.
65
- * The returned promise resolves with an object that contains the auth socket and agent PID,
66
- * or rejects with the contents of stderr. */
67
- const sshAgent = (cmdArgs) => __awaiter(void 0, void 0, void 0, function* () {
68
- return new Promise((resolve, reject) => {
69
- let stderr = "";
70
- let stdout = "";
71
- // There is a debug flag in ssh-agent but it causes the ssh-agent process to NOT fork
72
- const child = (0, node_child_process_1.spawn)("ssh-agent");
73
- child.stdout.on("data", (data) => {
74
- const str = data.toString("utf-8");
75
- if (cmdArgs.debug) {
76
- (0, stdio_1.print2)(str);
77
- }
78
- stdout += str;
79
- });
80
- child.stderr.on("data", (data) => {
81
- const str = data.toString("utf-8");
82
- if (cmdArgs.debug) {
83
- (0, stdio_1.print2)(str);
84
- }
85
- stderr += str;
86
- });
87
- const exitListener = child.on("exit", (code) => {
88
- exitListener.unref();
89
- if (code !== 0) {
90
- return reject(stderr);
91
- }
92
- const authSockMatch = stdout.match(AUTH_SOCK_MESSAGE);
93
- const agentPidMatch = stdout.match(AGENT_PID_MESSAGE);
94
- if (!(authSockMatch === null || authSockMatch === void 0 ? void 0 : authSockMatch[1]) || !(agentPidMatch === null || agentPidMatch === void 0 ? void 0 : agentPidMatch[1])) {
95
- return reject("Failed to parse ssh-agent stdout:\n" + stdout);
96
- }
97
- resolve({
98
- SSH_AUTH_SOCK: authSockMatch[1],
99
- SSH_AGENT_PID: agentPidMatch[1],
100
- });
101
- });
102
- });
60
+ const isSshAgentRunning = (args) => __awaiter(void 0, void 0, void 0, function* () {
61
+ try {
62
+ if (args.debug)
63
+ (0, stdio_1.print2)("Searching for active ssh-agents");
64
+ // TODO: There's a possible edge-case but unlikely that ssh-agent has an invalid process or PID.
65
+ // We can check to see if the active PID matches the current socket to mitigate this.
66
+ yield asyncSpawn(args, `pgrep`, ["-x", "ssh-agent"]);
67
+ if (args.debug)
68
+ (0, stdio_1.print2)("At least one SSH agent is running");
69
+ return true;
70
+ }
71
+ catch (_a) {
72
+ if (args.debug)
73
+ (0, stdio_1.print2)("No SSH agent is running!");
74
+ return false;
75
+ }
103
76
  });
104
- exports.sshAgent = sshAgent;
105
- const sshAgentKill = (args, sshAgentEnv) => __awaiter(void 0, void 0, void 0, function* () {
106
- return asyncSpawn(args, "ssh-agent", ["-k"], {
107
- env: Object.assign(Object.assign({}, process.env), sshAgentEnv),
108
- });
77
+ const isSshAgentAuthSocketSet = (args) => __awaiter(void 0, void 0, void 0, function* () {
78
+ try {
79
+ yield asyncSpawn(args, `sh`, ["-c", '[ -n "$SSH_AUTH_SOCK" ]']);
80
+ if (args.debug)
81
+ (0, stdio_1.print2)(`SSH_AUTH_SOCK=${process.env.SSH_AUTH_SOCK}`);
82
+ return true;
83
+ }
84
+ catch (_b) {
85
+ if (args.debug)
86
+ (0, stdio_1.print2)("SSH_AUTH_SOCK is not set!");
87
+ return false;
88
+ }
109
89
  });
110
- const sshAdd = (args, sshAgentEnv, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
111
- return asyncSpawn(args, "ssh-add",
112
- // In debug mode do not use the quiet flag. There is no debug flag in ssh-add.
113
- // Instead increase to maximum verbosity of 3 with -v flag.
114
- args.debug ? ["-v", "-v", "-v", "-"] : ["-q", "-"], { env: Object.assign(Object.assign({}, process.env), sshAgentEnv) }, privateKey);
90
+ const privateKeyExists = (args) => __awaiter(void 0, void 0, void 0, function* () {
91
+ try {
92
+ yield asyncSpawn(args, `sh`, [
93
+ "-c",
94
+ `KEY_PATH="${keys_1.PRIVATE_KEY_PATH}" && KEY_FINGERPRINT=$(ssh-keygen -lf "$KEY_PATH" | awk '{print $2}') && ssh-add -l | grep -q "$KEY_FINGERPRINT" && exit 0 || exit 1`,
95
+ ]);
96
+ if (args.debug)
97
+ (0, stdio_1.print2)("Private key exists in ssh agent");
98
+ return true;
99
+ }
100
+ catch (_c) {
101
+ if (args.debug)
102
+ (0, stdio_1.print2)("Private key does not exist in ssh agent");
103
+ return false;
104
+ }
115
105
  });
116
- exports.sshAdd = sshAdd;
117
- const sshAddList = (args, sshAgentEnv) => __awaiter(void 0, void 0, void 0, function* () {
118
- return asyncSpawn(args, "ssh-add", ["-l"], {
119
- env: Object.assign(Object.assign({}, process.env), sshAgentEnv),
120
- });
106
+ exports.privateKeyExists = privateKeyExists;
107
+ const addPrivateKey = (args) => __awaiter(void 0, void 0, void 0, function* () {
108
+ try {
109
+ yield asyncSpawn(args, `ssh-add`, [
110
+ keys_1.PRIVATE_KEY_PATH,
111
+ ...(args.debug ? ["-v", "-v", "-v"] : ["-q"]),
112
+ ]);
113
+ if (args.debug)
114
+ (0, stdio_1.print2)("Private key added to ssh agent");
115
+ return true;
116
+ }
117
+ catch (_d) {
118
+ if (args.debug)
119
+ (0, stdio_1.print2)("Failed to add private key to ssh agent");
120
+ return false;
121
+ }
121
122
  });
122
- exports.sshAddList = sshAddList;
123
+ exports.addPrivateKey = addPrivateKey;
123
124
  const withSshAgent = (args, fn) => __awaiter(void 0, void 0, void 0, function* () {
124
- const sshAgentEnv = yield (0, exports.sshAgent)(args);
125
- // The ssh-agent runs in a process that is not automatically terminated.
126
- // 1. Kill it when catching the main process termination signal.
127
- // 2. Also kill it if the encapsulated function throws an error.
128
- const abortListener = (_code) => {
129
- __1.TERMINATION_CONTROLLER.signal.removeEventListener("abort", abortListener);
130
- void sshAgentKill(args, sshAgentEnv);
131
- };
132
- __1.TERMINATION_CONTROLLER.signal.addEventListener("abort", abortListener);
133
- try {
134
- return yield fn(sshAgentEnv);
125
+ const isRunning = yield isSshAgentRunning(args);
126
+ const hasSocket = yield isSshAgentAuthSocketSet(args);
127
+ if (!isRunning || !hasSocket) {
128
+ throw "SSH agent is not running. Please start it by running: eval $(ssh-agent)";
135
129
  }
136
- finally {
137
- // keep the ssh-agent alive in debug mode
138
- if (!args.debug)
139
- yield sshAgentKill(args, sshAgentEnv);
130
+ const hasKey = yield (0, exports.privateKeyExists)(args);
131
+ if (!hasKey) {
132
+ yield (0, exports.addPrivateKey)(args);
140
133
  }
134
+ return yield fn();
141
135
  });
142
136
  exports.withSshAgent = withSshAgent;
@@ -11,7 +11,3 @@ You should have received a copy of the GNU General Public License along with @p0
11
11
  export declare type AgentArgs = {
12
12
  debug?: boolean;
13
13
  };
14
- export declare type SshAgentEnv = {
15
- SSH_AUTH_SOCK: string;
16
- SSH_AGENT_PID: string;
17
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p0security/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Execute infra CLI commands with P0 grants",
5
5
  "main": "index.ts",
6
6
  "repository": {