diff --git a/docs/manual/developer/05_tools_and_utilities.md b/docs/manual/developer/05_tools_and_utilities.md index 1bcc8220659..64d03b0b014 100644 --- a/docs/manual/developer/05_tools_and_utilities.md +++ b/docs/manual/developer/05_tools_and_utilities.md @@ -112,6 +112,19 @@ Example $ ./utils/build_stig_control.py -p rhel8 -m shared/references/disa-stig-rhel8-v1r5-xccdf-manual.xml ``` + +## Generating Controls From a Reference +When converting profile to use a control file this script can be helpful in creating the skeleton control. +The output of this script will need to be adjusted to add other keys such as title or description to the controls. +This script does require that `./utils/rule_dir_json.py` be run before this script is used. +See `./utils/build_control_from_reference.py --help` for the full set options the script provides. + + +Example +```bash + $ ./utils/build_control_from_reference.py --product rhel10 --reference ospp --output controls/ospp.yml +``` + ## Generating login banner regular expressions Rules like `banner_etc_issue` and `dconf_gnome_login_banner_text` will diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b074241aaea..5b1778f92d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -107,6 +107,7 @@ mypy_test("utils/import_srg_spreadsheet.py" "skip") mypy_test("utils/check_eof.py" "normal") mypy_test("utils/import_disa_stig.py" "skip") mypy_test("tests/cces-removed.py" "normal") +mypy_test("utils/build_control_from_reference.py" "skip") if(PYTHON_VERSION_MAJOR GREATER 2 AND PYTHON_VERSION_MINOR GREATER 7 AND PY_TRESTLE AND PY_LXML) mypy_test("utils/oscal/" "skip") @@ -366,3 +367,12 @@ if(PYTHON_VERSION_MAJOR GREATER 2 AND PY_GITHUB) ) endif() endif() + +if(PYTHON_VERSION_MAJOR GREATER 2) + add_test( + NAME "utils-build_control_from_reference_sanity" + COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/utils/build_control_from_reference.py" "--product" "rhel10" "--reference" "ospp" "--root" "${CMAKE_SOURCE_DIR}" "--output" "${CMAKE_SOURCE_DIR}/build/rhel10_ospp_control.yml" "--json" "${CMAKE_SOURCE_DIR}/build/rule_dirs.json" + ) + set_tests_properties("utils-build_control_from_reference_sanity" PROPERTIES FIXTURES_REQUIRED "rule-dir-json") + set_tests_properties("utils-build_control_from_reference_sanity" PROPERTIES DEPENDS "test-rule-dir-json") +endif() diff --git a/utils/build_control_from_reference.py b/utils/build_control_from_reference.py new file mode 100755 index 00000000000..6ff54d41ec9 --- /dev/null +++ b/utils/build_control_from_reference.py @@ -0,0 +1,102 @@ +#!/usr/bin/python3 + +import argparse +from collections import defaultdict +import os +import json +import sys +from typing import List, Dict +import yaml + + +import ssg.environment +import ssg.yaml + +SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +BUILD_ROOT = os.path.join(SSG_ROOT, "build") +RULES_JSON = os.path.join(BUILD_ROOT, "rule_dirs.json") +BUILD_CONFIG = os.path.join(BUILD_ROOT, "build_config.yml") + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Given a reference this script will create a control file.") + parser.add_argument("-j", "--json", type=str, + help=f"Path to the rule_dirs.json file. Defaults to {RULES_JSON}", + default=RULES_JSON) + parser.add_argument("-p", "--product", type=str, help="Product to build the control with", + required=True) + parser.add_argument("-r", "--root", type=str, + help=f"Path to the root of the project. Defaults to {SSG_ROOT}", + default=SSG_ROOT) + parser.add_argument("-ref", "--reference", type=str, + help="Reference to use for the profile. Example: ospp", required=True) + parser.add_argument("-c", "--build-config-yaml", default=BUILD_CONFIG, + help=f"YAML file with information about the build configuration. " + f"Defaults to {BUILD_CONFIG}") + parser.add_argument("-o", "--output", type=str, required=True, + help=f"Path to output the control file.") + return parser.parse_args() + + +def _get_rule_dirs(json_path: str) -> Dict[str, str]: + with open(json_path, "r") as f: + return json.load(f) + + +def _check_rule_dirs_path(json: str): + if not os.path.exists(json): + print(f"Path {json} does not exist.", file=sys.stderr) + print("Hint: run ./utils/rule_dir_json.py first.", file=sys.stderr) + raise SystemExit(1) + + +def _get_env_yaml(root: str, product: str, build_config_yaml: str) -> Dict: + product_dir = os.path.join(root, "products", product) + product_yaml_path = os.path.join(product_dir, "product.yml") + env_yaml = ssg.environment.open_environment( + build_config_yaml, product_yaml_path, os.path.join(root, "product_properties")) + return env_yaml + + +def _get_id_mapping(env_yaml, reference, json_path: str) -> Dict: + rule_dir_json: Dict = _get_rule_dirs(json_path) + id_mapping: Dict[str, list[str]] = defaultdict(list) # type: ignore [misc] # For old mypy + for rule_id, rule_obj in rule_dir_json.items(): + rule_yaml = os.path.join(rule_obj["dir"], "rule.yml") + rule = ssg.yaml.open_and_macro_expand(rule_yaml, env_yaml) + if "references" not in rule: + continue + ref_id = rule["references"].get(reference) + if not ref_id: + continue + ids: List[str] = ref_id.split(",") + for _id in ids: + id_mapping[_id].append(rule_id) + return id_mapping + + +def main() -> int: + args = _parse_args() + _check_rule_dirs_path(args.json) + env_yaml = _get_env_yaml(args.root, args.product, args.build_config_yaml) + id_mapping = _get_id_mapping(env_yaml, args.reference, args.json) + output = dict() + output["levels"] = [{'id': 'base'}] + output["controls"] = list() + for _id in sorted(id_mapping.keys()): + rules = id_mapping[_id] + control = dict() + control["id"] = _id + control["levels"] = ["base"] + control["rules"] = sorted(rules) + control["status"] = "automated" + output["controls"].append(control) + + with open(args.output, "w") as f: + f.write(yaml.dump(output, sort_keys=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())