Skip to content

Commit 046108a

Browse files
Daniel Abrahamcopybara-github
authored andcommitted
New search API samples.
PiperOrigin-RevId: 378797442
1 parent 7e7d211 commit 046108a

7 files changed

Lines changed: 474 additions & 2 deletions

File tree

common/datetime_converter.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def iso8601_datetime_utc(utc_date_time: str) -> datetime.datetime:
3434
Raises:
3535
ValueError: Invalid input value.
3636
"""
37+
# Work-around fixable issues in user-specified timestamps.
38+
utc_date_time = re.sub(r"(\d{2}-\d{2}-\d{2})\s+(\d)", r"\1T\2",
39+
utc_date_time).upper()
40+
if utc_date_time[-1] != "Z":
41+
utc_date_time += "Z"
42+
3743
# Append the suffix "+0000" in order to produce a timezone-aware UTC datetime,
3844
# because strptime's "%z" does not recognize the meaning of the "Z" suffix.
3945
try:

requirements.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
google-api-python-client>=1.7.11
21
google-auth
3-
google-auth-httplib2>=0.0.3
42
requests

search/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#

search/list_alerts.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2021 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
"""Executable and reusable sample for listing asset- and user-based alerts."""
18+
19+
import argparse
20+
import datetime
21+
import json
22+
import sys
23+
from typing import Any, Mapping, Optional, Sequence
24+
25+
from google.auth.transport import requests
26+
27+
from common import chronicle_auth
28+
from common import datetime_converter
29+
30+
CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com"
31+
32+
33+
def initialize_command_line_args(
34+
args: Optional[Sequence[str]] = None) -> Optional[argparse.Namespace]:
35+
"""Initializes and checks all the command-line arguments."""
36+
parser = argparse.ArgumentParser()
37+
chronicle_auth.add_argument_credentials_file(parser)
38+
parser.add_argument(
39+
"-ts",
40+
"--start_time",
41+
type=datetime_converter.iso8601_datetime_utc,
42+
help=("beginning of time range, as an ISO 8601 string " +
43+
"('yyyy-mm-ddThh:mm:ss')"))
44+
parser.add_argument(
45+
"-te",
46+
"--end_time",
47+
type=datetime_converter.iso8601_datetime_utc,
48+
help=("end of time range, as an ISO 8601 string ('yyyy-mm-ddThh:mm:ss')"))
49+
parser.add_argument(
50+
"-tl",
51+
"--local_time",
52+
action="store_true",
53+
help=("times are specified in the system's local timezone " +
54+
"(default = UTC)"))
55+
56+
# Sanity checks for the command-line arguments.
57+
parsed_args = parser.parse_args(args)
58+
s, e = parsed_args.start_time, parsed_args.end_time
59+
if parsed_args.local_time:
60+
s = s.replace(tzinfo=None).astimezone(datetime.timezone.utc)
61+
e = e.replace(tzinfo=None).astimezone(datetime.timezone.utc)
62+
if s > datetime.datetime.now().astimezone(datetime.timezone.utc):
63+
print("Error: start time should not be in the future")
64+
return None
65+
if e > datetime.datetime.now().astimezone(datetime.timezone.utc):
66+
print("Error: end time should not be in the future")
67+
return None
68+
if s >= e:
69+
print("Error: start time should not be same as or later than end time")
70+
return None
71+
72+
return parsed_args
73+
74+
75+
def list_alerts(
76+
http_session: requests.AuthorizedSession,
77+
start_time: datetime.datetime,
78+
end_time: datetime.datetime,
79+
page_size: Optional[int] = 100000) -> Mapping[str, Sequence[Any]]:
80+
"""Lists up to 100,000 asset- and user-based alerts in the given time range.
81+
82+
If you receive the maximum number of results, there might still be more
83+
discovered within the specified time range. You might want to narrow the time
84+
range and issue the call again to ensure you have visibility on the results.
85+
86+
Args:
87+
http_session: Authorized session for HTTP requests.
88+
start_time: The inclusive beginning of the time range of alerts to return,
89+
with any timezone (even a timezone-unaware datetime object, i.e. local
90+
time).
91+
end_time: The exclusive end of the time range of alerts to return, with any
92+
timezone (even a timezone-unaware datetime object, i.e. local time).
93+
page_size: Maximum number of alerts to return, up to 100,000 (default =
94+
100,000).
95+
96+
Returns:
97+
{
98+
"alerts": [
99+
...One or more asset alerts (if zero, no "alerts" field at all)...
100+
],
101+
"userAlerts": [
102+
...One or more user alerts (if zero, no "userAlerts" field at all)...
103+
]
104+
}
105+
106+
Asset alert structure:
107+
{
108+
"asset": {
109+
"hostname": "..." <-- Or IP address, MAC address, product ID
110+
},
111+
"alertInfos": [
112+
...One or more alert infos...
113+
]
114+
}
115+
116+
User alert structure:
117+
{
118+
"user": {
119+
"email": "..." <-- Or user name, Windows SID, employee ID, LDAP ID
120+
},
121+
"alertInfos": [
122+
...One or more alert infos...
123+
]
124+
}
125+
126+
Alert info structure:
127+
{
128+
"name": "...",
129+
"sourceProduct": "...",
130+
"timestamp": "yyyy-mm-ddThh:mm:ssZ",
131+
"rawLog": "...", <-- Base64 encoded
132+
"uri": [
133+
"https://customer.backstory.chronicle.security/..."
134+
],
135+
"udmEvent": {
136+
...
137+
}
138+
}
139+
140+
Raises:
141+
requests.exceptions.HTTPError: HTTP request resulted in an error
142+
(response.status_code >= 400).
143+
"""
144+
url = f"{CHRONICLE_API_BASE_URL}/v1/alert/listalerts"
145+
params = {
146+
"start_time": datetime_converter.strftime(start_time),
147+
"end_time": datetime_converter.strftime(end_time),
148+
"page_size": page_size
149+
}
150+
response = http_session.request("GET", url, params=params)
151+
152+
if response.status_code >= 400:
153+
print(response.text)
154+
response.raise_for_status()
155+
return response.json()
156+
157+
158+
if __name__ == "__main__":
159+
cli = initialize_command_line_args()
160+
if not cli:
161+
sys.exit(1) # A sanity check failed.
162+
163+
start, end = cli.start_time, cli.end_time
164+
if cli.local_time:
165+
start, end = start.replace(tzinfo=None), end.replace(tzinfo=None)
166+
167+
session = chronicle_auth.initialize_http_session(cli.credentials_file)
168+
print(json.dumps(list_alerts(session, start, end), indent=2))

search/list_alerts_test.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Tests for the "list_alerts" module."""
16+
17+
import datetime
18+
import unittest
19+
from unittest import mock
20+
21+
from google.auth.transport import requests
22+
23+
from . import list_alerts
24+
25+
26+
class ListAlertsTest(unittest.TestCase):
27+
28+
def test_initialize_command_line_args_local_time(self):
29+
actual = list_alerts.initialize_command_line_args([
30+
"--start_time=2021-05-07T11:22:33", "--end_time=2021-05-08T11:22:33",
31+
"--local_time"
32+
])
33+
self.assertIsNotNone(actual)
34+
35+
def test_initialize_command_line_args_utc(self):
36+
actual = list_alerts.initialize_command_line_args(
37+
["-ts=2021-05-07T11:22:33Z", "-te=2021-05-08T11:22:33Z"])
38+
self.assertIsNotNone(actual)
39+
40+
def test_initialize_command_line_args_future_start(self):
41+
start_time = datetime.datetime.utcnow().astimezone(datetime.timezone.utc)
42+
start_time += datetime.timedelta(days=2)
43+
end_time = start_time + datetime.timedelta(days=1)
44+
actual = list_alerts.initialize_command_line_args([
45+
start_time.strftime("-ts=%Y-%m-%dT%H:%M:%SZ"),
46+
end_time.strftime("-te=%Y-%m-%dT%H:%M:%SZ")
47+
])
48+
self.assertIsNone(actual)
49+
50+
def test_initialize_command_line_args_future_end(self):
51+
start_time = datetime.datetime.utcnow().astimezone(datetime.timezone.utc)
52+
start_time -= datetime.timedelta(days=2)
53+
end_time = start_time + datetime.timedelta(days=4)
54+
actual = list_alerts.initialize_command_line_args([
55+
start_time.strftime("-ts=%Y-%m-%dT%H:%M:%SZ"),
56+
end_time.strftime("-te=%Y-%m-%dT%H:%M:%SZ")
57+
])
58+
self.assertIsNone(actual)
59+
60+
def test_initialize_command_line_args_empty_range(self):
61+
start_time = datetime.datetime.utcnow().astimezone(datetime.timezone.utc)
62+
start_time -= datetime.timedelta(days=2)
63+
actual = list_alerts.initialize_command_line_args([
64+
start_time.strftime("-ts=%Y-%m-%dT%H:%M:%SZ"),
65+
start_time.strftime("-te=%Y-%m-%dT%H:%M:%SZ")
66+
])
67+
self.assertIsNone(actual)
68+
69+
def test_initialize_command_line_args_negative_range(self):
70+
start_time = datetime.datetime.utcnow().astimezone(datetime.timezone.utc)
71+
start_time -= datetime.timedelta(days=2)
72+
end_time = start_time - datetime.timedelta(days=4)
73+
actual = list_alerts.initialize_command_line_args([
74+
start_time.strftime("-ts=%Y-%m-%dT%H:%M:%SZ"),
75+
end_time.strftime("-te=%Y-%m-%dT%H:%M:%SZ"),
76+
])
77+
self.assertIsNone(actual)
78+
79+
@mock.patch.object(requests, "AuthorizedSession", autospec=True)
80+
@mock.patch.object(requests.requests, "Response", autospec=True)
81+
def test_list_alerts(self, mock_response, mock_session):
82+
mock_session.request.return_value = mock_response
83+
type(mock_response).status_code = mock.PropertyMock(return_value=200)
84+
mock_response.json.return_value = {"mock": "json"}
85+
actual = list_alerts.list_alerts(mock_session,
86+
datetime.datetime(2021, 5, 7, 11, 22, 33),
87+
datetime.datetime(2021, 5, 8, 11, 22, 33))
88+
self.assertEqual(actual, {"mock": "json"})
89+
90+
91+
if __name__ == "__main__":
92+
unittest.main()

0 commit comments

Comments
 (0)