22"""Command-line interface for mSCP.
33
44Defines `parse_cli`, the top-level entry point invoked from
5- :mod: `mscp.__main__`. Builds an `argparse` tree with subcommands
6- `` baseline`` / `` guidance`` / `` mapping`` / `` scap`` / `` admin` ` (the
5+ `mscp.__main__`. Builds an `argparse` tree with subcommands
6+ `baseline` / `guidance` / `mapping` / `scap` / `admin` (the
77last with its own nested utilities) and dispatches to the matching
88function in `mscp.generate` or `mscp.admin_utils`.
99"""
1010
1111# Standard python modules
1212import argparse
13+ import json
1314import sys
1415import platform
1516from pathlib import Path
@@ -58,13 +59,61 @@ def error(self, message: str) -> None:
5859 sys .exit (2 )
5960
6061
62+ class ListPlatformsAction (argparse .Action ):
63+ def __call__ (self , parser , namespace , values , option_string = None ):
64+ fmt = "human"
65+ if "--format" in sys .argv :
66+ idx = sys .argv .index ("--format" )
67+ if idx + 1 < len (sys .argv ):
68+ fmt = sys .argv [idx + 1 ]
69+
70+ platforms = mscp_data .get ("versions" , {}).get ("platforms" , {})
71+
72+ if fmt == "json" :
73+ print (
74+ json .dumps (
75+ {
76+ name : sorted ((e ["os_version" ] for e in versions ), reverse = True )
77+ for name , versions in platforms .items ()
78+ },
79+ indent = 2 ,
80+ )
81+ )
82+ else :
83+ platform_versions = {
84+ name : sorted ((e ["os_version" ] for e in versions ), reverse = True )
85+ for name , versions in platforms .items ()
86+ }
87+ col_width = max (len (name ) for name in platform_versions ) + 4
88+ max_rows = max (len (v ) for v in platform_versions .values ())
89+
90+ print ("Supported platforms (--os_name) and versions (--os_version):\n " )
91+ print (" " + "" .join (name .ljust (col_width ) for name in platform_versions ))
92+ print (
93+ " "
94+ + "" .join (
95+ ("-" * len (name )).ljust (col_width ) for name in platform_versions
96+ )
97+ )
98+ for i in range (max_rows ):
99+ print (
100+ " "
101+ + "" .join (
102+ (str (v [i ]) if i < len (v ) else "" ).ljust (col_width )
103+ for v in platform_versions .values ()
104+ )
105+ )
106+ print ()
107+ parser .exit ()
108+
109+
61110class SmartFormatter (argparse .HelpFormatter ):
62111 """Help formatter with two minor tweaks for the mSCP CLI.
63112
64113 - Single-letter / single-form options are indented to align with the
65114 long-form options for readability.
66- - Help strings prefixed with `` "R|"` ` are emitted with their original
67- newlines preserved (a common `` argparse` ` recipe for raw text).
115+ - Help strings prefixed with `"R|"` are emitted with their original
116+ newlines preserved (a common `argparse` recipe for raw text).
68117 """
69118
70119 def _format_action_invocation (self , action ):
@@ -88,14 +137,13 @@ def _split_lines(self, text, width):
88137def get_macos_version () -> float :
89138 """Return the running host's major macOS version as a float.
90139
91- Used as the default for the `` --os_version` ` flag so the CLI assumes
92- the current host's version unless overridden. Falls back to `` 26.0` `
140+ Used as the default for the `--os_version` flag so the CLI assumes
141+ the current host's version unless overridden. Falls back to `26.0`
93142 when `platform.mac_ver` returns an empty string (e.g. when run on a
94143 non-macOS host).
95144
96145 Returns:
97- float: Major version (e.g. ``15.0``), or ``26.0`` on a non-macOS
98- host.
146+ float: Major version (e.g. `15.0`), or `26.0` on a non-macOS host.
99147 """
100148 version_str , _ , _ = platform .mac_ver ()
101149 if version_str :
@@ -106,9 +154,9 @@ def get_macos_version() -> float:
106154
107155
108156def validate_file (arg : str ) -> Path | None :
109- """`argparse` type validator: ensure `` arg` ` points at an existing file.
157+ """`argparse` type validator: ensure `arg` points at an existing file.
110158
111- Used as the `` type=` ` argument on flags that take a path. Logs an
159+ Used as the `type=` argument on flags that take a path. Logs an
112160 error and calls `sys.exit` if the path doesn't resolve to a file.
113161
114162 Args:
@@ -128,16 +176,16 @@ def validate_file(arg: str) -> Path | None:
128176def parse_cli () -> None :
129177 """Build the mSCP argument parser, parse `sys.argv`, and dispatch.
130178
131- Constructs the top-level parser plus the `` baseline`` , `` guidance` `,
132- `` mapping`` , `` scap`` , and `` admin` ` subcommands (each with its own
179+ Constructs the top-level parser plus the `baseline`, `guidance`,
180+ `mapping`, `scap`, and `admin` subcommands (each with its own
133181 flags), applies log-verbosity overrides, validates the platform/OS
134182 arguments (rejects unsupported macOS / iOS versions), and then calls
135- the subcommand's bound `` func` ` with the parsed `argparse.Namespace`.
183+ the subcommand's bound `func` with the parsed `argparse.Namespace`.
136184
137- Side Effects :
138- Reads `` sys.argv`` ; configures the global mSCP logger;
139- mutates the global `config` dict for `` output_dir`` / `` rules_dir``;
140- may call `sys.exit` on validation failure.
185+ Note :
186+ Reads `sys.argv`; configures the global mSCP logger; mutates the
187+ global `config` dict for `output_dir` / `rules_dir`; may call
188+ `sys.exit` on validation failure.
141189 """
142190 parent_parser = Customparser ()
143191 parent_parser .add_argument (
@@ -172,9 +220,23 @@ def parse_cli() -> None:
172220 version = f"%(prog)s { _v .get ('version' , 'unknown' )} (build { _v .get ('build' , '?' )} , { _v .get ('build_date' , 'unknown' )} )" ,
173221 )
174222
223+ parser .add_argument (
224+ "--list_platforms" ,
225+ nargs = 0 ,
226+ action = ListPlatformsAction ,
227+ help = "list all supported platforms and versions" ,
228+ )
229+
230+ parser .add_argument (
231+ "--format" ,
232+ choices = ["human" , "json" ],
233+ default = "human" ,
234+ help = "output format for --list_platforms" ,
235+ )
236+
175237 parser .add_argument (
176238 "--os_name" ,
177- choices = [ "macos " , "ios " , "visionos" ] ,
239+ choices = mscp_data . get ( "versions " , {}). get ( "platforms " , {}) ,
178240 default = "macos" ,
179241 help = "operating system to be referenced when generating guidance" ,
180242 type = str ,
@@ -187,6 +249,15 @@ def parse_cli() -> None:
187249 help = "version of the operating system to be referenced when generating guidance (eg: 14.0, 15.0)." ,
188250 )
189251
252+ parser .add_argument (
253+ "-C" ,
254+ "--custom_dir" ,
255+ default = config ["custom_dir" ],
256+ type = Path ,
257+ metavar = "PATH" ,
258+ help = "Path to custom directory." ,
259+ )
260+
190261 parser .add_argument (
191262 "-R" ,
192263 "--rules_dir" ,
@@ -221,6 +292,15 @@ def parse_cli() -> None:
221292 )
222293 baseline_parser .set_defaults (func = generate_baseline )
223294
295+ baseline_parser .add_argument (
296+ "-b" ,
297+ "--baseline" ,
298+ type = validate_file ,
299+ help = argparse .SUPPRESS ,
300+ # help="when tailoring, if you provide a baseline.yaml file, it will only prompt for rules not already included",
301+ action = "store" ,
302+ )
303+
224304 baseline_parser .add_argument (
225305 "-c" ,
226306 "--controls" ,
@@ -342,12 +422,14 @@ def parse_cli() -> None:
342422 help = "generate the compliance script for the rules in the specified baseline" ,
343423 action = "store_true" ,
344424 )
425+
345426 guidance_parser .add_argument (
346427 "--audit_name" ,
347428 default = None ,
348429 help = "specify the name of audit plist and log (defaults to baseline name)" ,
349430 action = "store" ,
350431 )
432+
351433 guidance_parser .add_argument (
352434 "--reference" ,
353435 default = None ,
@@ -610,6 +692,8 @@ def parse_cli() -> None:
610692
611693 if args .output_dir :
612694 config ["output_dir" ] = str (args .output_dir .expanduser ().resolve ())
695+ if args .custom_dir :
696+ config ["custom_dir" ] = str (args .custom_dir .expanduser ().resolve ())
613697 except argparse .ArgumentError as e :
614698 logger .error ("Argument Error: {}" , e )
615699 parser .print_help ()
0 commit comments