@p0security/cli 0.6.2 → 0.7.1
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__/ssh.test.js +18 -4
- package/dist/commands/allow.d.ts +10 -0
- package/dist/commands/allow.js +57 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/scp.js +3 -13
- package/dist/commands/shared.d.ts +6 -0
- package/dist/commands/shared.js +12 -1
- package/dist/commands/ssh.js +15 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -1
- package/dist/plugins/aws/ssm/index.d.ts +1 -3
- package/dist/plugins/aws/ssm/index.js +83 -243
- package/dist/plugins/login.js +2 -0
- package/dist/plugins/oidc/login.d.ts +5 -0
- package/dist/plugins/oidc/login.js +133 -0
- package/dist/plugins/okta/login.js +4 -88
- package/dist/plugins/ping/login.d.ts +13 -0
- package/dist/plugins/ping/login.js +16 -0
- package/dist/plugins/ssh-agent/index.d.ts +10 -0
- package/dist/plugins/ssh-agent/index.js +142 -0
- package/dist/plugins/ssh-agent/types.d.ts +17 -0
- package/dist/plugins/ssh-agent/types.js +2 -0
- package/dist/types/allow.d.ts +14 -0
- package/dist/types/allow.js +2 -0
- package/dist/types/org.d.ts +12 -4
- package/dist/types/org.js +10 -0
- package/dist/util.d.ts +3 -0
- package/dist/util.js +11 -1
- package/package.json +1 -3
|
@@ -8,19 +8,13 @@ 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.
|
|
12
|
+
exports.sshOrScp = void 0;
|
|
16
13
|
const stdio_1 = require("../../../drivers/stdio");
|
|
17
14
|
const aws_1 = require("../../okta/aws");
|
|
15
|
+
const ssh_agent_1 = require("../../ssh-agent");
|
|
18
16
|
const install_1 = require("./install");
|
|
19
|
-
const lodash_1 = require("lodash");
|
|
20
17
|
const node_child_process_1 = require("node:child_process");
|
|
21
|
-
const node_stream_1 = require("node:stream");
|
|
22
|
-
const ps_tree_1 = __importDefault(require("ps-tree"));
|
|
23
|
-
const STARTING_SESSION_MESSAGE = /Starting session with SessionId: (.*)/;
|
|
24
18
|
/** Matches the error message that AWS SSM print1 when access is not propagated */
|
|
25
19
|
// Note that the resource will randomly be either the SSM document or the EC2 instance
|
|
26
20
|
const UNPROVISIONED_ACCESS_MESSAGE = /An error occurred \(AccessDeniedException\) when calling the StartSession operation: User: arn:aws:sts::.*:assumed-role\/P0GrantsRole.* is not authorized to perform: ssm:StartSession on resource: arn:aws:.*:.*:.* because no identity-based policy allows the ssm:StartSession action/;
|
|
@@ -42,7 +36,6 @@ const UNPROVISIONED_ACCESS_VALIDATION_WINDOW_MS = 5e3;
|
|
|
42
36
|
*/
|
|
43
37
|
const MAX_SSM_RETRIES = 30;
|
|
44
38
|
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
|
|
45
|
-
const LOCAL_PORT_FORWARDING_DOCUMENT_NAME = "AWS-StartPortForwardingSession";
|
|
46
39
|
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";
|
|
47
40
|
/** Checks if access has propagated through AWS to the SSM agent
|
|
48
41
|
*
|
|
@@ -88,53 +81,11 @@ const createBaseSsmCommand = (args) => {
|
|
|
88
81
|
args.instance,
|
|
89
82
|
];
|
|
90
83
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
args.documentName,
|
|
97
|
-
];
|
|
98
|
-
const command = (_a = args.command) === null || _a === void 0 ? void 0 : _a.trim();
|
|
99
|
-
if (command) {
|
|
100
|
-
ssmCommand.push("--parameters", `command='${command}'`);
|
|
101
|
-
}
|
|
102
|
-
return ssmCommand;
|
|
103
|
-
};
|
|
104
|
-
const createPortForwardingCommand = (args) => {
|
|
105
|
-
const [localPort, remotePort] = args.forwardPortAddress
|
|
106
|
-
.split(":")
|
|
107
|
-
.map(Number);
|
|
108
|
-
return [
|
|
109
|
-
...createBaseSsmCommand(args),
|
|
110
|
-
"--document-name",
|
|
111
|
-
// Port forwarding is a special case that uses an AWS-managed document, not the user-generated document we use for an SSH session
|
|
112
|
-
LOCAL_PORT_FORWARDING_DOCUMENT_NAME,
|
|
113
|
-
"--parameters",
|
|
114
|
-
`localPortNumber=${localPort},portNumber=${remotePort}`,
|
|
115
|
-
];
|
|
116
|
-
};
|
|
117
|
-
const createSsmCommands = (args) => {
|
|
118
|
-
const interactiveShellCommand = createInteractiveShellCommand(args);
|
|
119
|
-
const forwardPortAddress = args.forwardPortAddress;
|
|
120
|
-
if (!forwardPortAddress) {
|
|
121
|
-
return { shellCommand: interactiveShellCommand };
|
|
122
|
-
}
|
|
123
|
-
const portForwardingCommand = createPortForwardingCommand(Object.assign(Object.assign({}, args), { forwardPortAddress }));
|
|
124
|
-
if (args.noRemoteCommands) {
|
|
125
|
-
return { shellCommand: portForwardingCommand };
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
shellCommand: interactiveShellCommand,
|
|
129
|
-
subCommand: portForwardingCommand,
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
function spawnChildProcess(credential, command, args, stdio) {
|
|
133
|
-
return (0, node_child_process_1.spawn)(command, args, {
|
|
134
|
-
env: Object.assign(Object.assign({}, process.env), credential),
|
|
135
|
-
stdio,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
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 || {})),
|
|
86
|
+
stdio,
|
|
87
|
+
shell: false,
|
|
88
|
+
});
|
|
138
89
|
/** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess
|
|
139
90
|
*
|
|
140
91
|
* Requires `aws ssm` to be installed on the client machine.
|
|
@@ -142,12 +93,7 @@ function spawnChildProcess(credential, command, args, stdio) {
|
|
|
142
93
|
function spawnSsmNode(options) {
|
|
143
94
|
return __awaiter(this, void 0, void 0, function* () {
|
|
144
95
|
return new Promise((resolve, reject) => {
|
|
145
|
-
|
|
146
|
-
const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio);
|
|
147
|
-
// optionally buffer content into stdin for the child process
|
|
148
|
-
for (const command of (_a = options.writeStdin) !== null && _a !== void 0 ? _a : []) {
|
|
149
|
-
(_b = child.stdin) === null || _b === void 0 ? void 0 : _b.write(`${command}\n`);
|
|
150
|
-
}
|
|
96
|
+
const child = spawnChildProcess(options.credential, options.command, options.args, options.stdio, options.sshAgentEnv);
|
|
151
97
|
const { isAccessPropagated } = accessPropagationGuard(child);
|
|
152
98
|
const exitListener = child.on("exit", (code) => {
|
|
153
99
|
var _a, _b;
|
|
@@ -172,111 +118,7 @@ function spawnSsmNode(options) {
|
|
|
172
118
|
});
|
|
173
119
|
});
|
|
174
120
|
}
|
|
175
|
-
|
|
176
|
-
* A subprocess SSM session redirects its output through a proxy that filters certain messages reducing the verbosity of the output.
|
|
177
|
-
* The subprocess also makes sure to terminate any grandchild processes that might spawn during the session.
|
|
178
|
-
*
|
|
179
|
-
* This process should be used when multiple SSM sessions need to be spawned in parallel.
|
|
180
|
-
*/
|
|
181
|
-
const spawnSubprocessSsmNode = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
-
return new Promise((resolve, reject) => {
|
|
183
|
-
const child = spawnChildProcess(options.credential, "/usr/bin/env", options.command, ["ignore", "pipe", "pipe"]);
|
|
184
|
-
// Captures the starting session message and filters it from the output
|
|
185
|
-
const proxyStream = new node_stream_1.Transform({
|
|
186
|
-
transform(chunk, _, end) {
|
|
187
|
-
const message = chunk.toString("utf-8");
|
|
188
|
-
const match = message.match(STARTING_SESSION_MESSAGE);
|
|
189
|
-
if (!match) {
|
|
190
|
-
this.push(chunk);
|
|
191
|
-
}
|
|
192
|
-
end();
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
// Ensures that content from the child process is printed to the terminal and the proxy stream
|
|
196
|
-
child.stdout.pipe(proxyStream).pipe(process.stdout);
|
|
197
|
-
const { isAccessPropagated } = accessPropagationGuard(child);
|
|
198
|
-
const abortListener = (code) => {
|
|
199
|
-
options.abortController.signal.removeEventListener("abort", abortListener);
|
|
200
|
-
// AWS CLI typically will spawn a grandchild process for the SSM session. Using `ps-tree` will allow us
|
|
201
|
-
// to identify and terminate the grandchild process as well.
|
|
202
|
-
(0, ps_tree_1.default)(child.pid, function (_, children) {
|
|
203
|
-
// kill the original child process first so that messages from grandchildren are not printed to stdout
|
|
204
|
-
child.kill();
|
|
205
|
-
// Send a SIGTERM because other signals (e.g. SIGKILL) will not propagate to the grandchildren
|
|
206
|
-
(0, node_child_process_1.exec)(`kill -15 ${children.map((p) => p.PID).join(" ")}`);
|
|
207
|
-
});
|
|
208
|
-
resolve(code);
|
|
209
|
-
};
|
|
210
|
-
child.on("spawn", () => {
|
|
211
|
-
options.abortController.signal.addEventListener("abort", abortListener);
|
|
212
|
-
});
|
|
213
|
-
const exitListener = child.on("exit", (code) => {
|
|
214
|
-
var _a;
|
|
215
|
-
exitListener.unref();
|
|
216
|
-
// In the case of ephemeral AccessDenied exceptions due to unpropagated
|
|
217
|
-
// permissions, continually retry access until success
|
|
218
|
-
if (!isAccessPropagated()) {
|
|
219
|
-
options.abortController.signal.removeEventListener("abort", abortListener);
|
|
220
|
-
const attemptsRemaining = (_a = options === null || options === void 0 ? void 0 : options.attemptsRemaining) !== null && _a !== void 0 ? _a : MAX_SSM_RETRIES;
|
|
221
|
-
if (attemptsRemaining <= 0) {
|
|
222
|
-
reject("Access did not propagate through AWS before max retry attempts were exceeded. Please contact support@p0.dev for assistance.");
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
spawnSubprocessSsmNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
|
|
226
|
-
.then((code) => resolve(code))
|
|
227
|
-
.catch(reject);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
options.abortController.abort(code);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
/** Convert an SshCommandArgs into an SSM document "command" parameter */
|
|
235
|
-
const commandParameter = (args) => args.command
|
|
236
|
-
? `${args.command} ${args.arguments
|
|
237
|
-
.map((argument) =>
|
|
238
|
-
// escape all double quotes (") in commands such as `p0 ssh <instance>> echo 'hello; "world"'` because we
|
|
239
|
-
// need to encapsulate command arguments in double quotes as we pass them along to the remote shell
|
|
240
|
-
`"${String(argument).replace(/"/g, '\\"')}"`)
|
|
241
|
-
.join(" ")}`.trim()
|
|
242
|
-
: undefined;
|
|
243
|
-
/** Connect to an SSH backend using AWS Systems Manager (SSM) */
|
|
244
|
-
const ssm = (authn, request, args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
245
|
-
const isInstalled = yield (0, install_1.ensureSsmInstall)();
|
|
246
|
-
if (!isInstalled)
|
|
247
|
-
throw "Please try again after installing the required AWS utilities";
|
|
248
|
-
const credential = yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
|
|
249
|
-
account: request.instance.accountId,
|
|
250
|
-
role: request.role,
|
|
251
|
-
});
|
|
252
|
-
const ssmArgs = {
|
|
253
|
-
instance: request.instance.id,
|
|
254
|
-
region: request.instance.region,
|
|
255
|
-
documentName: request.documentName,
|
|
256
|
-
forwardPortAddress: args.L,
|
|
257
|
-
noRemoteCommands: args.N,
|
|
258
|
-
command: commandParameter(args),
|
|
259
|
-
};
|
|
260
|
-
const ssmCommands = createSsmCommands(ssmArgs);
|
|
261
|
-
yield startSsmProcesses(credential, ssmCommands);
|
|
262
|
-
});
|
|
263
|
-
exports.ssm = ssm;
|
|
264
|
-
/**
|
|
265
|
-
* Starts the SSM session and any additional processes that are requested for the session to function properly.
|
|
266
|
-
*/
|
|
267
|
-
const startSsmProcesses = (credential, commands) => __awaiter(void 0, void 0, void 0, function* () {
|
|
268
|
-
/** The AbortController is responsible for sending a shared signal to all spawned processes ({@link spawnSsmNode}) when the parent process is terminated unexpectedly. This is necessary because the spawned processes are detached and would otherwise continue running after the parent process is terminated. */
|
|
269
|
-
const abortController = new AbortController();
|
|
270
|
-
const args = { credential, abortController };
|
|
271
|
-
const processes = [
|
|
272
|
-
spawnSsmNode(Object.assign(Object.assign({}, args), { command: "/usr/bin/env", args: commands.shellCommand, stdio: ["inherit", "inherit", "pipe"] })),
|
|
273
|
-
];
|
|
274
|
-
if (commands.subCommand) {
|
|
275
|
-
processes.push(spawnSubprocessSsmNode(Object.assign(Object.assign({}, args), { command: commands.subCommand })));
|
|
276
|
-
}
|
|
277
|
-
yield Promise.all(processes);
|
|
278
|
-
});
|
|
279
|
-
const createProxyCommands = (data, args, debug) => {
|
|
121
|
+
const createProxyCommands = (data, args, sshAuthSock, debug) => {
|
|
280
122
|
const ssmCommand = [
|
|
281
123
|
...createBaseSsmCommand({
|
|
282
124
|
region: data.instance.region,
|
|
@@ -287,36 +129,59 @@ const createProxyCommands = (data, args, debug) => {
|
|
|
287
129
|
"--parameters",
|
|
288
130
|
'"portNumber=%p"',
|
|
289
131
|
];
|
|
132
|
+
const commonArgs = [
|
|
133
|
+
...(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
|
+
"-o",
|
|
138
|
+
`ProxyCommand=${ssmCommand.join(" ")}`,
|
|
139
|
+
];
|
|
140
|
+
if ("source" in args) {
|
|
141
|
+
return {
|
|
142
|
+
command: "scp",
|
|
143
|
+
args: [
|
|
144
|
+
...commonArgs,
|
|
145
|
+
// if a response is not received after three 5 minute attempts,
|
|
146
|
+
// the connection will be closed.
|
|
147
|
+
"-o",
|
|
148
|
+
"ServerAliveCountMax=3",
|
|
149
|
+
`-o`,
|
|
150
|
+
"ServerAliveInterval=300",
|
|
151
|
+
...(args.recursive ? ["-r"] : []),
|
|
152
|
+
args.source,
|
|
153
|
+
args.destination,
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
290
157
|
return {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
...
|
|
294
|
-
|
|
295
|
-
"-
|
|
296
|
-
"
|
|
297
|
-
"-o",
|
|
298
|
-
`ProxyCommand='${ssmCommand.join(" ")}'`,
|
|
299
|
-
// if a response is not received after three 5 minute attempts,
|
|
300
|
-
// the connection will be closed.
|
|
301
|
-
"-o",
|
|
302
|
-
"ServerAliveCountMax=3",
|
|
303
|
-
`-o`,
|
|
304
|
-
"ServerAliveInterval=300",
|
|
305
|
-
...(args.recursive ? ["-r"] : []),
|
|
306
|
-
args.source,
|
|
307
|
-
args.destination,
|
|
308
|
-
],
|
|
309
|
-
ssh: [
|
|
310
|
-
"ssh",
|
|
311
|
-
...(debug ? ["-v"] : []),
|
|
312
|
-
"-o",
|
|
313
|
-
`ProxyCommand='${ssmCommand.join(" ")}'`,
|
|
158
|
+
command: "ssh",
|
|
159
|
+
args: [
|
|
160
|
+
...commonArgs,
|
|
161
|
+
...(args.A ? ["-A"] : []),
|
|
162
|
+
...(args.L ? ["-L", args.L] : []),
|
|
163
|
+
...(args.N ? ["-N"] : []),
|
|
314
164
|
`${data.linuxUserName}@${data.instance.id}`,
|
|
165
|
+
...(args.command ? [args.command] : []),
|
|
166
|
+
...args.arguments.map((argument) =>
|
|
167
|
+
// escape all double quotes (") in commands such as `p0 ssh <instance>> echo 'hello; "world"'` because we
|
|
168
|
+
// need to encapsulate command arguments in double quotes as we pass them along to the remote shell
|
|
169
|
+
`"${String(argument).replace(/"/g, '\\"')}"`),
|
|
315
170
|
],
|
|
316
|
-
ssm: ssmCommand,
|
|
317
171
|
};
|
|
318
172
|
};
|
|
319
|
-
|
|
173
|
+
/** Converts arguments for manual execution - arguments may have to be quoted or certain characters escaped when executing the commands from a shell */
|
|
174
|
+
const transformForShell = (args) => {
|
|
175
|
+
return args.map((arg) => {
|
|
176
|
+
// The ProxyCommand option must be surrounded by single quotes
|
|
177
|
+
if (arg.startsWith("ProxyCommand=")) {
|
|
178
|
+
const [name, ...value] = arg.split("="); // contains the '=' character in the parameters option: ProxyCommand=aws ssm start-session ... --parameters "portNumber=%p"
|
|
179
|
+
return `${name}='${value.join("=")}'`;
|
|
180
|
+
}
|
|
181
|
+
return arg;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const sshOrScp = (authn, data, cmdArgs, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
320
185
|
if (!(yield (0, install_1.ensureSsmInstall)())) {
|
|
321
186
|
throw "Please try again after installing the required AWS utilities";
|
|
322
187
|
}
|
|
@@ -327,55 +192,30 @@ const scp = (authn, data, args, privateKey) => __awaiter(void 0, void 0, void 0,
|
|
|
327
192
|
account: data.instance.accountId,
|
|
328
193
|
role: data.role,
|
|
329
194
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
`ssh-add -q - <<< '${privateKey}'`,
|
|
356
|
-
// in debug mode, we'll see the keys that were added to the agent and more information about the agent
|
|
357
|
-
...(args.debug ? debug : []),
|
|
358
|
-
scpCommand,
|
|
359
|
-
`SCP_EXIT_CODE=$?`,
|
|
360
|
-
`exit $SCP_EXIT_CODE`,
|
|
361
|
-
];
|
|
362
|
-
if (args.debug) {
|
|
363
|
-
// Print commands that can be individually executed to reproduce behavior
|
|
364
|
-
// Remove the debug information - can be executed manually between steps
|
|
365
|
-
const reproCommands = (0, lodash_1.without)([
|
|
366
|
-
"bash",
|
|
367
|
-
...Object.entries(process.env).map(([key, value]) => `export ${key}='${value}'`),
|
|
368
|
-
...Object.entries(credential).map(([key, value]) => `export ${key}='${value}'`),
|
|
369
|
-
...writeStdin,
|
|
370
|
-
], ...debug);
|
|
371
|
-
(0, stdio_1.print2)(`Execute the following commands to create a similar SCP session:\n *** COMMANDS BEGIN ***\n${reproCommands.join("\n")}\n *** COMMANDS END ***`);
|
|
372
|
-
}
|
|
373
|
-
return spawnSsmNode({
|
|
374
|
-
credential,
|
|
375
|
-
writeStdin,
|
|
376
|
-
stdio: ["pipe", "inherit", "pipe"],
|
|
377
|
-
command: "bash",
|
|
378
|
-
args: [],
|
|
379
|
-
});
|
|
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);
|
|
202
|
+
if (cmdArgs.debug) {
|
|
203
|
+
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}`,
|
|
207
|
+
`${command} ${transformForShell(args).join(" ")}`,
|
|
208
|
+
];
|
|
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`);
|
|
210
|
+
}
|
|
211
|
+
return spawnSsmNode({
|
|
212
|
+
credential,
|
|
213
|
+
sshAgentEnv,
|
|
214
|
+
abortController: new AbortController(),
|
|
215
|
+
command,
|
|
216
|
+
args,
|
|
217
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
218
|
+
});
|
|
219
|
+
}));
|
|
380
220
|
});
|
|
381
|
-
exports.
|
|
221
|
+
exports.sshOrScp = sshOrScp;
|
package/dist/plugins/login.js
CHANGED
|
@@ -12,8 +12,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.pluginLoginMap = void 0;
|
|
13
13
|
const login_1 = require("./google/login");
|
|
14
14
|
const login_2 = require("./okta/login");
|
|
15
|
+
const login_3 = require("./ping/login");
|
|
15
16
|
exports.pluginLoginMap = {
|
|
16
17
|
google: login_1.googleLogin,
|
|
17
18
|
okta: login_2.oktaLogin,
|
|
19
|
+
ping: login_3.pingLogin,
|
|
18
20
|
"oidc-pkce": (org) => __awaiter(void 0, void 0, void 0, function* () { return yield exports.pluginLoginMap[org.providerType](org); }),
|
|
19
21
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { TokenResponse } from "../../types/oidc";
|
|
2
|
+
import { OrgData } from "../../types/org";
|
|
3
|
+
export declare const validateProviderDomain: (org: OrgData) => void;
|
|
4
|
+
/** Logs in to an Identity Provider via OIDC */
|
|
5
|
+
export declare const oidcLogin: (org: OrgData, scope: string) => Promise<TokenResponse>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.oidcLogin = exports.validateProviderDomain = void 0;
|
|
16
|
+
/** Copyright © 2024-present P0 Security
|
|
17
|
+
|
|
18
|
+
This file is part of @p0security/cli
|
|
19
|
+
|
|
20
|
+
@p0security/cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
21
|
+
|
|
22
|
+
@p0security/cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
23
|
+
|
|
24
|
+
You should have received a copy of the GNU General Public License along with @p0security/cli. If not, see <https://www.gnu.org/licenses/>.
|
|
25
|
+
**/
|
|
26
|
+
const oidc_1 = require("../../common/auth/oidc");
|
|
27
|
+
const fetch_1 = require("../../common/fetch");
|
|
28
|
+
const stdio_1 = require("../../drivers/stdio");
|
|
29
|
+
const util_1 = require("../../util");
|
|
30
|
+
const lodash_1 = require("lodash");
|
|
31
|
+
const open_1 = __importDefault(require("open"));
|
|
32
|
+
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
33
|
+
const validateProviderDomain = (org) => {
|
|
34
|
+
if (!org.providerDomain)
|
|
35
|
+
throw "Login requires a configured provider domain.";
|
|
36
|
+
};
|
|
37
|
+
exports.validateProviderDomain = validateProviderDomain;
|
|
38
|
+
/** Executes the first step of a device-authorization grant flow */
|
|
39
|
+
// cf. https://developer.okta.com/docs/guides/device-authorization-grant/main/
|
|
40
|
+
const authorize = (org, scope) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
if (org.providerType === undefined) {
|
|
42
|
+
throw "Login requires a configured provider type.";
|
|
43
|
+
}
|
|
44
|
+
const init = {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: oidc_1.OIDC_HEADERS,
|
|
47
|
+
body: (0, fetch_1.urlEncode)({
|
|
48
|
+
client_id: org.clientId,
|
|
49
|
+
scope,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
(0, exports.validateProviderDomain)(org);
|
|
53
|
+
// This is the "org" authorization server; the okta.apps.* scopes are not
|
|
54
|
+
// available with custom authorization servers
|
|
55
|
+
const url = org.providerType === "okta"
|
|
56
|
+
? `https:${org.providerDomain}/oauth2/v1/device/authorize`
|
|
57
|
+
: org.providerType === "ping"
|
|
58
|
+
? `https://${org.providerDomain}/${org.environmentId}/as/device_authorization`
|
|
59
|
+
: (0, util_1.throwAssertNever)(org.providerType);
|
|
60
|
+
const response = yield fetch(url, init);
|
|
61
|
+
yield (0, fetch_1.validateResponse)(response);
|
|
62
|
+
return (yield response.json());
|
|
63
|
+
});
|
|
64
|
+
/** Attempts to fetch this device's OIDC token
|
|
65
|
+
*
|
|
66
|
+
* The authorization may or may not be granted at this stage. If it is not, the
|
|
67
|
+
* authorization server will return "authorization_pending", in which case this
|
|
68
|
+
* function will return undefined.
|
|
69
|
+
*/
|
|
70
|
+
const fetchOidcToken = (org, authorize) => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
+
if (org.providerType === undefined) {
|
|
72
|
+
throw "Login requires a configured provider type.";
|
|
73
|
+
}
|
|
74
|
+
const init = {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: oidc_1.OIDC_HEADERS,
|
|
77
|
+
body: (0, fetch_1.urlEncode)({
|
|
78
|
+
client_id: org.clientId,
|
|
79
|
+
device_code: authorize.device_code,
|
|
80
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
(0, exports.validateProviderDomain)(org);
|
|
84
|
+
const url = org.providerType === "okta"
|
|
85
|
+
? `https:${org.providerDomain}/oauth2/v1/token`
|
|
86
|
+
: org.providerType === "ping"
|
|
87
|
+
? `https://${org.providerDomain}/${org.environmentId}/as/token`
|
|
88
|
+
: (0, util_1.throwAssertNever)(org.providerType);
|
|
89
|
+
const response = yield fetch(url, init);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
if (response.status === 400) {
|
|
92
|
+
const data = yield response.json();
|
|
93
|
+
if (data.error === "authorization_pending")
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
yield (0, fetch_1.validateResponse)(response);
|
|
97
|
+
}
|
|
98
|
+
return (yield response.json());
|
|
99
|
+
});
|
|
100
|
+
/** Waits until user device authorization is complete
|
|
101
|
+
*
|
|
102
|
+
* Returns the OIDC token after completion.
|
|
103
|
+
*/
|
|
104
|
+
const waitForActivation = (org, authorize) => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
while (Date.now() - start <= authorize.expires_in * 1e3) {
|
|
107
|
+
const response = yield fetchOidcToken(org, authorize);
|
|
108
|
+
if (!response)
|
|
109
|
+
yield (0, util_1.sleep)(authorize.interval * 1e3);
|
|
110
|
+
else
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
throw "Expired awaiting in-browser authorization.";
|
|
114
|
+
});
|
|
115
|
+
/** Logs in to an Identity Provider via OIDC */
|
|
116
|
+
const oidcLogin = (org, scope) => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
if (org.providerType === undefined) {
|
|
118
|
+
throw "Login requires a configured provider type.";
|
|
119
|
+
}
|
|
120
|
+
const authorizeResponse = yield authorize(org, scope);
|
|
121
|
+
(0, stdio_1.print2)(`Please use the opened browser window to continue your P0 login.
|
|
122
|
+
|
|
123
|
+
When prompted, confirm that ${(0, lodash_1.capitalize)(org.providerType)} displays this code:
|
|
124
|
+
|
|
125
|
+
${authorizeResponse.user_code}
|
|
126
|
+
|
|
127
|
+
Waiting for authorization...
|
|
128
|
+
`);
|
|
129
|
+
void (0, open_1.default)(authorizeResponse.verification_uri_complete);
|
|
130
|
+
const oidcResponse = yield waitForActivation(org, authorizeResponse);
|
|
131
|
+
return oidcResponse;
|
|
132
|
+
});
|
|
133
|
+
exports.oidcLogin = oidcLogin;
|
|
@@ -8,9 +8,6 @@ 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
12
|
exports.getSamlResponse = exports.oktaLogin = void 0;
|
|
16
13
|
/** Copyright © 2024-present P0 Security
|
|
@@ -25,81 +22,13 @@ You should have received a copy of the GNU General Public License along with @p0
|
|
|
25
22
|
**/
|
|
26
23
|
const oidc_1 = require("../../common/auth/oidc");
|
|
27
24
|
const fetch_1 = require("../../common/fetch");
|
|
28
|
-
const
|
|
29
|
-
const util_1 = require("../../util");
|
|
25
|
+
const login_1 = require("../oidc/login");
|
|
30
26
|
const jsdom_1 = require("jsdom");
|
|
31
27
|
const lodash_1 = require("lodash");
|
|
32
|
-
const open_1 = __importDefault(require("open"));
|
|
33
|
-
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
34
28
|
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
35
29
|
const ID_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token";
|
|
36
30
|
const TOKEN_EXCHANGE_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
37
31
|
const WEB_SSO_TOKEN_TYPE = "urn:okta:oauth:token-type:web_sso_token";
|
|
38
|
-
const validateProviderDomain = (org) => {
|
|
39
|
-
if (!org.providerDomain)
|
|
40
|
-
throw "Okta login requires a configured provider domain.";
|
|
41
|
-
};
|
|
42
|
-
/** Executes the first step of Okta's device-authorization grant flow */
|
|
43
|
-
// cf. https://developer.okta.com/docs/guides/device-authorization-grant/main/
|
|
44
|
-
const authorize = (org) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
-
const init = {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: oidc_1.OIDC_HEADERS,
|
|
48
|
-
body: (0, fetch_1.urlEncode)({
|
|
49
|
-
client_id: org.clientId,
|
|
50
|
-
scope: "openid email profile okta.apps.sso",
|
|
51
|
-
}),
|
|
52
|
-
};
|
|
53
|
-
validateProviderDomain(org);
|
|
54
|
-
// This is the "org" authorization server; the okta.apps.* scopes are not
|
|
55
|
-
// available with custom authorization servers
|
|
56
|
-
const response = yield fetch(`https:${org.providerDomain}/oauth2/v1/device/authorize`, init);
|
|
57
|
-
yield (0, fetch_1.validateResponse)(response);
|
|
58
|
-
return (yield response.json());
|
|
59
|
-
});
|
|
60
|
-
/** Attempts to fetch this device's OIDC token
|
|
61
|
-
*
|
|
62
|
-
* The authorization may or may not be granted at this stage. If it is not, the
|
|
63
|
-
* authorization server will return "authorization_pending", in which case this
|
|
64
|
-
* function will return undefined.
|
|
65
|
-
*/
|
|
66
|
-
const fetchOidcToken = (org, authorize) => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
-
const init = {
|
|
68
|
-
method: "POST",
|
|
69
|
-
headers: oidc_1.OIDC_HEADERS,
|
|
70
|
-
body: (0, fetch_1.urlEncode)({
|
|
71
|
-
client_id: org.clientId,
|
|
72
|
-
device_code: authorize.device_code,
|
|
73
|
-
grant_type: DEVICE_GRANT_TYPE,
|
|
74
|
-
}),
|
|
75
|
-
};
|
|
76
|
-
validateProviderDomain(org);
|
|
77
|
-
const response = yield fetch(`https:${org.providerDomain}/oauth2/v1/token`, init);
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
if (response.status === 400) {
|
|
80
|
-
const data = yield response.json();
|
|
81
|
-
if (data.error === "authorization_pending")
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
yield (0, fetch_1.validateResponse)(response);
|
|
85
|
-
}
|
|
86
|
-
return (yield response.json());
|
|
87
|
-
});
|
|
88
|
-
/** Waits until user device authorization is complete
|
|
89
|
-
*
|
|
90
|
-
* Returns the OIDC token after completion.
|
|
91
|
-
*/
|
|
92
|
-
const waitForActivation = (org, authorize) => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
-
const start = Date.now();
|
|
94
|
-
while (Date.now() - start <= authorize.expires_in * 1e3) {
|
|
95
|
-
const response = yield fetchOidcToken(org, authorize);
|
|
96
|
-
if (!response)
|
|
97
|
-
yield (0, util_1.sleep)(authorize.interval * 1e3);
|
|
98
|
-
else
|
|
99
|
-
return response;
|
|
100
|
-
}
|
|
101
|
-
throw "Expired awaiting in-browser authorization.";
|
|
102
|
-
});
|
|
103
32
|
/** Exchanges an Okta OIDC SSO token for an Okta app SSO token */
|
|
104
33
|
const fetchSsoWebToken = (appId, { org, credential }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
34
|
const init = {
|
|
@@ -116,7 +45,7 @@ const fetchSsoWebToken = (appId, { org, credential }) => __awaiter(void 0, void
|
|
|
116
45
|
requested_token_type: WEB_SSO_TOKEN_TYPE,
|
|
117
46
|
}),
|
|
118
47
|
};
|
|
119
|
-
validateProviderDomain(org);
|
|
48
|
+
(0, login_1.validateProviderDomain)(org);
|
|
120
49
|
const response = yield fetch(`https:${org.providerDomain}/oauth2/v1/token`, init);
|
|
121
50
|
yield (0, fetch_1.validateResponse)(response);
|
|
122
51
|
return (yield response.json());
|
|
@@ -127,7 +56,7 @@ const fetchSamlResponse = (org, { access_token }) => __awaiter(void 0, void 0, v
|
|
|
127
56
|
method: "GET",
|
|
128
57
|
headers: (0, lodash_1.omit)(oidc_1.OIDC_HEADERS, "Content-Type"),
|
|
129
58
|
};
|
|
130
|
-
validateProviderDomain(org);
|
|
59
|
+
(0, login_1.validateProviderDomain)(org);
|
|
131
60
|
const url = `https://${org.providerDomain}/login/token/sso?token=${encodeURIComponent(access_token)}`;
|
|
132
61
|
const response = yield fetch(url, init);
|
|
133
62
|
yield (0, fetch_1.validateResponse)(response);
|
|
@@ -137,20 +66,7 @@ const fetchSamlResponse = (org, { access_token }) => __awaiter(void 0, void 0, v
|
|
|
137
66
|
return samlInput === null || samlInput === void 0 ? void 0 : samlInput.value;
|
|
138
67
|
});
|
|
139
68
|
/** Logs in to Okta via OIDC */
|
|
140
|
-
const oktaLogin = (org) => __awaiter(void 0, void 0, void 0, function* () {
|
|
141
|
-
const authorizeResponse = yield authorize(org);
|
|
142
|
-
(0, stdio_1.print2)(`Please use the opened browser window to continue your P0 login.
|
|
143
|
-
|
|
144
|
-
When prompted, confirm that Okta displays this code:
|
|
145
|
-
|
|
146
|
-
${authorizeResponse.user_code}
|
|
147
|
-
|
|
148
|
-
Waiting for authorization...
|
|
149
|
-
`);
|
|
150
|
-
void (0, open_1.default)(authorizeResponse.verification_uri_complete);
|
|
151
|
-
const oidcResponse = yield waitForActivation(org, authorizeResponse);
|
|
152
|
-
return oidcResponse;
|
|
153
|
-
});
|
|
69
|
+
const oktaLogin = (org) => __awaiter(void 0, void 0, void 0, function* () { return (0, login_1.oidcLogin)(org, "openid email profile okta.apps.sso"); });
|
|
154
70
|
exports.oktaLogin = oktaLogin;
|
|
155
71
|
/** Retrieves a SAML response for an okta app */
|
|
156
72
|
// TODO: Inject Okta app
|