@p0security/cli 0.6.0 → 0.6.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/scp.js
CHANGED
|
@@ -58,6 +58,10 @@ const scpCommand = (yargs) => yargs.command("scp <source> <destination>",
|
|
|
58
58
|
.option("sudo", {
|
|
59
59
|
type: "boolean",
|
|
60
60
|
describe: "Add user to sudoers file",
|
|
61
|
+
})
|
|
62
|
+
.option("debug", {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
describe: "Print debug information, dangerous for sensitive data",
|
|
61
65
|
}), (0, firestore_1.guard)(scpAction));
|
|
62
66
|
exports.scpCommand = scpCommand;
|
|
63
67
|
/** Transfers files between a local and remote hosts using SSH.
|
|
@@ -12,4 +12,4 @@ import { ExerciseGrantResponse, ScpCommandArgs, SshCommandArgs } from "../../../
|
|
|
12
12
|
import { Authn } from "../../../types/identity";
|
|
13
13
|
/** Connect to an SSH backend using AWS Systems Manager (SSM) */
|
|
14
14
|
export declare const ssm: (authn: Authn, request: ExerciseGrantResponse, args: SshCommandArgs) => Promise<void>;
|
|
15
|
-
export declare const scp: (authn: Authn, data: ExerciseGrantResponse, args: ScpCommandArgs, privateKey: string) => Promise<
|
|
15
|
+
export declare const scp: (authn: Authn, data: ExerciseGrantResponse, args: ScpCommandArgs, privateKey: string) => Promise<number | null>;
|
|
@@ -16,6 +16,7 @@ exports.scp = exports.ssm = void 0;
|
|
|
16
16
|
const stdio_1 = require("../../../drivers/stdio");
|
|
17
17
|
const aws_1 = require("../../okta/aws");
|
|
18
18
|
const install_1 = require("./install");
|
|
19
|
+
const lodash_1 = require("lodash");
|
|
19
20
|
const node_child_process_1 = require("node:child_process");
|
|
20
21
|
const node_stream_1 = require("node:stream");
|
|
21
22
|
const ps_tree_1 = __importDefault(require("ps-tree"));
|
|
@@ -138,54 +139,39 @@ function spawnChildProcess(credential, command, args, stdio) {
|
|
|
138
139
|
*
|
|
139
140
|
* Requires `aws ssm` to be installed on the client machine.
|
|
140
141
|
*/
|
|
141
|
-
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
const { isAccessPropagated } = accessPropagationGuard(child);
|
|
145
|
-
const exitListener = child.on("exit", (code) => {
|
|
142
|
+
function spawnSsmNode(options) {
|
|
143
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
146
145
|
var _a, _b;
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
151
|
+
const { isAccessPropagated } = accessPropagationGuard(child);
|
|
152
|
+
const exitListener = child.on("exit", (code) => {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
exitListener.unref();
|
|
155
|
+
// In the case of ephemeral AccessDenied exceptions due to unpropagated
|
|
156
|
+
// permissions, continually retry access until success
|
|
157
|
+
if (!isAccessPropagated()) {
|
|
158
|
+
const attemptsRemaining = (_a = options === null || options === void 0 ? void 0 : options.attemptsRemaining) !== null && _a !== void 0 ? _a : MAX_SSM_RETRIES;
|
|
159
|
+
if (attemptsRemaining <= 0) {
|
|
160
|
+
reject("Access did not propagate through AWS before max retry attempts were exceeded. Please contact support@p0.dev for assistance.");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
spawnSsmNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
|
|
164
|
+
.then((code) => resolve(code))
|
|
165
|
+
.catch(reject);
|
|
154
166
|
return;
|
|
155
167
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
(_b = options.abortController) === null || _b === void 0 ? void 0 : _b.abort(code);
|
|
162
|
-
(0, stdio_1.print2)(`SSH session terminated`);
|
|
163
|
-
resolve(code);
|
|
168
|
+
(_b = options.abortController) === null || _b === void 0 ? void 0 : _b.abort(code);
|
|
169
|
+
(0, stdio_1.print2)(`SSH session terminated`);
|
|
170
|
+
resolve(code);
|
|
171
|
+
});
|
|
164
172
|
});
|
|
165
173
|
});
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Spawns a child process to add a private key to the ssh-agent. The SSH agent is included in the OpenSSH suite of tools
|
|
169
|
-
* and is used to hold private keys during a session. The SSH agent typically does not persist keys across system reboots
|
|
170
|
-
* or logout/login cycles. Once you log out or restart your system, any keys added to the SSH agent during that session
|
|
171
|
-
* will need to be added again in subsequent sessions.
|
|
172
|
-
*/
|
|
173
|
-
const executeScpCommand = (credential, command, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
174
|
-
// Execute should not leave any ssh-agent processes running after it's done performing an SCP transaction.
|
|
175
|
-
const execute = `
|
|
176
|
-
eval $(ssh-agent) >/dev/null 2>&1
|
|
177
|
-
trap 'kill $SSH_AGENT_PID' EXIT
|
|
178
|
-
ssh-add -q - <<< '${privateKey}'
|
|
179
|
-
${command}
|
|
180
|
-
SCP_EXIT_CODE=$?
|
|
181
|
-
exit $SCP_EXIT_CODE
|
|
182
|
-
`;
|
|
183
|
-
return spawnSsmNode({
|
|
184
|
-
credential,
|
|
185
|
-
command: "bash",
|
|
186
|
-
args: ["-c", execute],
|
|
187
|
-
});
|
|
188
|
-
});
|
|
174
|
+
}
|
|
189
175
|
/**
|
|
190
176
|
* A subprocess SSM session redirects its output through a proxy that filters certain messages reducing the verbosity of the output.
|
|
191
177
|
* The subprocess also makes sure to terminate any grandchild processes that might spawn during the session.
|
|
@@ -283,14 +269,14 @@ const startSsmProcesses = (credential, commands) => __awaiter(void 0, void 0, vo
|
|
|
283
269
|
const abortController = new AbortController();
|
|
284
270
|
const args = { credential, abortController };
|
|
285
271
|
const processes = [
|
|
286
|
-
spawnSsmNode(Object.assign(Object.assign({}, args), { command: "/usr/bin/env", args: commands.shellCommand })),
|
|
272
|
+
spawnSsmNode(Object.assign(Object.assign({}, args), { command: "/usr/bin/env", args: commands.shellCommand, stdio: ["inherit", "inherit", "pipe"] })),
|
|
287
273
|
];
|
|
288
274
|
if (commands.subCommand) {
|
|
289
275
|
processes.push(spawnSubprocessSsmNode(Object.assign(Object.assign({}, args), { command: commands.subCommand })));
|
|
290
276
|
}
|
|
291
277
|
yield Promise.all(processes);
|
|
292
278
|
});
|
|
293
|
-
const
|
|
279
|
+
const createProxyCommands = (data, args, debug) => {
|
|
294
280
|
const ssmCommand = [
|
|
295
281
|
...createBaseSsmCommand({
|
|
296
282
|
region: data.instance.region,
|
|
@@ -301,34 +287,92 @@ const createScpCommand = (data, args) => {
|
|
|
301
287
|
"--parameters",
|
|
302
288
|
'"portNumber=%p"',
|
|
303
289
|
];
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
290
|
+
return {
|
|
291
|
+
scp: [
|
|
292
|
+
"scp",
|
|
293
|
+
...(debug ? ["-v"] : []),
|
|
294
|
+
"-o",
|
|
295
|
+
`ProxyCommand='${ssmCommand.join(" ")}'`,
|
|
296
|
+
// if a response is not received after three 5 minute attempts,
|
|
297
|
+
// the connection will be closed.
|
|
298
|
+
"-o",
|
|
299
|
+
"ServerAliveCountMax=3",
|
|
300
|
+
`-o`,
|
|
301
|
+
"ServerAliveInterval=300",
|
|
302
|
+
...(args.recursive ? ["-r"] : []),
|
|
303
|
+
args.source,
|
|
304
|
+
args.destination,
|
|
305
|
+
],
|
|
306
|
+
ssh: [
|
|
307
|
+
"ssh",
|
|
308
|
+
...(debug ? ["-v"] : []),
|
|
309
|
+
"-o",
|
|
310
|
+
`ProxyCommand='${ssmCommand.join(" ")}'`,
|
|
311
|
+
`${data.linuxUserName}@${data.instance.id}`,
|
|
312
|
+
],
|
|
313
|
+
ssm: ssmCommand,
|
|
314
|
+
};
|
|
319
315
|
};
|
|
320
316
|
const scp = (authn, data, args, privateKey) => __awaiter(void 0, void 0, void 0, function* () {
|
|
321
317
|
if (!(yield (0, install_1.ensureSsmInstall)())) {
|
|
322
318
|
throw "Please try again after installing the required AWS utilities";
|
|
323
319
|
}
|
|
320
|
+
if (!privateKey) {
|
|
321
|
+
throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
|
|
322
|
+
}
|
|
324
323
|
const credential = yield (0, aws_1.assumeRoleWithOktaSaml)(authn, {
|
|
325
324
|
account: data.instance.accountId,
|
|
326
325
|
role: data.role,
|
|
327
326
|
});
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
const commands = createProxyCommands(data, args, args.debug);
|
|
328
|
+
const scpCommand = commands.scp.join(" ");
|
|
329
|
+
const sshCommand = commands.ssh.join(" ");
|
|
330
|
+
const debug = [
|
|
331
|
+
`echo "SSH_AUTH_SOCK: $SSH_AUTH_SOCK"`,
|
|
332
|
+
`echo "SSH_AGENT_PID: $SSH_AGENT_PID"`,
|
|
333
|
+
`echo '$(p0 aws role assume ${data.role})'`,
|
|
334
|
+
`echo "${sshCommand}"`,
|
|
335
|
+
`echo "${scpCommand}"`,
|
|
336
|
+
`echo "SSH Agent Keys:"`,
|
|
337
|
+
`ssh-add -l`,
|
|
338
|
+
];
|
|
339
|
+
/**
|
|
340
|
+
* Spawns a child process to add a private key to the ssh-agent. The SSH agent is included in the OpenSSH suite
|
|
341
|
+
* of tools and is used to hold private keys during a session. The SSH agent typically does not persist keys
|
|
342
|
+
* across system reboots or logout/login cycles. Once you log out or restart your system, any keys added to
|
|
343
|
+
* the SSH agent during that session will need to be added again in subsequent sessions.
|
|
344
|
+
*/
|
|
345
|
+
const writeStdin = [
|
|
346
|
+
// This might be overkill because we are already spawning a subprocess that will run the commands for us
|
|
347
|
+
// but just in case someone enters that subprocess we're also disabling the history of commands run.
|
|
348
|
+
`unset HISTFILE`,
|
|
349
|
+
// in debug mode, we want to see the pid of the ssh-agent and compare it to the environment variable
|
|
350
|
+
`eval $(ssh-agent)${args.debug ? "" : " >/dev/null 2>&1"}`,
|
|
351
|
+
`trap 'kill $SSH_AGENT_PID' EXIT`,
|
|
352
|
+
`ssh-add -q - <<< '${privateKey}'`,
|
|
353
|
+
// in debug mode, we'll see the keys that were added to the agent and more information about the agent
|
|
354
|
+
...(args.debug ? debug : []),
|
|
355
|
+
scpCommand,
|
|
356
|
+
`SCP_EXIT_CODE=$?`,
|
|
357
|
+
`exit $SCP_EXIT_CODE`,
|
|
358
|
+
];
|
|
359
|
+
if (args.debug) {
|
|
360
|
+
// Print commands that can be individually executed to reproduce behavior
|
|
361
|
+
// Remove the debug information - can be executed manually between steps
|
|
362
|
+
const reproCommands = (0, lodash_1.without)([
|
|
363
|
+
"bash",
|
|
364
|
+
...Object.entries(process.env).map(([key, value]) => `export ${key}='${value}'`),
|
|
365
|
+
...Object.entries(credential).map(([key, value]) => `export ${key}='${value}'`),
|
|
366
|
+
...writeStdin,
|
|
367
|
+
], ...debug);
|
|
368
|
+
(0, stdio_1.print2)(`Execute the following commands to create a similar SCP session:\n *** COMMANDS BEGIN ***\n${reproCommands.join("\n")}\n *** COMMANDS END ***`);
|
|
331
369
|
}
|
|
332
|
-
|
|
370
|
+
return spawnSsmNode({
|
|
371
|
+
credential,
|
|
372
|
+
writeStdin,
|
|
373
|
+
stdio: ["pipe", "inherit", "pipe"],
|
|
374
|
+
command: "bash",
|
|
375
|
+
args: [],
|
|
376
|
+
});
|
|
333
377
|
});
|
|
334
378
|
exports.scp = scp;
|