Skip to content

Commit 504ffac

Browse files
authored
Merge pull request #204 from simonebruzzechesse/main
New fetch_parser_candidates for parsers
2 parents b33bd52 + b5b09a5 commit 504ffac

14 files changed

Lines changed: 372 additions & 11 deletions

File tree

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.42.0] - 2026-04-15
9+
### Added
10+
- `fetch_parser_candidates()` method to retrieve parser candidates for a given log type
11+
- CLI command `secops parser fetch-candidates` for fetching parser candidates for given type
12+
813
## [0.41.0] - 2026-04-09
914
### Added
1015
- Comprehensive SOAR integration management capabilities

CLI.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,12 @@ secops parser list --log-type "OKTA" --page-size 50 --filter "state=ACTIVE"
593593
secops parser get --log-type "WINDOWS" --id "pa_12345"
594594
```
595595

596+
#### Fetch parser candidates:
597+
598+
```bash
599+
secops parser fetch-candidates --log-type "WINDOWS_DHCP" --parser-action "PARSER_ACTION_OPT_IN_TO_PREVIEW"
600+
```
601+
596602
#### Create a new parser:
597603

598604
```bash

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,12 @@ print(f"Parser content: {parser.get('text')}")
17391739
chronicle.activate_parser(log_type=log_type, id=parser_id)
17401740
chronicle.deactivate_parser(log_type=log_type, id=parser_id)
17411741

1742+
# Fetch parser candidates (unactivated prebuilt parsers)
1743+
candidates = chronicle.fetch_parser_candidates(
1744+
log_type=log_type,
1745+
parser_action="PARSER_ACTION_OPT_IN_TO_PREVIEW"
1746+
)
1747+
17421748
# Copy an existing parser as a starting point
17431749
copied_parser = chronicle.copy_parser(log_type=log_type, id="pa_existing_parser")
17441750

api_module_mapping.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/
553553
| logTypes.parsers.create | v1alpha | chronicle.parser.create_parser | secops parser create |
554554
| logTypes.parsers.deactivate | v1alpha | chronicle.parser.deactivate_parser | secops parser deactivate |
555555
| logTypes.parsers.delete | v1alpha | chronicle.parser.delete_parser | secops parser delete |
556+
| logTypes.parsers.fetchParserCandidates | v1alpha | chronicle.parser.fetch_parser_candidates | secops parser fetch-candidates |
556557
| logTypes.parsers.get | v1alpha | chronicle.parser.get_parser | secops parser get |
557558
| logTypes.parsers.list | v1alpha | chronicle.parser.list_parsers | secops parser list |
558559
| logTypes.parsers.validationReports.get | v1alpha | | |

examples/example.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ def example_udm_search(chronicle):
7878

7979

8080
def example_udm_search_view(chronicle):
81-
"""Example 14: UDM Search View."""
82-
print("\n=== Example 14: UDM Search View ===")
81+
"""Example 15: UDM Search View."""
82+
print("\n=== Example 15: UDM Search View ===")
8383
start_time, end_time = get_time_range()
8484

8585
try:
@@ -1413,9 +1413,48 @@ def example_parser_workflow(chronicle):
14131413
print(f"\nUnexpected error: {e}")
14141414

14151415

1416+
def example_fetch_parser_candidates(chronicle):
1417+
"""Example 13: Fetch Parser Candidates for a log type."""
1418+
print("\n=== Example 13: Fetch Parser Candidates ===")
1419+
1420+
log_type = "OKTA"
1421+
parser_action = "PARSER_ACTION_OPT_IN_TO_PREVIEW"
1422+
1423+
try:
1424+
print(
1425+
f"\nFetching parser candidates for log type '{log_type}' "
1426+
f"with action '{parser_action}'..."
1427+
)
1428+
candidates = chronicle.fetch_parser_candidates(
1429+
log_type=log_type,
1430+
parser_action=parser_action,
1431+
)
1432+
1433+
if not candidates:
1434+
print(f"No parser candidates found for log type '{log_type}'.")
1435+
return
1436+
1437+
print(f"Found {len(candidates)} parser candidate(s):")
1438+
for candidate in candidates:
1439+
name = candidate.get("name", "N/A")
1440+
state = candidate.get("state", "N/A")
1441+
parser_id = name.split("/")[-1]
1442+
print(f" - ID: {parser_id}, State: {state}")
1443+
1444+
except APIError as e:
1445+
print(f"\nAPI Error: {e}")
1446+
print("\nTroubleshooting tips:")
1447+
print(
1448+
"- Ensure the log type supports prebuilt parser candidates"
1449+
)
1450+
print("- Check if you have the required permissions")
1451+
except ValueError as e:
1452+
print(f"\nInvalid input: {e}")
1453+
1454+
14161455
def example_rule_test(chronicle):
1417-
"""Example 13: Test a detection rule against historical data."""
1418-
print("\n=== Example 13: Test a Detection Rule Against Historical Data ===")
1456+
"""Example 14: Test a detection rule against historical data."""
1457+
print("\n=== Example 14: Test a Detection Rule Against Historical Data ===")
14191458

14201459
# Define time range for testing - use a recent time period (last 7 days)
14211460
end_time = datetime.now(timezone.utc) - timedelta(minutes=15)
@@ -1491,8 +1530,9 @@ def example_rule_test(chronicle):
14911530
"10": example_udm_ingestion,
14921531
"11": example_gemini,
14931532
"12": example_parser_workflow,
1494-
"13": example_rule_test,
1495-
"14": example_udm_search_view,
1533+
"13": example_fetch_parser_candidates,
1534+
"14": example_rule_test,
1535+
"15": example_udm_search_view,
14961536
}
14971537

14981538

@@ -1507,7 +1547,7 @@ def main():
15071547
parser.add_argument(
15081548
"--example",
15091549
"-e",
1510-
help="Example number to run (1-14). If not specified, runs all examples.",
1550+
help="Example number to run (1-15). If not specified, runs all examples.",
15111551
)
15121552

15131553
args = parser.parse_args()

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.41.0"
7+
version = "0.42.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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
ListBasis,
138138
MonthlyScheduleDetails,
139139
OneTimeScheduleDetails,
140+
ParserAction,
140141
PrevalenceData,
141142
PythonVersion,
142143
ScheduleType,
@@ -151,6 +152,7 @@
151152
WidgetMetadata,
152153
)
153154
from secops.chronicle.nl_search import translate_nl_to_udm
155+
from secops.chronicle.parser import fetch_parser_candidates
154156
from secops.chronicle.reference_list import (
155157
ReferenceListSyntaxType,
156158
ReferenceListView,
@@ -243,6 +245,9 @@
243245
"search_raw_logs",
244246
# Natural Language Search
245247
"translate_nl_to_udm",
248+
# Parser
249+
"fetch_parser_candidates",
250+
"ParserAction",
246251
# Entity
247252
"import_entities",
248253
"summarize_entity",

src/secops/chronicle/client.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,17 @@
174174
)
175175
from secops.chronicle.models import (
176176
APIVersion,
177+
AlertState,
177178
CaseCloseReason,
178179
CaseList,
179180
CasePriority,
180181
DashboardChart,
181182
DashboardQuery,
182183
EntitySummary,
183184
InputInterval,
184-
TileType,
185-
AlertState,
186185
ListBasis,
186+
ParserAction,
187+
TileType,
187188
)
188189
from secops.chronicle.nl_search import nl_search as _nl_search
189190
from secops.chronicle.nl_search import translate_nl_to_udm
@@ -195,6 +196,9 @@
195196
from secops.chronicle.parser import create_parser as _create_parser
196197
from secops.chronicle.parser import deactivate_parser as _deactivate_parser
197198
from secops.chronicle.parser import delete_parser as _delete_parser
199+
from secops.chronicle.parser import (
200+
fetch_parser_candidates as _fetch_parser_candidates,
201+
)
198202
from secops.chronicle.parser import get_parser as _get_parser
199203
from secops.chronicle.parser import list_parsers as _list_parsers
200204
from secops.chronicle.parser import run_parser as _run_parser
@@ -2774,6 +2778,35 @@ def get_parser(
27742778
"""
27752779
return _get_parser(self, log_type=log_type, id=id)
27762780

2781+
def fetch_parser_candidates(
2782+
self,
2783+
log_type: str,
2784+
parser_action: ParserAction | str,
2785+
) -> list[Any]:
2786+
"""Retrieves prebuilt parser candidates.
2787+
2788+
Args:
2789+
log_type: Log type of the parser
2790+
parser_action: Action to perform on the parser candidates. Can be
2791+
a ParserAction enum value or a string. Valid values:
2792+
- ParserAction.PARSER_ACTION_UNSPECIFIED
2793+
- ParserAction.PARSER_ACTION_OPT_IN_TO_PREVIEW
2794+
- ParserAction.PARSER_ACTION_OPT_OUT_OF_PREVIEW
2795+
- ParserAction.CLONE_PREBUILT
2796+
2797+
Returns:
2798+
List of candidate parsers
2799+
2800+
Raises:
2801+
ValueError: If parser_action is an invalid string value
2802+
APIError: If the API request fails
2803+
"""
2804+
return _fetch_parser_candidates(
2805+
self,
2806+
log_type=log_type,
2807+
parser_action=parser_action,
2808+
)
2809+
27772810
def list_parsers(
27782811
self,
27792812
log_type: str = "-",

src/secops/chronicle/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,3 +1150,18 @@ class APIVersion(StrEnum):
11501150
V1 = "v1"
11511151
V1BETA = "v1beta"
11521152
V1ALPHA = "v1alpha"
1153+
1154+
1155+
class ParserAction(StrEnum):
1156+
"""Actions that can be performed on parser candidates.
1157+
1158+
See:
1159+
https://cloud.google.com/chronicle/docs/reference/rest/v1beta/
1160+
projects.locations.instances.logTypes.parsers/
1161+
fetchParserCandidates#ParserAction
1162+
"""
1163+
1164+
PARSER_ACTION_UNSPECIFIED = "PARSER_ACTION_UNSPECIFIED"
1165+
PARSER_ACTION_OPT_IN_TO_PREVIEW = "PARSER_ACTION_OPT_IN_TO_PREVIEW"
1166+
PARSER_ACTION_OPT_OUT_OF_PREVIEW = "PARSER_ACTION_OPT_OUT_OF_PREVIEW"
1167+
CLONE_PREBUILT = "CLONE_PREBUILT"

src/secops/chronicle/parser.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
import logging
2020
from typing import Any
2121

22-
from secops.chronicle.models import APIVersion
22+
from secops.chronicle.models import APIVersion, ParserAction
2323
from secops.chronicle.utils.format_utils import remove_none_values
2424
from secops.chronicle.utils.request_utils import (
2525
chronicle_paginated_request,
2626
chronicle_request,
2727
)
2828
from secops.exceptions import APIError, SecOpsError
2929

30+
3031
# Constants for size limits
3132
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB per log
3233
MAX_LOGS = 1000 # Maximum number of logs to process
@@ -235,6 +236,54 @@ def get_parser(
235236
)
236237

237238

239+
def fetch_parser_candidates(
240+
client: "ChronicleClient",
241+
log_type: str,
242+
parser_action: ParserAction | str,
243+
) -> list[Any]:
244+
"""Retrieves prebuilt parser candidates.
245+
246+
Args:
247+
client: ChronicleClient instance
248+
log_type: Log type of the parser
249+
parser_action: Action to perform on the parser candidates. Can be a
250+
ParserAction enum value or a string. Valid values:
251+
- ParserAction.PARSER_ACTION_UNSPECIFIED
252+
- ParserAction.PARSER_ACTION_OPT_IN_TO_PREVIEW
253+
- ParserAction.PARSER_ACTION_OPT_OUT_OF_PREVIEW
254+
- ParserAction.CLONE_PREBUILT
255+
256+
Returns:
257+
List of candidate parsers
258+
259+
Raises:
260+
ValueError: If log_type is empty or parser_action is an invalid string
261+
APIError: If the API request fails
262+
"""
263+
if not log_type:
264+
raise ValueError("log_type cannot be empty")
265+
if isinstance(parser_action, str) and not isinstance(
266+
parser_action, ParserAction
267+
):
268+
try:
269+
parser_action = ParserAction(parser_action)
270+
except ValueError as e:
271+
valid = ", ".join(m.value for m in ParserAction)
272+
raise ValueError(
273+
f'Invalid parser_action: "{parser_action}". '
274+
f"Valid values: {valid}"
275+
) from e
276+
277+
data = chronicle_request(
278+
client,
279+
method="GET",
280+
endpoint_path=f"logTypes/{log_type}/parsers:fetchParserCandidates",
281+
params={"parserAction": parser_action},
282+
error_message="Failed to fetch parser candidates",
283+
)
284+
return data.get("candidates", [])
285+
286+
238287
def list_parsers(
239288
client: "ChronicleClient",
240289
log_type: str = "-",

0 commit comments

Comments
 (0)