Skip to content

Commit 551dce2

Browse files
authored
Merge pull request #181 from PaperMtn/feature/request-helper-search
Migrate search.py to request helpers
2 parents d61fe74 + 6d6231e commit 551dce2

8 files changed

Lines changed: 255 additions & 92 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ 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.35.1] - 2026-02-23
9+
### Added
10+
- `as_list` parameter to `search_udm()` for returning events as a list instead of dictionary
11+
- CLI `--as-list` flag for `secops search` command
12+
13+
### Updated
14+
- Migrated `search_udm()` to use `chronicle_request` helper for improved error handling and consistency
15+
816
## [0.35.0] - 2026-02-18
917
### Added
1018
- CLI commands for rule retrohunt management

CLI.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ Search for events using UDM query syntax:
124124

125125
```bash
126126
secops search --query "metadata.event_type = \"NETWORK_CONNECTION\"" --max-events 10
127+
128+
# Get result as list
129+
secops search --query "metadata.event_type = \"NETWORK_CONNECTION\"" --max-events 10 --as-list
127130
```
128131

129132
Search using natural language:

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.0"
7+
version = "0.35.1"
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/client.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,8 @@ def search_udm(
873873
max_attempts: int = 30,
874874
timeout: int = 30,
875875
debug: bool = False,
876-
) -> dict[str, Any]:
876+
as_list: bool = False,
877+
) -> dict[str, Any] | list[dict[str, Any]]:
877878
"""Search UDM events in Chronicle.
878879
879880
Args:
@@ -885,13 +886,13 @@ def search_udm(
885886
max_attempts: Maximum number of polling attempts (default: 30)
886887
timeout: Timeout in seconds for each API request (default: 30)
887888
debug: Print debug information during execution
889+
as_list: If True, return a list of events instead of a dict
890+
with events list and nextPageToken.
888891
889892
Returns:
890-
Dictionary with search results containing:
891-
- events: List of UDM events with 'name' and 'udm' fields
892-
- total_events: Number of events returned
893-
- more_data_available: Boolean indicating
894-
if more results are available
893+
If as_list is True: List of Events.
894+
If as_list is False: Dict with event list, total number of event and
895+
flag to check if more data is available.
895896
896897
Raises:
897898
APIError: If the API request fails
@@ -906,6 +907,7 @@ def search_udm(
906907
max_attempts,
907908
timeout,
908909
debug,
910+
as_list,
909911
)
910912

911913
def find_udm_field_values(

src/secops/chronicle/search.py

Lines changed: 32 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@
1515
"""UDM search functionality for Chronicle."""
1616

1717
from datetime import datetime
18-
from typing import Any
18+
from typing import Any, TYPE_CHECKING
1919

20-
import requests
20+
from secops.chronicle.models import APIVersion
21+
from secops.chronicle.utils.request_utils import (
22+
chronicle_request,
23+
)
2124

22-
from secops.exceptions import APIError
25+
if TYPE_CHECKING:
26+
from secops.chronicle.client import ChronicleClient
2327

2428

2529
def search_udm(
26-
client,
30+
client: "ChronicleClient",
2731
query: str,
2832
start_time: datetime,
2933
end_time: datetime,
@@ -32,7 +36,8 @@ def search_udm(
3236
max_attempts: int = 30,
3337
timeout: int = 30,
3438
debug: bool = False,
35-
) -> dict[str, Any]:
39+
as_list: bool = False,
40+
) -> dict[str, Any] | list[dict[str, Any]]:
3641
"""Perform a UDM search query using the Chronicle V1alpha API.
3742
3843
Args:
@@ -46,23 +51,19 @@ def search_udm(
4651
for backwards compatibility)
4752
timeout: Timeout in seconds for each API request (default: 30)
4853
debug: Print debug information during execution
54+
as_list: Whether to return results as a list or dictionary
4955
5056
Returns:
51-
Dict containing the search results with events
57+
If as_list is True: List of Events.
58+
If as_list is False: Dict with event list, total number of event and
59+
flag to check if more data is available.
5260
5361
Raises:
5462
APIError: If the API request fails
5563
"""
56-
5764
# Unused parameters, kept for backward compatibility
5865
_ = (case_insensitive, max_attempts)
5966

60-
# Format the instance ID for the API call
61-
instance = client.instance_id
62-
63-
# Endpoint for UDM search
64-
url = f"{client.base_url}/{instance}:udmSearch"
65-
6667
# Format times for the API
6768
start_time_str = start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
6869
end_time_str = end_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
@@ -79,40 +80,21 @@ def search_udm(
7980
print(f"Executing UDM search: {query}")
8081
print(f"Time range: {start_time_str} to {end_time_str}")
8182

82-
try:
83-
response = client.session.get(url, params=params, timeout=timeout)
84-
85-
if response.status_code != 200:
86-
error_msg = (
87-
f"Error executing search: Status {response.status_code}, "
88-
f"Response: {response.text}"
89-
)
90-
if debug:
91-
print(f"Error: {error_msg}")
92-
raise APIError(error_msg)
93-
94-
# Parse the response
95-
response_data = response.json()
96-
97-
# Extract events and metadata
98-
events = response_data.get("events", [])
99-
more_data_available = response_data.get("moreDataAvailable", False)
100-
101-
if debug:
102-
print(f"Found {len(events)} events")
103-
print(f"More data available: {more_data_available}")
104-
105-
# Build the result structure to match the expected format
106-
result = {
107-
"events": events,
108-
"total_events": len(events),
109-
"more_data_available": more_data_available,
110-
}
111-
112-
return result
113-
114-
except requests.exceptions.RequestException as e:
115-
error_msg = f"Request failed: {str(e)}"
116-
if debug:
117-
print(f"Error: {error_msg}")
118-
raise APIError(error_msg) from e
83+
result = chronicle_request(
84+
client,
85+
method="GET",
86+
endpoint_path=":udmSearch",
87+
api_version=APIVersion.V1ALPHA,
88+
params=params,
89+
timeout=timeout,
90+
)
91+
92+
if as_list:
93+
return result.get("events", [])
94+
95+
events = result.get("events", [])
96+
return {
97+
"events": events,
98+
"total_events": len(events),
99+
"more_data_available": result.get("moreDataAvailable", False),
100+
}

src/secops/cli/commands/search.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from secops.cli.utils.common_args import (
2020
add_pagination_args,
2121
add_time_range_args,
22+
add_as_list_arg,
2223
)
2324
from secops.cli.utils.formatters import output_formatter
2425
from secops.cli.utils.time_utils import get_time_range
@@ -58,6 +59,7 @@ def setup_search_command(subparsers):
5859
"--csv", action="store_true", help="Output in CSV format"
5960
)
6061
add_time_range_args(search_parser)
62+
add_as_list_arg(search_parser)
6163
search_parser.set_defaults(func=handle_search_command)
6264

6365
search_subparser = search_parser.add_subparsers(
@@ -115,6 +117,7 @@ def handle_search_command(args, chronicle):
115117
start_time=start_time,
116118
end_time=end_time,
117119
max_events=args.max_events,
120+
as_list=args.as_list or False,
118121
)
119122
output_formatter(result, args.output)
120123
except Exception as e: # pylint: disable=broad-exception-caught

tests/chronicle/test_client.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -90,41 +90,6 @@ def test_chronicle_client_custom_session_user_agent():
9090
assert client.session.headers.get("User-Agent") == "secops-wrapper-sdk"
9191

9292

93-
def test_search_udm(chronicle_client):
94-
"""Test UDM search functionality."""
95-
# Mock the search request
96-
mock_response = Mock()
97-
mock_response.status_code = 200
98-
mock_response.json.return_value = {
99-
"events": [
100-
{
101-
"name": "projects/test-project/locations/us/instances/test-instance/events/event1",
102-
"udm": {
103-
"metadata": {
104-
"eventTimestamp": "2024-01-01T00:00:00Z",
105-
"eventType": "NETWORK_CONNECTION",
106-
},
107-
"target": {"ip": "192.168.1.1", "hostname": "test-host"},
108-
},
109-
}
110-
],
111-
"moreDataAvailable": False,
112-
}
113-
114-
with patch.object(chronicle_client.session, "get", return_value=mock_response):
115-
result = chronicle_client.search_udm(
116-
query='target.ip != ""',
117-
start_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
118-
end_time=datetime(2024, 1, 2, tzinfo=timezone.utc),
119-
max_events=10,
120-
)
121-
122-
assert "events" in result
123-
assert "total_events" in result
124-
assert result["total_events"] == 1
125-
assert result["events"][0]["udm"]["target"]["ip"] == "192.168.1.1"
126-
127-
12893
@patch("secops.chronicle.entity._detect_value_type_for_query")
12994
@patch("secops.chronicle.entity._summarize_entity_by_id")
13095
def test_summarize_entity_ip(mock_summarize_by_id, mock_detect, chronicle_client):

0 commit comments

Comments
 (0)