From 969b91eff87e7f45db427ae5787e5b18a2188b33 Mon Sep 17 00:00:00 2001 From: chienyee <108117323+chienyee@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:54:06 +0800 Subject: [PATCH 1/2] Updated to support mSCP 2.0+ Additional bug fixes for Python import errors --- .../macos_security_intune_mapper/README.md | 72 +++++++++++++- .../macos_security_intune_mapper/__init__.py | 3 + .../core/baseline_loader.py | 94 +++++++++++++++--- .../core/exporter.py | 4 +- .../core/policy_mapper.py | 6 +- .../core/rules_loader.py | 2 +- .../core/settings_catalog.py | 2 +- .../gui/export_dialog.py | 8 +- .../gui/main_window.py | 18 ++-- .../models/rule.py | 97 ++++++++++++++++++- .../test_baseline_loader.py | 49 ++++++++++ 11 files changed, 314 insertions(+), 41 deletions(-) create mode 100644 macOS/Tools/macos_security_intune_mapper/__init__.py create mode 100644 macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py diff --git a/macOS/Tools/macos_security_intune_mapper/README.md b/macOS/Tools/macos_security_intune_mapper/README.md index 7a6229a..a189373 100644 --- a/macOS/Tools/macos_security_intune_mapper/README.md +++ b/macOS/Tools/macos_security_intune_mapper/README.md @@ -9,10 +9,43 @@ Convert macOS security baselines to Microsoft Intune Settings Catalog policies a - **GUI Application**: User-friendly interface for generating policies - **CLI Tool**: Command-line interface for automation and scripting - **Settings Catalog Mapping**: Automatically maps baseline rules to Intune Settings Catalog format +- **DDM Support**: Supports Declarative Device Management (DDM) settings for modern macOS management - **Mobileconfig Generation**: Creates mobileconfig files for unmapped rules - **Multiple Export Formats**: Combined or split by section/type - **Dependency Analysis**: Identifies and handles rule dependencies - **ODV Resolution**: Automatically resolves Organizational Default Values (ODV) based on the selected baseline +- **Backward Compatible**: Supports both legacy and modern macOS Security Compliance Project structures + +## Baseline Structure Compatibility + +This tool has been updated to support the latest macOS Security Compliance Project baseline structure (2.0+) while maintaining backward compatibility with legacy baselines. + +**Legacy Structure** (pre-2.0): +- Location: `macos_security/baselines/*.yaml` +- Files: `cis_lvl1.yaml`, `800-53r5_high.yaml`, etc. +- Sections: `auditing`, `macos`, `passwordpolicy`, `systemsettings` +- Rule format: Simple dictionary structure + +**New Structure** (2.0+): +- Location: `macos_security/baselines/macos/*.yaml` +- Files: `cis_lvl1_macos_26.0.yaml`, `800-53r5_high_macos_26.0.yaml`, etc. +- Sections: `Auditing`, `Operating System`, `Password Policy`, `System Settings` +- Platform subdirectories: `macos/`, `ios/`, `visionos/` +- Rule format: Enhanced with platform-specific configurations and DDM support +- Enforcement details: Nested under `platforms.macOS..enforcement_info` + +**Automatic Detection**: The tool automatically detects which structure is in use by checking for the `baselines/macos/` subdirectory. It normalizes section names and file naming patterns internally, so you'll see clean baseline names (e.g., `cis_lvl1`) in the interface regardless of the source structure. + +## Recent Updates + +**v2.0 Compatibility (2026-07)**: +- ✅ Support for macOS Security Compliance Project 2.0+ structure +- ✅ Automatic detection and handling of platform-specific baseline directories (`macos/`, `ios/`, `visionos/`) +- ✅ Enhanced rule parsing for new YAML format with platform-specific configurations +- ✅ DDM (Declarative Device Management) support for modern macOS management +- ✅ Improved mobileconfig generation with new PayloadType/PayloadContent structure +- ✅ Backward compatibility maintained for legacy baselines +- ✅ Section name normalization (automatic mapping of old/new section names) ## Installation @@ -164,11 +197,24 @@ macos_security_intune_mapper/ External dependency (required): ``` -macos_security/ # downloaded from github.com/usnistgov/macos_security -├── baselines/ # YAML baseline definitions -└── rules/ # Individual rule definitions +macos_security/ # Downloaded from github.com/usnistgov/macos_security +├── baselines/ # YAML baseline definitions +│ ├── macos/ # macOS baselines (v2.0+) +│ │ ├── cis_lvl1_macos_26.0.yaml +│ │ ├── cis_lvl2_macos_26.0.yaml +│ │ └── ... +│ ├── ios/ # iOS baselines (v2.0+) +│ └── visionos/ # visionOS baselines (v2.0+) +└── rules/ # Individual rule definitions + ├── audit/ + ├── auth/ + ├── os/ + ├── pwpolicy/ + └── system_settings/ ``` +**Note**: For legacy structure (pre-2.0), baseline files are directly in `baselines/` without platform subdirectories. + ## Output Formats ### Settings Catalog JSON @@ -244,7 +290,11 @@ Both GUI and CLI generate logs in `macos_security_intune_mapper.log` with detail ### "Baseline not found" -Make sure the `macos_security` folder with baselines is in the correct location. Use `--list` to see available baselines. +Make sure the `macos_security` folder with baselines is in the correct location. The tool supports both structures: +- **New structure**: `macos_security/baselines/macos/*.yaml` (automatically detected) +- **Legacy structure**: `macos_security/baselines/*.yaml` + +Check that baseline files exist in the expected location. For the new structure, ensure files are in the `macos/` subdirectory. ### "Settings Catalog file not found" @@ -265,6 +315,20 @@ Intune Settings Catalog updates regularly. To refresh `settingsCatalog.json` wit 2. Save the response to `settingsCatalog.json` 3. Re-run the tool with updated settings +### Fewer Policies Mapped Than Expected + +If you're seeing fewer policies mapped after switching to the new macos_security structure (2.0+): +1. Ensure you're using the latest version of this tool (updated for 2.0+ compatibility) +2. The new rule format includes DDM settings which may map differently than legacy mobileconfig settings +3. Check the logs (`macos_security_intune_mapper.log`) for detailed mapping information +4. Some rules may now prefer DDM over mobileconfig, affecting the distribution of mapped vs. unmapped rules + +### macOS Security Version Compatibility + +- **Legacy baselines** (pre-2.0): Fully supported +- **New baselines** (2.0+): Fully supported with automatic detection +- **Mixed usage**: The tool can work with either structure but not both simultaneously (point to one macos_security folder) + ## License See LICENSE file for details. diff --git a/macOS/Tools/macos_security_intune_mapper/__init__.py b/macOS/Tools/macos_security_intune_mapper/__init__.py new file mode 100644 index 0000000..811d07f --- /dev/null +++ b/macOS/Tools/macos_security_intune_mapper/__init__.py @@ -0,0 +1,3 @@ +"""macOS Intune Mapper - Convert macOS security baselines to Intune policies.""" + +__version__ = "1.0.0" diff --git a/macOS/Tools/macos_security_intune_mapper/core/baseline_loader.py b/macOS/Tools/macos_security_intune_mapper/core/baseline_loader.py index a00b37a..d7b62fc 100644 --- a/macOS/Tools/macos_security_intune_mapper/core/baseline_loader.py +++ b/macOS/Tools/macos_security_intune_mapper/core/baseline_loader.py @@ -7,11 +7,27 @@ from pathlib import Path from typing import List, Optional, Dict, Any -from ..models.baseline import Baseline, BaselineSection +from models.baseline import Baseline, BaselineSection logger = logging.getLogger(__name__) +# Section name mapping: old format -> new format +# The new baseline structure (macos_security 2.0+) uses different section names +SECTION_NAME_MAPPING = { + "auditing": "Auditing", + "macos": "Operating System", + "passwordpolicy": "Password Policy", + "systemsettings": "System Settings", + "Supplemental": "Supplemental", # Unchanged + # Add reverse mappings for normalization + "Auditing": "Auditing", + "Operating System": "Operating System", + "Password Policy": "Password Policy", + "System Settings": "System Settings", +} + + class BaselineLoader: """Loads macOS security baselines from YAML files.""" @@ -22,19 +38,32 @@ def __init__(self, macos_security_path: Optional[str] = None): macos_security_path: Path to macos_security folder. If None, uses default. """ if macos_security_path: - self.baselines_path = Path(macos_security_path) / "baselines" + baselines_root = Path(macos_security_path) / "baselines" + # Check for new structure with macos/ subdirectory + macos_baselines = baselines_root / "macos" + if macos_baselines.exists(): + self.baselines_path = macos_baselines + else: + # Fall back to old structure (baselines/ directly) + self.baselines_path = baselines_root else: # Try to find macos_security folder current = Path(__file__).parent while current.parent != current: macos_security = current / "macos_security" if macos_security.exists() and (macos_security / "baselines").exists(): - self.baselines_path = macos_security / "baselines" + baselines_root = macos_security / "baselines" + # Check for new structure + macos_baselines = baselines_root / "macos" + if macos_baselines.exists(): + self.baselines_path = macos_baselines + else: + self.baselines_path = baselines_root break current = current.parent else: - # Default to relative path - self.baselines_path = Path("macos_security/baselines") + # Default to relative path (try new structure first) + self.baselines_path = Path("macos_security/baselines/macos") self._baselines_cache: Dict[str, Baseline] = {} logger.info(f"Baseline loader initialized with path: {self.baselines_path}") @@ -43,7 +72,7 @@ def list_baselines(self) -> List[str]: """List all available baselines. Returns: - List of baseline names (without .yaml extension) + List of baseline names (without .yaml extension and version suffix) """ if not self.baselines_path.exists(): logger.warning(f"Baselines path does not exist: {self.baselines_path}") @@ -51,8 +80,20 @@ def list_baselines(self) -> List[str]: baselines = [] for baseline_file in self.baselines_path.glob("*.yaml"): - if baseline_file.name != "all_rules.yaml": # Skip the all_rules baseline - baselines.append(baseline_file.stem) + if baseline_file.name != "all_rules.yaml" and not baseline_file.name.startswith("all_rules_"): + # Strip .yaml extension + basename = baseline_file.stem + + # For new structure, strip version suffix (e.g., _macos_26.0) + # Pattern: basename_platform_version (e.g., cis_lvl1_macos_26.0) + if "_macos_" in basename or "_ios_" in basename or "_visionos_" in basename: + # Find the last occurrence of platform suffix and remove it + parts = basename.rsplit("_", 2) # Split from right: ['cis_lvl1', 'macos', '26.0'] + if len(parts) == 3 and parts[1] in ["macos", "ios", "visionos"]: + basename = parts[0] # Use the baseline name without platform/version + + if basename not in baselines: + baselines.append(basename) return sorted(baselines) @@ -60,7 +101,7 @@ def load_baseline(self, baseline_name: str) -> Baseline: """Load a baseline from YAML file. Args: - baseline_name: Name of the baseline (without .yaml extension) + baseline_name: Name of the baseline (without .yaml extension and version suffix) Returns: Baseline object @@ -74,10 +115,27 @@ def load_baseline(self, baseline_name: str) -> Baseline: logger.debug(f"Returning cached baseline: {baseline_name}") return self._baselines_cache[baseline_name] - baseline_file = self.baselines_path / f"{baseline_name}.yaml" + # Try to find the baseline file with different naming patterns + baseline_file = None - if not baseline_file.exists(): - raise FileNotFoundError(f"Baseline file not found: {baseline_file}") + # Pattern 1: New structure with platform/version (e.g., cis_lvl1_macos_26.0.yaml) + for pattern in [f"{baseline_name}_macos_*.yaml", f"{baseline_name}_ios_*.yaml", f"{baseline_name}_visionos_*.yaml"]: + matching_files = list(self.baselines_path.glob(pattern)) + if matching_files: + # If multiple matches, use the first one (typically latest version) + baseline_file = matching_files[0] + logger.debug(f"Found baseline using pattern {pattern}: {baseline_file.name}") + break + + # Pattern 2: Old structure (e.g., cis_lvl1.yaml) + if not baseline_file: + old_pattern = self.baselines_path / f"{baseline_name}.yaml" + if old_pattern.exists(): + baseline_file = old_pattern + logger.debug(f"Found baseline using old naming: {baseline_file.name}") + + if not baseline_file or not baseline_file.exists(): + raise FileNotFoundError(f"Baseline file not found: {baseline_name} in {self.baselines_path}") logger.info(f"Loading baseline from: {baseline_file}") @@ -99,3 +157,15 @@ def load_baseline(self, baseline_name: str) -> Baseline: raise ValueError(f"Invalid YAML in baseline file: {e}") except Exception as e: raise ValueError(f"Failed to load baseline: {e}") + + @staticmethod + def normalize_section_name(section_name: str) -> str: + """Normalize section names from old to new format. + + Args: + section_name: Original section name (old or new format) + + Returns: + Normalized section name in new format + """ + return SECTION_NAME_MAPPING.get(section_name, section_name) diff --git a/macOS/Tools/macos_security_intune_mapper/core/exporter.py b/macOS/Tools/macos_security_intune_mapper/core/exporter.py index 7903f87..b89579c 100644 --- a/macOS/Tools/macos_security_intune_mapper/core/exporter.py +++ b/macOS/Tools/macos_security_intune_mapper/core/exporter.py @@ -8,8 +8,8 @@ from typing import List, Dict, Any, Set, Tuple from pathlib import Path -from ..models.policy import Policy, PolicySetting, MobileConfigPolicy -from ..models.rule import Rule +from models.policy import Policy, PolicySetting, MobileConfigPolicy +from models.rule import Rule logger = logging.getLogger(__name__) diff --git a/macOS/Tools/macos_security_intune_mapper/core/policy_mapper.py b/macOS/Tools/macos_security_intune_mapper/core/policy_mapper.py index 19b17c6..d841a29 100644 --- a/macOS/Tools/macos_security_intune_mapper/core/policy_mapper.py +++ b/macOS/Tools/macos_security_intune_mapper/core/policy_mapper.py @@ -5,9 +5,9 @@ import logging from typing import Optional, Dict, Any -from .settings_catalog import SettingsCatalog -from ..models.rule import Rule -from ..models.policy import Policy, PolicySetting, PolicyType +from core.settings_catalog import SettingsCatalog +from models.rule import Rule +from models.policy import Policy, PolicySetting, PolicyType logger = logging.getLogger(__name__) diff --git a/macOS/Tools/macos_security_intune_mapper/core/rules_loader.py b/macOS/Tools/macos_security_intune_mapper/core/rules_loader.py index dde4a7a..8b627d2 100644 --- a/macOS/Tools/macos_security_intune_mapper/core/rules_loader.py +++ b/macOS/Tools/macos_security_intune_mapper/core/rules_loader.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Dict, List, Optional -from ..models.rule import Rule +from models.rule import Rule logger = logging.getLogger(__name__) diff --git a/macOS/Tools/macos_security_intune_mapper/core/settings_catalog.py b/macOS/Tools/macos_security_intune_mapper/core/settings_catalog.py index f1fe960..1a77221 100644 --- a/macOS/Tools/macos_security_intune_mapper/core/settings_catalog.py +++ b/macOS/Tools/macos_security_intune_mapper/core/settings_catalog.py @@ -61,7 +61,7 @@ def from_intune(cls, access_token: str) -> "SettingsCatalog": Returns: SettingsCatalog instance """ - from ..utils.intune_api import IntuneAPI + from utils.intune_api import IntuneAPI logger.info("Fetching Settings Catalog from Intune") diff --git a/macOS/Tools/macos_security_intune_mapper/gui/export_dialog.py b/macOS/Tools/macos_security_intune_mapper/gui/export_dialog.py index 2d7795e..aa478be 100644 --- a/macOS/Tools/macos_security_intune_mapper/gui/export_dialog.py +++ b/macOS/Tools/macos_security_intune_mapper/gui/export_dialog.py @@ -7,10 +7,10 @@ from pathlib import Path from typing import List, Optional -from ..core.exporter import IntuneExporter -from ..models.policy import Policy -from ..models.rule import Rule -from ..models.baseline import Baseline +from core.exporter import IntuneExporter +from models.policy import Policy +from models.rule import Rule +from models.baseline import Baseline logger = logging.getLogger(__name__) diff --git a/macOS/Tools/macos_security_intune_mapper/gui/main_window.py b/macOS/Tools/macos_security_intune_mapper/gui/main_window.py index 718bc76..572fce5 100644 --- a/macOS/Tools/macos_security_intune_mapper/gui/main_window.py +++ b/macOS/Tools/macos_security_intune_mapper/gui/main_window.py @@ -8,14 +8,14 @@ from dataclasses import dataclass from typing import Optional, Dict, Any, List -from ..core.baseline_loader import BaselineLoader -from ..core.rules_loader import RulesLoader -from ..core.settings_catalog import SettingsCatalog -from ..core.policy_mapper import PolicyMapper -from ..models.baseline import Baseline -from ..models.rule import Rule -from ..models.policy import Policy, PolicySetting -from .export_dialog import ExportDialog +from core.baseline_loader import BaselineLoader +from core.rules_loader import RulesLoader +from core.settings_catalog import SettingsCatalog +from core.policy_mapper import PolicyMapper +from models.baseline import Baseline +from models.rule import Rule +from models.policy import Policy, PolicySetting +from gui.export_dialog import ExportDialog logger = logging.getLogger(__name__) @@ -1241,7 +1241,7 @@ def _apply_value_overrides_to_policy(self, policy: Policy, settings: List[Settin New Policy instance with overrides applied """ from copy import deepcopy - from ..models.policy import PolicySetting + from models.policy import PolicySetting # Create a deep copy of the policy policy_copy = deepcopy(policy) diff --git a/macOS/Tools/macos_security_intune_mapper/models/rule.py b/macOS/Tools/macos_security_intune_mapper/models/rule.py index 9c8747e..2c72abf 100644 --- a/macOS/Tools/macos_security_intune_mapper/models/rule.py +++ b/macOS/Tools/macos_security_intune_mapper/models/rule.py @@ -54,27 +54,114 @@ def from_dict(cls, data: Dict[str, Any]) -> "Rule": except ValueError: severity = Severity.MEDIUM + # Normalize mobileconfig_info from new format to old format + mobileconfig_info = cls._normalize_mobileconfig_info(data.get("mobileconfig_info", {})) + + # Extract check, result, fix from new structure if not at top level + check = data.get("check", "") + result = data.get("result", {}) + fix = data.get("fix", "") + + # If not found at top level, try to extract from platforms section + if not check and "platforms" in data: + platforms = data["platforms"] + if "macOS" in platforms: + macos_platforms = platforms["macOS"] + # Get enforcement_info from the first macOS version or from common enforcement_info + enforcement_info = macos_platforms.get("enforcement_info") + if not enforcement_info: + # Try first version + for version_key, version_data in macos_platforms.items(): + if isinstance(version_data, dict) and "enforcement_info" in version_data: + enforcement_info = version_data["enforcement_info"] + break + + if enforcement_info: + check_info = enforcement_info.get("check", {}) + if isinstance(check_info, dict): + check = check_info.get("shell", "") + result = check_info.get("result", {}) + fix = enforcement_info.get("fix", {}).get("shell", "") if isinstance(enforcement_info.get("fix"), dict) else "" + return cls( id=data.get("id", ""), title=data.get("title", ""), discussion=data.get("discussion", ""), - check=data.get("check", ""), - result=data.get("result", {}), - fix=data.get("fix", ""), + check=check, + result=result, + fix=fix, references=data.get("references", {}), macos_versions=data.get("macOS", []), tags=data.get("tags", []), severity=severity, mobileconfig=data.get("mobileconfig", False), - mobileconfig_info=data.get("mobileconfig_info", {}), + mobileconfig_info=mobileconfig_info, ddm_info=data.get("ddm_info", {}), odv=data.get("odv") ) + @staticmethod + def _normalize_mobileconfig_info(mobileconfig_info: Union[Dict, List]) -> Dict[str, Any]: + """Normalize mobileconfig_info from new format to old format. + + New format (list): + - PayloadType: com.apple.security.firewall + PayloadContent: + - EnableFirewall: true + - EnableStealthMode: true + + Old format (dict): + com.apple.security.firewall: + EnableFirewall: true + EnableStealthMode: true + + Args: + mobileconfig_info: Either list (new format) or dict (old format) + + Returns: + Normalized dict in old format + """ + if not mobileconfig_info: + return {} + + # If already a dict, return as-is (old format or already normalized) + if isinstance(mobileconfig_info, dict): + return mobileconfig_info + + # If it's a list, convert from new format to old format + if isinstance(mobileconfig_info, list): + normalized = {} + for payload in mobileconfig_info: + if not isinstance(payload, dict): + continue + + payload_type = payload.get('PayloadType') + payload_content = payload.get('PayloadContent', []) + + if not payload_type: + continue + + # Convert PayloadContent list to dict + settings = {} + if isinstance(payload_content, list): + for item in payload_content: + if isinstance(item, dict): + settings.update(item) + elif isinstance(payload_content, dict): + settings = payload_content + + if settings: + normalized[payload_type] = settings + + return normalized + + return {} + @property def has_mobileconfig(self) -> bool: """Check if rule has mobileconfig information.""" - return self.mobileconfig and bool(self.mobileconfig_info) + # In new format, mobileconfig flag may not exist, so check mobileconfig_info directly + return bool(self.mobileconfig_info) or (self.mobileconfig and bool(self.mobileconfig_info)) @property def has_ddm(self) -> bool: diff --git a/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py b/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py new file mode 100644 index 0000000..550f940 --- /dev/null +++ b/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Test script to verify baseline loader works with new structure.""" + +import sys +from pathlib import Path + +# Add parent to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +# Import after path is set +from shell_intune_samples.macOS.Tools.macos_security_intune_mapper.core.baseline_loader import BaselineLoader + +def test_baseline_detection(): + """Test that baselines can be detected from new structure.""" + print("Testing Baseline Loader with new structure...") + print("=" * 60) + + # Test with new structure + loader = BaselineLoader(r"c:\CAT\gitrepos\macos_security") + + print(f"\nBaseline path: {loader.baselines_path}") + print(f"Path exists: {loader.baselines_path.exists()}") + + # List baselines + baselines = loader.list_baselines() + + print(f"\nFound {len(baselines)} baselines:") + for baseline in sorted(baselines): + print(f" - {baseline}") + + # Test loading a specific baseline + if "cis_lvl1" in baselines: + print(f"\nTesting load of 'cis_lvl1' baseline...") + try: + baseline = loader.load_baseline("cis_lvl1") + print(f"✓ Successfully loaded: {baseline.name}") + print(f" Title: {baseline.title}") + print(f" Rules: {len(baseline.get_all_rules())}") + print(f" Sections: {len(baseline.profile)}") + for section in baseline.profile[:3]: + print(f" - {section.section}: {len(section.rules)} rules") + except Exception as e: + print(f"✗ Failed to load baseline: {e}") + + print("\n" + "=" * 60) + print("Test complete!") + +if __name__ == "__main__": + test_baseline_detection() From 5555902e727d651a073f508d71ac8f5dd90850bf Mon Sep 17 00:00:00 2001 From: chienyee <108117323+chienyee@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:55:45 +0800 Subject: [PATCH 2/2] Removed extra file. --- .../test_baseline_loader.py | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py diff --git a/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py b/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py deleted file mode 100644 index 550f940..0000000 --- a/macOS/Tools/macos_security_intune_mapper/test_baseline_loader.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -"""Test script to verify baseline loader works with new structure.""" - -import sys -from pathlib import Path - -# Add parent to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) - -# Import after path is set -from shell_intune_samples.macOS.Tools.macos_security_intune_mapper.core.baseline_loader import BaselineLoader - -def test_baseline_detection(): - """Test that baselines can be detected from new structure.""" - print("Testing Baseline Loader with new structure...") - print("=" * 60) - - # Test with new structure - loader = BaselineLoader(r"c:\CAT\gitrepos\macos_security") - - print(f"\nBaseline path: {loader.baselines_path}") - print(f"Path exists: {loader.baselines_path.exists()}") - - # List baselines - baselines = loader.list_baselines() - - print(f"\nFound {len(baselines)} baselines:") - for baseline in sorted(baselines): - print(f" - {baseline}") - - # Test loading a specific baseline - if "cis_lvl1" in baselines: - print(f"\nTesting load of 'cis_lvl1' baseline...") - try: - baseline = loader.load_baseline("cis_lvl1") - print(f"✓ Successfully loaded: {baseline.name}") - print(f" Title: {baseline.title}") - print(f" Rules: {len(baseline.get_all_rules())}") - print(f" Sections: {len(baseline.profile)}") - for section in baseline.profile[:3]: - print(f" - {section.section}: {len(section.rules)} rules") - except Exception as e: - print(f"✗ Failed to load baseline: {e}") - - print("\n" + "=" * 60) - print("Test complete!") - -if __name__ == "__main__": - test_baseline_detection()