Skip to content

Commit 137ec5a

Browse files
added audit-logs command (#160)
* added audit-logs command * remove format option * Added send-to command * added changelog * fix test * changed argument naming convention to singular * refactor search intervals * added format options * refactor * make begin date required * fix tests * added message when no results found and default header for table output * fix send-to when no results found * toggle py42 version * Fix integration test, removed output validation as made input dates dynamic * Add integration test for auditlogs * Fix style * added docs string and renamed variable * refactor: _send_to function * Rectify doc * Changed output fields for table format
1 parent 3ee7344 commit 137ec5a

File tree

10 files changed

+400
-105
lines changed

10 files changed

+400
-105
lines changed

CHANGELOG.md

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

1111
## Unreleased
1212

13+
### Added
14+
15+
- `code42 audit-logs` commands:
16+
- `search` to search for audit-logs.
17+
- `send-to` to send audit-logs to server.
18+
1319
### Changed
1420

1521
- The `--advanced-query` option on `alerts search` and `security-data (search|send-to)` commands has been updated:

integration/test_alerts.py

Lines changed: 17 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,31 @@
1-
import json
1+
from datetime import datetime
2+
from datetime import timedelta
23

34
import pytest
45
from integration import run_command
5-
from integration.util import cleanup_after_validation
66

7-
ALERT_COMMAND = "code42 alerts print -b 2020-05-18 -e 2020-05-20"
87

8+
begin_date = datetime.utcnow() - timedelta(days=20)
9+
end_date = datetime.utcnow() - timedelta(days=10)
10+
begin_date_str = begin_date.strftime("%Y-%m-%d")
11+
end_date_str = end_date.strftime("%Y-%m-%d")
912

10-
def _parse_response(response):
11-
return [json.loads(line) for line in response if len(line)]
12-
13-
14-
def _validate_field_value(field, value, response):
15-
parsed_response = _parse_response(response)
16-
assert len(parsed_response) > 0
17-
for record in parsed_response:
18-
assert record[field] == value
13+
ALERT_COMMAND = "code42 alerts search -b {} -e {}".format(begin_date_str, end_date_str)
1914

2015

2116
@pytest.mark.parametrize(
22-
"command, field, value",
17+
"command",
2318
[
24-
("{} --state OPEN".format(ALERT_COMMAND), "state", "OPEN"),
25-
("{} --state RESOLVED".format(ALERT_COMMAND), "state", "RESOLVED"),
26-
(
27-
"{} --actor user@code42.com".format(ALERT_COMMAND),
28-
"actor",
29-
"user@code42.com",
30-
),
31-
(
32-
"{} --rule-name 'File Upload Alert'".format(ALERT_COMMAND),
33-
"name",
34-
"File Upload Alert",
35-
),
36-
(
37-
"{} --rule-id 962a6a1c-54f6-4477-90bd-a08cc74cbf71".format(ALERT_COMMAND),
38-
"ruleId",
39-
"962a6a1c-54f6-4477-90bd-a08cc74cbf71",
40-
),
41-
(
42-
"{} --rule-type FedEndpointExfiltration".format(ALERT_COMMAND),
43-
"type",
44-
"FED_ENDPOINT_EXFILTRATION",
45-
),
46-
(
47-
"{} --description 'Alert on any file upload'".format(ALERT_COMMAND),
48-
"description",
49-
"Alert on any file upload events",
50-
),
19+
("{}".format(ALERT_COMMAND)),
20+
("{} --state OPEN".format(ALERT_COMMAND)),
21+
("{} --state RESOLVED".format(ALERT_COMMAND)),
22+
("{} --actor user@code42.com".format(ALERT_COMMAND)),
23+
("{} --rule-name 'File Upload Alert'".format(ALERT_COMMAND)),
24+
("{} --rule-id 962a6a1c-54f6-4477-90bd-a08cc74cbf71".format(ALERT_COMMAND)),
25+
("{} --rule-type FedEndpointExfiltration".format(ALERT_COMMAND)),
26+
("{} --description 'Alert on any file upload'".format(ALERT_COMMAND)),
5127
],
5228
)
53-
def test_alert_prints_to_stdout_and_filters_result_by_given_value(
54-
command, field, value
55-
):
29+
def test_alert_returns_success_return_code(command):
5630
return_code, response = run_command(command)
5731
assert return_code == 0
58-
_validate_field_value(field, value, response)
59-
60-
61-
def _validate_begin_date(response):
62-
parsed_response = _parse_response(response)
63-
assert len(parsed_response) > 0
64-
for record in parsed_response:
65-
assert record["createdAt"].startswith("2020-05-18")
66-
67-
68-
@pytest.mark.parametrize("command, validate", [(ALERT_COMMAND, _validate_begin_date)])
69-
def test_alert_prints_to_stdout_and_filters_result_between_given_date(
70-
command, validate
71-
):
72-
return_code, response = run_command(command)
73-
assert return_code == 0
74-
validate(response)
75-
76-
77-
def _validate_severity(response):
78-
record = json.loads(response)
79-
assert record["severity"] == "MEDIUM"
80-
81-
82-
@cleanup_after_validation("./integration/alerts")
83-
def test_alert_writes_to_file_and_filters_result_by_severity():
84-
command = (
85-
"code42 alerts write-to ./integration/alerts -b 2020-05-18 -e 2020-05-20 "
86-
"--severity MEDIUM"
87-
)
88-
return_code, response = run_command(command)
89-
return _validate_severity

integration/test_auditlogs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from datetime import datetime
2+
from datetime import timedelta
3+
4+
from integration import run_command
5+
6+
BASE_COMMAND = "code42 auditlogs search -b"
7+
8+
9+
def test_auditlogs_search():
10+
begin_date = datetime.utcnow() - timedelta(days=-10)
11+
begin_date_str = begin_date.strftime("%Y-%m-%d %H:%M:%S")
12+
return_code, response = run_command(BASE_COMMAND + begin_date_str)
13+
assert return_code == 0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"c42eventextractor==0.4.0",
3737
"keyring==18.0.1",
3838
"keyrings.alt==3.2.0",
39-
"py42>=1.8.1",
39+
"py42>=1.9",
4040
],
4141
extras_require={
4242
"dev": [

src/code42cli/cmds/auditlogs.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import json
2+
from _collections import OrderedDict
3+
4+
import click
5+
6+
from code42cli.click_ext.groups import OrderedGroup
7+
from code42cli.date_helper import parse_max_timestamp
8+
from code42cli.date_helper import parse_min_timestamp
9+
from code42cli.logger import get_logger_for_server
10+
from code42cli.options import begin_option
11+
from code42cli.options import end_option
12+
from code42cli.options import format_option
13+
from code42cli.options import sdk_options
14+
from code42cli.options import send_to_format_options
15+
from code42cli.options import server_options
16+
from code42cli.output_formats import OutputFormatter
17+
from code42cli.util import warn_interrupt
18+
19+
EVENT_KEY = "events"
20+
AUDIT_LOGS_KEYWORD = "audit-logs"
21+
22+
AUDIT_LOGS_DEFAULT_HEADER = OrderedDict()
23+
AUDIT_LOGS_DEFAULT_HEADER["timestamp"] = "Timestamp"
24+
AUDIT_LOGS_DEFAULT_HEADER["type$"] = "Type"
25+
AUDIT_LOGS_DEFAULT_HEADER["actorName"] = "ActorName"
26+
AUDIT_LOGS_DEFAULT_HEADER["actorIpAddress"] = "ActorIpAddress"
27+
AUDIT_LOGS_DEFAULT_HEADER["userName"] = "AffectedUser"
28+
AUDIT_LOGS_DEFAULT_HEADER["userId"] = "AffectedUserUID"
29+
# AUDIT_LOGS_DEFAULT_HEADER["success"] = "Success"
30+
# AUDIT_LOGS_DEFAULT_HEADER["resultCount"] = "ResultCount"
31+
32+
filter_option_usernames = click.option(
33+
"--username", required=False, help="Filter results by usernames.", multiple=True,
34+
)
35+
filter_option_user_ids = click.option(
36+
"--user-id", required=False, help="Filter results by user ids.", multiple=True,
37+
)
38+
39+
filter_option_user_ip_addresses = click.option(
40+
"--user-ip",
41+
required=False,
42+
help="Filter results by user ip addresses.",
43+
multiple=True,
44+
)
45+
filter_option_affected_user_ids = click.option(
46+
"--affected-user-id",
47+
required=False,
48+
help="Filter results by affected user ids.",
49+
multiple=True,
50+
)
51+
filter_option_affected_usernames = click.option(
52+
"--affected-username",
53+
required=False,
54+
help="Filter results by affected usernames.",
55+
multiple=True,
56+
)
57+
filter_option_event_types = click.option(
58+
"--event-type",
59+
required=False,
60+
help="Filter results by event types.",
61+
multiple=True,
62+
)
63+
64+
65+
def filter_options(f):
66+
f = begin_option(
67+
f,
68+
AUDIT_LOGS_KEYWORD,
69+
callback=lambda ctx, param, arg: parse_min_timestamp(arg),
70+
required=True,
71+
)
72+
f = end_option(
73+
f, AUDIT_LOGS_KEYWORD, callback=lambda ctx, param, arg: parse_max_timestamp(arg)
74+
)
75+
f = filter_option_event_types(f)
76+
f = filter_option_usernames(f)
77+
f = filter_option_user_ids(f)
78+
f = filter_option_user_ip_addresses(f)
79+
f = filter_option_affected_user_ids(f)
80+
f = filter_option_affected_usernames(f)
81+
return f
82+
83+
84+
@click.group(cls=OrderedGroup)
85+
@sdk_options(hidden=True)
86+
def audit_logs(state):
87+
"""Retrieve audit logs."""
88+
pass
89+
90+
91+
@audit_logs.command()
92+
@filter_options
93+
@format_option
94+
@sdk_options()
95+
def search(
96+
state,
97+
begin,
98+
end,
99+
event_type,
100+
username,
101+
user_id,
102+
user_ip,
103+
affected_user_id,
104+
affected_username,
105+
format,
106+
):
107+
"""Search audit logs."""
108+
_search(
109+
state.sdk,
110+
format,
111+
begin_time=begin,
112+
end_time=end,
113+
event_types=event_type,
114+
usernames=username,
115+
user_ids=user_id,
116+
user_ip_addresses=user_ip,
117+
affected_user_ids=affected_user_id,
118+
affected_usernames=affected_username,
119+
)
120+
121+
122+
@audit_logs.command()
123+
@filter_options
124+
@server_options
125+
@send_to_format_options
126+
@sdk_options()
127+
def send_to(
128+
state,
129+
hostname,
130+
protocol,
131+
format,
132+
begin,
133+
end,
134+
event_type,
135+
username,
136+
user_id,
137+
user_ip,
138+
affected_user_id,
139+
affected_username,
140+
):
141+
"""Send audit logs to the given server address."""
142+
_send_to(
143+
state.sdk,
144+
hostname,
145+
protocol,
146+
format,
147+
begin_time=begin,
148+
end_time=end,
149+
event_types=event_type,
150+
usernames=username,
151+
user_ids=user_id,
152+
user_ip_addresses=user_ip,
153+
affected_user_ids=affected_user_id,
154+
affected_usernames=affected_username,
155+
)
156+
157+
158+
def _search(sdk, format, **filter_args):
159+
160+
formatter = OutputFormatter(format, AUDIT_LOGS_DEFAULT_HEADER)
161+
response_gen = sdk.auditlogs.get_all(**filter_args)
162+
163+
events = []
164+
try:
165+
for response in response_gen:
166+
response_dict = json.loads(response.text)
167+
if EVENT_KEY in response_dict:
168+
events.extend(response_dict.get(EVENT_KEY))
169+
except KeyError:
170+
# API endpoint (get_page) returns a response without events key when no records are found
171+
# e.g {"paginationRangeStartIndex": 10000, "paginationRangeEndIndex": 10000, "totalResultCount": 1593}
172+
pass
173+
174+
event_count = len(events)
175+
if not event_count:
176+
click.echo("No results found.")
177+
elif event_count > 10:
178+
click.echo_via_pager(formatter.get_formatted_output(events))
179+
else:
180+
formatter.echo_formatted_list(events)
181+
182+
183+
def _send_to(sdk, hostname, protocol, format, **filter_args):
184+
logger = get_logger_for_server(hostname, protocol, format)
185+
with warn_interrupt():
186+
response_gen = sdk.auditlogs.get_all(**filter_args)
187+
try:
188+
for response in response_gen:
189+
if EVENT_KEY in response:
190+
for event in response[EVENT_KEY]:
191+
logger.info(event)
192+
else:
193+
logger.info("No results found.")
194+
except KeyError:
195+
pass

0 commit comments

Comments
 (0)