Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable templated SCE checks #12445

Merged
merged 25 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0820ceb
Add SCE check to service_enabled template
jan-cerny Sep 25, 2024
a9996af
Fix broken code
jan-cerny Sep 25, 2024
fbb591b
Allow rules to have both SCE and OVAL
jan-cerny Sep 25, 2024
0a9de92
Extract method _add_sce_check_element
jan-cerny Sep 25, 2024
258494e
Extract method _add_oval_check_element
jan-cerny Sep 25, 2024
33e84e4
Extract method _add_ocil_check_element
jan-cerny Sep 25, 2024
d6024fa
Extract build_templated_sce_check
jan-cerny Sep 25, 2024
320a51d
Remove unused variable included_checks_count
jan-cerny Sep 25, 2024
7c0c1f1
Remove unused code
jan-cerny Sep 27, 2024
1304fea
Convert function to a class
jan-cerny Sep 27, 2024
32b2db4
Extract method _build_static_sce_check
jan-cerny Sep 27, 2024
871caf7
Move build_templated_sce_check
jan-cerny Sep 27, 2024
98613ea
Simplify code
jan-cerny Sep 27, 2024
b47e594
Extract method dump_metadata
jan-cerny Sep 27, 2024
9d148ca
Extract method _build_rule
jan-cerny Sep 27, 2024
bfa2abc
Move variable local_env_yaml
jan-cerny Sep 27, 2024
65823aa
Extract method _find_content_dirs
jan-cerny Sep 27, 2024
ac88ae8
Document SCE in service_enabled template docs
jan-cerny Sep 30, 2024
35da1db
Add SCE check to service_disabled template
jan-cerny Sep 30, 2024
bd400c6
Resolve Code Climate problem
jan-cerny Oct 1, 2024
812fff3
Resolve Code Climate problem
jan-cerny Oct 1, 2024
dcbc2c5
Resolve Code Climate problem
jan-cerny Oct 1, 2024
8ce28ba
Resolve Code Climate problem
jan-cerny Oct 1, 2024
15b5e08
Add a simple test for the SCEBuilder class
jan-cerny Oct 9, 2024
f4f88d8
Add newline at end of file
jan-cerny Oct 9, 2024
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
10 changes: 5 additions & 5 deletions build-scripts/build_sce.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import ssg.build_sce
import ssg.environment
import ssg.templates
import ssg.products
from ssg.utils import mkdir_p


Expand All @@ -55,9 +56,6 @@ def parse_args():
)
p.add_argument(
"--output", required=True)
p.add_argument(
"scedirs", metavar="SCE_DIR", nargs="+",
help="SCE definition scripts to build for the specified product.")
args = p.parse_args()
return args

Expand All @@ -71,7 +69,9 @@ def parse_args():
env_yaml = ssg.environment.open_environment(
args.build_config_yaml, args.product_yaml)
empty = "/sce/empty/placeholder"
product_yaml = ssg.products.Product(args.product_yaml)
template_builder = ssg.templates.Builder(
env_yaml, empty, args.templates_dir, empty, empty, empty, None)
ssg.build_sce.checks(env_yaml, args.product_yaml, args.scedirs,
template_builder, args.output)
sce_builder = ssg.build_sce.SCEBuilder(
env_yaml, product_yaml, template_builder, args.output)
sce_builder.build()
8 changes: 1 addition & 7 deletions cmake/SSGCommon.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,6 @@ endmacro()
# <complex-check /> elements as necessary.
macro(ssg_build_sce PRODUCT)
set(BUILD_CHECKS_DIR "${CMAKE_CURRENT_BINARY_DIR}/checks")
# Unlike build_oval_unlinked, here we're ignoring the existing checks from
# templates and other places and we're merely appending/templating the
# content from the rules directories. That's why we ignore BUILD_CHECKS_DIR
# in the combine paths below.
set(SCE_COMBINE_PATHS "${SSG_SHARED}/checks/sce" "${CMAKE_CURRENT_SOURCE_DIR}/checks/sce")

if(SSG_SCE_ENABLED)
# Unlike build_oval_unlinked, we don't depend on templated content yet.
#
Expand All @@ -348,7 +342,7 @@ macro(ssg_build_sce PRODUCT)
# the XCCDF, so we'd have a dependency circle.
add_custom_command(
OUTPUT "${BUILD_CHECKS_DIR}/sce/metadata.json"
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_sce.py" --build-config-yaml "${CMAKE_BINARY_DIR}/build_config.yml" --product-yaml "${CMAKE_CURRENT_BINARY_DIR}/product.yml" --templates-dir "${SSG_SHARED}/templates" --output "${BUILD_CHECKS_DIR}/sce" ${SCE_COMBINE_PATHS}
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${SSG_BUILD_SCRIPTS}/build_sce.py" --build-config-yaml "${CMAKE_BINARY_DIR}/build_config.yml" --product-yaml "${CMAKE_CURRENT_BINARY_DIR}/product.yml" --templates-dir "${SSG_SHARED}/templates" --output "${BUILD_CHECKS_DIR}/sce"
COMMENT "[${PRODUCT}-content] generating sce/metadata.json"
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/product.yml"
)
Expand Down
4 changes: 2 additions & 2 deletions docs/templates/template_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ When the remediation is applied duplicate occurrences of `key` are removed.
If **daemonname** is not specified it means the name of the
daemon is the same as the name of service.

- Languages: Ansible, Bash, OVAL, Puppet, Ignition, Kubernetes, Blueprint, Kickstart
- Languages: Ansible, Bash, OVAL, Puppet, Ignition, Kubernetes, Blueprint, Kickstart, SCE

#### service_enabled
- Checks if a system service is enabled. Uses either systemd or SysV
Expand All @@ -723,7 +723,7 @@ When the remediation is applied duplicate occurrences of `key` are removed.
If **daemonname** is not specified it means the name of the
daemon is the same as the name of service.

- Languages: Ansible, Bash, OVAL, Puppet, Blueprint, Kickstart
- Languages: Ansible, Bash, OVAL, Puppet, Blueprint, Kickstart, SCE

#### shell_lineinfile
- Checks shell variable assignments in files. Remediations will paste
Expand Down
6 changes: 6 additions & 0 deletions shared/templates/service_disabled/sce-bash.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
# check-import = stdout
if [[ $(systemctl is-enabled {{{ DAEMONNAME }}}.service) == "masked" ]] ; then
exit "$XCCDF_RESULT_PASS"
fi
exit "$XCCDF_RESULT_FAIL"
1 change: 1 addition & 0 deletions shared/templates/service_disabled/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ supported_languages:
- puppet
- blueprint
- kickstart
- sce-bash
6 changes: 6 additions & 0 deletions shared/templates/service_enabled/sce-bash.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
# check-import = stdout
if [[ $(systemctl is-enabled {{{ DAEMONNAME }}}.service) == "enabled" ]] ; then
exit "$XCCDF_RESULT_PASS"
fi
exit "$XCCDF_RESULT_FAIL"
1 change: 1 addition & 0 deletions shared/templates/service_enabled/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ supported_languages:
- puppet
- blueprint
- kickstart
- sce-bash
270 changes: 136 additions & 134 deletions ssg/build_sce.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
from . import utils, products


def write_sce_file(sce_content, output_dir, filename):
with open(os.path.join(output_dir, filename), 'w') as output_file:
print(sce_content, file=output_file)


def load_sce_and_metadata(file_path, local_env_yaml):
"""
For the given SCE audit file (file_path) under the specified environment
Expand Down Expand Up @@ -95,140 +100,6 @@ def _check_is_loaded(already_loaded, filename):
return filename in already_loaded


def checks(env_yaml, yaml_path, sce_dirs, template_builder, output):
"""
Walks the build system and builds all SCE checks (and metadata entry)
into the output directory.
"""
product = utils.required_key(env_yaml, "product")
included_checks_count = 0
reversed_dirs = sce_dirs[::-1]
already_loaded = dict()
local_env_yaml = dict()
local_env_yaml.update(env_yaml)

# We maintain the same search structure as build_ovals.py even though we
# don't currently have any content under shared/checks/sce.
product_yaml = products.Product(yaml_path)
product_dir = product_yaml["product_dir"]
relative_guide_dir = utils.required_key(env_yaml, "benchmark_root")
guide_dir = os.path.abspath(os.path.join(product_dir, relative_guide_dir))
additional_content_directories = env_yaml.get("additional_content_directories", [])
add_content_dirs = [
os.path.abspath(os.path.join(product_dir, rd))
for rd in additional_content_directories
]

# First walk all rules under the product. These have higher priority than any
# out-of-tree SCE checks.
for _dir_path in find_rule_dirs_in_paths([guide_dir] + add_content_dirs):
rule_id = get_rule_dir_id(_dir_path)

rule_path = os.path.join(_dir_path, "rule.yml")
try:
rule = Rule.from_yaml(rule_path, env_yaml)
except DocumentationNotComplete:
# Happens on non-debug builds when a rule isn't yet completed. We
# don't want to build the SCE check for this rule yet so skip it
# and move on.
continue

local_env_yaml['rule_id'] = rule.id_
local_env_yaml['rule_title'] = rule.title
local_env_yaml['products'] = {product}

for _path in get_rule_dir_sces(_dir_path, product):
# To be compatible with later checks, use the rule_id (i.e., the
# value of _dir) to recreate the expected filename if this OVAL
# was in a rule directory. However, note that unlike
# build_oval.checks(...), we have to get this script's extension
# first.
_, ext = os.path.splitext(_path)
filename = "{0}{1}".format(rule_id, ext)

sce_content, metadata = load_sce_and_metadata(_path, local_env_yaml)
metadata['filename'] = filename

if not _check_is_applicable_for_product(metadata, product):
continue
if _check_is_loaded(already_loaded, rule_id):
continue

with open(os.path.join(output, filename), 'w') as output_file:
print(sce_content, file=output_file)

included_checks_count += 1
already_loaded[rule_id] = metadata

if rule.template:
langs = template_builder.get_resolved_langs_to_generate(rule)
if 'sce-bash' in langs:
# Here we know the specified rule has a template and this
# template actually generates (bash) SCE content. We
# prioritize bespoke SCE content over templated content,
# however, while we add this to our metadata, we do not
# bother (yet!) with generating the SCE content. This is done
# at a later time by build-scripts/build_templated_content.py.
if _check_is_loaded(already_loaded, rule_id):
continue

# While we don't _write_ it, we still need to parse SCE
# metadata from the templated content. Render it internally.
raw_sce_content = template_builder.get_lang_contents_for_templatable(
rule, langs['sce-bash']
)

ext = '.sh'
filename = rule_id + ext

# Load metadata and infer correct file name.
sce_content, metadata = load_sce_and_metadata_parsed(raw_sce_content)
metadata['filename'] = filename

# Skip the check if it isn't applicable for this product.
if not _check_is_applicable_for_product(metadata, product):
continue

with open(os.path.join(output, filename), 'w') as output_file:
print(sce_content, file=output_file)

# Finally, include it in our loaded content
included_checks_count += 1
already_loaded[rule_id] = metadata

# Finally take any shared SCE checks and build them as well. Note that
# there's no way for shorthand generation to include them if they do NOT
# align with a particular rule_id, so it is suggested that the former
# method be used.
for sce_dir in reversed_dirs:
if not os.path.isdir(sce_dir):
continue

for filename in sorted(os.listdir(sce_dir)):
rule_id, _ = os.path.splitext(filename)

sce_content, metadata = load_sce_and_metadata(filename, env_yaml)
metadata['filename'] = filename

if not _check_is_applicable_for_product(metadata, product):
continue
if _check_is_loaded(already_loaded, rule_id):
continue

with open(os.path.join(output, filename), 'w') as output_file:
print(sce_content, file=output_file)

included_checks_count += 1
already_loaded[rule_id] = metadata

# Finally, write out our metadata to disk so that we can reference it in
# later build stages (such as during building shorthand content).
metadata_path = os.path.join(output, 'metadata.json')
json.dump(already_loaded, open(metadata_path, 'w'))

return already_loaded


# Retrieve the SCE checks and return a list of path to each check script.
def collect_sce_checks(datastreamtree):
checklists = datastreamtree.find(
Expand All @@ -253,3 +124,134 @@ def collect_sce_checks(datastreamtree):
checks = datastreamtree.findall(checks_xpath)
# Extract the file paths of the SCE checks
return [check.get('href') for check in checks]


class SCEBuilder():
def __init__(self, env_yaml, product_yaml, template_builder, output_dir):
self.env_yaml = env_yaml
self.product_yaml = product_yaml
self.template_builder = template_builder
self.output_dir = output_dir
self.already_loaded = dict()

def _build_static_sce_check(self, rule_id, path, local_env_yaml):
# To be compatible with later checks, use the rule_id (i.e., the
# value of _dir) to recreate the expected filename if this OVAL
# was in a rule directory. However, note that unlike
# build_oval.checks(...), we have to get this script's extension
# first.
_, ext = os.path.splitext(path)
filename = "{0}{1}".format(rule_id, ext)

sce_content, metadata = load_sce_and_metadata(path, local_env_yaml)
metadata['filename'] = filename
product = utils.required_key(self.env_yaml, "product")

if not _check_is_applicable_for_product(metadata, product):
return
if _check_is_loaded(self.already_loaded, rule_id):
return

write_sce_file(sce_content, self.output_dir, filename)

self.already_loaded[rule_id] = metadata

def _get_rule_sce_lang(self, rule):
langs = self.template_builder.get_resolved_langs_to_generate(rule)
for lang in langs:
if lang.name == 'sce-bash':
return lang
return None

def _build_templated_sce_check(self, rule):
if not rule.template:
return
sce_lang = self._get_rule_sce_lang(rule)
if sce_lang is None:
return

# Here we know the specified rule has a template and this
# template actually generates (bash) SCE content. We
# prioritize bespoke SCE content over templated content,
# however, while we add this to our metadata, we do not
# bother (yet!) with generating the SCE content. This is done
# at a later time by build-scripts/build_templated_content.py.
if _check_is_loaded(self.already_loaded, rule.id_):
return

# While we don't _write_ it, we still need to parse SCE
# metadata from the templated content. Render it internally.
raw_sce_content = self.template_builder.get_lang_contents_for_templatable(
rule, sce_lang
)

ext = '.sh'
filename = rule.id_ + ext

# Load metadata and infer correct file name.
sce_content, metadata = load_sce_and_metadata_parsed(raw_sce_content)
metadata['filename'] = filename

# Skip the check if it isn't applicable for this product.
product = utils.required_key(self.env_yaml, "product")
if not _check_is_applicable_for_product(metadata, product):
return

write_sce_file(sce_content, self.output_dir, filename)

# Finally, include it in our loaded content
self.already_loaded[rule.id_] = metadata

def _build_rule(self, rule_dir_path):
local_env_yaml = dict()
local_env_yaml.update(self.env_yaml)
product = utils.required_key(self.env_yaml, "product")
rule_id = get_rule_dir_id(rule_dir_path)

rule_path = os.path.join(rule_dir_path, "rule.yml")
try:
rule = Rule.from_yaml(rule_path, self.env_yaml)
except DocumentationNotComplete:
# Happens on non-debug builds when a rule isn't yet completed. We
# don't want to build the SCE check for this rule yet so skip it
# and move on.
return

local_env_yaml['rule_id'] = rule.id_
local_env_yaml['rule_title'] = rule.title
local_env_yaml['products'] = {product}

for _path in get_rule_dir_sces(rule_dir_path, product):
self._build_static_sce_check(rule_id, _path, local_env_yaml)

self._build_templated_sce_check(rule)

def _dump_metadata(self):
# Finally, write out our metadata to disk so that we can reference it in
# later build stages (such as during building shorthand content).
metadata_path = os.path.join(self.output_dir, 'metadata.json')
json.dump(self.already_loaded, open(metadata_path, 'w'))

def _find_content_dirs(self):
# We maintain the same search structure as build_ovals.py even though we
# don't currently have any content under shared/checks/sce.
product_dir = self.product_yaml["product_dir"]
relative_guide_dir = utils.required_key(self.env_yaml, "benchmark_root")
guide_dir = os.path.abspath(os.path.join(product_dir, relative_guide_dir))
additional_content_directories = self.env_yaml.get(
"additional_content_directories", [])
add_content_dirs = [
os.path.abspath(os.path.join(product_dir, rd))
for rd in additional_content_directories
]
return ([guide_dir] + add_content_dirs)

def build(self):
"""
Walks the content repository and builds all SCE checks (and metadata entry)
into the output directory.
"""
content_dirs = self._find_content_dirs()
for _dir_path in find_rule_dirs_in_paths(content_dirs):
self._build_rule(_dir_path)
self._dump_metadata()
Loading
Loading