Skip to content

Commit

Permalink
Merge pull request #11036 from jan-cerny/faster_guides
Browse files Browse the repository at this point in the history
Add a faster alternative for generating HTML guides
  • Loading branch information
Mab879 authored Aug 31, 2023
2 parents d09e81a + 4c08686 commit e587c0b
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gh-pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
PAGES_DIR: __pages
steps:
- name: Install Deps
run: dnf install -y cmake git ninja-build openscap-utils python3-pyyaml python3-jinja2 python3-pytest ansible-lint libxslt python3-pip rsync
run: dnf install -y cmake git ninja-build openscap-utils python3-pyyaml python3-jinja2 python3-pytest ansible-lint libxslt python3-pip rsync python3-lxml
- name: Install deps python
run: pip3 install json2html
- name: Checkout
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ find_package(PythonInterp REQUIRED)

find_python_module(yaml REQUIRED)
find_python_module(jinja2 REQUIRED)
find_python_module(lxml)
find_python_module(pytest)
find_python_module(pytest_cov)
find_python_module(json2html)
Expand Down Expand Up @@ -245,6 +246,8 @@ message(STATUS "python myst-parser module (optional): ${PY_MYST_PARSER}")
message(STATUS "python openpyxl module (optional): ${PY_OPENPYXL}")
message(STATUS "python pandas module (optional): ${PY_PANDAS}")
message(STATUS "python pcre2 module (optional): ${PY_PCRE2}")
message(STATUS "python lxml module (optional): ${PY_LXML}")

message(STATUS " ")

message(STATUS "Build options:")
Expand Down
158 changes: 158 additions & 0 deletions build-scripts/generate_guides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/python3

import argparse
import collections
import lxml.etree as ET
import os

from ssg.constants import OSCAP_PROFILE, PREFIX_TO_NS
import ssg.build_guides

BenchmarkData = collections.namedtuple(
"BenchmarkData", ["title", "profiles", "product"])
XSLT_PATH = "/usr/share/openscap/xsl/xccdf-guide.xsl"


def get_benchmarks(ds, product):
benchmarks = {}
benchmark_xpath = "./ds:component/xccdf-1.2:Benchmark"
for benchmark_el in ds.xpath(benchmark_xpath, namespaces=PREFIX_TO_NS):
benchmark_id = benchmark_el.get("id")
title = benchmark_el.xpath(
"./xccdf-1.2:title", namespaces=PREFIX_TO_NS)[0].text
profiles = get_profiles(benchmark_el)
benchmarks[benchmark_id] = BenchmarkData(title, profiles, product)
return benchmarks


def get_profiles(benchmark_el):
profiles = {}
for profile_el in benchmark_el.xpath(
"./xccdf-1.2:Profile", namespaces=PREFIX_TO_NS):
profile_id = profile_el.get("id")
profile_title = profile_el.xpath(
"./xccdf-1.2:title", namespaces=PREFIX_TO_NS)[0].text
profiles[profile_id] = profile_title
return profiles


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--data-stream", required=True,
help="Path to a SCAP source data stream, eg. 'ssg-rhel9-ds.xml'")
parser.add_argument(
"--oscap-version", required=True,
help=f"Version of OpenSCAP that owns {XSLT_PATH}, eg. 1.3.8")
parser.add_argument(
"--product", required=True,
help="Product ID, eg. rhel9")
parser.add_argument(
"--output-dir", required=True,
help="Path to the directory where to generate the output files"
", eg. 'build/guides'")
args = parser.parse_args()
return args


def make_params(oscap_version, benchmark_id, profile_id):
params = {
"oscap-version": ET.XSLT.strparam(oscap_version),
"benchmark_id": ET.XSLT.strparam(benchmark_id),
"profile_id": ET.XSLT.strparam(profile_id)
}
return params


def make_output_file_name(profile_id, product):
short_profile_id = profile_id.replace(OSCAP_PROFILE, "")
output_file_name = "ssg-%s-guide-%s.html" % (product, short_profile_id)
return output_file_name


def make_output_file_path(profile_id, product, output_dir):
output_file_name = make_output_file_name(profile_id, product)
output_file_path = os.path.join(output_dir, output_file_name)
return output_file_path


def generate_html_guide(ds, transform, params, output_file_path):
html = transform(ds, **params)
html.write_output(output_file_path)


def make_index_options(benchmarks):
index_options = {}
for benchmark_id, benchmark_data in benchmarks.items():
options = []
for profile_id, profile_title in benchmark_data.profiles.items():
guide_file_name = make_output_file_name(
profile_id, benchmark_data.product)
data_benchmark_id = "" if len(benchmarks) == 1 else benchmark_id
option = (
f"<option value=\"{guide_file_name}\" data-benchmark-id=\""
f"{data_benchmark_id}\" data-profile-id=\"{profile_id}\">"
f"{profile_title}</option>")
options.append(option)
index_options[benchmark_id] = options
return index_options


def make_index_links(benchmarks):
index_links = []
for benchmark_id, benchmark_data in benchmarks.items():
for profile_id, profile_title in benchmark_data.profiles.items():
guide_file_name = make_output_file_name(
profile_id, benchmark_data.product)
a_target = (
f"<a target=\"guide\" href=\"{guide_file_name}\">"
f"{profile_title} in {benchmark_id}</a>")
index_links.append(a_target)
return index_links


def make_index_initial_src(benchmarks):
for benchmark_data in benchmarks.values():
for profile_id in benchmark_data.profiles:
return make_output_file_name(profile_id, benchmark_data.product)
return None


def generate_html_index(benchmarks, data_stream, output_dir):
benchmark_titles = {id_: data.title for id_, data in benchmarks.items()}
product = list(benchmarks.values())[0].product
input_basename = os.path.basename(data_stream)
index_links = make_index_links(benchmarks)
index_options = make_index_options(benchmarks)
index_initial_src = make_index_initial_src(benchmarks)
index_source = ssg.build_guides.build_index(
benchmark_titles, input_basename, index_links, index_options,
index_initial_src)
output_path = make_output_file_path("index", product, output_dir)
with open(output_path, "wb") as f:
f.write(index_source.encode("utf-8"))


def generate_html_guides(ds, benchmarks, oscap_version, output_dir):
xslt = ET.parse(XSLT_PATH)
transform = ET.XSLT(xslt)
for benchmark_id, benchmark_data in benchmarks.items():
for profile_id in benchmark_data.profiles:
params = make_params(oscap_version, benchmark_id, profile_id)
output_file_path = make_output_file_path(
profile_id, benchmark_data.product, output_dir)
generate_html_guide(ds, transform, params, output_file_path)


def main():
args = parse_args()
ds = ET.parse(args.data_stream)
benchmarks = get_benchmarks(ds, args.product)
if not os.path.exists(args.output_dir):
os.mkdir(args.output_dir)
generate_html_guides(ds, benchmarks, args.oscap_version, args.output_dir)
generate_html_index(benchmarks, args.data_stream, args.output_dir)


if __name__ == "__main__":
main()
24 changes: 17 additions & 7 deletions cmake/SSGCommon.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -598,13 +598,23 @@ endmacro()
# Build per-product HTML guides to see the status of various profiles and
# rules in the generated XCCDF guides.
macro(ssg_build_html_guides PRODUCT)
add_custom_command(
OUTPUT "${CMAKE_BINARY_DIR}/guides/ssg-${PRODUCT}-guide-index.html"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/guides"
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_all_guides.py" --input "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml" --output "${CMAKE_BINARY_DIR}/guides" build
DEPENDS generate-ssg-${PRODUCT}-ds.xml "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml"
COMMENT "[${PRODUCT}-guides] generating HTML guides for all profiles in ssg-${PRODUCT}-ds.xml"
)
if(PYTHON_VERSION_MAJOR GREATER 2 AND PY_LXML)
add_custom_command(
OUTPUT "${CMAKE_BINARY_DIR}/guides/ssg-${PRODUCT}-guide-index.html"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/guides"
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/generate_guides.py" --data-stream "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml" --product "${PRODUCT}" --output-dir "${CMAKE_BINARY_DIR}/guides" --oscap-version "${OSCAP_VERSION}"
DEPENDS generate-ssg-${PRODUCT}-ds.xml "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml"
COMMENT "[${PRODUCT}-guides] generating HTML guides for all profiles in ssg-${PRODUCT}-ds.xml"
)
else()
add_custom_command(
OUTPUT "${CMAKE_BINARY_DIR}/guides/ssg-${PRODUCT}-guide-index.html"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/guides"
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_all_guides.py" --input "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml" --output "${CMAKE_BINARY_DIR}/guides" build
DEPENDS generate-ssg-${PRODUCT}-ds.xml "${CMAKE_BINARY_DIR}/ssg-${PRODUCT}-ds.xml"
COMMENT "[${PRODUCT}-guides] generating HTML guides for all profiles in ssg-${PRODUCT}-ds.xml"
)
endif()
add_custom_target(
generate-ssg-${PRODUCT}-guide-index.html
DEPENDS "${CMAKE_BINARY_DIR}/guides/ssg-${PRODUCT}-guide-index.html"
Expand Down
1 change: 1 addition & 0 deletions docs/manual/developer/07_understanding_build_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ refer to their help text for more information and usage:
base product.
- `expand_jinja.py` -- helper script used by the BATS (Bash unit test
framework) to expand Jinja in test scripts.
- `generate_guides.py` -- Generate HTML guides and HTML index for every profile in the built SCAP source data stream.
- `generate_man_page.py` -- generates the ComplianceAsCode man page.
- `generate_profile_remediations.py` -- Generate profile oriented Bash remediation scripts or profile oriented Ansible Playbooks from the built SCAP source data stream. The output is similar to the output of the `oscap xccdf generate fix` command, but the tool `generate_profile_remediations.py` generates the scripts or Playbooks for all profiles in the given SCAP source data stream at once.
- `profile_tool.py` -- utility script to generate statistics about profiles
Expand Down

0 comments on commit e587c0b

Please sign in to comment.