Skip to content

Commit 35b10ff

Browse files
authored
Merge pull request #201 from ishree-dev/cli-changes
Add parser validation APIs and log-type CLI commands
2 parents 3a0d2cc + c630cef commit 35b10ff

16 files changed

Lines changed: 695 additions & 4 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ 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.40.0] - 2026-04-06
9+
### Added
10+
- Parser validation methods
11+
- `trigger_github_checks()` - Trigger GitHub checks for a parser against an associated pull request
12+
- `get_analysis_report()` - Retrieve a completed parser analysis report
13+
- CLI support for parser validation commands
14+
- `secops log-type trigger-checks` - Trigger parser validation checks for a PR
15+
- `secops log-type get-analysis-report` - Get details of a specific analysis report
16+
817
## [0.39.0] - 2026-04-02
918
### Updated
1019
- Refactored Chronicle modules to use centralized `chronicle_request` and `chronicle_paginated_request` helper functions for improved code consistency and maintainability

CLI.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,22 @@ Error messages are detailed and help identify issues:
696696
- Size limit violations
697697
- API-specific errors
698698

699+
#### Parser Validation
700+
701+
You can trigger and retrieve analysis reports for parsers associated with GitHub pull requests.
702+
703+
Trigger GitHub checks for a parser:
704+
705+
```bash
706+
secops log-type trigger-checks --log-type "WINDOWS_AD" --associated-pr "owner/repo/pull/123"
707+
```
708+
709+
Get a parser analysis report:
710+
711+
```bash
712+
secops log-type get-analysis-report --log-type "WINDOWS_AD" --parser-id "pa_12345" --report-id "report_12345"
713+
```
714+
699715
### Parser Extension Management
700716

701717
Parser extensions provide a flexible way to extend the capabilities of existing default (or custom) parsers without replacing them.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,27 @@ This workflow is useful for:
18981898
- Re-processing logs with updated parsers
18991899
- Debugging parsing issues
19001900

1901+
### Parser Validation
1902+
1903+
Trigger and retrieve analysis reports for parsers associated with GitHub pull requests:
1904+
1905+
```python
1906+
# Trigger GitHub checks for a parser against a PR
1907+
response = chronicle.trigger_github_checks(
1908+
associated_pr="owner/repo/pull/123",
1909+
log_type="WINDOWS_AD"
1910+
)
1911+
print(f"Triggered checks: {response}")
1912+
1913+
# Retrieve the analysis report
1914+
report = chronicle.get_analysis_report(
1915+
log_type="WINDOWS_AD",
1916+
parser_id="pa_1234567890",
1917+
report_id="report_0987654321"
1918+
)
1919+
print(f"Analysis report: {report}")
1920+
```
1921+
19011922
## Parser Extension
19021923

19031924
Parser extensions provide a flexible way to extend the capabilities of existing default (or custom) parsers without replacing them. The extensions let you customize the parser pipeline by adding new parsing logic, extracting and transforming fields, and updating or removing UDM field mappings.

api_module_mapping.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ Following shows mapping between SecOps [REST Resource](https://cloud.google.com/
296296
|logTypes.getLogTypeSetting |v1alpha| | |
297297
|logTypes.legacySubmitParserExtension |v1alpha| | |
298298
|logTypes.list |v1alpha| | |
299+
|logTypes.getParserAnalysisReport |v1alpha|chronicle.parser.get_analysis_report |secops log-type get-analysis-report |
300+
|logTypes.triggerGitHubChecks |v1alpha|chronicle.parser.trigger_github_checks |secops log-type trigger-checks |
299301
|logTypes.logs.export |v1alpha| | |
300302
|logTypes.logs.get |v1alpha| | |
301303
|logTypes.logs.import |v1alpha|chronicle.log_ingest.ingest_log |secops log ingest |

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.39.0"
7+
version = "0.40.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/case.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def execute_bulk_assign(
173173
Raises:
174174
APIError: If the API request fails
175175
"""
176-
body = {"casesIds": case_ids, "username": username}
176+
body = {"casesIds": case_ids, "userName": username}
177177

178178
return chronicle_request(
179179
client,

src/secops/chronicle/client.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@
351351
create_watchlist as _create_watchlist,
352352
update_watchlist as _update_watchlist,
353353
)
354+
from secops.chronicle.parser import (
355+
get_analysis_report as _get_analysis_report,
356+
trigger_github_checks as _trigger_github_checks,
357+
)
354358
from secops.exceptions import SecOpsError
355359

356360

@@ -778,6 +782,59 @@ def update_watchlist(
778782
update_mask,
779783
)
780784

785+
def get_analysis_report(
786+
self,
787+
log_type: str,
788+
parser_id: str,
789+
report_id: str,
790+
timeout: int = 60,
791+
) -> dict[str, Any]:
792+
"""Get a parser analysis report.
793+
Args:
794+
log_type: Log type of the parser.
795+
parser_id: The ID of the parser.
796+
report_id: The ID of the analysis report.
797+
timeout: Optional timeout in seconds (default: 60).
798+
Returns:
799+
Dictionary containing the analysis report.
800+
Raises:
801+
APIError: If the API request fails.
802+
"""
803+
return _get_analysis_report(
804+
self,
805+
log_type=log_type,
806+
parser_id=parser_id,
807+
report_id=report_id,
808+
timeout=timeout,
809+
)
810+
811+
def trigger_github_checks(
812+
self,
813+
associated_pr: str,
814+
log_type: str,
815+
timeout: int = 60,
816+
) -> dict[str, Any]:
817+
"""Trigger GitHub checks for a parser.
818+
819+
Args:
820+
associated_pr: The PR string (e.g., "owner/repo/pull/123").
821+
log_type: The string name of the LogType enum.
822+
timeout: Optional request timeout in seconds (default: 60).
823+
824+
Returns:
825+
Dictionary containing the response details.
826+
827+
Raises:
828+
SecOpsError: If modules or client stub are not available.
829+
APIError: If the API request fails.
830+
"""
831+
return _trigger_github_checks(
832+
self,
833+
associated_pr=associated_pr,
834+
log_type=log_type,
835+
timeout=timeout,
836+
)
837+
781838
def get_stats(
782839
self,
783840
query: str,

src/secops/chronicle/parser.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616

1717
import base64
1818
import json
19+
import logging
1920
from typing import Any
2021

22+
from secops.chronicle.models import APIVersion
2123
from secops.chronicle.utils.format_utils import remove_none_values
2224
from secops.chronicle.utils.request_utils import (
2325
chronicle_paginated_request,
2426
chronicle_request,
2527
)
28+
from secops.exceptions import APIError, SecOpsError
2629

2730
# Constants for size limits
2831
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB per log
@@ -433,3 +436,121 @@ def run_parser(
433436
print(f"Warning: Failed to parse statedump: {e}")
434437

435438
return result
439+
440+
441+
def trigger_github_checks(
442+
client: "ChronicleClient",
443+
associated_pr: str,
444+
log_type: str,
445+
timeout: int = 60,
446+
) -> dict[str, Any]:
447+
"""Trigger GitHub checks for a parser.
448+
449+
Args:
450+
client: ChronicleClient instance
451+
associated_pr: The PR string (e.g., "owner/repo/pull/123").
452+
log_type: The string name of the LogType enum.
453+
timeout: Optional request timeout in seconds (default: 60).
454+
455+
Returns:
456+
Dictionary containing the response details.
457+
458+
Raises:
459+
SecOpsError: If input is invalid.
460+
APIError: If the API request fails.
461+
"""
462+
463+
if not isinstance(log_type, str) or len(log_type.strip()) < 2:
464+
raise SecOpsError("log_type must be a valid string of length >= 2")
465+
466+
if not isinstance(associated_pr, str) or not associated_pr.strip():
467+
raise SecOpsError("associated_pr must be a non-empty string")
468+
469+
pr_parts = associated_pr.split("/")
470+
if len(pr_parts) != 4 or pr_parts[2] != "pull" or not pr_parts[3].isdigit():
471+
raise SecOpsError(
472+
"associated_pr must be in the format 'owner/repo/pull/<number>'"
473+
)
474+
if not isinstance(timeout, int) or timeout < 0:
475+
raise SecOpsError("timeout must be a non-negative integer")
476+
477+
try:
478+
parsers = list_parsers(client, log_type=log_type)
479+
except APIError as e:
480+
raise APIError(
481+
f"Failed to fetch parsers for log type {log_type}: {e}"
482+
) from e
483+
484+
if not parsers:
485+
logging.info(
486+
"No parsers found for log type %s. Using fallback parser ID.",
487+
log_type,
488+
)
489+
parser_name = f"logTypes/{log_type}/parsers/-"
490+
else:
491+
if len(parsers) > 1:
492+
logging.warning(
493+
"Multiple parsers found for log type %s. Using the first one.",
494+
log_type,
495+
)
496+
parser_name = parsers[0]["name"]
497+
498+
endpoint_path = f"{parser_name}:runAnalysis"
499+
payload = {
500+
"report_type": "GITHUB_PARSER_VALIDATION",
501+
"pull_request": associated_pr,
502+
}
503+
504+
return chronicle_request(
505+
client=client,
506+
method="POST",
507+
api_version="v1alpha",
508+
endpoint_path=endpoint_path,
509+
json=payload,
510+
timeout=timeout,
511+
)
512+
513+
514+
def get_analysis_report(
515+
client: "ChronicleClient",
516+
log_type: str,
517+
parser_id: str,
518+
report_id: str,
519+
timeout: int = 60,
520+
) -> dict[str, Any]:
521+
"""Get a parser analysis report.
522+
523+
Args:
524+
client: ChronicleClient instance
525+
log_type: Log type of the parser.
526+
parser_id: The ID of the parser.
527+
report_id: The ID of the analysis report.
528+
timeout: Optional timeout in seconds (default: 60).
529+
530+
Returns:
531+
Dictionary containing the analysis report.
532+
533+
Raises:
534+
SecOpsError: If input is invalid.
535+
APIError: If the API request fails.
536+
"""
537+
if not isinstance(log_type, str) or not log_type.strip():
538+
raise SecOpsError("log_type must be a non-empty string")
539+
if not isinstance(parser_id, str) or not parser_id.strip():
540+
raise SecOpsError("parser_id must be a non-empty string")
541+
if not isinstance(report_id, str) or not report_id.strip():
542+
raise SecOpsError("report_id must be a non-empty string")
543+
if not isinstance(timeout, int) or timeout < 0:
544+
raise SecOpsError("timeout must be a non-negative integer")
545+
546+
endpoint_path = (
547+
f"logTypes/{log_type}/parsers/{parser_id}/analysisReports/{report_id}"
548+
)
549+
550+
return chronicle_request(
551+
client=client,
552+
method="GET",
553+
api_version=APIVersion.V1ALPHA,
554+
endpoint_path=endpoint_path,
555+
timeout=timeout,
556+
)

src/secops/cli/cli_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from secops.cli.commands.investigation import setup_investigation_command
2727
from secops.cli.commands.iocs import setup_iocs_command
2828
from secops.cli.commands.log import setup_log_command
29+
from secops.cli.commands.log_type import setup_log_type_commands
2930
from secops.cli.commands.log_processing import (
3031
setup_log_processing_command,
3132
)
@@ -168,6 +169,7 @@ def build_parser() -> argparse.ArgumentParser:
168169
setup_investigation_command(subparsers)
169170
setup_iocs_command(subparsers)
170171
setup_log_command(subparsers)
172+
setup_log_type_commands(subparsers)
171173
setup_log_processing_command(subparsers)
172174
setup_parser_command(subparsers)
173175
setup_parser_extension_command(subparsers)

0 commit comments

Comments
 (0)