Skip to content

Commit

Permalink
feat: ssp filter allows filter of ssp based on profile (#805)
Browse files Browse the repository at this point in the history
* initial version of ssp filter

Signed-off-by: Frank Suits <[email protected]>

* refactor and boost coverage

Signed-off-by: Frank Suits <[email protected]>

* cleaned up smells

Signed-off-by: Frank Suits <[email protected]>

* removed model enum and converted strings to const

Signed-off-by: Frank Suits <[email protected]>

* reworked the fs top_level_model access and cleaned up further

Signed-off-by: Frank Suits <[email protected]>

* final cleanup of const

Signed-off-by: Frank Suits <[email protected]>

* optional on filecontent

Signed-off-by: Frank Suits <[email protected]>
  • Loading branch information
fsuits authored Oct 27, 2021
1 parent 998cdee commit 494ba1b
Show file tree
Hide file tree
Showing 27 changed files with 423 additions and 202 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
https://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

Expand Down
2 changes: 1 addition & 1 deletion docs/license.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
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,
Expand Down
6 changes: 6 additions & 0 deletions docs/trestle_author.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,9 @@ The `profile` author commands allow you to edit additions made by a profile to i
The `ssp-generate` sub-command creates a partial SSP (System Security Plan) from a profile and optional yaml header file. `ssp-assemble` can then assemble the markdown files into a single json SSP file.

For more details on its usage please see [the ssp authoring tutorial](https://ibm.github.io/compliance-trestle/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring).

## `trestle author ssp-filter`

The `ssp-filter` sub-command takes a given SSP and filters its contents based on a given profile. The SSP is assumed to contain a superset of controls needed by the profile, and the filter operation generates a new SSP with just the controls needed by that profile. If the profile references a control not in the SSP, the routine fails with an error.

For more details on its usage please see [the ssp authoring tutorial](https://ibm.github.io/compliance-trestle/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring).
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,11 @@ After manually edting the markdown and providing the responses for the control i
This will assemble the markdown files in the my_ssp directory and create a json SSP with name my_json_ssp in the system-security-plans directory.

As indicated for `ssp-generate`, please do not alter any of the horizontal rule lines or lines indicating the part or control id, e.g. `### Part a.`. You may run `ssp-generate` and `ssp-assemble` repeatedly for the same markdown directory, allowing a continuous editing and updating cycle.

## `trestle author ssp-filter`

Once you have an SSP in the trestle directory you can filter its contents with a profile by using the command `trestle author ssp-filter`. The SSP is assumed to contain a superset of the controls needed by the profile, and the filter operation will generate a new SSP with only those controls needed by the profile. The filter command is invoked as:

`trestle author ssp-filter --name my_ssp --profile my_profile --output my_culled_ssp`

Both the SSP and profile must be present in the trestle directory. This command will generate a new SSP in the directory. If the profile makes reference to a control not in the SSP then the routine will fail with an error message.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- Tutorials:
- Intro to trestle workflow: tutorials/trestle_sample_workflow.md
- Compliance posture: tutorials/continuous-compliance/continuous-compliance.md
- SSP, Catalog, and Profile Authoring: tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md
- Transformer construction: tutorials/task.transformer-construction/transformer-construction.md
- Task - tanium-to-oscal: tutorials/task.tanuim-to-oscal/transformation.md
- Trestle command-line interface (CLI):
Expand Down
2 changes: 1 addition & 1 deletion tests/data/json/test_profile_a.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"imports": [
{
"href": "trestle://catalogs/NIST_SP-800-53_rev5_catalog/catalog.json",
"href": "trestle://catalogs/nist_cat/catalog.json",
"include-controls": [
{
"with-ids": [
Expand Down
2 changes: 1 addition & 1 deletion tests/data/json/test_profile_b.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"imports": [
{
"href": "trestle://catalogs/NIST_SP-800-53_rev5_catalog/catalog.json",
"href": "trestle://catalogs/nist_cat/catalog.json",
"include-controls": [
{
"with-ids": [
Expand Down
2 changes: 1 addition & 1 deletion tests/data/json/test_profile_d.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"imports": [
{
"href": "trestle://catalogs/NIST_SP-800-53_rev5_catalog/catalog.json",
"href": "trestle://catalogs/nist_cat/catalog.json",
"include-controls": [
{
"with-ids": [
Expand Down
32 changes: 32 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
from trestle.cli import Trestle
from trestle.core import const, generators, utils
from trestle.core.base_model import OscalBaseModel
from trestle.core.commands.href import HrefCmd
from trestle.core.commands.import_ import ImportCmd
from trestle.core.common_types import TopLevelOscalModel
from trestle.core.err import TrestleError
from trestle.core.models.file_content_type import FileContentType
from trestle.core.repository import Repository
from trestle.oscal import catalog as cat
from trestle.oscal import common

Expand Down Expand Up @@ -194,6 +196,7 @@ def generate_control_list(label: str, count: int) -> List[cat.Control]:
for ii in range(count):
control = generators.generate_sample_model(cat.Control, True)
control.id = f'{label}-{ii + 1}'
control.params[0].id = f'{control.id}.param'
controls.append(control)
return controls

Expand Down Expand Up @@ -227,3 +230,32 @@ def generate_complex_catalog() -> cat.Catalog:
def patch_raise_exception() -> None:
"""Raise TrestleError exception, to be used for testing."""
raise TrestleError('Forced raising of an errors')


def setup_for_multi_profile(trestle_root: pathlib.Path, big_profile: bool, import_nist_cat: bool) -> None:
"""Initiallize trestle directory with catalogs and profiles."""
repo = Repository(trestle_root)
main_profile_name = 'main_profile'

if big_profile:
prof_path = JSON_NIST_DATA_PATH / 'NIST_SP-800-53_rev5_MODERATE-baseline_profile.json'
else:
prof_path = JSON_TEST_DATA_PATH / 'simple_test_profile.json'
repo.load_and_import_model(prof_path, main_profile_name)

for letter in 'abcd':
prof_name = f'test_profile_{letter}'
prof_path = JSON_TEST_DATA_PATH / f'{prof_name}.json'
repo.load_and_import_model(prof_path, prof_name)

complex_cat = generate_complex_catalog()
repo.import_model(complex_cat, 'complex_cat')

cat_name = 'nist_cat'
cat_path = JSON_NIST_DATA_PATH / JSON_NIST_CATALOG_NAME
if import_nist_cat:
repo.load_and_import_model(cat_path, cat_name)
new_href = f'trestle://catalogs/{cat_name}/catalog.json'
else:
new_href = str(cat_path.resolve())
assert HrefCmd.change_import_href(trestle_root, main_profile_name, new_href, 0) == 0
13 changes: 12 additions & 1 deletion tests/trestle/core/commands/author/catalog_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def test_catalog_interface(sample_catalog_rich_controls: cat.Catalog) -> None:
assert interface._catalog.controls[1].controls[0].title == new_title


def test_catalog_failures(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
def test_catalog_generate_failures(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test failures of author catalog."""
test_args = 'trestle author catalog-generate -n foo -o profiles'.split()
monkeypatch.setattr(sys, 'argv', test_args)
Expand All @@ -109,3 +109,14 @@ def test_catalog_failures(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatc
with pytest.raises(TrestleError):
monkeypatch.setattr(sys, 'argv', test_args)
Trestle().run()


def test_catalog_assemble_failures(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test failurs of catalog assemble."""
test_args = 'trestle author catalog-assemble -m foo -o my_md'.split()
monkeypatch.setattr(sys, 'argv', test_args)
assert Trestle().run() == 1

(tmp_trestle_dir / 'foo').mkdir()
monkeypatch.setattr(sys, 'argv', test_args)
assert Trestle().run() == 1
15 changes: 15 additions & 0 deletions tests/trestle/core/commands/author/profile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,18 @@ def test_profile_generate_assemble(
for name, exp_str in guid_dict['name_exp']:
prose = catalog_interface.get_control_part_prose('ac-1', name)
assert prose.find(exp_str) >= 0


def test_profile_failures(tmp_trestle_dir: pathlib.Path, tmp_path: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test failure modes of profile generate and assemble."""
test_args = 'trestle author profile-generate -n my_prof -o profiles'.split()
monkeypatch.setattr(sys, 'argv', test_args)
assert Trestle().run() == 1

# no trestle root specified
test_args = 'trestle author profile-generate -n my_prof -o new_prof'.split()
profile_generate = ProfileGenerate()
assert profile_generate._run(test_args) == 1

profile_assemble = ProfileAssemble()
assert profile_assemble._run(test_args) == 1
71 changes: 43 additions & 28 deletions tests/trestle/core/commands/author/ssp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,25 @@

from tests import test_utils

import trestle.oscal.ssp as ossp
from trestle.core import const
from trestle.core.commands.author.ssp import SSPAssemble, SSPGenerate
from trestle.core.commands.href import HrefCmd
from trestle.core.commands.import_ import ImportCmd
from trestle.core.commands.author.ssp import SSPAssemble, SSPFilter, SSPGenerate
from trestle.core.control_io import ControlIOReader
from trestle.core.markdown.markdown_api import MarkdownAPI
from trestle.core.profile_resolver import ProfileResolver
from trestle.utils import fs

prof_name = 'my_prof'
prof_name = 'main_profile'
ssp_name = 'my_ssp'
cat_name = 'imported_nist_cat'
cat_name = 'nist_cat'


def setup_for_ssp(include_header: bool,
big_profile: bool,
tmp_trestle_dir: pathlib.Path,
import_cat: bool = True) -> Tuple[argparse.Namespace, str]:
import_nist_cat: bool = True) -> Tuple[argparse.Namespace, str]:
"""Create the markdown ssp content from catalog and profile."""
cat_path = test_utils.JSON_NIST_DATA_PATH / test_utils.JSON_NIST_CATALOG_NAME
if big_profile:
prof_path = test_utils.JSON_NIST_DATA_PATH / 'NIST_SP-800-53_rev5_MODERATE-baseline_profile.json'
else:
prof_path = test_utils.JSON_TEST_DATA_PATH / 'simple_test_profile.json'
args = argparse.Namespace(
trestle_root=tmp_trestle_dir, file=str(prof_path), output=prof_name, verbose=True, regenerate=True
)
i = ImportCmd()
assert i._run(args) == 0

# need to change href in profile to either imported location or cached external
if import_cat:
args = argparse.Namespace(
trestle_root=tmp_trestle_dir, file=str(cat_path), output=cat_name, verbose=True, regenerate=True
)
assert i._run(args) == 0
new_href = f'trestle://catalogs/{cat_name}/catalog.json'
else:
new_href = str(cat_path.resolve())
assert HrefCmd.change_import_href(tmp_trestle_dir, prof_name, new_href, 0) == 0
test_utils.setup_for_multi_profile(tmp_trestle_dir, big_profile, import_nist_cat)

yaml_path = test_utils.YAML_TEST_DATA_PATH / 'good_simple.yaml'
sections = 'ImplGuidance:Implementation Guidance,ExpectedEvidence:Expected Evidence,guidance:Guidance'
Expand Down Expand Up @@ -197,7 +177,7 @@ def test_ssp_assemble(tmp_trestle_dir: pathlib.Path) -> None:
def test_ssp_generate_bad_name(tmp_trestle_dir: pathlib.Path) -> None:
"""Test bad output name."""
args = argparse.Namespace(
trestle_root=tmp_trestle_dir, profile='my_prof', output='catalogs', verbose=True, yaml_header='dummy.yaml'
trestle_root=tmp_trestle_dir, profile=prof_name, output='catalogs', verbose=True, yaml_header='dummy.yaml'
)
ssp_cmd = SSPGenerate()
assert ssp_cmd._run(args) == 1
Expand All @@ -217,3 +197,38 @@ def test_profile_resolver(tmp_trestle_dir: pathlib.Path) -> None:
assert len(resolved_catalog.groups) == 18

resolved_catalog.oscal_write(new_catalog_path)


def test_ssp_filter(tmp_trestle_dir: pathlib.Path) -> None:
"""Test the ssp filter."""
# install the catalog and profiles
gen_args, _, _ = setup_for_ssp(False, False, tmp_trestle_dir, True)
# create markdown with profile a
gen_args.profile = 'test_profile_a'
ssp_gen = SSPGenerate()
assert ssp_gen._run(gen_args) == 0

# create ssp from the markdown
ssp_assemble = SSPAssemble()
args = argparse.Namespace(trestle_root=tmp_trestle_dir, markdown=ssp_name, output=ssp_name, verbose=True)
assert ssp_assemble._run(args) == 0

# load the ssp so we can add a setparameter to it for more test coverage
ssp, _ = fs.load_top_level_model(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan, fs.FileContentType.JSON)
new_setparam = ossp.SetParameter(param_id='ac-1_prm_1', values=['new_value'])
ssp.control_implementation.set_parameters = [new_setparam]
fs.save_top_level_model(ssp, tmp_trestle_dir, ssp_name, fs.FileContentType.JSON)

# now filter the ssp through test_profile_d
args = argparse.Namespace(
trestle_root=tmp_trestle_dir, name=ssp_name, profile='test_profile_d', output='filtered_ssp', verbose=True
)
ssp_filter = SSPFilter()
assert ssp_filter._run(args) == 0

# now filter the ssp through test_profile_b
args = argparse.Namespace(
trestle_root=tmp_trestle_dir, name=ssp_name, profile='test_profile_b', output='filtered_ssp', verbose=True
)
ssp_filter = SSPFilter()
assert ssp_filter._run(args) == 1
Loading

0 comments on commit 494ba1b

Please sign in to comment.