Skip to content

Commit 8644e04

Browse files
AAH20cursoragent
andcommitted
Address review nits: timestamp, simulate, input filtering, docs
- default_timestamp no longer frozen at import time; computed per call - simulate() no longer creates the output directory - Input iteration skips non-.json/.jsn files and subdirectories - class_ on compliance-type prop documented with a comment - Added aws-config task section to cli.md and compliance_posture.md - Added test for non-json/directory skipping Addresses degenaro's Claude review on #2334. Signed-off-by: Ahmed Hassan <th3reality72@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bb85e8c commit 8644e04

5 files changed

Lines changed: 53 additions & 9 deletions

File tree

docs/tutorials/cli.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,31 @@ Example output OSCAL Observations file contents (snippet):
10861086

10871087
</details>
10881088

1089+
## `trestle task aws-config-result-to-oscal-ar`
1090+
1091+
The *trestle task aws-config-result-to-oscal-ar* command transforms AWS Config compliance evaluation results into OSCAL partial results `.json` files. Each input file is the JSON shape returned by `get-compliance-details-by-config-rule` / `get-compliance-details-by-resource` (`{"EvaluationResults": [EvaluationResult, ...]}`).
1092+
1093+
Specify required config parameters for input and output directories. Optional `output-overwrite` controls whether existing output may be replaced. Optional `timestamp` is an ISO 8601 string that overrides the Result/Observation timestamps.
1094+
1095+
<span style="color:green">
1096+
Example command invocation:
1097+
</span>
1098+
1099+
`$TRESTLE_BASEDIR$ trestle task aws-config-result-to-oscal-ar -c /home/user/task.config`
1100+
1101+
<span style="color:green">
1102+
Example config:
1103+
</span>
1104+
1105+
```conf
1106+
[task.aws-config-result-to-oscal-ar]
1107+
input-dir = /home/user/git/compliance/aws-config/input
1108+
output-dir = /home/user/git/compliance/oscal/output
1109+
output-overwrite = true
1110+
```
1111+
1112+
Only `.json` / `.jsn` files in `input-dir` are processed. Nested directories and other extensions are skipped. `simulate` does not create the output directory.
1113+
10891114
## `trestle task tanium-result-to-oscal-ar`
10901115

10911116
The *trestle task tanium-result-to-oscal-ar* command facilitates transformation of Tanuim reports, each

docs/tutorials/compliance_posture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The bad news is that a transformer to [OSCAL](https://pages.nist.gov/OSCAL) is n
3838

3939
However, there is plenty of good news:
4040

41-
- a transformer for your Cloud Service type may already exist, such as: [Tanium to OSCAL](../reference/API/trestle/tasks/tanium_result_to_oscal_ar.md), [OpenShift Compliance Operator to OSCAL](../reference/API/trestle/tasks/xccdf_result_to_oscal_ar.md)
41+
- a transformer for your Cloud Service type may already exist, such as: [Tanium to OSCAL](../reference/API/trestle/tasks/tanium_result_to_oscal_ar.md), [OpenShift Compliance Operator to OSCAL](../reference/API/trestle/tasks/xccdf_result_to_oscal_ar.md), [AWS Config to OSCAL](../reference/API/trestle/tasks/aws_config_result_to_oscal_ar.md)
4242
- once a transformer for a Cloud Service type has been written, it can be open-sourced/re-used
4343
- writing a transformer is fairly easy: just a few lines of Python code using [trestle](../index.md) as a foundation
4444

tests/trestle/tasks/aws_config_result_to_oscal_ar_test.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_simulate_does_not_write_output(self, tmp_path):
5656
task = AwsConfigResultToOscalAR(config)
5757
outcome = task.simulate()
5858
assert outcome == TaskOutcome.SIM_SUCCESS
59-
assert not output_dir.exists() or not list(output_dir.iterdir())
59+
assert not output_dir.exists()
6060

6161
def test_execute_produces_valid_oscal_result(self, tmp_path):
6262
output_dir = tmp_path / 'out'
@@ -83,3 +83,19 @@ def test_execute_respects_output_overwrite_false(self, tmp_path):
8383
config2['output-overwrite'] = 'false'
8484
task2 = AwsConfigResultToOscalAR(config2)
8585
assert task2.execute() == TaskOutcome.FAILURE
86+
87+
def test_execute_skips_non_json_and_directories(self, tmp_path):
88+
input_dir = tmp_path / 'in'
89+
input_dir.mkdir()
90+
(input_dir / 'nested').mkdir()
91+
(input_dir / 'readme.txt').write_text('not json', encoding='utf-8')
92+
sample = test_data_dir / 'aws-config-sample.json'
93+
(input_dir / 'aws-config-sample.json').write_text(sample.read_text(encoding='utf-8'), encoding='utf-8')
94+
95+
output_dir = tmp_path / 'out'
96+
config = _build_config(input_dir, output_dir)
97+
task = AwsConfigResultToOscalAR(config)
98+
assert task.execute() == TaskOutcome.SUCCESS
99+
produced = list(output_dir.glob('*.oscal.json'))
100+
assert len(produced) == 1
101+
assert produced[0].name == 'aws-config-sample.oscal.json'

trestle/tasks/aws_config_result_to_oscal_ar.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,11 @@ def _transform_work(self) -> TaskOutcome:
130130
except Exception:
131131
logger.warning('config invalid "timestamp"')
132132
return TaskOutcome(mode + 'failure')
133-
# ensure output dir exists
134-
opth.mkdir(exist_ok=True, parents=True)
135-
# process
133+
if not self._simulate:
134+
opth.mkdir(exist_ok=True, parents=True)
136135
for ifile in sorted(ipth.iterdir()):
136+
if not ifile.is_file() or ifile.suffix not in ['.json', '.jsn']:
137+
continue
137138
blob = self._read_file(ifile)
138139
transformer = AwsConfigResultToOscalARTransformer()
139140
results = transformer.transform(blob)

trestle/transforms/implementations/aws_config.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,9 @@ def transform(self, blob: str) -> Results:
8888
class _OscalResultsFactory:
8989
"""Build OSCAL entities from AWS Config EvaluationResult objects."""
9090

91-
default_timestamp = ResultsTransformer.get_timestamp()
92-
93-
def __init__(self, timestamp: str = default_timestamp) -> None:
91+
def __init__(self, timestamp: str | None = None) -> None:
9492
"""Initialize."""
95-
self._timestamp = timestamp
93+
self._timestamp = timestamp if timestamp is not None else ResultsTransformer.get_timestamp()
9694
self._observation_list: List[Observation] = []
9795
self._inventory_map: Dict[str, InventoryItem] = {}
9896
self._ns = AnyUrl('https://oscal-compass.github.io/compliance-trestle/schemas/oscal/ar/aws-config')
@@ -187,6 +185,10 @@ def _observation_properties(self, evaluation_result: Dict[str, Any]) -> List[Pro
187185
('result-token', evaluation_result.get('ResultToken')),
188186
):
189187
if value is not None:
188+
# Only compliance-type is classed for downstream filtering,
189+
# matching osco.py's scc_result pattern. Prop names stay
190+
# hyphenated (OSCAL); class_ uses the underscored identifier
191+
# style of osco's scc_* values.
190192
class_ = 'aws_config_compliance' if name == 'compliance-type' else None
191193
if class_:
192194
props.append(Property.model_construct(name=name, value=str(value), ns=self._ns, class_=class_))

0 commit comments

Comments
 (0)