Skip to content

Commit 2f67630

Browse files
author
Juliya Smith
authored
Feature/de (#40)
1 parent 43c5e79 commit 2f67630

24 files changed

+389
-155
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,21 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
3030
- `code42 high-risk-employee` commands:
3131
- `bulk` with subcommands:
3232
- `add`: that takes a csv file of users.
33-
- `generate-template`: that creates the csv file template. And parameters:
33+
- `generate-template`: that creates the file template. And parameters:
3434
- `cmd`: with options `add` and `remove`.
3535
- `path`
3636
- `remove`: that takes a list of users in a file.
3737
- `add` that takes parameters: `--username`, `--cloud-alias`, `--risk-factor`, and `--notes`.
3838
- `remove` that takes a username.
39+
- `code42 departing-employee` commands:
40+
- `bulk` with subcommands:
41+
- `add`: that takes a csv file of users.
42+
- `generate-template`: that creates the file template. And parameters:
43+
- `cmd`: with options `add` and `remove`.
44+
- `path`
45+
- `remove`: that takes a list of users in a file.
46+
- `add` that takes parameters: `--username`, `--cloud-alias`, `--departure-date`, and `--notes`.
47+
- `remove` that takes a username.
3948

4049
### Removed
4150

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Use the `code42` command to interact with your Code42 environment.
55
* `code42 security-data` is a CLI tool for extracting AED events.
66
Additionally, you can choose to only get events that Code42 previously did not observe since you last recorded a
77
checkpoint (provided you do not change your query).
8-
* `code42 high-risk-employee` is a collection of tools for managing the high risk employee detection list.
8+
* `code42 high-risk-employee` is a collection of tools for managing the high risk employee detection list. Similarly,
9+
there is `code42 departing-employee`.
910

1011
## Requirements
1112

src/code42cli/cmds/detectionlists/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ def create_high_risk_employee_list(cls, handlers):
6363
"""
6464
return cls(DetectionLists.HIGH_RISK_EMPLOYEE, handlers)
6565

66+
@classmethod
67+
def create_departing_employee_list(cls, handlers):
68+
"""Creates a departing employee detection list.
69+
70+
Args:
71+
handlers (DetectionListHandlers): A DTO containing implementations for adding /
72+
removing users from specific lists.
73+
74+
Returns:
75+
DetectionList: A departing employee detection list.
76+
"""
77+
return cls(DetectionLists.DEPARTING_EMPLOYEE, handlers)
78+
6679
def load_subcommands(self):
6780
"""Loads high risk employee related subcommands"""
6881
bulk = self.factory.create_bulk_command(lambda: self._load_bulk_subcommands())
@@ -151,7 +164,7 @@ def load_user_descriptions(argument_collection):
151164
_load_username_description(argument_collection)
152165
cloud_alias = argument_collection.arg_configs[DetectionListUserKeys.CLOUD_ALIAS]
153166
notes = argument_collection.arg_configs[DetectionListUserKeys.NOTES]
154-
cloud_alias.set_help(u"Alternative emails addresses for other cloud services.")
167+
cloud_alias.set_help(u"An alternative email address for another cloud service.")
155168
notes.set_help(u"Notes about the employee.")
156169

157170

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from code42cli.cmds.detectionlists import (
2+
DetectionList,
3+
DetectionListHandlers,
4+
load_user_descriptions,
5+
get_user_id,
6+
update_user,
7+
)
8+
9+
10+
def load_subcommands():
11+
handlers = _create_handlers()
12+
detection_list = DetectionList.create_departing_employee_list(handlers)
13+
return detection_list.load_subcommands()
14+
15+
16+
def _create_handlers():
17+
return DetectionListHandlers(
18+
add=add_departing_employee, remove=remove_departing_employee, load_add=_load_add_description
19+
)
20+
21+
22+
def add_departing_employee(
23+
sdk, profile, username, cloud_alias=None, departure_date=None, notes=None
24+
):
25+
"""Adds an employee to the departing employee detection list.
26+
27+
Args:
28+
sdk (py42.sdk.SDKClient): py42.
29+
profile (C42Profile): Your code42 profile.
30+
username (str): The username of the employee to add.
31+
cloud_alias (str): An alternative email address for another cloud service.
32+
departure_date (str): The date the employee is departing in format `YYYY-MM-DD`.
33+
notes: (str): Notes about the employee.
34+
"""
35+
user_id = get_user_id(sdk, username)
36+
update_user(sdk, user_id, cloud_alias, notes=notes)
37+
sdk.detectionlists.departing_employee.add(user_id, departure_date)
38+
39+
40+
def remove_departing_employee(sdk, profile, username):
41+
user_id = get_user_id(sdk, username)
42+
sdk.detectionlists.departing_employee.remove(user_id)
43+
44+
45+
def _load_add_description(argument_collection):
46+
load_user_descriptions(argument_collection)
47+
departure_date = argument_collection.arg_configs[u"departure_date"]
48+
departure_date.set_help(u"The date the employee is departing in format YYYY-MM-DD.")
Lines changed: 9 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
1-
import re
2-
from datetime import datetime, timedelta
3-
4-
from c42eventextractor.common import convert_datetime_to_timestamp
51
from py42.sdk.queries.fileevents.filters.event_filter import EventTimestamp
62

7-
_MAX_LOOK_BACK_DAYS = 90
8-
_FORMAT_VALUE_ERROR_MESSAGE = u"input must be a date/time string (e.g. 'YYYY-MM-DD', 'YY-MM-DD HH:MM', 'YY-MM-DD HH:MM:SS'), or a short value in days, hours, or minutes (e.g. 30d, 24h, 15m)"
9-
10-
11-
class DateArgumentException(Exception):
12-
def __init__(self, message=_FORMAT_VALUE_ERROR_MESSAGE):
13-
super(DateArgumentException, self).__init__(message)
14-
15-
16-
TIMESTAMP_REGEX = re.compile(u"(\d{4}-\d{2}-\d{2})\s*(.*)?")
17-
MAGIC_TIME_REGEX = re.compile(u"(\d+)([dhm])$")
3+
from code42cli.date_helper import DateArgumentException, parse_min_timestamp, parse_max_timestamp
184

195

206
def create_event_timestamp_filter(begin_date=None, end_date=None):
@@ -25,16 +11,16 @@ def create_event_timestamp_filter(begin_date=None, end_date=None):
2511
end_date: The end date for the range.
2612
"""
2713
if begin_date and end_date:
28-
min_timestamp = _parse_min_timestamp(begin_date)
29-
max_timestamp = _parse_max_timestamp(end_date)
14+
min_timestamp = parse_min_timestamp(begin_date)
15+
max_timestamp = parse_max_timestamp(end_date)
3016
return _create_in_range_filter(min_timestamp, max_timestamp)
3117

3218
elif begin_date and not end_date:
33-
min_timestamp = _parse_min_timestamp(begin_date)
19+
min_timestamp = parse_min_timestamp(begin_date)
3420
return _create_on_or_after_filter(min_timestamp)
3521

3622
elif end_date and not begin_date:
37-
max_timestamp = _parse_max_timestamp(end_date)
23+
max_timestamp = parse_max_timestamp(end_date)
3824
return _create_on_or_before_filter(max_timestamp)
3925

4026

@@ -43,90 +29,16 @@ def _create_in_range_filter(min_timestamp, max_timestamp):
4329
return EventTimestamp.in_range(min_timestamp, max_timestamp)
4430

4531

46-
def _create_on_or_after_filter(min_timestamp):
47-
return EventTimestamp.on_or_after(min_timestamp)
48-
49-
50-
def _create_on_or_before_filter(max_timestamp):
51-
return EventTimestamp.on_or_before(max_timestamp)
52-
53-
54-
def _parse_timestamp(date_str, rounding_func):
55-
timestamp_match = TIMESTAMP_REGEX.match(date_str)
56-
magic_match = MAGIC_TIME_REGEX.match(date_str)
57-
58-
if timestamp_match:
59-
date, time = timestamp_match.groups()
60-
dt = _get_dt_from_date_time_pair(date, time)
61-
if not time:
62-
dt = rounding_func(dt)
63-
64-
elif magic_match:
65-
num, period = magic_match.groups()
66-
dt = _get_dt_from_magic_time_pair(num, period)
67-
if period == u"d":
68-
dt = rounding_func(dt)
69-
70-
else:
71-
raise DateArgumentException()
72-
return dt
73-
74-
75-
def _parse_min_timestamp(begin_date_str):
76-
dt = _parse_timestamp(begin_date_str, _round_datetime_to_day_start)
77-
78-
boundary_date = _round_datetime_to_day_start(
79-
datetime.utcnow() - timedelta(days=_MAX_LOOK_BACK_DAYS)
80-
)
81-
if dt < boundary_date:
82-
raise DateArgumentException(u"'Begin date' must be within 90 days.")
83-
84-
return convert_datetime_to_timestamp(dt)
85-
86-
87-
def _parse_max_timestamp(end_date_str):
88-
dt = _parse_timestamp(end_date_str, _round_datetime_to_day_end)
89-
return convert_datetime_to_timestamp(dt)
90-
91-
92-
def _get_dt_from_date_time_pair(date, time):
93-
date_format = u"%Y-%m-%d %H:%M:%S"
94-
if time:
95-
time = u"{}:{}:{}".format(*time.split(":") + [u"00", u"00"])
96-
else:
97-
time = u"00:00:00"
98-
date_string = u"{} {}".format(date, time)
99-
try:
100-
dt = datetime.strptime(date_string, date_format)
101-
except ValueError:
102-
raise DateArgumentException()
103-
else:
104-
return dt
105-
106-
107-
def _get_dt_from_magic_time_pair(num, period):
108-
num = int(num)
109-
if period == u"d":
110-
dt = datetime.utcnow() - timedelta(days=num)
111-
elif period == u"h":
112-
dt = datetime.utcnow() - timedelta(hours=num)
113-
elif period == u"m":
114-
dt = datetime.utcnow() - timedelta(minutes=num)
115-
else:
116-
raise DateArgumentException(u"Couldn't parse magic time string: {}{}".format(num, period))
117-
return dt
118-
119-
12032
def _verify_timestamp_order(min_timestamp, max_timestamp):
12133
if min_timestamp is None or max_timestamp is None:
12234
return
12335
if min_timestamp >= max_timestamp:
12436
raise DateArgumentException(u"Begin date cannot be after end date")
12537

12638

127-
def _round_datetime_to_day_start(dt):
128-
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
39+
def _create_on_or_after_filter(min_timestamp):
40+
return EventTimestamp.on_or_after(min_timestamp)
12941

13042

131-
def _round_datetime_to_day_end(dt):
132-
return dt.replace(hour=23, minute=59, second=59, microsecond=999000)
43+
def _create_on_or_before_filter(max_timestamp):
44+
return EventTimestamp.on_or_before(max_timestamp)

src/code42cli/date_helper.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from datetime import datetime, timedelta
2+
import re
3+
4+
from c42eventextractor.common import convert_datetime_to_timestamp
5+
6+
7+
_FORMAT_VALUE_ERROR_MESSAGE = (
8+
u"input must be a date/time string (e.g. 'YYYY-MM-DD', "
9+
u"'YY-MM-DD HH:MM', 'YY-MM-DD HH:MM:SS'), or a short value in days, "
10+
u"hours, or minutes (e.g. 30d, 24h, 15m)"
11+
)
12+
13+
TIMESTAMP_REGEX = re.compile(u"(\d{4}-\d{2}-\d{2})\s*(.*)?")
14+
MAGIC_TIME_REGEX = re.compile(u"(\d+)([dhm])$")
15+
16+
17+
class DateArgumentException(Exception):
18+
def __init__(self, message=_FORMAT_VALUE_ERROR_MESSAGE):
19+
super(DateArgumentException, self).__init__(message)
20+
21+
22+
def parse_min_timestamp(begin_date_str, max_days_back=90):
23+
dt = _parse_timestamp(begin_date_str, _round_datetime_to_day_start)
24+
25+
boundary_date = _round_datetime_to_day_start(datetime.utcnow() - timedelta(days=max_days_back))
26+
if dt < boundary_date:
27+
raise DateArgumentException(u"'Begin date' must be within 90 days.")
28+
29+
return convert_datetime_to_timestamp(dt)
30+
31+
32+
def parse_max_timestamp(end_date_str):
33+
dt = _parse_timestamp(end_date_str, _round_datetime_to_day_end)
34+
return convert_datetime_to_timestamp(dt)
35+
36+
37+
def _parse_timestamp(date_str, rounding_func):
38+
timestamp_match = TIMESTAMP_REGEX.match(date_str)
39+
magic_match = MAGIC_TIME_REGEX.match(date_str)
40+
41+
if timestamp_match:
42+
date, time = timestamp_match.groups()
43+
dt = _get_dt_from_date_time_pair(date, time)
44+
if not time:
45+
dt = rounding_func(dt)
46+
47+
elif magic_match:
48+
num, period = magic_match.groups()
49+
dt = _get_dt_from_magic_time_pair(num, period)
50+
if period == u"d":
51+
dt = rounding_func(dt)
52+
53+
else:
54+
raise DateArgumentException()
55+
return dt
56+
57+
58+
def _get_dt_from_date_time_pair(date, time):
59+
date_format = u"%Y-%m-%d %H:%M:%S"
60+
if time:
61+
time = u"{}:{}:{}".format(*time.split(":") + [u"00", u"00"])
62+
else:
63+
time = u"00:00:00"
64+
date_string = u"{} {}".format(date, time)
65+
try:
66+
dt = datetime.strptime(date_string, date_format)
67+
except ValueError:
68+
raise DateArgumentException()
69+
else:
70+
return dt
71+
72+
73+
def _get_dt_from_magic_time_pair(num, period):
74+
num = int(num)
75+
if period == u"d":
76+
dt = datetime.utcnow() - timedelta(days=num)
77+
elif period == u"h":
78+
dt = datetime.utcnow() - timedelta(hours=num)
79+
elif period == u"m":
80+
dt = datetime.utcnow() - timedelta(minutes=num)
81+
else:
82+
raise DateArgumentException(u"Couldn't parse magic time string: {}{}".format(num, period))
83+
return dt
84+
85+
86+
def _round_datetime_to_day_start(dt):
87+
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
88+
89+
90+
def _round_datetime_to_day_end(dt):
91+
return dt.replace(hour=23, minute=59, second=59, microsecond=999000)

src/code42cli/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
from code42cli import PRODUCT_NAME
77
from code42cli.cmds import profile
8+
from code42cli.cmds.detectionlists import departing_employee as de
89
from code42cli.cmds.detectionlists import high_risk_employee as hre
10+
from code42cli.cmds.detectionlists.enums import DetectionLists
911
from code42cli.cmds.securitydata import main as secmain
1012
from code42cli.commands import Command
1113
from code42cli.invoker import CommandInvoker
@@ -46,7 +48,12 @@ def _load_top_commands():
4648
subcommand_loader=secmain.load_subcommands,
4749
),
4850
Command(
49-
u"high-risk-employee",
51+
DetectionLists.DEPARTING_EMPLOYEE,
52+
detection_lists_description.format(u"departing employee"),
53+
subcommand_loader=de.load_subcommands,
54+
),
55+
Command(
56+
DetectionLists.HIGH_RISK_EMPLOYEE,
5057
detection_lists_description.format(u"high risk employee"),
5158
subcommand_loader=hre.load_subcommands,
5259
),

0 commit comments

Comments
 (0)