-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds create-ssp entrypoint and tests
Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>
- Loading branch information
Showing
4 changed files
with
314 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |