diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 72ec2844..dfd48345 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.10' cache: 'pip' # Install dependencies needed for linting diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d557817..bf88bcf7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: # Define Python versions as specified in tox.ini - python-version: ['3.9','3.10','3.11','3.12'] + python-version: ['3.10','3.11','3.12','3.13'] steps: - name: Check out repository code @@ -41,18 +41,18 @@ jobs: env: PYTHONPATH: ${{ github.workspace }} # Map GitHub Actions Python versions to tox environments - TOXENV: py${{ matrix.python-version == '3.9' && '39' || matrix.python-version == '3.10' && '310' || matrix.python-version == '3.11' && '311' || matrix.python-version == '3.12' && '312'}} + TOXENV: py${{ matrix.python-version == '3.10' && '310' || matrix.python-version == '3.11' && '311' || matrix.python-version == '3.12' && '312' || matrix.python-version == '3.13' && '313'}} - # Only run coverage checks on Python 3.9 + # Only run coverage checks on Python 3.10 - name: Check coverage threshold - if: matrix.python-version == '3.9' + if: matrix.python-version == '3.10' run: | pip install coverage coverage report --fail-under=60 - # Only upload coverage report for Python 3.9 + # Only upload coverage report for Python 3.10 - name: Upload coverage report - if: matrix.python-version == '3.9' + if: matrix.python-version == '3.10' uses: actions/upload-artifact@v4 with: name: coverage-report diff --git a/AUTHORING_GUIDE.md b/AUTHORING_GUIDE.md index c6677b59..19c56829 100644 --- a/AUTHORING_GUIDE.md +++ b/AUTHORING_GUIDE.md @@ -23,7 +23,7 @@ secops-wrapper/ ### Prerequisites -- Python 3.9 or higher +- Python 3.10 or higher ### Setup diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b0ae70..9adeed2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.28.0] - 2025-12-10 +### Updated +- Minimum python version support to 3.10 from 3.9 as python 3.9 has reached its end of life. + ## [0.27.2] - 2025-12-08 ### Updated - Parser list method to handle pagination properly diff --git a/TESTING.md b/TESTING.md index 109f836d..f77fac6f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -6,7 +6,7 @@ This guide provides comprehensive instructions for setting up and running tests ### Prerequisites -- Python 3.9+ +- Python 3.10+ - Chronicle Instance Details: - Customer ID - Project Number diff --git a/pyproject.toml b/pyproject.toml index 20f35768..59308c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "hatchling.build" [project] name = "secops" -version = "0.27.2" +version = "0.28.0" description = "Python SDK for wrapping the Google SecOps API for common use cases" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" license = "Apache-2.0" authors = [ { name = "Google SecOps Team", email = "chronicle@google.com" } @@ -18,10 +18,10 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Security", ] dependencies = [ diff --git a/src/secops/__init__.py b/src/secops/__init__.py index 8ad8a2de..bfd8d6b5 100644 --- a/src/secops/__init__.py +++ b/src/secops/__init__.py @@ -16,7 +16,7 @@ __version__ = "0.1.2" -from secops.client import SecOpsClient from secops.auth import SecOpsAuth +from secops.client import SecOpsClient __all__ = ["SecOpsClient", "SecOpsAuth"] diff --git a/src/secops/auth.py b/src/secops/auth.py index 769d7c37..2d349540 100644 --- a/src/secops/auth.py +++ b/src/secops/auth.py @@ -15,10 +15,11 @@ """Authentication handling for Google SecOps SDK.""" import sys +from collections.abc import Sequence from dataclasses import asdict, dataclass, field from http import HTTPStatus from types import TracebackType -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any import google.auth import google.auth.transport.requests @@ -91,7 +92,7 @@ class RetryConfig: ) backoff_factor: float = 0.3 - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the config to a dictionary for urllib3.Retry.""" return asdict(self) @@ -105,12 +106,12 @@ class LogRetry(Retry): def increment( self, - method: Optional[str] = None, - url: Optional[str] = None, - response: Optional[BaseHTTPResponse] = None, - error: Optional[Exception] = None, - _pool: Optional[ConnectionPool] = None, - _stacktrace: Optional[TracebackType] = None, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, ) -> Retry: """Return a new Retry object with incremented retry counters and logs retry attempt. @@ -148,12 +149,12 @@ class SecOpsAuth: def __init__( self, - credentials: Optional[Credentials] = None, - service_account_path: Optional[str] = None, - service_account_info: Optional[Dict[str, Any]] = None, - impersonate_service_account: Optional[str] = None, - scopes: Optional[List[str]] = None, - retry_config: Optional[Union[RetryConfig, Dict[str, Any], bool]] = None, + credentials: Credentials | None = None, + service_account_path: str | None = None, + service_account_info: dict[str, Any] | None = None, + impersonate_service_account: str | None = None, + scopes: list[str] | None = None, + retry_config: RetryConfig | dict[str, Any] | bool | None = None, ): """Initialize authentication for SecOps. @@ -179,10 +180,10 @@ def __init__( def _get_credentials( self, - credentials: Optional[Credentials], - service_account_path: Optional[str], - service_account_info: Optional[Dict[str, Any]], - impersonate_service_account: Optional[str], + credentials: Credentials | None, + service_account_path: str | None, + service_account_info: dict[str, Any] | None, + impersonate_service_account: str | None, ) -> Credentials: """Get credentials from various sources.""" try: diff --git a/src/secops/chronicle/alert.py b/src/secops/chronicle/alert.py index dd914d43..a2d6a367 100644 --- a/src/secops/chronicle/alert.py +++ b/src/secops/chronicle/alert.py @@ -15,11 +15,12 @@ """Alert functionality for Chronicle.""" import json +import re import time from datetime import datetime, timezone -from typing import Dict, Any, Optional +from typing import Any + from secops.exceptions import APIError -import re def _fix_json_formatting(data): @@ -54,13 +55,13 @@ def get_alerts( client, start_time: datetime, end_time: datetime, - snapshot_query: Optional[str] = 'feedback_summary.status != "CLOSED"', - baseline_query: Optional[str] = None, - max_alerts: Optional[int] = 1000, - enable_cache: Optional[bool] = None, + snapshot_query: str | None = 'feedback_summary.status != "CLOSED"', + baseline_query: str | None = None, + max_alerts: int | None = 1000, + enable_cache: bool | None = None, max_attempts: int = 30, poll_interval: float = 1.0, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get alerts from Chronicle. This function uses the legacy:legacyFetchAlertsView endpoint to retrieve diff --git a/src/secops/chronicle/case.py b/src/secops/chronicle/case.py index 71230d97..81994a64 100644 --- a/src/secops/chronicle/case.py +++ b/src/secops/chronicle/case.py @@ -14,22 +14,23 @@ # """Case functionality for Chronicle.""" -from typing import Dict, Any, List, Optional from datetime import datetime +from typing import Any + +from secops.chronicle.models import Case, CaseList from secops.exceptions import APIError -from secops.chronicle.models import CaseList, Case def get_cases( client, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, + start_time: datetime | None = None, + end_time: datetime | None = None, page_size: int = 100, - page_token: Optional[str] = None, - case_ids: Optional[List[str]] = None, - asset_identifiers: Optional[List[str]] = None, - tenant_id: Optional[str] = None, -) -> Dict[str, Any]: + page_token: str | None = None, + case_ids: list[str] | None = None, + asset_identifiers: list[str] | None = None, + tenant_id: str | None = None, +) -> dict[str, Any]: """Get case data from Chronicle. Args: @@ -93,7 +94,7 @@ def get_cases( raise APIError(f"Failed to parse cases response: {str(e)}") from e -def get_cases_from_list(client, case_ids: List[str]) -> CaseList: +def get_cases_from_list(client, case_ids: list[str]) -> CaseList: """Get cases from Chronicle. Args: diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 56dc8c27..d60a4d8d 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -15,9 +15,10 @@ """Chronicle API client.""" import ipaddress import re +from collections.abc import Iterator from datetime import datetime from enum import Enum -from typing import Any, Dict, Iterator, List, Literal, Optional, Union +from typing import Any, Literal, Union from google.auth.transport import requests as google_auth_requests @@ -25,7 +26,6 @@ from secops.auth import RetryConfig from secops.chronicle.alert import get_alerts as _get_alerts from secops.chronicle.case import get_cases_from_list -from secops.chronicle.models import APIVersion from secops.chronicle.dashboard import DashboardAccessType, DashboardView from secops.chronicle.dashboard import add_chart as _add_chart from secops.chronicle.dashboard import create_dashboard as _create_dashboard @@ -103,9 +103,9 @@ from secops.chronicle.log_ingest import ( get_or_create_forwarder as _get_or_create_forwarder, ) +from secops.chronicle.log_ingest import import_entities as _import_entities from secops.chronicle.log_ingest import ingest_log as _ingest_log from secops.chronicle.log_ingest import ingest_udm as _ingest_udm -from secops.chronicle.log_ingest import import_entities as _import_entities from secops.chronicle.log_ingest import list_forwarders as _list_forwarders from secops.chronicle.log_ingest import update_forwarder as _update_forwarder from secops.chronicle.log_types import get_all_log_types as _get_all_log_types @@ -115,6 +115,7 @@ from secops.chronicle.log_types import is_valid_log_type as _is_valid_log_type from secops.chronicle.log_types import search_log_types as _search_log_types from secops.chronicle.models import ( + APIVersion, CaseList, DashboardChart, DashboardQuery, @@ -226,19 +227,39 @@ from secops.chronicle.rule_retrohunt import get_retrohunt as _get_retrohunt from secops.chronicle.rule_set import ( batch_update_curated_rule_set_deployments as _batch_update_curated_rule_set_deployments, # pylint: disable=line-too-long - list_curated_rule_sets as _list_curated_rule_sets, - list_curated_rule_set_categories as _list_curated_rule_set_categories, - list_curated_rules as _list_curated_rules, - get_curated_rule as _get_curated_rule, - get_curated_rule_set_category as _get_curated_rule_set_category, +) +from secops.chronicle.rule_set import get_curated_rule as _get_curated_rule +from secops.chronicle.rule_set import ( + get_curated_rule_by_name as _get_curated_rule_by_name, +) +from secops.chronicle.rule_set import ( get_curated_rule_set as _get_curated_rule_set, - list_curated_rule_set_deployments as _list_curated_rule_set_deployments, +) +from secops.chronicle.rule_set import ( + get_curated_rule_set_category as _get_curated_rule_set_category, +) +from secops.chronicle.rule_set import ( get_curated_rule_set_deployment as _get_curated_rule_set_deployment, - get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, - get_curated_rule_by_name as _get_curated_rule_by_name, - update_curated_rule_set_deployment as _update_curated_rule_set_deployment, +) +from secops.chronicle.rule_set import ( + get_curated_rule_set_deployment_by_name as _get_curated_rule_set_deployment_by_name, # pylint: disable=line-too-long +) +from secops.chronicle.rule_set import ( + list_curated_rule_set_categories as _list_curated_rule_set_categories, +) +from secops.chronicle.rule_set import ( + list_curated_rule_set_deployments as _list_curated_rule_set_deployments, +) +from secops.chronicle.rule_set import ( + list_curated_rule_sets as _list_curated_rule_sets, +) +from secops.chronicle.rule_set import list_curated_rules as _list_curated_rules +from secops.chronicle.rule_set import ( search_curated_detections as _search_curated_detections, ) +from secops.chronicle.rule_set import ( + update_curated_rule_set_deployment as _update_curated_rule_set_deployment, +) from secops.chronicle.rule_validation import validate_rule as _validate_rule from secops.chronicle.search import search_udm as _search_udm from secops.chronicle.stats import get_stats as _get_stats @@ -329,14 +350,14 @@ def __call__( selected_version = APIVersion(version or self._default) if allowed and selected_version not in allowed: raise SecOpsError( - f"API version '{selected_version}' is not supported for this " - f"endpoint. Allowed versions: {', '.join(allowed)}" + f'API version "{selected_version}" is not supported for this ' + f'endpoint. Allowed versions: {", ".join(allowed)}' ) domain = self._get_domain(self._region) return f"https://{domain}/{selected_version}" -def _detect_value_type(value: str) -> tuple[Optional[str], Optional[str]]: +def _detect_value_type(value: str) -> tuple[str | None, str | None]: """Detect value type from a string. Args: @@ -394,12 +415,12 @@ def __init__( project_id: str, customer_id: str, region: str = "us", - auth: Optional[Any] = None, - session: Optional[Any] = None, - extra_scopes: Optional[List[str]] = None, - credentials: Optional[Any] = None, - retry_config: Optional[Union[RetryConfig, Dict[str, Any], bool]] = None, - default_api_version: Union[APIVersion, str] = APIVersion.V1ALPHA, + auth: Any | None = None, + session: Any | None = None, + extra_scopes: list[str] | None = None, + credentials: Any | None = None, + retry_config: RetryConfig | dict[str, Any] | bool | None = None, + default_api_version: APIVersion | str = APIVersion.V1ALPHA, ): """Initialize ChronicleClient. @@ -420,7 +441,7 @@ def __init__( self.region = region self.default_api_version = APIVersion(default_api_version) self._default_forwarder_display_name: str = "Wrapper-SDK-Forwarder" - self._cached_default_forwarder_id: Optional[str] = None + self._cached_default_forwarder_id: str | None = None # Format the instance ID to match the expected format if region in ["dev", "staging"]: @@ -500,9 +521,9 @@ def fetch_udm_search_view( query: str, start_time: datetime, end_time: datetime, - snapshot_query: Optional[str] = 'feedback_summary.status != "CLOSED"', - max_events: Optional[int] = 10000, - max_detections: Optional[int] = 1000, + snapshot_query: str | None = 'feedback_summary.status != "CLOSED"', + max_events: int | None = 10000, + max_detections: int | None = 1000, case_insensitive: bool = True, ) -> str: """Fetch UDM Search View results. @@ -540,7 +561,7 @@ def fetch_udm_search_view( case_insensitive, ) - def validate_query(self, query: str) -> Dict[str, Any]: + def validate_query(self, query: str) -> dict[str, Any]: """Validate a Chronicle search query. Args: @@ -564,7 +585,7 @@ def get_stats( max_events: int = 10000, case_insensitive: bool = True, max_attempts: int = 30, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get statistics from a Chronicle search query. Args: @@ -598,7 +619,7 @@ def get_stats( max_attempts, ) - def _process_stats_results(self, results: Dict[str, Any]) -> Dict[str, Any]: + def _process_stats_results(self, results: dict[str, Any]) -> dict[str, Any]: """Process stats search results. Args: @@ -667,7 +688,7 @@ def search_udm( max_attempts: int = 30, timeout: int = 30, debug: bool = False, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Search UDM events in Chronicle. Args: @@ -703,8 +724,8 @@ def search_udm( ) def find_udm_field_values( - self, query: str, page_size: Optional[int] = None - ) -> Dict[str, Any]: + self, query: str, page_size: int | None = None + ) -> dict[str, Any]: """Fetch UDM field values that match a query. Args: @@ -724,10 +745,10 @@ def summarize_entity( value: str, start_time: datetime, end_time: datetime, - preferred_entity_type: Optional[str] = None, + preferred_entity_type: str | None = None, include_all_udm_types: bool = True, page_size: int = 1000, - page_token: Optional[str] = None, + page_token: str | None = None, ) -> EntitySummary: """ Get comprehensive summary information about an entity @@ -831,7 +852,7 @@ def get_alerts( start_time: datetime, end_time: datetime, snapshot_query: str = 'feedback_summary.status != "CLOSED"', - baseline_query: Optional[str] = None, + baseline_query: str | None = None, max_alerts: int = 1000, enable_cache: bool = True, max_attempts: int = 30, @@ -932,11 +953,11 @@ def _fix_json_formatting(self, json_str: str) -> str: def create_parser_extension( self, log_type: str, - log: Optional[str] = None, - parser_config: Optional[str] = None, - field_extractors: Optional[Union[str, Dict[str, Any]]] = None, - dynamic_parsing: Optional[Union[str, Dict[str, Any]]] = None, - ) -> Dict[str, Any]: + log: str | None = None, + parser_config: str | None = None, + field_extractors: str | dict[str, Any] | None = None, + dynamic_parsing: str | dict[str, Any] | None = None, + ) -> dict[str, Any]: """Create a new parser extension. Args: @@ -995,7 +1016,7 @@ def create_parser_extension( def get_parser_extension( self, log_type: str, extension_id: str - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get details of a parser extension. Args: @@ -1010,9 +1031,9 @@ def get_parser_extension( def list_parser_extensions( self, log_type: str, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """List parser extensions. Args: @@ -1052,9 +1073,7 @@ def delete_parser_extension(self, log_type: str, extension_id: str) -> None: _delete_parser_extension(self, log_type, extension_id) # pylint: disable=function-redefined - def _detect_value_type( - self, value: str - ) -> tuple[Optional[str], Optional[str]]: + def _detect_value_type(self, value: str) -> tuple[str | None, str | None]: """ Instance method version of _detect_value_type for backward compatibility. @@ -1090,8 +1109,8 @@ def _detect_value_type(self, value, value_type=None): def create_rule( self, rule_text: str, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Creates a new detection rule to find matches in logs. Args: @@ -1109,8 +1128,8 @@ def create_rule( def get_rule( self, rule_id: str, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Get a rule by ID. Args: @@ -1131,8 +1150,8 @@ def list_feeds( self, page_size: int = 100, page_token: str = None, - api_version: Optional[APIVersion] = None, - ) -> List[Dict[str, Any]]: + api_version: APIVersion | None = None, + ) -> list[dict[str, Any]]: """List feeds. Args: @@ -1149,8 +1168,8 @@ def list_feeds( return _list_feeds(self, page_size, page_token, api_version) def get_feed( - self, feed_id: str, api_version: Optional[APIVersion] = None - ) -> Dict[str, Any]: + self, feed_id: str, api_version: APIVersion | None = None + ) -> dict[str, Any]: """Get a feed by ID. Args: @@ -1168,9 +1187,9 @@ def get_feed( def create_feed( self, display_name: str, - details: Union[str, Dict[str, Any]], - api_version: Optional[APIVersion] = None, - ) -> Dict[str, Any]: + details: str | dict[str, Any], + api_version: APIVersion | None = None, + ) -> dict[str, Any]: """Create a new feed. Args: @@ -1192,10 +1211,10 @@ def create_feed( def update_feed( self, feed_id: str, - display_name: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, - api_version: Optional[APIVersion] = None, - ) -> Dict[str, Any]: + display_name: str | None = None, + details: dict[str, Any] | None = None, + api_version: APIVersion | None = None, + ) -> dict[str, Any]: """Update a feed. Args: @@ -1216,8 +1235,8 @@ def update_feed( return _update_feed(self, feed_id, feed_config, api_version) def enable_feed( - self, feed_id: str, api_version: Optional[APIVersion] = None - ) -> Dict[str, Any]: + self, feed_id: str, api_version: APIVersion | None = None + ) -> dict[str, Any]: """Enable a feed. Args: @@ -1233,8 +1252,8 @@ def enable_feed( return _enable_feed(self, feed_id, api_version) def disable_feed( - self, feed_id: str, api_version: Optional[APIVersion] = None - ) -> Dict[str, Any]: + self, feed_id: str, api_version: APIVersion | None = None + ) -> dict[str, Any]: """Disable a feed. Args: @@ -1250,8 +1269,8 @@ def disable_feed( return _disable_feed(self, feed_id, api_version) def generate_secret( - self, feed_id: str, api_version: Optional[APIVersion] = None - ) -> Dict[str, Any]: + self, feed_id: str, api_version: APIVersion | None = None + ) -> dict[str, Any]: """Generate a secret for a feed. Args: @@ -1267,7 +1286,7 @@ def generate_secret( return _generate_secret(self, feed_id, api_version) def delete_feed( - self, feed_id: str, api_version: Optional[APIVersion] = None + self, feed_id: str, api_version: APIVersion | None = None ) -> None: """Delete a feed. @@ -1285,11 +1304,11 @@ def delete_feed( def list_rules( self, - view: Optional[str] = "FULL", - page_size: Optional[int] = None, - page_token: Optional[str] = None, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + view: str | None = "FULL", + page_size: int | None = None, + page_token: str | None = None, + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Gets a list of rules. Args: @@ -1322,8 +1341,8 @@ def update_rule( self, rule_id: str, rule_text: str, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Updates a rule. Args: @@ -1343,8 +1362,8 @@ def delete_rule( self, rule_id: str, force: bool = False, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Deletes a rule. Args: @@ -1361,7 +1380,7 @@ def delete_rule( """ return _delete_rule(self, rule_id, force, api_version) - def enable_rule(self, rule_id: str, enabled: bool = True) -> Dict[str, Any]: + def enable_rule(self, rule_id: str, enabled: bool = True) -> dict[str, Any]: """Enables or disables a rule. Args: @@ -1378,8 +1397,8 @@ def enable_rule(self, rule_id: str, enabled: bool = True) -> Dict[str, Any]: return _enable_rule(self, rule_id, enabled) def search_rules( - self, query: str, api_version: Optional[APIVersion] = APIVersion.V1 - ) -> Dict[str, Any]: + self, query: str, api_version: APIVersion | None = APIVersion.V1 + ) -> dict[str, Any]: """Search for rules. Args: @@ -1401,7 +1420,7 @@ def run_rule_test( end_time: datetime, max_results: int = 100, timeout: int = 300, - ) -> Iterator[Dict[str, Any]]: + ) -> Iterator[dict[str, Any]]: """Tests a rule against historical data and returns matches. This function connects to the legacy:legacyRunTestRule streaming @@ -1433,7 +1452,7 @@ def run_rule_test( def get_alert( self, alert_id: str, include_detections: bool = False - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Gets an alert by ID. Args: @@ -1452,18 +1471,18 @@ def get_alert( def update_alert( self, alert_id: str, - confidence_score: Optional[int] = None, - reason: Optional[str] = None, - reputation: Optional[str] = None, - priority: Optional[str] = None, - status: Optional[str] = None, - verdict: Optional[str] = None, - risk_score: Optional[int] = None, - disregarded: Optional[bool] = None, - severity: Optional[int] = None, - comment: Optional[Union[str, Literal[""]]] = None, - root_cause: Optional[Union[str, Literal[""]]] = None, - ) -> Dict[str, Any]: + confidence_score: int | None = None, + reason: str | None = None, + reputation: str | None = None, + priority: str | None = None, + status: str | None = None, + verdict: str | None = None, + risk_score: int | None = None, + disregarded: bool | None = None, + severity: int | None = None, + comment: str | Literal[""] | None = None, + root_cause: str | Literal[""] | None = None, + ) -> dict[str, Any]: """Updates an alert's properties. Args: @@ -1526,19 +1545,19 @@ def update_alert( def bulk_update_alerts( self, - alert_ids: List[str], - confidence_score: Optional[int] = None, - reason: Optional[str] = None, - reputation: Optional[str] = None, - priority: Optional[str] = None, - status: Optional[str] = None, - verdict: Optional[str] = None, - risk_score: Optional[int] = None, - disregarded: Optional[bool] = None, - severity: Optional[int] = None, - comment: Optional[Union[str, Literal[""]]] = None, - root_cause: Optional[Union[str, Literal[""]]] = None, - ) -> List[Dict[str, Any]]: + alert_ids: list[str], + confidence_score: int | None = None, + reason: str | None = None, + reputation: str | None = None, + priority: str | None = None, + status: str | None = None, + verdict: str | None = None, + risk_score: int | None = None, + disregarded: bool | None = None, + severity: int | None = None, + comment: str | Literal[""] | None = None, + root_cause: str | Literal[""] | None = None, + ) -> list[dict[str, Any]]: """Updates multiple alerts with the same properties. This is a helper function that iterates through the list of alert IDs @@ -1585,9 +1604,9 @@ def search_rule_alerts( self, start_time: datetime, end_time: datetime, - rule_status: Optional[str] = None, - page_size: Optional[int] = None, - ) -> Dict[str, Any]: + rule_status: str | None = None, + page_size: int | None = None, + ) -> dict[str, Any]: """Search for alerts generated by rules. Args: @@ -1613,15 +1632,15 @@ def search_rule_alerts( def list_detections( self, rule_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - list_basis: Optional[ + start_time: datetime | None = None, + end_time: datetime | None = None, + list_basis: None | ( Literal["LIST_BASIS_UNSPECIFIED", "CREATED_TIME", "DETECTION_TIME"] - ] = None, - alert_state: Optional[str] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + ) = None, + alert_state: str | None = None, + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """List detections for a rule. Args: @@ -1661,7 +1680,7 @@ def list_detections( page_token, ) - def list_errors(self, rule_id: str) -> Dict[str, Any]: + def list_errors(self, rule_id: str) -> dict[str, Any]: """List execution errors for a rule. Args: @@ -1685,8 +1704,8 @@ def create_retrohunt( rule_id: str, start_time: datetime, end_time: datetime, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Creates a retrohunt for a rule. A retrohunt applies a rule to historical data within @@ -1712,8 +1731,8 @@ def get_retrohunt( self, rule_id: str, operation_id: str, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Get retrohunt status and results. Args: @@ -1734,7 +1753,7 @@ def get_retrohunt( def activate_parser( self, log_type: str, id: str # pylint: disable=redefined-builtin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Activate a custom parser. Args: @@ -1751,7 +1770,7 @@ def activate_parser( def activate_release_candidate_parser( self, log_type: str, id: str # pylint: disable=redefined-builtin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Activate the release candidate parser making it live for that customer. @@ -1771,7 +1790,7 @@ def activate_release_candidate_parser( def copy_parser( self, log_type: str, id: str # pylint: disable=redefined-builtin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Makes a copy of a prebuilt parser. Args: @@ -1788,7 +1807,7 @@ def copy_parser( def create_parser( self, log_type: str, parser_code: str, validated_on_empty_logs: bool - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Creates a new parser. Args: @@ -1812,7 +1831,7 @@ def create_parser( def deactivate_parser( self, log_type: str, id: str # pylint: disable=redefined-builtin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Deactivate a custom parser. Args: @@ -1832,7 +1851,7 @@ def delete_parser( log_type: str, id: str, # pylint: disable=redefined-builtin force: bool = False, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Delete a parser. Args: @@ -1852,7 +1871,7 @@ def delete_parser( def get_parser( self, log_type: str, id: str # pylint: disable=redefined-builtin - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get a parser by ID. Args: @@ -1870,10 +1889,10 @@ def get_parser( def list_parsers( self, log_type: str = "-", - page_size: Optional[int] = None, - page_token: Optional[str] = None, + page_size: int | None = None, + page_token: str | None = None, filter: str = None, # pylint: disable=redefined-builtin - ) -> Union[List[Any], Dict[str, Any]]: + ) -> list[Any] | dict[str, Any]: """List parsers. Args: @@ -1938,8 +1957,8 @@ def run_parser( # Rule Set methods def batch_update_curated_rule_set_deployments( - self, deployments: List[Dict[str, Any]] - ) -> Dict[str, Any]: + self, deployments: list[dict[str, Any]] + ) -> dict[str, Any]: """Batch update curated rule set deployments. Args: @@ -1962,9 +1981,9 @@ def batch_update_curated_rule_set_deployments( def list_curated_rule_sets( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, + ) -> list[dict[str, Any]]: """Get a list of all curated rule sets. Args: @@ -1981,9 +2000,9 @@ def list_curated_rule_sets( def list_curated_rule_set_categories( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, + ) -> list[dict[str, Any]]: """Get a list of all curated rule set categories. Args: @@ -2000,11 +2019,11 @@ def list_curated_rule_set_categories( def list_curated_rule_set_deployments( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - only_enabled: Optional[bool] = False, - only_alerting: Optional[bool] = False, - ) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, + only_enabled: bool | None = False, + only_alerting: bool | None = False, + ) -> list[dict[str, Any]]: """Get a list of all curated rule set deployments. Args: @@ -2027,7 +2046,7 @@ def get_curated_rule_set_deployment( self, rule_set_id: str, precision: str = "precise", - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get a curated rule set deployment by ID Args: @@ -2046,7 +2065,7 @@ def get_curated_rule_set_deployment_by_name( self, display_name: str, precision: str = "precise", - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get a curated rule set deployment by human-readable display name NOTE: This is a linear scan of all curated rules, so it may be inefficient for large rule sets. @@ -2068,9 +2087,9 @@ def get_curated_rule_set_deployment_by_name( def list_curated_rules( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, + ) -> list[dict[str, Any]]: """Get a list of all curated rules. Args: @@ -2085,7 +2104,7 @@ def list_curated_rules( """ return _list_curated_rules(self, page_size, page_token) - def get_curated_rule(self, rule_id: str) -> Dict[str, Any]: + def get_curated_rule(self, rule_id: str) -> dict[str, Any]: """Get a curated rule by ID. Args: @@ -2099,7 +2118,7 @@ def get_curated_rule(self, rule_id: str) -> Dict[str, Any]: """ return _get_curated_rule(self, rule_id) - def get_curated_rule_by_name(self, display_name: str) -> Dict[str, Any]: + def get_curated_rule_by_name(self, display_name: str) -> dict[str, Any]: """Get a curated rule by human-readable display name NOTE: This is a linear scan of all curated rules, so it may be inefficient for large rule sets. @@ -2117,8 +2136,8 @@ def get_curated_rule_by_name(self, display_name: str) -> Dict[str, Any]: return _get_curated_rule_by_name(self, display_name) def update_curated_rule_set_deployment( - self, deployment: Dict[str, Any] - ) -> Dict[str, Any]: + self, deployment: dict[str, Any] + ) -> dict[str, Any]: """Update a curated rule set deployment to enable or disable alerting or change precision. @@ -2139,7 +2158,7 @@ def update_curated_rule_set_deployment( """ return _update_curated_rule_set_deployment(self, deployment) - def get_curated_rule_set_category(self, category_id: str) -> Dict[str, Any]: + def get_curated_rule_set_category(self, category_id: str) -> dict[str, Any]: """Get a curated rule set category by ID. Args: @@ -2153,7 +2172,7 @@ def get_curated_rule_set_category(self, category_id: str) -> Dict[str, Any]: """ return _get_curated_rule_set_category(self, category_id) - def get_curated_rule_set(self, rule_set_id: str) -> Dict[str, Any]: + def get_curated_rule_set(self, rule_set_id: str) -> dict[str, Any]: """Get a curated rule set by ID. Args: @@ -2170,15 +2189,15 @@ def get_curated_rule_set(self, rule_set_id: str) -> Dict[str, Any]: def search_curated_detections( self, rule_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, + start_time: datetime | None = None, + end_time: datetime | None = None, list_basis: Union["ListBasis", str] = None, - alert_state: Optional[Union["AlertState", str]] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - max_resp_size_bytes: Optional[int] = None, - include_nested_detections: Optional[bool] = False, - ) -> Dict[str, Any]: + alert_state: Union["AlertState", str] | None = None, + page_size: int | None = None, + page_token: str | None = None, + max_resp_size_bytes: int | None = None, + include_nested_detections: bool | None = False, + ) -> dict[str, Any]: """Search for detections generated by a specific curated rule. Args: @@ -2258,9 +2277,9 @@ def translate_nl_to_udm(self, text: str) -> str: def gemini( self, query: str, - conversation_id: Optional[str] = None, + conversation_id: str | None = None, context_uri: str = "/search", - context_body: Optional[Dict[str, Any]] = None, + context_body: dict[str, Any] | None = None, ) -> GeminiResponse: """Query Chronicle Gemini with a prompt. @@ -2339,7 +2358,7 @@ def nl_search( max_events: int = 10000, case_insensitive: bool = True, max_attempts: int = 30, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Perform a search using natural language that is translated to UDM. Args: @@ -2370,13 +2389,13 @@ def ingest_log( self, log_type: str, log_message: str, - log_entry_time: Optional[datetime] = None, - collection_time: Optional[datetime] = None, - forwarder_id: Optional[str] = None, + log_entry_time: datetime | None = None, + collection_time: datetime | None = None, + forwarder_id: str | None = None, force_log_type: bool = False, - namespace: Optional[str] = None, - labels: Optional[Dict[str, str]] = None, - ) -> Dict[str, Any]: + namespace: str | None = None, + labels: dict[str, str] | None = None, + ) -> dict[str, Any]: """Ingest a log into Chronicle. Args: @@ -2412,9 +2431,9 @@ def ingest_log( def import_entities( self, - entities: Union[Dict[str, Any], List[Dict[str, Any]]], + entities: dict[str, Any] | list[dict[str, Any]], log_type: str, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Import entities into Chronicle. Args: @@ -2438,14 +2457,14 @@ def import_entities( def create_forwarder( self, display_name: str, - metadata: Optional[Dict[str, Any]] = None, + metadata: dict[str, Any] | None = None, upload_compression: bool = False, enable_server: bool = False, - regex_filters: Optional[List[Dict[str, Any]]] = None, - graceful_timeout: Optional[str] = None, - drain_timeout: Optional[str] = None, - http_settings: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + regex_filters: list[dict[str, Any]] | None = None, + graceful_timeout: str | None = None, + drain_timeout: str | None = None, + http_settings: dict[str, Any] | None = None, + ) -> dict[str, Any]: """Create a new forwarder in Chronicle. Args: @@ -2482,9 +2501,9 @@ def create_forwarder( def list_forwarders( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """List forwarders in Chronicle. Args: @@ -2503,7 +2522,7 @@ def list_forwarders( page_token=page_token, ) - def get_forwarder(self, forwarder_id: str) -> Dict[str, Any]: + def get_forwarder(self, forwarder_id: str) -> dict[str, Any]: """Get a forwarder by ID. Args: @@ -2520,16 +2539,16 @@ def get_forwarder(self, forwarder_id: str) -> Dict[str, Any]: def update_forwarder( self, forwarder_id: str, - display_name: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - upload_compression: Optional[bool] = None, - enable_server: Optional[bool] = None, - regex_filters: Optional[List[Dict[str, Any]]] = None, - graceful_timeout: Optional[str] = None, - drain_timeout: Optional[str] = None, - http_settings: Optional[Dict[str, Any]] = None, - update_mask: Optional[List[str]] = None, - ) -> Dict[str, Any]: + display_name: str | None = None, + metadata: dict[str, Any] | None = None, + upload_compression: bool | None = None, + enable_server: bool | None = None, + regex_filters: list[dict[str, Any]] | None = None, + graceful_timeout: str | None = None, + drain_timeout: str | None = None, + http_settings: dict[str, Any] | None = None, + update_mask: list[str] | None = None, + ) -> dict[str, Any]: """Update a forwarder in Chronicle. Args: @@ -2566,7 +2585,7 @@ def update_forwarder( update_mask=update_mask, ) - def delete_forwarder(self, forwarder_id: str) -> Dict[str, Any]: + def delete_forwarder(self, forwarder_id: str) -> dict[str, Any]: """Delete a forwarder from Chronicle. Args: @@ -2582,7 +2601,7 @@ def delete_forwarder(self, forwarder_id: str) -> Dict[str, Any]: def get_or_create_forwarder( self, display_name: str = "Wrapper-SDK-Forwarder" - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get an existing forwarder by name or create a new one if none exists. Args: @@ -2598,9 +2617,9 @@ def get_or_create_forwarder( def get_all_log_types( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, + ) -> list[dict[str, Any]]: """Get all available Chronicle log types. Args: @@ -2627,7 +2646,7 @@ def is_valid_log_type(self, log_type_id: str) -> bool: """ return _is_valid_log_type(client=self, log_type_id=log_type_id) - def get_log_type_description(self, log_type_id: str) -> Optional[str]: + def get_log_type_description(self, log_type_id: str) -> str | None: """Get the display name for a log type ID. Args: @@ -2643,7 +2662,7 @@ def search_log_types( search_term: str, case_sensitive: bool = False, search_in_description: bool = True, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Search log types by ID or description. Args: @@ -2664,9 +2683,9 @@ def search_log_types( def ingest_udm( self, - udm_events: Union[Dict[str, Any], List[Dict[str, Any]]], + udm_events: dict[str, Any] | list[dict[str, Any]], add_missing_ids: bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Ingest UDM events directly into Chronicle. Args: @@ -2687,7 +2706,7 @@ def ingest_udm( self, udm_events=udm_events, add_missing_ids=add_missing_ids ) - def get_data_export(self, data_export_id: str) -> Dict[str, Any]: + def get_data_export(self, data_export_id: str) -> dict[str, Any]: """Get information about a specific data export. Args: @@ -2712,10 +2731,10 @@ def create_data_export( gcs_bucket: str, start_time: datetime, end_time: datetime, - log_type: Optional[str] = None, - log_types: Optional[List[str]] = None, + log_type: str | None = None, + log_types: list[str] | None = None, export_all_logs: bool = False, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Create a new data export job. Args: @@ -2778,7 +2797,7 @@ def create_data_export( export_all_logs=export_all_logs, ) - def cancel_data_export(self, data_export_id: str) -> Dict[str, Any]: + def cancel_data_export(self, data_export_id: str) -> dict[str, Any]: """Cancel an in-progress data export. Args: @@ -2802,9 +2821,9 @@ def fetch_available_log_types( self, start_time: datetime, end_time: datetime, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """Fetch available log types for export within a time range. Args: @@ -2853,11 +2872,11 @@ def fetch_available_log_types( def update_data_export( self, data_export_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - gcs_bucket: Optional[str] = None, - log_types: Optional[List[str]] = None, - ) -> Dict[str, Any]: + start_time: datetime | None = None, + end_time: datetime | None = None, + gcs_bucket: str | None = None, + log_types: list[str] | None = None, + ) -> dict[str, Any]: """Update an existing data export job. Note: The job must be in the "IN_QUEUE" state to be updated. @@ -2887,10 +2906,10 @@ def update_data_export( def list_data_export( self, - filters: Optional[str] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + filters: str | None = None, + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """List data export jobs. Args: @@ -2922,11 +2941,11 @@ def create_data_table( self, name: str, description: str, - header: Dict[str, Union[DataTableColumnType, str]], - column_options: Optional[Dict[str, Dict[str, Any]]] = None, - rows: Optional[List[List[str]]] = None, - scopes: Optional[List[str]] = None, - ) -> Dict[str, Any]: + header: dict[str, DataTableColumnType | str], + column_options: dict[str, dict[str, Any]] | None = None, + rows: list[list[str]] | None = None, + scopes: list[str] | None = None, + ) -> dict[str, Any]: """Create a new data table. Args: @@ -2948,7 +2967,7 @@ def create_data_table( self, name, description, header, column_options, rows, scopes ) - def get_data_table(self, name: str) -> Dict[str, Any]: + def get_data_table(self, name: str) -> dict[str, Any]: """Get data table details. Args: @@ -2963,8 +2982,8 @@ def get_data_table(self, name: str) -> Dict[str, Any]: return _get_data_table(self, name) def list_data_tables( - self, order_by: Optional[str] = None - ) -> List[Dict[str, Any]]: + self, order_by: str | None = None + ) -> list[dict[str, Any]]: """List data tables. Args: @@ -2981,7 +3000,7 @@ def list_data_tables( def delete_data_table( self, name: str, force: bool = False - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Delete a data table. Args: @@ -2999,8 +3018,8 @@ def delete_data_table( return _delete_data_table(self, name, force) def create_data_table_rows( - self, name: str, rows: List[List[str]] - ) -> List[Dict[str, Any]]: + self, name: str, rows: list[list[str]] + ) -> list[dict[str, Any]]: """Create data table rows, chunking if necessary. Args: @@ -3017,8 +3036,8 @@ def create_data_table_rows( return _create_data_table_rows(self, name, rows) def list_data_table_rows( - self, name: str, order_by: Optional[str] = None - ) -> List[Dict[str, Any]]: + self, name: str, order_by: str | None = None + ) -> list[dict[str, Any]]: """List data table rows. Args: @@ -3035,8 +3054,8 @@ def list_data_table_rows( return _list_data_table_rows(self, name, order_by) def delete_data_table_rows( - self, name: str, row_ids: List[str] - ) -> List[Dict[str, Any]]: + self, name: str, row_ids: list[str] + ) -> list[dict[str, Any]]: """Delete data table rows. Args: @@ -3052,8 +3071,8 @@ def delete_data_table_rows( return _delete_data_table_rows(self, name, row_ids) def replace_data_table_rows( - self, name: str, rows: List[List[str]] - ) -> List[Dict[str, Any]]: + self, name: str, rows: list[list[str]] + ) -> list[dict[str, Any]]: """Replace all data table rows with new rows, chunking if necessary. This method replaces all existing rows in a data table with the provided @@ -3075,10 +3094,10 @@ def replace_data_table_rows( def update_data_table( self, name: str, - description: Optional[str] = None, - row_time_to_live: Optional[str] = None, - update_mask: Optional[List[str]] = None, - ) -> Dict[str, Any]: + description: str | None = None, + row_time_to_live: str | None = None, + update_mask: list[str] | None = None, + ) -> dict[str, Any]: """Update a data table using the PATCH method. Args: @@ -3105,8 +3124,8 @@ def update_data_table( def update_data_table_rows( self, name: str, - row_updates: List[Dict[str, Any]], - ) -> List[Dict[str, Any]]: + row_updates: list[dict[str, Any]], + ) -> list[dict[str, Any]]: """Update multiple data table rows in bulk. This method updates existing rows in a data table using their @@ -3161,8 +3180,8 @@ def update_data_table_rows( # Rule Exclusion methods def list_rule_exclusions( - self, page_size: int = 100, page_token: Optional[str] = None - ) -> Dict[str, Any]: + self, page_size: int = 100, page_token: str | None = None + ) -> dict[str, Any]: """List rule exclusions. Args: @@ -3177,7 +3196,7 @@ def list_rule_exclusions( """ return _list_rule_exclusions(self, page_size, page_token) - def get_rule_exclusion(self, exclusion_id: str) -> Dict[str, Any]: + def get_rule_exclusion(self, exclusion_id: str) -> dict[str, Any]: """Get a rule exclusion by name. Args: @@ -3194,7 +3213,7 @@ def get_rule_exclusion(self, exclusion_id: str) -> Dict[str, Any]: def create_rule_exclusion( self, display_name: str, refinement_type: str, query: str - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Creates a new rule exclusion. Args: @@ -3221,11 +3240,11 @@ def create_rule_exclusion( def patch_rule_exclusion( self, exclusion_id: str, - display_name: Optional[str] = None, - refinement_type: Optional[str] = None, - query: Optional[str] = None, - update_mask: Optional[str] = None, - ) -> Dict[str, Any]: + display_name: str | None = None, + refinement_type: str | None = None, + query: str | None = None, + update_mask: str | None = None, + ) -> dict[str, Any]: """Updates a rule exclusion. Args: @@ -3257,10 +3276,10 @@ def patch_rule_exclusion( def compute_rule_exclusion_activity( self, - exclusion_id: Optional[str] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - ) -> Dict[str, Any]: + exclusion_id: str | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + ) -> dict[str, Any]: """Compute activity statistics for rule exclusions. Args: @@ -3283,7 +3302,7 @@ def compute_rule_exclusion_activity( def get_rule_exclusion_deployment( self, exclusion_id: str - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get deployment information for a rule exclusion. Args: @@ -3300,13 +3319,11 @@ def get_rule_exclusion_deployment( def update_rule_exclusion_deployment( self, exclusion_id: str, - enabled: Optional[bool] = None, - archived: Optional[bool] = None, - detection_exclusion_application: Optional[ - Union[str, Dict[str, Any]] - ] = None, - update_mask: Optional[str] = None, - ) -> Dict[str, Any]: + enabled: bool | None = None, + archived: bool | None = None, + detection_exclusion_application: None | (str | dict[str, Any]) = None, + update_mask: str | None = None, + ) -> dict[str, Any]: """Update deployment settings for a rule exclusion. Args: @@ -3342,10 +3359,10 @@ def create_reference_list( self, name: str, description: str = "", - entries: List[str] = None, + entries: list[str] = None, syntax_type: ReferenceListSyntaxType = ReferenceListSyntaxType.STRING, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Create a new reference list. Args: @@ -3375,8 +3392,8 @@ def get_reference_list( self, name: str, view: ReferenceListView = ReferenceListView.FULL, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Get a single reference list. Args: @@ -3396,8 +3413,8 @@ def get_reference_list( def list_reference_lists( self, view: ReferenceListView = ReferenceListView.BASIC, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> List[Dict[str, Any]]: + api_version: APIVersion | None = APIVersion.V1, + ) -> list[dict[str, Any]]: """List reference lists. Args: @@ -3417,10 +3434,10 @@ def list_reference_lists( def update_reference_list( self, name: str, - description: Optional[str] = None, - entries: Optional[List[str]] = None, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + description: str | None = None, + entries: list[str] | None = None, + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """Update a reference list. Args: @@ -3444,9 +3461,9 @@ def generate_udm_key_value_mappings( self, log_format: str, log: str, - use_array_bracket_notation: Optional[bool] = None, - compress_array_fields: Optional[bool] = None, - ) -> Dict[str, Any]: + use_array_bracket_notation: bool | None = None, + compress_array_fields: bool | None = None, + ) -> dict[str, Any]: """Generate UDM key-value mappings for provided row log Args: @@ -3475,10 +3492,10 @@ def create_dashboard( self, display_name: str, access_type: str, - description: Optional[str] = None, - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - charts: Optional[Union[List[Dict[str, Any]], str]] = None, - ) -> Dict[str, Any]: + description: str | None = None, + filters: list[dict[str, Any]] | str | None = None, + charts: list[dict[str, Any]] | str | None = None, + ) -> dict[str, Any]: """Create a new native dashboard. Args: @@ -3510,7 +3527,7 @@ def create_dashboard( charts=charts, ) - def import_dashboard(self, dashboard: Dict[str, Any]) -> Dict[str, Any]: + def import_dashboard(self, dashboard: dict[str, Any]) -> dict[str, Any]: """Create a new native dashboard. Args: @@ -3525,7 +3542,7 @@ def import_dashboard(self, dashboard: Dict[str, Any]) -> Dict[str, Any]: return _import_dashboard(self, dashboard=dashboard) - def export_dashboard(self, dashboard_names: List[str]) -> Dict[str, Any]: + def export_dashboard(self, dashboard_names: list[str]) -> dict[str, Any]: """Export native dashboards. Args: @@ -3542,9 +3559,9 @@ def export_dashboard(self, dashboard_names: List[str]) -> Dict[str, Any]: def list_dashboards( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - ) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + ) -> dict[str, Any]: """List all available dashboards. Args: @@ -3563,8 +3580,8 @@ def list_dashboards( def get_dashboard( self, dashboard_id: str, - view: Optional[str] = None, - ) -> Dict[str, Any]: + view: str | None = None, + ) -> dict[str, Any]: """Get information about a specific dashboard. Args: @@ -3590,11 +3607,11 @@ def get_dashboard( def update_dashboard( self, dashboard_id: str, - display_name: Optional[str] = None, - description: Optional[str] = None, - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - charts: Optional[Union[List[Dict[str, Any]], str]] = None, - ) -> Dict[str, Any]: + display_name: str | None = None, + description: str | None = None, + filters: list[dict[str, Any]] | str | None = None, + charts: list[dict[str, Any]] | str | None = None, + ) -> dict[str, Any]: """Update an existing dashboard. Args: @@ -3616,7 +3633,7 @@ def update_dashboard( charts=charts, ) - def delete_dashboard(self, dashboard_id: str) -> Dict[str, Any]: + def delete_dashboard(self, dashboard_id: str) -> dict[str, Any]: """Delete an existing dashboard. Args: @@ -3628,16 +3645,16 @@ def add_chart( self, dashboard_id: str, display_name: str, - chart_layout: Union[Dict[str, Any], str], - tile_type: Optional[str] = None, - chart_datasource: Optional[Union[Dict[str, Any], str]] = None, - visualization: Optional[Union[Dict[str, Any], str]] = None, - drill_down_config: Optional[Union[Dict[str, Any], str]] = None, - description: Optional[str] = None, - query: Optional[str] = None, - interval: Optional[Union[InputInterval, Dict[str, Any], str]] = None, + chart_layout: dict[str, Any] | str, + tile_type: str | None = None, + chart_datasource: dict[str, Any] | str | None = None, + visualization: dict[str, Any] | str | None = None, + drill_down_config: dict[str, Any] | str | None = None, + description: str | None = None, + query: str | None = None, + interval: InputInterval | dict[str, Any] | str | None = None, **kwargs, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Add a chart to an existing dashboard. Args: @@ -3686,8 +3703,8 @@ def duplicate_dashboard( dashboard_id: str, display_name: str, access_type: str, - description: Optional[str] = None, - ) -> Dict[str, Any]: + description: str | None = None, + ) -> dict[str, Any]: """Duplicate an existing dashboard. Args: @@ -3716,7 +3733,7 @@ def remove_chart( self, dashboard_id: str, chart_id: str, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Remove a chart from a dashboard. Args: @@ -3735,7 +3752,7 @@ def remove_chart( chart_id=chart_id, ) - def get_chart(self, chart_id: str) -> Dict[str, Any]: + def get_chart(self, chart_id: str) -> dict[str, Any]: """Get information about a specific chart. Args: @@ -3749,13 +3766,9 @@ def get_chart(self, chart_id: str) -> Dict[str, Any]: def edit_chart( self, dashboard_id: str, - dashboard_chart: Optional[ - Union[Dict[str, Any], DashboardChart, str] - ] = None, - dashboard_query: Optional[ - Union[Dict[str, Any], DashboardQuery, str] - ] = None, - ) -> Dict[str, Any]: + dashboard_chart: None | (dict[str, Any] | DashboardChart | str) = None, + dashboard_query: None | (dict[str, Any] | DashboardQuery | str) = None, + ) -> dict[str, Any]: """Edit an existing chart in a dashboard. Args: @@ -3791,10 +3804,10 @@ def edit_chart( def execute_dashboard_query( self, query: str, - interval: Union[InputInterval, Dict[str, Any], str], - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - clear_cache: Optional[bool] = None, - ) -> Dict[str, Any]: + interval: InputInterval | dict[str, Any] | str, + filters: list[dict[str, Any]] | str | None = None, + clear_cache: bool | None = None, + ) -> dict[str, Any]: """Execute a query for a dashboard. Args: @@ -3815,7 +3828,7 @@ def execute_dashboard_query( clear_cache=clear_cache, ) - def get_dashboard_query(self, query_id: str) -> Dict[str, Any]: + def get_dashboard_query(self, query_id: str) -> dict[str, Any]: """Get the dashboard query details. Args: @@ -3827,8 +3840,8 @@ def get_dashboard_query(self, query_id: str) -> Dict[str, Any]: return _get_execute_query(self, query_id=query_id) def get_rule_deployment( - self, rule_id: str, api_version: Optional[APIVersion] = APIVersion.V1 - ) -> Dict[str, Any]: + self, rule_id: str, api_version: APIVersion | None = APIVersion.V1 + ) -> dict[str, Any]: """Get the current deployment for a rule. Args: @@ -3845,11 +3858,11 @@ def get_rule_deployment( def list_rule_deployments( self, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - filter_query: Optional[str] = None, - api_version: Optional[APIVersion] = APIVersion.V1, - ) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + filter_query: str | None = None, + api_version: APIVersion | None = APIVersion.V1, + ) -> dict[str, Any]: """List rule deployments for the instance. Args: @@ -3874,7 +3887,7 @@ def list_rule_deployments( def set_rule_alerting( self, rule_id: str, enabled: bool = True - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Enable or disable alerting for a rule deployment. Args: @@ -3893,11 +3906,11 @@ def update_rule_deployment( self, rule_id: str, *, - enabled: Optional[bool] = None, - alerting: Optional[bool] = None, - archived: Optional[bool] = None, - run_frequency: Optional[str] = None, - ) -> Dict[str, Any]: + enabled: bool | None = None, + alerting: bool | None = None, + archived: bool | None = None, + run_frequency: str | None = None, + ) -> dict[str, Any]: """Generic updateDeployment wrapper. See RuleDeployment fields: enabled, alerting, archived, runFrequency. diff --git a/src/secops/chronicle/dashboard.py b/src/secops/chronicle/dashboard.py index 7f84739d..16b18060 100644 --- a/src/secops/chronicle/dashboard.py +++ b/src/secops/chronicle/dashboard.py @@ -19,7 +19,7 @@ import json import sys -from typing import Any, Dict, List, Optional, Union +from typing import Any from secops.chronicle.models import ( DashboardChart, @@ -60,10 +60,10 @@ def create_dashboard( client, display_name: str, access_type: DashboardAccessType, - description: Optional[str] = None, - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - charts: Optional[Union[List[Dict[str, Any]], str]] = None, -) -> Dict[str, Any]: + description: str | None = None, + filters: list[dict[str, Any]] | str | None = None, + charts: list[dict[str, Any]] | str | None = None, +) -> dict[str, Any]: """Create a new native dashboard. Args: @@ -125,7 +125,7 @@ def create_dashboard( return response.json() -def import_dashboard(client, dashboard: Dict[str, Any]) -> Dict[str, Any]: +def import_dashboard(client, dashboard: dict[str, Any]) -> dict[str, Any]: """Import a native dashboard. Args: @@ -162,7 +162,7 @@ def import_dashboard(client, dashboard: Dict[str, Any]) -> Dict[str, Any]: return response.json() -def export_dashboard(client, dashboard_names: List[str]) -> Dict[str, Any]: +def export_dashboard(client, dashboard_names: list[str]) -> dict[str, Any]: """Export native dashboards. Args: @@ -199,9 +199,9 @@ def export_dashboard(client, dashboard_names: List[str]) -> Dict[str, Any]: def list_dashboards( client, - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """List all available dashboards in Basic View. Args: @@ -233,8 +233,8 @@ def list_dashboards( def get_dashboard( client, dashboard_id: str, - view: Optional[DashboardView] = None, -) -> Dict[str, Any]: + view: DashboardView | None = None, +) -> dict[str, Any]: """Get information about a specific dashboard. Args: @@ -272,11 +272,11 @@ def get_dashboard( def update_dashboard( client, dashboard_id: str, - display_name: Optional[str] = None, - description: Optional[str] = None, - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - charts: Optional[Union[List[Dict[str, Any]], str]] = None, -) -> Dict[str, Any]: + display_name: str | None = None, + description: str | None = None, + filters: list[dict[str, Any]] | str | None = None, + charts: list[dict[str, Any]] | str | None = None, +) -> dict[str, Any]: """Update an existing dashboard. Args: @@ -346,7 +346,7 @@ def update_dashboard( return response.json() -def delete_dashboard(client, dashboard_id: str) -> Dict[str, Any]: +def delete_dashboard(client, dashboard_id: str) -> dict[str, Any]: """Delete a dashboard. Args: @@ -381,8 +381,8 @@ def duplicate_dashboard( dashboard_id: str, display_name: str, access_type: DashboardAccessType, - description: Optional[str] = None, -) -> Dict[str, Any]: + description: str | None = None, +) -> dict[str, Any]: """Duplicate a existing dashboard. Args: @@ -430,16 +430,16 @@ def add_chart( client, dashboard_id: str, display_name: str, - chart_layout: Union[Dict[str, Any], str], - tile_type: Optional[TileType] = None, - chart_datasource: Optional[Union[Dict[str, Any], str]] = None, - visualization: Optional[Union[Dict[str, Any], str]] = None, - drill_down_config: Optional[Union[Dict[str, Any], str]] = None, - description: Optional[str] = None, - query: Optional[str] = None, - interval: Optional[Union[InputInterval, Dict[str, Any], str]] = None, + chart_layout: dict[str, Any] | str, + tile_type: TileType | None = None, + chart_datasource: dict[str, Any] | str | None = None, + visualization: dict[str, Any] | str | None = None, + drill_down_config: dict[str, Any] | str | None = None, + description: str | None = None, + query: str | None = None, + interval: InputInterval | dict[str, Any] | str | None = None, **kwargs, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Add a chart to a dashboard. Args: @@ -533,7 +533,7 @@ def add_chart( return response.json() -def get_chart(client, chart_id: str) -> Dict[str, Any]: +def get_chart(client, chart_id: str) -> dict[str, Any]: """Get detail for dashboard chart. Args: @@ -561,7 +561,7 @@ def remove_chart( client, dashboard_id: str, chart_id: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Remove a chart from a dashboard. Args: @@ -602,13 +602,9 @@ def remove_chart( def edit_chart( client, dashboard_id: str, - dashboard_query: Optional[ - Union[Dict[str, Any], DashboardQuery, str] - ] = None, - dashboard_chart: Optional[ - Union[Dict[str, Any], DashboardChart, str] - ] = None, -) -> Dict[str, Any]: + dashboard_query: None | (dict[str, Any] | DashboardQuery | str) = None, + dashboard_chart: None | (dict[str, Any] | DashboardChart | str) = None, +) -> dict[str, Any]: """Edit an existing chart in a dashboard. Args: diff --git a/src/secops/chronicle/dashboard_query.py b/src/secops/chronicle/dashboard_query.py index 43aae136..b9da07c7 100644 --- a/src/secops/chronicle/dashboard_query.py +++ b/src/secops/chronicle/dashboard_query.py @@ -18,19 +18,19 @@ """ import json -from secops.chronicle.models import InputInterval -from typing import Any, Dict, List, Optional, Union +from typing import Any +from secops.chronicle.models import InputInterval from secops.exceptions import APIError def execute_query( client, query: str, - interval: Union[InputInterval, Dict[str, Any], str], - filters: Optional[Union[List[Dict[str, Any]], str]] = None, - clear_cache: Optional[bool] = None, -) -> Dict[str, Any]: + interval: InputInterval | dict[str, Any] | str, + filters: list[dict[str, Any]] | str | None = None, + clear_cache: bool | None = None, +) -> dict[str, Any]: """Execute a dashboard query and retrieve results. Args: @@ -78,7 +78,7 @@ def execute_query( return response.json() -def get_execute_query(client, query_id: str) -> Dict[str, Any]: +def get_execute_query(client, query_id: str) -> dict[str, Any]: """Get a dashboard query details. Args: diff --git a/src/secops/chronicle/data_export.py b/src/secops/chronicle/data_export.py index 42bd9735..8a52b3ac 100644 --- a/src/secops/chronicle/data_export.py +++ b/src/secops/chronicle/data_export.py @@ -18,9 +18,10 @@ allowing users to export Chronicle data to Google Cloud Storage buckets. """ -from typing import Dict, Any, Optional, List -from datetime import datetime from dataclasses import dataclass +from datetime import datetime +from typing import Any + from secops.exceptions import APIError @@ -74,7 +75,7 @@ def _get_formatted_log_type(client, log_type: str) -> str: return log_type -def get_data_export(client, data_export_id: str) -> Dict[str, Any]: +def get_data_export(client, data_export_id: str) -> dict[str, Any]: """Get information about a specific data export. Args: @@ -111,10 +112,10 @@ def create_data_export( gcs_bucket: str, start_time: datetime, end_time: datetime, - log_type: Optional[str] = None, - log_types: Optional[List[str]] = None, + log_type: str | None = None, + log_types: list[str] | None = None, export_all_logs: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Create a new data export job. Args: @@ -223,7 +224,7 @@ def create_data_export( return response.json() -def cancel_data_export(client, data_export_id: str) -> Dict[str, Any]: +def cancel_data_export(client, data_export_id: str) -> dict[str, Any]: """Cancel an in-progress data export. Args: @@ -259,9 +260,9 @@ def fetch_available_log_types( client, start_time: datetime, end_time: datetime, - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """Fetch available log types for export within a time range. Args: @@ -360,11 +361,11 @@ def fetch_available_log_types( def update_data_export( client, data_export_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - gcs_bucket: Optional[str] = None, - log_types: Optional[List[str]] = None, -) -> Dict[str, Any]: + start_time: datetime | None = None, + end_time: datetime | None = None, + gcs_bucket: str | None = None, + log_types: list[str] | None = None, +) -> dict[str, Any]: """Update an existing data export job. Note: The job must be in the "IN_QUEUE" state to be updated. @@ -431,10 +432,10 @@ def update_data_export( def list_data_export( client, - filters: Optional[str] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + filters: str | None = None, + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """List data export jobs. Args: diff --git a/src/secops/chronicle/data_table.py b/src/secops/chronicle/data_table.py index 42455d56..4597bbfc 100644 --- a/src/secops/chronicle/data_table.py +++ b/src/secops/chronicle/data_table.py @@ -4,7 +4,7 @@ import re import sys from itertools import islice -from typing import Any, Dict, List, Optional, Union +from typing import Any from secops.exceptions import APIError, SecOpsError @@ -25,7 +25,7 @@ def __str__(self) -> str: REF_LIST_DATA_TABLE_ID_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]{0,254}$") -def validate_cidr_entries(entries: List[str]) -> None: +def validate_cidr_entries(entries: list[str]) -> None: """Check if IP addresses are valid CIDR notation. Args: @@ -67,11 +67,11 @@ def create_data_table( client: "Any", name: str, description: str, - header: Dict[str, Union[DataTableColumnType, str]], - column_options: Optional[Dict[str, Dict[str, Any]]] = None, - rows: Optional[List[List[str]]] = None, - scopes: Optional[List[str]] = None, -) -> Dict[str, Any]: + header: dict[str, DataTableColumnType | str], + column_options: dict[str, dict[str, Any]] | None = None, + rows: list[list[str]] | None = None, + scopes: list[str] | None = None, +) -> dict[str, Any]: """Create a new data table. Args: @@ -161,8 +161,8 @@ def create_data_table( def create_data_table_rows( - client: "Any", name: str, rows: List[List[str]] -) -> List[Dict[str, Any]]: + client: "Any", name: str, rows: list[list[str]] +) -> list[dict[str, Any]]: """Create data table rows, chunking if necessary. Args: @@ -213,8 +213,8 @@ def create_data_table_rows( def _create_data_table_rows( - client: "Any", name: str, rows: List[List[str]] -) -> Dict[str, Any]: + client: "Any", name: str, rows: list[list[str]] +) -> dict[str, Any]: """Create a batch of data table rows. Args: @@ -252,7 +252,7 @@ def delete_data_table( client: "Any", name: str, force: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Delete a data table. Args: @@ -291,8 +291,8 @@ def delete_data_table( def delete_data_table_rows( client: "Any", name: str, - row_ids: List[str], -) -> List[Dict[str, Any]]: + row_ids: list[str], +) -> list[dict[str, Any]]: """Delete data table rows. Args: @@ -316,7 +316,7 @@ def _delete_data_table_row( client: "Any", table_id: str, row_guid: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Delete a single data table row. Args: @@ -352,7 +352,7 @@ def _delete_data_table_row( def get_data_table( client: "Any", name: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get data table details. Args: @@ -380,8 +380,8 @@ def get_data_table( def list_data_tables( client: "Any", - order_by: Optional[str] = None, -) -> List[Dict[str, Any]]: + order_by: str | None = None, +) -> list[dict[str, Any]]: """List data tables. Args: @@ -428,8 +428,8 @@ def list_data_tables( def list_data_table_rows( client: "Any", name: str, - order_by: Optional[str] = None, -) -> List[Dict[str, Any]]: + order_by: str | None = None, +) -> list[dict[str, Any]]: """List data table rows. Args: @@ -478,10 +478,10 @@ def list_data_table_rows( def update_data_table( client: "Any", name: str, - description: Optional[str] = None, - row_time_to_live: Optional[str] = None, - update_mask: Optional[List[str]] = None, -) -> Dict[str, Any]: + description: str | None = None, + row_time_to_live: str | None = None, + update_mask: list[str] | None = None, +) -> dict[str, Any]: """Update a existing data table. Args: @@ -536,7 +536,7 @@ def update_data_table( return response.json() -def _estimate_row_json_size(row: List[str]) -> int: +def _estimate_row_json_size(row: list[str]) -> int: """Estimate the size of a row when formatted as JSON. Args: @@ -558,8 +558,8 @@ def _estimate_row_json_size(row: List[str]) -> int: def replace_data_table_rows( - client: "Any", name: str, rows: List[List[str]] -) -> List[Dict[str, Any]]: + client: "Any", name: str, rows: list[list[str]] +) -> list[dict[str, Any]]: """Replace all rows in a data table with new rows. Args: @@ -665,8 +665,8 @@ def replace_data_table_rows( def update_data_table_rows( client: "Any", name: str, - row_updates: List[Dict[str, Any]], -) -> List[Dict[str, Any]]: + row_updates: list[dict[str, Any]], +) -> list[dict[str, Any]]: """Update data table rows in bulk, chunking if necessary. Args: @@ -726,8 +726,8 @@ def update_data_table_rows( def _update_data_table_rows( client: "Any", name: str, - row_updates: List[Dict[str, Any]], -) -> Dict[str, Any]: + row_updates: list[dict[str, Any]], +) -> dict[str, Any]: """Update a batch of data table rows. Args: diff --git a/src/secops/chronicle/entity.py b/src/secops/chronicle/entity.py index 7de46553..429d4393 100644 --- a/src/secops/chronicle/entity.py +++ b/src/secops/chronicle/entity.py @@ -15,32 +15,32 @@ """ Provides entity search, analysis and summarization functionality for Chronicle. """ -import re import ipaddress +import re from datetime import datetime -from typing import Any, List, Optional, Tuple +from typing import Any -from secops.exceptions import APIError from secops.chronicle.models import ( + AlertCount, Entity, EntityMetadata, EntityMetrics, - TimeInterval, - TimelineBucket, - Timeline, - WidgetMetadata, EntitySummary, - AlertCount, - PrevalenceData, + FileMetadataAndProperties, FileProperty, FilePropertyGroup, - FileMetadataAndProperties, + PrevalenceData, + TimeInterval, + Timeline, + TimelineBucket, + WidgetMetadata, ) +from secops.exceptions import APIError def _detect_value_type_for_query( value: str, -) -> Tuple[Optional[str], Optional[str]]: +) -> tuple[str | None, str | None]: """Detect query fragment and preferred entity type from input value. Args: @@ -147,7 +147,7 @@ def _summarize_entity_by_id( return_prevalence: bool, include_all_udm_types: bool, page_size: int, - page_token: Optional[str], + page_token: str | None, ) -> dict: """Fetch entity summary data using the entity ID. @@ -203,10 +203,10 @@ def summarize_entity( value: str, start_time: datetime, end_time: datetime, - preferred_entity_type: Optional[str] = None, + preferred_entity_type: str | None = None, include_all_udm_types: bool = True, page_size: int = 1000, - page_token: Optional[str] = None, + page_token: str | None = None, ) -> EntitySummary: """Get comprehensive summary information about an entity. @@ -263,9 +263,9 @@ def summarize_entity( ) from e # Identify primary entity and collect all entities - all_entities: List[Entity] = [] - primary_entity: Optional[Entity] = None - primary_entity_id: Optional[str] = None + all_entities: list[Entity] = [] + primary_entity: Entity | None = None + primary_entity_id: str | None = None for summary_data in query_data.get("entitySummaries", []): for entity_data in summary_data.get("entity", []): diff --git a/src/secops/chronicle/feeds.py b/src/secops/chronicle/feeds.py index f7d06e84..b9ed7f22 100644 --- a/src/secops/chronicle/feeds.py +++ b/src/secops/chronicle/feeds.py @@ -15,13 +15,14 @@ """ Provides ingestion feed management functionality for Chronicle. """ -from secops.exceptions import APIError -from dataclasses import dataclass, asdict -from secops.chronicle.models import APIVersion -from typing import Dict, Any, List, TypedDict, Optional, Union, Annotated -import sys -import os import json +import os +import sys +from dataclasses import asdict, dataclass +from typing import Annotated, Any, TypedDict + +from secops.chronicle.models import APIVersion +from secops.exceptions import APIError # Use built-in StrEnum if Python 3.11+, otherwise create a compatible version if sys.version_info >= (3, 11): @@ -52,7 +53,7 @@ class CreateFeedModel: display_name: Annotated[str, "Display name for the feed"] details: Annotated[ - Union[str, Dict[str, Any]], "Feed details as JSON string or dict" + str | dict[str, Any], "Feed details as JSON string or dict" ] def __post_init__(self): @@ -63,7 +64,7 @@ def __post_init__(self): except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON string for details: {e}") from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON serialization.""" return asdict(self) @@ -79,10 +80,10 @@ class UpdateFeedModel: """ display_name: Annotated[ - Optional[str], "Optional display name for the feed" + str | None, "Optional display name for the feed" ] = None details: Annotated[ - Optional[Union[str, Dict[str, Any]]], + str | dict[str, Any] | None, "Optional feed details as JSON string or dict", ] = None @@ -94,7 +95,7 @@ def __post_init__(self): except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON string for details: {e}") from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON serialization.""" return asdict(self) @@ -132,8 +133,8 @@ def list_feeds( client, page_size: int = 100, page_token: str = None, - api_version: Optional[APIVersion] = None, -) -> List[Feed]: + api_version: APIVersion | None = None, +) -> list[Feed]: """List feeds. Args: @@ -174,7 +175,7 @@ def list_feeds( def get_feed( - client, feed_id: str, api_version: Optional[APIVersion] = None + client, feed_id: str, api_version: APIVersion | None = None ) -> Feed: """Get a feed by ID. @@ -204,7 +205,7 @@ def get_feed( def create_feed( client, feed_config: CreateFeedModel, - api_version: Optional[APIVersion] = None, + api_version: APIVersion | None = None, ) -> Feed: """Create a new feed. @@ -234,8 +235,8 @@ def update_feed( client, feed_id: str, feed_config: CreateFeedModel, - update_mask: Optional[Union[List[str], None]] = None, - api_version: Optional[APIVersion] = None, + update_mask: list[str] | None | None = None, + api_version: APIVersion | None = None, ) -> Feed: """Update an existing feed. @@ -278,7 +279,7 @@ def update_feed( def delete_feed( - client, feed_id: str, api_version: Optional[APIVersion] = None + client, feed_id: str, api_version: APIVersion | None = None ) -> None: """Delete a feed. @@ -300,7 +301,7 @@ def delete_feed( def disable_feed( - client, feed_id: str, api_version: Optional[APIVersion] = None + client, feed_id: str, api_version: APIVersion | None = None ) -> Feed: """Disable a feed. @@ -327,7 +328,7 @@ def disable_feed( def enable_feed( - client, feed_id: str, api_version: Optional[APIVersion] = None + client, feed_id: str, api_version: APIVersion | None = None ) -> Feed: """Enable a feed. @@ -354,7 +355,7 @@ def enable_feed( def generate_secret( - client, feed_id: str, api_version: Optional[APIVersion] = None + client, feed_id: str, api_version: APIVersion | None = None ) -> FeedSecret: """Generate a secret for a feed. diff --git a/src/secops/chronicle/gemini.py b/src/secops/chronicle/gemini.py index e124f266..c3a1969e 100644 --- a/src/secops/chronicle/gemini.py +++ b/src/secops/chronicle/gemini.py @@ -16,9 +16,10 @@ Provides access to Chronicle's Gemini conversational AI interface. """ -from typing import Dict, Any, List, Optional -from secops.exceptions import APIError import re +from typing import Any + +from secops.exceptions import APIError class Block: @@ -28,9 +29,7 @@ class Block: (text, code, HTML, etc.) returned in a Gemini conversation response. """ - def __init__( - self, block_type: str, content: str, title: Optional[str] = None - ): + def __init__(self, block_type: str, content: str, title: str | None = None): """Initialize a response block. Args: @@ -81,8 +80,8 @@ def __init__( self, display_text: str, action_type: str, - use_case_id: Optional[str] = None, - navigation: Optional[NavigationAction] = None, + use_case_id: str | None = None, + navigation: NavigationAction | None = None, ): """Initialize a suggested action. @@ -117,11 +116,11 @@ def __init__( name: str, input_query: str, create_time: str, - blocks: List[Block], - suggested_actions: Optional[List[SuggestedAction]] = None, - references: Optional[List[Block]] = None, - groundings: Optional[List[str]] = None, - raw_response: Optional[Dict[str, Any]] = None, + blocks: list[Block], + suggested_actions: list[SuggestedAction] | None = None, + references: list[Block] | None = None, + groundings: list[str] | None = None, + raw_response: dict[str, Any] | None = None, ): """Initialize a Gemini response. @@ -156,7 +155,7 @@ def __repr__(self) -> str: ) @classmethod - def from_api_response(cls, response: Dict[str, Any]) -> "GeminiResponse": + def from_api_response(cls, response: dict[str, Any]) -> "GeminiResponse": """Create a GeminiResponse object from an API response. Args: @@ -287,7 +286,7 @@ def strip_html_tags(html_content): return "\n\n".join(all_content) if all_content else "" - def get_code_blocks(self) -> List[Block]: + def get_code_blocks(self) -> list[Block]: """Get all CODE blocks. Returns: @@ -295,7 +294,7 @@ def get_code_blocks(self) -> List[Block]: """ return [block for block in self.blocks if block.block_type == "CODE"] - def get_html_blocks(self) -> List[Block]: + def get_html_blocks(self) -> list[Block]: """Get all HTML blocks. Returns: @@ -303,7 +302,7 @@ def get_html_blocks(self) -> List[Block]: """ return [block for block in self.blocks if block.block_type == "HTML"] - def get_raw_response(self) -> Dict[str, Any]: + def get_raw_response(self) -> dict[str, Any]: """Get the raw API response as a dictionary. This provides access to the complete, unprocessed API response for @@ -412,9 +411,9 @@ def opt_in_to_gemini(client) -> bool: def query_gemini( client, query: str, - conversation_id: Optional[str] = None, + conversation_id: str | None = None, context_uri: str = "/search", - context_body: Optional[Dict[str, Any]] = None, + context_body: dict[str, Any] | None = None, attempt_opt_in: bool = True, ) -> GeminiResponse: """Query Chronicle Gemini with a prompt. diff --git a/src/secops/chronicle/ioc.py b/src/secops/chronicle/ioc.py index 388b4cd9..9b629d04 100644 --- a/src/secops/chronicle/ioc.py +++ b/src/secops/chronicle/ioc.py @@ -14,8 +14,9 @@ # """IOC functionality for Chronicle.""" -from typing import Dict, Any from datetime import datetime +from typing import Any + from secops.exceptions import APIError @@ -26,7 +27,7 @@ def list_iocs( max_matches: int = 1000, add_mandiant_attributes: bool = True, prioritized_only: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """List IoCs from Chronicle. Args: diff --git a/src/secops/chronicle/log_ingest.py b/src/secops/chronicle/log_ingest.py index b2c477ff..caa64d84 100644 --- a/src/secops/chronicle/log_ingest.py +++ b/src/secops/chronicle/log_ingest.py @@ -19,8 +19,9 @@ import json import re import uuid +from collections.abc import Callable from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any from secops.chronicle.log_types import is_valid_log_type from secops.exceptions import APIError @@ -42,7 +43,7 @@ MULTI_LINE_LOG_FORMATS = ["WINDOWS", "XML", "JSON"] -def register_log_splitter(log_types: Union[str, List[str]]) -> Callable: +def register_log_splitter(log_types: str | list[str]) -> Callable: """Register a function as a log splitter for specific log types. Args: @@ -121,7 +122,7 @@ def initialize_multi_line_formats() -> None: _LOG_TYPE_ALIASES[variant.upper()] = base_format.upper() -def split_logs(log_type: str, log_content: str) -> List[str]: +def split_logs(log_type: str, log_content: str) -> list[str]: """Split a log content string into individual log entries based on log type. Args: @@ -159,7 +160,7 @@ def split_logs(log_type: str, log_content: str) -> List[str]: "CLOUDFLARE", ] ) -def split_json_logs(log_content: str) -> List[str]: +def split_json_logs(log_content: str) -> list[str]: """Split JSON log content into individual JSON objects. This splitter handles multi-line JSON formats: @@ -221,7 +222,7 @@ def split_json_logs(log_content: str) -> List[str]: "WINDOWS_FIREWALL", ] ) -def split_windows_logs(log_content: str) -> List[str]: +def split_windows_logs(log_content: str) -> list[str]: """Split Windows Event logs. This function handles various Windows log formats including single events @@ -281,7 +282,7 @@ def split_windows_logs(log_content: str) -> List[str]: "VMRAY_FLOG_XML", ] ) -def split_xml_logs(log_content: str) -> List[str]: +def split_xml_logs(log_content: str) -> list[str]: """Split XML format logs. Attempts to identify and separate individual XML documents. @@ -312,14 +313,14 @@ def split_xml_logs(log_content: str) -> List[str]: def create_forwarder( client: "ChronicleClient", display_name: str, - metadata: Optional[Dict[str, Any]] = None, + metadata: dict[str, Any] | None = None, upload_compression: bool = False, enable_server: bool = False, - regex_filters: Optional[List[Dict[str, Any]]] = None, - graceful_timeout: Optional[str] = None, - drain_timeout: Optional[str] = None, - http_settings: Optional[Dict[str, Any]] = None, -) -> Dict[str, Any]: + regex_filters: list[dict[str, Any]] | None = None, + graceful_timeout: str | None = None, + drain_timeout: str | None = None, + http_settings: dict[str, Any] | None = None, +) -> dict[str, Any]: """Create a new forwarder in Chronicle. Args: @@ -385,9 +386,9 @@ def create_forwarder( def list_forwarders( client: "ChronicleClient", - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """List forwarders in Chronicle. Args: @@ -433,7 +434,7 @@ def list_forwarders( def get_forwarder( client: "ChronicleClient", forwarder_id: str -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get a forwarder by ID. Args: @@ -461,16 +462,16 @@ def get_forwarder( def update_forwarder( client: "ChronicleClient", forwarder_id: str, - display_name: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - upload_compression: Optional[bool] = None, - enable_server: Optional[bool] = None, - regex_filters: Optional[List[Dict[str, Any]]] = None, - graceful_timeout: Optional[str] = None, - drain_timeout: Optional[str] = None, - http_settings: Optional[Dict[str, Any]] = None, - update_mask: Optional[List[str]] = None, -) -> Dict[str, Any]: + display_name: str | None = None, + metadata: dict[str, Any] | None = None, + upload_compression: bool | None = None, + enable_server: bool | None = None, + regex_filters: list[dict[str, Any]] | None = None, + graceful_timeout: str | None = None, + drain_timeout: str | None = None, + http_settings: dict[str, Any] | None = None, + update_mask: list[str] | None = None, +) -> dict[str, Any]: """Update an existing forwarder. Args: @@ -589,7 +590,7 @@ def update_forwarder( def delete_forwarder( client: "ChronicleClient", forwarder_id: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Delete a forwarder from Chronicle. Args: @@ -614,7 +615,7 @@ def delete_forwarder( def _find_forwarder_by_display_name( client: "ChronicleClient", display_name: str -) -> Optional[Dict[str, Any]]: +) -> dict[str, Any] | None: """Find an existing forwarder by its display name. This function calls list_forwarders which handles pagination to get @@ -647,8 +648,8 @@ def _find_forwarder_by_display_name( def get_or_create_forwarder( - client: "ChronicleClient", display_name: Optional[str] = None -) -> Dict[str, Any]: + client: "ChronicleClient", display_name: str | None = None +) -> dict[str, Any]: """Get an existing forwarder by name or create a new one if none exists. This function now includes caching for the default forwarder to reduce @@ -778,14 +779,14 @@ def extract_forwarder_id(forwarder_name: str) -> str: def ingest_log( client: "ChronicleClient", log_type: str, - log_message: Union[str, List[str]], - log_entry_time: Optional[datetime] = None, - collection_time: Optional[datetime] = None, - namespace: Optional[str] = None, - labels: Optional[Dict[str, str]] = None, - forwarder_id: Optional[str] = None, + log_message: str | list[str], + log_entry_time: datetime | None = None, + collection_time: datetime | None = None, + namespace: str | None = None, + labels: dict[str, str] | None = None, + forwarder_id: str | None = None, force_log_type: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Ingest one or more logs into Chronicle. Args: @@ -914,9 +915,9 @@ def ingest_log( def ingest_udm( client: "ChronicleClient", - udm_events: Union[Dict[str, Any], List[Dict[str, Any]]], + udm_events: dict[str, Any] | list[dict[str, Any]], add_missing_ids: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Ingest UDM events directly into Chronicle. Args: @@ -1047,9 +1048,9 @@ def ingest_udm( def import_entities( client: "ChronicleClient", - entities: Union[Dict[str, Any], List[Dict[str, Any]]], + entities: dict[str, Any] | list[dict[str, Any]], log_type: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Import entities into Chronicle. Args: diff --git a/src/secops/chronicle/log_types.py b/src/secops/chronicle/log_types.py index 99b34772..197f1b56 100644 --- a/src/secops/chronicle/log_types.py +++ b/src/secops/chronicle/log_types.py @@ -20,21 +20,21 @@ product or vendor. """ -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from secops.chronicle.client import ChronicleClient # Cache for log types to avoid repeated API calls -_LOG_TYPES_CACHE: Optional[List[Dict[str, Any]]] = None +_LOG_TYPES_CACHE: list[dict[str, Any]] | None = None def _fetch_log_types_from_api( client: "ChronicleClient", - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]]: """Fetch log types from Chronicle API with pagination. Args: @@ -49,14 +49,14 @@ def _fetch_log_types_from_api( Exception: If request fails. """ url = f"{client.base_url}/{client.instance_id}/logTypes" - all_log_types: List[Dict[str, Any]] = [] + all_log_types: list[dict[str, Any]] = [] # Determine if we should fetch all pages or just one fetch_all_pages = page_size is None current_page_token = page_token while True: - params: Dict[str, Any] = {} + params: dict[str, Any] = {} # Set page size (use default of 1000 if fetching all pages) params["pageSize"] = page_size if page_size else 1000 @@ -84,9 +84,9 @@ def _fetch_log_types_from_api( def load_log_types( client: "ChronicleClient", - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]]: """Load and cache log types from Chronicle. Args: @@ -119,9 +119,9 @@ def load_log_types( def get_all_log_types( client: "ChronicleClient", - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> List[Dict[str, Any]]: + page_size: int | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]]: """Get all available Chronicle log types. Args: @@ -169,7 +169,7 @@ def is_valid_log_type( def get_log_type_description( log_type_id: str, client: "ChronicleClient", -) -> Optional[str]: +) -> str | None: """Get the description for a log type ID. Args: @@ -195,7 +195,7 @@ def search_log_types( case_sensitive: bool = False, search_in_description: bool = True, client: "ChronicleClient" = None, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Search for log types matching a search term. Args: diff --git a/src/secops/chronicle/models.py b/src/secops/chronicle/models.py index 229fac30..6374c6e2 100644 --- a/src/secops/chronicle/models.py +++ b/src/secops/chronicle/models.py @@ -13,12 +13,12 @@ # limitations under the License. # """Data models for Chronicle API responses.""" -import sys import json +import sys from dataclasses import asdict, dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any from secops.exceptions import SecOpsError @@ -26,6 +26,7 @@ if sys.version_info >= (3, 11): from enum import StrEnum else: + class StrEnum(str, Enum): """String enum implementation for Python versions before 3.11.""" @@ -98,7 +99,7 @@ class DomainInfo: class AssetInfo: """Information about an asset entity.""" - ip: List[str] + ip: list[str] @dataclass @@ -108,7 +109,7 @@ class Entity: name: str metadata: EntityMetadata metric: EntityMetrics - entity: Dict # Can contain domain or asset info + entity: dict # Can contain domain or asset info @dataclass @@ -132,7 +133,7 @@ class TimelineBucket: class Timeline: """Timeline information.""" - buckets: List[TimelineBucket] + buckets: list[TimelineBucket] bucket_size: str @@ -165,16 +166,16 @@ class FilePropertyGroup: """Represents a group of file properties.""" title: str - properties: List[FileProperty] + properties: list[FileProperty] @dataclass class FileMetadataAndProperties: """Represents file metadata and properties.""" - metadata: List[FileProperty] - properties: List[FilePropertyGroup] - query_state: Optional[str] = None + metadata: list[FileProperty] + properties: list[FilePropertyGroup] + query_state: str | None = None @dataclass @@ -183,16 +184,16 @@ class EntitySummary: Complete entity summary response, potentially combining multiple API calls. """ - primary_entity: Optional[Entity] = None - related_entities: List[Entity] = field(default_factory=list) - alert_counts: Optional[List[AlertCount]] = None - timeline: Optional[Timeline] = None - widget_metadata: Optional[WidgetMetadata] = None - prevalence: Optional[List[PrevalenceData]] = None - tpd_prevalence: Optional[List[PrevalenceData]] = None - file_metadata_and_properties: Optional[FileMetadataAndProperties] = None + primary_entity: Entity | None = None + related_entities: list[Entity] = field(default_factory=list) + alert_counts: list[AlertCount] | None = None + timeline: Timeline | None = None + widget_metadata: WidgetMetadata | None = None + prevalence: list[PrevalenceData] | None = None + tpd_prevalence: list[PrevalenceData] | None = None + file_metadata_and_properties: FileMetadataAndProperties | None = None has_more_alerts: bool = False - next_page_token: Optional[str] = None + next_page_token: str | None = None class DataExportStage(str, Enum): @@ -211,8 +212,8 @@ class DataExportStatus: """Status of a data export request.""" stage: DataExportStage - progress_percentage: Optional[int] = None - error: Optional[str] = None + progress_percentage: int | None = None + error: str | None = None @dataclass @@ -224,7 +225,7 @@ class DataExport: end_time: datetime gcs_bucket: str data_export_status: DataExportStatus - log_type: Optional[str] = None + log_type: str | None = None export_all_logs: bool = False @@ -254,8 +255,8 @@ def __init__( stage: str, priority: str, status: str, - soar_platform_info: Optional[SoarPlatformInfo] = None, - alert_ids: Optional[list[str]] = None, + soar_platform_info: SoarPlatformInfo | None = None, + alert_ids: list[str] | None = None, ): self.id = id self.display_name = display_name @@ -290,7 +291,7 @@ def __init__(self, cases: list[Case]): self.cases = cases self._case_map = {case.id: case for case in cases} - def get_case(self, case_id: str) -> Optional[Case]: + def get_case(self, case_id: str) -> Case | None: """Get a case by ID.""" return self._case_map.get(case_id) @@ -329,11 +330,11 @@ class TileType(str, Enum): class InputInterval: """Input interval values to query.""" - time_window: Optional[Dict[str, Any]] = None - relative_time: Optional[Dict[str, Any]] = None + time_window: dict[str, Any] | None = None + relative_time: dict[str, Any] | None = None @classmethod - def from_dict(cls, data: Dict[str, Any]): + def from_dict(cls, data: dict[str, Any]): """Create from a dictionary.""" return cls( time_window=data.get("time_window") or data.get("timeWindow"), @@ -351,7 +352,7 @@ def __post_init__(self): "One of `time_window` or `relative_time` must be set." ) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to a dictionary.""" result = {} if self.time_window: @@ -366,7 +367,7 @@ class DashboardQuery: """Dashboard query Model.""" query: str - input: Union[InputInterval, str] + input: InputInterval | str name: str etag: str @@ -380,7 +381,7 @@ def __post_init__(self): raise SecOpsError(f"Value must be valid JSON string: {e}") from e @classmethod - def from_dict(cls, data: Dict[str, Any]): + def from_dict(cls, data: dict[str, Any]): """Create from a dictionary.""" return cls( query=data.get("query"), @@ -393,13 +394,13 @@ def from_dict(cls, data: Dict[str, Any]): etag=data.get("etag"), ) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to a dictionary.""" return asdict( self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None} ) - def update_fields(self) -> List[str]: + def update_fields(self) -> list[str]: """Return a list of fields that have been modified.""" return [ f"dashboard_query.{field}" @@ -414,12 +415,12 @@ class DashboardChart: name: str etag: str - display_name: Optional[str] = None - description: Optional[str] = None - tile_type: Optional[TileType] = None - visualization: Optional[Union[Dict[str, Any], str]] = None - drill_down_config: Optional[Union[Dict[str, Any], str]] = None - chart_datasource: Optional[Union[Dict[str, Any], str]] = None + display_name: str | None = None + description: str | None = None + tile_type: TileType | None = None + visualization: dict[str, Any] | str | None = None + drill_down_config: dict[str, Any] | str | None = None + chart_datasource: dict[str, Any] | str | None = None def __post_init__(self): """Post init to handle field validation and transformation.""" @@ -436,7 +437,7 @@ def __post_init__(self): raise SecOpsError(f"Value must be valid JSON string: {e}") from e @classmethod - def from_dict(cls, data: Dict[str, Any]): + def from_dict(cls, data: dict[str, Any]): """Create from a dictionary.""" return cls( name=data.get("name"), @@ -453,13 +454,13 @@ def from_dict(cls, data: Dict[str, Any]): ), ) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to a dictionary.""" return asdict( self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None} ) - def update_fields(self) -> List[str]: + def update_fields(self) -> list[str]: """Return a list of fields that have been modified.""" return [ f"dashboard_chart.{field}" diff --git a/src/secops/chronicle/nl_search.py b/src/secops/chronicle/nl_search.py index 89420e24..6f4f5ed9 100644 --- a/src/secops/chronicle/nl_search.py +++ b/src/secops/chronicle/nl_search.py @@ -16,7 +16,8 @@ import time from datetime import datetime -from typing import Dict, Any +from typing import Any + from secops.exceptions import APIError @@ -104,7 +105,7 @@ def nl_search( max_events: int = 10000, case_insensitive: bool = True, max_attempts: int = 30, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Perform a search using natural language that is translated to UDM. Args: diff --git a/src/secops/chronicle/parser.py b/src/secops/chronicle/parser.py index 0401e837..b670ba40 100644 --- a/src/secops/chronicle/parser.py +++ b/src/secops/chronicle/parser.py @@ -15,7 +15,7 @@ """Parser management functionality for Chronicle.""" import base64 -from typing import Any, Dict, List, Optional, Union +from typing import Any from secops.exceptions import APIError @@ -29,7 +29,7 @@ def activate_parser( client: "ChronicleClient", log_type: str, id: str, # pylint: disable=redefined-builtin -) -> Dict[str, Any]: +) -> dict[str, Any]: """Activate a custom parser. Args: @@ -60,7 +60,7 @@ def activate_release_candidate_parser( client: "ChronicleClient", log_type: str, id: str, # pylint: disable=redefined-builtin -) -> Dict[str, Any]: +) -> dict[str, Any]: """Activate the release candidate parser making it live for that customer. Args: @@ -91,7 +91,7 @@ def copy_parser( client: "ChronicleClient", log_type: str, id: str, # pylint: disable=redefined-builtin -) -> Dict[str, Any]: +) -> dict[str, Any]: """Makes a copy of a prebuilt parser. Args: @@ -123,7 +123,7 @@ def create_parser( log_type: str, parser_code: str, validated_on_empty_logs: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Creates a new parser. Args: @@ -157,7 +157,7 @@ def deactivate_parser( client: "ChronicleClient", log_type: str, id: str, # pylint: disable=redefined-builtin -) -> Dict[str, Any]: +) -> dict[str, Any]: """Deactivate a custom parser. Args: @@ -189,7 +189,7 @@ def delete_parser( log_type: str, id: str, # pylint: disable=redefined-builtin force: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Delete a parser. Args: @@ -221,7 +221,7 @@ def get_parser( client: "ChronicleClient", log_type: str, id: str, # pylint: disable=redefined-builtin -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get a Parser by ID. Args: @@ -250,10 +250,10 @@ def get_parser( def list_parsers( client: "ChronicleClient", log_type: str = "-", - page_size: Optional[int] = None, - page_token: Optional[str] = None, + page_size: int | None = None, + page_token: str | None = None, filter: str = None, # pylint: disable=redefined-builtin -) -> Union[List[Any], Dict[str, Any]]: +) -> list[Any] | dict[str, Any]: """List parsers. Args: @@ -316,10 +316,10 @@ def run_parser( client: "ChronicleClient", log_type: str, parser_code: str, - parser_extension_code: Optional[str], - logs: List[str], + parser_extension_code: str | None, + logs: list[str], statedump_allowed: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Run parser against sample logs. Args: diff --git a/src/secops/chronicle/parser_extension.py b/src/secops/chronicle/parser_extension.py index d7eb51c8..80dc0dcb 100644 --- a/src/secops/chronicle/parser_extension.py +++ b/src/secops/chronicle/parser_extension.py @@ -17,7 +17,8 @@ import base64 import json from dataclasses import dataclass, field -from typing import Dict, Any, Optional, Union +from typing import Any + from secops.exceptions import APIError @@ -25,12 +26,12 @@ class ParserExtensionConfig: """Parser extension configuration.""" - log: Optional[str] = None - parser_config: Optional[str] = None - field_extractors: Optional[Union[Dict, str]] = None - dynamic_parsing: Optional[Union[Dict, str]] = None - encoded_log: Optional[str] = field(init=False, default=None) - encoded_cbn_snippet: Optional[str] = field(init=False, default=None) + log: str | None = None + parser_config: str | None = None + field_extractors: dict | str | None = None + dynamic_parsing: dict | str | None = None + encoded_log: str | None = field(init=False, default=None) + encoded_cbn_snippet: str | None = field(init=False, default=None) @staticmethod def encode_base64(text: str) -> str: @@ -100,7 +101,7 @@ def validate(self) -> None: "dynamic_parsing must be specified" ) - def to_dict(self) -> Dict: + def to_dict(self) -> dict: """Convert to dictionary format for API request. Returns: @@ -127,8 +128,8 @@ def to_dict(self) -> Dict: def create_parser_extension( client, log_type: str, - extension_config: Union[ParserExtensionConfig, Dict[str, Any]], -) -> Dict[str, Any]: + extension_config: ParserExtensionConfig | dict[str, Any], +) -> dict[str, Any]: """Create a parser extension. Args: @@ -163,7 +164,7 @@ def create_parser_extension( def get_parser_extension( client, log_type: str, extension_id: str -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get a parser extension. Args: @@ -190,9 +191,9 @@ def get_parser_extension( def list_parser_extensions( client, log_type: str, - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """List parser extensions. Args: diff --git a/src/secops/chronicle/reference_list.py b/src/secops/chronicle/reference_list.py index bf82c621..2aacc176 100644 --- a/src/secops/chronicle/reference_list.py +++ b/src/secops/chronicle/reference_list.py @@ -2,7 +2,7 @@ import sys from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any from secops.chronicle.data_table import ( REF_LIST_DATA_TABLE_ID_REGEX, @@ -25,7 +25,7 @@ def __str__(self) -> str: # Add a local reference to the imported function for backward compatibility # with tests -_validate_cidr_entries = validate_cidr_entries +validate_cidr_entries_local = validate_cidr_entries class ReferenceListSyntaxType(StrEnum): @@ -65,10 +65,10 @@ def create_reference_list( client: "Any", name: str, description: str = "", - entries: List[str] = None, + entries: list[str] = None, syntax_type: ReferenceListSyntaxType = ReferenceListSyntaxType.STRING, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Create a new reference list. Args: @@ -100,7 +100,7 @@ def create_reference_list( # Validate CIDR entries if using CIDR syntax type if syntax_type == ReferenceListSyntaxType.CIDR: - _validate_cidr_entries(entries) + validate_cidr_entries_local(entries) response = client.session.post( f"{client.base_url(api_version, list(APIVersion))}/" @@ -126,8 +126,8 @@ def get_reference_list( client: "Any", name: str, view: ReferenceListView = ReferenceListView.FULL, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Get a single reference list. Args: @@ -165,8 +165,8 @@ def get_reference_list( def list_reference_lists( client: "Any", view: ReferenceListView = ReferenceListView.BASIC, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> List[Dict[str, Any]]: + api_version: APIVersion | None = APIVersion.V1, +) -> list[dict[str, Any]]: """List reference lists. Args: @@ -215,10 +215,10 @@ def list_reference_lists( def update_reference_list( client: "Any", name: str, - description: Optional[str] = None, - entries: Optional[List[str]] = None, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + description: str | None = None, + entries: list[str] | None = None, + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Update a reference list. Args: diff --git a/src/secops/chronicle/rule.py b/src/secops/chronicle/rule.py index 860b9bef..d6bdd6ab 100644 --- a/src/secops/chronicle/rule.py +++ b/src/secops/chronicle/rule.py @@ -14,17 +14,19 @@ # """Rule management functionality for Chronicle.""" -from typing import Dict, Any, Iterator, Optional, List, Literal -from datetime import datetime, timezone import json +import re +from collections.abc import Iterator +from datetime import datetime, timezone +from typing import Any, Literal + from secops.chronicle.models import APIVersion from secops.exceptions import APIError, SecOpsError -import re def create_rule( - client, rule_text: str, api_version: Optional[APIVersion] = APIVersion.V1 -) -> Dict[str, Any]: + client, rule_text: str, api_version: APIVersion | None = APIVersion.V1 +) -> dict[str, Any]: """Creates a new detection rule to find matches in logs. Args: @@ -55,8 +57,8 @@ def create_rule( def get_rule( - client, rule_id: str, api_version: Optional[APIVersion] = APIVersion.V1 -) -> Dict[str, Any]: + client, rule_id: str, api_version: APIVersion | None = APIVersion.V1 +) -> dict[str, Any]: """Get a rule by ID. Args: @@ -86,11 +88,11 @@ def get_rule( def list_rules( client, - view: Optional[str] = "FULL", - page_size: Optional[int] = None, - page_token: Optional[str] = None, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + view: str | None = "FULL", + page_size: int | None = None, + page_token: str | None = None, + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Gets a list of rules. Args: @@ -156,8 +158,8 @@ def update_rule( client, rule_id: str, rule_text: str, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Updates a rule. Args: @@ -194,8 +196,8 @@ def delete_rule( client, rule_id: str, force: bool = False, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Deletes a rule. Args: @@ -227,7 +229,7 @@ def delete_rule( return response.json() -def enable_rule(client, rule_id: str, enabled: bool = True) -> Dict[str, Any]: +def enable_rule(client, rule_id: str, enabled: bool = True) -> dict[str, Any]: """Enables or disables a rule. Args: @@ -246,7 +248,7 @@ def enable_rule(client, rule_id: str, enabled: bool = True) -> Dict[str, Any]: def set_rule_alerting( client, rule_id: str, alerting_enabled: bool = True -) -> Dict[str, Any]: +) -> dict[str, Any]: """Enables or disables alerting for a rule deployment. Args: @@ -264,8 +266,8 @@ def set_rule_alerting( def get_rule_deployment( - client, rule_id: str, api_version: Optional[APIVersion] = APIVersion.V1 -) -> Dict[str, Any]: + client, rule_id: str, api_version: APIVersion | None = APIVersion.V1 +) -> dict[str, Any]: """Gets the current deployment for a rule. Args: @@ -293,11 +295,11 @@ def get_rule_deployment( def list_rule_deployments( client, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - filter_query: Optional[str] = None, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + filter_query: str | None = None, + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Lists rule deployments for the instance. Args: @@ -316,7 +318,7 @@ def list_rule_deployments( APIError: If the API request fails. """ - params: Dict[str, Any] = {} + params: dict[str, Any] = {} if page_size: params["pageSize"] = page_size if page_token: @@ -335,7 +337,7 @@ def list_rule_deployments( raise APIError(f"Failed to list rule deployments: {response.text}") return response.json() - deployments: Dict[str, Any] = {"ruleDeployments": []} + deployments: dict[str, Any] = {"ruleDeployments": []} more = True while more: response = client.session.get(url, params=params) @@ -358,8 +360,8 @@ def list_rule_deployments( def search_rules( - client, query: str, api_version: Optional[APIVersion] = APIVersion.V1 -) -> Dict[str, Any]: + client, query: str, api_version: APIVersion | None = APIVersion.V1 +) -> dict[str, Any]: """Search for rules. Args: @@ -396,7 +398,7 @@ def run_rule_test( end_time: datetime, max_results: int = 100, timeout: int = 300, -) -> Iterator[Dict[str, Any]]: +) -> Iterator[dict[str, Any]]: """Tests a rule against historical data and returns matches. This function connects to the legacy:legacyRunTestRule streaming @@ -508,12 +510,12 @@ def update_rule_deployment( client, rule_id: str, *, - enabled: Optional[bool] = None, - alerting: Optional[bool] = None, - archived: Optional[bool] = None, - run_frequency: Optional[Literal["LIVE", "HOURLY", "DAILY"]] = None, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + enabled: bool | None = None, + alerting: bool | None = None, + archived: bool | None = None, + run_frequency: Literal["LIVE", "HOURLY", "DAILY"] | None = None, + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Update deployment settings for a rule. This wraps the RuleDeployment update behavior and supports partial updates @@ -549,8 +551,8 @@ def update_rule_deployment( f"{client.instance_id}/rules/{rule_id}/deployment" ) - body: Dict[str, Any] = {} - fields: List[str] = [] + body: dict[str, Any] = {} + fields: list[str] = [] if enabled is not None: body["enabled"] = enabled diff --git a/src/secops/chronicle/rule_alert.py b/src/secops/chronicle/rule_alert.py index 17c1b81c..ab65e4e5 100644 --- a/src/secops/chronicle/rule_alert.py +++ b/src/secops/chronicle/rule_alert.py @@ -15,13 +15,14 @@ """Alert functionality for Chronicle rules.""" from datetime import datetime -from typing import Dict, Any, Optional, List, Union, Literal +from typing import Any, Literal + from secops.exceptions import APIError def get_alert( client, alert_id: str, include_detections: bool = False -) -> Dict[str, Any]: +) -> dict[str, Any]: """Gets an alert by ID. Args: @@ -55,18 +56,18 @@ def get_alert( def update_alert( client, alert_id: str, - confidence_score: Optional[int] = None, - reason: Optional[str] = None, - reputation: Optional[str] = None, - priority: Optional[str] = None, - status: Optional[str] = None, - verdict: Optional[str] = None, - risk_score: Optional[int] = None, - disregarded: Optional[bool] = None, - severity: Optional[int] = None, - comment: Optional[Union[str, Literal[""]]] = None, - root_cause: Optional[Union[str, Literal[""]]] = None, -) -> Dict[str, Any]: + confidence_score: int | None = None, + reason: str | None = None, + reputation: str | None = None, + priority: str | None = None, + status: str | None = None, + verdict: str | None = None, + risk_score: int | None = None, + disregarded: bool | None = None, + severity: int | None = None, + comment: str | Literal[""] | None = None, + root_cause: str | Literal[""] | None = None, +) -> dict[str, Any]: """Updates an alert's properties. Args: @@ -199,19 +200,19 @@ def update_alert( def bulk_update_alerts( client, - alert_ids: List[str], - confidence_score: Optional[int] = None, - reason: Optional[str] = None, - reputation: Optional[str] = None, - priority: Optional[str] = None, - status: Optional[str] = None, - verdict: Optional[str] = None, - risk_score: Optional[int] = None, - disregarded: Optional[bool] = None, - severity: Optional[int] = None, - comment: Optional[Union[str, Literal[""]]] = None, - root_cause: Optional[Union[str, Literal[""]]] = None, -) -> List[Dict[str, Any]]: + alert_ids: list[str], + confidence_score: int | None = None, + reason: str | None = None, + reputation: str | None = None, + priority: str | None = None, + status: str | None = None, + verdict: str | None = None, + risk_score: int | None = None, + disregarded: bool | None = None, + severity: int | None = None, + comment: str | Literal[""] | None = None, + root_cause: str | Literal[""] | None = None, +) -> list[dict[str, Any]]: """Updates multiple alerts with the same properties. This is a helper function that iterates through the list of alert IDs @@ -266,9 +267,9 @@ def search_rule_alerts( client, start_time: datetime, end_time: datetime, - rule_status: Optional[str] = None, - page_size: Optional[int] = None, -) -> Dict[str, Any]: + rule_status: str | None = None, + page_size: int | None = None, +) -> dict[str, Any]: """Search for alerts generated by rules. Args: diff --git a/src/secops/chronicle/rule_detection.py b/src/secops/chronicle/rule_detection.py index b06cba05..220fbabf 100644 --- a/src/secops/chronicle/rule_detection.py +++ b/src/secops/chronicle/rule_detection.py @@ -14,23 +14,24 @@ # """Detection functionality for Chronicle rules.""" -from typing import Dict, Any, Optional, Literal from datetime import datetime +from typing import Any, Literal + from secops.exceptions import APIError def list_detections( client, rule_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, + start_time: datetime | None = None, + end_time: datetime | None = None, list_basis: Literal[ "LIST_BASIS_UNSPECIFIED", "CREATED_TIME", "DETECTION_TIME" ] = "LIST_BASIS_UNSPECIFIED", - alert_state: Optional[str] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, -) -> Dict[str, Any]: + alert_state: str | None = None, + page_size: int | None = None, + page_token: str | None = None, +) -> dict[str, Any]: """List detections for a rule. Args: @@ -112,7 +113,7 @@ def list_detections( return response.json() -def list_errors(client, rule_id: str) -> Dict[str, Any]: +def list_errors(client, rule_id: str) -> dict[str, Any]: """List execution errors for a rule. Args: diff --git a/src/secops/chronicle/rule_exclusion.py b/src/secops/chronicle/rule_exclusion.py index 0450ece9..feb6f006 100644 --- a/src/secops/chronicle/rule_exclusion.py +++ b/src/secops/chronicle/rule_exclusion.py @@ -18,7 +18,7 @@ import sys from dataclasses import asdict, dataclass from datetime import datetime -from typing import Annotated, Any, Dict, Optional, Union +from typing import Annotated, Any from secops.exceptions import APIError, SecOpsError @@ -48,10 +48,10 @@ class RuleExclusionType(StrEnum): class UpdateRuleDeployment: """Model for updating rule deployment.""" - enabled: Annotated[Optional[bool], "Optional enabled flag of rule"] = None - archived: Annotated[Optional[bool], "Optional archived flag of rule"] = None + enabled: Annotated[bool | None, "Optional enabled flag of rule"] = None + archived: Annotated[bool | None, "Optional archived flag of rule"] = None detection_exclusion_application: Annotated[ - Optional[Union[str, Dict[str, Any]]], + str | dict[str, Any] | None, "Optional detection exclusion application of rule", ] = None @@ -72,14 +72,14 @@ def __post_init__(self): f"{e}" ) from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON serialization.""" return asdict(self) def list_rule_exclusions( - client, page_size: int = 100, page_token: Optional[str] = None -) -> Dict[str, Any]: + client, page_size: int = 100, page_token: str | None = None +) -> dict[str, Any]: """List rule exclusions. Args: @@ -107,7 +107,7 @@ def list_rule_exclusions( return response.json() -def get_rule_exclusion(client, exclusion_id: str) -> Dict[str, Any]: +def get_rule_exclusion(client, exclusion_id: str) -> dict[str, Any]: """Get a rule exclusion by name. Args: @@ -137,7 +137,7 @@ def get_rule_exclusion(client, exclusion_id: str) -> Dict[str, Any]: def create_rule_exclusion( client, display_name: str, refinement_type: RuleExclusionType, query: str -) -> Dict[str, Any]: +) -> dict[str, Any]: """Creates a new rule exclusion. Args: @@ -174,11 +174,11 @@ def create_rule_exclusion( def patch_rule_exclusion( client, exclusion_id: str, - display_name: Optional[str] = None, - refinement_type: Optional[RuleExclusionType] = None, - query: Optional[str] = None, - update_mask: Optional[str] = None, -) -> Dict[str, Any]: + display_name: str | None = None, + refinement_type: RuleExclusionType | None = None, + query: str | None = None, + update_mask: str | None = None, +) -> dict[str, Any]: """Updates a rule exclusion using provided id. Args: @@ -228,10 +228,10 @@ def patch_rule_exclusion( def compute_rule_exclusion_activity( client, - exclusion_id: Optional[str] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, -) -> Dict[str, Any]: + exclusion_id: str | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, +) -> dict[str, Any]: """Compute activity statistics for rule exclusions. Args: @@ -285,7 +285,7 @@ def compute_rule_exclusion_activity( return response.json() -def get_rule_exclusion_deployment(client, exclusion_id: str) -> Dict[str, Any]: +def get_rule_exclusion_deployment(client, exclusion_id: str) -> dict[str, Any]: """Get deployment information for a rule exclusion. Args: @@ -319,8 +319,8 @@ def update_rule_exclusion_deployment( client, exclusion_id: str, deployment_details: UpdateRuleDeployment, - update_mask: Optional[str] = None, -) -> Dict[str, Any]: + update_mask: str | None = None, +) -> dict[str, Any]: """Update deployment settings for a rule exclusion. Args: diff --git a/src/secops/chronicle/rule_retrohunt.py b/src/secops/chronicle/rule_retrohunt.py index 1c309b5f..9d85a80c 100644 --- a/src/secops/chronicle/rule_retrohunt.py +++ b/src/secops/chronicle/rule_retrohunt.py @@ -15,7 +15,7 @@ """Retrohunt functionality for Chronicle rules.""" from datetime import datetime -from typing import Any, Dict, Optional +from typing import Any from secops.chronicle.models import APIVersion from secops.exceptions import APIError @@ -26,8 +26,8 @@ def create_retrohunt( rule_id: str, start_time: datetime, end_time: datetime, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Creates a retrohunt for a rule. A retrohunt applies a rule to historical data within the specified @@ -70,8 +70,8 @@ def get_retrohunt( client, rule_id: str, operation_id: str, - api_version: Optional[APIVersion] = APIVersion.V1, -) -> Dict[str, Any]: + api_version: APIVersion | None = APIVersion.V1, +) -> dict[str, Any]: """Get retrohunt status and results. Args: diff --git a/src/secops/chronicle/rule_set.py b/src/secops/chronicle/rule_set.py index b5a5837b..8dea188d 100644 --- a/src/secops/chronicle/rule_set.py +++ b/src/secops/chronicle/rule_set.py @@ -14,10 +14,11 @@ # """Curated rule set functionality for Chronicle.""" -from typing import Dict, Any, List, Optional, Union from datetime import datetime -from secops.exceptions import APIError, SecOpsError +from typing import Any + from secops.chronicle.models import AlertState, ListBasis +from secops.exceptions import APIError, SecOpsError def _paginated_request( @@ -25,10 +26,10 @@ def _paginated_request( path: str, items_key: str, *, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - extra_params: Optional[Dict[str, Any]] = None, -) -> Dict[str, Any]: + page_size: int | None = None, + page_token: str | None = None, + extra_params: dict[str, Any] | None = None, +) -> dict[str, Any]: """ Helper to get items from endpoints that use pagination. @@ -82,9 +83,9 @@ def _paginated_request( def list_curated_rule_sets( client, - page_size: Optional[str] = None, - page_token: Optional[str] = None, -) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + page_size: str | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]] | dict[str, Any]: """Get a list of all curated rule sets Args: @@ -113,7 +114,7 @@ def list_curated_rule_sets( return result.get("curatedRuleSets", []) -def get_curated_rule_set(client, rule_set_id: str) -> Dict[str, Any]: +def get_curated_rule_set(client, rule_set_id: str) -> dict[str, Any]: """Get a curated rule set by ID Args: @@ -140,9 +141,9 @@ def get_curated_rule_set(client, rule_set_id: str) -> Dict[str, Any]: def list_curated_rule_set_categories( client, - page_size: Optional[str] = None, - page_token: Optional[str] = None, -) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + page_size: str | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]] | dict[str, Any]: """Get a list of all curated rule set categories Args: @@ -171,7 +172,7 @@ def list_curated_rule_set_categories( return result.get("curatedRuleSetCategories", []) -def get_curated_rule_set_category(client, category_id: str) -> Dict[str, Any]: +def get_curated_rule_set_category(client, category_id: str) -> dict[str, Any]: """Get a curated rule set category by ID Args: @@ -200,9 +201,9 @@ def get_curated_rule_set_category(client, category_id: str) -> Dict[str, Any]: def list_curated_rules( client, - page_size: Optional[str] = None, - page_token: Optional[str] = None, -) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + page_size: str | None = None, + page_token: str | None = None, +) -> list[dict[str, Any]] | dict[str, Any]: """Get a list of all curated rules Args: @@ -231,7 +232,7 @@ def list_curated_rules( return result.get("curatedRules", []) -def get_curated_rule(client, rule_id: str) -> Dict[str, Any]: +def get_curated_rule(client, rule_id: str) -> dict[str, Any]: """Get a curated rule by ID Args: @@ -257,7 +258,7 @@ def get_curated_rule(client, rule_id: str) -> Dict[str, Any]: return response.json() -def get_curated_rule_by_name(client, display_name: str) -> Dict[str, Any]: +def get_curated_rule_by_name(client, display_name: str) -> dict[str, Any]: """Get a curated rule by display name NOTE: This is a linear scan of all curated rules, so it may be inefficient for large rule sets. @@ -285,11 +286,11 @@ def get_curated_rule_by_name(client, display_name: str) -> Dict[str, Any]: def list_curated_rule_set_deployments( client, - page_size: Optional[str] = None, - page_token: Optional[str] = None, - only_enabled: Optional[bool] = False, - only_alerting: Optional[bool] = False, -) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + page_size: str | None = None, + page_token: str | None = None, + only_enabled: bool | None = False, + only_alerting: bool | None = False, +) -> list[dict[str, Any]] | dict[str, Any]: """Get a list of all curated rule set deployment statuses Args: @@ -358,7 +359,7 @@ def get_curated_rule_set_deployment( client, rule_set_id: str, precision: str = "precise", -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get the deployment status of a curated rule set by ID Args: @@ -401,7 +402,7 @@ def get_curated_rule_set_deployment_by_name( client, display_name: str, precision: str = "precise", -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get the deployment status of a curated rule set by its display name NOTE: This is a linear scan of all curated rule sets, so it may be inefficient for large rule sets. @@ -440,8 +441,8 @@ def get_curated_rule_set_deployment_by_name( def update_curated_rule_set_deployment( - client, deployment: Dict[str, Any] -) -> Dict[str, Any]: + client, deployment: dict[str, Any] +) -> dict[str, Any]: """Update a curated rule set deployment to enable or disable alerting or change precision. @@ -504,8 +505,8 @@ def update_curated_rule_set_deployment( def batch_update_curated_rule_set_deployments( - client, deployments: List[Dict[str, Any]] -) -> Dict[str, Any]: + client, deployments: list[dict[str, Any]] +) -> dict[str, Any]: """Batch update curated rule set deployments. Args: @@ -597,15 +598,15 @@ def make_deployment_name(category_id, rule_set_id, precision): def search_curated_detections( client, rule_id: str, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - list_basis: Union[ListBasis, str] = None, - alert_state: Optional[Union[AlertState, str]] = None, - page_size: Optional[int] = None, - page_token: Optional[str] = None, - max_resp_size_bytes: Optional[int] = None, - include_nested_detections: Optional[bool] = False, -) -> Dict[str, Any]: + start_time: datetime | None = None, + end_time: datetime | None = None, + list_basis: ListBasis | str = None, + alert_state: AlertState | str | None = None, + page_size: int | None = None, + page_token: str | None = None, + max_resp_size_bytes: int | None = None, + include_nested_detections: bool | None = False, +) -> dict[str, Any]: """Search for detections generated by a specific curated rule. Args: diff --git a/src/secops/chronicle/rule_validation.py b/src/secops/chronicle/rule_validation.py index 4cdd0864..0ebc75bf 100644 --- a/src/secops/chronicle/rule_validation.py +++ b/src/secops/chronicle/rule_validation.py @@ -14,7 +14,8 @@ # """Rule validation functionality for Chronicle.""" -from typing import Dict, Optional, NamedTuple +from typing import NamedTuple + from secops.exceptions import APIError @@ -29,8 +30,8 @@ class ValidationResult(NamedTuple): """ success: bool - message: Optional[str] = None - position: Optional[Dict[str, int]] = None + message: str | None = None + position: dict[str, int] | None = None def validate_rule(client, rule_text: str) -> ValidationResult: diff --git a/src/secops/chronicle/search.py b/src/secops/chronicle/search.py index 0b7ff4ae..037a6e5a 100644 --- a/src/secops/chronicle/search.py +++ b/src/secops/chronicle/search.py @@ -15,10 +15,12 @@ """UDM search functionality for Chronicle.""" from datetime import datetime -from typing import Dict, Any -from secops.exceptions import APIError +from typing import Any + import requests +from secops.exceptions import APIError + def search_udm( client, @@ -30,7 +32,7 @@ def search_udm( max_attempts: int = 30, timeout: int = 30, debug: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Perform a UDM search query using the Chronicle V1alpha API. Args: diff --git a/src/secops/chronicle/stats.py b/src/secops/chronicle/stats.py index d9d01780..5b5f4e0a 100644 --- a/src/secops/chronicle/stats.py +++ b/src/secops/chronicle/stats.py @@ -14,7 +14,8 @@ # """Statistics functionality for Chronicle searches.""" from datetime import datetime -from typing import Dict, Any +from typing import Any + from secops.exceptions import APIError @@ -28,7 +29,7 @@ def get_stats( max_events: int = 10000, case_insensitive: bool = True, max_attempts: int = 30, -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Get statistics from a Chronicle search query using the Chronicle V1alpha API. @@ -91,7 +92,7 @@ def get_stats( return process_stats_results(results["stats"]) -def process_stats_results(stats: Dict[str, Any]) -> Dict[str, Any]: +def process_stats_results(stats: dict[str, Any]) -> dict[str, Any]: """Process stats search results. Args: diff --git a/src/secops/chronicle/udm_mapping.py b/src/secops/chronicle/udm_mapping.py index 40a1ee85..cea5eeba 100644 --- a/src/secops/chronicle/udm_mapping.py +++ b/src/secops/chronicle/udm_mapping.py @@ -16,7 +16,7 @@ import base64 import sys -from typing import Any, Dict, Optional +from typing import Any from secops.exceptions import APIError @@ -46,9 +46,9 @@ def generate_udm_key_value_mappings( client, log_format: RowLogFormat, log: str, - use_array_bracket_notation: Optional[bool] = None, - compress_array_fields: Optional[bool] = None, -) -> Dict[str, Any]: + use_array_bracket_notation: bool | None = None, + compress_array_fields: bool | None = None, +) -> dict[str, Any]: """Generate key-value mappings for a UDM field using Chronicle V1alpha API. This function retrieves all unique values for the specified UDM field diff --git a/src/secops/chronicle/udm_search.py b/src/secops/chronicle/udm_search.py index 5a997f3b..0a7e7012 100644 --- a/src/secops/chronicle/udm_search.py +++ b/src/secops/chronicle/udm_search.py @@ -15,7 +15,7 @@ """UDM search functionality for Chronicle.""" from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any from secops.exceptions import APIError, SecOpsError @@ -81,8 +81,8 @@ def fetch_udm_search_csv( def find_udm_field_values( - client, query: str, page_size: Optional[int] = None -) -> Dict[str, Any]: + client, query: str, page_size: int | None = None +) -> dict[str, Any]: """Fetch UDM field values that match a query. Args: @@ -121,11 +121,11 @@ def fetch_udm_search_view( query: str, start_time: datetime, end_time: datetime, - snapshot_query: Optional[str] = 'feedback_summary.status != "CLOSED"', - max_events: Optional[int] = 10000, - max_detections: Optional[int] = 1000, + snapshot_query: str | None = 'feedback_summary.status != "CLOSED"', + max_events: int | None = 10000, + max_detections: int | None = 1000, case_insensitive: bool = True, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Fetch UDM search result view. Args: @@ -193,7 +193,7 @@ def fetch_udm_search_view( except ValueError as e: raise APIError(f"Failed to parse UDM search response: {str(e)}") from e - final_resp: List[Dict[str, Any]] = [] + final_resp: list[dict[str, Any]] = [] complete: bool = False for resp in json_resp: if not resp.get("complete", "") and not resp.get("error", ""): diff --git a/src/secops/chronicle/validate.py b/src/secops/chronicle/validate.py index 7d225d29..64ec8287 100644 --- a/src/secops/chronicle/validate.py +++ b/src/secops/chronicle/validate.py @@ -14,11 +14,12 @@ # """Query validation functionality for Chronicle.""" -from typing import Dict, Any +from typing import Any + from secops.exceptions import APIError -def validate_query(client, query: str) -> Dict[str, Any]: +def validate_query(client, query: str) -> dict[str, Any]: """Validate a UDM query against the Chronicle API. Args: diff --git a/src/secops/cli/cli_client.py b/src/secops/cli/cli_client.py index ddbfdd61..499949ec 100644 --- a/src/secops/cli/cli_client.py +++ b/src/secops/cli/cli_client.py @@ -4,37 +4,33 @@ import argparse import sys -from typing import Tuple, Dict from secops import SecOpsClient from secops.chronicle import ChronicleClient -from secops.cli.commands.config import setup_config_command -from secops.cli.commands.search import setup_search_command -from secops.cli.commands.udm_search import setup_udm_search_view_command -from secops.cli.commands.stats import setup_stats_command -from secops.cli.commands.entity import setup_entity_command -from secops.cli.commands.iocs import setup_iocs_command -from secops.cli.commands.log import setup_log_command -from secops.cli.commands.parser import setup_parser_command -from secops.cli.commands.feed import setup_feed_command -from secops.cli.commands.rule import setup_rule_command from secops.cli.commands.alert import setup_alert_command from secops.cli.commands.case import setup_case_command +from secops.cli.commands.config import setup_config_command +from secops.cli.commands.curated_rule import setup_curated_rules_command +from secops.cli.commands.dashboard import setup_dashboard_command +from secops.cli.commands.dashboard_query import setup_dashboard_query_command +from secops.cli.commands.data_table import setup_data_table_command +from secops.cli.commands.entity import setup_entity_command from secops.cli.commands.export import setup_export_command +from secops.cli.commands.feed import setup_feed_command +from secops.cli.commands.forwarder import setup_forwarder_command from secops.cli.commands.gemini import setup_gemini_command from secops.cli.commands.help import setup_help_command -from secops.cli.commands.data_table import setup_data_table_command +from secops.cli.commands.iocs import setup_iocs_command +from secops.cli.commands.log import setup_log_command +from secops.cli.commands.parser import setup_parser_command +from secops.cli.commands.parser_extension import setup_parser_extension_command from secops.cli.commands.reference_list import setup_reference_list_command +from secops.cli.commands.rule import setup_rule_command from secops.cli.commands.rule_exclusion import setup_rule_exclusion_command -from secops.cli.commands.parser_extension import setup_parser_extension_command -from secops.cli.commands.dashboard import setup_dashboard_command -from secops.cli.commands.dashboard_query import setup_dashboard_query_command -from secops.cli.commands.forwarder import setup_forwarder_command -from secops.cli.commands.curated_rule import setup_curated_rules_command -from secops.cli.utils.common_args import ( - add_common_args, - add_chronicle_args, -) +from secops.cli.commands.search import setup_search_command +from secops.cli.commands.stats import setup_stats_command +from secops.cli.commands.udm_search import setup_udm_search_view_command +from secops.cli.utils.common_args import add_chronicle_args, add_common_args from secops.cli.utils.config_utils import load_config from secops.exceptions import AuthenticationError, SecOpsError @@ -86,8 +82,8 @@ def setup_client( def _setup_client_core( args: argparse.Namespace, client: SecOpsClient, - config: Dict[str, str], -) -> Tuple[SecOpsClient, ChronicleClient]: + config: dict[str, str], +) -> tuple[SecOpsClient, ChronicleClient]: """Set up and return SecOpsClient and Chronicle client based on args or config file. Args take precedence over config file. diff --git a/src/secops/cli/commands/alert.py b/src/secops/cli/commands/alert.py index 639b59c1..b3cb0251 100644 --- a/src/secops/cli/commands/alert.py +++ b/src/secops/cli/commands/alert.py @@ -17,8 +17,8 @@ import sys from secops.cli.utils.common_args import add_time_range_args -from secops.cli.utils.time_utils import get_time_range from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_alert_command(subparsers): diff --git a/src/secops/cli/commands/config.py b/src/secops/cli/commands/config.py index c1ff51d6..4ac10859 100644 --- a/src/secops/cli/commands/config.py +++ b/src/secops/cli/commands/config.py @@ -15,12 +15,12 @@ """Google SecOps CLI config commands""" from secops.cli.constants import CONFIG_FILE -from secops.cli.utils.config_utils import load_config, save_config from secops.cli.utils.common_args import ( add_chronicle_args, add_common_args, add_time_range_args, ) +from secops.cli.utils.config_utils import load_config, save_config def setup_config_command(subparsers): diff --git a/src/secops/cli/commands/dashboard.py b/src/secops/cli/commands/dashboard.py index ea57f067..db34d0c6 100644 --- a/src/secops/cli/commands/dashboard.py +++ b/src/secops/cli/commands/dashboard.py @@ -17,9 +17,9 @@ import json import sys +from secops.cli.utils.common_args import add_pagination_args from secops.cli.utils.formatters import output_formatter from secops.exceptions import APIError, SecOpsError -from secops.cli.utils.common_args import add_pagination_args def setup_dashboard_command(subparsers): @@ -405,11 +405,11 @@ def handle_dashboard_create_command(args, chronicle): filters = args.filters if args.filters else None charts = args.charts if args.charts else None if args.filters_file: - with open(args.filters_file, "r", encoding="utf-8") as f: + with open(args.filters_file, encoding="utf-8") as f: filters = f.read() if args.charts_file: - with open(args.charts_file, "r", encoding="utf-8") as f: + with open(args.charts_file, encoding="utf-8") as f: charts = f.read() result = chronicle.create_dashboard( @@ -435,17 +435,17 @@ def handle_dashboard_update_command(args, chronicle): charts = args.charts if args.charts else None if args.filters_file: try: - with open(args.filters_file, "r", encoding="utf-8") as f: + with open(args.filters_file, encoding="utf-8") as f: filters = f.read() - except IOError as e: + except OSError as e: print(f"Error reading filters file: {e}", file=sys.stderr) sys.exit(1) if args.charts_file: try: - with open(args.charts_file, "r", encoding="utf-8") as f: + with open(args.charts_file, encoding="utf-8") as f: charts = f.read() - except IOError as e: + except OSError as e: print(f"Error reading charts file: {e}", file=sys.stderr) sys.exit(1) @@ -503,26 +503,26 @@ def handle_dashboard_add_chart_command(args, chronicle): query = args.query if args.query else None if args.query_file: try: - with open(args.query_file, "r", encoding="utf-8") as f: + with open(args.query_file, encoding="utf-8") as f: query = f.read() - except IOError as e: + except OSError as e: print(f"Error reading query file: {e}", file=sys.stderr) sys.exit(1) chart_layout = args.chart_layout if args.chart_layout else None if args.chart_layout_file: try: - with open(args.chart_layout_file, "r", encoding="utf-8") as f: + with open(args.chart_layout_file, encoding="utf-8") as f: chart_layout = f.read() - except IOError as e: + except OSError as e: print(f"Error reading chart layout file: {e}", file=sys.stderr) sys.exit(1) visualization = args.visualization if args.visualization else None if args.visualization_file: try: - with open(args.visualization_file, "r", encoding="utf-8") as f: + with open(args.visualization_file, encoding="utf-8") as f: visualization = f.read() - except IOError as e: + except OSError as e: print(f"Error reading visualization file: {e}", file=sys.stderr) sys.exit(1) @@ -531,11 +531,9 @@ def handle_dashboard_add_chart_command(args, chronicle): ) if args.drill_down_config_file: try: - with open( - args.drill_down_config_file, "r", encoding="utf-8" - ) as f: + with open(args.drill_down_config_file, encoding="utf-8") as f: drill_down_config = f.read() - except IOError as e: + except OSError as e: print( f"Error reading drill down config file: {e}", file=sys.stderr, @@ -547,11 +545,9 @@ def handle_dashboard_add_chart_command(args, chronicle): ) if args.chart_datasource_file: try: - with open( - args.chart_datasource_file, "r", encoding="utf-8" - ) as f: + with open(args.chart_datasource_file, encoding="utf-8") as f: chart_datasource = f.read() - except IOError as e: + except OSError as e: print( f"Error reading chart datasource file: {e}", file=sys.stderr, @@ -616,10 +612,10 @@ def handle_dashboard_edit_chart_command(args, chronicle): if args.dashboard_query_from_file: try: with open( - args.dashboard_query_from_file, "r", encoding="utf-8" + args.dashboard_query_from_file, encoding="utf-8" ) as f: dashboard_query = f.read() - except IOError as e: + except OSError as e: print( f"Error reading dashboard query file: {e}", file=sys.stderr ) @@ -628,10 +624,10 @@ def handle_dashboard_edit_chart_command(args, chronicle): if args.dashboard_chart_from_file: try: with open( - args.dashboard_chart_from_file, "r", encoding="utf-8" + args.dashboard_chart_from_file, encoding="utf-8" ) as f: dashboard_chart = f.read() - except IOError as e: + except OSError as e: print( f"Error reading dashboard chart file: {e}", file=sys.stderr ) @@ -664,9 +660,9 @@ def handle_dashboard_import_command(args, chronicle): dashboard_data = args.dashboard_data elif args.dashboard_data_file: try: - with open(args.dashboard_data_file, "r", encoding="utf-8") as f: + with open(args.dashboard_data_file, encoding="utf-8") as f: dashboard_data = f.read() - except IOError as e: + except OSError as e: print( f"Error reading dashboard data file: {e}", file=sys.stderr ) @@ -677,9 +673,9 @@ def handle_dashboard_import_command(args, chronicle): chart_data = args.chart_data elif args.chart_data_file: try: - with open(args.chart_data_file, "r", encoding="utf-8") as f: + with open(args.chart_data_file, encoding="utf-8") as f: chart_data = f.read() - except IOError as e: + except OSError as e: print(f"Error reading chart data file: {e}", file=sys.stderr) sys.exit(1) @@ -688,9 +684,9 @@ def handle_dashboard_import_command(args, chronicle): query_data = args.query_data elif args.query_data_file: try: - with open(args.query_data_file, "r", encoding="utf-8") as f: + with open(args.query_data_file, encoding="utf-8") as f: query_data = f.read() - except IOError as e: + except OSError as e: print(f"Error reading query data file: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/secops/cli/commands/dashboard_query.py b/src/secops/cli/commands/dashboard_query.py index 21ab83bb..7078082e 100644 --- a/src/secops/cli/commands/dashboard_query.py +++ b/src/secops/cli/commands/dashboard_query.py @@ -87,9 +87,9 @@ def handle_dashboard_query_execute_command(args, chronicle): query = args.query if args.query else None if args.query_file: try: - with open(args.query_file, "r", encoding="utf-8") as f: + with open(args.query_file, encoding="utf-8") as f: query = f.read() - except IOError as e: + except OSError as e: print(f"Error reading query file: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/secops/cli/commands/data_table.py b/src/secops/cli/commands/data_table.py index e8530728..3ef8b123 100644 --- a/src/secops/cli/commands/data_table.py +++ b/src/secops/cli/commands/data_table.py @@ -368,9 +368,9 @@ def handle_dt_replace_rows_command(args, chronicle): sys.exit(1) elif args.rows_file: try: - with open(args.rows_file, "r", encoding="utf-8") as f: + with open(args.rows_file, encoding="utf-8") as f: rows = json.load(f) - except (json.JSONDecodeError, IOError) as e: + except (json.JSONDecodeError, OSError) as e: print(f"Error reading from file: {e}", file=sys.stderr) sys.exit(1) else: @@ -445,9 +445,9 @@ def handle_dt_update_rows_command(args, chronicle): sys.exit(1) elif args.rows_file: try: - with open(args.rows_file, "r", encoding="utf-8") as f: + with open(args.rows_file, encoding="utf-8") as f: row_updates = json.load(f) - except (json.JSONDecodeError, IOError) as e: + except (json.JSONDecodeError, OSError) as e: print(f"Error reading from file: {e}", file=sys.stderr) sys.exit(1) else: diff --git a/src/secops/cli/commands/entity.py b/src/secops/cli/commands/entity.py index 75b67ce9..634d6157 100644 --- a/src/secops/cli/commands/entity.py +++ b/src/secops/cli/commands/entity.py @@ -147,7 +147,7 @@ def handle_entity_command(args, chronicle): def handle_import_entities_command(args, chronicle): """Handle import entities command.""" try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: entities = json.load(f) result = chronicle.import_entities( diff --git a/src/secops/cli/commands/export.py b/src/secops/cli/commands/export.py index 90f1aa4e..db43ace3 100644 --- a/src/secops/cli/commands/export.py +++ b/src/secops/cli/commands/export.py @@ -16,12 +16,12 @@ import sys -from secops.cli.utils.time_utils import get_time_range -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import ( - add_time_range_args, add_pagination_args, + add_time_range_args, ) +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_export_command(subparsers): diff --git a/src/secops/cli/commands/forwarder.py b/src/secops/cli/commands/forwarder.py index 67059026..0836fc4a 100644 --- a/src/secops/cli/commands/forwarder.py +++ b/src/secops/cli/commands/forwarder.py @@ -17,8 +17,8 @@ import json import sys -from secops.exceptions import APIError from secops.cli.utils.common_args import add_pagination_args +from secops.exceptions import APIError def setup_forwarder_command(subparsers): diff --git a/src/secops/cli/commands/log.py b/src/secops/cli/commands/log.py index f8c4f42e..8bcd693d 100644 --- a/src/secops/cli/commands/log.py +++ b/src/secops/cli/commands/log.py @@ -14,8 +14,8 @@ # """Google SecOps CLI log commands""" -import sys import json +import sys from secops.cli.utils.formatters import output_formatter @@ -115,7 +115,7 @@ def handle_log_ingest_command(args, chronicle): try: log_message = args.message if args.file: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: log_message = f.read() # Process labels if provided @@ -161,7 +161,7 @@ def handle_log_ingest_command(args, chronicle): def handle_udm_ingest_command(args, chronicle): """Handle UDM ingestion command.""" try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: udm_events = json.load(f) result = chronicle.ingest_udm(udm_events=udm_events) @@ -203,7 +203,7 @@ def handle_generate_udm_mapping_command(args, chronicle): try: log = "" if args.log_file: - with open(args.log_file, "r", encoding="utf-8") as f: + with open(args.log_file, encoding="utf-8") as f: log = f.read() elif args.log: log = args.log diff --git a/src/secops/cli/commands/parser.py b/src/secops/cli/commands/parser.py index 0ca058c8..1e8ffd82 100644 --- a/src/secops/cli/commands/parser.py +++ b/src/secops/cli/commands/parser.py @@ -6,8 +6,8 @@ import base64 import sys -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import add_pagination_args +from secops.cli.utils.formatters import output_formatter from secops.exceptions import APIError, SecOpsError @@ -265,9 +265,9 @@ def handle_parser_create_command(args, chronicle): parser_code = "" if args.parser_code_file: try: - with open(args.parser_code_file, "r", encoding="utf-8") as f: + with open(args.parser_code_file, encoding="utf-8") as f: parser_code = f.read() - except IOError as e: + except OSError as e: print(f"Error reading parser code file: {e}", file=sys.stderr) sys.exit(1) elif args.parser_code: @@ -335,9 +335,9 @@ def handle_parser_run_command(args, chronicle): parser_code = "" if args.parser_code_file: try: - with open(args.parser_code_file, "r", encoding="utf-8") as f: + with open(args.parser_code_file, encoding="utf-8") as f: parser_code = f.read() - except IOError as e: + except OSError as e: print(f"Error reading parser code file: {e}", file=sys.stderr) sys.exit(1) elif args.parser_code: @@ -364,10 +364,10 @@ def handle_parser_run_command(args, chronicle): if args.parser_extension_code_file: try: with open( - args.parser_extension_code_file, "r", encoding="utf-8" + args.parser_extension_code_file, encoding="utf-8" ) as f: parser_extension_code = f.read() - except IOError as e: + except OSError as e: print( f"Error reading parser extension code file: {e}", file=sys.stderr, @@ -380,9 +380,9 @@ def handle_parser_run_command(args, chronicle): logs = [] if args.logs_file: try: - with open(args.logs_file, "r", encoding="utf-8") as f: + with open(args.logs_file, encoding="utf-8") as f: logs = [line.strip() for line in f if line.strip()] - except IOError as e: + except OSError as e: print(f"Error reading logs file: {e}", file=sys.stderr) sys.exit(1) elif args.log: diff --git a/src/secops/cli/commands/parser_extension.py b/src/secops/cli/commands/parser_extension.py index efe9a6e2..4e542dff 100644 --- a/src/secops/cli/commands/parser_extension.py +++ b/src/secops/cli/commands/parser_extension.py @@ -17,9 +17,9 @@ import sys from typing import Any -from secops.exceptions import APIError -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import add_pagination_args +from secops.cli.utils.formatters import output_formatter +from secops.exceptions import APIError def setup_parser_extension_command(subparsers: Any) -> None: @@ -163,9 +163,9 @@ def handle_parser_extension_create_command(args, chronicle): log = args.log elif args.log_file: try: - with open(args.log_file, "r", encoding="utf-8") as f: + with open(args.log_file, encoding="utf-8") as f: log = f.read().strip() - except IOError as e: + except OSError as e: print(f"Error reading log file: {e}", file=sys.stderr) sys.exit(1) @@ -175,9 +175,9 @@ def handle_parser_extension_create_command(args, chronicle): parser_config = args.parser_config elif args.parser_config_file: try: - with open(args.parser_config_file, "r", encoding="utf-8") as f: + with open(args.parser_config_file, encoding="utf-8") as f: parser_config = f.read().strip() - except IOError as e: + except OSError as e: print(f"Error reading CBN snippet file: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/secops/cli/commands/reference_list.py b/src/secops/cli/commands/reference_list.py index aac68267..74b8d042 100644 --- a/src/secops/cli/commands/reference_list.py +++ b/src/secops/cli/commands/reference_list.py @@ -131,9 +131,9 @@ def handle_rl_create_command(args, chronicle): entries = [] if args.entries_file: try: - with open(args.entries_file, "r", encoding="utf-8") as f: + with open(args.entries_file, encoding="utf-8") as f: entries = [line.strip() for line in f if line.strip()] - except IOError as e: + except OSError as e: print(f"Error reading entries file: {e}", file=sys.stderr) sys.exit(1) elif args.entries: @@ -160,9 +160,9 @@ def handle_rl_update_command(args, chronicle): entries = None if args.entries_file: try: - with open(args.entries_file, "r", encoding="utf-8") as f: + with open(args.entries_file, encoding="utf-8") as f: entries = [line.strip() for line in f if line.strip()] - except IOError as e: + except OSError as e: print(f"Error reading entries file: {e}", file=sys.stderr) sys.exit(1) elif args.entries: diff --git a/src/secops/cli/commands/rule.py b/src/secops/cli/commands/rule.py index c36a1d68..ab460b7d 100644 --- a/src/secops/cli/commands/rule.py +++ b/src/secops/cli/commands/rule.py @@ -17,13 +17,13 @@ import json import sys -from secops.exceptions import APIError -from secops.cli.utils.time_utils import get_time_range -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import ( - add_time_range_args, add_pagination_args, + add_time_range_args, ) +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range +from secops.exceptions import APIError def setup_rule_command(subparsers): @@ -252,7 +252,7 @@ def handle_rule_get_command(args, chronicle): def handle_rule_create_command(args, chronicle): """Handle rule create command.""" try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: rule_text = f.read() result = chronicle.create_rule(rule_text) @@ -265,7 +265,7 @@ def handle_rule_create_command(args, chronicle): def handle_rule_update_command(args, chronicle): """Handle rule update command.""" try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: rule_text = f.read() result = chronicle.update_rule(args.id, rule_text) @@ -299,7 +299,7 @@ def handle_rule_delete_command(args, chronicle): def handle_rule_validate_command(args, chronicle): """Handle rule validate command.""" try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: rule_text = f.read() result = chronicle.validate_rule(rule_text) @@ -324,7 +324,7 @@ def handle_rule_test_command(args, chronicle): as JSON objects. """ try: - with open(args.file, "r", encoding="utf-8") as f: + with open(args.file, encoding="utf-8") as f: rule_text = f.read() start_time, end_time = get_time_range(args) diff --git a/src/secops/cli/commands/rule_exclusion.py b/src/secops/cli/commands/rule_exclusion.py index f4791dcc..aeb2b07c 100644 --- a/src/secops/cli/commands/rule_exclusion.py +++ b/src/secops/cli/commands/rule_exclusion.py @@ -16,12 +16,12 @@ import sys -from secops.cli.utils.time_utils import get_time_range -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import ( - add_time_range_args, add_pagination_args, + add_time_range_args, ) +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_rule_exclusion_command(subparsers): diff --git a/src/secops/cli/commands/search.py b/src/secops/cli/commands/search.py index 6b754534..acc15b53 100644 --- a/src/secops/cli/commands/search.py +++ b/src/secops/cli/commands/search.py @@ -16,12 +16,12 @@ import sys -from secops.cli.utils.time_utils import get_time_range -from secops.cli.utils.formatters import output_formatter from secops.cli.utils.common_args import ( - add_time_range_args, add_pagination_args, + add_time_range_args, ) +from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_search_command(subparsers): diff --git a/src/secops/cli/commands/stats.py b/src/secops/cli/commands/stats.py index fb0f9c8d..7a58e73a 100644 --- a/src/secops/cli/commands/stats.py +++ b/src/secops/cli/commands/stats.py @@ -17,8 +17,8 @@ import sys from secops.cli.utils.common_args import add_time_range_args -from secops.cli.utils.time_utils import get_time_range from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_stats_command(subparsers): diff --git a/src/secops/cli/commands/udm_search.py b/src/secops/cli/commands/udm_search.py index 2420ec0e..6255caf5 100644 --- a/src/secops/cli/commands/udm_search.py +++ b/src/secops/cli/commands/udm_search.py @@ -17,8 +17,8 @@ import sys from secops.cli.utils.common_args import add_time_range_args -from secops.cli.utils.time_utils import get_time_range from secops.cli.utils.formatters import output_formatter +from secops.cli.utils.time_utils import get_time_range def setup_udm_search_view_command(subparsers): @@ -99,9 +99,9 @@ def handle_udm_search_view_command(args, chronicle): query = args.query if args.query_file: try: - with open(args.query_file, "r", encoding="utf-8") as f: + with open(args.query_file, encoding="utf-8") as f: query = f.read() - except IOError as e: + except OSError as e: print(f"Error reading query file: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/secops/cli/utils/config_utils.py b/src/secops/cli/utils/config_utils.py index cfd405a9..91bd4bd3 100644 --- a/src/secops/cli/utils/config_utils.py +++ b/src/secops/cli/utils/config_utils.py @@ -16,12 +16,12 @@ import json import sys -from typing import Any, Dict +from typing import Any from secops.cli.constants import CONFIG_DIR, CONFIG_FILE -def load_config() -> Dict[str, Any]: +def load_config() -> dict[str, Any]: """Load configuration from config file. Returns: @@ -31,9 +31,9 @@ def load_config() -> Dict[str, Any]: return {} try: - with open(CONFIG_FILE, "r", encoding="utf-8") as f: + with open(CONFIG_FILE, encoding="utf-8") as f: return json.load(f) - except (json.JSONDecodeError, IOError): + except (json.JSONDecodeError, OSError): print( f"Warning: Failed to load config from {CONFIG_FILE}", file=sys.stderr, @@ -41,7 +41,7 @@ def load_config() -> Dict[str, Any]: return {} -def save_config(config: Dict[str, Any]) -> None: +def save_config(config: dict[str, Any]) -> None: """Save configuration to config file. Args: @@ -53,7 +53,7 @@ def save_config(config: Dict[str, Any]) -> None: try: with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=2) - except IOError as e: + except OSError as e: print( f"Error: Failed to save config to {CONFIG_FILE}: {e}", file=sys.stderr, diff --git a/src/secops/cli/utils/time_utils.py b/src/secops/cli/utils/time_utils.py index 139839ad..828e9e32 100644 --- a/src/secops/cli/utils/time_utils.py +++ b/src/secops/cli/utils/time_utils.py @@ -15,7 +15,6 @@ """Google SecOps CLI datetime utils""" import argparse -from typing import Tuple from datetime import datetime, timedelta, timezone @@ -33,7 +32,7 @@ def parse_datetime(dt_str: str) -> datetime: return datetime.fromisoformat(dt_str.replace("Z", "+00:00")) -def get_time_range(args: argparse.Namespace) -> Tuple[datetime, datetime]: +def get_time_range(args: argparse.Namespace) -> tuple[datetime, datetime]: """Get start and end time from arguments. Args: diff --git a/src/secops/client.py b/src/secops/client.py index bf35f5ec..48fcedd5 100644 --- a/src/secops/client.py +++ b/src/secops/client.py @@ -14,7 +14,7 @@ """Main client for Google SecOps SDK.""" -from typing import Any, Dict, Optional, Union +from typing import Any from google.auth.credentials import Credentials @@ -27,11 +27,11 @@ class SecOpsClient: def __init__( self, - credentials: Optional[Credentials] = None, - service_account_path: Optional[str] = None, - service_account_info: Optional[Dict[str, Any]] = None, - impersonate_service_account: Optional[str] = None, - retry_config: Optional[Union[RetryConfig, Dict[str, Any], bool]] = None, + credentials: Credentials | None = None, + service_account_path: str | None = None, + service_account_info: dict[str, Any] | None = None, + impersonate_service_account: str | None = None, + retry_config: RetryConfig | dict[str, Any] | bool | None = None, ): """Initialize the SecOps client. @@ -57,7 +57,7 @@ def chronicle( customer_id: str, project_id: str, region: str = "us", - default_api_version: Union[str, Any] = "v1alpha", + default_api_version: str | Any = "v1alpha", ) -> ChronicleClient: """Get Chronicle API client. diff --git a/tests/chronicle/test_data_tables.py b/tests/chronicle/test_data_tables.py index 77928f19..12770654 100644 --- a/tests/chronicle/test_data_tables.py +++ b/tests/chronicle/test_data_tables.py @@ -434,7 +434,7 @@ def test_create_reference_list_success( ) @patch("secops.chronicle.reference_list.REF_LIST_DATA_TABLE_ID_REGEX") - @patch("secops.chronicle.reference_list._validate_cidr_entries") + @patch("secops.chronicle.reference_list.validate_cidr_entries_local") def test_create_reference_list_cidr_success( self, mock_validate_cidr: Mock, @@ -1286,9 +1286,9 @@ def test_update_data_table_rows_single_oversized_row( # TODO: Add more unit tests for: # - APIError scenarios for each function (e.g., 404 Not Found, 500 Server Error) # - Pagination in list_data_tables and list_data_table_rows, list_reference_lists - # - create_data_table with CIDR validation failure (_validate_cidr_entries raises SecOpsError) + # - create_data_table with CIDR validation failure (validate_cidr_entries_local raises SecOpsError) # - create_reference_list with CIDR validation failure - # - _validate_cidr_entries itself + # - validate_cidr_entries_local itself # - REF_LIST_DATA_TABLE_ID_REGEX utility if used directly by other parts (though it's tested via create methods) # - Edge cases for row chunking in create_data_table_rows (e.g. single massive row) # - delete_data_table_row specific tests (if _delete_data_table_row is complex enough) diff --git a/tox.ini b/tox.ini index bf9e7772..917a4cbd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312 +envlist = py310, py311, py312, py313 isolated_build = True [testenv]