Skip to content

Commit 8171357

Browse files
authored
feat: task cis-xlsx-to-oscal-cd (#1764)
* task cis-xlsx-to-oscal-cd Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * docs Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * signatures & trace Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * tmpdir Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * hack Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * give up. Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix failing macos pipeline (thanks Vikas!) Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * SortHelper Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Add debugging try/except Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * 2nd try Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * add utf-8 encoding to open Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix sonar code smells Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * improve test spot checking Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * tests: bad_confg and bad_overwrite Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Simplify & improve test coverage Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * 100% test coverage! Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> * Fix license. Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com> --------- Signed-off-by: Lou DeGenaro <lou.degenaro@gmail.com>
1 parent 9d5ebf3 commit 8171357

12 files changed

+1253
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: trestle.tasks.cis_xlsx_to_oscal_cd
3+
description: Documentation for trestle.tasks.cis_xlsx_to_oscal_cd module
4+
---
5+
6+
::: trestle.tasks.cis_xlsx_to_oscal_cd
7+
handler: python
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[task.cis-xlsx-to-oscal-cd]
2+
3+
benchmark-file = tests/data/tasks/cis-xlsx-to-oscal-cd/CIS_IBM_Db2_11_Benchmark_v1.1.0.snippet.xlsx
4+
benchmark-title = CIS IBM Db2 11 Benchmark
5+
benchmark-version = 1.1.0
6+
7+
namespace = https://oscal-compass/compliance-trestle/schemas/oscal/cd
8+
9+
component-name = IBM Db2 11
10+
component-description = IBM Db2 11
11+
component-type = software
12+
13+
profile-version = v8
14+
profile-source = catalogs/CIS_controls_v8/catalog.json
15+
profile-description = CIS catalog v8
16+
17+
output-dir = tests/data/tasks/cis-xlsx-to-oscal-cd/output
18+
output-overwrite = true

tests/trestle/core/utils_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from trestle.common.err import TrestleError
4242
from trestle.common.model_utils import ModelUtils
4343
from trestle.common.str_utils import AliasMode
44+
from trestle.common.str_utils import as_bool
4445

4546

4647
def load_good_catalog() -> catalog.Catalog:
@@ -352,3 +353,10 @@ def test_prune_empty_dirs(tmp_path: pathlib.Path) -> None:
352353
assert not (tmp_path / 'sub1/sub11/sub111').exists()
353354
assert foo_path.exists()
354355
assert bar_path.exists()
356+
357+
358+
def test_as_bool(tmp_path: pathlib.Path) -> None:
359+
"""Test as_bool function."""
360+
assert as_bool('true')
361+
assert not as_bool('false')
362+
assert not as_bool(None)
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# Copyright (c) 2025 The OSCAL Compass Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""cis-xlsx-to-oscal-cd task tests."""
15+
16+
import configparser
17+
import os
18+
import pathlib
19+
from typing import Dict
20+
from unittest.mock import patch
21+
22+
import trestle.tasks.cis_xlsx_to_oscal_cd as cis_xlsx_to_oscal_cd
23+
from trestle.oscal.component import ComponentDefinition
24+
from trestle.tasks.base_task import TaskOutcome
25+
26+
db2_config = 'tests/data/tasks/cis-xlsx-to-oscal-cd/test-cis-xlsx-to-oscal-cd.db2.snippet.config'
27+
28+
29+
def _get_section(tmp_path: pathlib.Path, file_: str) -> Dict:
30+
"""Get section."""
31+
config = configparser.ConfigParser()
32+
config_path = pathlib.Path(file_)
33+
config.read(config_path)
34+
section = config['task.cis-xlsx-to-oscal-cd']
35+
section['output-dir'] = str(tmp_path)
36+
return section
37+
38+
39+
def test_cis_xlsx_to_oscal_cd_compare(tmp_path: pathlib.Path):
40+
"""Test compare."""
41+
x = cis_xlsx_to_oscal_cd.SortHelper.compare('A', 'B')
42+
assert x == -1
43+
x = cis_xlsx_to_oscal_cd.SortHelper.compare('0.0', '1.0')
44+
assert x == -1
45+
x = cis_xlsx_to_oscal_cd.SortHelper.compare('1.0', '0.0')
46+
assert x == 1
47+
x = cis_xlsx_to_oscal_cd.SortHelper.compare('1.1', '1.1')
48+
assert x == 0
49+
50+
51+
def test_cis_xlsx_to_oscal_cd_print_info(tmp_path: pathlib.Path):
52+
"""Test print_info call."""
53+
section = _get_section(tmp_path, db2_config)
54+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
55+
retval = tgt.print_info()
56+
assert retval is None
57+
58+
59+
def test_cis_xlsx_to_oscal_cd_simulate(tmp_path: pathlib.Path):
60+
"""Test simulate call."""
61+
section = _get_section(tmp_path, db2_config)
62+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
63+
retval = tgt.simulate()
64+
assert retval == TaskOutcome.SIM_SUCCESS
65+
assert len(os.listdir(str(tmp_path))) == 0
66+
67+
68+
def test_cis_xlsx_to_oscal_cd_execute(tmp_path: pathlib.Path):
69+
"""Test execute call - db2."""
70+
section = _get_section(tmp_path, db2_config)
71+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
72+
retval = tgt.execute()
73+
assert retval == TaskOutcome.SUCCESS
74+
_validate_db2(tmp_path)
75+
76+
77+
def test_cis_xlsx_to_oscal_cd_execute_combined(tmp_path: pathlib.Path):
78+
"""Test execute call - db2."""
79+
section = _get_section(tmp_path, db2_config)
80+
section['benchmark-file'
81+
] = 'tests/data/tasks/cis-xlsx-to-oscal-cd/CIS_IBM_Db2_11_Benchmark_v1.1.0.snippet_combined.xlsx'
82+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
83+
retval = tgt.execute()
84+
assert retval == TaskOutcome.SUCCESS
85+
_validate_db2(tmp_path)
86+
87+
88+
def test_cis_xlsx_to_oscal_cd_execute_missing_column(tmp_path: pathlib.Path):
89+
"""Test execute call - missing column."""
90+
section = _get_section(tmp_path, db2_config)
91+
section['benchmark-file'
92+
] = 'tests/data/tasks/cis-xlsx-to-oscal-cd/CIS_IBM_Db2_11_Benchmark_v1.1.0.snippet_missing_column.xlsx'
93+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
94+
retval = tgt.execute()
95+
assert retval == TaskOutcome.FAILURE
96+
97+
98+
def test_cis_xlsx_to_oscal_cd_execute_bad_config(tmp_path: pathlib.Path):
99+
"""Test execute call - bad config."""
100+
section = _get_section(tmp_path, db2_config)
101+
del section['benchmark-file']
102+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
103+
retval = tgt.execute()
104+
assert retval == TaskOutcome.FAILURE
105+
106+
107+
def test_cis_xlsx_to_oscal_cd_execute_bad_overwrite(tmp_path: pathlib.Path):
108+
"""Test execute call - bad overwrite."""
109+
section = _get_section(tmp_path, db2_config)
110+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
111+
retval = tgt.execute()
112+
assert retval == TaskOutcome.SUCCESS
113+
section['output-overwrite'] = 'false'
114+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
115+
retval = tgt.execute()
116+
assert retval == TaskOutcome.FAILURE
117+
118+
119+
def test_cis_xlsx_to_oscal_cd_execute_merge(tmp_path: pathlib.Path):
120+
"""Test execute call - merge."""
121+
section = _get_section(tmp_path, db2_config)
122+
section['benchmark-file'
123+
] = 'tests/data/tasks/cis-xlsx-to-oscal-cd/CIS_IBM_Db2_11_Benchmark_v1.1.0.snippet_merge.xlsx'
124+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
125+
retval = tgt.execute()
126+
assert retval == TaskOutcome.SUCCESS
127+
_validate_db2(tmp_path)
128+
129+
130+
def test_cis_xlsx_to_oscal_cd_execute_rule_prefix(tmp_path: pathlib.Path):
131+
"""Test execute call - rule prefix."""
132+
section = _get_section(tmp_path, db2_config)
133+
section['benchmark-rule-prefix'] = 'CIS'
134+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
135+
retval = tgt.execute()
136+
assert retval == TaskOutcome.SUCCESS
137+
_validate_db2(tmp_path)
138+
139+
140+
def test_cis_xlsx_to_oscal_cd_execute_control_prefix(tmp_path: pathlib.Path):
141+
"""Test execute call - control prefix."""
142+
section = _get_section(tmp_path, db2_config)
143+
section['benchmark-control-prefix'] = 'cisc'
144+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
145+
retval = tgt.execute()
146+
assert retval == TaskOutcome.SUCCESS
147+
_validate_db2(tmp_path)
148+
149+
150+
def test_cis_xlsx_to_oscal_cd_execute_control_bad(tmp_path: pathlib.Path):
151+
"""Test execute call - control bad."""
152+
section = _get_section(tmp_path, db2_config)
153+
section['benchmark-file'
154+
] = 'tests/data/tasks/cis-xlsx-to-oscal-cd/CIS_IBM_Db2_11_Benchmark_v1.1.0.snippet_bad_control.xlsx'
155+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
156+
retval = tgt.execute()
157+
assert retval == TaskOutcome.FAILURE
158+
159+
160+
def test_cis_xlsx_to_oscal_cd_execute_columns_exclude(tmp_path: pathlib.Path):
161+
"""Test execute call - control prefix."""
162+
section = _get_section(tmp_path, db2_config)
163+
section['columns-exclude'] = '"Recommendation #", "Profile", "Description"'
164+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
165+
retval = tgt.execute()
166+
assert retval == TaskOutcome.SUCCESS
167+
_validate_db2(tmp_path)
168+
169+
170+
def test_cis_xlsx_to_oscal_cd_execute_config_missing(tmp_path: pathlib.Path):
171+
"""Test execute call - config_missing."""
172+
section = None
173+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
174+
retval = tgt.execute()
175+
assert retval == TaskOutcome.FAILURE
176+
177+
178+
def test_cis_xlsx_to_oscal_cd_execute_count_mismatch(tmp_path: pathlib.Path):
179+
"""Test execute call - count mismatch."""
180+
with patch('trestle.tasks.cis_xlsx_to_oscal_cd.CombineHelper._populate_combined_map') as mock_original_function:
181+
mock_original_function.return_value = -5
182+
section = _get_section(tmp_path, db2_config)
183+
tgt = cis_xlsx_to_oscal_cd.CisXlsxToOscalCd(section)
184+
retval = tgt.execute()
185+
assert retval == TaskOutcome.FAILURE
186+
187+
188+
def test_cis_xlsx_to_oscal_cd_execute_csv_row_mgr(tmp_path: pathlib.Path):
189+
"""Test execute call - csv row mgr."""
190+
row_names = ['row1', 'row2', 'row3']
191+
# create new row mgr
192+
csv_row_mgr = cis_xlsx_to_oscal_cd.CsvRowMgr(row_names)
193+
# test valid case
194+
try:
195+
csv_row_mgr.put('row1', '')
196+
except RuntimeError:
197+
assert 0 == 1
198+
# test invalid case
199+
try:
200+
csv_row_mgr.put('rowX', '')
201+
assert 0 == 1
202+
except RuntimeError:
203+
pass
204+
205+
206+
def _validate_db2(tmp_path: pathlib.Path):
207+
"""Validate produced OSCAL for db2 cd."""
208+
# read catalog
209+
file_path = tmp_path / 'component-definition.json'
210+
component_definition = ComponentDefinition.oscal_read(file_path)
211+
# spot check
212+
assert len(component_definition.components) == 1
213+
assert component_definition.metadata.title == 'CIS IBM Db2 11 Benchmark'
214+
assert component_definition.metadata.version == '1.1.0'
215+
component = component_definition.components[0]
216+
assert component.type == 'software'
217+
assert len(component.props) == 552
218+
prop = component.props[0]
219+
assert prop.name == 'Rule_Id'
220+
assert prop.ns == 'https://oscal-compass/compliance-trestle/schemas/oscal/cd'
221+
assert prop.value == 'CIS-1.1.1'
222+
assert prop.remarks == 'rule_set_00'
223+
assert len(component.control_implementations) == 1
224+
prop = component.props[551]
225+
assert prop.name == 'Group_Description_Level_1'
226+
assert prop.ns == 'https://oscal-compass/compliance-trestle/schemas/oscal/cd'
227+
assert prop.value.startswith('This section provides guidance on various database configuration parameters.')
228+
assert prop.remarks == 'rule_set_22'
229+
assert len(component.control_implementations) == 1
230+
control_implementation = component.control_implementations[0]
231+
assert len(control_implementation.implemented_requirements) == 6
232+
assert control_implementation.source == 'catalogs/CIS_controls_v8/catalog.json'
233+
assert control_implementation.description == 'CIS catalog v8'
234+
implemented_requirement = control_implementation.implemented_requirements[0]
235+
assert implemented_requirement.control_id == 'cisc-7.4'
236+
assert len(implemented_requirement.props) == 1
237+
prop = implemented_requirement.props[0]
238+
assert prop.name == 'Rule_Id'
239+
assert prop.ns == 'https://oscal-compass/compliance-trestle/schemas/oscal/cd'
240+
assert prop.value == 'CIS-1.1.1'
241+
implemented_requirement = control_implementation.implemented_requirements[5]
242+
assert implemented_requirement.control_id == 'cisc-3.10'

trestle/common/str_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ def as_string(string_or_none: Optional[str]) -> str:
123123
return string_or_none if string_or_none else ''
124124

125125

126+
def as_bool(string_or_none: Optional[str]) -> bool:
127+
"""Convert string to boolean."""
128+
if string_or_none:
129+
if string_or_none.lower() in ['false']:
130+
rval = False
131+
else:
132+
rval = True
133+
else:
134+
rval = False
135+
return rval
136+
137+
126138
def string_from_root(item_with_root: Optional[Any]) -> str:
127139
"""Convert root to string if present."""
128140
return as_string(item_with_root.__root__) if item_with_root else ''

0 commit comments

Comments
 (0)