diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adeed2..5d985ea 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 59308c5..4388a25 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" diff --git a/src/secops/cli/commands/config.py b/src/secops/cli/commands/config.py index 4ac1085..04e9993 100644 --- a/src/secops/cli/commands/config.py +++ b/src/secops/cli/commands/config.py @@ -32,9 +32,8 @@ def setup_config_command(subparsers): config_parser = subparsers.add_parser( "config", help="Manage CLI configuration" ) - config_subparsers = config_parser.add_subparsers( - help="Config command", required=True - ) + config_subparsers = config_parser.add_subparsers(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 cc19ae5..f2ebd57 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) @@ -96,7 +98,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" @@ -116,9 +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", required=True - ) + 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" @@ -141,7 +143,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 db34d0c..3331ab3 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 7078082..829b492 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 3ef8b12..3190126 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 db43ace..5ffd2f2 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 3e25016..c15a602 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 0836fc4..f441d07 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 8bcd693..0b8890c 100644 --- a/src/secops/cli/commands/log.py +++ b/src/secops/cli/commands/log.py @@ -23,9 +23,8 @@ 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 - ) + log_subparsers = log_parser.add_subparsers(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 1e8ffd8..a9574be 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 4e542df..c6cdabb 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 74b8d04..98d989f 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 ab460b7..78c825c 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 aeb2b07..a634f7b 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( diff --git a/src/secops/cli/commands/search.py b/src/secops/cli/commands/search.py index acc15b5..cd58514 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() + 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", @@ -78,10 +82,17 @@ 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: - 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,