Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e2ce35
chore: update issue templates
brodjieski Jun 16, 2026
5faa520
chore: update issue templates
brodjieski Jun 16, 2026
adabf4d
Merge branch 'usnistgov:dev_2.0' into dev_2.0
brodjieski Jun 16, 2026
2bd3893
refactor: create EnforcementInfo class to support hierarchy
brodjieski Jun 17, 2026
cba7d77
chore: re-add macOS 14 support for initial release
brodjieski Jun 17, 2026
c76c934
refactor: split macsecurityrule into references, mobileconfig, and en…
brodjieski Jun 17, 2026
ef71c5c
refactor: add runtime validation and fix schema issues
brodjieski Jun 17, 2026
e966758
feat: add remove_mscp_apple_release and standardize --version flag
brodjieski Jun 17, 2026
87ff9a0
refactor: fix version datatype in removal and add success string to v…
brodjieski Jun 17, 2026
1e64c4e
chore: lint cleanup
brodjieski Jun 17, 2026
fd2b5cd
Merge branch 'usnistgov:dev_2.0' into dev_2.0
brodjieski Jun 17, 2026
78f1bb4
fix: re-added missing imports
brodjieski Jun 18, 2026
8011455
removed fix blob from 26 and 14
robertgendler Jun 18, 2026
58edd89
feat: add CLI smoke test workflow and --no-docs flag for guidance
brodjieski Jun 18, 2026
0ad95b9
refactor: adjust CLI tests
brodjieski Jun 18, 2026
c40798f
fix: exit 1 when admin validate finds invalid YAML files
brodjieski Jun 18, 2026
d2aed73
refactor: rename collect_all_rules, fix -l listing, suppress admin sp…
brodjieski Jun 19, 2026
96c0f15
perf: avoid repeated collect_platform_rules in admin baselines
brodjieski Jun 19, 2026
7f2e4ee
Merge remote-tracking branch 'upstream/dev_2.0' into dev_2.0
brodjieski Jun 19, 2026
0f9857e
chore[rule]: correct yaml linting
brodjieski Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/cli-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: CLI Smoke Tests

on:
workflow_dispatch:
push:
branches:
- dev_2.0
paths-ignore:
- 'archive/**'
pull_request:
branches:
- dev_2.0
paths-ignore:
- 'archive/**'

jobs:
cli-smoke-tests:
name: CLI smoke tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: pip install -r requirements.txt

- name: list_platforms flag
run: python mscp.py --list_platforms

- name: baseline list_tags

Check failure on line 37 in .github/workflows/cli-tests.yml

View workflow job for this annotation

GitHub Actions / yamllint

37:33 [trailing-spaces] trailing spaces
run: python mscp.py baseline --list_tags

- name: baseline generate (macos 800-53r5 moderate)
run: >
python mscp.py
--os_name macos --os_version 26.0
--output_dir /tmp/mscp-test-output
baseline --keyword 800-53r5_moderate

- name: baseline generate (macos cis_lvl1)
run: >
python mscp.py
--os_name macos --os_version 26.0
--output_dir /tmp/mscp-test-output
baseline --keyword cis_lvl1

- name: guidance markdown (macos 800-53r5 moderate)
run: >
python mscp.py
--os_name macos --os_version 26.0
--output_dir /tmp/mscp-test-output
guidance --markdown --no-docs
baselines/macos/800-53r5_moderate_macos_26.0.yaml

- name: guidance script (macos cis_lvl1)
run: >
python mscp.py
--os_name macos --os_version 26.0
--output_dir /tmp/mscp-test-output
guidance --script --no-docs
baselines/macos/cis_lvl1_macos_26.0.yaml

- name: scap xccdf (macos 800-53r5 moderate)
run: >
python mscp.py
--os_name macos --os_version 26.0
--output_dir /tmp/mscp-test-output
scap --xccdf --baseline 800-53r5_moderate

- name: admin validate
run: python mscp.py admin validate

cli_tests_success:
needs:
- cli-smoke-tests
if: always()
name: CLI tests successful
runs-on: ubuntu-latest
steps:
- name: Check job status
if: >-
${{
(
contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
)
}}
run: exit 1
21 changes: 13 additions & 8 deletions src/mscp/admin_utils/build_baselines.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
mscp_data,
remove_dir_contents,
)
from ..classes import Macsecurityrule
from ..common_utils import logging_config
from ..classes.rule_library import RuleLibrary
from ..generate import (
generate_baseline,
)
Expand Down Expand Up @@ -52,6 +53,7 @@ def build_all_baselines(args: argparse.Namespace) -> None:
new baseline YAML files into it.
"""
logger.info("Building all supported baselines...")
logging_config.suppress_spinner = True

# clear existing default baselines
baselines_dir = Path(config.get("baseline_dir", ""))
Expand All @@ -75,25 +77,28 @@ def build_all_baselines(args: argparse.Namespace) -> None:
"800-53r5_privacy",
}

all_rules: list[Macsecurityrule] = Macsecurityrule.collect_all_rules(
args.os_name, args.os_version, args.tailor, parent_values="Default"
)

all_tags, benchmark_map = collect_tags_and_benchmarks(all_rules)
library = RuleLibrary.from_rules_dir()
all_tags, benchmark_map = collect_tags_and_benchmarks(list(library))

all_tags[:] = [x for x in all_tags if x not in excluded_tags]

# cache rules per (platform, version) so generate_baseline doesn't re-collect on every call
platform_rules = {
platform: list(library.by_platform(platform).by_os(os_version=float(args.os_version)))
for platform in mscp_data["versions"]["platforms"]
}

# process every discovered benchmark and generate a baseline file
for keyword, platforms in benchmark_map.items():
args.keyword = keyword
for platform in platforms:
args.os_name = platform.lower()
generate_baseline(args, admin=True)
generate_baseline(args, admin=True, preloaded_rules=platform_rules.get(args.os_name, []))

# process every discovered tag and generate a baseline file for every supported platform
for platform in mscp_data["versions"]["platforms"]:
args.os_name = platform

for tag in all_tags:
args.keyword = tag
generate_baseline(args, admin=True)
generate_baseline(args, admin=True, preloaded_rules=platform_rules[platform])
4 changes: 2 additions & 2 deletions src/mscp/classes/legacy_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ def migrate(
)

# 3. Load the current rule library for the target platform/version.
# collect_all_rules assigns each rule its current section, so the
# collect_platform_rules assigns each rule its current section, so the
# resulting baseline will reflect today's section structure.
all_current_rules = Macsecurityrule.collect_all_rules(
all_current_rules = Macsecurityrule.collect_platform_rules(
os_type=os_type,
os_version=int(os_version),
parent_values=self.parent_values or "default",
Expand Down
13 changes: 7 additions & 6 deletions src/mscp/classes/macsecurityrule.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class Macsecurityrule(BaseModelWithAccessors):
The top-level domain object for mSCP. Combines rule metadata (title,
discussion, references), enforcement information (`check`, `fix`,
`mechanism`), and platform / version targeting. Instances are normally
constructed via `load_rules` or `collect_all_rules` rather than
constructed via `load_rules` or `collect_platform_rules` rather than
directly.

Attributes:
Expand Down Expand Up @@ -492,30 +492,31 @@ def load_rules(
return rules

@classmethod
def collect_all_rules(
def collect_platform_rules(
cls,
os_type: str,
os_version: int,
tailoring: bool = False,
parent_values: str = "default",
) -> list["Macsecurityrule"]:
"""Load every rule under ``config["rules_dir"]`` for an OS/version.
"""Load every rule under ``config["rules_dir"]`` for a specific OS type and version.

Walks each subfolder of the rules directory (skipping
``sysprefs``), maps each folder name through `Sectionmap` to the
matching section file, and delegates per-section loading to
`load_rules`.
`load_rules`. Rules that do not declare support for the requested
``os_type`` / ``os_version`` are skipped.

Args:
os_type: Operating system family (e.g. ``"macOS"``).
os_type: Operating system family (e.g. ``"macos"``).
os_version: Operating system version.
tailoring: If true, skips customization overrides. Defaults to
``False``.
parent_values: ODV lookup key forwarded to `load_rules`.
Defaults to ``"default"``.

Returns:
All rules across all sections that match the given platform.
All rules across all sections that match the given platform and version.
"""

logger.info("=== LOADING ALL RULES ===")
Expand Down
4 changes: 2 additions & 2 deletions src/mscp/classes/rule_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def from_rules_dir(cls) -> RuleLibrary:

Reads the platform/version matrix from the bundled
``mscp-data.yaml`` (via ``mscp_data``) and calls
``Macsecurityrule.collect_all_rules`` once per combination. Use
``Macsecurityrule.collect_platform_rules`` once per combination. Use
``by_platform`` or ``by_os`` to narrow the result to a specific
platform.

Expand All @@ -76,7 +76,7 @@ def from_rules_dir(cls) -> RuleLibrary:
if os_version is None:
continue
all_rules.extend(
Macsecurityrule.collect_all_rules(
Macsecurityrule.collect_platform_rules(
os_type=os_type,
os_version=os_version,
)
Expand Down
8 changes: 8 additions & 0 deletions src/mscp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,14 @@ def parse_cli() -> None:
action="store_true",
)

guidance_parser.add_argument(
"--no-docs",
dest="no_docs",
default=False,
help="skip generating the asciidoctor, PDF, and HTML documents",
action="store_true",
)

mapping_parser: argparse.ArgumentParser = subparsers.add_parser(
"mapping",
help="generate custom rules from compliance framework mappings",
Expand Down
3 changes: 2 additions & 1 deletion src/mscp/common_utils/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
``-vv`` / ``--debug``, plus a rotating file sink under ``logs/mscp.log``.
`function_filter` lets developers narrow stderr output to a single
module via the ``MSCP_DEV_FILTER`` environment variable.
The module-level `verbose_logging` flag is read by
The module-level `verbose_logging` and `suppress_spinner` flags are read by
`spinner_utils.conditional_inject_spinner` to decide whether to show a
spinner.
"""
Expand All @@ -23,6 +23,7 @@
from .logger_instance import logger

verbose_logging: bool = False
suppress_spinner: bool = False


def function_filter(record):
Expand Down
32 changes: 22 additions & 10 deletions src/mscp/common_utils/spinner_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@
When verbose logging is active, log lines and a spinner clobber each
other in the terminal. `conditional_inject_spinner` injects the spinner
only when `logging_config.verbose_logging` is false; otherwise it runs
the wrapped function with a stopped spinner so the log output stays
clean.
the wrapped function with a no-op shim so calls like `sp.ok()` and
`sp.text =` are silently swallowed.
"""

import functools
from yaspin import yaspin
from . import logging_config


class _NoOpSpinner:
"""Silent drop-in for a yaspin spinner used when output is suppressed."""

def __setattr__(self, name, value):
pass

def __getattr__(self, name):
return lambda *a, **kw: None


def conditional_inject_spinner(**spinner_kwargs):
"""Decorator factory that injects a `yaspin` spinner only when quiet.

Behaves like `yaspin.inject_spinner` (the spinner is started before
the wrapped call and stopped after), but skips both start and stop
when `logging_config.verbose_logging` is true. The wrapped function
receives the spinner as its first positional argument.
the wrapped call and stopped after), but passes a no-op shim instead
when `logging_config.verbose_logging` or `logging_config.suppress_spinner`
is true, so calls like `sp.ok()` and `sp.text =` inside the wrapped
function produce no output.

Args:
**spinner_kwargs: Keyword arguments forwarded to `yaspin()` to
Expand All @@ -33,16 +44,17 @@ def conditional_inject_spinner(**spinner_kwargs):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
suppress = logging_config.verbose_logging
suppress = logging_config.verbose_logging or logging_config.suppress_spinner

if suppress:
return func(_NoOpSpinner(), *args, **kwargs)

sp = yaspin(**spinner_kwargs)
if not suppress:
sp.start()
sp.start()
try:
return func(sp, *args, **kwargs)
finally:
if not suppress:
sp.stop()
sp.stop()

return wrapper

Expand Down
9 changes: 6 additions & 3 deletions src/mscp/common_utils/validate_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Standard python modules
import argparse
import sys
from pathlib import Path

from jsonschema import Draft202012Validator
Expand Down Expand Up @@ -106,9 +107,11 @@ def validate_yaml_file(args: argparse.Namespace) -> None:
print(f"✅ VALID: {yaml}")
logger.info(f"✅ VALID: {yaml}")

if not error_found:
print(f"✅ All YAML files passed validation.")
logger.success(f"✅ All YAML files passed validation.")
if error_found:
sys.exit(1)

print(f"✅ All YAML files passed validation.")
logger.success(f"✅ All YAML files passed validation.")


def validate_rule_folder_structure(path_str: str) -> Path:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ references:
macos_14:
- CCE-93003-2
visionos_26:
- CCE-95597-1
- CCE-95597-1
800-53r5:
- IA-5
800-171r3:
Expand Down Expand Up @@ -69,7 +69,7 @@ platforms:
visionOS:
'26.0':
supervised: true
introduced: '2.0'
introduced: '2.0'
tags:
- 800-53r5_low
- 800-53r5_moderate
Expand Down
Loading
Loading