Skip to content

Commit 54b6b0e

Browse files
authored
working cmd pattern (#23)
1 parent 062e662 commit 54b6b0e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+3598
-2503
lines changed

CHANGELOG.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
The intended audience of this file is for py42 consumers -- as such, changes that don't affect
99
how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here.
1010

11+
## Unreleased
12+
13+
### Added
14+
15+
- `code42 profile create` command.
16+
17+
### Removed
18+
19+
- `code42 profile set` command. Use `code42 profile create` instead.
20+
1121
## 0.4.4 - 2020-04-01
1222

13-
###Added
23+
### Added
1424

1525
- Added message to STDERR when no results are found
1626

17-
###Fixed
27+
### Fixed
1828

1929
- Add milliseconds to end timestamp, to represent end of day with milliseconds precision.
2030

21-
## Unreleased
22-
2331
## 0.4.3 - 2020-03-17
2432

2533
### Added

CONTRIBUTING.md

+47-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ $ pre-commit install
1919
```
2020

2121
This will set up a pre-commit hook that will automatically format your code to our desired styles whenever you commit.
22-
It requires python 3.6 to run, so be sure to have a python 3.6 executable of some kind in your PATH when you commit.
22+
It requires python 3.6+ to run, so be sure to have a qualifying python executable in your PATH when you commit.
2323

2424
## General
2525

@@ -75,3 +75,49 @@ Example:
7575
```python
7676
def test_add_one_and_one_equals_two():
7777
```
78+
79+
### Adding a new command
80+
81+
See class documentation on the [Command](src/code42cli/commands.py) class for an explanation of its constructor parameters.
82+
83+
1. If you are creating a new top-level command, create a new instance of `Command` and add it to the list returned
84+
by `_load_top_commands()` function in `code42cli.main`.
85+
86+
2. If you are creating a new subcommand, find the top-level command that this will be a subcommand of in
87+
`_load_top_commands()` in `code42cli.main` and navigate to the function assigned to be its subcommand loader.
88+
Then, add a new instance of `Command` to the list returned by that function.
89+
90+
3. For commands that actually are executed (rather than just being groups), you will add a `handler` function as a constructor parameter.
91+
This will be the function that you want to execute when your command is run.
92+
* _Positional_ arguments of the handler will automatically become _required_ cli arguments
93+
* _Keyword_ arguments of the handler will automatically become _optional_ cli arguments
94+
* the cli argument name will be the same as the handler param name except with `_` replaced with `-`, and prefixed with `--` if optional
95+
96+
For example, consider the following python function:
97+
98+
```python
99+
def handler_example(one, two, three=None, four=None):
100+
pass
101+
```
102+
103+
When the above function is supplied as a `Command`'s `handler` parameter, the result will be a command that can be executed as follows
104+
(assuming `cmd` is the name given to the command):
105+
106+
```bash
107+
$ code42 cmd oneval twoval --three threeval --four fourval
108+
```
109+
110+
4. To add descriptions to your cli arguments to appear in the help text, your command takes a function as the `arg_customizer` parameter.
111+
The entire [`ArgConfigCollection`](src/code42cli/args.py) that was automatically created is supplied as the only argument to this function
112+
and can be modified by it. See `code42cli.cmds.profile._load_profile_create_descriptions` for an example of this.
113+
114+
5. If one of your handler's parameters is named `sdk`, you will automatically get a `--profile` argument available in the cli and the `sdk` parameter
115+
will automatically contain an instance of `py42.sdk.SDKClient` that was created with the given (or default) profile.
116+
- A cli parameter named `--sdk` will _not_ be added in this case.
117+
118+
6. If you have an `sdk` parameter, a parameter named `profile` will automatically contain the info of the profile that was used to create the sdk.
119+
- A parameter named `profile` behaves normally if you do not also have a parameter named `sdk`.
120+
121+
7. Each command accepts a `use_single_arg_obj` bool in its constructor. If set to true, this will instead cause the handler to be called with a single object
122+
containing all of the args as attributes, which will be passed to a variable named `args` in your handler. Since your handler will only contain the parameter `args`,
123+
the names of your cli parameters need to built manually in your `arg_customizer` if you use this option. An example of this can be seen in `code42cli.cmds.securitydata.main`.

README.md

+38-33
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Additionally, you can choose to only get events that Code42 previously did not o
1111
- Code42 Server 6.8.x+
1212

1313
## Installation
14+
1415
Install the `code42` CLI using:
1516

1617
```bash
@@ -19,39 +20,35 @@ $ python setup.py install
1920

2021
## Usage
2122

22-
First, set your profile:
23+
First, create your profile:
2324
```bash
24-
code42 profile set --profile MY_FIRST_PROFILE -s https://example.authority.com -u [email protected]
25+
code42 profile create MY_FIRST_PROFILE https://example.authority.com [email protected]
2526
```
26-
The `--profile` flag is required the first time and it takes a name.
27-
On subsequent uses of `set`, not specifying the profile will set the default profile.
2827

2928
Your profile contains the necessary properties for logging into Code42 servers.
30-
After running `code42 profile set`, the program prompts you about storing a password.
29+
After running `code42 profile create`, the program prompts you about storing a password.
3130
If you agree, you are then prompted to input your password.
3231

33-
Your password is not stored in plain-text and is not shown when you do `code42 profile show`.
32+
Your password is not shown when you do `code42 profile show`.
3433
However, `code42 profile show` will confirm that a password exists for your profile.
3534
If you do not set a password, you will be securely prompted to enter a password each time you run a command.
3635

37-
For development purposes, you may need to ignore ssl errors. If you need to do this, do:
38-
```bash
39-
code42 profile set --disable-ssl-errors
40-
```
36+
For development purposes, you may need to ignore ssl errors. If you need to do this, use the `--disable-ssl-errors` option when creating your profile:
4137

42-
To re-enable SSL errors, do:
4338
```bash
44-
code42 profile set --enable-ssl-errors
39+
code42 profile create MY_FIRST_PROFILE https://example.authority.com [email protected] --disable-ssl-errors
4540
```
4641

4742
You can add multiple profiles with different names and the change the default profile with the `use` command:
43+
4844
```bash
4945
code42 profile use MY_SECOND_PROFILE
5046
```
51-
When the `--profile` flag is available on other commands, such as those in `securitydata`,
52-
it will use that profile instead of the default one.
47+
48+
When the `--profile` flag is available on other commands, such as those in `securitydata`, it will use that profile instead of the default one.
5349

5450
To see all your profiles, do:
51+
5552
```bash
5653
code42 profile list
5754
```
@@ -62,6 +59,7 @@ Using the CLI, you can query for events and send them to three possible destinat
6259
* A server, such as SysLog
6360

6461
To print events to stdout, do:
62+
6563
```bash
6664
code42 securitydata print -b 2020-02-02
6765
```
@@ -72,67 +70,74 @@ To specify a time, do:
7270
```bash
7371
code42 securitydata print -b 2020-02-02 12:51
7472
```
73+
7574
Begin date will be ignored if provided on subsequent queries using `-i`.
7675

7776
Use different format with `-f`:
77+
7878
```bash
7979
code42 securitydata print -b 2020-02-02 -f CEF
8080
```
81+
8182
The available formats are CEF, JSON, and RAW-JSON.
8283

8384
To write events to a file, do:
85+
8486
```bash
8587
code42 securitydata write-to filename.txt -b 2020-02-02
8688
```
8789

8890
To send events to a server, do:
91+
8992
```bash
9093
code42 securitydata send-to syslog.company.com -p TCP -b 2020-02-02
9194
```
9295

9396
To only get events that Code42 previously did not observe since you last recorded a checkpoint, use the `-i` flag.
97+
9498
```bash
9599
code42 securitydata send-to syslog.company.com -i
96100
```
101+
97102
This is only guaranteed if you did not change your query.
98103

99104
To send events to a server using a specific profile, do:
105+
100106
```bash
101107
code42 securitydata send-to --profile PROFILE_FOR_RECURRING_JOB syslog.company.com -b 2020-02-02 -f CEF -i
102108
```
103109

104110
You can also use wildcard for queries, but note, if they are not in quotes, you may get unexpected behavior.
111+
105112
```bash
106113
code42 securitydata print --actor "*"
107114
```
108115

109-
110116
Each destination-type subcommand shares query parameters
111-
* `-t` (exposure types)
112-
* `-b` (begin date)
113-
* `-e` (end date)
114-
* `--c42username`
115-
* `--actor`
116-
* `--md5`
117-
* `--sha256`
118-
* `--source`
119-
* `--filename`
120-
* `--filepath`
121-
* `--processOwner`
122-
* `--tabURL`
123-
* `--include-non-exposure` (does not work with `-t`)
124-
* `--advanced-query` (raw JSON query)
117+
118+
- `-t` (exposure types)
119+
- `-b` (begin date)
120+
- `-e` (end date)
121+
- `--c42username`
122+
- `--actor`
123+
- `--md5`
124+
- `--sha256`
125+
- `--source`
126+
- `--filename`
127+
- `--filepath`
128+
- `--processOwner`
129+
- `--tabURL`
130+
- `--include-non-exposure` (does not work with `-t`)
131+
- `--advanced-query` (raw JSON query)
125132

126133
You cannot use other query parameters if you use `--advanced-query`.
127134
To learn more about acceptable arguments, add the `-h` flag to `code42` or any of the destination-type subcommands.
128135

129-
130-
# Known Issues
136+
## Known Issues
131137

132138
Only the first 10,000 of each set of events containing the exact same insertion timestamp is reported.
133139

134-
135-
# Troubleshooting
140+
## Troubleshooting
136141

137142
If you keep getting prompted for your password, try resetting with `code42 profile reset-pw`.
138143
If that doesn't work, delete your credentials file located at ~/.code42cli or the entry in keychain.

setup.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
from codecs import open
12
from os import path
23
from setuptools import find_packages, setup
3-
from codecs import open
44

55
here = path.abspath(path.dirname(__file__))
66

@@ -20,7 +20,12 @@
2020
packages=find_packages("src"),
2121
package_dir={"": "src"},
2222
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4",
23-
install_requires=["c42eventextractor==0.2.2", "keyring==18.0.1","py42==0.6.0"],
23+
install_requires=[
24+
"c42eventextractor==0.2.2",
25+
"keyring==18.0.1",
26+
"keyrings.alt==3.2.0",
27+
"py42==0.6.0",
28+
],
2429
license="MIT",
2530
include_package_data=True,
2631
zip_safe=False,

src/code42cli/args.py

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import inspect
2+
3+
PROFILE_HELP = u"The name of the Code42 profile use when executing this command."
4+
5+
6+
class ArgConfig(object):
7+
"""Stores a set of argparse commands for later use by a command."""
8+
9+
def __init__(self, *args, **kwargs):
10+
self._settings = {}
11+
self._settings[u"action"] = kwargs.get(u"action")
12+
self._settings[u"choices"] = kwargs.get(u"choices")
13+
self._settings[u"default"] = kwargs.get(u"default")
14+
self._settings[u"help"] = kwargs.get(u"help")
15+
self._settings[u"options_list"] = list(args)
16+
self._settings[u"nargs"] = kwargs.get(u"nargs")
17+
18+
@property
19+
def settings(self):
20+
return self._settings
21+
22+
def set_choices(self, choices):
23+
self._settings[u"choices"] = choices
24+
25+
def set_help(self, help):
26+
self._settings[u"help"] = help
27+
28+
def add_short_option_name(self, short_name):
29+
self._settings[u"options_list"].append(short_name)
30+
31+
32+
class ArgConfigCollection(object):
33+
def __init__(self):
34+
self._arg_configs = {}
35+
36+
@property
37+
def arg_configs(self):
38+
return self._arg_configs
39+
40+
def append(self, name, arg_config):
41+
self._arg_configs[name] = arg_config
42+
43+
def extend(self, arg_config_dict):
44+
self.arg_configs.update(arg_config_dict)
45+
46+
47+
def get_auto_arg_configs(handler):
48+
"""Looks at the parameter names of `handler` and builds an `ArgConfigCollection` containing argparse
49+
parameters based on them."""
50+
arg_configs = ArgConfigCollection()
51+
if callable(handler):
52+
# get the number of positional and keyword args
53+
argspec = inspect.getargspec(handler)
54+
num_args = len(argspec.args)
55+
num_kw_args = len(argspec.defaults) if argspec.defaults else 0
56+
57+
for arg_position, key in enumerate(argspec.args):
58+
# do not create cli parameters for arguments named "sdk", "args", or "kwargs"
59+
if not key in [u"sdk", u"args", u"kwargs"]:
60+
arg_config = _create_auto_args_config(
61+
arg_position, key, argspec, num_args, num_kw_args
62+
)
63+
_set_smart_defaults(arg_config)
64+
arg_configs.append(key, arg_config)
65+
66+
if u"sdk" in argspec.args:
67+
_build_sdk_arg_configs(arg_configs)
68+
69+
return arg_configs
70+
71+
72+
def _create_auto_args_config(arg_position, key, argspec, num_args, num_kw_args):
73+
default = None
74+
param_name = key.replace(u"_", u"-")
75+
difference = num_args - num_kw_args
76+
last_positional_arg_idx = difference - 1
77+
# postional arguments will come first, so if the arg position
78+
# is greater than the index of the last positional arg, it's a kwarg.
79+
if arg_position > last_positional_arg_idx:
80+
# this is a keyword arg, treat it as an optional cli arg.
81+
default_value = argspec.defaults[arg_position - difference]
82+
option_names = [u"--{}".format(param_name)]
83+
default = default_value
84+
else:
85+
# this is a positional arg, treat it as a required cli arg.
86+
option_names = [param_name]
87+
return ArgConfig(*option_names, default=default)
88+
89+
90+
def _set_smart_defaults(arg_config):
91+
default = arg_config.settings.get(u"default")
92+
# make a parameter allow lists as input if its default value is a list,
93+
# e.g. --my-param one two three four
94+
nargs = u"+" if type(default) == list else None
95+
arg_config.settings[u"nargs"] = nargs
96+
# make the param not require a value (e.g. --enable) if the default value of
97+
# the param is a bool.
98+
if type(default) == bool:
99+
arg_config.settings[u"action"] = u"store_{}".format(default).lower()
100+
101+
102+
def _build_sdk_arg_configs(arg_config_collection):
103+
"""Add extra cli parameters that will always be relevant when a handler needs the sdk."""
104+
profile = ArgConfig(u"--profile", help=PROFILE_HELP)
105+
debug = ArgConfig(u"-d", u"--debug", action=u"store_true", help=u"Turn on Debug logging.")
106+
extras = {u"profile": profile, u"debug": debug}
107+
arg_config_collection.extend(extras)

0 commit comments

Comments
 (0)