Skip to content

Commit

Permalink
PSCE-245 - Add pydantic for basic data validation of Trestle Rule (#50)
Browse files Browse the repository at this point in the history
* refactor: reworks csv_to_yaml for pydantic

Update write empty keys to write default keys
Create transformer for CSV reading and writing
Moves CSV specific logic t csv_transformer
Moves YAML specific logic to yaml_transformer and adds writing logic

Signed-off-by: Jennifer Power <[email protected]>

* docs: updates comment

Signed-off-by: Jennifer Power <[email protected]>

* docs: improves comment and variable naming

Signed-off-by: Jennifer Power <[email protected]>

* refactor: reduce extra code in RulesYAMLTransformer for simplicity

Signed-off-by: Jennifer Power <[email protected]>

* feat: adds Field aliases to remove underscores from YAML

Signed-off-by: Jennifer Power <[email protected]>

* refactor: breaks transformer into ToRuleTransformer and FromRuleTransformer

To align with upstream trestle, the transformers are broken down into single
responsibilties and importing the Transformer Base.

Signed-off-by: Jennifer Power <[email protected]>

---------

Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 authored Oct 9, 2023
1 parent fb1ad0b commit e05c334
Show file tree
Hide file tree
Showing 19 changed files with 759 additions and 437 deletions.
161 changes: 72 additions & 89 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ compliance-trestle = {git = "https://github.com/IBM/compliance-trestle.git", rev
github3-py = "^4.0.1"
python-gitlab = "^3.15.0"
ruamel-yaml = "^0.17.32"
pydantic = "1.10.2"

[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
Expand Down
106 changes: 105 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,22 @@
import os
import pathlib
from tempfile import TemporaryDirectory
from typing import Generator, Tuple, TypeVar
from typing import Any, Dict, Generator, Tuple, TypeVar

import pytest
from git.repo import Repo
from trestle.common.err import TrestleError
from trestle.core.commands.init import InitCmd

from trestlebot import const
from trestlebot.transformers.trestle_rule import (
ComponentInfo,
Control,
Parameter,
Profile,
TrestleRule,
)


T = TypeVar("T")

Expand Down Expand Up @@ -74,3 +83,98 @@ def tmp_trestle_dir() -> YieldFixture[str]:
f"Initialization failed for temporary trestle directory: {e}."
)
yield tmpdir


@pytest.fixture(scope="function")
def valid_rule_data() -> Dict[str, Any]:
return {
const.RULE_INFO_TAG: {
const.NAME: "example_rule_1",
const.DESCRIPTION: "My rule description for example rule 1",
const.PROFILE: {
const.DESCRIPTION: "Simple NIST Profile",
const.HREF: "profiles/simplified_nist_profile/profile.json",
},
const.PARAMETER: {
const.NAME: "prm_1",
const.DESCRIPTION: "prm_1 description",
const.ALTERNATIVE_VALUES: {
"default": "5%",
"5pc": "5%",
"10pc": "10%",
"15pc": "15%",
"20pc": "20%",
},
const.DEFAULT_VALUE: "5%",
},
}
}


@pytest.fixture(scope="function")
def invalid_param_rule_data() -> Dict[str, Any]:
return {
const.RULE_INFO_TAG: {
const.NAME: "example_rule_1",
const.DESCRIPTION: "My rule description for example rule 1",
const.PROFILE: {
const.DESCRIPTION: "Simple NIST Profile",
const.HREF: "profiles/simplified_nist_profile/profile.json",
},
const.PARAMETER: {
const.NAME: "prm_1",
const.DESCRIPTION: "prm_1 description",
const.ALTERNATIVE_VALUES: {
"5pc": "5%",
"10pc": "10%",
"15pc": "15%",
"20pc": "20%",
},
const.DEFAULT_VALUE: "5%",
},
}
}


@pytest.fixture(scope="function")
def missing_key_rule_data() -> Dict[str, Any]:
return {
const.RULE_INFO_TAG: {
const.DESCRIPTION: "My rule description for example rule 1",
const.PROFILE: {
const.DESCRIPTION: "Simple NIST Profile",
const.HREF: "profiles/simplified_nist_profile/profile.json",
},
const.PARAMETER: {
const.NAME: "prm_1",
const.DESCRIPTION: "prm_1 description",
const.ALTERNATIVE_VALUES: {
"default": "5%",
"5pc": "5%",
"10pc": "10%",
"15pc": "15%",
"20pc": "20%",
},
const.DEFAULT_VALUE: "5%",
},
}
}


@pytest.fixture(scope="function")
def test_rule() -> TrestleRule:
test_trestle_rule: TrestleRule = TrestleRule(
name="test",
description="test",
component=ComponentInfo(name="test_comp", type="test", description="test"),
parameter=Parameter(
name="test",
description="test",
alternative_values={},
default_value="test",
),
profile=Profile(
description="test", href="test", include_controls=[Control(id="ac-1")]
),
)
return test_trestle_rule
17 changes: 17 additions & 0 deletions tests/data/yaml/test_invalid_rule.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
x-trestle-rule-info:
name: example_rule_1
description: My rule description for example rule 1
parameter:
name: prm_1
description: prm_1 description
alternative-values: "invalid"
default-value: true
profile:
description: Simple NIST Profile
href: profiles/simplified_nist_profile/profile.json
include-controls:
- id: ac-2
x-trestle-component-info:
name: Component 1
description: Component 1 description
type: service
17 changes: 17 additions & 0 deletions tests/data/yaml/test_rule_invalid_params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
x-trestle-rule-info:
name: example_rule_1
description: My rule description for example rule 1
parameter:
name: prm_1
description: prm_1 description
alternative-values: {'5pc': '5%', '10pc': '10%', '15pc': '15%', '20pc': '20%'}
default-value: '5%'
profile:
description: Simple NIST Profile
href: profiles/simplified_nist_profile/profile.json
include-controls:
- id: ac-2
x-trestle-component-info:
name: Component 1
description: Component 1 description
type: service
10 changes: 5 additions & 5 deletions tests/trestlebot/tasks/test_rule_transform_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from tests.testutils import setup_rules_view
from trestlebot.tasks.base_task import TaskException
from trestlebot.tasks.rule_transform_task import RuleTransformTask
from trestlebot.transformers.yaml_to_csv import RulesYAMLTransformer
from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer


test_comp = "test_comp"
Expand All @@ -39,7 +39,7 @@ def test_rule_transform_task(tmp_trestle_dir: str) -> None:
"""Test rule transform task."""
trestle_root = pathlib.Path(tmp_trestle_dir)
setup_rules_view(trestle_root, test_comp, test_rules_dir)
transformer = RulesYAMLTransformer()
transformer = ToRulesYAMLTransformer()
rule_transform_task = RuleTransformTask(
tmp_trestle_dir, test_rules_dir, transformer
)
Expand Down Expand Up @@ -70,7 +70,7 @@ def test_rule_transform_task_with_no_rules(tmp_trestle_dir: str) -> None:
"""Test rule transform task with no rules."""
trestle_root = pathlib.Path(tmp_trestle_dir)
setup_rules_view(trestle_root, test_comp, test_rules_dir, skip_rules=True)
transformer = RulesYAMLTransformer()
transformer = ToRulesYAMLTransformer()
rule_transform_task = RuleTransformTask(
tmp_trestle_dir, test_rules_dir, transformer
)
Expand All @@ -85,7 +85,7 @@ def test_rule_transform_task_with_invalid_rule(tmp_trestle_dir: str) -> None:
"""Test rule transform task with invalid rule."""
trestle_root = pathlib.Path(tmp_trestle_dir)
setup_rules_view(trestle_root, test_comp, test_rules_dir, incomplete_rule=True)
transformer = RulesYAMLTransformer()
transformer = ToRulesYAMLTransformer()
rule_transform_task = RuleTransformTask(
tmp_trestle_dir, test_rules_dir, transformer
)
Expand All @@ -100,7 +100,7 @@ def test_rule_transform_task_with_skip(tmp_trestle_dir: str) -> None:
"""Test rule transform task with skip."""
trestle_root = pathlib.Path(tmp_trestle_dir)
setup_rules_view(trestle_root, test_comp, test_rules_dir)
transformer = RulesYAMLTransformer()
transformer = ToRulesYAMLTransformer()
rule_transform_task = RuleTransformTask(
tmp_trestle_dir, test_rules_dir, transformer, skip_model_list=[test_comp]
)
Expand Down
34 changes: 22 additions & 12 deletions tests/trestlebot/transformers/test_csv_to_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@

import csv
import pathlib
from dataclasses import fields

import pytest
import ruamel.yaml as yaml
from ruamel.yaml import YAML

from trestlebot.transformers.csv_to_yaml import YAMLBuilder
from trestlebot.transformers.trestle_rule import TrestleRule
from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -71,18 +70,29 @@ def test_write_to_yaml(setup_yaml_builder: YAMLBuilder, tmp_trestle_dir: str) ->
write_sample_csv(csv_file)
setup_yaml_builder.read_from_csv(csv_file)
setup_yaml_builder.write_to_yaml(yaml_file)
yaml = YAML(typ="safe")
with open(yaml_file, "r") as f:
data = yaml.safe_load(f)
assert len(data) == 1
data = yaml.load(f)
# The file will contain a separate YAML document for each rule
assert len(data) == 2


def test_write_empty_trestle_rule_keys(
def test_default_test_trestle_rule_keys(
setup_yaml_builder: YAMLBuilder, tmp_trestle_dir: str
) -> None:
yaml_file = pathlib.Path(tmp_trestle_dir) / "test.yaml"
setup_yaml_builder.write_empty_trestle_rule_keys(yaml_file)
with open(yaml_file, "r") as f:
data = yaml.safe_load(f)
assert all(value == "" for value in data.values())
expected_keys = {field.name for field in fields(TrestleRule)}
assert expected_keys == set(data.keys())
setup_yaml_builder.write_default_trestle_rule_keys(yaml_file)

# Check that the YAML file written is valid and integrates with the rule
# YAML transformer
transformer = ToRulesYAMLTransformer()
rule = transformer.transform(yaml_file.read_text())

assert rule.name == "example rule"
assert rule.description == "example description"
assert rule.component.name == "example component"
assert rule.component.description == "example description"
assert rule.component.type == "service"
assert rule.profile.description == "example profile"
assert rule.profile.href == "example href"
assert len(rule.profile.include_controls) == 1
70 changes: 70 additions & 0 deletions tests/trestlebot/transformers/test_csv_transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/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 CSV Transformer."""

import csv
import pathlib
from typing import List

import pytest

from trestlebot.transformers.csv_transformer import CSVBuilder
from trestlebot.transformers.trestle_rule import TrestleRule


def test_csv_builder(test_rule: TrestleRule, tmp_trestle_dir: str) -> None:
"""Test CSV builder on a happy path"""

csv_builder = CSVBuilder()
csv_builder.add_row(test_rule)

assert len(csv_builder._rows) == 1
row = csv_builder._rows[0]
assert row["Rule_Id"] == test_rule.name
assert row["Rule_Description"] == test_rule.description
assert row["Component_Title"] == test_rule.component.name
assert row["Component_Type"] == test_rule.component.type
assert row["Component_Description"] == test_rule.component.description
assert row["Control_Id_List"] == "ac-1"
assert row["Parameter_Id"] == test_rule.parameter.name # type: ignore
assert row["Parameter_Description"] == test_rule.parameter.description # type: ignore
assert row["Parameter_Value_Alternatives"] == "{}"
assert row["Parameter_Value_Default"] == test_rule.parameter.default_value # type: ignore
assert row["Profile_Description"] == test_rule.profile.description
assert row["Profile_Source"] == test_rule.profile.href

trestle_root = pathlib.Path(tmp_trestle_dir)
tmp_csv_path = trestle_root.joinpath("test.csv")
csv_builder.write_to_file(tmp_csv_path)

assert tmp_csv_path.exists()

first_row: List[str] = []
with open(tmp_csv_path, "r", newline="") as csvfile:
csv_reader = csv.reader(csvfile)
first_row = next(csv_reader)

for column in csv_builder._csv_columns.get_required_column_names():
assert column in first_row


def test_validate_row() -> None:
"""Test validate row with an invalid row."""
row = {"Rule_Id": "test"}
csv_builder = CSVBuilder()
with pytest.raises(RuntimeError, match="Row missing key: *"):
csv_builder.validate_row(row)
Loading

0 comments on commit e05c334

Please sign in to comment.