Skip to content

Commit bf604e1

Browse files
threat-puntercopybara-github
authored andcommitted
Add support to manage saved searches using Content Manager
PiperOrigin-RevId: 830583625
1 parent 03b46a1 commit bf604e1

20 files changed

+1943
-3
lines changed

tools/content_manager/README.md

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
Content Manager is a command-line tool that can be used to manage content in
66
[Google SecOps](https://cloud.google.com/security/products/security-operations)
7-
such as rules, data, tables, reference lists, and rule exclusions. Content
8-
Manager can be utilized in a CI/CD pipeline to implement Detection-as-Code with
9-
Google SecOps or ran locally using
7+
such as rules, data, tables, reference lists, rule exclusions, and saved
8+
searches. Content Manager can be utilized in a CI/CD pipeline to implement
9+
Detection-as-Code with Google SecOps or ran locally using
1010
[Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials) for authentication.
1111

1212
If you're new to the concept of managing detection rules and other content using
@@ -31,6 +31,7 @@ in a CI/CD pipeline (in GitHub, GitLab, CircleCI, etc) to do the following:
3131
* Retrieve the latest version of all reference lists from Google SecOps and write them to local files along with their current state/configuration
3232
* Create or update reference lists in Google SecOps based on local files
3333
* Manage [rule exclusions](https://cloud.google.com/chronicle/docs/detection/rule-exclusions) in Google SecOps based on a local config file
34+
* Manage [saved searches](https://docs.cloud.google.com/chronicle/docs/investigation/udm-search#search-manager) in Google SecOps based on a local config file
3435

3536
Sample detection rules can be found in the [Google SecOps Detection Rules](https://github.com/chronicle/detection-rules/tree/main) repo.
3637

@@ -136,6 +137,11 @@ chronicle.findingsRefinements.create
136137
chronicle.findingsRefinements.get
137138
chronicle.findingsRefinements.list
138139
chronicle.findingsRefinements.update
140+
# Permissions required to managed saved searches
141+
chronicle.searchQueries.get
142+
chronicle.searchQueries.list
143+
chronicle.searchQueries.create
144+
chronicle.searchQueries.update
139145
```
140146

141147
If you're unable to configure your CI/CD pipeline to authenticate using Workload
@@ -210,6 +216,7 @@ Commands:
210216
reference-lists Manage reference lists.
211217
rule-exclusions Manage rule exclusions.
212218
rules Manage rules.
219+
saved-searches Manage saved searches.
213220
```
214221

215222
A logical first step after reading the contents of this readme file and
@@ -657,6 +664,98 @@ Example output from update remote rule exclusions command.
657664
01-May-25 12:15:36 MDT | INFO | dump_rule_exclusion_config | Writing rule exclusion config to /Users/x/Documents/projects/detection-rules/tools/content_manager/rule_exclusions_config.yaml
658665
```
659666
667+
## Managing saved searches in Google SecOps
668+
669+
### Retrieve saved searches from Google SecOps
670+
671+
The `saved-searches get` command retrieves the latest version of all saved
672+
searches from Google SecOps and writes them to a `saved_search_config.yaml`
673+
file.
674+
675+
The saved search content, configuration, and metadata is written to the
676+
`saved_search_config.yaml` file.
677+
678+
Example output from `saved-searches get` command:
679+
680+
```
681+
(venv) $ python -m content_manager saved-searches get
682+
10-Nov-25 14:11:37 MST | INFO | <module> | Content Manager started
683+
10-Nov-25 14:11:37 MST | INFO | get_saved_searches | Attempting to pull latest version of all saved searches from Google SecOps and update the local config file
684+
10-Nov-25 14:11:38 MST | INFO | get_remote_saved_searches | Attempting to retrieve all saved searches from Google SecOps
685+
10-Nov-25 14:11:38 MST | INFO | get_remote_saved_searches | Retrieved 11 saved searches
686+
10-Nov-25 14:11:38 MST | INFO | get_remote_saved_searches | Retrieved a total of 11 saved searches
687+
10-Nov-25 14:11:38 MST | INFO | dump_saved_search_config | Writing saved search config to /Users/x/Documents/projects/detection-rules/tools/content_manager/saved_search_config.yaml
688+
```
689+
690+
### Update saved searches in Google SecOps
691+
692+
The `saved-searches update` command updates saved searches in Google
693+
SecOps based on the local config file (`saved_search_config.yaml`).
694+
695+
Saved search updates include:
696+
697+
* Create a new saved search
698+
* Update the display name (title) for a saved search
699+
* Update the query for a saved search
700+
* Update the description for a saved search
701+
* Update the sharing settings for a saved search
702+
* Update placeholder variable and placeholder variable descriptions for a
703+
saved search
704+
705+
Please refer to the example saved searches in the `saved_search_config.yaml`
706+
file to understand the expected format for these files.
707+
708+
To create a new saved search, add a new entry to the
709+
`saved_search_config.yaml` file and execute the `saved-searches update`
710+
command. Please see the example below.
711+
712+
```
713+
Top 10 Suricata Rules:
714+
description: Statistical Search Workshop
715+
query: |-
716+
metadata.vendor_name = "Suricata" nocase
717+
$rule_name = security_result.rule_name
718+
match:
719+
$rule_name
720+
outcome:
721+
$event_count = count_distinct(metadata.id)
722+
order:
723+
$event_count desc
724+
limit:
725+
10
726+
sharing_mode: MODE_SHARED_WITH_CUSTOMER
727+
```
728+
729+
Existing saved searches can be updated by modifying the
730+
`saved_search_config.yaml` file and executing the `saved-searches update`
731+
command.
732+
733+
Example output from update saved searches command.
734+
735+
```
736+
(venv) $ python -m content_manager saved-searches update
737+
10-Nov-25 14:25:46 MST | INFO | <module> | Content Manager started
738+
10-Nov-25 14:25:46 MST | INFO | update_saved_searches | Attempting to update saved searches in Google SecOps based on the local config file
739+
10-Nov-25 14:25:46 MST | INFO | update_remote_saved_searches | Attempting to update saved searches in Google SecOps based on local config file /Users/x/Documents/projects/detection-rules/tools/content_manager/saved_search_config.yaml
740+
10-Nov-25 14:25:46 MST | INFO | load_saved_search_config | Loading saved search config from /Users/x/Documents/projects/detection-rules/tools/content_manager/saved_search_config.yaml
741+
10-Nov-25 14:25:46 MST | INFO | load_saved_search_config | Loaded 12 saved search config entries from file /Users/x/Documents/projects/detection-rules/tools/content_manager/saved_search_config.yaml
742+
10-Nov-25 14:25:46 MST | INFO | update_remote_saved_searches | Attempting to retrieve latest version of all saved searches from Google SecOps
743+
10-Nov-25 14:25:46 MST | INFO | get_remote_saved_searches | Attempting to retrieve all saved searches from Google SecOps
744+
10-Nov-25 14:25:47 MST | INFO | get_remote_saved_searches | Retrieved 12 saved searches
745+
10-Nov-25 14:25:47 MST | INFO | get_remote_saved_searches | Retrieved a total of 12 saved searches
746+
10-Nov-25 14:25:47 MST | INFO | update_remote_saved_searches | Checking if any saved search updates are required
747+
10-Nov-25 14:25:47 MST | INFO | update_remote_saved_searches | Saved search Top 10 Suricata Rules - Description for local and remote saved search is different. Remote saved search will be updated
748+
10-Nov-25 14:25:47 MST | INFO | update_remote_saved_searches | Saved search Top 10 Suricata Rules - Updating remote saved search
749+
10-Nov-25 14:25:48 MST | INFO | update | Logging summary of saved search changes...
750+
10-Nov-25 14:25:48 MST | INFO | update | Saved searches created: 0
751+
10-Nov-25 14:25:48 MST | INFO | update | Saved searches updated: 1
752+
10-Nov-25 14:25:48 MST | INFO | update | updated saved search ('Top 10 Suricata Rules', 'projects/1234567891234/locations/us/instances/3f0ac524-5ae1-4bfd-b86d-53afc953e7e6/users/me/searchQueries/baf471b7-067f-4a73-91c4-12cff0c0c29c')
753+
10-Nov-25 14:25:48 MST | INFO | get_remote_saved_searches | Attempting to retrieve all saved searches from Google SecOps
754+
10-Nov-25 14:25:49 MST | INFO | get_remote_saved_searches | Retrieved 12 saved searches
755+
10-Nov-25 14:25:49 MST | INFO | get_remote_saved_searches | Retrieved a total of 12 saved searches
756+
10-Nov-25 14:25:49 MST | INFO | dump_saved_search_config | Writing saved search config to /Users/x/Documents/projects/detection-rules/tools/content_manager/saved_search_config.yaml
757+
```
758+
660759
## Need help?
661760
662761
Please open an issue in this repo or reach out in the Google Cloud Security [community](https://secopscommunity.com).

tools/content_manager/content_manager/__main__.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from content_manager.reference_lists import ReferenceLists
3131
from content_manager.rule_exclusions import RuleExclusions
3232
from content_manager.rules import Rules
33+
from content_manager.saved_searches import SavedSearches
3334
import dotenv
3435
import google.auth.transport.requests
3536
from google_secops_api import auth
@@ -48,6 +49,7 @@
4849
DATA_TABLES_DIR = ROOT_DIR / "data_tables"
4950
DATA_TABLE_CONFIG_FILE = ROOT_DIR / "data_table_config.yaml"
5051
RULE_EXCLUSIONS_CONFIG_FILE = ROOT_DIR / "rule_exclusions_config.yaml"
52+
SAVED_SEARCH_CONFIG_FILE = ROOT_DIR / "saved_search_config.yaml"
5153

5254
dotenv.load_dotenv()
5355

@@ -454,6 +456,48 @@ def update(cls):
454456
RuleExclusionOperations.get()
455457

456458

459+
class SavedSearchOperations:
460+
"""Manage saved searches in Google SecOps."""
461+
462+
@classmethod
463+
def get(cls):
464+
"""Retrieves the latest version of saved searches from Google SecOps and updates the local config file."""
465+
http_session = initialize_http_session()
466+
467+
remote_saved_searches = SavedSearches.get_remote_saved_searches(
468+
http_session=http_session
469+
)
470+
471+
if not remote_saved_searches.saved_searches:
472+
LOGGER.info("No saved searches retrieved")
473+
return
474+
475+
remote_saved_searches.dump_saved_search_config()
476+
477+
@classmethod
478+
def update(cls):
479+
"""Update saved searches in Google SecOps based on local config file."""
480+
http_session = initialize_http_session()
481+
482+
saved_search_updates = SavedSearches.update_remote_saved_searches(
483+
http_session=http_session
484+
)
485+
486+
if not saved_search_updates:
487+
return
488+
489+
# Log summary of saved search updates that occurred.
490+
LOGGER.info("Logging summary of saved search changes...")
491+
for update_type, saved_search_names in saved_search_updates.items():
492+
LOGGER.info("Saved searches %s: %s", update_type, len(saved_search_names))
493+
for saved_search_name in saved_search_names:
494+
LOGGER.info("%s saved search %s", update_type, saved_search_name)
495+
496+
# Retrieve the latest version of all saved searches after any changes
497+
# were made.
498+
SavedSearchOperations.get()
499+
500+
457501
@click.group()
458502
def cli():
459503
"""Content Manager - Manage content in Google SecOps such as rules, data tables, reference lists, and exclusions."""
@@ -727,6 +771,39 @@ def update():
727771
RuleExclusionOperations.update()
728772

729773

774+
@click.group()
775+
def saved_searches():
776+
"""Manage saved searches."""
777+
778+
779+
@saved_searches.command(
780+
"get",
781+
short_help="""Retrieve the latest version of all saved searches from Google SecOps and updates the local config file.""",
782+
)
783+
def get_saved_searches():
784+
"""Retrieve the latest version of all saved searches from Google SecOps and update the local config file."""
785+
LOGGER.info(
786+
"Attempting to pull latest version of all saved searches from Google "
787+
"SecOps and update the local config file"
788+
)
789+
SavedSearchOperations.get()
790+
791+
792+
@saved_searches.command(
793+
"update",
794+
short_help=(
795+
"Update saved searches in Google SecOps based on the local config file."
796+
),
797+
)
798+
def update_saved_searches():
799+
"""Update saved searches in Google SecOps based on the local config file."""
800+
LOGGER.info(
801+
"Attempting to update saved searches in Google SecOps based on the local"
802+
" config file"
803+
)
804+
SavedSearchOperations.update()
805+
806+
730807
if __name__ == "__main__":
731808
LOGGER.info("Content Manager started")
732809

@@ -740,10 +817,12 @@ def update():
740817
REF_LIST_CONFIG_FILE.touch(exist_ok=True)
741818
DATA_TABLE_CONFIG_FILE.touch(exist_ok=True)
742819
RULE_EXCLUSIONS_CONFIG_FILE.touch(exist_ok=True)
820+
SAVED_SEARCH_CONFIG_FILE.touch(exist_ok=True)
743821

744822
cli.add_command(rules)
745823
cli.add_command(data_tables)
746824
cli.add_command(reference_lists)
747825
cli.add_command(rule_exclusions)
826+
cli.add_command(saved_searches)
748827

749828
cli()

tools/content_manager/content_manager/common/custom_exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ class ReferenceListConfigError(Exception):
4545

4646
class RuleExclusionConfigError(Exception):
4747
"""Raised when an issue with the rule exclusion config file is found."""
48+
49+
50+
class SavedSearchConfigError(Exception):
51+
"""Raised when an issue with the saved search config file is found."""

0 commit comments

Comments
 (0)