Skip to content

Commit 3f138cc

Browse files
authored
Feature/magic time args (#37)
* implement magic strings * updated changelog * put back u-prefixes * missed u * more u's * more u's * add magic string description to readme * - refactor duplicate logic in _parse_{min,max}_timestamp into _parse_timestamp - fix tests to use new DateArgumentException
1 parent 70e3bf9 commit 3f138cc

7 files changed

Lines changed: 170 additions & 85 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1818
- `--filename` flag renamed to `--file-name`.
1919
- `--filepath` flag renamed to `--file-path`.
2020
- `--processOwner` flag renamed to `--process-owner`
21+
- `-b|--begin` and `-e|--end` arguments now accept shorthand date-range strings for days, hours, and minute intervals going back from the current time (e.g. `30d`, `24h`, `15m`).
2122
- Default profile validation logic added to prevent confusing error states.
2223

2324
### Added

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,31 @@ Using the CLI, you can query for events and send them to three possible destinat
6161
To print events to stdout, do:
6262

6363
```bash
64-
code42 security-data print -b 2020-02-02
64+
code42 security-data print -b <begin_date>
6565
```
6666

6767
Note that `-b` or `--begin` is usually required.
68-
To specify a time, do:
68+
69+
And end date can also be given with `-e` or `--end` to query for a specific date range (if end is not passed, it will get all events up to the present time).
70+
71+
To specify a begin/end time, you can pass a date or a date w/ time as a string:
72+
73+
```bash
74+
code42 security-data print -b '2020-02-02 12:51:00'
75+
```
76+
77+
```bash
78+
code42 security-data print -b 2020-02-02
79+
```
80+
81+
or a shorthand string specifying either days, hours, or minutes back from the current time:
82+
83+
```bash
84+
code42 security-data print -b 30d
85+
```
6986

7087
```bash
71-
code42 security-data print -b 2020-02-02 12:51
88+
code42 security-data print -b 10d -e 12h
7289
```
7390

7491
Begin date will be ignored if provided on subsequent queries using `-i`.
Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import re
12
from datetime import datetime, timedelta
23

34
from c42eventextractor.common import convert_datetime_to_timestamp
45
from py42.sdk.queries.fileevents.filters.event_filter import EventTimestamp
56

67
_MAX_LOOK_BACK_DAYS = 90
7-
_FORMAT_VALUE_ERROR_MESSAGE = u"input must be a date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format."
8+
_FORMAT_VALUE_ERROR_MESSAGE = u"input must be a date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format, 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])$")
818

919

1020
def create_event_timestamp_filter(begin_date=None, end_date=None):
@@ -14,34 +24,20 @@ def create_event_timestamp_filter(begin_date=None, end_date=None):
1424
begin_date: The begin date for the range.
1525
end_date: The end date for the range.
1626
"""
17-
1827
if begin_date and end_date:
1928
min_timestamp = _parse_min_timestamp(begin_date)
2029
max_timestamp = _parse_max_timestamp(end_date)
2130
return _create_in_range_filter(min_timestamp, max_timestamp)
31+
2232
elif begin_date and not end_date:
2333
min_timestamp = _parse_min_timestamp(begin_date)
2434
return _create_on_or_after_filter(min_timestamp)
35+
2536
elif end_date and not begin_date:
2637
max_timestamp = _parse_max_timestamp(end_date)
2738
return _create_on_or_before_filter(max_timestamp)
2839

2940

30-
def _parse_max_timestamp(end_date):
31-
if len(end_date) == 1:
32-
end_date = _get_end_date_with_eod_time_if_needed(end_date)
33-
max_time = _parse_timestamp(end_date)
34-
max_time = _add_milliseconds(max_time)
35-
else:
36-
max_time = _parse_timestamp(end_date)
37-
38-
return convert_datetime_to_timestamp(max_time)
39-
40-
41-
def _add_milliseconds(max_time):
42-
return max_time + timedelta(milliseconds=999)
43-
44-
4541
def _create_in_range_filter(min_timestamp, max_timestamp):
4642
_verify_timestamp_order(min_timestamp, max_timestamp)
4743
return EventTimestamp.in_range(min_timestamp, max_timestamp)
@@ -55,45 +51,79 @@ def _create_on_or_before_filter(max_timestamp):
5551
return EventTimestamp.on_or_before(max_timestamp)
5652

5753

58-
def _get_end_date_with_eod_time_if_needed(end_date):
59-
return end_date[0], "23:59:59"
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
6073

6174

6275
def _parse_min_timestamp(begin_date_str):
63-
min_time = _parse_timestamp(begin_date_str)
64-
min_timestamp = convert_datetime_to_timestamp(min_time)
65-
boundary_date = datetime.utcnow() - timedelta(days=_MAX_LOOK_BACK_DAYS)
66-
boundary = convert_datetime_to_timestamp(boundary_date)
67-
if min_timestamp and min_timestamp < boundary:
68-
raise ValueError(u"'Begin date' must be within 90 days.")
69-
return min_timestamp
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+
time = time or u"00:00:00"
95+
date_string = u"{} {}".format(date, time)
96+
try:
97+
dt = datetime.strptime(date_string, date_format)
98+
except ValueError:
99+
raise DateArgumentException()
100+
else:
101+
return dt
102+
103+
104+
def _get_dt_from_magic_time_pair(num, period):
105+
num = int(num)
106+
if period == u"d":
107+
dt = datetime.utcnow() - timedelta(days=num)
108+
elif period == u"h":
109+
dt = datetime.utcnow() - timedelta(hours=num)
110+
elif period == u"m":
111+
dt = datetime.utcnow() - timedelta(minutes=num)
112+
else:
113+
raise DateArgumentException(u"Couldn't parse magic time string: {}{}".format(num, period))
114+
return dt
70115

71116

72117
def _verify_timestamp_order(min_timestamp, max_timestamp):
73118
if min_timestamp is None or max_timestamp is None:
74119
return
75120
if min_timestamp >= max_timestamp:
76-
raise ValueError(u"Begin date cannot be after end date")
121+
raise DateArgumentException(u"Begin date cannot be after end date")
77122

78123

79-
def _parse_timestamp(date_and_time):
80-
try:
81-
date_str = _join_date_and_time(date_and_time)
82-
date_format = u"%Y-%m-%d" if len(date_and_time) == 1 else u"%Y-%m-%d %H:%M:%S"
83-
time = datetime.strptime(date_str, date_format)
84-
return time
85-
except ValueError:
86-
raise ValueError(_FORMAT_VALUE_ERROR_MESSAGE)
124+
def _round_datetime_to_day_start(dt):
125+
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
87126

88127

89-
def _join_date_and_time(date_and_time):
90-
if not date_and_time:
91-
return None
92-
date_str = date_and_time[0]
93-
if len(date_and_time) == 1:
94-
return date_str
95-
if len(date_and_time) == 2:
96-
date_str = "{0} {1}".format(date_str, date_and_time[1])
97-
else:
98-
raise ValueError(_FORMAT_VALUE_ERROR_MESSAGE)
99-
return date_str
128+
def _round_datetime_to_day_end(dt):
129+
return dt.replace(hour=23, minute=59, second=59, microsecond=999000)

src/code42cli/cmds/securitydata/extraction.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ def _create_filters(args):
117117

118118
def _get_event_timestamp_filter(begin_date, end_date):
119119
try:
120-
begin_date = begin_date.strip().split() if begin_date else None
121-
end_date = end_date.strip().split() if end_date else None
120+
begin_date = begin_date.strip() if begin_date else None
121+
end_date = end_date.strip() if end_date else None
122122
return date_helper.create_event_timestamp_filter(begin_date, end_date)
123-
except ValueError as ex:
123+
except date_helper.DateArgumentException as ex:
124124
print_error(str(ex))
125125
exit(1)
126126

src/code42cli/cmds/securitydata/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,17 @@ def _load_search_args(arg_collection):
106106
u"-b",
107107
u"--{}".format(enums.SearchArguments.BEGIN_DATE),
108108
help=u"The beginning of the date range in which to look for events, "
109-
u"in YYYY-MM-DD (UTC) or YYYY-MM-DD HH:MM:SS (UTC+24-hr time) format.",
109+
u"can be a date/time in YYYY-MM-DD (UTC) or YYYY-MM-DD HH:MM:SS (UTC+24-hr time) format "
110+
u"or a short value representing days (30d), hours (24h) or minutes (15m) from current "
111+
u"time.",
110112
),
111113
enums.SearchArguments.END_DATE: ArgConfig(
112114
u"-e",
113115
u"--{}".format(enums.SearchArguments.END_DATE),
114116
help=u"The end of the date range in which to look for events, "
115-
u"in YYYY-MM-DD (UTC) or YYYY-MM-DD HH:MM:SS (UTC+24-hr time) format.",
117+
u"can be a date/time in YYYY-MM-DD (UTC) or YYYY-MM-DD HH:MM:SS (UTC+24-hr time) format "
118+
u"or a short value representing days (30d), hours (24h) or minutes (15m) from current "
119+
u"time.",
116120
),
117121
enums.SearchArguments.EXPOSURE_TYPES: ArgConfig(
118122
u"-t",

tests/cmds/securitydata/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ def get_test_date_str(days_ago):
3434
begin_date_str_with_time = "{0} 3:12:33".format(begin_date_str)
3535
end_date_str = get_test_date_str(days_ago=10)
3636
end_date_str_with_time = "{0} 11:22:43".format(end_date_str)
37-
begin_date_list = [get_test_date_str(days_ago=89)]
38-
begin_date_list_with_time = [get_test_date_str(days_ago=89), "3:12:33"]
39-
end_date_list = [get_test_date_str(days_ago=10)]
40-
end_date_list_with_time = [get_test_date_str(days_ago=10), "11:22:43"]
37+
begin_date_str = get_test_date_str(days_ago=89)
38+
begin_date_with_time = [get_test_date_str(days_ago=89), "3:12:33"]
39+
end_date_str = get_test_date_str(days_ago=10)
40+
end_date_with_time = [get_test_date_str(days_ago=10), "11:22:43"]
4141

4242

4343
@pytest.fixture(autouse=True)
Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import pytest
2-
from code42cli.cmds.securitydata.date_helper import create_event_timestamp_filter
2+
from code42cli.cmds.securitydata.date_helper import (
3+
create_event_timestamp_filter,
4+
DateArgumentException,
5+
)
36

47
from .conftest import (
5-
begin_date_list,
6-
begin_date_list_with_time,
7-
end_date_list,
8-
end_date_list_with_time,
8+
begin_date_str,
9+
begin_date_with_time,
10+
end_date_str,
11+
end_date_with_time,
912
get_filter_value_from_json,
1013
get_test_date_str,
1114
)
@@ -22,57 +25,87 @@ def test_create_event_timestamp_filter_when_given_nones_returns_none():
2225

2326

2427
def test_create_event_timestamp_filter_builds_expected_query():
25-
ts_range = create_event_timestamp_filter(begin_date_list)
28+
ts_range = create_event_timestamp_filter(begin_date_str)
2629
actual = get_filter_value_from_json(ts_range, filter_index=0)
27-
expected = "{0}T00:00:00.000Z".format(begin_date_list[0])
30+
expected = "{0}T00:00:00.000Z".format(begin_date_str)
2831
assert actual == expected
2932

3033

3134
def test_create_event_timestamp_filter_when_given_begin_with_time_builds_expected_query():
32-
ts_range = create_event_timestamp_filter(begin_date_list_with_time)
35+
time_str = u"{} {}".format(*begin_date_with_time)
36+
ts_range = create_event_timestamp_filter(time_str)
3337
actual = get_filter_value_from_json(ts_range, filter_index=0)
34-
expected = "{0}T0{1}.000Z".format(begin_date_list_with_time[0], begin_date_list_with_time[1])
38+
expected = "{0}T0{1}.000Z".format(*begin_date_with_time)
3539
assert actual == expected
3640

3741

3842
def test_create_event_timestamp_filter_when_given_end_builds_expected_query():
39-
ts_range = create_event_timestamp_filter(begin_date_list, end_date_list)
43+
ts_range = create_event_timestamp_filter(begin_date_str, end_date_str)
4044
actual = get_filter_value_from_json(ts_range, filter_index=1)
41-
expected = "{0}T23:59:59.999Z".format(end_date_list[0])
45+
expected = "{0}T23:59:59.999Z".format(end_date_str)
4246
assert actual == expected
4347

4448

4549
def test_create_event_timestamp_filter_when_given_end_with_time_builds_expected_query():
46-
ts_range = create_event_timestamp_filter(begin_date_list, end_date_list_with_time)
50+
end_date_str = "{} {}".format(*end_date_with_time)
51+
ts_range = create_event_timestamp_filter(begin_date_str, end_date_str)
4752
actual = get_filter_value_from_json(ts_range, filter_index=1)
48-
expected = "{0}T{1}.000Z".format(end_date_list_with_time[0], end_date_list_with_time[1])
53+
expected = "{0}T{1}.000Z".format(*end_date_with_time)
4954
assert actual == expected
5055

5156

5257
def test_create_event_timestamp_filter_when_given_both_begin_and_end_builds_expected_query():
53-
ts_range = create_event_timestamp_filter(begin_date_list, end_date_list_with_time)
58+
end_date = "{} {}".format(*end_date_with_time)
59+
ts_range = create_event_timestamp_filter(begin_date_str, end_date)
5460
actual_begin = get_filter_value_from_json(ts_range, filter_index=0)
5561
actual_end = get_filter_value_from_json(ts_range, filter_index=1)
56-
expected_begin = "{0}T00:00:00.000Z".format(begin_date_list[0])
57-
expected_end = "{0}T{1}.000Z".format(end_date_list_with_time[0], end_date_list_with_time[1])
62+
expected_begin = "{0}T00:00:00.000Z".format(begin_date_str)
63+
expected_end = "{0}T{1}.000Z".format(*end_date_with_time)
5864
assert actual_begin == expected_begin
5965
assert actual_end == expected_end
6066

6167

6268
def test_create_event_timestamp_filter_when_begin_more_than_ninety_days_back_causes_value_error():
63-
begin_date_tuple = (get_test_date_str(days_ago=91),)
64-
with pytest.raises(ValueError):
65-
create_event_timestamp_filter(begin_date_tuple)
69+
begin_date_str = get_test_date_str(days_ago=91)
70+
with pytest.raises(DateArgumentException):
71+
create_event_timestamp_filter(begin_date_str)
6672

6773

6874
def test_create_event_timestamp_filter_when_end_is_before_begin_causes_value_error():
69-
begin_date_tuple = (get_test_date_str(days_ago=5),)
70-
end_date_str = (get_test_date_str(days_ago=7),)
71-
with pytest.raises(ValueError):
72-
create_event_timestamp_filter(begin_date_tuple, end_date_str)
75+
begin_date = get_test_date_str(days_ago=5)
76+
end_date = get_test_date_str(days_ago=7)
77+
with pytest.raises(DateArgumentException):
78+
create_event_timestamp_filter(begin_date, end_date)
79+
80+
81+
def test_create_event_timestamp_filter_when_args_are_magic_days_builds_expected_query():
82+
begin_magic_str = "10d"
83+
end_magic_str = "6d"
84+
ts_range = create_event_timestamp_filter(begin_magic_str, end_magic_str)
85+
actual_begin = get_filter_value_from_json(ts_range, filter_index=0)
86+
expected_begin = "{}T00:00:00.000Z".format(get_test_date_str(days_ago=10))
87+
actual_end = get_filter_value_from_json(ts_range, filter_index=1)
88+
expected_end = "{}T23:59:59.999Z".format(get_test_date_str(days_ago=6))
89+
assert actual_begin == expected_begin
90+
assert actual_end == expected_end
7391

7492

75-
def test_create_event_timestamp_filter_when_given_three_date_args_raises_value_error():
76-
begin_date_tuple = (get_test_date_str(days_ago=5), "12:00:00", "end_date=12:00:00")
77-
with pytest.raises(ValueError):
78-
create_event_timestamp_filter(begin_date_tuple)
93+
def test_create_event_timestamp_filter_when_given_improperly_formatted_arg_raises_value_error():
94+
missing_seconds = "{} {}".format(get_test_date_str(days_ago=5), "12:00")
95+
month_first_date = "01-01-2020"
96+
time_typo = "{} {}".format(get_test_date_str(days_ago=5), "b20:30:00")
97+
bad_magic = "2months"
98+
bad_magic_2 = "100s"
99+
bad_magic_3 = "10 d"
100+
with pytest.raises(DateArgumentException):
101+
create_event_timestamp_filter(missing_seconds)
102+
with pytest.raises(DateArgumentException):
103+
create_event_timestamp_filter(month_first_date)
104+
with pytest.raises(DateArgumentException):
105+
create_event_timestamp_filter(time_typo)
106+
with pytest.raises(DateArgumentException):
107+
create_event_timestamp_filter(bad_magic)
108+
with pytest.raises(DateArgumentException):
109+
create_event_timestamp_filter(bad_magic_2)
110+
with pytest.raises(DateArgumentException):
111+
create_event_timestamp_filter(bad_magic_3)

0 commit comments

Comments
 (0)