@p0security/cli 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/grant.test.d.ts +1 -0
- package/dist/commands/__tests__/grant.test.js +55 -0
- package/dist/commands/grant.d.ts +4 -0
- package/dist/commands/grant.js +17 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/request.d.ts +0 -8
- package/dist/commands/request.js +3 -102
- package/dist/commands/shared/request.d.ts +14 -0
- package/dist/commands/shared/request.js +115 -0
- package/dist/commands/shared/ssh.d.ts +7 -1
- package/dist/commands/shared/ssh.js +9 -7
- package/dist/plugins/aws/ssh.d.ts +2 -2
- package/dist/plugins/aws/ssh.js +54 -0
- package/dist/plugins/google/ssh.d.ts +0 -10
- package/dist/plugins/google/ssh.js +45 -0
- package/dist/plugins/ssh/index.d.ts +2 -2
- package/dist/plugins/ssh/index.js +65 -93
- package/dist/types/ssh.d.ts +21 -1
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
/** Copyright © 2024-present P0 Security
|
|
16
|
+
|
|
17
|
+
This file is part of @p0security/cli
|
|
18
|
+
|
|
19
|
+
@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.
|
|
20
|
+
|
|
21
|
+
@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.
|
|
22
|
+
|
|
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
|
+
**/
|
|
25
|
+
const api_1 = require("../../drivers/api");
|
|
26
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
27
|
+
const grant_1 = require("../grant");
|
|
28
|
+
const yargs_1 = __importDefault(require("yargs"));
|
|
29
|
+
jest.mock("../../drivers/api");
|
|
30
|
+
jest.mock("../../drivers/auth");
|
|
31
|
+
jest.mock("../../drivers/stdio");
|
|
32
|
+
const mockFetchCommand = api_1.fetchCommand;
|
|
33
|
+
const mockPrint1 = stdio_1.print1;
|
|
34
|
+
const mockPrint2 = stdio_1.print2;
|
|
35
|
+
describe("grant", () => {
|
|
36
|
+
beforeEach(() => jest.clearAllMocks());
|
|
37
|
+
describe("when valid grant command", () => {
|
|
38
|
+
const command = "grant gcloud role viewer --to someone@test.com --principal-type user";
|
|
39
|
+
function mockFetch() {
|
|
40
|
+
mockFetchCommand.mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
message: "a message",
|
|
43
|
+
id: "abcefg",
|
|
44
|
+
isPreexisting: false,
|
|
45
|
+
isPersistent: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
it(`should print request response`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
mockFetch();
|
|
50
|
+
yield (0, grant_1.grantCommand)((0, yargs_1.default)()).parse(command);
|
|
51
|
+
expect(mockPrint2.mock.calls).toMatchSnapshot();
|
|
52
|
+
expect(mockPrint1).not.toHaveBeenCalled();
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.grantCommand = 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
|
+
const firestore_1 = require("../drivers/firestore");
|
|
15
|
+
const request_1 = require("./shared/request");
|
|
16
|
+
const grantCommand = (yargs) => yargs.command("grant [arguments..]", "Grant access to another identity", request_1.requestArgs, (0, firestore_1.guard)((0, request_1.request)("grant")));
|
|
17
|
+
exports.grantCommand = grantCommand;
|
package/dist/commands/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const stdio_1 = require("../drivers/stdio");
|
|
|
18
18
|
const version_1 = require("../middlewares/version");
|
|
19
19
|
const allow_1 = require("./allow");
|
|
20
20
|
const aws_1 = require("./aws");
|
|
21
|
+
const grant_1 = require("./grant");
|
|
21
22
|
const login_1 = require("./login");
|
|
22
23
|
const ls_1 = require("./ls");
|
|
23
24
|
const request_1 = require("./request");
|
|
@@ -28,6 +29,7 @@ const yargs_1 = __importDefault(require("yargs"));
|
|
|
28
29
|
const helpers_1 = require("yargs/helpers");
|
|
29
30
|
const commands = [
|
|
30
31
|
aws_1.awsCommand,
|
|
32
|
+
grant_1.grantCommand,
|
|
31
33
|
login_1.loginCommand,
|
|
32
34
|
ls_1.lsCommand,
|
|
33
35
|
request_1.requestCommand,
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import { Authn } from "../types/identity";
|
|
2
|
-
import { RequestResponse } from "../types/request";
|
|
3
1
|
import yargs from "yargs";
|
|
4
2
|
export declare const requestCommand: (yargs: yargs.Argv<{}>) => yargs.Argv<{
|
|
5
3
|
arguments: string[];
|
|
6
4
|
}>;
|
|
7
|
-
export declare const request: <T>(args: yargs.ArgumentsCamelCase<{
|
|
8
|
-
arguments: string[];
|
|
9
|
-
wait?: boolean;
|
|
10
|
-
}>, authn?: Authn, options?: {
|
|
11
|
-
message?: "all" | "approval-required" | "none";
|
|
12
|
-
}) => Promise<RequestResponse<T> | undefined>;
|
package/dist/commands/request.js
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
3
|
+
exports.requestCommand = void 0;
|
|
13
4
|
/** Copyright © 2024-present P0 Security
|
|
14
5
|
|
|
15
6
|
This file is part of @p0security/cli
|
|
@@ -20,97 +11,7 @@ This file is part of @p0security/cli
|
|
|
20
11
|
|
|
21
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/>.
|
|
22
13
|
**/
|
|
23
|
-
const api_1 = require("../drivers/api");
|
|
24
|
-
const auth_1 = require("../drivers/auth");
|
|
25
14
|
const firestore_1 = require("../drivers/firestore");
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const typescript_1 = require("typescript");
|
|
29
|
-
const WAIT_TIMEOUT = 300e3;
|
|
30
|
-
const APPROVED = { message: "Your request was approved", code: 0 };
|
|
31
|
-
const DENIED = { message: "Your request was denied", code: 2 };
|
|
32
|
-
const ERRORED = { message: "Your request encountered an error", code: 1 };
|
|
33
|
-
const COMPLETED_REQUEST_STATUSES = {
|
|
34
|
-
APPROVED,
|
|
35
|
-
APPROVED_NOTIFIED: APPROVED,
|
|
36
|
-
DONE: APPROVED,
|
|
37
|
-
DONE_NOTIFIED: APPROVED,
|
|
38
|
-
DENIED,
|
|
39
|
-
ERRORED,
|
|
40
|
-
};
|
|
41
|
-
const isCompletedStatus = (status) => status in COMPLETED_REQUEST_STATUSES;
|
|
42
|
-
const requestArgs = (yargs) => yargs
|
|
43
|
-
.parserConfiguration({ "unknown-options-as-args": true })
|
|
44
|
-
.help(false) // Turn off help in order to forward the --help command to the backend so P0 can provide the available requestable resources
|
|
45
|
-
.option("wait", {
|
|
46
|
-
alias: "w",
|
|
47
|
-
boolean: true,
|
|
48
|
-
default: false,
|
|
49
|
-
describe: "Block until the command is completed",
|
|
50
|
-
})
|
|
51
|
-
.option("arguments", {
|
|
52
|
-
array: true,
|
|
53
|
-
string: true,
|
|
54
|
-
default: [],
|
|
55
|
-
});
|
|
56
|
-
const requestCommand = (yargs) => yargs.command("request [arguments..]", "Manually request permissions on a resource", requestArgs, (0, firestore_1.guard)(exports.request));
|
|
15
|
+
const request_1 = require("./shared/request");
|
|
16
|
+
const requestCommand = (yargs) => yargs.command("request [arguments..]", "Manually request permissions on a resource", request_1.requestArgs, (0, firestore_1.guard)((0, request_1.request)("request")));
|
|
57
17
|
exports.requestCommand = requestCommand;
|
|
58
|
-
const waitForRequest = (tenantId, requestId, logMessage) => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
-
return yield new Promise((resolve) => {
|
|
60
|
-
if (logMessage)
|
|
61
|
-
(0, stdio_1.print2)("Will wait up to 5 minutes for this request to complete...");
|
|
62
|
-
let cancel = undefined;
|
|
63
|
-
const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${tenantId}/permission-requests/${requestId}`), (snap) => {
|
|
64
|
-
const data = snap.data();
|
|
65
|
-
if (!data)
|
|
66
|
-
return;
|
|
67
|
-
const { status } = data;
|
|
68
|
-
if (isCompletedStatus(status)) {
|
|
69
|
-
if (cancel)
|
|
70
|
-
clearTimeout(cancel);
|
|
71
|
-
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
72
|
-
const { message, code } = COMPLETED_REQUEST_STATUSES[status];
|
|
73
|
-
if (code !== 0 || logMessage)
|
|
74
|
-
(0, stdio_1.print2)(message);
|
|
75
|
-
resolve(code);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
cancel = setTimeout(() => {
|
|
79
|
-
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
80
|
-
(0, stdio_1.print2)("Your request did not complete within 5 minutes.");
|
|
81
|
-
resolve(4);
|
|
82
|
-
}, WAIT_TIMEOUT);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
const request = (args, authn, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
-
const resolvedAuthn = authn !== null && authn !== void 0 ? authn : (yield (0, auth_1.authenticate)());
|
|
87
|
-
const { userCredential } = resolvedAuthn;
|
|
88
|
-
const data = yield (0, api_1.fetchCommand)(resolvedAuthn, args, [
|
|
89
|
-
"request",
|
|
90
|
-
...args.arguments,
|
|
91
|
-
]);
|
|
92
|
-
if (data && "ok" in data && "message" in data && data.ok) {
|
|
93
|
-
const logMessage = !(options === null || options === void 0 ? void 0 : options.message) ||
|
|
94
|
-
(options === null || options === void 0 ? void 0 : options.message) === "all" ||
|
|
95
|
-
((options === null || options === void 0 ? void 0 : options.message) === "approval-required" &&
|
|
96
|
-
!data.isPreexisting &&
|
|
97
|
-
!data.isPersistent);
|
|
98
|
-
if (logMessage)
|
|
99
|
-
(0, stdio_1.print2)(data.message);
|
|
100
|
-
const { id } = data;
|
|
101
|
-
if (args.wait && id && userCredential.user.tenantId) {
|
|
102
|
-
const code = yield waitForRequest(userCredential.user.tenantId, id, logMessage);
|
|
103
|
-
if (code) {
|
|
104
|
-
typescript_1.sys.exit(code);
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return data;
|
|
108
|
-
}
|
|
109
|
-
else
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
throw data;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
exports.request = request;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Authn } from "../../types/identity";
|
|
2
|
+
import { RequestResponse } from "../../types/request";
|
|
3
|
+
import yargs from "yargs";
|
|
4
|
+
export declare const requestArgs: <T>(yargs: yargs.Argv<T>) => yargs.Argv<T & {
|
|
5
|
+
wait: boolean;
|
|
6
|
+
} & {
|
|
7
|
+
arguments: string[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare const request: (command: "grant" | "request") => <T>(args: yargs.ArgumentsCamelCase<{
|
|
10
|
+
arguments: string[];
|
|
11
|
+
wait?: boolean;
|
|
12
|
+
}>, authn?: Authn, options?: {
|
|
13
|
+
message?: "all" | "approval-required" | "none";
|
|
14
|
+
}) => Promise<RequestResponse<T> | undefined>;
|
|
@@ -0,0 +1,115 @@
|
|
|
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.request = exports.requestArgs = 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 api_1 = require("../../drivers/api");
|
|
24
|
+
const auth_1 = require("../../drivers/auth");
|
|
25
|
+
const firestore_1 = require("../../drivers/firestore");
|
|
26
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
27
|
+
const firestore_2 = require("firebase/firestore");
|
|
28
|
+
const typescript_1 = require("typescript");
|
|
29
|
+
const WAIT_TIMEOUT = 300e3;
|
|
30
|
+
const APPROVED = { message: "Your request was approved", code: 0 };
|
|
31
|
+
const DENIED = { message: "Your request was denied", code: 2 };
|
|
32
|
+
const ERRORED = { message: "Your request encountered an error", code: 1 };
|
|
33
|
+
const COMPLETED_REQUEST_STATUSES = {
|
|
34
|
+
APPROVED,
|
|
35
|
+
APPROVED_NOTIFIED: APPROVED,
|
|
36
|
+
DONE: APPROVED,
|
|
37
|
+
DONE_NOTIFIED: APPROVED,
|
|
38
|
+
DENIED,
|
|
39
|
+
ERRORED,
|
|
40
|
+
};
|
|
41
|
+
const isCompletedStatus = (status) => status in COMPLETED_REQUEST_STATUSES;
|
|
42
|
+
const requestArgs = (yargs) => yargs
|
|
43
|
+
.parserConfiguration({ "unknown-options-as-args": true })
|
|
44
|
+
.help(false) // Turn off help in order to forward the --help command to the backend so P0 can provide the available requestable resources
|
|
45
|
+
.option("wait", {
|
|
46
|
+
alias: "w",
|
|
47
|
+
boolean: true,
|
|
48
|
+
default: false,
|
|
49
|
+
describe: "Block until the command is completed",
|
|
50
|
+
})
|
|
51
|
+
.option("arguments", {
|
|
52
|
+
array: true,
|
|
53
|
+
string: true,
|
|
54
|
+
default: [],
|
|
55
|
+
});
|
|
56
|
+
exports.requestArgs = requestArgs;
|
|
57
|
+
const waitForRequest = (tenantId, requestId, logMessage) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
return yield new Promise((resolve) => {
|
|
59
|
+
if (logMessage)
|
|
60
|
+
(0, stdio_1.print2)("Will wait up to 5 minutes for this request to complete...");
|
|
61
|
+
let cancel = undefined;
|
|
62
|
+
const unsubscribe = (0, firestore_2.onSnapshot)((0, firestore_1.doc)(`o/${tenantId}/permission-requests/${requestId}`), (snap) => {
|
|
63
|
+
const data = snap.data();
|
|
64
|
+
if (!data)
|
|
65
|
+
return;
|
|
66
|
+
const { status } = data;
|
|
67
|
+
if (isCompletedStatus(status)) {
|
|
68
|
+
if (cancel)
|
|
69
|
+
clearTimeout(cancel);
|
|
70
|
+
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
71
|
+
const { message, code } = COMPLETED_REQUEST_STATUSES[status];
|
|
72
|
+
if (code !== 0 || logMessage)
|
|
73
|
+
(0, stdio_1.print2)(message);
|
|
74
|
+
resolve(code);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
cancel = setTimeout(() => {
|
|
78
|
+
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
79
|
+
(0, stdio_1.print2)("Your request did not complete within 5 minutes.");
|
|
80
|
+
resolve(4);
|
|
81
|
+
}, WAIT_TIMEOUT);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
const request = (command) => (args, authn, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const resolvedAuthn = authn !== null && authn !== void 0 ? authn : (yield (0, auth_1.authenticate)());
|
|
86
|
+
const { userCredential } = resolvedAuthn;
|
|
87
|
+
const data = yield (0, api_1.fetchCommand)(resolvedAuthn, args, [
|
|
88
|
+
command,
|
|
89
|
+
...args.arguments,
|
|
90
|
+
]);
|
|
91
|
+
if (data && "ok" in data && "message" in data && data.ok) {
|
|
92
|
+
const logMessage = !(options === null || options === void 0 ? void 0 : options.message) ||
|
|
93
|
+
(options === null || options === void 0 ? void 0 : options.message) === "all" ||
|
|
94
|
+
((options === null || options === void 0 ? void 0 : options.message) === "approval-required" &&
|
|
95
|
+
!data.isPreexisting &&
|
|
96
|
+
!data.isPersistent);
|
|
97
|
+
if (logMessage)
|
|
98
|
+
(0, stdio_1.print2)(data.message);
|
|
99
|
+
const { id } = data;
|
|
100
|
+
if (args.wait && id && userCredential.user.tenantId) {
|
|
101
|
+
const code = yield waitForRequest(userCredential.user.tenantId, id, logMessage);
|
|
102
|
+
if (code) {
|
|
103
|
+
typescript_1.sys.exit(code);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw data;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
exports.request = request;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Authn } from "../../types/identity";
|
|
2
2
|
import { Request } from "../../types/request";
|
|
3
|
-
import { CliSshRequest, SshRequest, SupportedSshProvider } from "../../types/ssh";
|
|
3
|
+
import { CliSshRequest, SshProvider, SshRequest, SupportedSshProvider } from "../../types/ssh";
|
|
4
4
|
import yargs from "yargs";
|
|
5
5
|
export declare type BaseSshCommandArgs = {
|
|
6
6
|
sudo?: boolean;
|
|
@@ -23,6 +23,12 @@ export declare type SshCommandArgs = BaseSshCommandArgs & {
|
|
|
23
23
|
arguments: string[];
|
|
24
24
|
command?: string;
|
|
25
25
|
};
|
|
26
|
+
export declare type CommandArgs = ScpCommandArgs | SshCommandArgs;
|
|
27
|
+
export declare const SSH_PROVIDERS: Record<SupportedSshProvider, SshProvider<any, any, any, any>>;
|
|
28
|
+
export declare const isSudoCommand: (args: {
|
|
29
|
+
sudo?: boolean;
|
|
30
|
+
command?: string;
|
|
31
|
+
}) => boolean;
|
|
26
32
|
export declare const provisionRequest: (authn: Authn, args: yargs.ArgumentsCamelCase<BaseSshCommandArgs>, destination: string) => Promise<{
|
|
27
33
|
request: Request<CliSshRequest>;
|
|
28
34
|
publicKey: string;
|
|
@@ -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.requestToSsh = exports.provisionRequest = void 0;
|
|
12
|
+
exports.requestToSsh = exports.provisionRequest = exports.isSudoCommand = exports.SSH_PROVIDERS = void 0;
|
|
13
13
|
/** Copyright © 2024-present P0 Security
|
|
14
14
|
|
|
15
15
|
This file is part of @p0security/cli
|
|
@@ -27,10 +27,10 @@ const stdio_1 = require("../../drivers/stdio");
|
|
|
27
27
|
const ssh_1 = require("../../plugins/aws/ssh");
|
|
28
28
|
const ssh_2 = require("../../plugins/google/ssh");
|
|
29
29
|
const ssh_3 = require("../../types/ssh");
|
|
30
|
-
const request_1 = require("
|
|
30
|
+
const request_1 = require("./request");
|
|
31
31
|
const firestore_2 = require("firebase/firestore");
|
|
32
32
|
const lodash_1 = require("lodash");
|
|
33
|
-
|
|
33
|
+
exports.SSH_PROVIDERS = {
|
|
34
34
|
aws: ssh_1.awsSshProvider,
|
|
35
35
|
gcloud: ssh_2.gcpSshProvider,
|
|
36
36
|
};
|
|
@@ -48,19 +48,21 @@ const validateSshInstall = (authn, args) => __awaiter(void 0, void 0, void 0, fu
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
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);
|
|
51
|
+
return yield exports.SSH_PROVIDERS[request.permission.spec.type].toCliRequest(request, options);
|
|
52
52
|
});
|
|
53
|
+
const isSudoCommand = (args) => args.sudo || args.command === "sudo";
|
|
54
|
+
exports.isSudoCommand = isSudoCommand;
|
|
53
55
|
const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
56
|
yield validateSshInstall(authn, args);
|
|
55
57
|
const { publicKey, privateKey } = yield (0, keys_1.createKeyPair)();
|
|
56
|
-
const response = yield (0, request_1.request)(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
|
|
58
|
+
const response = yield (0, request_1.request)("request")(Object.assign(Object.assign({}, (0, lodash_1.pick)(args, "$0", "_")), { arguments: [
|
|
57
59
|
"ssh",
|
|
58
60
|
"session",
|
|
59
61
|
destination,
|
|
60
62
|
"--public-key",
|
|
61
63
|
publicKey,
|
|
62
64
|
...(args.provider ? ["--provider", args.provider] : []),
|
|
63
|
-
...(
|
|
65
|
+
...((0, exports.isSudoCommand)(args) ? ["--sudo"] : []),
|
|
64
66
|
...(args.reason ? ["--reason", args.reason] : []),
|
|
65
67
|
...(args.account ? ["--account", args.account] : []),
|
|
66
68
|
], wait: true }), authn, { message: "approval-required" });
|
|
@@ -81,5 +83,5 @@ const provisionRequest = (authn, args, destination) => __awaiter(void 0, void 0,
|
|
|
81
83
|
return { request: cliRequest, publicKey, privateKey };
|
|
82
84
|
});
|
|
83
85
|
exports.provisionRequest = provisionRequest;
|
|
84
|
-
const requestToSsh = (request) => SSH_PROVIDERS[request.permission.spec.type].requestToSsh(request);
|
|
86
|
+
const requestToSsh = (request) => exports.SSH_PROVIDERS[request.permission.spec.type].requestToSsh(request);
|
|
85
87
|
exports.requestToSsh = requestToSsh;
|
|
@@ -9,5 +9,5 @@ This file is part of @p0security/cli
|
|
|
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
11
|
import { SshProvider } from "../../types/ssh";
|
|
12
|
-
import { AwsSshPermissionSpec, AwsSshRequest } from "./types";
|
|
13
|
-
export declare const awsSshProvider: SshProvider<AwsSshPermissionSpec, undefined, AwsSshRequest>;
|
|
12
|
+
import { AwsCredentials, AwsSshPermissionSpec, AwsSshRequest } from "./types";
|
|
13
|
+
export declare const awsSshProvider: SshProvider<AwsSshPermissionSpec, undefined, AwsSshRequest, AwsCredentials>;
|
package/dist/plugins/aws/ssh.js
CHANGED
|
@@ -10,6 +10,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.awsSshProvider = void 0;
|
|
13
|
+
const util_1 = require("../../util");
|
|
14
|
+
const aws_1 = require("../okta/aws");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const idc_1 = require("./idc");
|
|
17
|
+
const install_1 = require("./ssm/install");
|
|
18
|
+
/** Maximum number of attempts to start an SSH session
|
|
19
|
+
*
|
|
20
|
+
* Each attempt consumes ~ 1 s.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_SSH_RETRIES = 30;
|
|
23
|
+
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
|
|
24
|
+
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
13
25
|
exports.awsSshProvider = {
|
|
14
26
|
requestToSsh: (request) => {
|
|
15
27
|
const { permission, generated } = request;
|
|
@@ -21,4 +33,46 @@ exports.awsSshProvider = {
|
|
|
21
33
|
? Object.assign(Object.assign({}, common), { role: name, type: "aws", access: "role" }) : Object.assign(Object.assign({}, common), { idc, permissionSet: name, type: "aws", access: "idc" });
|
|
22
34
|
},
|
|
23
35
|
toCliRequest: (request) => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign(Object.assign({}, request), { cliLocalData: undefined })); }),
|
|
36
|
+
cloudProviderLogin: (authn, request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
+
var _a, _b, _c, _d;
|
|
38
|
+
if (!(yield (0, install_1.ensureSsmInstall)())) {
|
|
39
|
+
throw "Please try again after installing the required AWS utilities";
|
|
40
|
+
}
|
|
41
|
+
const { config } = yield (0, config_1.getAwsConfig)(authn, request.accountId);
|
|
42
|
+
if (!((_a = config.login) === null || _a === void 0 ? void 0 : _a.type) || ((_b = config.login) === null || _b === void 0 ? void 0 : _b.type) === "iam") {
|
|
43
|
+
throw "This account is not configured for SSH access via the P0 CLI";
|
|
44
|
+
}
|
|
45
|
+
return ((_c = config.login) === null || _c === void 0 ? void 0 : _c.type) === "idc"
|
|
46
|
+
? yield (0, idc_1.assumeRoleWithIdc)(request)
|
|
47
|
+
: ((_d = config.login) === null || _d === void 0 ? void 0 : _d.type) === "federated"
|
|
48
|
+
? yield (0, aws_1.assumeRoleWithOktaSaml)(authn, request)
|
|
49
|
+
: (0, util_1.throwAssertNever)(config.login);
|
|
50
|
+
}),
|
|
51
|
+
proxyCommand: (request) => {
|
|
52
|
+
return [
|
|
53
|
+
"aws",
|
|
54
|
+
"ssm",
|
|
55
|
+
"start-session",
|
|
56
|
+
"--region",
|
|
57
|
+
request.region,
|
|
58
|
+
"--target",
|
|
59
|
+
"%h",
|
|
60
|
+
"--document-name",
|
|
61
|
+
START_SSH_SESSION_DOCUMENT_NAME,
|
|
62
|
+
"--parameters",
|
|
63
|
+
'"portNumber=%p"',
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
reproCommands: (request) => {
|
|
67
|
+
// TODO: Add manual commands for IDC login
|
|
68
|
+
if (request.access !== "idc") {
|
|
69
|
+
return [
|
|
70
|
+
`eval $(p0 aws role assume ${request.role} --account ${request.accountId})`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
},
|
|
75
|
+
preTestAccessPropagationArgs: () => undefined,
|
|
76
|
+
maxRetries: MAX_SSH_RETRIES,
|
|
77
|
+
friendlyName: "AWS",
|
|
24
78
|
};
|
|
@@ -1,13 +1,3 @@
|
|
|
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
1
|
import { SshProvider } from "../../types/ssh";
|
|
12
2
|
import { GcpSshPermissionSpec, GcpSshRequest } from "./types";
|
|
13
3
|
export declare const gcpSshProvider: SshProvider<GcpSshPermissionSpec, {
|
|
@@ -10,7 +10,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.gcpSshProvider = void 0;
|
|
13
|
+
/** Copyright © 2024-present P0 Security
|
|
14
|
+
|
|
15
|
+
This file is part of @p0security/cli
|
|
16
|
+
|
|
17
|
+
@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
20
|
+
|
|
21
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
**/
|
|
23
|
+
const ssh_1 = require("../../commands/shared/ssh");
|
|
13
24
|
const ssh_key_1 = require("./ssh-key");
|
|
25
|
+
/** Maximum number of attempts to start an SSH session
|
|
26
|
+
*
|
|
27
|
+
* The length of each attempt varies based on the type of error from a few seconds to < 1s
|
|
28
|
+
*/
|
|
29
|
+
const MAX_SSH_RETRIES = 120;
|
|
14
30
|
exports.gcpSshProvider = {
|
|
15
31
|
requestToSsh: (request) => {
|
|
16
32
|
return {
|
|
@@ -26,4 +42,33 @@ exports.gcpSshProvider = {
|
|
|
26
42
|
linuxUserName: yield (0, ssh_key_1.importSshKey)(request.permission.spec.publicKey, options),
|
|
27
43
|
} }));
|
|
28
44
|
}),
|
|
45
|
+
cloudProviderLogin: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
46
|
+
proxyCommand: (request) => {
|
|
47
|
+
return [
|
|
48
|
+
"gcloud",
|
|
49
|
+
"compute",
|
|
50
|
+
"start-iap-tunnel",
|
|
51
|
+
request.id,
|
|
52
|
+
"%p",
|
|
53
|
+
// --listen-on-stdin flag is required for interactive SSH session.
|
|
54
|
+
// It is undocumented on page https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel
|
|
55
|
+
// but mention on page https://cloud.google.com/iap/docs/tcp-by-host
|
|
56
|
+
// and also found in `gcloud ssh --dry-run` output
|
|
57
|
+
"--listen-on-stdin",
|
|
58
|
+
`--zone=${request.zone}`,
|
|
59
|
+
`--project=${request.projectId}`,
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
reproCommands: () => undefined,
|
|
63
|
+
preTestAccessPropagationArgs: (cmdArgs) => {
|
|
64
|
+
if ((0, ssh_1.isSudoCommand)(cmdArgs)) {
|
|
65
|
+
return Object.assign(Object.assign({}, cmdArgs), {
|
|
66
|
+
// `sudo -v` prints `Sorry, user <user> may not run sudo on <hostname>.` to stderr when user is not a sudoer.
|
|
67
|
+
// It prints nothing to stdout when user is a sudoer - which is important because we don't want any output from the pre-test.
|
|
68
|
+
command: "sudo", arguments: ["-v"] });
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
},
|
|
72
|
+
maxRetries: MAX_SSH_RETRIES,
|
|
73
|
+
friendlyName: "Google Cloud",
|
|
29
74
|
};
|
|
@@ -8,7 +8,7 @@ This file is part of @p0security/cli
|
|
|
8
8
|
|
|
9
9
|
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
10
10
|
**/
|
|
11
|
-
import {
|
|
11
|
+
import { CommandArgs } from "../../commands/shared/ssh";
|
|
12
12
|
import { Authn } from "../../types/identity";
|
|
13
13
|
import { SshRequest } from "../../types/ssh";
|
|
14
|
-
export declare const sshOrScp: (authn: Authn,
|
|
14
|
+
export declare const sshOrScp: (authn: Authn, request: SshRequest, cmdArgs: CommandArgs, privateKey: string) => Promise<number | null>;
|
|
@@ -10,13 +10,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.sshOrScp = void 0;
|
|
13
|
+
/** Copyright © 2024-present P0 Security
|
|
14
|
+
|
|
15
|
+
This file is part of @p0security/cli
|
|
16
|
+
|
|
17
|
+
@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
18
|
+
|
|
19
|
+
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
20
|
+
|
|
21
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
**/
|
|
23
|
+
const ssh_1 = require("../../commands/shared/ssh");
|
|
13
24
|
const keys_1 = require("../../common/keys");
|
|
14
25
|
const stdio_1 = require("../../drivers/stdio");
|
|
15
|
-
const util_1 = require("../../util");
|
|
16
|
-
const config_1 = require("../aws/config");
|
|
17
|
-
const idc_1 = require("../aws/idc");
|
|
18
|
-
const install_1 = require("../aws/ssm/install");
|
|
19
|
-
const aws_1 = require("../okta/aws");
|
|
20
26
|
const ssh_agent_1 = require("../ssh-agent");
|
|
21
27
|
const node_child_process_1 = require("node:child_process");
|
|
22
28
|
/** Matches the error message that AWS SSM print1 when access is not propagated */
|
|
@@ -35,18 +41,11 @@ const UNAUTHORIZED_TUNNEL_USER_MESSAGE = /Error while connecting \[4033: 'not au
|
|
|
35
41
|
const UNAUTHORIZED_INSTANCES_GET_MESSAGE = /Required 'compute\.instances\.get' permission/;
|
|
36
42
|
const DESTINATION_READ_ERROR = /Error while connecting \[4010: 'destination read failed'\]/;
|
|
37
43
|
const GOOGLE_LOGIN_MESSAGE = /You do not currently have an active account selected/;
|
|
44
|
+
const SUDO_MESSAGE = /Sorry, user .+ may not run sudo on .+/; // The output of `sudo -v` when the user is not allowed to run sudo
|
|
38
45
|
/** Maximum amount of time after SSH subprocess starts to check for {@link UNPROVISIONED_ACCESS_MESSAGES}
|
|
39
46
|
* in the process's stderr
|
|
40
47
|
*/
|
|
41
48
|
const DEFAULT_VALIDATION_WINDOW_MS = 5e3;
|
|
42
|
-
/** Maximum number of attempts to start an SSH session
|
|
43
|
-
*
|
|
44
|
-
* Note that each attempt consumes ~ 1 s.
|
|
45
|
-
*/
|
|
46
|
-
const DEFAULT_MAX_SSH_RETRIES = 30;
|
|
47
|
-
const GCP_MAX_SSH_RETRIES = 120; // GCP requires more time to propagate access
|
|
48
|
-
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
|
|
49
|
-
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
50
49
|
/**
|
|
51
50
|
* AWS
|
|
52
51
|
* There are 2 cases of unprovisioned access in AWS
|
|
@@ -57,7 +56,7 @@ const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
|
57
56
|
* 2: results in CONNECTION_CLOSED_MESSAGE
|
|
58
57
|
*
|
|
59
58
|
* Google Cloud
|
|
60
|
-
* There are
|
|
59
|
+
* There are 7 cases of unprovisioned access in Google Cloud.
|
|
61
60
|
* These are all potentially subject to propagation delays.
|
|
62
61
|
* 1. The linux user name is not present in the user's Google Workspace profile `posixAccounts` attribute
|
|
63
62
|
* 2. The public key is not present in the user's Google Workspace profile `sshPublicKeys` attribute
|
|
@@ -66,17 +65,20 @@ const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
|
66
65
|
* 5. The user doesn't have osLogin or osAdminLogin role to the instance
|
|
67
66
|
* 5.a. compute.instances.get permission is missing
|
|
68
67
|
* 5.b. compute.instances.osLogin permission is missing
|
|
69
|
-
* 6
|
|
68
|
+
* 6. compute.instances.osAdminLogin is not provisioned but compute.instances.osLogin is - happens when a user upgrades existing access to sudo
|
|
69
|
+
* 7: Rare occurrence, the exact conditions so far undetermined (together with CONNECTION_CLOSED_MESSAGE)
|
|
70
70
|
*
|
|
71
71
|
* 1, 2, 3 (yes!), 5b: result in PUBLIC_KEY_DENIED_MESSAGE
|
|
72
72
|
* 4: results in UNAUTHORIZED_TUNNEL_USER_MESSAGE and also CONNECTION_CLOSED_MESSAGE
|
|
73
73
|
* 5a: results in UNAUTHORIZED_INSTANCES_GET_MESSAGE
|
|
74
|
-
* 6: results in
|
|
74
|
+
* 6: results in SUDO_MESSAGE
|
|
75
|
+
* 7: results in DESTINATION_READ_ERROR and also CONNECTION_CLOSED_MESSAGE
|
|
75
76
|
*/
|
|
76
77
|
const UNPROVISIONED_ACCESS_MESSAGES = [
|
|
77
78
|
{ pattern: UNAUTHORIZED_START_SESSION_MESSAGE },
|
|
78
79
|
{ pattern: CONNECTION_CLOSED_MESSAGE },
|
|
79
80
|
{ pattern: PUBLIC_KEY_DENIED_MESSAGE },
|
|
81
|
+
{ pattern: SUDO_MESSAGE },
|
|
80
82
|
{ pattern: UNAUTHORIZED_TUNNEL_USER_MESSAGE },
|
|
81
83
|
{ pattern: UNAUTHORIZED_INSTANCES_GET_MESSAGE, validationWindowMs: 30e3 },
|
|
82
84
|
{ pattern: DESTINATION_READ_ERROR },
|
|
@@ -127,11 +129,6 @@ const spawnChildProcess = (credential, command, args, stdio) => (0, node_child_p
|
|
|
127
129
|
stdio,
|
|
128
130
|
shell: false,
|
|
129
131
|
});
|
|
130
|
-
const friendlyProvider = (provider) => provider === "aws"
|
|
131
|
-
? "AWS"
|
|
132
|
-
: provider === "gcloud"
|
|
133
|
-
? "Google Cloud"
|
|
134
|
-
: (0, util_1.throwAssertNever)(provider);
|
|
135
132
|
/** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess
|
|
136
133
|
*
|
|
137
134
|
* Requires `aws ssm` to be installed on the client machine.
|
|
@@ -139,6 +136,7 @@ const friendlyProvider = (provider) => provider === "aws"
|
|
|
139
136
|
function spawnSshNode(options) {
|
|
140
137
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
138
|
return new Promise((resolve, reject) => {
|
|
139
|
+
const provider = ssh_1.SSH_PROVIDERS[options.provider];
|
|
142
140
|
const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio);
|
|
143
141
|
// TODO ENG-2284 support login with Google Cloud: currently return a boolean to indicate if the exception was a Google login error.
|
|
144
142
|
const { isAccessPropagated, isGoogleLoginException } = accessPropagationGuard(child, options.debug);
|
|
@@ -153,7 +151,7 @@ function spawnSshNode(options) {
|
|
|
153
151
|
(0, stdio_1.print2)(`Waiting for access to propagate. Retrying SSH session... (remaining attempts: ${attemptsRemaining})`);
|
|
154
152
|
}
|
|
155
153
|
if (attemptsRemaining <= 0) {
|
|
156
|
-
reject(`Access did not propagate through ${
|
|
154
|
+
reject(`Access did not propagate through ${provider.friendlyName} before max retry attempts were exceeded. Please contact support@p0.dev for assistance.`);
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
159
157
|
spawnSshNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
|
|
@@ -166,50 +164,16 @@ function spawnSshNode(options) {
|
|
|
166
164
|
return;
|
|
167
165
|
}
|
|
168
166
|
(_a = options.abortController) === null || _a === void 0 ? void 0 : _a.abort(code);
|
|
169
|
-
(
|
|
167
|
+
if (!options.isAccessPropagationPreTest)
|
|
168
|
+
(0, stdio_1.print2)(`SSH session terminated`);
|
|
170
169
|
resolve(code);
|
|
171
170
|
});
|
|
172
171
|
});
|
|
173
172
|
});
|
|
174
173
|
}
|
|
175
|
-
const
|
|
176
|
-
let proxyCommand;
|
|
177
|
-
if (data.type === "aws") {
|
|
178
|
-
proxyCommand = [
|
|
179
|
-
"aws",
|
|
180
|
-
"ssm",
|
|
181
|
-
"start-session",
|
|
182
|
-
"--region",
|
|
183
|
-
data.region,
|
|
184
|
-
"--target",
|
|
185
|
-
"%h",
|
|
186
|
-
"--document-name",
|
|
187
|
-
START_SSH_SESSION_DOCUMENT_NAME,
|
|
188
|
-
"--parameters",
|
|
189
|
-
'"portNumber=%p"',
|
|
190
|
-
];
|
|
191
|
-
}
|
|
192
|
-
else if (data.type === "gcloud") {
|
|
193
|
-
proxyCommand = [
|
|
194
|
-
"gcloud",
|
|
195
|
-
"compute",
|
|
196
|
-
"start-iap-tunnel",
|
|
197
|
-
data.id,
|
|
198
|
-
"%p",
|
|
199
|
-
// --listen-on-stdin flag is required for interactive SSH session.
|
|
200
|
-
// It is undocumented on page https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel
|
|
201
|
-
// but mention on page https://cloud.google.com/iap/docs/tcp-by-host
|
|
202
|
-
// and also found in `gcloud ssh --dry-run` output
|
|
203
|
-
"--listen-on-stdin",
|
|
204
|
-
`--zone=${data.zone}`,
|
|
205
|
-
`--project=${data.projectId}`,
|
|
206
|
-
];
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
throw (0, util_1.assertNever)(data);
|
|
210
|
-
}
|
|
174
|
+
const createCommand = (data, args, proxyCommand) => {
|
|
211
175
|
const commonArgs = [
|
|
212
|
-
...(debug ? ["-v"] : []),
|
|
176
|
+
...(args.debug ? ["-v"] : []),
|
|
213
177
|
"-o",
|
|
214
178
|
`ProxyCommand=${proxyCommand.join(" ")}`,
|
|
215
179
|
];
|
|
@@ -257,45 +221,53 @@ const transformForShell = (args) => {
|
|
|
257
221
|
return arg;
|
|
258
222
|
});
|
|
259
223
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
224
|
+
/** Construct another command to use for testing access propagation prior to actually logging in the user to the ssh session */
|
|
225
|
+
const preTestAccessPropagationIfNeeded = (sshProvider, request, cmdArgs, proxyCommand, credential) => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
const testCmdArgs = sshProvider.preTestAccessPropagationArgs(cmdArgs);
|
|
227
|
+
// Pre-testing comes at a performance cost because we have to execute another ssh subprocess after
|
|
228
|
+
// a successful test. Only do when absolutely necessary.
|
|
229
|
+
if (testCmdArgs) {
|
|
230
|
+
const { command, args } = createCommand(request, testCmdArgs, proxyCommand);
|
|
231
|
+
// Assumes that this is a non-interactive ssh command that exits automatically
|
|
232
|
+
return spawnSshNode({
|
|
233
|
+
credential,
|
|
234
|
+
abortController: new AbortController(),
|
|
235
|
+
command,
|
|
236
|
+
args,
|
|
237
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
238
|
+
debug: cmdArgs.debug,
|
|
239
|
+
provider: request.type,
|
|
240
|
+
attemptsRemaining: sshProvider.maxRetries,
|
|
241
|
+
isAccessPropagationPreTest: true,
|
|
242
|
+
});
|
|
268
243
|
}
|
|
269
|
-
return
|
|
270
|
-
? yield (0, idc_1.assumeRoleWithIdc)(data)
|
|
271
|
-
: ((_d = config.login) === null || _d === void 0 ? void 0 : _d.type) === "federated"
|
|
272
|
-
? yield (0, aws_1.assumeRoleWithOktaSaml)(authn, data)
|
|
273
|
-
: (0, util_1.throwAssertNever)(config.login);
|
|
244
|
+
return null;
|
|
274
245
|
});
|
|
275
|
-
const sshOrScp = (authn,
|
|
246
|
+
const sshOrScp = (authn, request, cmdArgs, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
276
247
|
if (!privateKey) {
|
|
277
248
|
throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
|
|
278
249
|
}
|
|
279
|
-
|
|
280
|
-
const credential =
|
|
250
|
+
const sshProvider = ssh_1.SSH_PROVIDERS[request.type];
|
|
251
|
+
const credential = yield sshProvider.cloudProviderLogin(authn, request);
|
|
252
|
+
const proxyCommand = sshProvider.proxyCommand(request);
|
|
281
253
|
return (0, ssh_agent_1.withSshAgent)(cmdArgs, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
282
|
-
const { command, args } =
|
|
254
|
+
const { command, args } = createCommand(request, cmdArgs, proxyCommand);
|
|
283
255
|
if (cmdArgs.debug) {
|
|
284
|
-
const reproCommands =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
256
|
+
const reproCommands = sshProvider.reproCommands(request);
|
|
257
|
+
if (reproCommands) {
|
|
258
|
+
const repro = [
|
|
259
|
+
`eval $(ssh-agent)`,
|
|
260
|
+
`ssh-add "${keys_1.PRIVATE_KEY_PATH}"`,
|
|
261
|
+
...reproCommands,
|
|
262
|
+
`${command} ${transformForShell(args).join(" ")}`,
|
|
263
|
+
].join("\n");
|
|
264
|
+
(0, stdio_1.print2)(`Execute the following commands to create a similar SSH/SCP session:\n*** COMMANDS BEGIN ***\n${repro}\n*** COMMANDS END ***"\n`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const exitCode = yield preTestAccessPropagationIfNeeded(sshProvider, request, cmdArgs, proxyCommand, credential);
|
|
268
|
+
if (exitCode && exitCode !== 0) {
|
|
269
|
+
return exitCode; // Only exit if there was an error when pre-testing
|
|
297
270
|
}
|
|
298
|
-
const maxRetries = data.type === "gcloud" ? GCP_MAX_SSH_RETRIES : DEFAULT_MAX_SSH_RETRIES;
|
|
299
271
|
return spawnSshNode({
|
|
300
272
|
credential,
|
|
301
273
|
abortController: new AbortController(),
|
|
@@ -303,8 +275,8 @@ const sshOrScp = (authn, data, cmdArgs, privateKey) => __awaiter(void 0, void 0,
|
|
|
303
275
|
args,
|
|
304
276
|
stdio: ["inherit", "inherit", "pipe"],
|
|
305
277
|
debug: cmdArgs.debug,
|
|
306
|
-
provider:
|
|
307
|
-
attemptsRemaining: maxRetries,
|
|
278
|
+
provider: request.type,
|
|
279
|
+
attemptsRemaining: sshProvider.maxRetries,
|
|
308
280
|
});
|
|
309
281
|
}));
|
|
310
282
|
});
|
package/dist/types/ssh.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ This file is part of @p0security/cli
|
|
|
8
8
|
|
|
9
9
|
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
10
10
|
**/
|
|
11
|
+
import { CommandArgs } from "../commands/shared/ssh";
|
|
11
12
|
import { AwsSsh, AwsSshPermissionSpec, AwsSshRequest } from "../plugins/aws/types";
|
|
12
13
|
import { GcpSsh, GcpSshPermissionSpec, GcpSshRequest } from "../plugins/google/types";
|
|
14
|
+
import { Authn } from "./identity";
|
|
13
15
|
import { Request } from "./request";
|
|
14
16
|
export declare type CliSshRequest = AwsSsh | GcpSsh;
|
|
15
17
|
export declare type PluginSshRequest = AwsSshPermissionSpec | GcpSshPermissionSpec;
|
|
@@ -18,10 +20,28 @@ export declare type CliPermissionSpec<P extends PluginSshRequest, C extends obje
|
|
|
18
20
|
};
|
|
19
21
|
export declare const SupportedSshProviders: readonly ["aws", "gcloud"];
|
|
20
22
|
export declare type SupportedSshProvider = (typeof SupportedSshProviders)[number];
|
|
21
|
-
export declare type SshProvider<PR extends PluginSshRequest = PluginSshRequest, O extends object | undefined = undefined, SR extends SshRequest = SshRequest> = {
|
|
23
|
+
export declare type SshProvider<PR extends PluginSshRequest = PluginSshRequest, O extends object | undefined = undefined, SR extends SshRequest = SshRequest, C extends object | undefined = undefined> = {
|
|
22
24
|
requestToSsh: (request: CliPermissionSpec<PR, O>) => SR;
|
|
25
|
+
/** Converts a backend request to a CLI request */
|
|
23
26
|
toCliRequest: (request: Request<PR>, options?: {
|
|
24
27
|
debug?: boolean;
|
|
25
28
|
}) => Promise<Request<CliSshRequest>>;
|
|
29
|
+
/** Logs in the user to the cloud provider */
|
|
30
|
+
cloudProviderLogin: (authn: Authn, request: SR) => Promise<C>;
|
|
31
|
+
/** Returns the command and its arguments that are going to be injected as the ssh ProxyCommand option */
|
|
32
|
+
proxyCommand: (request: SR) => string[];
|
|
33
|
+
/** Each element in the returned array is a command that can be run to reproduce the
|
|
34
|
+
* steps of logging in the user to the ssh session. */
|
|
35
|
+
reproCommands: (request: SR) => string[] | undefined;
|
|
36
|
+
/** Arguments for a pre-test command to verify access propagation prior
|
|
37
|
+
* to actually logging in the user to the ssh session.
|
|
38
|
+
* This must return arguments for a non-interactive command - meaning the `command`
|
|
39
|
+
* and potentially the `args` props must be specified in the returned scp/ssh command.
|
|
40
|
+
* If the return value is undefined then no pre-testing is done prior to executing
|
|
41
|
+
* the actual ssh/scp command.
|
|
42
|
+
*/
|
|
43
|
+
preTestAccessPropagationArgs: (cmdArgs: CommandArgs) => CommandArgs | undefined;
|
|
44
|
+
maxRetries: number;
|
|
45
|
+
friendlyName: string;
|
|
26
46
|
};
|
|
27
47
|
export declare type SshRequest = AwsSshRequest | GcpSshRequest;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@p0security/cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Execute infra CLI commands with P0 grants",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"express": "^4.18.2",
|
|
26
26
|
"firebase": "^10.7.2",
|
|
27
27
|
"inquirer": "^9.2.15",
|
|
28
|
-
"jsdom": "^24.
|
|
28
|
+
"jsdom": "^24.1.1",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
30
|
"node-forge": "^1.3.1",
|
|
31
31
|
"open": "^8.4.0",
|
|
@@ -67,5 +67,6 @@
|
|
|
67
67
|
"lint": "yarn prettier --check . && yarn run eslint --max-warnings 0 .",
|
|
68
68
|
"p0": "node --no-deprecation ./p0",
|
|
69
69
|
"prepublishOnly": "npm run clean && npm run build"
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
71
72
|
}
|