From b4269330c5682fee06cc22b807eaf844b7efa72b Mon Sep 17 00:00:00 2001 From: Michel Oosterhof Date: Tue, 9 Dec 2025 09:21:59 +0800 Subject: [PATCH 1/4] Fix: Display help message for commands without arguments --- src/secops/cli/commands/config.py | 3 ++- src/secops/cli/commands/curated_rule.py | 13 ++++++++++--- src/secops/cli/commands/dashboard.py | 4 +++- src/secops/cli/commands/dashboard_query.py | 4 +++- src/secops/cli/commands/data_table.py | 1 + src/secops/cli/commands/export.py | 1 + src/secops/cli/commands/feed.py | 1 + src/secops/cli/commands/forwarder.py | 3 +++ src/secops/cli/commands/log.py | 3 ++- src/secops/cli/commands/parser.py | 1 + src/secops/cli/commands/parser_extension.py | 3 ++- src/secops/cli/commands/reference_list.py | 1 + src/secops/cli/commands/rule.py | 1 + src/secops/cli/commands/rule_exclusion.py | 1 + 14 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/secops/cli/commands/config.py b/src/secops/cli/commands/config.py index c1ff51d6..62f20eb5 100644 --- a/src/secops/cli/commands/config.py +++ b/src/secops/cli/commands/config.py @@ -33,8 +33,9 @@ def setup_config_command(subparsers): "config", help="Manage CLI configuration" ) config_subparsers = config_parser.add_subparsers( - help="Config command", required=True + help="Config command" ) + config_parser.set_defaults(func=lambda args: config_parser.print_help()) # Set config command set_parser = config_subparsers.add_parser( diff --git a/src/secops/cli/commands/curated_rule.py b/src/secops/cli/commands/curated_rule.py index cc19ae51..872c7596 100644 --- a/src/secops/cli/commands/curated_rule.py +++ b/src/secops/cli/commands/curated_rule.py @@ -96,7 +96,8 @@ def setup_curated_rules_command(subparsers): # ---- rule-set ---- rule_set = lvl1.add_parser("rule-set", help="Manage curated rule sets") - rule_set_subparser = rule_set.add_subparsers(dest="rset_cmd", required=True) + rule_set_subparser = rule_set.add_subparsers(dest="rset_cmd") + rule_set.set_defaults(func=lambda args, _: rule_set.print_help()) rule_set_list = rule_set_subparser.add_parser( "list", help="List curated rule sets" @@ -117,7 +118,10 @@ def setup_curated_rules_command(subparsers): "rule-set-category", help="Manage curated rule set categories" ) rule_set_cat_subparser = rule_set_cat.add_subparsers( - dest="rcat_cmd", required=True + dest="rcat_cmd" + ) + rule_set_cat.set_defaults( + func=lambda args, _: rule_set_cat.print_help() ) rule_set_cat_list = rule_set_cat_subparser.add_parser( @@ -141,7 +145,10 @@ def setup_curated_rules_command(subparsers): "rule-set-deployment", help="Manage curated rule set deployments" ) rule_set_deployment_subparser = rule_set_deployment.add_subparsers( - dest="rdep_cmd", required=True + dest="rdep_cmd" + ) + rule_set_deployment.set_defaults( + func=lambda args, _: rule_set_deployment.print_help() ) rule_set_deployment_list = rule_set_deployment_subparser.add_parser( diff --git a/src/secops/cli/commands/dashboard.py b/src/secops/cli/commands/dashboard.py index ea57f067..7c4e3b44 100644 --- a/src/secops/cli/commands/dashboard.py +++ b/src/secops/cli/commands/dashboard.py @@ -30,7 +30,9 @@ def setup_dashboard_command(subparsers): dashboard_subparsers = dashboard_parser.add_subparsers( dest="dashboard_command", help="Dashboard command to execute", - required=True, + ) + dashboard_parser.set_defaults( + func=lambda args, _: dashboard_parser.print_help() ) # List dashboards diff --git a/src/secops/cli/commands/dashboard_query.py b/src/secops/cli/commands/dashboard_query.py index 21ab83bb..265d7ae2 100644 --- a/src/secops/cli/commands/dashboard_query.py +++ b/src/secops/cli/commands/dashboard_query.py @@ -28,7 +28,9 @@ def setup_dashboard_query_command(subparsers): dashboard_query_subparsers = dashboard_query_parser.add_subparsers( dest="dashboard_query_command", help="Dashboard query command to execute", - required=True, + ) + dashboard_query_parser.set_defaults( + func=lambda args, _: dashboard_query_parser.print_help() ) # Execute query diff --git a/src/secops/cli/commands/data_table.py b/src/secops/cli/commands/data_table.py index e8530728..f2a0fda1 100644 --- a/src/secops/cli/commands/data_table.py +++ b/src/secops/cli/commands/data_table.py @@ -27,6 +27,7 @@ def setup_data_table_command(subparsers): dt_subparsers = dt_parser.add_subparsers( dest="dt_command", help="Data table command" ) + dt_parser.set_defaults(func=lambda args, _: dt_parser.print_help()) # List data tables command list_parser = dt_subparsers.add_parser("list", help="List data tables") diff --git a/src/secops/cli/commands/export.py b/src/secops/cli/commands/export.py index 90f1aa4e..094f51a9 100644 --- a/src/secops/cli/commands/export.py +++ b/src/secops/cli/commands/export.py @@ -30,6 +30,7 @@ def setup_export_command(subparsers): export_subparsers = export_parser.add_subparsers( dest="export_command", help="Export command" ) + export_parser.set_defaults(func=lambda args, _: export_parser.print_help()) # List available log types command log_types_parser = export_subparsers.add_parser( diff --git a/src/secops/cli/commands/feed.py b/src/secops/cli/commands/feed.py index 3e25016a..c15a6022 100644 --- a/src/secops/cli/commands/feed.py +++ b/src/secops/cli/commands/feed.py @@ -25,6 +25,7 @@ def setup_feed_command(subparsers): feed_subparsers = feed_parser.add_subparsers( dest="feed_command", help="Feed command" ) + feed_parser.set_defaults(func=lambda args, _: feed_parser.print_help()) # List feeds command list_parser = feed_subparsers.add_parser("list", help="List feeds") diff --git a/src/secops/cli/commands/forwarder.py b/src/secops/cli/commands/forwarder.py index 67059026..fd2e5176 100644 --- a/src/secops/cli/commands/forwarder.py +++ b/src/secops/cli/commands/forwarder.py @@ -29,6 +29,9 @@ def setup_forwarder_command(subparsers): forwarder_subparsers = forwarder_parser.add_subparsers( dest="forwarder_command", help="Forwarder command" ) + forwarder_parser.set_defaults( + func=lambda args, _: forwarder_parser.print_help() + ) # Create forwarder command create_parser = forwarder_subparsers.add_parser( diff --git a/src/secops/cli/commands/log.py b/src/secops/cli/commands/log.py index f8c4f42e..95c1e5f9 100644 --- a/src/secops/cli/commands/log.py +++ b/src/secops/cli/commands/log.py @@ -24,8 +24,9 @@ def setup_log_command(subparsers): """Set up the log command parser.""" log_parser = subparsers.add_parser("log", help="Ingest logs") log_subparsers = log_parser.add_subparsers( - help="Log command", required=True + help="Log command" ) + log_parser.set_defaults(func=lambda args, _: log_parser.print_help()) # Ingest log command ingest_parser = log_subparsers.add_parser("ingest", help="Ingest raw logs") diff --git a/src/secops/cli/commands/parser.py b/src/secops/cli/commands/parser.py index 0ca058c8..dbcb77ca 100644 --- a/src/secops/cli/commands/parser.py +++ b/src/secops/cli/commands/parser.py @@ -18,6 +18,7 @@ def setup_parser_command(subparsers): parser_subparsers = parser_parser.add_subparsers( dest="parser_command", help="Parser command" ) + parser_parser.set_defaults(func=lambda args, _: parser_parser.print_help()) # --- Activate Parser Command --- activate_parser_sub = parser_subparsers.add_parser( diff --git a/src/secops/cli/commands/parser_extension.py b/src/secops/cli/commands/parser_extension.py index efe9a6e2..1d471d13 100644 --- a/src/secops/cli/commands/parser_extension.py +++ b/src/secops/cli/commands/parser_extension.py @@ -32,7 +32,8 @@ def setup_parser_extension_command(subparsers: Any) -> None: "parser-extension", help="Manage parser extensions", ) - parser_ext_sub = parser_ext.add_subparsers(dest="subcommand", required=True) + parser_ext_sub = parser_ext.add_subparsers(dest="subcommand") + parser_ext.set_defaults(func=lambda args, _: parser_ext.print_help()) # Create parser extension create = parser_ext_sub.add_parser( diff --git a/src/secops/cli/commands/reference_list.py b/src/secops/cli/commands/reference_list.py index aac68267..efbfc0b6 100644 --- a/src/secops/cli/commands/reference_list.py +++ b/src/secops/cli/commands/reference_list.py @@ -31,6 +31,7 @@ def setup_reference_list_command(subparsers): rl_subparsers = rl_parser.add_subparsers( dest="rl_command", help="Reference list command" ) + rl_parser.set_defaults(func=lambda args, _: rl_parser.print_help()) # List reference lists command list_parser = rl_subparsers.add_parser("list", help="List reference lists") diff --git a/src/secops/cli/commands/rule.py b/src/secops/cli/commands/rule.py index c36a1d68..005edd58 100644 --- a/src/secops/cli/commands/rule.py +++ b/src/secops/cli/commands/rule.py @@ -32,6 +32,7 @@ def setup_rule_command(subparsers): rule_subparsers = rule_parser.add_subparsers( dest="rule_command", help="Rule command" ) + rule_parser.set_defaults(func=lambda args, _: rule_parser.print_help()) # List rules command list_parser = rule_subparsers.add_parser("list", help="List rules") diff --git a/src/secops/cli/commands/rule_exclusion.py b/src/secops/cli/commands/rule_exclusion.py index f4791dcc..ed90daa7 100644 --- a/src/secops/cli/commands/rule_exclusion.py +++ b/src/secops/cli/commands/rule_exclusion.py @@ -32,6 +32,7 @@ def setup_rule_exclusion_command(subparsers): re_subparsers = re_parser.add_subparsers( dest="re_command", help="Rule exclusion command" ) + re_parser.set_defaults(func=lambda args, _: re_parser.print_help()) # Create rule exclusion command create_parser = re_subparsers.add_parser( From 2343d04e004d537498b606d13c50719baae07214 Mon Sep 17 00:00:00 2001 From: Mihir Vala <179564180+mihirvala-crestdata@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:08:17 +0530 Subject: [PATCH 2/4] chore: added help print for remaining commands --- src/secops/cli/commands/curated_rule.py | 14 ++++++-------- src/secops/cli/commands/search.py | 10 +++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/secops/cli/commands/curated_rule.py b/src/secops/cli/commands/curated_rule.py index 872c7596..f2ebd57a 100644 --- a/src/secops/cli/commands/curated_rule.py +++ b/src/secops/cli/commands/curated_rule.py @@ -29,11 +29,13 @@ def setup_curated_rules_command(subparsers): top = subparsers.add_parser( "curated-rule", help="Manage curated rules and rule sets" ) - lvl1 = top.add_subparsers(dest="curated_cmd", required=True) + lvl1 = top.add_subparsers(dest="curated_cmd") + top.set_defaults(func=lambda args, _: top.print_help()) # ---- rules ---- rules = lvl1.add_parser("rule", help="Manage curated rules") - rules_sp = rules.add_subparsers(dest="rule_cmd", required=True) + rules_sp = rules.add_subparsers(dest="rule_cmd") + rules.set_defaults(func=lambda args, _: rules.print_help()) rules_list = rules_sp.add_parser("list", help="List curated rules") add_pagination_args(rules_list) @@ -117,12 +119,8 @@ def setup_curated_rules_command(subparsers): rule_set_cat = lvl1.add_parser( "rule-set-category", help="Manage curated rule set categories" ) - rule_set_cat_subparser = rule_set_cat.add_subparsers( - dest="rcat_cmd" - ) - rule_set_cat.set_defaults( - func=lambda args, _: rule_set_cat.print_help() - ) + rule_set_cat_subparser = rule_set_cat.add_subparsers(dest="rcat_cmd") + rule_set_cat.set_defaults(func=lambda args, _: rule_set_cat.print_help()) rule_set_cat_list = rule_set_cat_subparser.add_parser( "list", help="List curated rule set categories" diff --git a/src/secops/cli/commands/search.py b/src/secops/cli/commands/search.py index 6b754534..c4b2c230 100644 --- a/src/secops/cli/commands/search.py +++ b/src/secops/cli/commands/search.py @@ -31,13 +31,17 @@ def setup_search_command(subparsers): subparsers: Subparsers object to add to """ search_parser = subparsers.add_parser("search", help="Search UDM events") - search_parser.add_argument("--query", help="UDM query string") - search_parser.add_argument( + + # Create mutually exclusive group for query types + query_group = search_parser.add_mutually_exclusive_group(required=True) + query_group.add_argument("--query", help="UDM query string") + query_group.add_argument( "--nl-query", "--nl_query", dest="nl_query", help="Natural language query", ) + search_parser.add_argument( "--max-events", "--max_events", @@ -81,7 +85,7 @@ def handle_search_command(args, chronicle): start_time, end_time = get_time_range(args) try: - if args.csv and args.fields: + if args.csv and args.fields and args.query: fields = [f.strip() for f in args.fields.split(",")] result = chronicle.fetch_udm_search_csv( query=args.query, From e9940d5ecb42afcfd6c6ebfd23cb980d872c0d29 Mon Sep 17 00:00:00 2001 From: Mihir Vala <179564180+mihirvala-crestdata@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:11:27 +0530 Subject: [PATCH 3/4] chore: fixed search arg check. format and linting fix. --- src/secops/chronicle/client.py | 4 ++-- src/secops/cli/commands/config.py | 4 +--- src/secops/cli/commands/log.py | 4 +--- src/secops/cli/commands/search.py | 9 ++++++++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/secops/chronicle/client.py b/src/secops/chronicle/client.py index 56dc8c27..5474228e 100644 --- a/src/secops/chronicle/client.py +++ b/src/secops/chronicle/client.py @@ -329,8 +329,8 @@ 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}" diff --git a/src/secops/cli/commands/config.py b/src/secops/cli/commands/config.py index 62f20eb5..e1a4255d 100644 --- a/src/secops/cli/commands/config.py +++ b/src/secops/cli/commands/config.py @@ -32,9 +32,7 @@ def setup_config_command(subparsers): config_parser = subparsers.add_parser( "config", help="Manage CLI configuration" ) - config_subparsers = config_parser.add_subparsers( - help="Config command" - ) + config_subparsers = config_parser.add_subparsers(help="Config command") config_parser.set_defaults(func=lambda args: config_parser.print_help()) # Set config command diff --git a/src/secops/cli/commands/log.py b/src/secops/cli/commands/log.py index 95c1e5f9..d3f63f8b 100644 --- a/src/secops/cli/commands/log.py +++ b/src/secops/cli/commands/log.py @@ -23,9 +23,7 @@ def setup_log_command(subparsers): """Set up the log command parser.""" log_parser = subparsers.add_parser("log", help="Ingest logs") - log_subparsers = log_parser.add_subparsers( - help="Log command" - ) + log_subparsers = log_parser.add_subparsers(help="Log command") log_parser.set_defaults(func=lambda args, _: log_parser.print_help()) # Ingest log command diff --git a/src/secops/cli/commands/search.py b/src/secops/cli/commands/search.py index c4b2c230..553ad700 100644 --- a/src/secops/cli/commands/search.py +++ b/src/secops/cli/commands/search.py @@ -33,7 +33,7 @@ def setup_search_command(subparsers): search_parser = subparsers.add_parser("search", help="Search UDM events") # Create mutually exclusive group for query types - query_group = search_parser.add_mutually_exclusive_group(required=True) + query_group = search_parser.add_mutually_exclusive_group() query_group.add_argument("--query", help="UDM query string") query_group.add_argument( "--nl-query", @@ -82,6 +82,13 @@ def handle_search_command(args, chronicle): args: Command line arguments chronicle: Chronicle client """ + # Require query or nl_query + if not args.query and not args.nl_query: + print( + "\nError: One of --query or --nl-query is required", file=sys.stderr + ) + sys.exit(1) + start_time, end_time = get_time_range(args) try: From e1e1b4827694f05b066baa1a12c8956a1435067b Mon Sep 17 00:00:00 2001 From: Mihir Vala <179564180+mihirvala-crestdata@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:18:31 +0530 Subject: [PATCH 4/4] chore: added changelog. updated project version --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adeed2e..5d985ea5 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.1] - 2025-12-11 +### Updated +- CLI to show help when required sub-command/argument not provided. + ## [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. diff --git a/pyproject.toml b/pyproject.toml index 59308c5f..4388a25d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "secops" -version = "0.28.0" +version = "0.28.1" description = "Python SDK for wrapping the Google SecOps API for common use cases" readme = "README.md" requires-python = ">=3.10"