Skip to content

Commit

Permalink
fixup! ls: Show keys and values
Browse files Browse the repository at this point in the history
  • Loading branch information
nbrahms committed Feb 28, 2024
2 parents 7eedeb9 + 94a2fce commit 52463b4
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 24 deletions.
108 changes: 106 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Supports creating access requests for cloud resources, assuming AWS roles, and c
- [Installation](#installation)
- [Configuration](#configuration)
- [Command Reference](#command-reference)
- [Example Usage](#example-usage)
- [Support](#support)
- [Contributing](#contributing)
- [Copyright](#copyright)
Expand Down Expand Up @@ -79,9 +80,112 @@ To view help, use the `--help` option with any command.
p0 login <org> Log in to p0 using a web browser
p0 ls [arguments..] List request-command arguments
p0 request [arguments..] Manually request permissions on a resource
p0 ssh <instance> SSH into a virtual machine
p0 ssh <destination> SSH into a virtual machine
```

## Example Usage

### Create an access request

To view the resources available for access requests, run:

```
p0 request --help
```

Sample output:

```
Request access to a resource using P0
Commands:
p0 request aws Amazon Web Services
p0 request azure-ad Entra ID
p0 request gcloud Google Cloud
p0 request okta Okta
p0 request ssh <destination> Secure Shell (SSH) session
p0 request workspace Google Workspace
Options:
--help Show help [boolean]
--reason Reason access is needed [string]
-w, --wait Block until the request is completed [boolean]
```

Run `--help` on any of these commands for information on requesting that resource. For example, to request a Google Cloud role, run

```
p0 request gcloud --help
```

```
Google Cloud
Commands:
p0 request gcloud resource <accesses..> GCP resource
p0 request gcloud role <names..> Custom or predefined role
p0 request gcloud permission <names..> GCP permissions
Options:
--help Show help [boolean]
--reason Reason access is needed [string]
-w, --wait Block until the request is completed [boolean]
```

If you don't know the name of the role you need, you can use the `p0 ls` command. `p0 ls` accepts the same arguments that you provide to `p0 request` and lists the available options for access within your selected resource. For example, to view the available Google Cloud roles, run

```
p0 ls gcloud role names --like bigquery
```

Now, to request `bigquery.admin`, run:

```
p0 request gcloud role bigquery.admin
```

This will create an access request on Slack. Once your access request is approved, you will automatically get access to the Bigquery Admin role.

### Assume an AWS IAM Role

You can use the P0 CLI to assume a role in AWS.

To use this feature, you will need to have installed and configured the AWS CLI. If you have not done so already, you can follow the [installation steps](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).

List the roles that you have permissions to assume via:

```
p0 aws role ls
```

If you don't see your desired role, you will first need to request access to it. You can do that with `p0 request aws role <ROLE_NAME>`.

Once you have permissions, you can run

```
$(p0 aws role assume <ROLE_NAME>)
```

### SSH into an AWS Instance

You can request access to an AWS instance and open a SSH session once access is provisioned with a single command in the P0 CLI.

To use this feature, you will need to have installed and configured the AWS CLI and the Session Manager plugin. If you have not done so already, you can follow the [AWS CLI installation steps](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [Session Manager plugin installation step](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html).

To see the available AWS instances, run:

```
p0 ls ssh destination
```

You can start a SSH session with:

```
p0 ssh <INSTANCE_NAME>
```

If you already have access, this will directly open the SSH session. Otherwise, it will request access, wait for approval, and open a SSH session once the access is provisioned.

## Support

If you encounter any issues with the P0 CLI, you can open a GitHub issue on this repo, email `support@p0.dev`, or reach out to us on our [community slack](https://join.slack.com/t/p0securitycommunity/shared_invite/zt-1zouzlovp-1kuym9RfuzkJ17ZlvAf6mQ).
Expand All @@ -94,4 +198,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)

Copyright © 2024-present P0 Security.

The P0 Security CLI is licensed under the terms of the GNU General Public License version 3. See [COPYING.md](COPYING.md) for details.
The P0 Security CLI is licensed under the terms of the GNU General Public License version 3. See [LICENSE.md](LICENSE.md) for details.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"p0",
"README.md",
"CONTRIBUTING.md",
"COPYING.md"
"LICENSE.md"
],
"dependencies": {
"@rgrove/parse-xml": "^4.1.0",
Expand Down
20 changes: 15 additions & 5 deletions src/commands/__tests__/__snapshots__/ls.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,31 @@ Unknown argument: foo",
}
`;
exports[`ls when valid ls command should print list response 1`] = `
exports[`ls when valid ls command should print friendly message if no items: stderr 1`] = `
[
[
"instance-1[02m - Instance 1[00m",
"No destinations",
],
]
`;
exports[`ls when valid ls command should print friendly message if no items: stdout 1`] = `[]`;
exports[`ls when valid ls command should print list response: stderr 1`] = `
[
[
"instance-2[02m - Instance 2[00m",
"Showing destinations:",
],
]
`;
exports[`ls when valid ls command should print list response 2`] = `
exports[`ls when valid ls command should print list response: stdout 1`] = `
[
[
"Showing destinations:",
"instance-1 - Group / Resource 1",
],
[
"instance-2 - Resource 2",
],
]
`;
25 changes: 17 additions & 8 deletions src/commands/__tests__/ls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,34 @@ const mockPrint1 = print1 as jest.Mock;
const mockPrint2 = print2 as jest.Mock;

describe("ls", () => {
beforeEach(() => jest.clearAllMocks());

describe("when valid ls command", () => {
const command = "ls ssh destination";

beforeAll(() => {
const mockItems = (items: object[]) =>
mockFetchCommand.mockResolvedValue({
ok: true,
term: "",
arg: "destination",
items: [
{ key: "instance-1", value: "Instance 1" },
{ key: "instance-2", value: "Instance 2" },
],
items,
});
});

it("should print list response", async () => {
mockItems([
{ key: "instance-1", group: "Group", value: "Resource 1" },
{ key: "instance-2", value: "Resource 2" },
]);
await lsCommand(yargs()).parse(command);
expect(mockPrint1.mock.calls).toMatchSnapshot("stdout");
expect(mockPrint2.mock.calls).toMatchSnapshot("stderr");
});

it("should print friendly message if no items", async () => {
mockItems([]);
await lsCommand(yargs()).parse(command);
expect(mockPrint1.mock.calls).toMatchSnapshot();
expect(mockPrint2.mock.calls).toMatchSnapshot();
expect(mockPrint1.mock.calls).toMatchSnapshot("stdout");
expect(mockPrint2.mock.calls).toMatchSnapshot("stderr");
});
});

Expand Down
8 changes: 6 additions & 2 deletions src/commands/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,22 @@ const ls = async (
const allArguments = [...args._, ...args.arguments];

if (data && "ok" in data && data.ok) {
const label = pluralize(data.arg);
if (data.items.length === 0) {
print2(`No ${label}`);
return;
}
const truncationPart = data.isTruncated
? ` the first ${data.items.length}`
: "";
const argPart = pluralize(data.arg);
const postfixPart = data.term
? ` matching '${data.term}'`
: data.isTruncated
? ` (use \`p0
${allArguments.join(" ")} <like>\` to narrow results)`
: "";

print2(`Showing${truncationPart} ${argPart}${postfixPart}:`);
print2(`Showing${truncationPart} ${label}${postfixPart}:`);
const isSameValue = data.items.every((i) => !i.group && i.key === i.value);
const maxLength = max(data.items.map((i) => i.key.length)) || 0;
for (const item of data.items) {
Expand Down
27 changes: 23 additions & 4 deletions src/commands/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,27 @@ import { pick } from "lodash";
import yargs from "yargs";

export type SshCommandArgs = {
instance: string;
destination: string;
command?: string;
L?: string; // port forwarding option
arguments: string[];
};

// Matches strings with the pattern "digits:digits" (e.g. 1234:5678)
const LOCAL_PORT_FORWARD_PATTERN = /^\d+:\d+$/;

/** Maximum amount of time to wait after access is approved to wait for access
* to be configured
*/
const GRANT_TIMEOUT_MILLIS = 60e3;

export const sshCommand = (yargs: yargs.Argv) =>
yargs.command<SshCommandArgs>(
"ssh <instance> [command [arguments..]]",
"ssh <destination> [command [arguments..]]",
"SSH into a virtual machine",
(yargs) =>
yargs
.positional("instance", {
.positional("destination", {
type: "string",
demandOption: true,
})
Expand All @@ -57,6 +61,20 @@ export const sshCommand = (yargs: yargs.Argv) =>
array: true,
string: true,
default: [] as string[],
})
.check((argv: yargs.ArgumentsCamelCase<SshCommandArgs>) => {
if (argv.L == null) return true;

return (
argv.L.match(LOCAL_PORT_FORWARD_PATTERN) ||
"Local port forward should be in the format `local_port:remote_port`"
);
})
.option("L", {
type: "string",
describe:
// the order of the sockets in the address matches the ssh man page
"Forward a local port to the remote host; `local_socket:remote_socket`",
}),
guard(ssh)
);
Expand Down Expand Up @@ -125,12 +143,13 @@ const waitForProvisioning = async <P extends PluginRequest>(
* - AWS EC2 via SSM with Okta SAML
*/
const ssh = async (args: yargs.ArgumentsCamelCase<SshCommandArgs>) => {
// Prefix is required because the backend uses it to determine that this is an AWS request
const authn = await authenticate();
await validateSshInstall(authn);
const response = await request(
{
...pick(args, "$0", "_"),
arguments: ["ssh", args.instance, "--provider", "aws"],
arguments: ["ssh", args.destination, "--provider", "aws"],
wait: true,
},
authn,
Expand Down
26 changes: 24 additions & 2 deletions src/plugins/aws/ssm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ const MAX_SSM_RETRIES = 30;
const INSTANCE_ARN_PATTERN =
/^arn:aws:ssm:([^:]+):([^:]+):managed-instance\/([^:]+)$/;

/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
const LOCAL_PORT_FORWARDING_DOCUMENT_NAME = "AWS-StartPortForwardingSession";

type SsmArgs = {
instance: string;
region: string;
requestId: string;
documentName: string;
credential: AwsCredentials;
command?: string;
forwardPortAddress?: string;
};

/** Checks if access has propagated through AWS to the SSM agent
Expand Down Expand Up @@ -88,6 +92,12 @@ const accessPropagationGuard = (
};

const createSsmCommand = (args: Omit<SsmArgs, "requestId">) => {
const hasCommand = args.command && args.command.trim();

if (hasCommand && args.forwardPortAddress) {
throw "Invalid arguments. Specify either a command or port forwarding, not both.";
}

const ssmCommand = [
"aws",
"ssm",
Expand All @@ -97,11 +107,22 @@ const createSsmCommand = (args: Omit<SsmArgs, "requestId">) => {
"--target",
args.instance,
"--document-name",
args.documentName,
// Port forwarding is a special case that uses an AWS-managed document and
// not the user-generated document we use for an SSH session
args.forwardPortAddress
? LOCAL_PORT_FORWARDING_DOCUMENT_NAME
: args.documentName,
];

if (args.command && args.command.trim()) {
if (hasCommand) {
ssmCommand.push("--parameters", `command='${args.command}'`);
} else if (args.forwardPortAddress) {
const [localPort, remotePort] = args.forwardPortAddress.split(":");

ssmCommand.push(
"--parameters",
`localPortNumber=${localPort},portNumber=${remotePort}`
);
}

return ssmCommand;
Expand Down Expand Up @@ -193,6 +214,7 @@ export const ssm = async (
region: region!,
documentName: request.generated.documentName,
requestId: request.id,
forwardPortAddress: args.L,
credential,
command: commandParameter(args),
};
Expand Down

0 comments on commit 52463b4

Please sign in to comment.