Skip to content

Commit

Permalink
feat(entrypoint): adds create-ssp entrypoint and tests
Browse files Browse the repository at this point in the history
SSP creation is becoming more customized with
the addition of yaml header. Adding a create ssp entrypoint
will will reduce error  when working with autosync command and
SSPs

Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 committed Jan 30, 2024
1 parent 1f84696 commit 9ec729b
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 33 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ trestlebot-autosync = "trestlebot.entrypoints.autosync:main"
trestlebot-rules-transform = "trestlebot.entrypoints.rule_transform:main"
trestlebot-create-cd = "trestlebot.entrypoints.create_cd:main"
trestlebot-sync-upstreams = "trestlebot.entrypoints.sync_upstreams:main"
trestlebot-create-ssp = "trestlebot.entrypoints.create_ssp:main"

[tool.poetry.dependencies]
python = '^3.8.1'
Expand Down
62 changes: 29 additions & 33 deletions tests/e2e/test_e2e_ssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
prepare_upstream_repo,
setup_for_ssp,
)
from trestlebot.const import ERROR_EXIT_CODE, INVALID_ARGS_EXIT_CODE, SUCCESS_EXIT_CODE
from trestlebot.tasks.authored.ssp import AuthoredSSP, SSPIndex
from trestlebot.const import ERROR_EXIT_CODE, SUCCESS_EXIT_CODE
from trestlebot.tasks.authored.ssp import SSPIndex


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -78,17 +78,6 @@
ERROR_EXIT_CODE,
True,
),
(
"failure/missing args",
{
"branch": "test",
"oscal-model": "ssp",
"committer-name": "test",
"committer-email": "[email protected]",
},
INVALID_ARGS_EXIT_CODE,
False,
),
],
)
def test_ssp_editing_e2e(
Expand All @@ -111,33 +100,40 @@ def test_ssp_editing_e2e(
tmp_repo_path = pathlib.Path(tmp_repo_str)

args = setup_for_ssp(tmp_repo_path, test_prof, [test_comp_name], test_ssp_md)
remote_url = "http://localhost:8080/test.git"
repo.create_remote("origin", url=remote_url)

# Create or generate the SSP
if not skip_create:
index_path = os.path.join(tmp_repo_str, "ssp-index.json")
ssp_index = SSPIndex(index_path)
authored_ssp = AuthoredSSP(tmp_repo_str, ssp_index)
authored_ssp.create_new_default(
create_args: Dict[str, str] = {
"markdown-path": command_args["markdown-path"],
"branch": command_args["branch"],
"committer-name": command_args["committer-name"],
"committer-email": command_args["committer-email"],
"ssp-name": test_ssp_name,
"profile-name": test_prof,
"compdefs": test_comp_name,
}
command = build_test_command(
tmp_repo_str, "create-ssp", create_args, image_name
)
run_response = subprocess.run(command, capture_output=True)
assert run_response.returncode == response
assert (tmp_repo_path / command_args["markdown-path"]).exists()

# Make a change to the SSP
ssp, ssp_path = ModelUtils.load_model_for_class(
tmp_repo_path,
test_ssp_name,
test_prof,
[test_comp_name],
test_ssp_md,
SystemSecurityPlan,
FileContentType.JSON,
)
ssp.metadata.title = "New Title"
ssp.oscal_write(ssp_path)
else:
ssp_generate = SSPGenerate()
assert ssp_generate._run(args) == 0

ssp_path: pathlib.Path = ModelUtils.get_model_path_for_name_and_class(
tmp_repo_path,
test_ssp_name,
SystemSecurityPlan,
FileContentType.JSON,
)
assert not ssp_path.exists()

remote_url = "http://localhost:8080/test.git"
repo.create_remote("origin", url=remote_url)

command = build_test_command(tmp_repo_str, "autosync", command_args, image_name)
run_response = subprocess.run(command, capture_output=True)
assert run_response.returncode == response
Expand All @@ -151,8 +147,8 @@ def test_ssp_editing_e2e(
)

# Check that the correct files are present with the correct content
assert (tmp_repo_path / command_args["markdown-path"]).exists()
ssp_index.reload()
index_path = os.path.join(tmp_repo_str, "ssp-index.json")
ssp_index = SSPIndex(index_path)
assert ssp_index.get_profile_by_ssp(test_ssp_name) == test_prof
assert ssp_index.get_comps_by_ssp(test_ssp_name) == [test_comp_name]
assert ssp_index.get_leveraged_by_ssp(test_ssp_name) is None
Expand Down
1 change: 1 addition & 0 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
JSON_TEST_DATA_PATH = pathlib.Path("tests/data/json/").resolve()
YAML_TEST_DATA_PATH = pathlib.Path("tests/data/yaml/").resolve()
TEST_SSP_INDEX = JSON_TEST_DATA_PATH / "test_ssp_index.json"
TEST_YAML_HEADER = YAML_TEST_DATA_PATH / "extra_yaml_header.yaml"

# E2E test constants
TRESTLEBOT_TEST_IMAGE_NAME = "localhost/trestlebot:latest"
Expand Down
80 changes: 80 additions & 0 deletions tests/trestlebot/entrypoints/test_create_ssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Red Hat, Inc.


"""Test for Create SSP CLI"""

import logging
import pathlib
from typing import Any, Dict, Tuple
from unittest.mock import patch

import pytest
from git import Repo

from tests.testutils import TEST_YAML_HEADER, args_dict_to_list, setup_for_ssp
from trestlebot.entrypoints.create_ssp import main as cli_main


@pytest.fixture
def base_args_dict() -> Dict[str, str]:
return {
"branch": "main",
"committer-name": "test",
"profile-name": "simplified_nist_profile",
"ssp-name": "test-ssp",
"committer-email": "[email protected]",
"working-dir": ".",
"file-patterns": ".",
}


test_cat = "simplified_nist_catalog"
test_prof = "simplified_nist_profile"
test_comp_name = "test_comp"
test_ssp_md = "md_ssp"
test_repo_url = "git.test.com/test/repo.git"

# Expecting more md files to be included in the commit
# but checking that the md directory was created and the ssp-index.json was created
expected_files = [
"md_ssp/test-ssp/ac/ac-2.1.md",
"system-security-plans/test-ssp/system-security-plan.json",
"ssp-index.json",
]


def test_create_ssp(
tmp_repo: Tuple[str, Repo], base_args_dict: Dict[str, str], caplog: Any
) -> None:
"""Test create-ssp with valid args and a custom yaml header."""
repo_path, repo = tmp_repo
tmp_repo_path = pathlib.Path(repo_path)
repo.create_remote("origin", url=test_repo_url)

args_dict = base_args_dict
args_dict["working-dir"] = repo_path
args_dict["markdown-path"] = test_ssp_md
args_dict["compdefs"] = test_comp_name
args_dict["ssp-index-path"] = str(tmp_repo_path / "ssp-index.json")
args_dict["yaml-header-path"] = str(TEST_YAML_HEADER)

_ = setup_for_ssp(tmp_repo_path, test_prof, [test_comp_name], test_ssp_md)

with patch("git.remote.Remote.push") as mock_push, patch(
"sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]
):
mock_push.return_value = "Mocked Results"
with pytest.raises(SystemExit, match="0"):
cli_main()

# Verify that the correct files were included
commit = next(repo.iter_commits())
assert all(file in commit.stats.files for file in expected_files)
assert len(commit.stats.files) == 17

assert any(
record.levelno == logging.INFO
and "Changes pushed to main successfully" in record.message
for record in caplog.records
)
143 changes: 143 additions & 0 deletions trestlebot/entrypoints/create_ssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Red Hat, Inc.

"""
Entrypoint for system security plan bootstrapping.
This will create and initial SSP with markdown, an SSP index, and a SSP JSON file.
"""

import argparse
import logging
import pathlib
import sys
from typing import List

from trestlebot.const import SUCCESS_EXIT_CODE
from trestlebot.entrypoints.entrypoint_base import (
EntrypointBase,
comma_sep_to_list,
handle_exception,
)
from trestlebot.entrypoints.log import set_log_level_from_args
from trestlebot.tasks.assemble_task import AssembleTask
from trestlebot.tasks.authored.ssp import AuthoredSSP, SSPIndex
from trestlebot.tasks.base_task import ModelFilter, TaskBase


logger = logging.getLogger(__name__)


class CreateSSPEntrypoint(EntrypointBase):
"""Entrypoint for ssp bootstrapping."""

def __init__(self, parser: argparse.ArgumentParser) -> None:
"""Initialize."""
super().__init__(parser)
self.setup_create_ssp_arguments()

def setup_create_ssp_arguments(self) -> None:
"""Setup specific arguments for this entrypoint."""
self.parser.add_argument(
"--ssp-name", required=True, type=str, help="Name of SSP to create."
)
self.parser.add_argument(
"--profile-name",
required=True,
help="Name of profile in the trestle workspace to include in the SSP.",
)
self.parser.add_argument(
"--compdefs",
required=True,
type=str,
help="Comma-separated list of component definitions to include in the SSP",
)
self.parser.add_argument(
"--leveraged-ssp",
required=False,
type=str,
help="Provider SSP to leverage for the new SSP.",
)
self.parser.add_argument(
"--markdown-path",
required=True,
type=str,
help="Path to create markdown files in.",
)
self.parser.add_argument(
"--yaml-header-path",
required=False,
type=str,
help="Optionally set a path to a YAML file for custom SSP Markdown YAML headers.",
)
self.parser.add_argument(
"--version",
required=False,
type=str,
help="Optionally set the SSP version.",
)
self.parser.add_argument(
"--ssp-index-path",
required=False,
type=str,
default="ssp-index.json",
help="Optionally set the path to the SSP index file.",
)

def run(self, args: argparse.Namespace) -> None:
"""Run the entrypoint."""
exit_code: int = SUCCESS_EXIT_CODE
try:
set_log_level_from_args(args)

# If the ssp index file does not exist, create it.
ssp_index_path = pathlib.Path(args.ssp_index_path)
if not ssp_index_path.exists():
# Create a parent directory
ssp_index_path.parent.mkdir(parents=True, exist_ok=True)

ssp_index = SSPIndex(args.ssp_index_path)
authored_ssp = AuthoredSSP(args.working_dir, ssp_index)

comps: List[str] = comma_sep_to_list(args.compdefs)
authored_ssp.create_new_default(
ssp_name=args.ssp_name,
profile_name=args.profile_name,
compdefs=comps,
markdown_path=args.markdown_path,
leveraged_ssp=args.leveraged_ssp,
yaml_header=args.yaml_header_path,
)

# The starting point for SSPs is the markdown, so assemble into JSON.
model_filter: ModelFilter = ModelFilter([], [args.ssp_name])
assemble_task = AssembleTask(
authored_object=authored_ssp,
markdown_dir=args.markdown_path,
version=args.version,
model_filter=model_filter,
)
pre_tasks: List[TaskBase] = [assemble_task]

super().run_base(args, pre_tasks)
except Exception as e:
exit_code = handle_exception(e)

sys.exit(exit_code)


def main() -> None:
"""Run the CLI."""
parser = argparse.ArgumentParser(
description="Create new system security plan for editing."
)
create_ssp = CreateSSPEntrypoint(parser=parser)

args = parser.parse_args()
create_ssp.run(args)


if __name__ == "__main__":
main()

0 comments on commit 9ec729b

Please sign in to comment.