Skip to content

Commit edc6a63

Browse files
authored
Merge pull request #187 from dandye/search_raw_logs
feat(chronicle): implement search_raw_logs functionality
2 parents 1036143 + cd6f445 commit edc6a63

13 files changed

Lines changed: 685 additions & 2 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.36.0] - 2026-03-10
9+
### Added
10+
- Raw log search functionality with `search_raw_logs()` method
11+
- CLI command `secops search raw-logs` for searching raw logs
12+
813
## [0.35.3] - 2026-03-03
914
### Updated
1015
- Dashboard methods to use centralized `chronicle_request` helper function for improved code consistency and maintainability

CLI.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,21 @@ Search ingested UDM field values that match a query:
174174
secops search udm-field-values --query "source" --page-size 10
175175
```
176176

177+
### Search Raw Logs
178+
179+
Search for raw logs in Chronicle using the query language:
180+
181+
```bash
182+
secops search raw-logs \
183+
--query 'raw = \"authentication\"' \
184+
--snapshot-query 'user != ""' \
185+
--time-window 24 \
186+
--case-sensitive \
187+
--log-types "OKTA,AZURE_AD" \
188+
--max-aggregations-per-field 100 \
189+
--page-size 25
190+
```
191+
177192
### Get Statistics
178193

179194
Run statistical analyses on your data:

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,27 @@ results = chronicle.find_udm_field_values(
11091109
}
11101110
```
11111111

1112+
### Raw Log Search
1113+
1114+
Search for raw logs in Chronicle using the query language:
1115+
1116+
```python
1117+
from datetime import datetime, timedelta, timezone
1118+
1119+
# Set time range for search
1120+
end_time = datetime.now(timezone.utc)
1121+
start_time = end_time - timedelta(hours=24)
1122+
1123+
results = chronicle.search_raw_logs(
1124+
query='raw != "authentication"',
1125+
start_time=start_time,
1126+
end_time=end_time,
1127+
snapshot_query='status = "success"',
1128+
max_aggregations_per_field=100,
1129+
page_size=20
1130+
)
1131+
```
1132+
11121133
### Statistics Queries
11131134

11141135
Get statistics about network connections grouped by hostname:

api_module_mapping.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/
360360
|rules.retrohunts.list |v1alpha| | |
361361
|rules.updateDeployment |v1alpha| | |
362362
|searchEntities |v1alpha| | |
363-
|searchRawLogs |v1alpha| | |
363+
|searchRawLogs |v1alpha|chronicle.log_search.search_raw_logs |secops search raw-logs |
364364
|summarizeEntitiesFromQuery |v1alpha|chronicle.entity.summarize_entity |secops entity |
365365
|summarizeEntity |v1alpha|chronicle.entity.summarize_entity | |
366366
|testFindingsRefinement |v1alpha| | |

examples/log_search_example.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2025 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+
"""Example usage of raw log search functionality."""
16+
17+
import argparse
18+
from datetime import datetime, timedelta
19+
from pprint import pprint
20+
21+
from secops.chronicle import ChronicleClient
22+
from secops.exceptions import APIError
23+
24+
25+
def main():
26+
"""Run raw log search example."""
27+
parser = argparse.ArgumentParser(description="Chronicle Raw Log Search Example")
28+
parser.add_argument("--project_id", required=True, help="GCP Project ID")
29+
parser.add_argument("--customer_id", required=True, help="Chronicle Customer ID")
30+
parser.add_argument("--region", default="us", help="Chronicle Region")
31+
parser.add_argument("--query", default="user = \"user\"", help="Raw log search query")
32+
parser.add_argument("--days", type=int, default=1, help="Search time range in days")
33+
34+
args = parser.parse_args()
35+
36+
client = ChronicleClient(
37+
project_id=args.project_id,
38+
customer_id=args.customer_id,
39+
region=args.region,
40+
)
41+
42+
end_time = datetime.utcnow()
43+
start_time = end_time - timedelta(days=args.days)
44+
45+
print(f"Searching raw logs from {start_time} to {end_time}")
46+
print(f"Query: {args.query}")
47+
48+
try:
49+
# Example 1: Basic Search
50+
results = client.search_raw_logs(
51+
query=args.query,
52+
start_time=start_time,
53+
end_time=end_time,
54+
page_size=10,
55+
)
56+
57+
print("\nResults:")
58+
pprint(results)
59+
60+
# Example 2: Filtering by Log Type (if available)
61+
# Note: Replace 'OKTA' with a valid log type in your environment
62+
# print("\nSearching with Log Type filter:")
63+
# results_filtered = client.search_raw_logs(
64+
# query=args.query,
65+
# start_time=start_time,
66+
# end_time=end_time,
67+
# page_size=10,
68+
# log_types=["OKTA"]
69+
# )
70+
# pprint(results_filtered)
71+
72+
except APIError as e:
73+
print(f"API Error: {e}")
74+
except Exception as e:
75+
print(f"Error: {e}")
76+
77+
78+
if __name__ == "__main__":
79+
main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "secops"
7-
version = "0.35.3"
7+
version = "0.36.0"
88
description = "Python SDK for wrapping the Google SecOps API for common use cases"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/secops/chronicle/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
RowLogFormat,
185185
generate_udm_key_value_mappings,
186186
)
187+
from secops.chronicle.log_search import search_raw_logs
187188
from secops.chronicle.udm_search import (
188189
fetch_udm_search_csv,
189190
fetch_udm_search_view,
@@ -210,6 +211,7 @@
210211
"validate_query",
211212
"get_stats",
212213
"search_udm",
214+
"search_raw_logs",
213215
# Natural Language Search
214216
"translate_nl_to_udm",
215217
# Entity

src/secops/chronicle/client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@
309309
)
310310
from secops.chronicle.rule_validation import validate_rule as _validate_rule
311311
from secops.chronicle.search import search_udm as _search_udm
312+
from secops.chronicle.log_search import search_raw_logs as _search_raw_logs
312313
from secops.chronicle.stats import get_stats as _get_stats
313314
from secops.chronicle.udm_mapping import RowLogFormat
314315
from secops.chronicle.udm_mapping import (
@@ -910,6 +911,48 @@ def search_udm(
910911
as_list,
911912
)
912913

914+
def search_raw_logs(
915+
self,
916+
query: str,
917+
start_time: datetime,
918+
end_time: datetime,
919+
snapshot_query: str | None = None,
920+
case_sensitive: bool = False,
921+
log_types: list[str] | None = None,
922+
max_aggregations_per_field: int | None = None,
923+
page_size: int | None = None,
924+
) -> dict[str, Any]:
925+
"""Search for raw logs in Chronicle.
926+
927+
Args:
928+
query: Query to search for raw logs.
929+
start_time: Search start time (inclusive).
930+
end_time: Search end time (exclusive).
931+
snapshot_query: Optional. Query to filter results.
932+
case_sensitive: Optional. Whether search is case-sensitive.
933+
log_types: Optional. Limit results to specific log types
934+
by display name (e.g. ["OKTA"]).
935+
max_aggregations_per_field: Optional. Max values for a UDM field.
936+
page_size: Optional. Maximum number of results to return.
937+
938+
Returns:
939+
Dictionary containing search results.
940+
941+
Raises:
942+
APIError: If the API request fails.
943+
"""
944+
return _search_raw_logs(
945+
self,
946+
query=query,
947+
start_time=start_time,
948+
end_time=end_time,
949+
snapshot_query=snapshot_query,
950+
case_sensitive=case_sensitive,
951+
log_types=log_types,
952+
max_aggregations_per_field=max_aggregations_per_field,
953+
page_size=page_size,
954+
)
955+
913956
def find_udm_field_values(
914957
self, query: str, page_size: int | None = None
915958
) -> dict[str, Any]:

src/secops/chronicle/log_search.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2026 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+
"""Raw log search functionality for Chronicle."""
16+
17+
from datetime import datetime
18+
from typing import TYPE_CHECKING, Any
19+
20+
from secops.chronicle.models import APIVersion
21+
from secops.chronicle.utils.request_utils import chronicle_request
22+
23+
if TYPE_CHECKING:
24+
from secops.chronicle.client import ChronicleClient
25+
26+
27+
def search_raw_logs(
28+
client: "ChronicleClient",
29+
query: str,
30+
start_time: datetime,
31+
end_time: datetime,
32+
snapshot_query: str | None = None,
33+
case_sensitive: bool = False,
34+
log_types: list[str] | None = None,
35+
max_aggregations_per_field: int | None = None,
36+
page_size: int | None = None,
37+
) -> dict[str, Any]:
38+
"""Search for raw logs in Chronicle.
39+
40+
Args:
41+
client: The ChronicleClient instance.
42+
query: Query to search for raw logs.
43+
start_time: Search start time (inclusive).
44+
end_time: Search end time (exclusive).
45+
snapshot_query: Optional. Query to filter results.
46+
case_sensitive: Optional. Whether search is case-sensitive.
47+
log_types: Optional. Limit results to specific log types
48+
(e.g. ["OKTA"]).
49+
max_aggregations_per_field: Optional. Max values for a UDM field.
50+
page_size: Optional. Maximum number of results to return.
51+
52+
Returns:
53+
Dictionary containing search results.
54+
55+
Raises:
56+
APIError: If the API request fails.
57+
"""
58+
search_query: dict[str, Any] = {
59+
"baselineQuery": query,
60+
"baselineTimeRange": {
61+
"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
62+
"endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
63+
},
64+
"caseSensitive": case_sensitive,
65+
}
66+
67+
if snapshot_query:
68+
search_query["snapshotQuery"] = snapshot_query
69+
70+
if log_types:
71+
# The API expects a list of LogType objects, filtering by displayName
72+
search_query["logTypes"] = [{"displayName": lt} for lt in log_types]
73+
74+
if max_aggregations_per_field is not None:
75+
search_query["maxAggregationsPerField"] = max_aggregations_per_field
76+
77+
if page_size is not None:
78+
search_query["pageSize"] = page_size
79+
80+
return chronicle_request(
81+
client,
82+
method="POST",
83+
endpoint_path=":searchRawLogs",
84+
api_version=APIVersion.V1ALPHA,
85+
json=search_query,
86+
)

0 commit comments

Comments
 (0)