Repository: oscal-compass/compliance-trestle (python)

Issue:
Add RISK support in to component definition spread sheet (csv) support 
## Issue description / feature objectives

Add the following optional columns as expected but optional to the csv-to-oscal-cd task:

- $Original_Risk_Rating
    - Provide the Original Risk Rating from the scanner
- $Adjusted_Risk_Rating
    - Provide the Adjusted Risk Rating as approved by the CIO
- $Risk_Adjustment
    - Whether there was a Risk Adjustment

These values, if specified, are to be added to the resulting OSCAL component definition as Properties.

## Caveats / Assumptions

Provide sample/test/tutorial csv's with these additional fields/descriptions.

## Completion Criteria

Test cases for code coverage added.


Pull request:
feat: add risk properties support to csv-to-oscal-cd task
## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Hot fix (emergency fix and release)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Documentation (change which affects the documentation site)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Release (`develop` -> `main`)

## Quality assurance (all should be covered).

- [x] My code follows the code style of this project.
- [x] Documentation for my change is up to date?
- [x] My PR meets testing requirements.
- [x] All new and existing tests passed.
- [x] All commits are signed-off.

## Summary

## Key links:

Resolved #1544
My approach to this issue is as follows:

In `csv-to-oscal-cd.py`:

1. Add three constants to represent the three risk headings.
2. Add the constants to the `_columns_optional` and `_rule_property_column_names` lists because they are optional and not required when component type is validation.
3. Modify the `print_info()` to correctly show the three newly added optional headings.

Then, make a new csv file with all three risk headings added for testing.

In `csv-to-oscal-cd-test.py`:
1. Write one test case to verify the presence of the three risk props in the resulting `component-definition.json`.
2. Write one test case to verify the absence of the three risk props in the resulting `component-definition.json`, when the component type is validation. 

# Before you merge

- Ensure it is a 'squash commit' if not a release.
- Ensure CI is currently passing
- Check sonar. If you are working for a fork a maintainer will reach out, if required.


Patch:
diff --git a/trestle/tasks/csv_to_oscal_cd.py b/trestle/tasks/csv_to_oscal_cd.py
index 72447359..455b6131 100644
--- a/trestle/tasks/csv_to_oscal_cd.py
+++ b/trestle/tasks/csv_to_oscal_cd.py
@@ -61,6 +61,9 @@ PARAMETER_ID = f'{PARAMETER}_Id'
 PARAMETER_DESCRIPTION = f'{PARAMETER}_Description'
 PARAMETER_VALUE_DEFAULT = f'{PARAMETER}_Value_Default'
 PARAMETER_VALUE_ALTERNATIVES = f'{PARAMETER}_Value_Alternatives'
+ORIGINAL_RISK_RATING = 'Original_Risk_Rating'
+ADJUSTED_RISK_RATING = 'Adjusted_Risk_Rating'
+RISK_ADJUSTMENT = 'Risk_Adjustment'
 
 validation = 'validation'
 prefix_rule_set = 'rule_set_'
@@ -190,7 +193,10 @@ class CsvToOscalComponentDefinition(TaskBase):
             text1 = '                         '
         text1 = '  optional columns:      '
         for text2 in CsvColumn.get_optional_column_names():
-            text2 += ' (see note 2)'
+            if text2 in [f'{ORIGINAL_RISK_RATING}', f'{ADJUSTED_RISK_RATING}', f'{RISK_ADJUSTMENT}']:
+                text2 += ' (see note 1)'
+            else:
+                text2 += ' (see note 2)'
             logger.info(text1 + '$' + text2)
             text1 = '                         '
         for text2 in CsvColumn.get_parameter_column_names():
@@ -1367,6 +1373,9 @@ class CsvColumn():
     _columns_optional = [
         f'{CHECK_ID}',
         f'{CHECK_DESCRIPTION}',
+        f'{ORIGINAL_RISK_RATING}',
+        f'{ADJUSTED_RISK_RATING}',
+        f'{RISK_ADJUSTMENT}',
     ]
 
     _columns_parameter = [
@@ -1440,6 +1449,9 @@ class CsvColumn():
         f'{PARAMETER_VALUE_ALTERNATIVES}',
         f'{CHECK_ID}',
         f'{CHECK_DESCRIPTION}',
+        f'{ORIGINAL_RISK_RATING}',
+        f'{ADJUSTED_RISK_RATING}',
+        f'{RISK_ADJUSTMENT}',
     ]
 
     @staticmethod


Test patch:
diff --git a/tests/trestle/tasks/csv_to_oscal_cd_test.py b/tests/trestle/tasks/csv_to_oscal_cd_test.py
index 392b4a53..e1d075ec 100644
--- a/tests/trestle/tasks/csv_to_oscal_cd_test.py
+++ b/tests/trestle/tasks/csv_to_oscal_cd_test.py
@@ -1234,6 +1234,87 @@ def test_execute_add_property(tmp_path: pathlib.Path) -> None:
     assert component.props[6].value == 'add-fetcher-description'
 
 
+def test_execute_with_risk_properties(tmp_path: pathlib.Path) -> None:
+    """Test execute with risk properties."""
+    _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config')
+    # add risk properties
+    rows = _get_rows('tests/data/csv/bp.sample.v2.csv')
+    rows[0].append('Original_Risk_Rating')
+    rows[0].append('Adjusted_Risk_Rating')
+    rows[0].append('Risk_Adjustment')
+    # row 3 will be tested
+    rows[2].append('add-original-risk-rating')
+    rows[2].append('add-adjusted-risk-rating')
+    rows[2].append('add-risk-adjustment')
+    # ensure all rows have a value for the risk columns to execute task successfully
+    for row in rows[3:]:
+        row.append('')
+        row.append('')
+        row.append('')
+    with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader:
+        mock_csv_reader.return_value = rows
+        tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section)
+        retval = tgt.execute()
+        assert retval == TaskOutcome.SUCCESS
+    # read component-definition
+    fp = pathlib.Path(tmp_path) / 'component-definition.json'
+    cd = ComponentDefinition.oscal_read(fp)
+    # spot check
+    component = cd.components[0]
+    # the bp.sample.v2.csv before mock has 62 properties
+    assert len(component.props) == 65
+    assert component.props[0].name == 'Rule_Id'
+    assert component.props[1].name == 'Rule_Description'
+    assert component.props[2].name == 'Check_Id'
+    assert component.props[3].name == 'Check_Description'
+    assert component.props[4].name == 'Original_Risk_Rating'
+    assert component.props[4].value == 'add-original-risk-rating'
+    assert component.props[5].name == 'Adjusted_Risk_Rating'
+    assert component.props[5].value == 'add-adjusted-risk-rating'
+    assert component.props[6].name == 'Risk_Adjustment'
+    assert component.props[6].value == 'add-risk-adjustment'
+
+
+def test_execute_with_ignored_risk_properties(tmp_path: pathlib.Path) -> None:
+    """Test execute with ignored risk properties when component type is validation."""
+    _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config')
+    # add risk properties
+    rows = _get_rows('tests/data/csv/bp.sample.v2.csv')
+    rows[0].append('Original_Risk_Rating')
+    rows[0].append('Adjusted_Risk_Rating')
+    rows[0].append('Risk_Adjustment')
+    # row 3 will be tested
+    rows[2].append('add-original-risk-rating')
+    rows[2].append('add-adjusted-risk-rating')
+    rows[2].append('add-risk-adjustment')
+    # ensure all rows have a value for the risk columns to execute task successfully
+    for row in rows[3:]:
+        row.append('')
+        row.append('')
+        row.append('')
+    # set validation component type
+    assert rows[0][9] == 'Component_Type'
+    assert rows[2][9] == 'Service'
+    rows[2][9] = 'Validation'
+    with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader:
+        mock_csv_reader.return_value = rows
+        tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section)
+        retval = tgt.execute()
+        assert retval == TaskOutcome.SUCCESS
+    # read component-definition
+    fp = pathlib.Path(tmp_path) / 'component-definition.json'
+    cd = ComponentDefinition.oscal_read(fp)
+    # spot check
+    component = cd.components[0]
+    assert component.type == 'Validation'
+    # there are 4 component validation props: Rule_Id, Check_Id, Check_Description, Reference_Id
+    assert len(component.props) == 4
+    for prop in component.props:
+        assert prop.name != 'Original_Risk_Rating'
+        assert prop.name != 'Adjusted_Risk_Rating'
+        assert prop.name != 'Risk_Adjustment'
+
+
 def test_execute_add_user_property(tmp_path: pathlib.Path) -> None:
     """Test execute add user property."""
     _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config')