Skip to content

Commit

Permalink
feat: adds profile filtering to create-cd entrypoint
Browse files Browse the repository at this point in the history
Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 committed Oct 24, 2023
1 parent 618cd85 commit fa5d3c2
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 45 deletions.
141 changes: 141 additions & 0 deletions tests/data/json/filter_profile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{
"profile": {
"uuid": "1019f424-1556-4aa3-9df3-337b97c2c856",
"metadata": {
"title": "Simplified NIST profile for filtering",
"last-modified": "2021-06-08T13:57:34.337491-04:00",
"version": "Final",
"oscal-version": "1.0.0",
"roles": [
{
"id": "creator",
"title": "Document Creator"
},
{
"id": "contact",
"title": "Contact"
}
],
"parties": [
{
"uuid": "cde369ce-57f8-4ec1-847f-2681a9a881e7",
"type": "organization",
"name": "Joint Task Force, Transformation Initiative",
"email-addresses": [
"[email protected]"
],
"addresses": [
{
"addr-lines": [
"National Institute of Standards and Technology",
"Attn: Computer Security Division",
"Information Technology Laboratory",
"100 Bureau Drive (Mail Stop 8930)"
],
"city": "Gaithersburg",
"state": "MD",
"postal-code": "20899-8930"
}
]
}
],
"responsible-parties": [
{
"role-id": "creator",
"party-uuids": [
"cde369ce-57f8-4ec1-847f-2681a9a881e7"
]
},
{
"role-id": "contact",
"party-uuids": [
"cde369ce-57f8-4ec1-847f-2681a9a881e7"
]
}
]
},
"imports": [
{
"href": "trestle://catalogs/simplified_nist_catalog/catalog.json",
"include-controls": [
{
"with-ids": [
"ac-1",
"ac-2",
"ac-2.1",
"ac-2.2",
"ac-2.3",
"ac-2.4",
"ac-2.5",
"ac-2.13",
"ac-3",
"ac-4",
"ac-4.4"
]
}
]
}
],
"merge": {
"as-is": true
},
"modify": {
"set-parameters": [
{
"param_id": "ac-1_prm_1",
"class": "newclassfromprof",
"depends-on": "newdependsonfromprof",
"usage": "new usage from prof",
"props": [
{
"name": "param_1_prop",
"value": "prop value from prof"
},
{
"name": "param_1_prop_2",
"value": "new prop value from prof"
}
],
"links": [
{
"href": "#123456789",
"text": "new text from prof"
},
{
"href": "#new_link",
"text": "new link text"
}
],
"constraints": [
{
"description": "new constraint"
}
],
"guidelines": [
{
"prose": "new guideline"
}
]
},
{
"param_id": "ac-4.4_prm_3",
"values": [
"hacking the system"
]
},
{
"param_id": "loose_2",
"values": [
"loose_2_val_from_prof"
]
},
{
"param_id": "bad_param_id",
"values": [
"this will cause warning"
]
}
]
}
}
}
37 changes: 21 additions & 16 deletions tests/trestlebot/tasks/authored/test_compdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
import re

import pytest
from trestle.common.model_utils import ModelUtils
from trestle.core.catalog.catalog_interface import CatalogInterface
from trestle.core.profile_resolver import ProfileResolver
from trestle.oscal import profile as prof
from trestle.common.err import TrestleError
from trestle.oscal.profile import Profile

from tests import testutils
from trestlebot.const import RULES_VIEW_DIR, YAML_EXTENSION
from trestlebot.tasks.authored.base_authored import AuthoredObjectException
from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition
from trestlebot.tasks.authored.compdef import (
AuthoredComponentDefinition,
FilterByProfile,
)
from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer


Expand Down Expand Up @@ -89,24 +90,18 @@ def test_create_new_default(tmp_trestle_dir: str) -> None:

def test_create_new_default_with_filter(tmp_trestle_dir: str) -> None:
"""Test creating new default component definition with filter"""

filter_profile = "filter_profile"
# Prepare the workspace
trestle_root = pathlib.Path(tmp_trestle_dir)
_ = testutils.setup_for_profile(trestle_root, test_prof, "")
testutils.load_from_json(trestle_root, filter_profile, filter_profile, Profile)
authored_comp = AuthoredComponentDefinition(tmp_trestle_dir)

profile_path = ModelUtils.get_model_path_for_name_and_class(
trestle_root, test_prof, prof.Profile
)

catalog = ProfileResolver.get_resolved_profile_catalog(
trestle_root, profile_path=profile_path
)

catalog_interface = CatalogInterface(catalog)
catalog_interface.delete_control("ac-5")
filter_by_profile = FilterByProfile(trestle_root, filter_profile)

authored_comp.create_new_default(
test_prof, test_comp, "test", "My desc", "service", catalog_interface
test_prof, test_comp, "test", "My desc", "service", filter_by_profile
)

rules_view_dir = trestle_root / RULES_VIEW_DIR
Expand Down Expand Up @@ -138,3 +133,13 @@ def test_create_new_default_no_profile(tmp_trestle_dir: str) -> None:
authored_comp.create_new_default(
"fake", test_comp, "test", "My desc", "service"
)


def test_filter_by_profile_with_no_profile(tmp_trestle_dir: str) -> None:
"""Test creating a profile filter with a non-existent profile"""
trestle_root = pathlib.Path(tmp_trestle_dir)

with pytest.raises(
TrestleError, match="Profile fake does not exist in the workspace"
):
_ = FilterByProfile(trestle_root, "fake")
35 changes: 26 additions & 9 deletions trestlebot/entrypoints/create_cd.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@

import argparse
import logging
from typing import List
import pathlib
from typing import List, Optional

from trestlebot.const import RULE_PREFIX, RULES_VIEW_DIR
from trestlebot.entrypoints.entrypoint_base import EntrypointBase
from trestlebot.entrypoints.log import set_log_level_from_args
from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition
from trestlebot.tasks.authored.compdef import (
AuthoredComponentDefinition,
FilterByProfile,
)
from trestlebot.tasks.authored.types import AuthoredType
from trestlebot.tasks.base_task import ModelFilter, TaskBase
from trestlebot.tasks.regenerate_task import RegenerateTask
Expand Down Expand Up @@ -79,18 +83,23 @@ def setup_create_cd_arguments(self) -> None:
default="service",
help="Type of component definition",
)
self.parser.add_argument(
"--filter-by-profile",
required=False,
type=str,
help="Optionally filter the controls in the component definition by a profile.",
)

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

set_log_level_from_args(args)
pre_tasks: List[TaskBase] = []
filter_by_profile: Optional[FilterByProfile] = None
trestle_root: pathlib.Path = pathlib.Path(args.working_dir)

# In this case we only want to do the transformation and generation for this component
# definition, so we skip all other component definitions and components.
filter: ModelFilter = ModelFilter(
[], [args.compdef_name, args.component_title, f"{RULE_PREFIX}*"]
)
if args.filter_by_profile:
filter_by_profile = FilterByProfile(trestle_root, args.filter_by_profile)

authored_comp = AuthoredComponentDefinition(args.working_dir)
authored_comp.create_new_default(
Expand All @@ -99,22 +108,30 @@ def run(self, args: argparse.Namespace) -> None:
args.component_title,
args.component_description,
args.component_definition_type,
filter_by_profile,
)

transformer: ToRulesYAMLTransformer = ToRulesYAMLTransformer()

# In this case we only want to do the transformation and generation for this component
# definition, so we skip all other component definitions and components.
workspace_filter: ModelFilter = ModelFilter(
[], [args.compdef_name, args.component_title, f"{RULE_PREFIX}*"]
)

rule_transform_task: RuleTransformTask = RuleTransformTask(
working_dir=args.working_dir,
rules_view_dir=RULES_VIEW_DIR,
rule_transformer=transformer,
filter=filter,
filter=workspace_filter,
)
pre_tasks.append(rule_transform_task)

regenerate_task = RegenerateTask(
working_dir=args.working_dir,
authored_model=AuthoredType.COMPDEF.value,
markdown_dir=args.markdown_path,
filter=filter,
filter=workspace_filter,
)
pre_tasks.append(regenerate_task)

Expand Down
49 changes: 29 additions & 20 deletions trestlebot/tasks/authored/compdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@
from trestlebot.transformers.yaml_transformer import FromRulesYAMLTransformer


class FilterByProfile:
"""Filter controls by a profile."""

def __init__(self, trestle_root: pathlib.Path, profile_name: str) -> None:
"""Initialize."""
filter_profile_path = ModelUtils.get_model_path_for_name_and_class(
trestle_root, profile_name, prof.Profile
)

if filter_profile_path is None:
raise TrestleError(
f"Profile {profile_name} does not exist in the workspace"
)

catalog = ProfileResolver.get_resolved_profile_catalog(
trestle_root, filter_profile_path
)
self._catalog = CatalogInterface(catalog)

def __call__(self, control_id: str) -> bool:
"""Filter controls by catalog."""
return control_id in self._catalog.get_control_ids()


class AuthoredComponentDefinition(AuthorObjectBase):
"""
Class for authoring OSCAL Component Definitions in automation
Expand Down Expand Up @@ -103,7 +127,7 @@ def create_new_default(
comp_title: str,
comp_description: str,
comp_type: str,
filter_controls: Optional[CatalogInterface] = None,
filter_by_profile: Optional[FilterByProfile] = None,
) -> None:
"""
Create the new component definition with default info.
Expand All @@ -114,7 +138,8 @@ def create_new_default(
comp_title: Title of the component
comp_description: Description of the component
comp_type: Type of the component
filter_controls: Optional catalog to filter the controls to include from the profile
filter_by_profile: Optional filter to use for the component definition control
implementation controls
Notes:
The beginning of the Component Definition workflow is to create a new
Expand All @@ -128,7 +153,7 @@ def create_new_default(

if existing_profile_path is None:
raise AuthoredObjectException(
f"Profile {profile_name} does not exist in the workspace"
f"Profile {profile_name} does not exist in the workspace."
)

rule_dir: pathlib.Path = trestle_root.joinpath(RULES_VIEW_DIR, compdef_name)
Expand All @@ -140,28 +165,12 @@ def create_new_default(

rules_view_builder = RulesViewBuilder(trestle_root)

filter_func: Optional[Callable[[str], bool]] = None
if filter_controls is not None:
filter_func = FilterByCatalog(filter_controls)

rules_view_builder.add_rules_for_profile(
existing_profile_path, component_info, filter_func
existing_profile_path, component_info, filter_by_profile
)
rules_view_builder.write_to_yaml(rule_dir)


class FilterByCatalog:
"""Filter controls by catalog."""

def __init__(self, catalog: CatalogInterface) -> None:
"""Initialize."""
self._catalog = catalog

def __call__(self, control_id: str) -> bool:
"""Filter controls by catalog."""
return control_id in self._catalog.get_control_ids()


class RulesViewBuilder:
"""Write TrestleRule objects to YAML files in rules view."""

Expand Down

0 comments on commit fa5d3c2

Please sign in to comment.