Skip to content

Commit

Permalink
feat: adds create-ssp entrypoint and tests
Browse files Browse the repository at this point in the history
Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>
  • Loading branch information
jpower432 committed Dec 1, 2023
1 parent 6766eca commit 670d423
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 35 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ repository = 'https://github.com/RedHatProductSecurity/trestle-bot'
trestlebot-autosync = "trestlebot.entrypoints.autosync:main"
trestlebot-rules-transform = "trestlebot.entrypoints.rule_transform:main"
trestlebot-create-cd = "trestlebot.entrypoints.create_cd:main"
trestlebot-create-ssp = "trestlebot.entrypoints.create_ssp:main"

[tool.poetry.dependencies]
python = '^3.8.1'
Expand Down
62 changes: 27 additions & 35 deletions tests/e2e/test_e2e_ssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
from trestle.oscal.ssp import SystemSecurityPlan

from tests.testutils import build_test_command, 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 @@ -79,17 +79,6 @@
ERROR_EXIT_CODE,
True,
),
(
"failure/missing args",
{
"branch": "test",
"oscal-model": "ssp",
"committer-name": "test",
"committer-email": "test@email.com",
},
INVALID_ARGS_EXIT_CODE,
False,
),
],
)
def test_ssp_editing_e2e(
Expand Down Expand Up @@ -123,34 +112,38 @@ def test_ssp_editing_e2e(
init._run(args)

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)
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)
run_response = subprocess.run(command, capture_output=True)
assert run_response.returncode == response
Expand All @@ -164,9 +157,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
assert ssp_path.exists()
106 changes: 106 additions & 0 deletions tests/trestlebot/entrypoints/test_create_ssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/python

# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Test for create-ssp CLI"""

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

import pytest

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


@pytest.fixture
def valid_args_dict() -> Dict[str, str]:
return {
"branch": "main",
"ssp-name": "ssp",
"markdown-path": "/my/path",
"profile-name": "profile",
"compdefs": "compdefs",
"committer-name": "test",
"committer-email": "test@email.com",
"working-dir": "tmp",
"file-patterns": ".",
}


def test_with_filtered_leveraged_ssp(
valid_args_dict: Dict[str, str], caplog: Any
) -> None:
"""Test with filtered and leveraged ssp set."""
args_dict = valid_args_dict
args_dict["filtered-ssp"] = "filtered-ssp"
args_dict["leveraged-ssp"] = "leveraged-ssp"
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="2"):
cli_main()

assert any(
record.levelno == logging.ERROR
and record.message == "Cannot use both --filtered-ssp and --leveraged-ssp"
for record in caplog.records
)


def test_with_no_profile(valid_args_dict: Dict[str, str], caplog: Any) -> None:
"""Test with non profile name set."""
args_dict = valid_args_dict
args_dict["profile-name"] = ""
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="2"):
cli_main()

assert any(
record.levelno == logging.ERROR
and record.message == "Must set profile name with --profile-name."
for record in caplog.records
)


def test_with_no_compdef(valid_args_dict: Dict[str, str], caplog: Any) -> None:
"""Test with non compdef set."""
args_dict = valid_args_dict
args_dict["compdef"] = ""
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit, match="2"):
cli_main()

assert any(
record.levelno == logging.ERROR
and record.message == "Must set component definitions with --compdefs."
for record in caplog.records
)


def test_with_no_filter_criteria(valid_args_dict: Dict[str, str], caplog: Any) -> None:
"""Test with invalid args for filtering."""
args_dict = valid_args_dict
args_dict["filtered-ssp"] = "filtered-ssp"
args_dict["profile-name"] = ""
args_dict["compdefs"] = ""
with patch("sys.argv", ["trestlebot", *args_dict_to_list(args_dict)]):
with pytest.raises(SystemExit):
cli_main()

err_msg = """Must filter by profile or component definitions with --profile-name and --compdefs."""
assert any(
record.levelno == logging.ERROR and record.message == err_msg
for record in caplog.records
)
180 changes: 180 additions & 0 deletions trestlebot/entrypoints/create_ssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/python

# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""
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 INVALID_ARGS_EXIT_CODE
from trestlebot.entrypoints.entrypoint_base import EntrypointBase, comma_sep_to_list
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=False,
help="Name of profile in the trestle workspace to include in the SSP.",
)
self.parser.add_argument(
"--compdefs",
required=False,
type=str,
help="Comma-separated list of component definitions to include in the SSP",
)
self.parser.add_argument(
"--filtered-ssp",
required=False,
type=str,
help="Path to a SSP to filter for the new SSP.",
)
self.parser.add_argument(
"--leveraged-ssp",
required=False,
type=str,
help="Provider SSP to leverage for the new SSP. Cannot be used with --filtered-ssp.",
)
self.parser.add_argument(
"--markdown-path",
required=True,
type=str,
help="Path to create markdown files in.",
)
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 filter the controls in the component definition by a profile.",
)

def validate_args(self, args: argparse.Namespace) -> None:
"""Validate arguments."""
if args.filtered_ssp:
if args.leveraged_ssp:
logger.error("Cannot use both --filtered-ssp and --leveraged-ssp")
sys.exit(INVALID_ARGS_EXIT_CODE)
# Profile or component definitions are required for SSP filtering.
if not args.profile_name and not args.compdefs:
logger.error(
"Must filter by profile or component definitions with --profile-name and --compdefs."
)
sys.exit(INVALID_ARGS_EXIT_CODE)

# Profile and component definitions are required for a new SSP.
if not args.filtered_ssp:
if not args.profile_name:
logger.error("Must set profile name with --profile-name.")
sys.exit(INVALID_ARGS_EXIT_CODE)
if not args.compdefs:
logger.error("Must set component definitions with --compdefs.")
sys.exit(INVALID_ARGS_EXIT_CODE)

def run(self, args: argparse.Namespace) -> None:
"""Run the entrypoint."""

set_log_level_from_args(args)
self.validate_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)

pre_tasks: List[TaskBase] = []
ssp_index = SSPIndex(args.ssp_index_path)
authored_ssp = AuthoredSSP(args.working_dir, ssp_index)

comps: List[str] = comma_sep_to_list(args.compdefs)
if args.filtered_ssp:
# Create with filter starts with an existing SSP and filters it.
# No need to assemble.
authored_ssp.create_new_with_filter(
ssp_name=args.ssp_name,
input_ssp=args.filtered_ssp,
version=args.version,
markdown_path=args.markdown_path,
profile_name=args.profile_name,
compdefs=comps,
)
else:
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,
)

# The starting point for SSPs in 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.append(assemble_task)

super().run_base(args, pre_tasks)


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 670d423

Please sign in to comment.