Repository: oscal-compass/compliance-trestle
Pull request #1670 (2024-08-23 14:26:58)

chore: Trestle 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

Bumping into 3.4.0 from 3.3.0

## 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/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..6a015eec
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    commit-message:
+      prefix: build
+      include: scope
+    reviewers:
+      - "oscal-compass/compliance-trestle-maintainers"
+
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    commit-message:
+      prefix: build
+      include: scope
+    reviewers:
+      - "oscal-compass/compliance-trestle-maintainers"
+    open-pull-requests-limit: 10
diff --git a/.github/workflows/python-push.yml b/.github/workflows/python-push.yml
index 85d2e7a5..9deb0d1e 100644
--- a/.github/workflows/python-push.yml
+++ b/.github/workflows/python-push.yml
@@ -10,7 +10,23 @@ on:
       - main
 
 jobs:
+  set-versions:
+    runs-on: ubuntu-latest
+    outputs:
+      min: ${{ steps.versions.outputs.min }}
+      max: ${{ steps.versions.outputs.max }}
+    steps:
+    - uses: actions/checkout@v4
+    - id: versions
+      run: |
+        min_version=$(jq '.PYTHON_MIN' -r version.json)
+        max_version=$(jq '.PYTHON_MAX' -r version.json)
+        echo "min=$min_version" 
+        echo "max=$max_version"
+        echo "min=$min_version" >> $GITHUB_OUTPUT
+        echo "max=$max_version" >> $GITHUB_OUTPUT
   build:
+    needs: set-versions
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
@@ -22,7 +38,7 @@ jobs:
           path: ~/Library/Caches/pip
         - os: windows-latest
           path: ~\AppData\Local\pip\Cache
-        python-version: ['${{ vars.PYTHON_MIN }}', '${{ vars.PYTHON_MAX }}']
+        python-version: [ '${{ needs.set-versions.outputs.min }}', '${{ needs.set-versions.outputs.max }}']
 
     steps:
     - name: Don't mess with  line endings
@@ -41,50 +57,52 @@ jobs:
         key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('setup.cfg') }}
         restore-keys: |
          ${{ matrix.os }}-${{ matrix.python-version }}-pip-
+    - name: Is core test version
+      id: core-version
+      run: echo "core=${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == needs.set-versions.outputs.max ) }}" >> $GITHUB_OUTPUT
 
     - name: Install build tools
       run: |
         make develop
     - name: Setup pre-commit
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make pre-commit
     - name: Install dependencies
       run: |
         make install
     - name: Run md document formatting (mdformat)
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make mdformat
     - name: Run code formatting (yapf)
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make code-format
     - name: Run code linting (flake8)
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make code-lint
     - name: Run code typing check (mypy)
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       continue-on-error: true 
       run: |
         make code-typing
     - name: Validate website content (mkdocs)
-      if: ${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make docs-validate
     - name: Pytest Fast
-      if: ${{ !(matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core != 'true'
       run: |
         make test
     - name: Pytest Cov
-      if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make test-cov
   deploy:
-
     runs-on: ubuntu-latest
-    needs: build
+    needs: [ build, set-versions ]
     permissions:
       id-token: write
     environment:
@@ -97,10 +115,10 @@ jobs:
         submodules: true
         fetch-depth: 0
         token: ${{ secrets.ADMIN_PAT }} 
-    - name: Set up Python ${{ vars.PYTHON_MAX }}
+    - name: Set up Python ${{ needs.set-versions.outputs.max }}
       uses: actions/setup-python@v5
       with:
-        python-version: ${{ vars.PYTHON_MAX }}
+        python-version: ${{ needs.set-versions.outputs.max }}
     - name: Install build tools
       run: |
         make develop
@@ -126,7 +144,7 @@ jobs:
 
   deploy-docs:
     runs-on: ubuntu-latest
-    needs: deploy
+    needs: [ deploy, set-versions ]
     concurrency: 
       group: ${{ github.ref }}-${{ github.workflow }}-${{ github.job }}-docs
       cancel-in-progress: true
@@ -138,11 +156,11 @@ jobs:
         submodules: true
         fetch-depth: 0
         token: ${{ secrets.ADMIN_PAT }} 
-    - name: Set up Python ${{ vars.PYTHON_MAX }}
+    - name: Set up Python ${{ needs.set-versions.outputs.max }}
       uses: actions/setup-python@v5
       # This is deliberately not using a custom credential as it relies on native github actions token to have push rights.
       with:
-        python-version: ${{ vars.PYTHON_MAX }}
+        python-version: ${{ needs.set-versions.outputs.max }}
     - name: Install build tools
       run: |
         make develop
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 945075ab..1484f000 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,8 +13,6 @@ Before embarking on a more ambitious contribution, please quickly [get in touch]
 requires extensive rework (by you or by us), sits in backlog for a long time, or
 cannot be accepted at all!**
 
-We have also adopted [Contributor Covenant Code of Conduct](https://oscal-compass.github.io/compliance-trestle/mkdocs_code_of_conduct/).
-
 ### Proposing new features
 
 If you would like to implement a new feature, please [raise an issue](https://github.com/oscal-compass/compliance-trestle/issues)
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index a9730030..da60ba7e 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -6,8 +6,6 @@ Christopher Butler [butler54](https://github.com/butler54)
 
 Lou Degenaro [degenaro](https://github.com/degenaro)
 
-Frank Suits [fsuits](https://github.com/fsuits)
-
 Jennifer Power [jpower432](https://github.com/jpower432)
 
 Manjiree Gadgil [mrgadgil](https://github.com/mrgadgil)
diff --git a/README.md b/README.md
index d00e401d..2407cf76 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# <img alt="Logo" width="50px" src="./images/compliance-trestle-800x800.png" style="vertical-align: middle;" /> Compliance-trestle (also known as `trestle`)
+# <img alt="Logo" width="50px" src="https://raw.githubusercontent.com/oscal-compass/compliance-trestle/main/images/compliance-trestle-800x800.png" style="vertical-align: middle;" /> Compliance-trestle (also known as `trestle`)
 
 ![[OS Compatibility](#prerequisites)](https://img.shields.io/badge/platform-osx%20%7C%20linux%20%7C%20windows-orange.svg)
 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/compliance-trestle)
@@ -102,6 +102,10 @@ Please refer to the community [README](https://github.com/oscal-compass/communit
 
 Our project welcomes external contributions. Please consult [contributing](https://oscal-compass.github.io/compliance-trestle/contributing/mkdocs_contributing/) to get started.
 
+## Code of Conduct
+
+Participation in the OSCAL Compass community is governed by the [Code of Conduct](https://github.com/oscal-compass/community/blob/main/CODE_OF_CONDUCT.md).
+
 ## License & Authors
 
 If you would like to see the detailed LICENSE click [here](LICENSE).
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 00000000..b2d2a70d
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,26 @@
+# Roadmap
+
+The [public roadmap](https://github.com/orgs/oscal-compass/projects/2) for Trestle is published as a GitHub project board.
+
+This project serves as the up-to-date description of high-level initiatives and individual features that have been reviewed and identified as valuable to the community.
+
+High level roadmap items are identified as `Epics`. These represent larger initiatives that need design collaboration and will result in issues representing individual features.
+
+### Timelines
+
+**Please take any defined dates as estimations and not strict commitments to a delivery timeline.**
+
+We strive to release ready-to-go features monthly. This is documented through milestones. Bug and security fixes will be released on-demand.
+
+We use 12-week iterations for high-level initiatives that must be broken down and completed. It allows the time to collect community comments, break it down into smaller issues, and schedule a monthly release.
+
+### How to get involved
+
+Each roadmap item is represented as a GitHub Issue. Discussions or any feedback on current roadmap items can take place in threads on the corresponding issue. To stay up to date with roadmap items, please join the OSCAL-Compass community [meetings](https://github.com/oscal-compass/community).
+
+### How to add an item to the roadmap?
+
+**Contributors are encouraged to get feedback early by submitting issues to new work they would like to complete and getting feedback.**
+
+New items on the roadmap are typically driven by changes in OSCAL and new feature requests through GitHub Issues.
+During issue triage, feature requests are identified as `Epics` and added to the roadmap or individual features that can be directly added to the project `Backlog` and/or an upcoming release milestone.
diff --git a/docs/mkdocs_code_of_conduct.md b/docs/mkdocs_code_of_conduct.md
index d66c2b9b..1d430795 100644
--- a/docs/mkdocs_code_of_conduct.md
+++ b/docs/mkdocs_code_of_conduct.md
@@ -1,5 +1,4 @@
 # Code of Conduct 
 
-The code of conduct now resides at the organization level in the community repository. Please visit https://github.com/oscal-compass/community/blob/main/CODE_OF_CONDUCT.md to review the common code of conduct.
 
-Note: The code of conduct is subject to change as `compliance-trestle` is on-boarded as a CNFC sandbox project.
+Please refer to our [OSCAL Compass Community Code of Conduct](https://github.com/oscal-compass/community/blob/main/CODE_OF_CONDUCT.md).
diff --git a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md
index 5679e173..9a1d2acb 100644
--- a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md
+++ b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md
@@ -1049,6 +1049,8 @@ In addition, this is the only control markdown where the moustache (`{{}}`) item
 
 The markdown can have guidance per-component in the control, as shown by the line, `### ACME Component`.  Any prose directly under a `##` implementation section will apply to the overall system component, but sections in a sub-header of the form `###` will only apply to that particular component.
 
+The `--include-all-parts` flag controls how the main system component (`This System`) is written in the markdown. By default, control parts are not written out unless a component has a rule associated with that particular control part or `statement`. When `--include-all-parts` is set, all control parts will be written with the main component present under the `## Implementation for part <part>` section. All other components will still be conditionally added based on the presence of rules.
+
 After generating the markdown for the resolved profile catalog you may then edit the files and provide text in the sections with `Add control implementation...` in them.  But do not remove the horizontal rule
 lines or modify/remove the lines with `### ` in them, corresponding to system components.
 
diff --git a/setup.cfg b/setup.cfg
index 0b3f35c1..116f7002 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -65,7 +65,7 @@ dev =
     pytest-cov>=2.10.0
     pytest-xdist
     pre-commit>=2.4.0
-    setuptools
+    setuptools>=61
     urllib3==1.26.19
     wheel
     yapf
diff --git a/trestle/common/const.py b/trestle/common/const.py
index 381aff39..48f1ecd4 100644
--- a/trestle/common/const.py
+++ b/trestle/common/const.py
@@ -328,6 +328,11 @@ HELP_MARKDOWN_NAME = 'Name of the output generated profile markdown folder'
 
 HELP_COMPDEFS = 'Comma-separated list of component-definitions for the ssp.'
 
+HELP_INCLUDE_ALL_PARTS = (
+    'Include all control parts when generating SSP markdown.'
+    + ' By default, only controls and parts with an attached rule will be written out.'
+)
+
 FILTER_BY_PROFILE = 'filter-by-profile'
 
 FILTER_BY_COMPONENTS = 'filter-by-components'
diff --git a/trestle/core/catalog/catalog_reader.py b/trestle/core/catalog/catalog_reader.py
index f887ea19..4ed76d13 100644
--- a/trestle/core/catalog/catalog_reader.py
+++ b/trestle/core/catalog/catalog_reader.py
@@ -201,30 +201,45 @@ class CatalogReader():
                 ssp.control_implementation.implemented_requirements
             )
             ssp.control_implementation.implemented_requirements.append(control_imp_req)
+        CatalogReader._add_statement_to_impl_requirement(control_imp_req, statement_id)
+        return control_imp_req
+
+    @staticmethod
+    def _add_statement_to_impl_requirement(impl_req: ossp.ImplementedRequirement, statement_id: str) -> ossp.Statement:
+        """Update the implemented requirement with a new statement."""
         statement = gens.generate_sample_model(ossp.Statement)
         statement.statement_id = statement_id
         statement.by_components = None
-        control_imp_req.statements = as_list(control_imp_req.statements)
-        control_imp_req.statements.append(statement)
-        return control_imp_req
+        impl_req.statements = as_list(impl_req.statements)
+        impl_req.statements.append(statement)
+        return statement
 
     @staticmethod
     def _get_by_comp_from_imp_req(
         imp_req: ossp.ImplementedRequirement, statement_id: str, comp_uuid: str
     ) -> ossp.ByComponent:
         if statement_id:
+            found_statement: Optional[ossp.Statement] = None
             for statement in as_list(imp_req.statements):
                 if statement.statement_id == statement_id:
-                    for by_comp in as_list(statement.by_components):
-                        if by_comp.component_uuid == comp_uuid:
-                            return by_comp
-                    # didnt find bycomp so need to make one
-                    by_comp = gens.generate_sample_model(ossp.ByComponent)
-                    by_comp.component_uuid = comp_uuid
-                    by_comp.implementation_status = com.ImplementationStatus(state=const.STATUS_PLANNED)
-                    statement.by_components = as_list(statement.by_components)
-                    statement.by_components.append(by_comp)
-                    return by_comp
+                    found_statement = statement
+                    break
+
+            # If the statement doesn't exist, create it
+            if not found_statement:
+                found_statement = CatalogReader._add_statement_to_impl_requirement(imp_req, statement_id)
+            else:
+                for by_comp in as_list(found_statement.by_components):
+                    if by_comp.component_uuid == comp_uuid:
+                        return by_comp
+
+            # didnt find bycomp or new statement so need to make one
+            by_comp = gens.generate_sample_model(ossp.ByComponent)
+            by_comp.component_uuid = comp_uuid
+            by_comp.implementation_status = com.ImplementationStatus(state=const.STATUS_PLANNED)
+            found_statement.by_components = as_list(found_statement.by_components)
+            found_statement.by_components.append(by_comp)
+            return by_comp
         else:
             for by_comp in as_list(imp_req.by_components):
                 if by_comp.component_uuid == comp_uuid:
@@ -235,7 +250,6 @@ class CatalogReader():
             imp_req.by_components = as_list(imp_req.by_components)
             imp_req.by_components.append(by_comp)
             return by_comp
-        raise TrestleError(f'Internal error seeking by_comp for component {comp_uuid} and statement {statement_id}')
 
     @staticmethod
     def _read_comp_info_from_md(control_file_path: pathlib.Path,
diff --git a/trestle/core/catalog/catalog_writer.py b/trestle/core/catalog/catalog_writer.py
index 4f706533..a255dcd1 100644
--- a/trestle/core/catalog/catalog_writer.py
+++ b/trestle/core/catalog/catalog_writer.py
@@ -165,9 +165,11 @@ class CatalogWriter():
                 # adds it to prof-param-value-origin
                 if prof_param_value_origin != '' and prof_param_value_origin is not None:
                     if context.purpose == ContextPurpose.PROFILE:
-                        new_dict[const.PROFILE_PARAM_VALUE_ORIGIN] = prof_param_value_origin
+                        if const.AGGREGATES not in [prop.name for prop in as_list(param.props)]:
+                            new_dict[const.PROFILE_PARAM_VALUE_ORIGIN] = prof_param_value_origin
                 else:
-                    new_dict[const.PROFILE_PARAM_VALUE_ORIGIN] = const.REPLACE_ME_PLACEHOLDER
+                    if const.AGGREGATES not in [prop.name for prop in as_list(param.props)]:
+                        new_dict[const.PROFILE_PARAM_VALUE_ORIGIN] = const.REPLACE_ME_PLACEHOLDER
                 # then insert the original, incoming values as values
                 if param_id in control_param_dict:
                     orig_param = control_param_dict[param_id]
@@ -180,6 +182,8 @@ class CatalogWriter():
                         new_dict.pop(const.VALUES)
                     if new_dict[const.GUIDELINES] is None:
                         new_dict.pop(const.GUIDELINES)
+                    if const.AGGREGATES in [prop.name for prop in as_list(orig_param.props)]:
+                        new_dict.pop(const.PROFILE_PARAM_VALUE_ORIGIN)
             else:
                 # if the profile doesnt change this param at all, show it in the header with values
                 tmp_dict = ModelUtils.parameter_to_dict(param_dict, True)
diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py
index 065e75c2..e129e53f 100644
--- a/trestle/core/commands/author/ssp.py
+++ b/trestle/core/commands/author/ssp.py
@@ -81,6 +81,14 @@ class SSPGenerate(AuthorCommonCommand):
             action='store_true',
             default=False
         )
+        self.add_argument(
+            '-iap',
+            '--include-all-parts',
+            help=const.HELP_INCLUDE_ALL_PARTS,
+            required=False,
+            action='store_true',
+            default=False
+        )
 
     def _run(self, args: argparse.Namespace) -> int:
         try:
@@ -110,7 +118,8 @@ class SSPGenerate(AuthorCommonCommand):
                 md_path,
                 yaml_header,
                 args.overwrite_header_values,
-                args.force_overwrite
+                args.force_overwrite,
+                args.include_all_parts
             )
 
         except Exception as e:  # pragma: no cover
@@ -125,7 +134,8 @@ class SSPGenerate(AuthorCommonCommand):
         md_path: pathlib.Path,
         yaml_header: Dict[str, Any],
         overwrite_header_values: bool,
-        force_overwrite: bool
+        force_overwrite: bool,
+        include_all_parts: bool
     ) -> int:
         """
         Generate the ssp markdown from the profile and compdefs.
@@ -157,6 +167,7 @@ class SSPGenerate(AuthorCommonCommand):
         context.overwrite_header_values = overwrite_header_values
         context.allowed_sections = None
         context.comp_def_name_list = compdef_name_list
+        context.include_all_parts = include_all_parts
 
         # if file not recognized as URI form, assume it represents name of file in trestle directory
         profile_in_trestle_dir = '://' not in profile_name_or_href
diff --git a/trestle/core/control_context.py b/trestle/core/control_context.py
index 560c6b54..a2ccb977 100644
--- a/trestle/core/control_context.py
+++ b/trestle/core/control_context.py
@@ -44,6 +44,7 @@ class ControlContext:
     trestle_root: pathlib.Path
     md_root: pathlib.Path
     prompt_responses: bool
+    include_all_parts: bool
     overwrite_header_values: bool
     set_parameters_flag: bool
     cli_yaml_header: Optional[Dict[Any, Any]] = None
@@ -71,6 +72,7 @@ class ControlContext:
         trestle_root: pathlib.Path,
         md_root: pathlib.Path,
         prompt_responses: bool = False,
+        include_all_parts: bool = False,
         overwrite_header_values: bool = False,
         set_parameters_flag: bool = False,
         cli_yaml_header: Optional[Dict[Any, Any]] = None,
@@ -97,6 +99,7 @@ class ControlContext:
             trestle_root,
             md_root,
             prompt_responses,
+            include_all_parts,
             overwrite_header_values,
             set_parameters_flag,
             cli_yaml_header=cli_yaml_header,
@@ -132,6 +135,7 @@ class ControlContext:
             context.trestle_root,
             context.md_root,
             context.prompt_responses,
+            context.include_all_parts,
             context.overwrite_header_values,
             context.set_parameters_flag,
             cli_yaml_header=copy.deepcopy(context.cli_yaml_header),
diff --git a/trestle/core/control_interface.py b/trestle/core/control_interface.py
index cd7cc162..4e92c2bf 100644
--- a/trestle/core/control_interface.py
+++ b/trestle/core/control_interface.py
@@ -670,6 +670,24 @@ class ControlInterface:
             return choices_str
         return ''
 
+    @staticmethod
+    def _param_as_aggregated_value(
+        param: common.Parameter,
+        param_dict: Dict[str, common.Parameter],
+        verbose: bool = False,
+        brackets: bool = False
+    ) -> str:
+        """Convert parameter aggregation to str."""
+        # review is an aggregated parameter
+        if const.AGGREGATES in [prop.name for prop in as_list(param.props)]:
+            aggregated_values = ''
+            for prop in as_list(param.props):
+                if prop.value not in param_dict:
+                    continue
+                aggregated_values += ', '.join(as_list(param_dict[prop.value].values)) + ', '
+            return aggregated_values[:-2]
+        return ''
+
     @staticmethod
     def _param_label_choices_as_str(param: common.Parameter, verbose: bool = False, brackets: bool = False) -> str:
         """Convert param label or choices to string, using choices if present."""
@@ -681,6 +699,7 @@ class ControlInterface:
     @staticmethod
     def _param_values_assignment_str(
         param: common.Parameter,
+        param_dict: Dict[str, common.Parameter],
         value_assigned_prefix: Optional[str] = None,
         value_not_assigned_prefix: Optional[str] = None
     ) -> str:
@@ -692,6 +711,9 @@ class ControlInterface:
         # otherwise use param selection if present
         if not param_str:
             param_str = ControlInterface._param_selection_as_str(param, True, False)
+        # otherwise use param aggregated values if present
+        if not param_str:
+            param_str = ControlInterface._param_as_aggregated_value(param, param_dict, True, False)
         # finally use label and param_id as fallbacks
         if not param_str:
             param_str = param.label if param.label else param.id
@@ -722,7 +744,8 @@ class ControlInterface:
         brackets: bool = False,
         params_format: Optional[str] = None,
         value_assigned_prefix: Optional[str] = None,
-        value_not_assigned_prefix: Optional[str] = None
+        value_not_assigned_prefix: Optional[str] = None,
+        param_dict: Dict[str, common.Parameter] = None
     ) -> Optional[str]:
         """
         Convert parameter to string based on best available representation.
@@ -755,7 +778,7 @@ class ControlInterface:
                 param_str = ''
         elif param_rep == ParameterRep.ASSIGNMENT_FORM:
             param_str = ControlInterface._param_values_assignment_str(
-                param, value_assigned_prefix, value_not_assigned_prefix
+                param, param_dict, value_assigned_prefix, value_not_assigned_prefix
             )
             if not param_str:
                 param_str = ''
@@ -854,11 +877,20 @@ class ControlInterface:
             elif param_dict[param_ids[i]] is not None:
                 param = param_dict[param_ids[i]]
                 param_str = ControlInterface.param_to_str(
-                    param, param_rep, False, False, params_format, value_assigned_prefix, value_not_assigned_prefix
+                    param,
+                    param_rep,
+                    False,
+                    False,
+                    params_format,
+                    value_assigned_prefix,
+                    value_not_assigned_prefix,
+                    param_dict
                 )
                 text = text.replace(staches[i], param_str, 1).strip()
                 if show_value_warnings and param_rep != ParameterRep.LABEL_OR_CHOICES and not param.values:
-                    logger.warning(f'Parameter {param_id} has no values and was referenced by prose.')
+                    # verifies the current parameter is not an aggregated parameter to throw a warning
+                    if const.AGGREGATES not in [prop.name for prop in as_list(param.props)]:
+                        logger.warning(f'Parameter {param_id} has no values and was referenced by prose.')
             elif show_value_warnings:
                 logger.warning(f'Control prose references param {param_ids[i]} with no specified value.')
         # there may be staches remaining that we can't replace if not in param_dict
diff --git a/trestle/core/control_writer.py b/trestle/core/control_writer.py
index 8d9fbae3..481e2393 100644
--- a/trestle/core/control_writer.py
+++ b/trestle/core/control_writer.py
@@ -142,6 +142,22 @@ class ControlWriter():
         else:
             self._insert_status(ImplementationStatus(state=const.STATUS_PLANNED), level)
 
+    def _process_main_component(self, part_id: str, part_label: str, comp_dict: CompDict) -> None:
+        """Handle prompts for This System component."""
+        self._md_file.new_paragraph()
+        self._md_file.new_header(3, const.SSP_MAIN_COMP_NAME)
+        self._md_file.new_paragraph()
+        prose = f'{const.SSP_ADD_THIS_SYSTEM_IMPLEMENTATION_FOR_CONTROL_TEXT}: {part_id} -->'
+        status = ImplementationStatus(state=const.STATUS_PLANNED)
+        sys_comp_dict: Dict[str, ComponentImpInfo] = comp_dict.get(const.SSP_MAIN_COMP_NAME, {})
+        if sys_comp_dict and part_label in sys_comp_dict:
+            comp_info = sys_comp_dict[part_label]
+            if comp_info.prose:
+                prose = comp_info.prose
+            status = comp_info.status
+        self._md_file.new_paraline(prose)
+        self._insert_status(status, 4)
+
     def _add_component_control_prompts(self, control_id: str, comp_dict: CompDict, context: ControlContext) -> bool:
         """Add prompts to the markdown for the control itself, per component."""
         if context.purpose not in [ContextPurpose.COMPONENT, ContextPurpose.SSP]:
@@ -151,19 +167,10 @@ class ControlWriter():
         did_write = False
         # do special handling for This System
         if context.purpose == ContextPurpose.SSP:
-            self._md_file.new_paragraph()
-            self._md_file.new_header(3, const.SSP_MAIN_COMP_NAME)
-            self._md_file.new_paragraph()
-            prose = f'{const.SSP_ADD_THIS_SYSTEM_IMPLEMENTATION_FOR_CONTROL_TEXT}: {control_id} -->'
-            status = ImplementationStatus(state=const.STATUS_PLANNED)
-            if const.SSP_MAIN_COMP_NAME in comp_dict:
-                comp_info = list(comp_dict[const.SSP_MAIN_COMP_NAME].values())[0]
-                if comp_info.prose:
-                    prose = comp_info.prose
-                status = comp_info.status
-            self._md_file.new_paraline(prose)
-            self._insert_status(status, 4)
+            # Passing in the control id and the empty key ('') for the label
+            self._process_main_component(control_id, '', comp_dict)
             did_write = True
+
         sorted_comp_names = sorted(comp_dict.keys())
         for comp_name in sorted_comp_names:
             dic = comp_dict[comp_name]
@@ -209,14 +216,11 @@ class ControlWriter():
                         # if no label guess the label from the sub-part id
                         part_label = ControlInterface.get_label(prt)
                         part_label = prt.id.split('.')[-1] if not part_label else part_label
-                        # only write out part if rules apply to it
-                        rules_apply = False
-                        for _, dic in comp_dict.items():
-                            if part_label in dic and dic[part_label].rules:
-                                rules_apply = True
-                                break
-                        if not rules_apply:
+
+                        # Determine if this part should be written out
+                        if self._skip_part(context, part_label, comp_dict):
                             continue
+
                         if not did_write_part:
                             self._md_file.new_line(const.SSP_MD_LEAVE_BLANK_TEXT)
                             # insert extra line to make mdformat happy
@@ -224,11 +228,17 @@ class ControlWriter():
                         self._md_file.new_hr()
                         self._md_file.new_header(level=2, title=f'Implementation for part {part_label}')
                         wrote_label_content = False
+                        # To reduce verbosity by default, the main component
+                        # is conditionally written for each part dependant on the control context.
+                        if context.purpose == ContextPurpose.SSP and context.include_all_parts:
+                            self._process_main_component(prt.id, part_label, comp_dict)
+                            wrote_label_content = True
                         sorted_comp_names = sorted(comp_dict.keys())
                         for comp_name in sorted_comp_names:
-                            dic = comp_dict[comp_name]
+                            # This System already handled
                             if comp_name == const.SSP_MAIN_COMP_NAME:
                                 continue
+                            dic = comp_dict[comp_name]
                             if part_label in dic:
                                 # insert the component name for ssp but not for comp_def
                                 # because there should only be one component in generated comp_def markdown
@@ -256,6 +266,24 @@ class ControlWriter():
                     self._insert_comp_info(part_label, dic, context)
         self._md_file.new_hr()
 
+    def _skip_part(self, context: ControlContext, part_label: str, comp_dict: CompDict) -> bool:
+        """
+        Check if a part should be skipped based on rules and context.
+
+        Notes: The default logic is to conditionally add control parts based
+        on whether the component has rules associated with that part. This can be
+        changed using the control context for SSP markdown.
+        """
+        if context.purpose == ContextPurpose.SSP and context.include_all_parts:
+            return False
+        else:
+            no_applied_rules = True
+            for _, dic in comp_dict.items():
+                if part_label in dic and dic[part_label].rules:
+                    no_applied_rules = False
+                    break
+        return no_applied_rules
+
     def _dump_subpart_infos(self, level: int, part: Dict[str, Any]) -> None:
         name = part['name']
         title = self._sections_dict.get(name, name) if self._sections_dict else name
diff --git a/trestle/core/repository.py b/trestle/core/repository.py
index dfbff4fa..33a2a2a2 100644
--- a/trestle/core/repository.py
+++ b/trestle/core/repository.py
@@ -388,7 +388,7 @@ class AgileAuthoring(Repository):
         markdown_dir: str,
         set_parameters: bool = False,
         regenerate: bool = False,
-        version: str = ''
+        version: Optional[str] = None
     ) -> bool:
         """Assemble catalog markdown into OSCAL Catalog in JSON."""
         logger.debug(f'Assembling model {name} of type catalog.')
@@ -423,10 +423,10 @@ class AgileAuthoring(Repository):
         markdown_dir: str,
         set_parameters: bool = False,
         regenerate: bool = False,
-        version: str = '',
-        sections: str = '',
-        required_sections: str = '',
-        allowed_sections: str = ''
+        version: Optional[str] = None,
+        sections: Optional[str] = None,
+        required_sections: Optional[str] = None,
+        allowed_sections: Optional[str] = None
     ) -> bool:
         """Assemble profile markdown into OSCAL Profile in JSON."""
         logger.debug(f'Assembling model {name} of type profile.')
@@ -492,7 +492,7 @@ class AgileAuthoring(Repository):
         markdown_dir: str,
         compdefs: str,
         regenerate: bool = False,
-        version: str = ''
+        version: Optional[str] = None
     ) -> bool:
         """Assemble ssp markdown into OSCAL SSP in JSON."""
         logger.debug(f'Assembling model {name} of type ssp.')
@@ -525,7 +525,7 @@ class AgileAuthoring(Repository):
         name: str,
         output: str,
         force_overwrite: bool = False,
-        yaml_header: str = '',
+        yaml_header: Optional[str] = None,
         overwrite_header_values: bool = False
     ) -> bool:
         """Generate catalog markdown from OSCAL Catalog in JSON."""
@@ -558,10 +558,10 @@ class AgileAuthoring(Repository):
         name: str,
         output: str,
         force_overwrite: bool = False,
-        yaml_header: str = '',
+        yaml_header: Optional[str] = None,
         overwrite_header_values: bool = False,
-        sections: str = '',
-        required_sections: str = ''
+        sections: Optional[str] = None,
+        required_sections: Optional[str] = None
     ) -> bool:
         """Generate profile markdown from OSCAL Profile in JSON."""
         logger.debug(f'Generating markdown for {name} of type profile.')
@@ -620,9 +620,10 @@ class AgileAuthoring(Repository):
         profile: str,
         output: str,
         compdefs: str,
-        leveraged_ssp: str = '',
+        leveraged_ssp: Optional[str] = None,
         force_overwrite: bool = False,
-        yaml_header: str = '',
+        include_all_parts: bool = False,
+        yaml_header: Optional[str] = None,
         overwrite_header_values: bool = False
     ) -> bool:
         """Generate ssp markdown from OSCAL Profile and Component Definitions."""
@@ -637,6 +638,7 @@ class AgileAuthoring(Repository):
             leveraged_ssp=leveraged_ssp,
             trestle_root=self.root_dir,
             force_overwrite=force_overwrite,
+            include_all_parts=include_all_parts,
             yaml_header=yaml_header,
             overwrite_header_values=overwrite_header_values,
             verbose=verbose
diff --git a/trestle/tasks/cis_xlsx_to_oscal_catalog.py b/trestle/tasks/cis_xlsx_to_oscal_catalog.py
index cc68c6ae..1fd5d52d 100644
--- a/trestle/tasks/cis_xlsx_to_oscal_catalog.py
+++ b/trestle/tasks/cis_xlsx_to_oscal_catalog.py
@@ -78,7 +78,7 @@ class XlsxHelper:
     def _mapper(self) -> None:
         """Map columns heading names to column numbers."""
         self._col_name_to_number = {}
-        cols = self._work_sheet.max_column
+        cols = self._work_sheet.max_column + 1
         row = 1
         for col in range(row, cols):
             cell = self._work_sheet.cell(row, col)
@@ -281,19 +281,27 @@ class CisXlsxToOscalCatalog(TaskBase):
 
     def _add_property(self, xlsx_helper: XlsxHelper, props: List[Property], row: int, key: str) -> None:
         """Add property."""
-        name = self._get_normalized_name(key)
-        value = xlsx_helper.get(row, key)
-        if value:
-            props.append(self._create_property(name, value))
+        try:
+            name = self._get_normalized_name(key)
+            value = xlsx_helper.get(row, key)
+            if value:
+                props.append(self._create_property(name, value))
+        except KeyError as e:
+            text = f'key {e} not found.'
+            logger.debug(text)
 
     def _add_property_boolean(self, xlsx_helper: XlsxHelper, props: List[Property], row: int, key: str) -> None:
         """Add property."""
-        name = self._get_normalized_name(key)
-        value = xlsx_helper.get(row, key)
-        if value:
-            props.append(self._create_property(name, 'True'))
-        else:
-            props.append(self._create_property(name, 'False'))
+        try:
+            name = self._get_normalized_name(key)
+            value = xlsx_helper.get(row, key)
+            if value:
+                props.append(self._create_property(name, 'True'))
+            else:
+                props.append(self._create_property(name, 'False'))
+        except KeyError as e:
+            text = f'key {e} not found.'
+            logger.debug(text)
 
     def _add_part(self, xlsx_helper: XlsxHelper, parts: List[Part], id_: str, row: int, key: str) -> None:
         """Add part."""
diff --git a/version.json b/version.json
new file mode 100644
index 00000000..beea20fa
--- /dev/null
+++ b/version.json
@@ -0,0 +1,4 @@
+{
+    "PYTHON_MIN": "3.9",
+    "PYTHON_MAX": "3.11"
+}
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index 607bcb57..3d8ef517 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -7,7 +7,21 @@ on:
     branches:
       - develop
 jobs:
+  set-versions:
+    runs-on: ubuntu-latest
+    outputs:
+      min: ${{ steps.versions.outputs.min }}
+      max: ${{ steps.versions.outputs.max }}
+    steps:
+    - uses: actions/checkout@v4
+    - id: versions
+      run: |
+        min_version=$(jq '.PYTHON_MIN' -r version.json)
+        max_version=$(jq '.PYTHON_MAX' -r version.json)
+        echo "min=$min_version" >> $GITHUB_OUTPUT
+        echo "max=$max_version" >> $GITHUB_OUTPUT
   lint:
+    needs: set-versions
     runs-on: ubuntu-latest
     steps:
     - name: Don't mess with line endings
@@ -19,13 +33,13 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v5
       with:
-        python-version: ${{ vars.PYTHON_MAX }}
+        python-version: ${{ needs.set-versions.outputs.max }}
     - uses: actions/cache@v4
       with:
         path: ~/.cache/pip
-        key: ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-${{ hashFiles('setup.cfg') }}
+        key: ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-${{ hashFiles('setup.cfg') }}
         restore-keys: |
-          ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-
+          ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-
     - name: Install build tools
       run: |
         make develop
@@ -58,6 +72,7 @@ jobs:
 # This test simulates what it is like for a user to install trestle today.
 # Coverage cannot be calculated as part of
   bdist:
+    needs: set-versions
     runs-on: ubuntu-latest
     steps:
     - name: Don't mess with line endings
@@ -72,13 +87,13 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v5
       with:
-        python-version: ${{ vars.PYTHON_MAX }}
+        python-version: ${{ needs.set-versions.outputs.max }}
     - uses: actions/cache@v4
       with:
         path: ~/.cache/pip
-        key: ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-${{ hashFiles('setup.cfg') }}
+        key: ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-${{ hashFiles('setup.cfg') }}
         restore-keys: |
-          ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-
+          ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-
     - name: Install build tools
       run: |
         make develop
@@ -88,11 +103,12 @@ jobs:
 
   test:
     # This test
+    needs: set-versions
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [ '${{ vars.PYTHON_MIN }}', '${{ vars.PYTHON_MAX }}' ]
+        python-version: [ '${{ needs.set-versions.outputs.min }}', '${{ needs.set-versions.outputs.max }}' ]
         include:
         - os: ubuntu-latest
           path: ~/.cache/pip
@@ -118,29 +134,32 @@ jobs:
         key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('setup.cfg') }}
         restore-keys: |
          ${{ matrix.os }}-${{ matrix.python-version }}-pip-
+    - name: Is core test version
+      id: core-version
+      run: echo "core=${{ (matrix.os == 'ubuntu-latest' && matrix.python-version == needs.set-versions.outputs.max ) }}" >> $GITHUB_OUTPUT
     - name: Install build tools
       run: |
         make develop
     - name: Pytest Fast
-      if: ${{ !(matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX) }}
+      if: steps.core-version.outputs.core != 'true'
       run: |
         make test
     - name: Pytest Cov
-      if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX }}
+      if: steps.core-version.outputs.core == 'true'
       run: |
         make test-cov
 
     - name: Upload artifact
-      if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == vars.PYTHON_MAX }}
+      if: steps.core-version.outputs.core == 'true'
       uses: actions/upload-artifact@v2
       with:
         name: coverage
         path: coverage.xml
 
   sonar:
-    if: ${{ github.event.pull_request.base.repo.url == github.event.pull_request.head.repo.url }}
+    if: ${{ (github.event.pull_request.base.repo.url == github.event.pull_request.head.repo.url && github.triggering_actor != 'dependabot[bot]' ) }}
     runs-on: ubuntu-latest
-    needs: test
+    needs: [ test, set-versions]
     steps:
     - name: Don't mess with line endings
       run: |
@@ -154,13 +173,13 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v5
       with:
-        python-version: ${{ vars.PYTHON_MAX }}
+        python-version: ${{ needs.set-versions.outputs.max }}
     - uses: actions/cache@v4
       with:
         path: ~/.cache/pip
-        key: ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-${{ hashFiles('setup.cfg') }}
+        key: ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-${{ hashFiles('setup.cfg') }}
         restore-keys: |
-          ubuntu-latest-${{ vars.PYTHON_MAX }}-pip-
+          ubuntu-latest-${{ needs.set-versions.outputs.max }}-pip-
     - name: Install build tools
       run: |
         make develop
@@ -178,7 +197,7 @@ jobs:
           -Dsonar.python.coverage.reportPaths=coverage.xml
           -Dsonar.tests=tests/
           -Dsonar.sources=trestle/ 
-          -Dsonar.python.version=${{ vars.PYTHON_MAX }}
+          -Dsonar.python.version=${{ needs.set-versions.outputs.max }}
           -Dsonar.projectKey=compliance-trestle
           -Dsonar.organization=compliance-trestle
           -Dsonar.cpd.exclusions=trestle/oscal/*.py
diff --git a/tests/data/json/profile_aggregation.json b/tests/data/json/profile_aggregation.json
new file mode 100644
index 00000000..f6e6459a
--- /dev/null
+++ b/tests/data/json/profile_aggregation.json
@@ -0,0 +1,106 @@
+{
+  "profile": {
+    "uuid": "83be30e4-6c88-40e0-9927-286ff0e43d59",
+    "metadata": {
+      "title": "SI-7 profile",
+      "last-modified": "2024-06-24T19:35:43.973109+00:00",
+      "version": "0.0.1",
+      "oscal-version": "1.1.2"
+    },
+    "imports": [
+      {
+        "href": "https://raw.githubusercontent.com/usnistgov/oscal-content/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json",
+        "include-controls": [
+          {
+            "with-ids": [
+              "si-7"
+            ]
+          }
+        ]
+      }
+    ],
+    "modify": {
+      "set-parameters": [
+        {
+          "param-id": "si-07_odp.01",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "software"
+          ]
+        },
+        {
+          "param-id": "si-07_odp.02",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "firmware"
+          ]
+        },
+        {
+          "param-id": "si-07_odp.03",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "information"
+          ]
+        },
+        {
+          "param-id": "si-07_odp.04",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "notify the System Owner, ISSO, ISSM, and the Incident Response team"
+          ]
+        },
+        {
+          "param-id": "si-07_odp.05",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "notify the System Owner, ISSO, ISSM, and the Incident Response team"
+          ]
+        },
+        {
+          "param-id": "si-07_odp.06",
+          "props": [
+            {
+              "name": "param-value-origin",
+              "value": "OCISO"
+            }
+          ],
+          "values": [
+            "notify the System Owner, ISSO, ISSM, and the Incident Response team"
+          ]
+        },
+        {
+          "param-id": "si-7_prm_1"
+        },
+        {
+          "param-id": "si-7_prm_2"
+        }
+      ],
+      "alters": []
+    }
+  }
+}
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 354d17a9..29c746ad 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -484,6 +484,7 @@ def setup_for_ssp(
         output=output_name,
         verbose=0,
         overwrite_header_values=False,
+        include_all_parts=False,
         yaml_header=yaml_path,
         allowed_sections=None,
         force_overwrite=None
@@ -510,6 +511,7 @@ def setup_for_ssp_fedramp(
         output=output_name,
         verbose=0,
         overwrite_header_values=False,
+        include_all_parts=False,
         yaml_header=None,
         allowed_sections=None,
         force_overwrite=None
diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py
index b2afbfc9..ec4df8d7 100644
--- a/tests/trestle/core/commands/author/ssp_test.py
+++ b/tests/trestle/core/commands/author/ssp_test.py
@@ -1168,3 +1168,210 @@ def test_ssp_gen_and_assemble_add_props(tmp_trestle_dir: pathlib.Path) -> None:
     assert len(smt_a.props) == 1
     assert smt_a.props[0].name == 'smt_prop'
     assert smt_a.props[0].value == 'smt prop'
+
+
+def test_ssp_gen_and_assemble_implementation_parts(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
+    """Test ssp generate and assemble edit implementation parts."""
+    gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name)
+    args_compdefs = gen_args.compdefs
+    gen_args.include_all_parts = True
+
+    ssp_gen = SSPGenerate()
+    assert ssp_gen._run(gen_args) == 0
+
+    prose_sys = 'My response for This System'
+    prose_aa = 'My response for comp aa'
+    prose_sys_a = 'My response for This System part a.'
+    prose_aa_a = 'My response for comp aa part a.'
+
+    # ac-1 edit
+    ac_1_path = tmp_trestle_dir / ssp_name / 'ac/ac-1.md'
+    assert test_utils.substitute_text_in_file(
+        ac_1_path, '<!-- Add implementation prose for the main This System component for control: ac-1 -->', prose_sys
+    )
+    assert test_utils.substitute_text_in_file(
+        ac_1_path,
+        '<!-- Add implementation prose for the main This System component for control: ac-1_smt.a -->',
+        prose_sys_a
+    )
+    assert test_utils.substitute_text_in_file(ac_1_path, 'imp req prose for ac-1 from comp aa', prose_aa)
+    assert test_utils.substitute_text_in_file(ac_1_path, 'statement prose for part a. from comp aa', prose_aa_a)
+    # change status for sys comp
+    assert test_utils.substitute_text_in_file(ac_1_path, 'Status: planned', 'Status: alternative')
+
+    part_a_text_edited = """## Implementation for part a.
+
+### This System
+
+My response for This System part a.
+
+#### Implementation Status: planned
+
+### comp_aa
+
+My response for comp aa part a.
+
+#### Rules:
+
+  - comp_rule_aa_1
+
+#### Implementation Status: partial
+
+### comp_ab
+
+<!-- Add control implementation description here for item a. -->
+
+#### Rules:
+
+  - comp_rule_ab_1
+
+#### Implementation Status: partial
+
+______________________________________________________________________
+"""
+
+    md_api = MarkdownAPI()
+    _, tree = md_api.processor.process_markdown(ac_1_path)
+    node = tree.get_node_for_key('## Implementation for part a.')
+    assert node.content.raw_text == part_a_text_edited
+
+    # now assemble the edited controls into json ssp
+    ssp_assemble = SSPAssemble()
+    args = argparse.Namespace(
+        trestle_root=tmp_trestle_dir,
+        markdown=ssp_name,
+        output=ssp_name,
+        verbose=0,
+        regenerate=False,
+        version='',
+        name=None,
+        compdefs=args_compdefs
+    )
+    assert ssp_assemble._run(args) == 0
+
+    # Verify the correct information is in the assembled ssp
+    ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan, FileContentType.JSON)
+    imp_reqs = ssp.control_implementation.implemented_requirements
+    ac_1_imp_req = next((i_req for i_req in imp_reqs if i_req.control_id == 'ac-1'), None)
+    assert ac_1_imp_req is not None
+
+    # comp_aa is the first component in the list and This System is the last
+    assert ac_1_imp_req.by_components[0].description == prose_aa  # type: ignore
+    assert ac_1_imp_req.by_components[2].implementation_status.state == 'alternative'  # type: ignore
+    assert ac_1_imp_req.by_components[2].description == prose_sys  # type: ignore
+    ac_1_a_smt = next((smt for smt in ac_1_imp_req.statements if smt.statement_id == 'ac-1_smt.a'), None)
+    assert ac_1_a_smt is not None
+    assert ac_1_a_smt.by_components[0].description == prose_aa_a  # type: ignore
+    assert ac_1_a_smt.by_components[2].description == prose_sys_a  # type: ignore
+
+    # Regeneration checks to make sure the markdown is not overwritten
+    assert ssp_gen._run(gen_args) == 0
+    _, tree = md_api.processor.process_markdown(ac_1_path)
+    node = tree.get_node_for_key('## Implementation for part a.')
+    assert node.content.raw_text == part_a_text_edited
+    assert test_utils.confirm_text_in_file(ac_1_path, const.SSP_MD_IMPLEMENTATION_QUESTION, prose_sys)
+
+
+def test_ssp_generate_no_cds_include_all_parts(tmp_trestle_dir: pathlib.Path) -> None:
+    """Test the ssp generator with no comp defs and include all parts are true."""
+    args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name)
+
+    args.compdefs = None
+    args.include_all_parts = True
+    ssp_cmd = SSPGenerate()
+    assert ssp_cmd._run(args) == 0
+    md_dir = tmp_trestle_dir / ssp_name
+    ac_1 = md_dir / 'ac/ac-1.md'
+    assert ac_1.exists()
+    at_2 = md_dir / 'at/at-2.md'
+    assert at_2.exists()
+
+    md_api = MarkdownAPI()
+    header, tree = md_api.processor.process_markdown(ac_1)
+    assert header[const.TRESTLE_GLOBAL_TAG][const.SORT_ID] == 'ac-01'
+
+    part_a_text_no_comp = """## Implementation for part a.
+
+### This System
+
+<!-- Add implementation prose for the main This System component for control: ac-1_smt.a -->
+
+#### Implementation Status: planned
+
+______________________________________________________________________
+"""
+
+    node = tree.get_node_for_key('## Implementation for part a.')
+    assert node.content.raw_text == part_a_text_no_comp
+
+
+def test_ssp_generate_aggregates_no_cds(tmp_trestle_dir: pathlib.Path) -> None:
+    """Test the ssp generator with no comp defs does aggregate values from aggregated parameters."""
+    args, _ = setup_for_ssp(tmp_trestle_dir, 'profile_aggregation', ssp_name)
+
+    args.compdefs = None
+    ssp_cmd = SSPGenerate()
+    assert ssp_cmd._run(args) == 0
+    md_dir = tmp_trestle_dir / ssp_name
+    si_7 = md_dir / 'si-7.md'
+    assert si_7.exists()
+
+    md_api = MarkdownAPI()
+    header, tree = md_api.processor.process_markdown(si_7)
+    si_7_odp_01 = header['x-trestle-set-params']['si-07_odp.01']
+    si_7_odp_01['ssp-values'] = ['changed value in the ssp markdown']
+
+    md_api.write_markdown_with_header(si_7, header, tree.content.raw_text)
+
+    # now assemble the edited controls into json ssp
+    ssp_assemble = SSPAssemble()
+    assemble_args = argparse.Namespace(
+        trestle_root=tmp_trestle_dir,
+        markdown=ssp_name,
+        output=ssp_name,
+        verbose=0,
+        regenerate=False,
+        version='',
+        name=None,
+        compdefs=None
+    )
+    assert ssp_assemble._run(assemble_args) == 0
+
+    # Verify the correct information is in the assembled ssp
+    ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan, FileContentType.JSON)
+    imp_reqs = ssp.control_implementation.implemented_requirements
+    si_7_imp_req = next((i_req for i_req in imp_reqs if i_req.control_id == 'si-7'), None)
+    si_07_odp_01 = next((param for param in si_7_imp_req.set_parameters if param.param_id == 'si-07_odp.01'), None)
+    changed_value_in_ssp = next(
+        (val for val in si_07_odp_01.values if val == 'changed value in the ssp markdown'), None
+    )
+    assert changed_value_in_ssp is not None
+
+    # regenerate the SSP again
+    ssp_cmd = SSPGenerate()
+    assert ssp_cmd._run(args) == 0
+    md_dir = tmp_trestle_dir / ssp_name
+    si_7 = md_dir / 'si-7.md'
+    assert si_7.exists()
+
+    md_api = MarkdownAPI()
+    header, tree = md_api.processor.process_markdown(si_7)
+    si_7_odp_01 = header['x-trestle-set-params']['si-07_odp.01']
+    assert 'changed value in the ssp markdown' in si_7_odp_01['ssp-values']
+
+
+def test_ssp_generate_aggregates_no_param_value_orig(tmp_trestle_dir: pathlib.Path) -> None:
+    """Test the ssp generator aggregate parameters have no parame-value-origin."""
+    args, _ = setup_for_ssp(tmp_trestle_dir, 'profile_aggregation', ssp_name)
+
+    args.compdefs = None
+    ssp_cmd = SSPGenerate()
+    assert ssp_cmd._run(args) == 0
+    md_dir = tmp_trestle_dir / ssp_name
+    si_7 = md_dir / 'si-7.md'
+    assert si_7.exists()
+
+    md_api = MarkdownAPI()
+    header, _ = md_api.processor.process_markdown(si_7)
+    si_7_prm_1 = header['x-trestle-set-params']['si-7_prm_1']
+    assert const.PARAM_VALUE_ORIGIN not in si_7_prm_1.keys()
diff --git a/tests/trestle/tasks/cis_xlsx_to_oscal_catalog_test.py b/tests/trestle/tasks/cis_xlsx_to_oscal_catalog_test.py
index 996a5ba2..cd1b1ec2 100644
--- a/tests/trestle/tasks/cis_xlsx_to_oscal_catalog_test.py
+++ b/tests/trestle/tasks/cis_xlsx_to_oscal_catalog_test.py
@@ -185,6 +185,57 @@ def test_cis_xlsx_to_oscal_catalog_unexpected_section(tmp_path: pathlib.Path):
         assert retval == TaskOutcome.FAILURE
 
 
+def test_cis_xlsx_to_oscal_catalog_no_status(tmp_path: pathlib.Path):
+    """Test no column with name status."""
+    folder = 'tests/data/tasks/cis-xlsx-to-oscal-catalog'
+    file_ = f'{folder}/CIS_RedHat_OpenShift_Container_Platform_Benchmark_v1.2.0-2.snippet.xlsx'
+    wb_hacked = load_workbook(file_)
+    sheet = wb_hacked['Combined Profiles']
+    cell = sheet.cell(1, 5)
+    assert cell.value == 'status'
+    cell.value = 'X'
+    with mock.patch('trestle.tasks.cis_xlsx_to_oscal_catalog.load_workbook') as load_workbook_mock:
+        load_workbook_mock.return_value = wb_hacked
+        section = _get_section(tmp_path, ocp_config)
+        tgt = cis_xlsx_to_oscal_catalog.CisXlsxToOscalCatalog(section)
+        retval = tgt.execute()
+        assert retval == TaskOutcome.SUCCESS
+
+
+def test_cis_xlsx_to_oscal_catalog_no_mitre(tmp_path: pathlib.Path):
+    """Test no column with name mitre."""
+    folder = 'tests/data/tasks/cis-xlsx-to-oscal-catalog'
+    file_ = f'{folder}/CIS_RedHat_OpenShift_Container_Platform_Benchmark_v1.2.0-2.snippet.xlsx'
+    wb_hacked = load_workbook(file_)
+    sheet = wb_hacked['Combined Profiles']
+    cell = sheet.cell(1, 20)
+    assert cell.value == 'MITRE ATT&CK Mappings'
+    cell.value = 'X'
+    with mock.patch('trestle.tasks.cis_xlsx_to_oscal_catalog.load_workbook') as load_workbook_mock:
+        load_workbook_mock.return_value = wb_hacked
+        section = _get_section(tmp_path, ocp_config)
+        tgt = cis_xlsx_to_oscal_catalog.CisXlsxToOscalCatalog(section)
+        retval = tgt.execute()
+        assert retval == TaskOutcome.SUCCESS
+
+
+def test_cis_xlsx_to_oscal_catalog_no_v7_ig1(tmp_path: pathlib.Path):
+    """Test no column with name v7 IG1."""
+    folder = 'tests/data/tasks/cis-xlsx-to-oscal-catalog'
+    file_ = f'{folder}/CIS_RedHat_OpenShift_Container_Platform_Benchmark_v1.2.0-2.snippet.xlsx'
+    wb_hacked = load_workbook(file_)
+    sheet = wb_hacked['Combined Profiles']
+    cell = sheet.cell(1, 14)
+    assert cell.value == 'v7 IG1'
+    cell.value = 'Z'
+    with mock.patch('trestle.tasks.cis_xlsx_to_oscal_catalog.load_workbook') as load_workbook_mock:
+        load_workbook_mock.return_value = wb_hacked
+        section = _get_section(tmp_path, ocp_config)
+        tgt = cis_xlsx_to_oscal_catalog.CisXlsxToOscalCatalog(section)
+        retval = tgt.execute()
+        assert retval == TaskOutcome.SUCCESS
+
+
 def test_cis_xlsx_to_oscal_catalog_execute_rhel(tmp_path: pathlib.Path):
     """Test execute call - rhel."""
     section = _get_section(tmp_path, rhel_config)