@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.
@@ -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.
@@ -22,6 +22,7 @@ export declare type ScpCommandArgs = BaseSshCommandArgs & {
22
22
  source: string;
23
23
  destination: string;
24
24
  recursive?: boolean;
25
+ debug?: boolean;
25
26
  };
26
27
  export declare type SshCommandArgs = BaseSshCommandArgs & {
27
28
  sudo?: boolean;
@@ -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<void>;
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
- const spawnSsmNode = (options) => __awaiter(void 0, void 0, void 0, function* () {
142
- return new Promise((resolve, reject) => {
143
- const child = spawnChildProcess(options.credential, options.command, options.args, ["inherit", "inherit", "pipe"]);
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
- exitListener.unref();
148
- // In the case of ephemeral AccessDenied exceptions due to unpropagated
149
- // permissions, continually retry access until success
150
- if (!isAccessPropagated()) {
151
- const attemptsRemaining = (_a = options === null || options === void 0 ? void 0 : options.attemptsRemaining) !== null && _a !== void 0 ? _a : MAX_SSM_RETRIES;
152
- if (attemptsRemaining <= 0) {
153
- reject("Access did not propagate through AWS before max retry attempts were exceeded. Please contact support@p0.dev for assistance.");
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
- spawnSsmNode(Object.assign(Object.assign({}, options), { attemptsRemaining: attemptsRemaining - 1 }))
157
- .then((code) => resolve(code))
158
- .catch(reject);
159
- return;
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 createScpCommand = (data, args) => {
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
- // TODO: add support for original SSH too.
305
- return [
306
- "scp",
307
- "-o",
308
- `ProxyCommand='${ssmCommand.join(" ")}'`,
309
- // if a response is not received after three 5 minute attempts,
310
- // the connection will be closed.
311
- "-o",
312
- "ServerAliveCountMax=3",
313
- `-o`,
314
- "ServerAliveInterval=300",
315
- ...(args.recursive ? ["-r"] : []),
316
- args.source,
317
- args.destination,
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 command = createScpCommand(data, args).join(" ");
329
- if (!privateKey) {
330
- throw "Failed to load a private key for this request. Please contact support@p0.dev for assistance.";
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
- yield executeScpCommand(credential, command, privateKey);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p0security/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Execute infra CLI commands with P0 grants",
5
5
  "main": "index.ts",
6
6
  "repository": {