From 645c9951a1d6a0862ea8ca93a24ea788f9ce3dcc Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 4 Nov 2024 15:24:49 +0100 Subject: [PATCH] Introduce the 'aap_rules_validation' role (#16) * add default_environment to org Default for controller_rules testing * add users of different types * create first version of controller_rules_validation role * sample of controller_rules used by controller_rules_validation role * playbooks used to test controller_rules_validation role * add default empty rules to avoid errors * improve and clarify example rules * create roles README * improvements * align role name with collection scope * replace deprecated with_dict * fix markdown linting issues * fix new role name * improve examples --- roles/aap_rules_validation/README.md | 364 ++++++++++++++++++ roles/aap_rules_validation/defaults/main.yml | 158 ++++++++ .../tasks/check_credentials_encryption.yml | 56 +++ .../tasks/check_fields_regex.yml | 37 ++ .../tasks/check_hosts.yml | 38 ++ .../tasks/check_inventories.yml | 29 ++ .../tasks/check_mandatory_fields.yml | 76 ++++ .../tasks/check_objects_count.yml | 91 +++++ .../tasks/check_organizations.yml | 90 +++++ .../tasks/check_roles.yml | 132 +++++++ .../tasks/check_users.yml | 79 ++++ roles/aap_rules_validation/tasks/main.yml | 57 +++ .../aap_rules_validation/tasks/rule_check.yml | 105 +++++ tests/configs/aap_rules.yml | 230 +++++++++++ tests/configs/organizations.yml | 1 + tests/configs/user_accounts.yml | 13 + ...troller_rules_validation_filetree_read.yml | 7 + .../controller_rules_validation_include.yml | 11 + 18 files changed, 1574 insertions(+) create mode 100644 roles/aap_rules_validation/README.md create mode 100644 roles/aap_rules_validation/defaults/main.yml create mode 100644 roles/aap_rules_validation/tasks/check_credentials_encryption.yml create mode 100644 roles/aap_rules_validation/tasks/check_fields_regex.yml create mode 100644 roles/aap_rules_validation/tasks/check_hosts.yml create mode 100644 roles/aap_rules_validation/tasks/check_inventories.yml create mode 100644 roles/aap_rules_validation/tasks/check_mandatory_fields.yml create mode 100644 roles/aap_rules_validation/tasks/check_objects_count.yml create mode 100644 roles/aap_rules_validation/tasks/check_organizations.yml create mode 100644 roles/aap_rules_validation/tasks/check_roles.yml create mode 100644 roles/aap_rules_validation/tasks/check_users.yml create mode 100644 roles/aap_rules_validation/tasks/main.yml create mode 100644 roles/aap_rules_validation/tasks/rule_check.yml create mode 100644 tests/configs/aap_rules.yml create mode 100644 tests/playbooks/controller_rules_validation_filetree_read.yml create mode 100644 tests/playbooks/controller_rules_validation_include.yml diff --git a/roles/aap_rules_validation/README.md b/roles/aap_rules_validation/README.md new file mode 100644 index 00000000..c3f9f279 --- /dev/null +++ b/roles/aap_rules_validation/README.md @@ -0,0 +1,364 @@ +# aap_rules_validation + +An ansible role which audit the declared AAP configuration and validate it against a set of user-defined rules. + +At the end of the role's execution, a structured data list and human readable report are generated containing the detected violations. + +The actual version of the role supports only the controller component + +## Requirements + +n/a + +## Role Input Variables + +| Variable Name | Default Value | Required | Description | +| :------------ | :-----------: | :------: | :---------- | +| `aap_rules` | `[]` | yes | The list of rules to enforce on the declared configuration | +| `fail_if_violations_found` | `true` | no | Force the role to fails if if it finds violations | +| `print_rules_violations_data` | `true` | no | Print the detailed violation data before printing the violation messages | +| `audited_objects` | a list of all the objet types | no | The objects to be audited. See the roles defaults main.yml file for the complete list | +| `warn_about_audited_types_not_in_rules_objects` | `false` | no | Treat the objects to be audited but are not in any rule as a violation | +| `warn_about_rules_objects_not_in_audited_types` | `false` | no | Treat the objects that are defined in rules are not in the audited objects list as a violation | + +## Role Output Variables + +| Variable Name | Description | +| :------------ | :---------- | +| `rules_violations_data` | a list of dictionaries containing all the found rules violation details | +| `rules_violations_msgs` | a list of all the found rules violation messages | + +### rules_violations_msgs format + +Each `rules_violations_msgs` list element has the following syntax : + +```markdown +Rule ID | Object Type | Object Scope | Object Name | Violation message related to this specific object" +``` + +Example of `rules_violations_msgs` : + +```json +fatal: [localhost]: FAILED! => { + "rules_violations_msgs | unique": [ + "Rule n°1 | organizations | global | Satellite | max_hosts is not set", + "Rule n°2 | organizations | global | Default | The EE (Automation Hub Default Execution Environment) is forbidden.", + "Rule n°3 | projects | Default | Test Project 3 | The value of the field name (Test Project 3) do not respect the regex (^\\[Default\\].*)", + "Rule n°4 | groups | global | group3 | The mandatory field 'description' is not defined" + ] +} +``` + +### rules_violations_data structure + +Each `rules_violations_data` list element contains the following elements : + +| Sub-element Name | Description | +| :------------ | :---------- | +| `msg` | The violation message relative to this object. Same format as in `rules_violations_msgs` | +| `object_name` | Name of the non-compliant object | +| `object_organization` | Name of the organization to which belong the affected object if available. | +| `object_scope` | Scope of the non-compliant object, one of two values : `global` or `organization` | +| `object_type` | Type of the non-compliant object | +| `rule_broken` | The rule not respected by the non-compliant object | +| `rule_id` | Rule name if available otherwise the rule order (index + 1) | +| `rule_index` | Index of rule (starting from 0) | + +Example of `rules_violations_data` : + +```json +"rules_violations_data | unique": [ + { + "msg": "Rule n°1 | organizations | global | Satellite | max_hosts is not set", + "object_name": "Satellite", + "object_organization": "__undefined_org__", + "object_scope": "global", + "object_type": "organizations", + "rule_broken": "max_hosts_per_organization", + "rule_id": "n°1", + "rule_index": 0 + }, + ... +``` + +## Rules + +The rules should be defined as a list in the variable `aap_rules` + +Each element of the list is a rule that is audited seperately + +There is generic rules fields which are object-type-agnostic and other fields that are applicable to specific object type + +### Generic Rules + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `rule_name` | string | A descriptive string set by the user to better identify the rule | +| `organizations` | list | Limit the audit to the objects belonging to the organizations specified in this list. Not applicable to every rule. See the rules below for mor details. | +| `objects` | list | The object types to be audited | +| `exceptions` | dictionary | The specific objects to be discarded from the audit. Applicable only to specific rules. See specific rules details below. | +| `mandatory_fields` | list | The fields that are mandatory. It is a violation if they are empty or undefined. At the moment, `organizations` is ineffective with this rule. | +| `minimum_defined_globally` | integer | The minimum objects count allowed to be defined globally for the objects specified in `objects` | +| `maximum_defined_globally` | integer | The maximum objects count allowed to be defined globally for the objects specified in `objects` | +| `minimum_defined_per_org` | integer | The minimum objects count allowed to be defined in the organizations specified in `organizations` for the objects specified in `objects` | +| `maximum_defined_per_org` | integer | The maximum objects count allowed to be defined in the organizations specified in `organizations` for the objects specified in `objects` | +| `fields_regex` | dictionary | control if the fields of the objects defined in `objects` respect the declared regular expression. The dictionary keys are the fields to be monitored and the values are the corresponding regex. See examples below. | + +#### Generic Rule Examples + +Here's examples of generic rules + +```yaml +aap_rules: + + # ------- Rule n°1 # Generic - Make 'description' a mandatory fields for the listed objects + - objects: + - credentials + - inventories + - inventory_sources + - job_templates + - projects + - teams + - users + - instances + mandatory_fields: + - description # the field 'description' must be defined and non-empty for each of the object types listed in 'objects' + + # ------- Rule n°2 # Generic - Minimum and Maximum + - rule_name: Minimum and Maximum + organizations: + - Satellite + - Default + objects: + - credentials + - groups + - inventories + - inventory_sources + - job_templates + - projects + - teams + - users + - credential_types + - instances + minimum_defined_globally: 3 # each object type listed in 'objects' must be defined at least 3 times in all of AAP + maximum_defined_globally: 5 + minimum_defined_per_org: 2 # each object type listed in 'objects' must be defined at least 2 times in each organization + maximum_defined_per_org: 4 + + # ------- Rule n°3 # Generic - Check if 'projects' and 'credentials' of the 'Default' org respect the regex + - organizations: + - Default + objects: + - projects + - credentials + fields_regex: + name: '^\[Default\].*' # name must start with '[Default]' + description: "^DESC - .*" # description must start with 'DESC - ' + scm_type: ^git$ # accept only git scm + scm_branch: ^main$ # branch naming rule : accept only main branch +``` + +### Object specific Rules + +#### Organizations + +The following rules are specific to organizations and are ineffective for other type of objects. + +The organizations specific rules are compatible with the `exceptions` field + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `max_hosts_per_organization` | integer | The maximum hosts count allowed for the organization | +| `allowed_organization_default_environments` | list | The only possible EEs to be used in the organization | +| `forbidden_organization_default_environments` | integer | The maximum hosts count allowed for the organization | + +##### Organizations specific rules example 1 : Allow only the specified EEs and deny everything else + +```yaml +aap_rules: + + # # ------- Rule n°4 # Organizations - Allow only the following EEs for the organizations 'Satellite' and 'Default' + - objects: + - organizations + organizations: + - Satellite + - Default + max_hosts_per_organization: 100 + allowed_organization_default_environments: + - Automation Hub Default Execution Environment + - Custom EE +``` + +##### Organizations specific rules example 2 : Deny only the specified EEs and allow everything else + +```yaml + # # ------- Rule n°5 # Organizations - Allow all EEs except the listed forbidden EEs for all organizations except the org 'Satellite' + - objects: + - organizations + max_hosts_per_organization: 10 + forbidden_organization_default_environments: + - Automation Hub Default Execution Environment + exceptions: + organizations: + - Satellite +``` + +#### Inventories + +The following rule is specific to the inventories. However it needs both `controller_hosts` and `controller_inventories` to be defined to work correctly. + +The inventories specific rules are compatible with the `exceptions` field + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `max_hosts_per_inventory` | integer | The maximum hosts count allowed for the organization | + +##### Inventories specific rules examples + +```yaml +aap_rules: + + # ------- Rule n°6 # inventories - The static hosts maximum count of each inventories of the organization 'Satellite' and 'Default' should not exceed 10 except the 'localhost' inventory + + - organizations: + - Satellite + - Default + objects: + - inventories + max_hosts_per_inventory: 10 # needs 'controller_hosts' and 'controller_inventories' to be defined + exceptions: + inventories: + - localhost +``` + +#### Hosts + +The following rule is specific to hosts. However it needs both `controller_groups` and `controller_hosts` to be defined to work correctly. + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `allow_ungrouped_hosts`| boolean | Set to `true` to flag ungrouped hosts as violations | + +##### Hosts specific rules examples + +```yaml +aap_rules: + + # ------- Rule n°7 # Hosts + - objects: + - hosts + allow_ungrouped_hosts: false # needs 'controller_groups' and 'controller_hosts' to be defined +``` + +#### Credentials + +The following rules are specific to credentials. + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `encrypt_credentials_sensitive_data` | boolean | Set to `true` to activate sensitive data encryption check | +| `credential_sensitive_data` | dictionary | The credential types and the lists of the sensitive fields to check. The dictionary keys are the credential type to check and the values are the list of the input sub-fields. See the example below. | + +**Important Note**: The sensitive data encryption check **will not work** if the credentials transit through intermediary variables, like when the `filetree_read` role is used. + +##### Credentials specific rules examples + +```yaml +aap_rules: + + # ------- Rule n°8 # Credentials + - encrypt_credentials_sensitive_data: true # DO NOT WORK with intermediary variables (filetree_read) + organizations: + - Default + - Satellite + objects: + - credentials + credential_sensitive_data: + Source Control: + - password + Red Hat Virtualization: + - password + Vault: + - vault_password +``` + +#### Users + +The following rules are specific to users. + +The users rules are compatible with the `exceptions` option. + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `allow_superusers`| boolean | Set to `false` to flag superusers as a violation | +| `allow_system_auditors`| boolean | Set to `false` to flag system auditors as a violation | +| `encrypt_user_passwords`| boolean | Set to `true` to flag unvaulted users passwords as a violation | + +**Important Note**: The `encrypt_user_passwords` option **will not work** if the users transit through intermediary variables, like when the `filetree_read` role is used. + +##### Users specific rules examples + +```yaml +aap_rules: + + # ------- Rule n°9 # Users - do not allow system auditors or super-admins except the 'controller-admin' and 'admin' users + - objects: + - users + allow_superusers: false + allow_system_auditors: false + encrypt_user_passwords: true + exceptions: + users: + - controller_admin + - admin +``` + +#### Roles + +The following rules are specific to roles. + +| Variable Name | Type | Description | +| :------------ | :------: | :---------- | +| `allowed_roles`| dictionary | The allowed objects roles. Any role not explicitly listed in this dictionary will be flagged as a violation. The dictionary keys are the object types and the values are the list of the allowed roles. See the example below. | +| `forbidden_roles`| dictionary | The forbidden objects roles. The roles listed in this dictionary will be considered as a violation. Any other role not specified in this dictionary is allowed. The dictionary keys are the object types and the values are the list of the forbidden roles. See the example below. | + +##### Roles specific rules example 1 : Allow only the specified roles. Deny anything else + +```yaml +aap_rules: + + # ------- Rule n°10 # Roles - Allow ONLY 'read' on projects, 'member' on organizations and 'admin' on teams + - objects: + - roles + allowed_roles: + projects: + - update + - read + organizations: + - member + target_teams: + - member +``` + +##### Roles specific rules example 2 : Deny only the specified roles. Allow everything else + +```yaml + # ------- Rule n°11 # Roles - Do not allow 'admin' on projects, 'admin' on organizations and 'admin' on teams, allow everything else. + - objects: + - roles + forbidden_roles: + projects: + - admin + organizations: + - admin + target_teams: + - admin +``` + +## License + +[GPLv3+](https://github.com/ansible/galaxy_collection#licensing) + +## Author + +[Hamza Bouabdallah](https://github.com/w4hf) diff --git a/roles/aap_rules_validation/defaults/main.yml b/roles/aap_rules_validation/defaults/main.yml new file mode 100644 index 00000000..d97e6b62 --- /dev/null +++ b/roles/aap_rules_validation/defaults/main.yml @@ -0,0 +1,158 @@ +--- +aap_rules: [] + +fail_if_violations_found: true + +print_rules_violations_data: true + +warn_about_audited_types_not_in_rules_objects: false +warn_about_rules_objects_not_in_audited_types: false + +audited_objects: + - applications + - credential_input_sources + - credentials + - credential_types + - execution_environments + - groups + - hosts + - instance_groups + - instances + - inventories + - inventory_sources + - job_templates + - labels + - notification_templates + - organizations + - projects + - roles + - settings + - teams + - users + - workflow_job_templates + +__object_var_names: + projects: controller_projects + job_templates: controller_templates + inventories: controller_inventories + applications: controller_applications + hosts: controller_hosts + credential_input_sources: controller_credential_input_sources + credentials: controller_credentials + credential_types: controller_credential_types + execution_environments: controller_execution_environments + groups: controller_groups + instance_groups: controller_instance_groups + instances: controller_instances + inventory_sources: controller_inventory_sources + labels: controller_labels + notification_templates: controller_notifications + organizations: controller_organizations + roles: controller_roles + settings: controller_settings + teams: controller_teams + users: controller_user_accounts + workflow_job_templates: controller_workflows + schedules: controller_schedules + +__singular: + users: user + teams: team + target_teams: target_team + inventories: inventory + job_templates: job_template + workflows: workflow + credentials: credential + organizations: organization + lookup_organization: lookup_organization + projects: project + instance_groups: instance_group + +__plural: + user: users + team: teams + target_team: target_teams + inventory: inventories + job_template: job_templates + workflow: workflows + credential: credentials + organization: organizations + lookup_organizations: lookup_organizations + project: projects + instance_group: instance_groups + +__org_scoped_objects: + - applications + - credentials + - inventories + - inventory_sources + - job_templates + - notification_templates + - projects + - teams + - users + - workflow_job_templates + +__global_scoped_objects: + - credential_input_sources + - credential_types + - execution_environments + - hosts + - groups + - instance_groups + - instances + - labels + - organizations + - roles + - schedules + - settings + +__scope: + applications: organization + credentials: organization + groups: global + inventories: organization + inventory_sources: organization + job_templates: organization + notification_templates: organization + projects: organization + teams: organization + users: global + workflow_job_templates: organization + credential_input_sources: global + credential_types: global + execution_environments: global + hosts: global + instance_groups: global + instances: global + labels: global + organizations: global + roles: global + schedules: global + settings: global + +__name_field: + applications: name + credentials: name + groups: name + inventories: name + inventory_sources: name + job_templates: name + notification_templates: name + projects: name + teams: name + users: username + workflow_job_templates: name + credential_input_sources: target_credential # optimally it should be "target_credential + '_' + input_field_name" + credential_types: name + execution_environments: name + hosts: name + instance_groups: name + instances: hostname + labels: name + organizations: name + roles: __undefined__ + schedules: name + settings: __undefined__ + +... diff --git a/roles/aap_rules_validation/tasks/check_credentials_encryption.yml b/roles/aap_rules_validation/tasks/check_credentials_encryption.yml new file mode 100644 index 00000000..08e7a74c --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_credentials_encryption.yml @@ -0,0 +1,56 @@ +--- +- name: Init sensitive data list + ansible.builtin.set_fact: + __sensitive_data: [] + __unencrypted: [] + +- name: Set sensitive list + ansible.builtin.set_fact: + __sensitive_data: "{{ __sensitive_data + ([item.key] | product(item.value)) }}" + loop: "{{ rule['credential_sensitive_data'] | dict2items }}" + +- name: Extract unencrypted credentials of all orgs + ansible.builtin.set_fact: + __unencrypted: "{{ __unencrypted + controller_credentials | selectattr('credential_type', 'defined') | selectattr('credential_type', 'equalto', sensitive_cred_type) | selectattr(sensitive_path, 'defined') | rejectattr(sensitive_path, 'vault_encrypted') | product([sensitive_path]) }}" + vars: + sensitive_cred_type: "{{ item[0] }}" + sensitive_path: "inputs.{{ item[1] }}" + loop: "{{ __sensitive_data }}" + when: rule['organizations'] is not defined + or rule['organizations'] == None + or rule['organizations'] | length == 0 + +- name: Extract unencrypted credentials of rule's orgs + ansible.builtin.set_fact: + __unencrypted: "{{ __unencrypted + controller_credentials | selectattr('organization', 'defined') | selectattr('organization', 'in', rule['organizations']) | selectattr('credential_type', 'defined') | selectattr('credential_type', 'equalto', sensitive_cred_type) | selectattr(sensitive_path, 'defined') | rejectattr(sensitive_path, 'vault_encrypted') | product([sensitive_path]) }}" + vars: + sensitive_cred_type: "{{ item[0] }}" + sensitive_path: "inputs.{{ item[1] }}" + loop: "{{ __sensitive_data }}" + when: rule['organizations'] is defined + and rule['organizations'] != None + and rule['organizations'] | length > 0 + +- name: Update rules violation regarding credentials encryption + ansible.builtin.set_fact: + rules_violations_msgs: "{{ (rules_violations_msgs + [msg]) }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'encrypt_credentials_sensitive_data', + 'object_type': 'credentials', + 'object_scope': 'organizations', + 'object_organization': __cred_org, + 'object_name': __cred_name, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | credentials | {{ __cred_org }} | {{ __cred_name }} | The credential sensitive field '{{ unencrypted_cred[1] }}' is not encrypted" + __cred_name: "{{ unencrypted_cred[0]['name'] }}" + __cred_org: "{{ unencrypted_cred[0]['organization'] | default('__undefined_org__') }}" + loop: "{{ __unencrypted }}" + loop_control: + loop_var: unencrypted_cred +... diff --git a/roles/aap_rules_validation/tasks/check_fields_regex.yml b/roles/aap_rules_validation/tasks/check_fields_regex.yml new file mode 100644 index 00000000..25ddb1ab --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_fields_regex.yml @@ -0,0 +1,37 @@ +--- +- name: Init regex issues list + ansible.builtin.set_fact: + __regex_issue: [] + +- name: Extract objects with regex issue - {{ object_type }} + ansible.builtin.set_fact: + __regex_issue: "{{ __regex_issue + ([{'field_regex':field_regex.key, 'regex_value':field_regex.value, 'object_type':object_type}] | product(lookup('vars', __object_var_names[object_type]) | selectattr(field_regex.key, 'defined') | rejectattr(field_regex.key, 'regex', field_regex.value))) }}" + loop: "{{ rule['fields_regex'] | dict2items }}" + loop_control: + loop_var: field_regex + +- name: Update violation msgs with regex issue - {{ object_type }} + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'fields_regex', + 'object_type': object_type, + 'object_scope': __scope[object_type], + 'object_organization': item[1]['organization'] | default('__undefined_org__'), + 'object_name': __object_name, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ __object_type }} | {{ __object_scope }} | {{ __object_name }} | The value of the field {{ __field }} ({{ __value }}) do not respect the regex ({{ __regex }})" + __object_type: "{{ item[0]['object_type'] }}" + __object_scope: "{{ item[1]['organization'] | default(__scope[object_type]) | default('__undefined_org__') }}" + __object_name: "{{ item[1][__name_field[object_type]] | default(item[1]['user']) | default('__undefined_name__') }}" # TO BE CHECKED FOR ALTERNATIVE 'name' fields + __field: "{{ item[0]['field_regex'] }}" + __value: "{{ item[1][item[0]['field_regex']] }}" + __regex: "{{ item[0]['regex_value'] }}" + loop: "{{ __regex_issue }}" +... diff --git a/roles/aap_rules_validation/tasks/check_hosts.yml b/roles/aap_rules_validation/tasks/check_hosts.yml new file mode 100644 index 00000000..70d3c983 --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_hosts.yml @@ -0,0 +1,38 @@ +--- +- name: Check for ungrouped hosts + when: rule['allow_ungrouped_hosts'] is defined + and not rule['allow_ungrouped_hosts'] + and controller_groups is defined + and controller_hosts is defined + block: + - name: Extract all and grouped hosts + ansible.builtin.set_fact: + __grouped_hosts: "{{ controller_groups | selectattr('hosts', 'defined') | map(attribute='hosts') | flatten | unique }}" + __all_hosts: "{{ controller_hosts | map(attribute='name') | flatten | unique }}" + + - name: Extract ungrouped hosts + ansible.builtin.set_fact: + __ungrouped_hosts: "{{ __all_hosts | difference(__grouped_hosts) }}" + + - name: Update violations if ungrouped hosts found + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'allow_ungrouped_hosts', + 'object_type': 'hosts', + 'object_scope': 'global', + 'object_organization': '__organizationless__', + 'object_name': '__multiple_objects__', + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | hosts | global | {{ __ungrouped_hosts_names }} | Found {{ __ungrouped_hosts | length }} ungrouped hosts" + __ungrouped_hosts_names: "{{ (__ungrouped_hosts[:3] | join(',') + '...') if (__ungrouped_hosts | length) > 3 else (__ungrouped_hosts | join(',')) }}" + when: __ungrouped_hosts is defined + and __ungrouped_hosts != None + and __ungrouped_hosts | length > 0 +... diff --git a/roles/aap_rules_validation/tasks/check_inventories.yml b/roles/aap_rules_validation/tasks/check_inventories.yml new file mode 100644 index 00000000..33523546 --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_inventories.yml @@ -0,0 +1,29 @@ +--- +- name: Check if inventory hosts count is inferior to maximum allowed + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'max_hosts_per_inventory', + 'object_type': 'inventories', + 'object_scope': 'organization', + 'object_organization': __inventory_org, + 'object_name': __inventory_name, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | inventories | {{ __inventory_org }} | {{ __inventory_name }} | Inventory has more hosts ({{ __hosts_in_inventory }}) than allowed ({{ rule['max_hosts_per_inventory'] }})" + __hosts_in_inventory: "{{ controller_hosts | selectattr('inventory', 'equalto', inventory['name']) | length }}" + __inventory_org: "{{ inventory['organization'] | default('__undefined_org__') }}" + __inventory_name: "{{ inventory['name'] | default('__undefined_name__') }}" + when: controller_hosts is defined + and controller_hosts != None + and controller_hosts | length > 0 + and rule['max_hosts_per_inventory'] is defined + and rule['max_hosts_per_inventory'] > 0 + and inventory['name'] not in (rule['exceptions']['inventories'] | default([])) + and controller_hosts | selectattr('inventory', 'equalto', inventory['name']) | length > rule['max_hosts_per_inventory'] +... diff --git a/roles/aap_rules_validation/tasks/check_mandatory_fields.yml b/roles/aap_rules_validation/tasks/check_mandatory_fields.yml new file mode 100644 index 00000000..b8b7d486 --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_mandatory_fields.yml @@ -0,0 +1,76 @@ +--- +- name: Init - {{ object_type }} + ansible.builtin.set_fact: + __defined_none: [] + __defined_empty: [] + __undefined: [] + +- name: Extract objects with mandatory defined but empty (None) fields - {{ object_type }} + ansible.builtin.set_fact: + __defined_none: "{{ __defined_none + [{'mandatory_field':mandatory_field, 'object_type': object_type}] | product(lookup('vars', __object_var_names[object_type]) | selectattr(mandatory_field, 'defined') | selectattr(mandatory_field, 'equalto', None)) }}" + loop: "{{ rule['mandatory_fields'] }}" + loop_control: + loop_var: mandatory_field + +- name: Extract objects with mandatory defined but contains empty string - {{ object_type }} + ansible.builtin.set_fact: + __defined_empty: "{{ __defined_empty + [{'mandatory_field':mandatory_field, 'object_type': object_type}] | product(lookup('vars', __object_var_names[object_type]) | selectattr(mandatory_field, 'defined') | selectattr(mandatory_field, 'equalto', '')) }}" + loop: "{{ rule['mandatory_fields'] }}" + loop_control: + loop_var: mandatory_field + +- name: Update violation msgs with mandatory defined but empty fields - {{ object_type }} + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'mandatory_fields', + 'object_type': object_type, + 'object_scope': __scope[object_type], + 'object_organization': __object_org, + 'object_name': __object_name, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ __object_type }} | {{ __object_scope }} | {{ __object_name }} | The mandatory field '{{ __field }}' is empty" + __field: "{{ item[0]['mandatory_field'] }}" + __object_type: "{{ item[0]['object_type'] }}" + __object_scope: "{{ item[1]['organization'] | default(__scope[object_type]) | default('__undefined_org__') }}" + __object_name: "{{ item[1][__name_field[object_type]] | default(item[1]['user']) | default('__undefined_name__') }}" # TO BE CHECKED FOR ALTERNATIVE 'name' fileds + __object_org: "{{ item[1]['organization'] | default('__undefined_org__') }}" + loop: "{{ __defined_empty + __defined_none }}" + +- name: Extract objects with undefined mandatory fields - {{ object_type }} + ansible.builtin.set_fact: + __undefined: "{{ __undefined + [{'mandatory_field':mandatory_field, 'object_type': object_type}] | product(lookup('vars', __object_var_names[object_type]) | selectattr(mandatory_field, 'undefined')) }}" + loop: "{{ rule['mandatory_fields'] }}" + loop_control: + loop_var: mandatory_field + +- name: Update violation msgs with mandatory undefined fields - {{ object_type }} + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'mandatory_fields', + 'object_type': __object_type, + 'object_scope': __scope[object_type], + 'object_organization': __object_org, + 'object_name': __object_name, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ __object_type }} | {{ __object_scope }} | {{ __object_name }} | The mandatory field '{{ __field }}' is not defined" + __field: "{{ item[0]['mandatory_field'] }}" + __object_type: "{{ item[0]['object_type'] }}" + __object_scope: "{{ item[1]['organization'] | default(__scope[object_type]) | default('__undefined_org__') }}" + __object_name: "{{ item[1][__name_field[object_type]] | default(item[1]['user']) | default('__undefined_name__') }}" # TO BE CHECKED FOR ALTERNATIVE 'name' fields + __object_org: "{{ item[1]['organization'] | default('__undefined_org__') }}" + loop: "{{ __undefined }}" +... diff --git a/roles/aap_rules_validation/tasks/check_objects_count.yml b/roles/aap_rules_validation/tasks/check_objects_count.yml new file mode 100644 index 00000000..1f27cfd7 --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_objects_count.yml @@ -0,0 +1,91 @@ +--- +- name: Global objects minimum count check + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'minimum_defined_globally', + 'object_type': object_type, + 'object_scope': 'global', + 'object_organization': '__organizationless__', + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ object_type }} | global | Global {{ object_type }} count ({{ lookup('vars', __object_var_names[object_type]) | length }}) is inferior to the minimum allowed ({{ rule['minimum_defined_globally'] }})" + when: rule['minimum_defined_globally'] is defined + and (lookup('vars', __object_var_names[object_type]) | unique | length < rule['minimum_defined_globally']) + +- name: Per organization objects minimum count check + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'minimum_defined_per_org', + 'object_type': object_type, + 'object_scope': 'organization', + 'object_organization': org, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ object_type }} | {{ org }} | Organization {{ org }} {{ object_type }} count ({{ __object_count }}) is inferior to the minimum allowed ({{ __org_minimum }})" + __object_count: "{{ lookup('vars', __object_var_names[object_type]) | selectattr('organization', 'defined') | selectattr('organization', 'equalto', org) | unique | length }}" + __org_minimum: "{{ rule['minimum_defined_per_org'] }}" + when: rule['organizations'] is defined + and object_type in __org_scoped_objects + and rule['minimum_defined_per_org'] is defined + and ((lookup('vars', __object_var_names[object_type]) | selectattr('organization', 'defined') | selectattr('organization', 'equalto', org) | unique | length) < rule['minimum_defined_per_org']) + loop: "{{ rule['organizations'] }}" + loop_control: + loop_var: org + +- name: Objects count maximum check + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'maximum_defined_globally', + 'object_type': object_type, + 'object_scope': 'global', + 'object_organization': '__organizationless__', + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ object_type }} | global | Global {{ object_type }} count ({{ lookup('vars', __object_var_names[object_type]) | length }}) is superior to the maximum allowed ({{ rule['maximum_defined_globally'] }})" + when: rule['maximum_defined_globally'] is defined + and lookup('vars', __object_var_names[object_type]) | length > rule['maximum_defined_globally'] + +- name: Per organization objects maximum count check + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'maximum_defined_per_org', + 'object_type': object_type, + 'object_scope': 'organization', + 'object_organization': org, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | {{ object_type }} | {{ org }} | Organization {{ org }} {{ object_type }} count ({{ __object_count }}) is superior to the maximum allowed ({{ __org_maximum }})" + __object_count: "{{ lookup('vars', __object_var_names[object_type]) | selectattr('organization', 'defined') | selectattr('organization', 'equalto', org) | unique | length }}" + __org_maximum: "{{ rule['maximum_defined_per_org'] }}" + when: rule['organizations'] is defined + and object_type in __org_scoped_objects + and rule['maximum_defined_per_org'] is defined + and ((lookup('vars', __object_var_names[object_type]) | selectattr('organization', 'defined') | selectattr('organization', 'equalto', org) | unique | length) > rule['maximum_defined_per_org']) + loop: "{{ rule['organizations'] }}" + loop_control: + loop_var: org +... diff --git a/roles/aap_rules_validation/tasks/check_organizations.yml b/roles/aap_rules_validation/tasks/check_organizations.yml new file mode 100644 index 00000000..69275e1f --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_organizations.yml @@ -0,0 +1,90 @@ +--- +- name: Check organizations max_hosts when defined + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'max_hosts_per_organization', + 'object_type': 'organizations', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': org['name'], + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | organizations | global | {{ org['name'] }} | max_hosts ({{ org['max_hosts'] }}) is superior to {{ rule['max_hosts_per_organization'] }}" + when: org['name'] not in (rule['exceptions']['organizations'] | default([])) + and org['max_hosts'] is defined + and rule['max_hosts_per_organization'] is defined + and org['max_hosts'] | int > rule['max_hosts_per_organization'] | int + +- name: Check organizations undefined or unset max_hosts + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'max_hosts_per_organization', + 'object_type': 'organizations', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': org['name'], + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | organizations | global | {{ org['name'] }} | max_hosts is not set" + when: org['name'] not in (rule['exceptions']['organizations'] | default([])) + and rule['max_hosts_per_organization'] is defined + and (org['max_hosts'] is not defined + or org['max_hosts'] == "" + or org['max_hosts'] == None) + +- name: Check if organizations default EE is in allowed list + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'max_hosts_per_organization', + 'object_type': 'organizations', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': org['name'], + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | organizations | global | {{ org['name'] }} | The EE ({{ org['default_environment'] }}) is not allowed." + when: org['name'] not in (rule['exceptions']['organizations'] | default([])) + and org['default_environment'] is defined + and rule['allowed_organization_default_environments'] is defined + and org['default_environment'] not in rule['allowed_organization_default_environments'] + +- name: Check if organizations default EE is in forbidden list + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'max_hosts_per_organization', + 'object_type': 'organizations', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': org['name'], + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | organizations | global | {{ org['name'] }} | The EE ({{ org['default_environment'] }}) is forbidden." + when: org['name'] not in (rule['exceptions']['organizations'] | default([])) + and org['default_environment'] is defined + and rule['forbidden_organization_default_environments'] is defined + and org['default_environment'] in rule['forbidden_organization_default_environments'] +... diff --git a/roles/aap_rules_validation/tasks/check_roles.yml b/roles/aap_rules_validation/tasks/check_roles.yml new file mode 100644 index 00000000..e09277ee --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_roles.yml @@ -0,0 +1,132 @@ +--- +- name: Init variables + ansible.builtin.set_fact: + __forbidden_roles: [] + __forbidden: [] + __objects_and_roles: [] + __objects_and_roles_product: [] + unallowed_roles: [] + +- name: Verify forbidden roles presence + when: rule['forbidden_roles'] is defined + and rule['forbidden_roles'] != None + and rule['forbidden_roles'] | length > 0 + block: + + - name: Set forbidden roles list + ansible.builtin.set_fact: + __forbidden_roles: "{{ __forbidden_roles + ([item.key] | product(item.value)) }}" + loop: "{{ rule['forbidden_roles'] | dict2items }}" + + - name: Extract declared forbidden roles + ansible.builtin.set_fact: + __forbidden: "{{ __forbidden + + ( + ( + ( + (controller_roles | selectattr(__role_target, 'defined')) + + (controller_roles | selectattr(__singular[__role_target], 'defined')) + ) | selectattr('role', 'defined') | selectattr('role', 'equalto', __role_verb)) + + ( + (controller_roles | selectattr(__role_target,'defined')) + + (controller_roles | selectattr(__singular[__role_target], 'defined')) + ) | selectattr('roles', 'defined') | selectattr('roles', 'contains', __role_verb) + ) | product([{'role_target_object': __role_target, 'role_verb': __role_verb}]) + }}" + vars: + __role_target: "{{ item[0] }}" + __role_verb: "{{ item[1] }}" + loop: "{{ __forbidden_roles }}" + + - name: Update violations related to forbidden roles + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'forbidden_roles', + 'object_type': 'roles', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': '__undefined_name__', + 'role_details': + { + 'users': __role_users, + 'teams': __role_teams, + 'targets': __role_target, + 'role': __role_verb + }, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | roles | global | __undefined_org__ | The role '{{ __role_verb }}' on '{{ __role_target }}' requested for '{{ __role_benefactor }}' is forbidden" + __role_users: "{{ item[0]['user'] | default('') + item[0]['users'] | default([]) | join(',') }}" + __role_teams: "{{ item[0]['team'] | default('') + item[0]['teams'] | default([]) | join(',') }}" + __role_target: "{{ item[1]['role_target_object'] }}" + __role_verb: "{{ item[1]['role_verb'] }}" + __role_benefactor: "{{ (('team(s) ' + __role_teams) if __role_teams != '' else '') + ((' and ') if __role_teams != '' and __role_users != '' else '' ) + (('user(s) ' + __role_users) if __role_users != '' else '' ) }}" + loop: "{{ __forbidden }}" + +- name: Verify non-allowed roles presence + when: rule['allowed_roles'] is defined + and rule['allowed_roles'] != None + and rule['allowed_roles'] | length > 0 + block: + + - name: Extract and format declared roles + ansible.builtin.set_fact: + __objects_and_roles: "{{ __objects_and_roles + + [{ + 'objects': item | dict2items | rejectattr('key', 'in' ,['role', 'roles', 'teams', 'team', 'teams', 'user', 'users']) | map(attribute='key'), + 'roles': [ item['role'] | default('') ] | select() + item['roles'] | default([]) | select() + }] }}" + loop: "{{ controller_roles }}" + + - name: Expand declared roles for easier comparaison + ansible.builtin.set_fact: + __objects_and_roles_product: "{{ __objects_and_roles_product + item['objects'] | product(item['roles']) }}" + loop: "{{ __objects_and_roles }}" + + - name: Compute unallowed_roles + ansible.builtin.set_fact: + unallowed_roles: "{{ unallowed_roles + [ {'object': __object, 'role': __role } ] }}" + when: ( + (__plural[__object] is defined and rule['allowed_roles'][__plural[__object]] is not defined) + or (__singular[__object] is defined and rule['allowed_roles'][__object] is not defined) + ) + or + ( + (__plural[__object] is defined and rule['allowed_roles'][__plural[__object]] is defined and __role not in rule['allowed_roles'][__plural[__object]]) + or (__singular[__object] is defined and rule['allowed_roles'][__object] is defined and __role not in rule['allowed_roles'][__object]) + ) + vars: + __object: "{{ item[0] }}" + __role: "{{ item[1] }}" + loop: "{{ __objects_and_roles_product }}" + + - name: Update violations related to non-allowed roles + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'allowed_roles', + 'object_type': 'roles', + 'object_scope': 'global', + 'object_organization': '__undefined_org__', + 'object_name': '__undefined_name__', + 'role_details': + { + 'targets': item['object'], + 'role': item['role'] + }, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | roles | global | __undefined_org__ | The role '{{ item['role'] }}' on '{{ item['object'] }}' is not allowed." + loop: "{{ unallowed_roles }}" +... diff --git a/roles/aap_rules_validation/tasks/check_users.yml b/roles/aap_rules_validation/tasks/check_users.yml new file mode 100644 index 00000000..786e66e5 --- /dev/null +++ b/roles/aap_rules_validation/tasks/check_users.yml @@ -0,0 +1,79 @@ +--- +- name: Update rules violation regarding users passwords encryption + ansible.builtin.set_fact: + rules_violations_msgs: "{{ (rules_violations_msgs + [msg]) }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'encrypt_user_passwords', + 'object_type': 'users', + 'object_scope': 'organizations', + 'object_organization': __user_org, + 'object_name': __username, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | users | {{ __user_org }} | {{ __username }} | The user's password is not encrypted" + __username: "{{ __unencrypted_user['username'] | default(__unencrypted_user['user']) }}" + __user_org: "{{ __unencrypted_user['organization'] | default('__undefined_org__') }}" + loop: "{{ controller_user_accounts | selectattr('password', 'defined') | rejectattr('password', 'vault_encrypted') }}" + when: rule['encrypt_user_passwords'] is defined + and rule['encrypt_user_passwords'] | bool + and __unencrypted_user['username'] | default(__unencrypted_user['user']) not in (rule['exceptions']['users'] | default([])) + loop_control: + loop_var: __unencrypted_user + +- name: Update rules violation regarding superusers + ansible.builtin.set_fact: + rules_violations_msgs: "{{ (rules_violations_msgs + [msg]) }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'allow_superusers', + 'object_type': 'users', + 'object_scope': 'organizations', + 'object_organization': __user_org, + 'object_name': __username, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | users | {{ __user_org }} | {{ __username }} | Superusers are not allowed" + __username: "{{ __superusers['username'] | default(__superusers['user']) }}" + __user_org: "{{ __superusers['organization'] | default('__undefined_org__') }}" + loop: "{{ controller_user_accounts | selectattr('is_superuser', 'defined') | rejectattr('is_superuser', 'false') }}" + when: rule['allow_superusers'] is defined + and not rule['allow_superusers'] | bool + and __superusers['username'] | default(__superusers['user']) not in (rule['exceptions']['users'] | default([])) + loop_control: + loop_var: __superusers + +- name: Update rules violation regarding systm auditors + ansible.builtin.set_fact: + rules_violations_msgs: "{{ (rules_violations_msgs + [msg]) }}" + rules_violations_data: "{{ rules_violations_data + + [{ + 'rule_id': rule_id, + 'rule_index': rule_index, + 'rule_broken': 'allow_system_auditors', + 'object_type': 'users', + 'object_scope': 'organizations', + 'object_organization': __user_org, + 'object_name': __username, + 'msg': msg + }] + }}" + vars: + msg: "Rule {{ rule_id }} | users | {{ __user_org }} | {{ __username }} | System Auditors are not allowed" + __username: "{{ __system_auditors['username'] | default(__system_auditors['user']) }}" + __user_org: "{{ __system_auditors['organization'] | default('__undefined_org__') }}" + when: rule['allow_system_auditors'] is defined + and not rule['allow_system_auditors'] | bool + and __system_auditors['username'] | default(__system_auditors['user']) not in (rule['exceptions']['users'] | default([])) + loop: "{{ controller_user_accounts | selectattr('is_system_auditor', 'defined') | rejectattr('is_system_auditor', 'false') }}" + loop_control: + loop_var: __system_auditors +... diff --git a/roles/aap_rules_validation/tasks/main.yml b/roles/aap_rules_validation/tasks/main.yml new file mode 100644 index 00000000..4f6ce677 --- /dev/null +++ b/roles/aap_rules_validation/tasks/main.yml @@ -0,0 +1,57 @@ +--- +- name: Look for rules violations + when: aap_rules is defined and aap_rules != None and aap_rules | length > 0 + block: + - name: Init rules violations + ansible.builtin.set_fact: + rules_violations_msgs: [] + rules_violations_data: [] + __objects_in_rules: [] + + - name: Rules check loop + ansible.builtin.include_tasks: + file: tasks/rule_check.yml + loop: "{{ aap_rules }}" + when: rule['objects'] is defined and rule['objects'] != None and rule['objects'] | length > 0 + loop_control: + loop_var: rule + index_var: rule_index + + - name: Flag discrepancy between objects in rules and in audited types as violations + when: warn_about_audited_types_not_in_rules_objects or warn_about_rules_objects_not_in_audited_types + block: + - name: Get object types declared rules + ansible.builtin.set_fact: + __objects_in_rules: "{{ aap_rules | map(attribute='objects') | flatten | unique }}" + + - name: Object types in rules that are not part of audited object types + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + vars: + __diff: "{{ __objects_in_rules | difference(audited_objects) }}" + msg: "The object type(s) '{{ __diff | join(',') }}' are mentioned in rules but are not audited." + when: warn_about_audited_types_not_in_rules_objects and __diff is defined and __diff | length > 0 + + - name: Audited object types that are not mentioned in any rule + ansible.builtin.set_fact: + rules_violations_msgs: "{{ rules_violations_msgs + [msg] }}" + vars: + __diff: "{{ audited_objects | difference(__objects_in_rules) }}" + msg: "The object types(s) '{{ __diff | join(',') }}' are audited but are not mentioned in any rule." + when: warn_about_rules_objects_not_in_audited_types and __diff is defined and __diff | length > 0 + + - name: Print rules violations data + ansible.builtin.debug: + var: rules_violations_data | unique + when: print_rules_violations_data is defined and print_rules_violations_data | bool + + - name: Print rules violations messages + ansible.builtin.debug: + var: rules_violations_msgs | unique + failed_when: fail_if_violations_found is defined and fail_if_violations_found | bool and rules_violations_msgs | length > 0 + + - name: Print success message + ansible.builtin.debug: + msg: "Success. No rules violations detected." + when: rules_violations_msgs | length == 0 +... diff --git a/roles/aap_rules_validation/tasks/rule_check.yml b/roles/aap_rules_validation/tasks/rule_check.yml new file mode 100644 index 00000000..a1902527 --- /dev/null +++ b/roles/aap_rules_validation/tasks/rule_check.yml @@ -0,0 +1,105 @@ +--- +- name: Set rule ID + ansible.builtin.set_fact: + rule_id: "{{ rule['rule_name'] | default('n°' + (rule_index + 1) | string) }}" + +- name: Objects count check + ansible.builtin.include_tasks: + file: tasks/check_objects_count.yml + when: lookup('vars', __object_var_names[object_type], default='__undefined__') != '__undefined__' and + ((rule['minimum_defined_globally'] is defined and rule['minimum_defined_globally'] > 0) + or (rule['maximum_defined_globally'] is defined and rule['maximum_defined_globally'] > 0) + or (rule['minimum_defined_per_org'] is defined and rule['minimum_defined_per_org'] > 0) + or (rule['maximum_defined_per_org'] is defined and rule['maximum_defined_per_org'] > 0)) + loop: "{{ rule['objects'] }}" + loop_control: + loop_var: object_type + +- name: Mandatory field check + ansible.builtin.include_tasks: + file: tasks/check_mandatory_fields.yml + when: rule['mandatory_fields'] is defined + and rule['mandatory_fields'] != None + and rule['mandatory_fields'] | length > 0 + and lookup('vars', __object_var_names[object_type], default='__undefined__') != '__undefined__' + loop: "{{ rule['objects'] }}" + loop_control: + loop_var: object_type + +- name: Regex check loop + ansible.builtin.include_tasks: + file: tasks/check_fields_regex.yml + when: rule['fields_regex'] is defined + and lookup('vars', __object_var_names[object_type], default='__undefined__') != '__undefined__' + loop: "{{ rule['objects'] }}" + loop_control: + loop_var: object_type + +- name: Organizations specific checks - {{ object_type }} + ansible.builtin.include_tasks: + file: tasks/check_organizations.yml + when: controller_organizations is defined + and controller_organizations | length > 0 + and 'organizations' in rule['objects'] + and (org['name'] in rule['organizations'] if rule['organizations'] is defined else true) + loop: "{{ controller_organizations }}" + loop_control: + loop_var: org + +- name: Inventory specific checks + ansible.builtin.include_tasks: + file: tasks/check_inventories.yml + when: controller_inventories is defined + and controller_inventories | length > 0 + and 'inventories' in rule['objects'] + loop: "{{ controller_inventories }}" + loop_control: + loop_var: inventory + +- name: Hosts specific checks + ansible.builtin.include_tasks: + file: tasks/check_hosts.yml + when: controller_hosts is defined + and 'hosts' in rule['objects'] + and rule['allow_ungrouped_hosts'] is defined + and not rule['allow_ungrouped_hosts'] + +- name: Credential encryption check + ansible.builtin.include_tasks: + file: tasks/check_credentials_encryption.yml + when: controller_credentials is defined + and controller_credentials | length > 0 + and 'credentials' in rule['objects'] + and rule['encrypt_credentials_sensitive_data'] is defined + and rule['encrypt_credentials_sensitive_data'] | bool + and rule['credential_sensitive_data'] is defined + and rule['credential_sensitive_data'] | length > 0 + +- name: User specific checks + ansible.builtin.include_tasks: + file: tasks/check_users.yml + when: controller_user_accounts is defined + and controller_user_accounts | length > 0 + and 'users' in rule['objects'] + and + ( + (rule['allow_superusers'] is defined and not rule['allow_superusers'] | bool ) + or + (rule['allow_system_auditors'] is defined and not rule['allow_system_auditors'] | bool ) + or + (rule['encrypt_user_passwords'] is defined and rule['encrypt_user_passwords'] | bool ) + ) + +- name: Roles specific checks + ansible.builtin.include_tasks: + file: tasks/check_roles.yml + when: controller_roles is defined + and controller_roles | length > 0 + and 'roles' in rule['objects'] + and + ( + (rule['allowed_roles'] is defined and rule['allowed_roles'] | length > 0 ) + or + (rule['forbidden_roles'] is defined and rule['forbidden_roles'] | length > 0 ) + ) +... diff --git a/tests/configs/aap_rules.yml b/tests/configs/aap_rules.yml new file mode 100644 index 00000000..e08759b8 --- /dev/null +++ b/tests/configs/aap_rules.yml @@ -0,0 +1,230 @@ +--- +aap_rules: + + # ------- Rule n°1 # Generic - Minimum and Maximum + - rule_name: Generic Stuff + organizations: + - Satellite + - Default + objects: + - credentials + - groups + - inventories + - inventory_sources + - job_templates + - projects + - teams + - users + - credential_types + - instances + minimum_defined_globally: 3 + maximum_defined_globally: 5 + minimum_defined_per_org: 2 + maximum_defined_per_org: 4 + + # ------- Rule n°2 # Generic - Regex + - organizations: + - Default + objects: + - projects + - credentials + fields_regex: + name: '^\[Default\].*' # name must start with '[Default]' + description: "^DESC - .*" # description must start with 'DESC - ' + scm_type: ^git$ # accept only git scm + scm_branch: ^main$ # branch naming rule : accept only main branch + + # ------- Rule n°3 # Generic - Mandatory fields + - objects: + - credentials + - groups + - inventories + - inventory_sources + - job_templates + - projects + - teams + - users + - credential_types + - instances + mandatory_fields: + - description + + # ------- Rule n°4 # Organizations - Allow only the following EEs + - objects: + - organizations + organizations: + - Satellite + - Default + max_hosts_per_organization: 100 + allowed_organization_default_environments: + - Automation Hub Default Execution Environment + - Custom EE + + # ------- Rule n°5 # Organizations - Allow all EEs except the listed forbidden EEs for all organizations except Satellite + - objects: + - organizations + max_hosts_per_organization: 10 + forbidden_organization_default_environments: + - Automation Hub Default Execution Environment + exceptions: + organizations: + - Satellite + + # ------- Rule n°6 # inventories + - organizations: + - Satellite + - Default + objects: + - inventories + max_hosts_per_inventory: 1 # needs 'controller_hosts' and 'controller_inventories' to be defined + exceptions: + inventories: + - localhost + + # ------- Rule n°7 # hosts + - objects: + - hosts + allow_ungrouped_hosts: false # needs 'controller_groups' and 'controller_hosts' to be defined + + # ------- Rule n°8 # Credentials + - rule_name: Credentials Check # Optional arbitrary string + encrypt_credentials_sensitive_data: true # DO NOT WORK with intermediary variables (filetree_read) + organizations: + - Default + - Satellite + objects: + - credentials + credential_sensitive_data: + Source Control: + - password + Red Hat Virtualization: + - password + Vault: + - vault_password + + # ------- Rule n°9 # Users + - objects: + - users + allow_superusers: false + allow_system_auditors: false + encrypt_user_passwords: true + exceptions: + users: + - controller_admin + - admin + + # ------- Rule n°10 # Roles - Allow ONLY 'read' on projects, 'member' on organizations and 'admin' on teams. Any other role is forbidden. + - objects: + - roles + allowed_roles: + projects: + - update + - read + organizations: + - member + target_teams: + - member + + # ------- Rule n°11 # Roles - Do not allow 'admin' on projects, 'admin' on organizations and 'admin' on teams, allow everything else + - objects: + - roles + forbidden_roles: + projects: + - admin + organizations: + - admin + target_teams: + - admin + +# # -------------------------------------------- All Possible Options : +# - rule_name: Custom rule name # Optional arbitrary string +# organizations: +# - Satellite # Audited organization +# - Default +# objects: +# # org scope: +# - applications +# - credentials +# - inventories +# - inventory_sources +# - job_templates +# - notification_templates +# - projects +# - teams +# - users +# - workflow_job_templates +# # global scope: +# - credential_input_sources +# - credential_types +# - execution_environments +# - groups +# - hosts +# - instance_groups +# - instances +# - labels +# - organizations +# - roles +# - schedules +# - settings + +# # ------ generic rules +# minimum_defined: 1 # at least 1 organization is defined +# maximum_defined: 2 # no more than 2 organization +# mandatory_fields: +# - description # force use of description & max_hosts field +# - max_hosts +# fields_regex: +# name: '\[Default\].*' # names should start with '[Default]' +# description: "^DESC - .*" # description should start with 'DESC - ' +# scm_type: ^git$ # accept only git scm +# scm_branch: ^main$ # accept only main branch + +# # ------ organization specific rules +# max_hosts_per_organization: 100 +# allowed_organization_default_environments: +# - Automation Hub Minimal Execution Environment +# forbidden_organization_default_environments: +# - Automation Hub Default Execution Environment +# - Custom EE + +# # ------ inventory specific rules +# max_hosts_per_inventory: 1 # needs both 'controller_inventories' and 'controller_hosts' to be defined + +# # ------ hosts specific rules +# allow_ungrouped_hosts: false # needs both 'controller_groups' and 'controller_hosts' to be defined + +# # ------ Credentials specific rules +# encrypt_credentials_sensitive_data: true # DO NOT WORK with intermediary variables (filetree_read) +# credential_sensitive_data: +# Source Control: +# - password + +# # ------- Users specific rules +# allow_superusers: false +# allow_system_auditors: false +# encrypt_user_passwords: true # DO NOT WORK with intermediary variables (filetree_read) +# exceptions: +# users: +# - admin + +# # ------- Roles specific rules +# allowed_roles: +# projects: +# - read +# - update +# organizations: +# - member +# target_teams: +# - admin +# - member +# +# forbidden_roles: +# projects: +# - update +# - admin +# organizations: +# - admin +# target_teams: +# - admin + +... diff --git a/tests/configs/organizations.yml b/tests/configs/organizations.yml index c0380a93..75f8c088 100644 --- a/tests/configs/organizations.yml +++ b/tests/configs/organizations.yml @@ -2,6 +2,7 @@ controller_organizations: - name: Satellite - name: Default + default_environment: Automation Hub Default Execution Environment - name: Test-dispatch-dependencies galaxy_credentials: - galaxy-server diff --git a/tests/configs/user_accounts.yml b/tests/configs/user_accounts.yml index f73dad90..7ee16217 100644 --- a/tests/configs/user_accounts.yml +++ b/tests/configs/user_accounts.yml @@ -3,4 +3,17 @@ controller_user_accounts: - user: controller_user is_superuser: false password: controller_password + - user: second_controller_user + is_superuser: false + organization: Default + password: controller_password + - user: controller_auditor + is_system_auditor: true + password: controller_password + - user: controller_admin + is_superuser: true + password: controller_password + - user: second_controller_admin + is_superuser: true + password: controller_password ... diff --git a/tests/playbooks/controller_rules_validation_filetree_read.yml b/tests/playbooks/controller_rules_validation_filetree_read.yml new file mode 100644 index 00000000..38cc67aa --- /dev/null +++ b/tests/playbooks/controller_rules_validation_filetree_read.yml @@ -0,0 +1,7 @@ +--- +- name: Check for controller rules violations with configuration read using filetree_read + hosts: localhost + gather_facts: false + roles: + - role: infra.aap_controller_configuration_extended.filetree_read + - role: infra.aap_controller_configuration_extended.aap_rules_validation diff --git a/tests/playbooks/controller_rules_validation_include.yml b/tests/playbooks/controller_rules_validation_include.yml new file mode 100644 index 00000000..cbc899c0 --- /dev/null +++ b/tests/playbooks/controller_rules_validation_include.yml @@ -0,0 +1,11 @@ +--- +- name: Check for controller rules violations with configuration read using include_vars + hosts: localhost + gather_facts: false + pre_tasks: + - name: Read configuration + ansible.builtin.include_vars: + dir: ../configs/ + roles: + - role: infra.aap_controller_configuration_extended.aap_rules_validation +...