Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PSCE-245 - Add pydantic for basic data validation of Trestle Rule #50

Merged
merged 6 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading