Repository: oscal-compass/compliance-trestle
Pull request #1837 (2025-03-17 22:21:06)

chore: trestle patch release
## 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)
- [ ] 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)
- [X] 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

Trestle patch release

## Key links:

- [Sonar coverage](https://sonarcloud.io/dashboard?id=compliance-trestle)

# 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/SECURITY-INSIGHTS.yml b/SECURITY-INSIGHTS.yml
new file mode 100644
index 00000000..fdc6c6dc
--- /dev/null
+++ b/SECURITY-INSIGHTS.yml
@@ -0,0 +1,69 @@
+header:
+  schema-version: 2.0.0
+  last-updated: '2025-03-06'
+  last-reviewed: '2025-03-06'
+  url: https://github.com/oscal-compass
+
+project:
+  name: OSCAL Compass
+  administrators:
+    - name: OSCAL Compass Oversight Committee
+      email: oscal-compass-oversight@googlegroups.com
+      primary: true
+  documentation:
+    code-of-conduct: https://github.com/oscal-compass/community/blob/main/CODE_OF_CONDUCT.md
+  repositories:
+    - name: Trestle
+      url: https://github.com/oscal-compass/compliance-trestle
+      comment: |
+        Command line tool and SDK for interacting with OSCAL-based compliance-as-code documents.
+  vulnerability-reporting:
+    reports-accepted: true
+    bug-bounty-available: false
+    security-policy: https://github.com/oscal-compass/community/blob/main/SECURITY.md
+
+repository:
+  url: https://github.com/oscal-compass/compliance-trestle
+  status: active
+  accepts-change-request: true
+  accepts-automated-change-request: true
+  # From the complaince-trestle MAINTAINER.md file
+  # https://github.com/oscal-compass/compliance-trestle/blob/develop/MAINTAINERS.md
+  core-team:
+  - name:        Alejandro Jose Leiva Palomo
+    primary:     true
+  - name:        Christopher Butler
+    primary:     true
+  - name:        Lou Degenaro
+    primary:     true
+  - name:        Jennifer Power
+    primary:     true
+  - name:        Manjiree Gadgil
+    primary:     true
+  - name:        Vikas Agarwal
+    primary:     true
+  documentation:
+    contributing-guide: https://github.com/oscal-compass/compliance-trestle/blob/develop/CONTRIBUTING.md
+    security-policy: https://github.com/oscal-compass/community/blob/main/SECURITY.md
+  license:
+    url: https://github.com/oscal-compass/compliance-trestle/blob/develop/LICENSE
+    expression: Apache 2.0
+  release:
+    changelog: https://github.com/oscal-compass/compliance-trestle/blob/develop/CHANGELOG.md
+    automated-pipeline: true
+    attestations:
+      - name: PyPI Publish Attestation
+        location: https://pypi.org/project/compliance-trestle/#compliance_trestle-{version}.tar.gz
+        predicate-uri: https://docs.pypi.org/attestations/publish/v1
+        comment: |
+          This attestation communicates the Trusted Publisher identity
+          used to publish the project.
+    distribution-points:
+      - uri: https://github.com/oscal-compass/compliance-trestle/releases
+        comment: GitHub Release Page
+      - uri: https://pypi.org/project/compliance-trestle/
+  security:
+    assessments:
+      self:
+        comment: |
+          Self assessment has not yet been completed.
diff --git a/mkdocs.yml b/mkdocs.yml
index 92d6975f..a857493e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -38,7 +38,7 @@ plugins:
     raise_error_excludes:
       # This is to remove some false positives for links which work.
       # Anchors are validated again by core mkdocs
-      404: [ '#trestle.*' ]
+      404: [ '*#trestle.*' ]
 - mkdocstrings:
     default_handler: python
     handlers:
diff --git a/setup.cfg b/setup.cfg
index 6d28c550..ef822655 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -39,7 +39,7 @@ install_requires =
     pywin32 >= 1.0;platform_system=='Windows'
     defusedxml
     openpyxl~=3.0
-    Jinja2 == 3.1.4
+    Jinja2 == 3.1.6
     cmarkgfm==2024.1.* #Update regularly
     orjson
     requests>=2.32.2
diff --git a/trestle/core/ssp_io.py b/trestle/core/ssp_io.py
index f938bba8..f398ae3c 100644
--- a/trestle/core/ssp_io.py
+++ b/trestle/core/ssp_io.py
@@ -17,8 +17,7 @@ import pathlib
 from typing import Dict, List, Optional, Tuple
 
 from trestle.common.common_types import TypeWithByComps
-from trestle.common.const import CONTROL_ORIGINATION, IMPLEMENTATION_STATUS, ITEM, SSP_MAIN_COMP_NAME
-from trestle.common.const import STATUS_OPERATIONAL
+from trestle.common.const import CONTROL_ORIGINATION, IMPLEMENTATION_STATUS, ITEM
 from trestle.common.err import TrestleError
 from trestle.common.list_utils import as_list
 from trestle.core.catalog import catalog_interface
@@ -219,20 +218,23 @@ class SSPMarkdownWriter():
         rules: List[str],
         status: str,
         show_rules: bool,
-        show_status: bool
+        show_status: bool,
+        show_comp: bool,
+        level: int
     ) -> None:
-        header = f'Component: {comp_name}'
-        md_writer.new_header(1, header)
+        if comp_name and show_comp:
+            header = f'Component: {comp_name}'
+            md_writer.new_header(level, header)
         md_writer.set_indent_level(-1)
         md_writer.new_line(prose)
         md_writer.set_indent_level(-1)
         if rules and show_rules:
-            md_writer.new_header(2, title='Rules:')
+            md_writer.new_header((level + 1), title='Rules:')
             md_writer.set_indent_level(-1)
             md_writer.new_list(rules)
             md_writer.set_indent_level(-1)
         if status and show_status:
-            md_writer.new_header(2, title=f'Implementation Status: {status}')
+            md_writer.new_header((level + 1), title=f'Implementation Status: {status}')
 
     def get_control_response(
         self,
@@ -267,21 +269,16 @@ class SSPMarkdownWriter():
 
         md_writer = MDWriter(None)
 
-        system_prose = ''
-        system_rules = []
-        system_status = STATUS_OPERATIONAL
+        # write out top-level implementation statements, whether or not this control has parts
         imp_req_responses = self._get_responses_by_components(imp_req, write_empty_responses)
-        if SSP_MAIN_COMP_NAME in imp_req_responses:
-            system_prose, system_rules, system_status = imp_req_responses[SSP_MAIN_COMP_NAME]
-
-        SSPMarkdownWriter._write_component_prompt(
-            md_writer, SSP_MAIN_COMP_NAME, system_prose, system_rules, system_status, show_rules, show_status
-        )
-
-        # if a control has no statement sub-parts then get the response bycomps from the imp_req itself
-        # otherwise get them from the statements in the imp_req
-        # an imp_req and a statement are both things that can have bycomps
-        has_bycomps = imp_req.statements if imp_req.statements else [imp_req]
+        for comp_name, comp_response in imp_req_responses.items():
+            prose, rules, status = comp_response
+            SSPMarkdownWriter._write_component_prompt(
+                md_writer, comp_name, prose, rules, status, show_rules, show_status, show_comp, 1
+            )
+
+        # iterate over statements, if present, to write out each part
+        has_bycomps = imp_req.statements if imp_req.statements else []
         for has_bycomp in has_bycomps:
             statement_id = getattr(has_bycomp, 'statement_id', f'{control_id}_smt')
             label = statement_id
@@ -302,13 +299,10 @@ class SSPMarkdownWriter():
                     header = f'Implementation for part {label}'
                     md_writer.new_header(1, title=header)
                 for comp_name, comp_response in response_per_component.items():
-                    if comp_name == SSP_MAIN_COMP_NAME:
-                        continue
                     prose, rules, status = comp_response
-                    if show_comp:
-                        SSPMarkdownWriter._write_component_prompt(
-                            md_writer, comp_name, prose, rules, status, show_rules, show_status
-                        )
+                    SSPMarkdownWriter._write_component_prompt(
+                        md_writer, comp_name, prose, rules, status, show_rules, show_status, show_comp, 2
+                    )
 
         lines = md_writer.get_lines()
 
@@ -339,7 +333,7 @@ class SSPMarkdownWriter():
                 status = by_comp.implementation_status.state
             rules, _ = ControlInterface.get_rule_list_for_item(by_comp)
 
-            if prose or (not prose and write_empty_responses):
+            if prose or write_empty_responses:
                 if subheader:
                     response_per_component[subheader] = (prose, rules, status)
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5f583b82..68864fa6 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -485,7 +485,8 @@ def setup_for_ssp(
     output_name: str,
     use_yaml: bool = False,
     leveraged_ssp_name: str = '',
-    comp_names='comp_def_a,comp_def_b'
+    comp_names='comp_def_a,comp_def_b',
+    include_all_parts: bool = False
 ) -> Tuple[argparse.Namespace, pathlib.Path]:
     """Create the comp_def, profile and catalog content needed for ssp-generate."""
     for comp_name in comp_names.split(','):
@@ -508,7 +509,7 @@ def setup_for_ssp(
         output=output_name,
         verbose=0,
         overwrite_header_values=False,
-        include_all_parts=False,
+        include_all_parts=include_all_parts,
         yaml_header=yaml_path,
         allowed_sections=None,
         force_overwrite=None
diff --git a/tests/trestle/core/ssp_io_test.py b/tests/trestle/core/ssp_io_test.py
index dc0a6973..b9a6f97c 100644
--- a/tests/trestle/core/ssp_io_test.py
+++ b/tests/trestle/core/ssp_io_test.py
@@ -90,7 +90,7 @@ def test_ssp_writer(testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, m
 
 def test_ssp_get_control_response(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
     """Test generating SSP from the sample profile and generate markdown representation of it."""
-    args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name)
+    args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, include_all_parts=True)
     ssp_cmd = SSPGenerate()
     assert ssp_cmd._run(args) == 0
 
@@ -119,14 +119,20 @@ def test_ssp_get_control_response(tmp_trestle_dir: pathlib.Path, monkeypatch: Mo
     assert md_text
     tree = DocsMarkdownNode.build_tree_from_markdown(md_text.split('\n'))
 
-    assert tree.get_node_for_key('## Implementation for part a.')
-    assert len(list(tree.get_all_headers_for_level(2))) == 4
-    assert len(list(tree.get_all_headers_for_level(3))) == 5
+    assert len(list(tree.get_all_headers_for_level(2))) == 6  # top level This, aa, ab + 3 part labels
+
+    part_a = tree.get_node_for_key('## Implementation for part a.')
+    assert part_a
+    assert len(list(part_a.get_all_headers_for_level(3))) == 3  # should be 3 components: This, aa, bb
+    assert len(list(part_a.get_all_headers_for_level(4))) == 5  # should be 3 statuses + 2 rules
 
     md_text = ssp_io.get_control_response('ac-3', 1, True)
     assert md_text
     tree = DocsMarkdownNode.build_tree_from_markdown(md_text.split('\n'))
 
+    assert len(list(tree.get_all_headers_for_level(2))) == 2  # should be 2 components: This, aa
+    assert len(list(tree.get_all_headers_for_level(3))) == 2  # should be 2 statuses
+
     # change responses
     new_imp_prose = 'edited imp req prose'
     new_a_prose = 'edited a prose'
@@ -146,12 +152,12 @@ def test_ssp_get_control_response(tmp_trestle_dir: pathlib.Path, monkeypatch: Mo
     # confirm edited response is there and that comp name presence is controlled
     md_text = ssp_io.get_control_response('ac-1', 1, False, False)
     assert 'comp_aa' not in md_text
-    assert new_a_prose not in md_text
+    assert new_a_prose in md_text
 
     md_text = ssp_io.get_control_response('ac-1', 1, False, True)
     assert 'comp_aa' in md_text
     assert new_a_prose in md_text
-    assert '\n### Implementation Status: partial' in md_text
+    assert '\n#### Implementation Status: partial' in md_text
 
 
 def test_writers(testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: