Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ endif::[]

This document provides a high-level view of the changes to the macOS Security Compliance Project.

== [Unreleased]

=== Scripts
* Add `--markdown-tree` flag to `mscp guidance` - generates a paginated Markdown directory tree (one page per rule, `NN-` ordered filenames, `index.md` per section) ready to use with Docusaurus, Starlight, MkDocs, VitePress, or any CommonMark-based static site generator. Drop the output directory into a docs folder - no post-processing required. Also included in `--all`.

=== Bug Fixes
* Fix crash in `adoc/rule.adoc.jinja` when `rule.references.hhs` is absent - rules predating the HICP framework do not carry this key.

== [mSCP 2.x] - 2025-02-28
IMPORTANT: This release is a major update and includes breaking changes. Please review the documentation before upgrading.

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ packages = ["src/mscp"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "--import-mode=importlib"
2 changes: 1 addition & 1 deletion src/mscp/classes/macsecurityrule.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# Additional python modules
from lxml import etree
from pydantic import BaseModel, ConfigDict, Field
from pydantic import ConfigDict, Field

# Local python modules
from ._base import BaseModelWithAccessors
Expand Down
7 changes: 7 additions & 0 deletions src/mscp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ def parse_cli() -> None:
help="generate documentation in markdown format",
action="store_true",
)
guidance_parser.add_argument(
"--markdown-tree",
help="""R|generate documentation as a paginated Markdown directory tree
(one page per rule, NN- ordered filenames, index.md per section)
ready to use with Docusaurus, Starlight, MkDocs, or VitePress""",
action="store_true",
)
guidance_parser.add_argument(
"-p",
"--profiles",
Expand Down
11 changes: 9 additions & 2 deletions src/mscp/common_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
(`conditional_inject_spinner`).
"""

from .config import config, set_custom_dir, ensure_custom_dirs, search_paths
from .config import (
config,
CONFIG_PATH,
set_custom_dir,
ensure_custom_dirs,
search_paths,
)
from .constants import SCHEMA_PATH, APPLE_OS, NIX_OS, PLATFORM_MAP
from .customization import collect_overrides
from .file_handling import (
Expand Down Expand Up @@ -77,7 +83,8 @@
"SCHEMA_PATH",
"APPLE_OS",
"NIX_OS",
"PLATFORM_MAPvalidate_yaml_file",
"PLATFORM_MAP",
"validate_yaml_file",
"logger",
"get_supported_languages",
"collect_overrides",
Expand Down
90 changes: 90 additions & 0 deletions src/mscp/data/templates/documents/markdown/rule.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
{% set additional_info = rule | get_nested(["platforms", rule.os_type, "enforcement_info", "fix", "additional_info"]) %}
{% set check_shell = rule | get_nested(["platforms", rule.os_type, "enforcement_info", "check", "shell"]) %}
{% set fix_shell = rule | get_nested(["platforms", rule.os_type, "enforcement_info", "fix", "shell"]) %}
{% if not markdown_tree | default(false) %}

### {{ rule.title }}

{% endif %}
{% if rule.tags is defined or rule.tags is not none and "supplemental" in rule.tags %}
{{ rule.discussion | include_replace | asciidoc_to_markdown }}
{% else %}
Expand All @@ -18,6 +20,10 @@
{% if not rule.tags | select('in', check_tags) | list %}
{% if rule.tags is not defined or rule.tags is none or "supplemental" not in rule.tags %}
{% if rule.os_type.lower() in NIX_OS %}
{% if markdown_tree | default(false) %}
## {% trans %}Check{% endtrans %}

{% endif %}
{% trans %}To check the state of the system, run the following command(s){% endtrans %}:

```bash
Expand All @@ -35,6 +41,87 @@
{% trans %}If the result is not{% endtrans %} _{{ rule.result_value }}_, {% trans %}this is a finding{% endtrans %}.
{% endif %}

{% if markdown_tree | default(false) %}
## {% trans %}Remediation Description{% endtrans %}

{% if rule.fix is not none %}
{% trans %}Perform the following to configure the system to meet the requirements{% endtrans %}:
{% endif %}

{% if rule.mobileconfig_info is none and rule.fix is not none or fix_shell %}
{% trans %}Run the following command(s){% endtrans %}:

```bash
{{ rule.fix | asciidoc_to_markdown if rule.fix is not none else fix_shell }}
```

{% elif additional_info is not none %}
{{ additional_info | asciidoc_to_markdown }}
{% elif rule.mobileconfig_info is not none and fix_shell %}
{% if fix_shell %}
```bash
{{ fix_shell -}}
```
{% endif %}

{% if rule.mobileconfig_info %}
```xml
{{ rule.mobileconfig_info | mobileconfig_payloads_to_xml -}}
```
{% endif %}
{% else %}
{% if rule.os_name == "macos" %}
{{ rule.fix | asciidoc_to_markdown }}
{% else %}
{% trans %}Deploy a configuration profile containing the following payload.{% endtrans %}

```xml
{{ rule.mobileconfig_info | mobileconfig_payloads_to_xml -}}
```
{% endif %}
{% endif %}

## {% trans %}References{% endtrans %}

| | |
|---|---|
| **ID** | `{{ rule.rule_id }}` |
{% if rule.severity is not none %}
| **Severity** | {{ rule.severity }} |
{% endif %}
| **800-53r5** | {{ rule.references.nist.nist_800_53r5 | group_ulify if rule.references.nist.nist_800_53r5 is not none }} |
{% if "800-171" in baseline.title | upper or show_all_tags %}
| **800-171r3** | {{ rule.references.nist.nist_800_171r3 | render_rules if rule.references.nist.nist_800_171r3 is not none }} |
{% endif %}
{% if "STIG" in baseline.title | upper or show_all_tags %}
| **DISA STIG(s)** | {{ rule.references.disa.disa_stig | render_rules if rule.references.disa.disa_stig is not none }} |
{% if rule.references.disa.sfr is not none %}
| **SFR** | {{ rule.references.disa.sfr | render_rules if rule.references.disa.sfr is not none }} |
{% endif %}
{% endif %}
{% if "CIS" in baseline.title | upper or show_all_tags %}
| **CIS Benchmark** | {{ rule.references.cis.benchmark | render_rules if rule.references.cis.benchmark is not none }} |
| **CIS Controls V8** | {{ rule.references.cis.controls_v8 | render_rules if rule.references.cis.controls_v8 is not none }} |
{% endif %}
{% if "INDIGO" in baseline.title | upper or show_all_tags %}
| **indigo** | {{ rule.references.bsi.indigo | render_rules if rule.references.bsi.indigo is not none }} |
{% endif %}
{% if "CMMC" in baseline.title | upper or show_all_tags %}
| **CMMC** | {{ rule.references.disa.cmmc | render_rules if rule.references.disa.cmmc is not none }} |
{% endif %}
{% if "HICP_LP" in baseline.title | upper or show_all_tags %}
| **HICP** | {{ rule.references.hhs.hicp | render_rules if rule.references.hhs.hicp is not none }} |
{% endif %}
{% if rule.references.nist.cce is not none %}
| **CCE** | {{ rule.references.nist.cce | render_rules }} |
{% endif %}
{% if "references" in rule.customized %}
| **Custom References** | {{ rule.references.custom_refs.references | render_references if rule.references.custom_refs is not none }} |
{% endif %}
{% if show_all_tags %}
| **TAGS** | {{ rule.tags | render_rules }} |
{% endif %}
{% else %}
<table class="remediation">
<tr>
<td>
Expand Down Expand Up @@ -84,6 +171,7 @@
</table>
{% endif %}

{% if not markdown_tree | default(false) %}
<table class="outer-table" border="1">
<tr>
<td> ID </td>
Expand Down Expand Up @@ -170,3 +258,5 @@
</tr>
</table>
{% endif %}
{% endif %}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% include "foreword.jinja" %}

{% include "scope.jinja" %}

{% include "authors.jinja" %}

{% include "acronyms.jinja" %}
65 changes: 46 additions & 19 deletions src/mscp/generate/guidance.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from ..generate.guidance_support import (
generate_ddm,
generate_documents,
generate_markdown_tree,
generate_excel,
generate_profiles,
generate_script,
Expand Down Expand Up @@ -155,8 +156,9 @@ def generate_guidance(sp: Yaspin, args: argparse.Namespace) -> None:
args (argparse.Namespace): Parsed CLI arguments. Expected attributes:
``baseline``, ``os_name``, ``language``, ``dark``, ``hash``,
``reference``, ``logo``, ``audit_name``, ``profiles``, ``ddm``,
``script``, ``xlsx``, ``gary``, ``markdown``, ``manifest``,
``all``, ``consolidated_profile``, ``granular_profiles``.
``script``, ``xlsx``, ``gary``, ``markdown``, ``markdown_tree``,
``manifest``, ``all``, ``consolidated_profile``,
``granular_profiles``.
"""
# Transparently migrate legacy (pre-2.0) baselines before deriving any
# paths. Updating args.baseline here means all subsequent path derivations
Expand Down Expand Up @@ -329,6 +331,18 @@ def generate_guidance(sp: Yaspin, args: argparse.Namespace) -> None:
language=args.language,
)

if args.markdown_tree:
logger.info("Generating paginated Markdown tree")
sp.text = "Generating Markdown tree"
time.sleep(1)
generate_markdown_tree(
build_path,
baseline,
current_version_data,
show_all_tags,
language=args.language,
)

if args.manifest:
logger.info("Generating JSON manifest")
sp.text = "Generating JSON manifest"
Expand Down Expand Up @@ -380,23 +394,36 @@ def generate_guidance(sp: Yaspin, args: argparse.Namespace) -> None:
time.sleep(1)
generate_excel(spreadsheet_output_file, baseline)

logger.info("Generating markdown documents")
sp.text = "Generating markdown"
time.sleep(1)
generate_documents(
sp,
md_output_file,
baseline,
b64logo,
pdf_theme,
html_css,
logo_path,
baseline.platform["os"],
current_version_data,
show_all_tags,
output_format="markdown",
language=args.language,
)
if not args.markdown:
logger.info("Generating markdown documents")
sp.text = "Generating markdown"
time.sleep(1)
generate_documents(
sp,
md_output_file,
baseline,
b64logo,
pdf_theme,
html_css,
logo_path,
baseline.platform["os"],
current_version_data,
show_all_tags,
output_format="markdown",
language=args.language,
)

if not args.markdown_tree:
logger.info("Generating paginated Markdown tree")
sp.text = "Generating Markdown tree"
time.sleep(1)
generate_markdown_tree(
build_path,
baseline,
current_version_data,
show_all_tags,
language=args.language,
)

logger.info("Generating JSON manifest")
sp.text = "Generating JSON manifest"
Expand Down
5 changes: 4 additions & 1 deletion src/mscp/generate/guidance_support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"""Guidance artifact sub-generators used by `generate_guidance`.

Re-exports: `generate_ddm` (DDM JSON/ZIP artifacts), `generate_documents`
(AsciiDoc / PDF / HTML / Markdown), `generate_excel` (Excel workbook),
(AsciiDoc / PDF / HTML / Markdown), `generate_markdown_tree` (paginated
Markdown tree for static site generators), `generate_excel` (Excel workbook),
`generate_profiles` (configuration profiles), `generate_script` and
`generate_restore_script` (compliance shell scripts), and
`generate_manifest` (JSON manifest).
Expand All @@ -11,6 +12,7 @@
__all__ = [
"generate_ddm",
"generate_documents",
"generate_markdown_tree",
"generate_excel",
"generate_profiles",
"generate_script",
Expand All @@ -21,6 +23,7 @@

from .ddm import generate_ddm
from .documents import generate_documents
from .markdown_tree import generate_markdown_tree
from .excel import generate_excel
from .profiles import generate_profiles
from .script import generate_script, generate_restore_script
Expand Down
Loading
Loading