Skip to content

Commit cd89652

Browse files
Merge pull request #692 from brodjieski/dev_2.0
Refactor with CLI enhancements and author templates
2 parents 1d1d3b3 + 3b80bf8 commit cd89652

11 files changed

Lines changed: 214 additions & 62 deletions

File tree

schema/mscp_baseline.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"items": {
2727
"type": "object",
2828
"required": [
29-
"name"
29+
"name",
30+
"organization"
3031
],
3132
"additionalProperties": false,
3233
"properties": {
@@ -37,6 +38,10 @@
3738
"organization": {
3839
"type": "string",
3940
"description": "Organization the author is affiliated with"
41+
},
42+
"additional": {
43+
"type": "boolean",
44+
"description": "Author will be listed in addition to the primary MSCP contributors. Typically added as part of tailoring a baseline."
4045
}
4146
}
4247
}

schema/mscp_rule.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,12 @@
421421
"enum": [
422422
"com.apple.configuration.services.configuration-files",
423423
"com.apple.configuration.diskmanagement.settings",
424-
"com.apple.configuration.passcode.settings"
424+
"com.apple.configuration.passcode.settings",
425+
"com.apple.configuration.intelligence.settings",
426+
"com.apple.configuration.external-intelligence.settings",
427+
"com.apple.configuration.softwareupdate.settings",
428+
"com.apple.configuration.siri.settings",
429+
"com.apple.configuration.keyboard.settings"
425430
]
426431
},
427432
"service": {

src/mscp/classes/baseline.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Standard python modules
1212
from collections import OrderedDict, defaultdict
1313
from pathlib import Path
14-
from typing import Any
14+
from typing import Any, Optional
1515

1616
# Additional python modules
1717
import pandas as pd
@@ -24,6 +24,7 @@
2424

2525
__all__ = ["Author", "Profile", "Baseline"]
2626

27+
2728
class BaseModelWithAccessors(BaseModel):
2829
"""Pydantic base class with dict-style accessors.
2930
@@ -75,16 +76,30 @@ def __setitem__(self, key: str, value: Any) -> None:
7576

7677

7778
class Author(BaseModelWithAccessors):
78-
"""One author or owning organisation of a baseline.
79+
"""One author or owning organization of a baseline.
7980
8081
Attributes:
8182
name (str | None): Personal name of the author, if available.
82-
organization (str | None): Organisation the author represents, if
83+
organization (str | None): Organization the author represents, if
8384
applicable.
8485
"""
8586

8687
name: str | None
8788
organization: str | None
89+
additional: Optional[bool] = None
90+
91+
def is_additional(self) -> bool:
92+
"""Return true if this author is in addition to MSCP contributors.
93+
94+
This is a simple helper that checks the `additional` field, which
95+
should be set to true for any author that is not a primary contributor
96+
to MSCP. This allows generated guidance to flag such authors
97+
and their contributions for special attention.
98+
99+
Returns:
100+
bool: True when `additional` is true, false otherwise.
101+
"""
102+
return self.additional is True
88103

89104

90105
class Profile(BaseModelWithAccessors):

src/mscp/cli.py

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
"""Command-line interface for mSCP.
33
44
Defines `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
77
last with its own nested utilities) and dispatches to the matching
88
function in `mscp.generate` or `mscp.admin_utils`.
99
"""
1010

1111
# Standard python modules
1212
import argparse
13+
import json
1314
import sys
1415
import platform
1516
from 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+
61110
class 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):
88137
def 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

108156
def 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:
128176
def 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()

src/mscp/common_utils/version_data.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@
1212
# Local python modules
1313
from .logger_instance import logger
1414

15-
# Additional python modules
15+
OS_NAME_MAP = {
16+
"tahoe": "Tahoe",
17+
"sequoia": "Sequoia",
18+
"sonoma": "Sonoma",
19+
"ventura": "Ventura",
20+
"ios_26": "iOS 26",
21+
"ios_18": "iOS 18",
22+
"ios_17": "iOS 17",
23+
"visionos_26": "VisionOS 26",
24+
}
1625

1726

1827
def get_version_data(
@@ -54,7 +63,11 @@ def get_version_data(
5463

5564
valid_versions = [e.get("os_version") for e in platforms[os_name.lower()]]
5665
match = next(
57-
(e for e in platforms[os_name.lower()] if e.get("os_version") == os_version),
66+
(
67+
e
68+
for e in platforms[os_name.lower()]
69+
if e.get("os_version") == os_version
70+
),
5871
None,
5972
)
6073

@@ -63,6 +76,10 @@ def get_version_data(
6376
f"Unknown os_version {os_version!r} for {os_name!r}. "
6477
f"Valid versions: {valid_versions}"
6578
)
79+
else:
80+
match["compliance_version"] = (
81+
f"{OS_NAME_MAP.get(match['os_name'].lower(), match['os_name'])} Guidance, Revision {match['revision']}"
82+
)
6683

6784
return match
6885

0 commit comments

Comments
 (0)