Skip to content

Commit 2c08291

Browse files
authored
Merge pull request #14060 from Mab879/per_product_control_files
Add per product control files
2 parents f471d0c + 7bfaf6b commit 2c08291

File tree

18 files changed

+111
-29
lines changed

18 files changed

+111
-29
lines changed

build-scripts/compile_all.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,15 @@ def main():
213213
load_benchmark_source_data_from_directory_tree(loader, env_yaml, product_yaml)
214214

215215
controls_dir = os.path.join(project_root_abspath, "controls")
216+
product_controls_dir = os.path.join(product_yaml['product_dir'], 'controls')
217+
controls_dirs = [controls_dir]
218+
if os.path.exists(product_controls_dir):
219+
controls_dirs.append(product_controls_dir)
216220

217221
existing_rules = find_existing_rules(project_root_abspath)
218222

219223
controls_manager = ssg.controls.ControlsManager(
220-
controls_dir, env_yaml, existing_rules)
224+
controls_dirs, env_yaml, existing_rules)
221225
controls_manager.load()
222226
controls_manager.remove_selections_not_known(loader.all_rules)
223227
controls_manager.add_references(loader.all_rules)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
content-version: 0.1.79
3+
title: ADR-0003 - Per Product Controls
4+
status: proposed
5+
---
6+
7+
## Context
8+
As of late October 2025 there was over 50 control files in the `controls` folder.
9+
Many of these control files are product specific.
10+
The goal of this ADR is to help keep product specific information separate from the global standard like ANSSI.
11+
12+
13+
## Decision
14+
We will allow the creation of `controls` directory under each product.
15+
All product specific control files will be moved to the product specific control file.
16+
The product specific control files in `products/example/controls` can override the controls in `controls`.
17+
18+
## Consequences
19+
This will create more places to look for control files, but it will help keep product specific information with the product.

docs/manual/developer/03_creating_content.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,8 @@ controls:
837837
- systemd_target_multi_user
838838
```
839839

840+
Control files that apply to multiple products should be stored in `controls` folder in the root of the project.
841+
If the control file is only applicable to one product it should be store in the `controls` directory under the products folder.
840842

841843
### Defining levels
842844

docs/manual/developer/04_style_guide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ Benchmark sections must be in the following order, if they are present.
317317

318318
### Controls
319319

320-
These rules apply to the files in `controls/`.
320+
These rules apply to the files in `controls/` and `products/*/controls`.
321+
Product specific controls should be stored under the respective controls directory.
321322
All the above [YAML](#yaml) rules apply.
322323

323324
#### Control Sections
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

ssg/controls.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import collections
66
import os
77
import copy
8+
import sys
89
from glob import glob
10+
from typing import List, Dict
911

1012
import ssg.entities.common
1113
import ssg.yaml
@@ -369,6 +371,7 @@ class Policy(ssg.entities.common.XCCDFEntity):
369371
product (list): A list of products associated with the policy.
370372
"""
371373
def __init__(self, filepath, env_yaml=None):
374+
self.controls_dirs = []
372375
self.id = None
373376
self.env_yaml = env_yaml
374377
self.filepath = filepath
@@ -637,6 +640,7 @@ def load(self):
637640
controls_dir = yaml_contents.get("controls_dir")
638641
if controls_dir:
639642
self.controls_dir = os.path.join(os.path.dirname(self.filepath), controls_dir)
643+
self.controls_dirs = [self.controls_dir]
640644
self.id = ssg.utils.required_key(yaml_contents, "id")
641645
self.policy = ssg.utils.required_key(yaml_contents, "policy")
642646
self.title = ssg.utils.required_key(yaml_contents, "title")
@@ -786,17 +790,17 @@ def add_references(self, rules):
786790
control.add_references(self.reference_type, rules)
787791

788792

789-
class ControlsManager():
793+
class ControlsManager:
790794
"""
791795
Manages the loading, processing, and saving of control policies.
792796
793797
Attributes:
794-
controls_dir (str): The directory where control policy files are located.
798+
controls_dirs (List[str]): The directories where control policy files are located.
795799
env_yaml (str, optional): The environment YAML file.
796800
existing_rules (dict, optional): Existing rules to check against.
797801
policies (dict): A dictionary of loaded policies.
798802
"""
799-
def __init__(self, controls_dir, env_yaml=None, existing_rules=None):
803+
def __init__(self, controls_dirs: List[str], env_yaml=None, existing_rules=None):
800804
"""
801805
Initializes the Controls class.
802806
@@ -805,19 +809,25 @@ def __init__(self, controls_dir, env_yaml=None, existing_rules=None):
805809
env_yaml (str, optional): Path to the environment YAML file. Defaults to None.
806810
existing_rules (dict, optional): Dictionary of existing rules. Defaults to None.
807811
"""
808-
self.controls_dir = os.path.abspath(controls_dir)
812+
self.controls_dirs = [os.path.abspath(controls_dir) for controls_dir in controls_dirs]
809813
self.env_yaml = env_yaml
810814
self.existing_rules = existing_rules
811815
self.policies = {}
812816

813817
def _load(self, format):
814-
if not os.path.exists(self.controls_dir):
815-
return
816-
for filename in sorted(glob(os.path.join(self.controls_dir, "*." + format))):
817-
filepath = os.path.join(self.controls_dir, filename)
818-
policy = Policy(filepath, self.env_yaml)
819-
policy.load()
820-
self.policies[policy.id] = policy
818+
for controls_dir in self.controls_dirs:
819+
if not os.path.isdir(controls_dir):
820+
continue
821+
for filepath in sorted(glob(os.path.join(controls_dir, "*." + format))):
822+
policy = Policy(filepath, self.env_yaml)
823+
policy.load()
824+
if policy.id in self.policies:
825+
print(f"Policy {policy.id} was defined first at "
826+
f"{self.policies[policy.id].filepath} and now another policy "
827+
f"with the same ID is being loaded from {policy.filepath}."
828+
f"Overriding with later.",
829+
file=sys.stderr)
830+
self.policies[policy.id] = policy
821831
self.check_all_rules_exist()
822832
self.resolve_controls()
823833

@@ -948,7 +958,7 @@ def get_control(self, policy_id, control_id):
948958
control = policy.get_control(control_id)
949959
return control
950960

951-
def get_all_controls_dict(self, policy_id):
961+
def get_all_controls_dict(self, policy_id: str) -> Dict[str, Control]:
952962
"""
953963
Retrieve all controls for a given policy as a dictionary.
954964
@@ -959,7 +969,6 @@ def get_all_controls_dict(self, policy_id):
959969
Dict[str, list]: A dictionary where the keys are control IDs and the values are lists
960970
of controls.
961971
"""
962-
# type: (str) -> typing.Dict[str, list]
963972
policy = self._get_policy(policy_id)
964973
return policy.controls_by_id
965974

0 commit comments

Comments
 (0)