Skip to content

Commit 0ae6af8

Browse files
timabrmsnalanag13
andauthored
Feature/INTEG-950 alert polling (#51)
Co-authored-by: Alan Grgic <[email protected]>
1 parent c36cb66 commit 0ae6af8

34 files changed

+1817
-731
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1212

1313
### Added
1414

15+
- Ability to search/poll for alerts with checkpointing and sending to console, a file, or a server in json format.
1516
- `code42 alert-rules` commands:
1617
- `add-user` with parameters `--rule-id` and `--username`.
1718
- `remove-user` that takes a rule ID and optionally `--username`.

setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
package_dir={"": "src"},
2222
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4",
2323
install_requires=[
24-
"c42eventextractor==0.2.9",
24+
"c42eventextractor==0.3.0b1",
2525
"keyring==18.0.1",
2626
"keyrings.alt==3.2.0",
27-
"py42>=1.1.1",
27+
"py42>=1.1.3",
2828
],
2929
license="MIT",
3030
include_package_data=True,

src/code42cli/args.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self, *args, **kwargs):
1818
u"help": kwargs.get(u"help"),
1919
u"options_list": list(args),
2020
u"nargs": kwargs.get(u"nargs"),
21+
u"metavar": kwargs.get(u"metavar"),
2122
u"required": kwargs.get(u"required"),
2223
}
2324

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from c42eventextractor.extractors import AlertExtractor
2+
from py42.sdk.queries.alerts.filters import (
3+
Actor,
4+
AlertState,
5+
Severity,
6+
DateObserved,
7+
Description,
8+
RuleName,
9+
RuleId,
10+
RuleType,
11+
)
12+
13+
import code42cli.cmds.search_shared.enums as enums
14+
import code42cli.errors as errors
15+
from code42cli.cmds.search_shared.cursor_store import AlertCursorStore
16+
from code42cli.cmds.search_shared.extraction import (
17+
verify_begin_date_requirements,
18+
create_handlers,
19+
exit_if_advanced_query_used_with_other_search_args,
20+
create_time_range_filter,
21+
)
22+
from code42cli.logger import get_main_cli_logger
23+
24+
logger = get_main_cli_logger()
25+
26+
27+
def extract(sdk, profile, output_logger, args):
28+
"""Extracts alerts using the given command-line arguments.
29+
30+
Args:
31+
sdk (py42.sdk.SDKClient): The py42 sdk.
32+
profile (Code42Profile): The profile under which to execute this command.
33+
output_logger (Logger): The logger specified by which subcommand you use. For example,
34+
print: uses a logger that streams to stdout.
35+
write-to: uses a logger that logs to a file.
36+
send-to: uses a logger that sends logs to a server.
37+
args: Command line args used to build up alert query filters.
38+
"""
39+
store = AlertCursorStore(profile.name) if args.incremental else None
40+
handlers = create_handlers(output_logger, store, event_key=u"alerts", sdk=sdk)
41+
extractor = AlertExtractor(sdk, handlers)
42+
if args.advanced_query:
43+
exit_if_advanced_query_used_with_other_search_args(args)
44+
extractor.extract_advanced(args.advanced_query)
45+
else:
46+
verify_begin_date_requirements(args, store)
47+
_verify_alert_state(args.state)
48+
_verify_alert_severity(args.severity)
49+
filters = _create_alert_filters(args)
50+
extractor.extract(*filters)
51+
if handlers.TOTAL_EVENTS == 0 and not errors.ERRORED:
52+
logger.print_info(u"No results found\n")
53+
54+
55+
def _verify_alert_state(alert_state):
56+
options = list(enums.AlertState())
57+
if alert_state and alert_state not in options:
58+
logger.print_and_log_error(
59+
u"'{0}' is not a valid alert state, options are {1}.".format(alert_state, options)
60+
)
61+
exit(1)
62+
63+
64+
def _verify_alert_severity(severity):
65+
if severity is None:
66+
return
67+
options = list(enums.AlertSeverity())
68+
for s in severity:
69+
if s not in options:
70+
logger.print_and_log_error(
71+
u"'{0}' is not a valid alert severity, options are {1}".format(s, options)
72+
)
73+
exit(1)
74+
75+
76+
def _create_alert_filters(args):
77+
filters = []
78+
alert_timestamp_filter = create_time_range_filter(DateObserved, args.begin, args.end)
79+
not alert_timestamp_filter or filters.append(alert_timestamp_filter)
80+
not args.actor or filters.append(Actor.is_in(args.actor))
81+
not args.actor_contains or [filters.append(Actor.contains(arg)) for arg in args.actor_contains]
82+
not args.exclude_actor or filters.append(Actor.not_in(args.exclude_actor))
83+
not args.exclude_actor_contains or [
84+
filters.append(Actor.not_contains(arg)) for arg in args.exclude_actor_contains
85+
]
86+
not args.rule_name or filters.append(RuleName.is_in(args.rule_name))
87+
not args.exclude_rule_name or filters.append(RuleName.not_in(args.exclude_rule_name))
88+
not args.rule_id or filters.append(RuleId.is_in(args.rule_id))
89+
not args.exclude_rule_id or filters.append(RuleId.not_in(args.exclude_rule_id))
90+
not args.rule_type or filters.append(RuleType.is_in(args.rule_type))
91+
not args.exclude_rule_type or filters.append(RuleType.not_in(args.exclude_rule_type))
92+
not args.description or filters.append(Description.contains(args.description))
93+
not args.severity or filters.append(Severity.is_in(args.severity))
94+
not args.state or filters.append(AlertState.eq(args.state))
95+
return filters

src/code42cli/cmds/alerts/main.py

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
from code42cli.args import ArgConfig
2+
from code42cli.commands import Command
3+
from code42cli.cmds.alerts.extraction import extract
4+
from code42cli.cmds.search_shared import args, logger_factory
5+
from code42cli.cmds.search_shared.enums import (
6+
AlertFilterArguments,
7+
AlertState,
8+
AlertSeverity,
9+
ServerProtocol,
10+
RuleType,
11+
)
12+
from code42cli.cmds.search_shared.cursor_store import AlertCursorStore
13+
14+
15+
def load_subcommands():
16+
"""Sets up the `alerts` subcommand with all of its subcommands."""
17+
usage_prefix = u"code42 alerts"
18+
19+
print_func = Command(
20+
u"print",
21+
u"Print alerts to stdout",
22+
u"{} {}".format(usage_prefix, u"print <optional-args>"),
23+
handler=print_out,
24+
arg_customizer=_load_search_args,
25+
use_single_arg_obj=True,
26+
)
27+
28+
write = Command(
29+
u"write-to",
30+
u"Write alerts to the file with the given name.",
31+
u"{} {}".format(usage_prefix, u"write-to <filename> <optional-args>"),
32+
handler=write_to,
33+
arg_customizer=_load_write_to_args,
34+
use_single_arg_obj=True,
35+
)
36+
37+
send = Command(
38+
u"send-to",
39+
u"Send alerts to the given server address.",
40+
u"{} {}".format(usage_prefix, u"send-to <server-address> <optional-args>"),
41+
handler=send_to,
42+
arg_customizer=_load_send_to_args,
43+
use_single_arg_obj=True,
44+
)
45+
46+
clear = Command(
47+
u"clear-checkpoint",
48+
u"Remove the saved alert checkpoint from 'incremental' (-i) mode.",
49+
u"{} {}".format(usage_prefix, u"clear-checkpoint <optional-args>"),
50+
handler=clear_checkpoint,
51+
)
52+
53+
return [print_func, write, send, clear]
54+
55+
56+
def clear_checkpoint(sdk, profile):
57+
"""Removes the stored checkpoint that keeps track of the last alert retrieved for the given profile..
58+
To use, run `code42 alerts clear-checkpoint`.
59+
This affects `incremental` mode by causing it to behave like it has never been run before.
60+
"""
61+
AlertCursorStore(profile.name).replace_stored_cursor_timestamp(None)
62+
63+
64+
def print_out(sdk, profile, args):
65+
"""Activates 'print' command. It gets alerts and prints them to stdout."""
66+
logger = logger_factory.get_logger_for_stdout(args.format)
67+
extract(sdk, profile, logger, args)
68+
69+
70+
def write_to(sdk, profile, args):
71+
"""Activates 'write-to' command. It gets alerts and writes them to the given file."""
72+
logger = logger_factory.get_logger_for_file(args.output_file, args.format)
73+
extract(sdk, profile, logger, args)
74+
75+
76+
def send_to(sdk, profile, args):
77+
"""Activates 'send-to' command. It getsalerts and logs them to the given server."""
78+
logger = logger_factory.get_logger_for_server(args.server, args.protocol, args.format)
79+
extract(sdk, profile, logger, args)
80+
81+
82+
def _load_write_to_args(arg_collection):
83+
output_file = ArgConfig(u"output_file", help=u"The name of the local file to send output to.")
84+
arg_collection.append(u"output_file", output_file)
85+
_load_search_args(arg_collection)
86+
87+
88+
def _load_send_to_args(arg_collection):
89+
send_to_args = {
90+
u"server": ArgConfig(u"server", help=u"The server address to send output to."),
91+
u"protocol": ArgConfig(
92+
u"-p",
93+
u"--protocol",
94+
choices=ServerProtocol(),
95+
default=ServerProtocol.UDP,
96+
help=u"Protocol used to send logs to server.",
97+
),
98+
}
99+
100+
arg_collection.extend(send_to_args)
101+
_load_search_args(arg_collection)
102+
103+
104+
def _load_search_args(arg_collection):
105+
filter_args = {
106+
AlertFilterArguments.SEVERITY: ArgConfig(
107+
u"--{}".format(AlertFilterArguments.SEVERITY),
108+
nargs=u"+",
109+
help=u"Filter alerts by severity. Defaults to returning all severities. Available choices={0}".format(
110+
list(AlertSeverity())
111+
),
112+
),
113+
AlertFilterArguments.STATE: ArgConfig(
114+
u"--{}".format(AlertFilterArguments.STATE),
115+
help=u"Filter alerts by state. Defaults to returning all states. Available choices={0}".format(
116+
list(AlertState())
117+
),
118+
),
119+
AlertFilterArguments.ACTOR: ArgConfig(
120+
u"--{}".format(AlertFilterArguments.ACTOR.replace("_", "-")),
121+
metavar=u"ACTOR",
122+
help=u"Filter alerts by including the given actor(s) who triggered the alert. Args must match actor username exactly.",
123+
nargs=u"+",
124+
),
125+
AlertFilterArguments.ACTOR_CONTAINS: ArgConfig(
126+
u"--{}".format(AlertFilterArguments.ACTOR_CONTAINS.replace("_", "-")),
127+
metavar=u"ACTOR",
128+
help=u"Filter alerts by including actor(s) whose username contains the given string.",
129+
nargs=u"+",
130+
),
131+
AlertFilterArguments.EXCLUDE_ACTOR: ArgConfig(
132+
u"--{}".format(AlertFilterArguments.EXCLUDE_ACTOR.replace("_", "-")),
133+
metavar=u"ACTOR",
134+
help=u"Filter alerts by excluding the given actor(s) who triggered the alert. Args must match actor username exactly.",
135+
nargs=u"+",
136+
),
137+
AlertFilterArguments.EXCLUDE_ACTOR_CONTAINS: ArgConfig(
138+
u"--{}".format(AlertFilterArguments.EXCLUDE_ACTOR_CONTAINS.replace("_", "-")),
139+
metavar=u"ACTOR",
140+
help=u"Filter alerts by excluding actor(s) whose username contains the given string.",
141+
nargs=u"+",
142+
),
143+
AlertFilterArguments.RULE_NAME: ArgConfig(
144+
u"--{}".format(AlertFilterArguments.RULE_NAME.replace("_", "-")),
145+
metavar=u"RULE_NAME",
146+
help=u"Filter alerts by including the given rule name(s).",
147+
nargs=u"+",
148+
),
149+
AlertFilterArguments.EXCLUDE_RULE_NAME: ArgConfig(
150+
u"--{}".format(AlertFilterArguments.EXCLUDE_RULE_NAME.replace("_", "-")),
151+
metavar=u"RULE_NAME",
152+
help=u"Filter alerts by excluding the given rule name(s).",
153+
nargs=u"+",
154+
),
155+
AlertFilterArguments.RULE_ID: ArgConfig(
156+
u"--{}".format(AlertFilterArguments.RULE_ID.replace("_", "-")),
157+
metavar=u"RULE_ID",
158+
help=u"Filter alerts by including the given rule id(s).",
159+
nargs=u"+",
160+
),
161+
AlertFilterArguments.EXCLUDE_RULE_ID: ArgConfig(
162+
u"--{}".format(AlertFilterArguments.EXCLUDE_RULE_ID.replace("_", "-")),
163+
metavar=u"RULE_ID",
164+
help=u"Filter alerts by excluding the given rule id(s).",
165+
nargs=u"+",
166+
),
167+
AlertFilterArguments.RULE_TYPE: ArgConfig(
168+
u"--{}".format(AlertFilterArguments.RULE_TYPE.replace("_", "-")),
169+
metavar=u"RULE_TYPE",
170+
help=u"Filter alerts by including the given rule type(s). Available choices={0}".format(
171+
list(RuleType())
172+
),
173+
nargs=u"+",
174+
),
175+
AlertFilterArguments.EXCLUDE_RULE_TYPE: ArgConfig(
176+
u"--{}".format(AlertFilterArguments.EXCLUDE_RULE_TYPE.replace("_", "-")),
177+
metavar=u"RULE_TYPE",
178+
help=u"Filter alerts by excluding the given rule type(s). Available choices={0}".format(
179+
list(RuleType())
180+
),
181+
nargs=u"+",
182+
),
183+
AlertFilterArguments.DESCRIPTION: ArgConfig(
184+
u"--{}".format(AlertFilterArguments.DESCRIPTION),
185+
help=u"Filter alerts by description. Does fuzzy search by default.",
186+
),
187+
}
188+
search_args = args.create_search_args(search_for=u"alerts", filter_args=filter_args)
189+
arg_collection.extend(search_args)

src/code42cli/cmds/alerts/util.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from code42cli.compat import range
2+
3+
_BATCH_SIZE = 500
4+
5+
6+
def get_alert_details(sdk, alert_summary_list):
7+
alert_ids = [alert[u"id"] for alert in alert_summary_list]
8+
batches = [alert_ids[i : i + _BATCH_SIZE] for i in range(0, len(alert_ids), _BATCH_SIZE)]
9+
results = []
10+
for batch in batches:
11+
r = sdk.alerts.get_details(batch)
12+
results.extend(r[u"alerts"])
13+
results = sorted(results, key=lambda x: x[u"createdAt"], reverse=True)
14+
return results

src/code42cli/cmds/detectionlists/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def __init__(self, add=None, remove=None, load_add=None):
5959
class DetectionList(object):
6060
"""An object representing a Code42 detection list. Use this class by passing in handlers for
6161
adding and removing employees. This class will handle the bulk-related commands and some
62-
shared help texts.
62+
search_shared help texts.
6363
6464
Args:
6565
list_name (str or unicode): An option from the DetectionLists enum. For convenience, use one of the
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from code42cli.cmds.search_shared.enums import SearchArguments, OutputFormat, AlertOutputFormat
2+
from code42cli.args import ArgConfig
3+
4+
5+
def create_search_args(search_for, filter_args):
6+
search_args = {
7+
SearchArguments.ADVANCED_QUERY: ArgConfig(
8+
u"--{}".format(SearchArguments.ADVANCED_QUERY.replace(u"_", u"-")),
9+
metavar=u"QUERY_JSON",
10+
help=u"A raw JSON {0} query. "
11+
u"Useful for when the provided query parameters do not satisfy your requirements.\n"
12+
u"WARNING: Using advanced queries is incompatible with other query-building args.".format(
13+
search_for
14+
),
15+
),
16+
SearchArguments.BEGIN_DATE: ArgConfig(
17+
u"-b",
18+
u"--{}".format(SearchArguments.BEGIN_DATE),
19+
metavar=u"DATE",
20+
help=u"The beginning of the date range in which to look for {1}, "
21+
u"can be a date/time in yyyy-MM-dd (UTC) or yyyy-MM-dd HH:MM:SS (UTC+24-hr time) format "
22+
u"where the 'time' portion of the string can be partial (e.g. '2020-01-01 12' or '2020-01-01 01:15') "
23+
u"or a short value representing days (30d), hours (24h) or minutes (15m) from current "
24+
u"time.".format(u"beginning", search_for),
25+
),
26+
SearchArguments.END_DATE: ArgConfig(
27+
u"-e",
28+
u"--{}".format(SearchArguments.END_DATE),
29+
metavar=u"DATE",
30+
help=u"The end of the date range in which to look for {0}, "
31+
u"argument format options are the same as --begin.".format(search_for),
32+
),
33+
}
34+
format_enum = AlertOutputFormat() if search_for == "alerts" else OutputFormat()
35+
format_and_incremental_args = {
36+
u"format": ArgConfig(
37+
u"-f",
38+
u"--format",
39+
choices=format_enum,
40+
default=format_enum.JSON,
41+
help=u"The format used for outputting {0}.".format(search_for),
42+
),
43+
u"incremental": ArgConfig(
44+
u"-i",
45+
u"--incremental",
46+
action=u"store_true",
47+
help=u"Only get {0} that were not previously retrieved.".format(search_for),
48+
),
49+
}
50+
search_args.update(filter_args)
51+
search_args.update(format_and_incremental_args)
52+
return search_args

0 commit comments

Comments
 (0)