Skip to content

Commit 272e1b0

Browse files
authored
Feature/advanced query from file (#161)
* add workflow * style fix * point cla action to code42-cla repo * Add CLA note to contributing guide * style * style2 * update changelog * note checkpoint update in changelog * style * make filename argument require `@` prefix * reword build query exception
1 parent 82a9e15 commit 272e1b0

File tree

17 files changed

+427
-262
lines changed

17 files changed

+427
-262
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1212

1313
### Changed
1414

15+
- The `--advanced-query` option on `alerts search` and `security-data (search|send-to)` commands has been updated:
16+
- It can now accept the query as a JSON string or as the path to a file containing the JSON query.
17+
- It can be used with the `--use-checkpoint/-c` option.
18+
1519
- Now, when adding a cloud alias to a detection list user, such as during `departing-employee add`, it will remove the existing cloud alias if one exists.
1620
- Before, it would error and the cloud alias would not get added.
1721

src/code42cli/click_ext/__init__.py

Whitespace-only changes.

src/code42cli/click_ext/groups.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import difflib
2+
import re
3+
from collections import OrderedDict
4+
5+
import click
6+
from py42.exceptions import Py42ForbiddenError
7+
from py42.exceptions import Py42HTTPError
8+
from py42.exceptions import Py42InvalidRuleOperationError
9+
from py42.exceptions import Py42LegalHoldNotFoundOrPermissionDeniedError
10+
from py42.exceptions import Py42UserAlreadyAddedError
11+
12+
from code42cli.errors import Code42CLIError
13+
from code42cli.errors import LoggedCLIError
14+
from code42cli.errors import UserDoesNotExistError
15+
from code42cli.logger import get_main_cli_logger
16+
17+
_DIFFLIB_CUT_OFF = 0.6
18+
19+
20+
class ExceptionHandlingGroup(click.Group):
21+
"""A `click.Group` subclass to add custom exception handling."""
22+
23+
logger = get_main_cli_logger()
24+
_original_args = None
25+
26+
def make_context(self, info_name, args, parent=None, **extra):
27+
28+
# grab the original command line arguments for logging purposes
29+
self._original_args = " ".join(args)
30+
31+
return super().make_context(info_name, args, parent=parent, **extra)
32+
33+
def invoke(self, ctx):
34+
try:
35+
return super().invoke(ctx)
36+
37+
except click.UsageError as err:
38+
self._suggest_cmd(err)
39+
40+
except LoggedCLIError:
41+
raise
42+
43+
except Code42CLIError as err:
44+
self.logger.log_error(str(err))
45+
raise
46+
47+
except click.ClickException:
48+
raise
49+
50+
except click.exceptions.Exit:
51+
raise
52+
53+
except (
54+
UserDoesNotExistError,
55+
Py42UserAlreadyAddedError,
56+
Py42InvalidRuleOperationError,
57+
Py42LegalHoldNotFoundOrPermissionDeniedError,
58+
) as err:
59+
self.logger.log_error(err)
60+
raise Code42CLIError(str(err))
61+
62+
except Py42ForbiddenError as err:
63+
self.logger.log_verbose_error(self._original_args, err.response.request)
64+
raise LoggedCLIError(
65+
"You do not have the necessary permissions to perform this task. "
66+
"Try using or creating a different profile."
67+
)
68+
69+
except Py42HTTPError as err:
70+
self.logger.log_verbose_error(self._original_args, err.response.request)
71+
raise LoggedCLIError("Problem making request to server.")
72+
73+
except OSError:
74+
raise
75+
76+
except Exception:
77+
self.logger.log_verbose_error()
78+
raise LoggedCLIError("Unknown problem occurred.")
79+
80+
@staticmethod
81+
def _suggest_cmd(usage_err):
82+
"""Handles fuzzy suggestion of commands that are close to the bad command entered."""
83+
if usage_err.message is not None:
84+
match = re.match("No such command '(.*)'.", usage_err.message)
85+
if match:
86+
bad_arg = match.groups()[0]
87+
available_commands = list(usage_err.ctx.command.commands.keys())
88+
suggested_commands = difflib.get_close_matches(
89+
bad_arg, available_commands, cutoff=_DIFFLIB_CUT_OFF
90+
)
91+
if not suggested_commands:
92+
raise usage_err
93+
usage_err.message = "No such command '{}'. Did you mean {}?".format(
94+
bad_arg, " or ".join(suggested_commands)
95+
)
96+
raise usage_err
97+
98+
99+
class OrderedGroup(click.Group):
100+
"""A `click.Group` subclass that uses an `OrderedDict` to store commands so the help text lists
101+
them in the order they were defined/added to the group.
102+
"""
103+
104+
def __init__(self, name=None, commands=None, **attrs):
105+
super().__init__(name, commands, **attrs)
106+
# the registered subcommands by their exported names.
107+
self.commands = commands or OrderedDict()
108+
109+
def list_commands(self, ctx):
110+
return self.commands

src/code42cli/click_ext/options.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import click
2+
3+
4+
def incompatible_with(incompatible_opts):
5+
"""Factory for creating custom `click.Option` subclasses that enforce incompatibility with the
6+
option strings passed to this function.
7+
"""
8+
9+
if isinstance(incompatible_opts, str):
10+
incompatible_opts = [incompatible_opts]
11+
12+
class IncompatibleOption(click.Option):
13+
def __init__(self, *args, **kwargs):
14+
super().__init__(*args, **kwargs)
15+
16+
def handle_parse_result(self, ctx, opts, args):
17+
# if None it means we're in autocomplete mode and don't want to validate
18+
if ctx.obj is not None:
19+
found_incompatible = ", ".join(
20+
[
21+
"--{}".format(opt.replace("_", "-"))
22+
for opt in opts
23+
if opt in incompatible_opts
24+
]
25+
)
26+
if self.name in opts and found_incompatible:
27+
name = self.name.replace("_", "-")
28+
raise click.BadOptionUsage(
29+
option_name=self.name,
30+
message="--{} can't be used with: {}".format(
31+
name, found_incompatible
32+
),
33+
)
34+
return super().handle_parse_result(ctx, opts, args)
35+
36+
return IncompatibleOption

src/code42cli/click_ext/types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import click
2+
3+
4+
class FileOrString(click.File):
5+
"""Declares a parameter to be a file (if the argument begins with `@`), otherwise accepts it as
6+
a string.
7+
"""
8+
9+
def __init__(self):
10+
super().__init__("r")
11+
12+
def convert(self, value, param, ctx):
13+
if value.startswith("@") or value == "-":
14+
value = value.lstrip("@")
15+
file = super().convert(value, param, ctx)
16+
return file.read()
17+
else:
18+
return value

src/code42cli/cmds/alert_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from code42cli import PRODUCT_NAME
99
from code42cli.bulk import generate_template_cmd_factory
1010
from code42cli.bulk import run_bulk_process
11+
from code42cli.click_ext.groups import OrderedGroup
1112
from code42cli.cmds.shared import get_user_id
1213
from code42cli.errors import Code42CLIError
1314
from code42cli.file_readers import read_csv_arg
1415
from code42cli.options import format_option
15-
from code42cli.options import OrderedGroup
1616
from code42cli.options import sdk_options
1717
from code42cli.output_formats import OutputFormatter
1818

src/code42cli/cmds/alerts.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from py42.sdk.queries.alerts.filters import RuleType
88
from py42.sdk.queries.alerts.filters import Severity
99

10+
import code42cli.click_ext.groups
1011
import code42cli.cmds.search.extraction as ext
1112
import code42cli.cmds.search.options as searchopt
1213
import code42cli.errors as errors
@@ -155,7 +156,7 @@ def alert_options(f):
155156
return f
156157

157158

158-
@click.group(cls=opt.OrderedGroup)
159+
@click.group(cls=code42cli.click_ext.groups.OrderedGroup)
159160
@opt.sdk_options(hidden=True)
160161
def alerts(state):
161162
"""Tools for getting alert data."""
@@ -177,13 +178,12 @@ def _call_extractor(
177178
extractor = _get_alert_extractor(cli_state.sdk, handlers)
178179
extractor.use_or_query = or_query
179180
if advanced_query:
180-
extractor.extract_advanced(advanced_query)
181-
else:
182-
if begin or end:
183-
cli_state.search_filters.append(
184-
ext.create_time_range_filter(f.DateObserved, begin, end)
185-
)
186-
extractor.extract(*cli_state.search_filters)
181+
cli_state.search_filters = advanced_query
182+
if begin or end:
183+
cli_state.search_filters.append(
184+
ext.create_time_range_filter(f.DateObserved, begin, end)
185+
)
186+
extractor.extract(*cli_state.search_filters)
187187

188188

189189
@alerts.command()

src/code42cli/cmds/departing_employee.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from code42cli.bulk import generate_template_cmd_factory
55
from code42cli.bulk import run_bulk_process
6+
from code42cli.click_ext.groups import OrderedGroup
67
from code42cli.cmds.detectionlists import update_user
78
from code42cli.cmds.detectionlists.options import cloud_alias_option
89
from code42cli.cmds.detectionlists.options import notes_option
@@ -11,7 +12,6 @@
1112
from code42cli.errors import Code42CLIError
1213
from code42cli.file_readers import read_csv_arg
1314
from code42cli.file_readers import read_flat_file_arg
14-
from code42cli.options import OrderedGroup
1515
from code42cli.options import sdk_options
1616

1717

src/code42cli/cmds/high_risk_employee.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from code42cli.bulk import generate_template_cmd_factory
66
from code42cli.bulk import run_bulk_process
7+
from code42cli.click_ext.groups import OrderedGroup
78
from code42cli.cmds.detectionlists import add_risk_tags as _add_risk_tags
89
from code42cli.cmds.detectionlists import handle_list_args
910
from code42cli.cmds.detectionlists import remove_risk_tags as _remove_risk_tags
@@ -15,7 +16,6 @@
1516
from code42cli.errors import Code42CLIError
1617
from code42cli.file_readers import read_csv_arg
1718
from code42cli.file_readers import read_flat_file_arg
18-
from code42cli.options import OrderedGroup
1919
from code42cli.options import sdk_options
2020

2121
risk_tag_option = click.option(

src/code42cli/cmds/legal_hold.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
from code42cli.bulk import generate_template_cmd_factory
1010
from code42cli.bulk import run_bulk_process
11+
from code42cli.click_ext.groups import OrderedGroup
1112
from code42cli.cmds.shared import get_user_id
1213
from code42cli.errors import UserNotInLegalHoldError
1314
from code42cli.file_readers import read_csv_arg
1415
from code42cli.options import format_option
15-
from code42cli.options import OrderedGroup
1616
from code42cli.options import sdk_options
1717
from code42cli.output_formats import OutputFormat
1818
from code42cli.output_formats import OutputFormatter

0 commit comments

Comments
 (0)