diff --git a/README.md b/README.md index 7b29ae65c8..cb40861994 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe | Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | |---|---|---|---|---| | AWS | 564 | 82 | 33 | 10 | -| GCP | 77 | 13 | 5 | 3 | +| GCP | 77 | 13 | 6 | 3 | | Azure | 140 | 18 | 7 | 3 | -| Kubernetes | 83 | 7 | 2 | 7 | +| Kubernetes | 83 | 7 | 4 | 7 | | Microsoft365 | 5 | 2 | 1 | 0 | > You can list the checks, services, compliance frameworks and categories with `prowler --list-checks`, `prowler --list-services`, `prowler --list-compliance` and `prowler --list-categories`. diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index e252c4c376..741f740c5e 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -5,8 +5,8 @@ All notable changes to the **Prowler API** are documented in this file. --- ## [v1.6.0] (Prowler UNRELEASED) - ### Added +- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167). - Support for read only replicas in the database [(#7210)](https://github.com/prowler-cloud/prowler/pull/7210). --- @@ -29,6 +29,6 @@ All notable changes to the **Prowler API** are documented in this file. - Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700). - Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800). - Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863). -- Increase the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869). +- Increased the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869). --- diff --git a/api/src/backend/api/db_utils.py b/api/src/backend/api/db_utils.py index 1bcf14209c..a7dfee863f 100644 --- a/api/src/backend/api/db_utils.py +++ b/api/src/backend/api/db_utils.py @@ -318,3 +318,15 @@ class InvitationStateEnum(EnumType): class InvitationStateEnumField(PostgresEnumField): def __init__(self, *args, **kwargs): super().__init__("invitation_state", *args, **kwargs) + + +# Postgres enum definition for Integration type + + +class IntegrationTypeEnum(EnumType): + enum_type_name = "integration_type" + + +class IntegrationTypeEnumField(PostgresEnumField): + def __init__(self, *args, **kwargs): + super().__init__("integration_type", *args, **kwargs) diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 55bf313b9d..dbd73b41db 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -24,6 +24,7 @@ from api.db_utils import ( from api.models import ( ComplianceOverview, Finding, + Integration, Invitation, Membership, PermissionChoices, @@ -648,3 +649,19 @@ class ServiceOverviewFilter(ScanSummaryFilter): } ) return super().is_valid() + + +class IntegrationFilter(FilterSet): + inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date") + integration_type = ChoiceFilter(choices=Integration.IntegrationChoices.choices) + integration_type__in = ChoiceInFilter( + choices=Integration.IntegrationChoices.choices, + field_name="integration_type", + lookup_expr="in", + ) + + class Meta: + model = Integration + fields = { + "inserted_at": ["date", "gte", "lte"], + } diff --git a/api/src/backend/api/migrations/0013_integrations_enum.py b/api/src/backend/api/migrations/0013_integrations_enum.py new file mode 100644 index 0000000000..524ecbbb3d --- /dev/null +++ b/api/src/backend/api/migrations/0013_integrations_enum.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.5 on 2025-03-03 15:46 + +from functools import partial + +from django.db import migrations + +from api.db_utils import IntegrationTypeEnum, PostgresEnumMigration, register_enum +from api.models import Integration + +IntegrationTypeEnumMigration = PostgresEnumMigration( + enum_name="integration_type", + enum_values=tuple( + integration_type[0] + for integration_type in Integration.IntegrationChoices.choices + ), +) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0012_scan_report_output"), + ] + + operations = [ + migrations.RunPython( + IntegrationTypeEnumMigration.create_enum_type, + reverse_code=IntegrationTypeEnumMigration.drop_enum_type, + ), + migrations.RunPython( + partial(register_enum, enum_class=IntegrationTypeEnum), + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/api/src/backend/api/migrations/0014_integrations.py b/api/src/backend/api/migrations/0014_integrations.py new file mode 100644 index 0000000000..2fb3d76880 --- /dev/null +++ b/api/src/backend/api/migrations/0014_integrations.py @@ -0,0 +1,131 @@ +# Generated by Django 5.1.5 on 2025-03-03 15:46 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + +import api.db_utils +import api.rls +from api.rls import RowLevelSecurityConstraint + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0013_integrations_enum"), + ] + + operations = [ + migrations.CreateModel( + name="Integration", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("enabled", models.BooleanField(default=False)), + ("connected", models.BooleanField(blank=True, null=True)), + ( + "connection_last_checked_at", + models.DateTimeField(blank=True, null=True), + ), + ( + "integration_type", + api.db_utils.IntegrationTypeEnumField( + choices=[ + ("amazon_s3", "Amazon S3"), + ("saml", "SAML"), + ("aws_security_hub", "AWS Security Hub"), + ("jira", "JIRA"), + ("slack", "Slack"), + ] + ), + ), + ("configuration", models.JSONField(default=dict)), + ("_credentials", models.BinaryField(db_column="credentials")), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={"db_table": "integrations", "abstract": False}, + ), + migrations.AddConstraint( + model_name="integration", + constraint=RowLevelSecurityConstraint( + "tenant_id", + name="rls_on_integration", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ), + migrations.CreateModel( + name="IntegrationProviderRelationship", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ( + "integration", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="api.integration", + ), + ), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.provider" + ), + ), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={ + "db_table": "integration_provider_mappings", + "constraints": [ + models.UniqueConstraint( + fields=("integration_id", "provider_id"), + name="unique_integration_provider_rel", + ), + ], + }, + ), + migrations.AddConstraint( + model_name="IntegrationProviderRelationship", + constraint=RowLevelSecurityConstraint( + "tenant_id", + name="rls_on_integrationproviderrelationship", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ), + migrations.AddField( + model_name="integration", + name="providers", + field=models.ManyToManyField( + blank=True, + related_name="integrations", + through="api.IntegrationProviderRelationship", + to="api.provider", + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index b407603814..0ac9f912a0 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -21,6 +21,7 @@ from uuid6 import uuid7 from api.db_utils import ( CustomUserManager, FindingDeltaEnumField, + IntegrationTypeEnumField, InvitationStateEnumField, MemberRoleEnumField, ProviderEnumField, @@ -1138,3 +1139,80 @@ class ScanSummary(RowLevelSecurityProtectedModel): class JSONAPIMeta: resource_name = "scan-summaries" + + +class Integration(RowLevelSecurityProtectedModel): + class IntegrationChoices(models.TextChoices): + S3 = "amazon_s3", _("Amazon S3") + SAML = "saml", _("SAML") + AWS_SECURITY_HUB = "aws_security_hub", _("AWS Security Hub") + JIRA = "jira", _("JIRA") + SLACK = "slack", _("Slack") + + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + inserted_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + enabled = models.BooleanField(default=False) + connected = models.BooleanField(null=True, blank=True) + connection_last_checked_at = models.DateTimeField(null=True, blank=True) + integration_type = IntegrationTypeEnumField(choices=IntegrationChoices.choices) + configuration = models.JSONField(default=dict) + _credentials = models.BinaryField(db_column="credentials") + + providers = models.ManyToManyField( + Provider, + related_name="integrations", + through="IntegrationProviderRelationship", + blank=True, + ) + + class Meta(RowLevelSecurityProtectedModel.Meta): + db_table = "integrations" + + constraints = [ + RowLevelSecurityConstraint( + field="tenant_id", + name="rls_on_%(class)s", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ] + + class JSONAPIMeta: + resource_name = "integrations" + + @property + def credentials(self): + if isinstance(self._credentials, memoryview): + encrypted_bytes = self._credentials.tobytes() + elif isinstance(self._credentials, str): + encrypted_bytes = self._credentials.encode() + else: + encrypted_bytes = self._credentials + decrypted_data = fernet.decrypt(encrypted_bytes) + return json.loads(decrypted_data.decode()) + + @credentials.setter + def credentials(self, value): + encrypted_data = fernet.encrypt(json.dumps(value).encode()) + self._credentials = encrypted_data + + +class IntegrationProviderRelationship(RowLevelSecurityProtectedModel): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + integration = models.ForeignKey(Integration, on_delete=models.CASCADE) + provider = models.ForeignKey(Provider, on_delete=models.CASCADE) + inserted_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "integration_provider_mappings" + constraints = [ + models.UniqueConstraint( + fields=["integration_id", "provider_id"], + name="unique_integration_provider_rel", + ), + RowLevelSecurityConstraint( + field="tenant_id", + name="rls_on_%(class)s", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ] diff --git a/api/src/backend/api/rls.py b/api/src/backend/api/rls.py index 59845b53bf..285b06a974 100644 --- a/api/src/backend/api/rls.py +++ b/api/src/backend/api/rls.py @@ -58,11 +58,11 @@ class RowLevelSecurityConstraint(models.BaseConstraint): drop_sql_query = """ ALTER TABLE %(table_name)s NO FORCE ROW LEVEL SECURITY; ALTER TABLE %(table_name)s DISABLE ROW LEVEL SECURITY; - REVOKE ALL ON TABLE %(table_name) TO %(db_user)s; + REVOKE ALL ON TABLE %(table_name)s FROM %(db_user)s; """ drop_policy_sql_query = """ - DROP POLICY IF EXISTS %(db_user)s_%(table_name)s_{statement} on %(table_name)s; + DROP POLICY IF EXISTS %(db_user)s_%(raw_table_name)s_{statement} ON %(table_name)s; """ def __init__( @@ -104,16 +104,20 @@ class RowLevelSecurityConstraint(models.BaseConstraint): def remove_sql(self, model: Any, schema_editor: Any) -> Any: field_column = schema_editor.quote_name(self.target_field) + raw_table_name = model._meta.db_table + table_name = raw_table_name + if self.partition_name: + raw_table_name = f"{raw_table_name}_{self.partition_name}" + table_name = raw_table_name + full_drop_sql_query = ( f"{self.drop_sql_query}" - f"{''.join([self.drop_policy_sql_query.format(statement) for statement in self.statements])}" + f"{''.join([self.drop_policy_sql_query.format(statement=statement) for statement in self.statements])}" ) - table_name = model._meta.db_table - if self.partition_name: - table_name = f"{table_name}_{self.partition_name}" return Statement( full_drop_sql_query, table_name=Table(table_name, schema_editor.quote_name), + raw_table_name=raw_table_name, field_column=field_column, db_user=DB_USER, partition_name=self.partition_name, diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index c2fdccdedf..99f2ce68b2 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.5.0 + version: 1.6.0 description: |- Prowler API specification. @@ -1608,6 +1608,298 @@ paths: schema: $ref: '#/components/schemas/FindingMetadataResponse' description: '' + /api/v1/integrations: + get: + operationId: integrations_list + description: Retrieve a list of all configured integrations with options for + filtering by various criteria. + summary: List all integrations + parameters: + - in: query + name: fields[integrations] + schema: + type: array + items: + type: string + enum: + - inserted_at + - updated_at + - enabled + - connected + - connection_last_checked_at + - integration_type + - configuration + - providers + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: query + name: filter[inserted_at] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__date] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__gte] + schema: + type: string + format: date-time + - in: query + name: filter[inserted_at__lte] + schema: + type: string + format: date-time + - in: query + name: filter[integration_type] + schema: + type: string + enum: + - amazon_s3 + - aws_security_hub + - jira + - saml + - slack + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + - in: query + name: filter[integration_type__in] + schema: + type: array + items: + type: string + enum: + - amazon_s3 + - aws_security_hub + - jira + - saml + - slack + description: |- + Multiple values may be separated by commas. + + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + explode: false + style: form + - name: filter[search] + required: false + in: query + description: A search term. + schema: + type: string + - in: query + name: include + schema: + type: array + items: + type: string + enum: + - providers + description: include query parameter to allow the client to customize which + related resources should be returned. + explode: false + - name: page[number] + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + - name: page[size] + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: sort + required: false + in: query + description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)' + schema: + type: array + items: + type: string + enum: + - id + - -id + - inserted_at + - -inserted_at + - updated_at + - -updated_at + - enabled + - -enabled + - connected + - -connected + - connection_last_checked_at + - -connection_last_checked_at + - integration_type + - -integration_type + - configuration + - -configuration + - providers + - -providers + - url + - -url + explode: false + tags: + - Integration + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PaginatedIntegrationList' + description: '' + post: + operationId: integrations_create + description: Register a new integration with the system, providing necessary + configuration details. + summary: Create a new integration + tags: + - Integration + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/IntegrationCreateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/IntegrationCreateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/IntegrationCreateRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/IntegrationCreateResponse' + description: '' + /api/v1/integrations/{id}: + get: + operationId: integrations_retrieve + description: Fetch detailed information about a specific integration by its + ID. + summary: Retrieve integration details + parameters: + - in: query + name: fields[integrations] + schema: + type: array + items: + type: string + enum: + - inserted_at + - updated_at + - enabled + - connected + - connection_last_checked_at + - integration_type + - configuration + - providers + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this integration. + required: true + - in: query + name: include + schema: + type: array + items: + type: string + enum: + - providers + description: include query parameter to allow the client to customize which + related resources should be returned. + explode: false + tags: + - Integration + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/IntegrationResponse' + description: '' + patch: + operationId: integrations_partial_update + description: Modify certain fields of an existing integration without affecting + other settings. + summary: Partially update an integration + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this integration. + required: true + tags: + - Integration + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/PatchedIntegrationUpdateRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedIntegrationUpdateRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedIntegrationUpdateRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/IntegrationUpdateResponse' + description: '' + delete: + operationId: integrations_destroy + description: Remove an integration from the system by its ID. + summary: Delete an integration + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this integration. + required: true + tags: + - Integration + security: + - jwtAuth: [] + responses: + '204': + description: No response body /api/v1/invitations/accept: post: operationId: invitations_accept_create @@ -6049,6 +6341,536 @@ components: type: string enum: - findings + Integration: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/TypeC82Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: + type: string + format: uuid + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + enabled: + type: boolean + connected: + type: boolean + nullable: true + connection_last_checked_at: + type: string + format: date-time + nullable: true + integration_type: + enum: + - amazon_s3 + - saml + - aws_security_hub + - jira + - slack + type: string + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + configuration: {} + required: + - integration_type + relationships: + type: object + properties: + providers: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + title: Resource Identifier + description: The identifier of the related object. + type: + type: string + enum: + - providers + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common + attributes and relationships. + required: + - id + - type + required: + - data + description: A related resource object from type providers + title: providers + required: + - providers + IntegrationCreate: + type: object + required: + - type + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/TypeC82Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + enabled: + type: boolean + readOnly: true + connected: + type: boolean + readOnly: true + nullable: true + connection_last_checked_at: + type: string + format: date-time + readOnly: true + nullable: true + integration_type: + enum: + - amazon_s3 + - saml + - aws_security_hub + - jira + - slack + type: string + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + configuration: + oneOf: + - type: object + title: Amazon S3 + properties: + bucket_name: + type: string + description: The name of the S3 bucket where files will be stored. + output_directory: + type: string + description: The directory path within the bucket where files + will be saved. + required: + - bucket_name + - output_directory + credentials: + oneOf: + - type: object + title: AWS Credentials + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to assume. + Required for AWS role assumption. + external_id: + type: string + description: An identifier to enhance security for role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + role_session_name: + type: string + description: |- + An identifier for the role session, useful for tracking sessions in AWS logs. The regex used to validate this parameter is a string of characters consisting of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@- + + Examples: + - MySession123 + - User_Session-1 + - Test.Session@2 + pattern: ^[a-zA-Z0-9=,.@_-]+$ + writeOnly: true + required: + - integration_type + - configuration + - credentials + relationships: + type: object + properties: + providers: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + title: Resource Identifier + description: The identifier of the related object. + type: + type: string + enum: + - providers + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common + attributes and relationships. + required: + - id + - type + required: + - data + description: A related resource object from type providers + title: providers + IntegrationCreateRequest: + type: object + properties: + data: + type: object + required: + - type + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - integrations + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + enabled: + type: boolean + readOnly: true + connected: + type: boolean + readOnly: true + nullable: true + connection_last_checked_at: + type: string + format: date-time + readOnly: true + nullable: true + integration_type: + enum: + - amazon_s3 + - saml + - aws_security_hub + - jira + - slack + type: string + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + configuration: + oneOf: + - type: object + title: Amazon S3 + properties: + bucket_name: + type: string + description: The name of the S3 bucket where files will be + stored. + output_directory: + type: string + description: The directory path within the bucket where files + will be saved. + required: + - bucket_name + - output_directory + credentials: + oneOf: + - type: object + title: AWS Credentials + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to + assume. Required for AWS role assumption. + external_id: + type: string + description: An identifier to enhance security for role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, + if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + role_session_name: + type: string + description: |- + An identifier for the role session, useful for tracking sessions in AWS logs. The regex used to validate this parameter is a string of characters consisting of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@- + + Examples: + - MySession123 + - User_Session-1 + - Test.Session@2 + pattern: ^[a-zA-Z0-9=,.@_-]+$ + writeOnly: true + required: + - integration_type + - configuration + - credentials + relationships: + type: object + properties: + providers: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + title: Resource Identifier + description: The identifier of the related object. + type: + type: string + enum: + - providers + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share + common attributes and relationships. + required: + - id + - type + required: + - data + description: A related resource object from type providers + title: providers + required: + - data + IntegrationCreateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/IntegrationCreate' + required: + - data + IntegrationResponse: + type: object + properties: + data: + $ref: '#/components/schemas/Integration' + required: + - data + IntegrationUpdate: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/TypeC82Enum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: {} + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + enabled: + type: boolean + connected: + type: boolean + readOnly: true + nullable: true + connection_last_checked_at: + type: string + format: date-time + readOnly: true + nullable: true + integration_type: + enum: + - amazon_s3 + - saml + - aws_security_hub + - jira + - slack + type: string + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + readOnly: true + configuration: + oneOf: + - type: object + title: Amazon S3 + properties: + bucket_name: + type: string + description: The name of the S3 bucket where files will be stored. + output_directory: + type: string + description: The directory path within the bucket where files + will be saved. + required: + - bucket_name + - output_directory + credentials: + oneOf: + - type: object + title: AWS Credentials + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to assume. + Required for AWS role assumption. + external_id: + type: string + description: An identifier to enhance security for role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + role_session_name: + type: string + description: |- + An identifier for the role session, useful for tracking sessions in AWS logs. The regex used to validate this parameter is a string of characters consisting of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@- + + Examples: + - MySession123 + - User_Session-1 + - Test.Session@2 + pattern: ^[a-zA-Z0-9=,.@_-]+$ + writeOnly: true + relationships: + type: object + properties: + providers: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + title: Resource Identifier + description: The identifier of the related object. + type: + type: string + enum: + - providers + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common + attributes and relationships. + required: + - id + - type + required: + - data + description: A related resource object from type providers + title: providers + IntegrationUpdateResponse: + type: object + properties: + data: + $ref: '#/components/schemas/IntegrationUpdate' + required: + - data Invitation: type: object required: @@ -6833,6 +7655,15 @@ components: $ref: '#/components/schemas/Finding' required: - data + PaginatedIntegrationList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Integration' + required: + - data PaginatedInvitationList: type: object properties: @@ -6932,6 +7763,151 @@ components: $ref: '#/components/schemas/User' required: - data + PatchedIntegrationUpdateRequest: + type: object + properties: + data: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - integrations + id: {} + attributes: + type: object + properties: + inserted_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + enabled: + type: boolean + connected: + type: boolean + readOnly: true + nullable: true + connection_last_checked_at: + type: string + format: date-time + readOnly: true + nullable: true + integration_type: + enum: + - amazon_s3 + - saml + - aws_security_hub + - jira + - slack + type: string + description: |- + * `amazon_s3` - Amazon S3 + * `saml` - SAML + * `aws_security_hub` - AWS Security Hub + * `jira` - JIRA + * `slack` - Slack + readOnly: true + configuration: + oneOf: + - type: object + title: Amazon S3 + properties: + bucket_name: + type: string + description: The name of the S3 bucket where files will be + stored. + output_directory: + type: string + description: The directory path within the bucket where files + will be saved. + required: + - bucket_name + - output_directory + credentials: + oneOf: + - type: object + title: AWS Credentials + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to + assume. Required for AWS role assumption. + external_id: + type: string + description: An identifier to enhance security for role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, + if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + role_session_name: + type: string + description: |- + An identifier for the role session, useful for tracking sessions in AWS logs. The regex used to validate this parameter is a string of characters consisting of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@- + + Examples: + - MySession123 + - User_Session-1 + - Test.Session@2 + pattern: ^[a-zA-Z0-9=,.@_-]+$ + writeOnly: true + relationships: + type: object + properties: + providers: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + format: uuid + title: Resource Identifier + description: The identifier of the related object. + type: + type: string + enum: + - providers + title: Resource Type Name + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share + common attributes and relationships. + required: + - id + - type + required: + - data + description: A related resource object from type providers + title: providers + required: + - data PatchedInvitationUpdateRequest: type: object properties: @@ -10216,6 +11192,10 @@ components: type: string enum: - users + TypeC82Enum: + type: string + enum: + - integrations TypeD4dEnum: type: string enum: @@ -10521,3 +11501,7 @@ tags: - name: Task description: Endpoints for task management, allowing retrieval of task status and revoking tasks that have not started. +- name: Integration + description: Endpoints for managing third-party integrations, including registration, + configuration, retrieval, and deletion of integrations such as S3, JIRA, or other + services. diff --git a/api/src/backend/api/tests/test_rbac.py b/api/src/backend/api/tests/test_rbac.py index fd499a0398..7365bae3d4 100644 --- a/api/src/backend/api/tests/test_rbac.py +++ b/api/src/backend/api/tests/test_rbac.py @@ -1,7 +1,19 @@ +from unittest.mock import ANY, Mock, patch + import pytest from django.urls import reverse from rest_framework import status -from unittest.mock import patch, ANY, Mock + +from api.models import ( + Membership, + ProviderGroup, + ProviderGroupMembership, + Role, + RoleProviderGroupRelationship, + User, + UserRoleRelationship, +) +from api.v1.serializers import TokenSerializer @pytest.mark.django_db @@ -304,3 +316,96 @@ class TestProviderViewSet: reverse("provider-connection", kwargs={"pk": provider.id}) ) assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +class TestLimitedVisibility: + TEST_EMAIL = "rbac@rbac.com" + TEST_PASSWORD = "thisisapassword123" + + @pytest.fixture + def limited_admin_user( + self, django_db_setup, django_db_blocker, tenants_fixture, providers_fixture + ): + with django_db_blocker.unblock(): + tenant = tenants_fixture[0] + provider = providers_fixture[0] + user = User.objects.create_user( + name="testing", + email=self.TEST_EMAIL, + password=self.TEST_PASSWORD, + ) + Membership.objects.create( + user=user, + tenant=tenant, + role=Membership.RoleChoices.OWNER, + ) + + role = Role.objects.create( + name="limited_visibility", + tenant=tenant, + manage_users=True, + manage_account=True, + manage_billing=True, + manage_providers=True, + manage_integrations=True, + manage_scans=True, + unlimited_visibility=False, + ) + UserRoleRelationship.objects.create( + user=user, + role=role, + tenant=tenant, + ) + + provider_group = ProviderGroup.objects.create( + name="limited_visibility_group", + tenant=tenant, + ) + ProviderGroupMembership.objects.create( + tenant=tenant, + provider=provider, + provider_group=provider_group, + ) + + RoleProviderGroupRelationship.objects.create( + tenant=tenant, role=role, provider_group=provider_group + ) + + return user + + @pytest.fixture + def authenticated_client_rbac_limited( + self, limited_admin_user, tenants_fixture, client + ): + client.user = limited_admin_user + tenant_id = tenants_fixture[0].id + serializer = TokenSerializer( + data={ + "type": "tokens", + "email": self.TEST_EMAIL, + "password": self.TEST_PASSWORD, + "tenant_id": tenant_id, + } + ) + serializer.is_valid(raise_exception=True) + access_token = serializer.validated_data["access"] + client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" + return client + + def test_integrations( + self, authenticated_client_rbac_limited, integrations_fixture, providers_fixture + ): + # Integration 2 is related to provider1 and provider 2 + # This user cannot see provider 2 + integration = integrations_fixture[1] + + response = authenticated_client_rbac_limited.get( + reverse("integration-detail", kwargs={"pk": integration.id}) + ) + + assert response.status_code == status.HTTP_200_OK + assert integration.providers.count() == 2 + assert ( + response.json()["data"]["relationships"]["providers"]["meta"]["count"] == 1 + ) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index c9a612366c..597561b310 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -14,6 +14,7 @@ from django.urls import reverse from rest_framework import status from api.models import ( + Integration, Invitation, Membership, Provider, @@ -4571,3 +4572,415 @@ class TestScheduleViewSet: reverse("schedule-daily"), data=json_payload, format="json" ) assert response.status_code == status.HTTP_404_NOT_FOUND + + +@pytest.mark.django_db +class TestIntegrationViewSet: + def test_integrations_list(self, authenticated_client, integrations_fixture): + response = authenticated_client.get(reverse("integration-list")) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == len(integrations_fixture) + + def test_integrations_retrieve(self, authenticated_client, integrations_fixture): + integration1, *_ = integrations_fixture + response = authenticated_client.get( + reverse("integration-detail", kwargs={"pk": integration1.id}), + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["data"]["id"] == str(integration1.id) + assert ( + response.json()["data"]["attributes"]["configuration"] + == integration1.configuration + ) + + def test_integrations_invalid_retrieve(self, authenticated_client): + response = authenticated_client.get( + reverse( + "integration-detail", + kwargs={"pk": "f498b103-c760-4785-9a3e-e23fafbb7b02"}, + ) + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.parametrize( + "include_values, expected_resources", + [ + ("providers", ["providers"]), + ], + ) + def test_integrations_list_include( + self, + include_values, + expected_resources, + authenticated_client, + integrations_fixture, + ): + response = authenticated_client.get( + reverse("integration-list"), {"include": include_values} + ) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == len(integrations_fixture) + assert "included" in response.json() + + included_data = response.json()["included"] + for expected_type in expected_resources: + assert any( + d.get("type") == expected_type for d in included_data + ), f"Expected type '{expected_type}' not found in included data" + + @pytest.mark.parametrize( + "integration_type, configuration, credentials", + [ + # Amazon S3 - AWS credentials + ( + Integration.IntegrationChoices.S3, + { + "bucket_name": "bucket-name", + "output_directory": "output-directory", + }, + { + "role_arn": "arn:aws", + "external_id": "external-id", + }, + ), + # Amazon S3 - No credentials (AWS self-hosted) + ( + Integration.IntegrationChoices.S3, + { + "bucket_name": "bucket-name", + "output_directory": "output-directory", + }, + {}, + ), + ], + ) + def test_integrations_create_valid( + self, + authenticated_client, + providers_fixture, + integration_type, + configuration, + credentials, + ): + provider = Provider.objects.first() + + data = { + "data": { + "type": "integrations", + "attributes": { + "integration_type": integration_type, + "configuration": configuration, + "credentials": credentials, + }, + "relationships": { + "providers": { + "data": [{"type": "providers", "id": str(provider.id)}] + } + }, + } + } + response = authenticated_client.post( + reverse("integration-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_201_CREATED + assert Integration.objects.count() == 1 + integration = Integration.objects.first() + assert integration.configuration == data["data"]["attributes"]["configuration"] + assert ( + integration.integration_type + == data["data"]["attributes"]["integration_type"] + ) + assert "credentials" not in response.json()["data"]["attributes"] + assert ( + str(provider.id) + == data["data"]["relationships"]["providers"]["data"][0]["id"] + ) + + def test_integrations_create_valid_relationships( + self, + authenticated_client, + providers_fixture, + ): + provider1, provider2, *_ = providers_fixture + + data = { + "data": { + "type": "integrations", + "attributes": { + "integration_type": Integration.IntegrationChoices.S3, + "configuration": { + "bucket_name": "bucket-name", + "output_directory": "output-directory", + }, + "credentials": { + "role_arn": "arn:aws", + "external_id": "external-id", + }, + }, + "relationships": { + "providers": { + "data": [ + {"type": "providers", "id": str(provider1.id)}, + {"type": "providers", "id": str(provider2.id)}, + ] + } + }, + } + } + response = authenticated_client.post( + reverse("integration-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_201_CREATED + assert Integration.objects.first().providers.count() == 2 + + @pytest.mark.parametrize( + "attributes, error_code, error_pointer", + ( + [ + ( + { + "integration_type": "whatever", + "configuration": { + "bucket_name": "bucket-name", + "output_directory": "output-directory", + }, + "credentials": { + "role_arn": "arn:aws", + "external_id": "external-id", + }, + }, + "invalid_choice", + "integration_type", + ), + ( + { + "integration_type": "amazon_s3", + "configuration": {}, + "credentials": { + "role_arn": "arn:aws", + "external_id": "external-id", + }, + }, + "required", + "bucket_name", + ), + ( + { + "integration_type": "amazon_s3", + "configuration": { + "bucket_name": "bucket_name", + "output_directory": "output_directory", + "invalid_key": "invalid_value", + }, + "credentials": { + "role_arn": "arn:aws", + "external_id": "external-id", + }, + }, + "invalid", + None, + ), + ( + { + "integration_type": "amazon_s3", + "configuration": { + "bucket_name": "bucket_name", + "output_directory": "output_directory", + }, + "credentials": {"invalid_key": "invalid_key"}, + }, + "invalid", + None, + ), + ] + ), + ) + def test_integrations_invalid_create( + self, + authenticated_client, + attributes, + error_code, + error_pointer, + ): + data = { + "data": { + "type": "integrations", + "attributes": attributes, + } + } + response = authenticated_client.post( + reverse("integration-list"), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"][0]["code"] == error_code + assert ( + response.json()["errors"][0]["source"]["pointer"] + == f"/data/attributes/{error_pointer}" + if error_pointer + else "/data" + ) + + def test_integrations_partial_update( + self, authenticated_client, integrations_fixture + ): + integration, *_ = integrations_fixture + data = { + "data": { + "type": "integrations", + "id": str(integration.id), + "attributes": { + "credentials": { + "aws_access_key_id": "new_value", + }, + # integration_type is `amazon_s3` + "configuration": { + "bucket_name": "new_bucket_name", + "output_directory": "new_output_directory", + }, + }, + } + } + response = authenticated_client.patch( + reverse("integration-detail", kwargs={"pk": integration.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_200_OK + integration.refresh_from_db() + assert integration.credentials["aws_access_key_id"] == "new_value" + assert integration.configuration["bucket_name"] == "new_bucket_name" + assert integration.configuration["output_directory"] == "new_output_directory" + + def test_integrations_partial_update_relationships( + self, authenticated_client, integrations_fixture + ): + integration, *_ = integrations_fixture + data = { + "data": { + "type": "integrations", + "id": str(integration.id), + "attributes": { + "credentials": { + "aws_access_key_id": "new_value", + }, + # integration_type is `amazon_s3` + "configuration": { + "bucket_name": "new_bucket_name", + "output_directory": "new_output_directory", + }, + }, + "relationships": {"providers": {"data": []}}, + } + } + + assert integration.providers.count() > 0 + response = authenticated_client.patch( + reverse("integration-detail", kwargs={"pk": integration.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_200_OK + integration.refresh_from_db() + assert integration.providers.count() == 0 + + def test_integrations_partial_update_invalid_content_type( + self, authenticated_client, integrations_fixture + ): + integration, *_ = integrations_fixture + response = authenticated_client.patch( + reverse("integration-detail", kwargs={"pk": integration.id}), + data={}, + ) + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + + def test_integrations_partial_update_invalid_content( + self, authenticated_client, integrations_fixture + ): + integration, *_ = integrations_fixture + data = { + "data": { + "type": "integrations", + "id": str(integration.id), + "attributes": {"invalid_config": "value"}, + } + } + response = authenticated_client.patch( + reverse("integration-detail", kwargs={"pk": integration.id}), + data=json.dumps(data), + content_type="application/vnd.api+json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_integrations_delete( + self, + authenticated_client, + integrations_fixture, + ): + integration, *_ = integrations_fixture + response = authenticated_client.delete( + reverse("integration-detail", kwargs={"pk": integration.id}) + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + + def test_integrations_delete_invalid(self, authenticated_client): + response = authenticated_client.delete( + reverse( + "integration-detail", + kwargs={"pk": "e67d0283-440f-48d1-b5f8-38d0763474f4"}, + ) + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.parametrize( + "filter_name, filter_value, expected_count", + ( + [ + ("inserted_at", TODAY, 2), + ("inserted_at.gte", "2024-01-01", 2), + ("inserted_at.lte", "2024-01-01", 0), + ("integration_type", Integration.IntegrationChoices.S3, 2), + ("integration_type", Integration.IntegrationChoices.SLACK, 0), + ( + "integration_type__in", + f"{Integration.IntegrationChoices.S3},{Integration.IntegrationChoices.SLACK}", + 2, + ), + ] + ), + ) + def test_integrations_filters( + self, + authenticated_client, + integrations_fixture, + filter_name, + filter_value, + expected_count, + ): + response = authenticated_client.get( + reverse("integration-list"), + {f"filter[{filter_name}]": filter_value}, + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == expected_count + + @pytest.mark.parametrize( + "filter_name", + ( + [ + "invalid", + ] + ), + ) + def test_integrations_filters_invalid(self, authenticated_client, filter_name): + response = authenticated_client.get( + reverse("integration-list"), + {f"filter[{filter_name}]": "whatever"}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py new file mode 100644 index 0000000000..ec57d14874 --- /dev/null +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -0,0 +1,122 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework_json_api import serializers +from rest_framework_json_api.serializers import ValidationError + + +class BaseValidateSerializer(serializers.Serializer): + def validate(self, data): + if hasattr(self, "initial_data"): + initial_data = set(self.initial_data.keys()) - {"id", "type"} + unknown_keys = initial_data - set(self.fields.keys()) + if unknown_keys: + raise ValidationError(f"Invalid fields: {unknown_keys}") + return data + + +# Integrations + + +class S3ConfigSerializer(BaseValidateSerializer): + bucket_name = serializers.CharField() + output_directory = serializers.CharField() + + class Meta: + resource_name = "integrations" + + +class AWSCredentialSerializer(BaseValidateSerializer): + role_arn = serializers.CharField(required=False) + external_id = serializers.CharField(required=False) + role_session_name = serializers.CharField(required=False) + session_duration = serializers.IntegerField( + required=False, min_value=900, max_value=43200 + ) + aws_access_key_id = serializers.CharField(required=False) + aws_secret_access_key = serializers.CharField(required=False) + aws_session_token = serializers.CharField(required=False) + + class Meta: + resource_name = "integrations" + + +@extend_schema_field( + { + "oneOf": [ + { + "type": "object", + "title": "AWS Credentials", + "properties": { + "role_arn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role " + "assumption.", + }, + "external_id": { + "type": "string", + "description": "An identifier to enhance security for role assumption.", + }, + "aws_access_key_id": { + "type": "string", + "description": "The AWS access key ID. Only required if the environment lacks pre-configured " + "AWS credentials.", + }, + "aws_secret_access_key": { + "type": "string", + "description": "The AWS secret access key. Required if 'aws_access_key_id' is provided or if " + "no AWS credentials are pre-configured.", + }, + "aws_session_token": { + "type": "string", + "description": "The session token for temporary credentials, if applicable.", + }, + "session_duration": { + "type": "integer", + "minimum": 900, + "maximum": 43200, + "default": 3600, + "description": "The duration (in seconds) for the role session.", + }, + "role_session_name": { + "type": "string", + "description": "An identifier for the role session, useful for tracking sessions in AWS logs. " + "The regex used to validate this parameter is a string of characters consisting of " + "upper- and lower-case alphanumeric characters with no spaces. You can also include " + "underscores or any of the following characters: =,.@-\n\n" + "Examples:\n" + "- MySession123\n" + "- User_Session-1\n" + "- Test.Session@2", + "pattern": "^[a-zA-Z0-9=,.@_-]+$", + }, + }, + }, + ] + } +) +class IntegrationCredentialField(serializers.JSONField): + pass + + +@extend_schema_field( + { + "oneOf": [ + { + "type": "object", + "title": "Amazon S3", + "properties": { + "bucket_name": { + "type": "string", + "description": "The name of the S3 bucket where files will be stored.", + }, + "output_directory": { + "type": "string", + "description": "The directory path within the bucket where files will be saved.", + }, + }, + "required": ["bucket_name", "output_directory"], + }, + ] + } +) +class IntegrationConfigField(serializers.JSONField): + pass diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 6b01190189..333a6474bf 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -16,6 +16,8 @@ from rest_framework_simplejwt.tokens import RefreshToken from api.models import ( ComplianceOverview, Finding, + Integration, + IntegrationProviderRelationship, Invitation, InvitationRoleRelationship, Membership, @@ -34,6 +36,12 @@ from api.models import ( UserRoleRelationship, ) from api.rls import Tenant +from api.v1.serializer_utils.integrations import ( + AWSCredentialSerializer, + IntegrationConfigField, + IntegrationCredentialField, + S3ConfigSerializer, +) # Tokens @@ -1606,8 +1614,8 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer): "manage_account", # Disable for the first release # "manage_billing", - # "manage_integrations", # /Disable for the first release + "manage_integrations", "manage_providers", "manage_scans", "permission_state", @@ -2013,3 +2021,201 @@ class ScheduleDailyCreateSerializer(serializers.Serializer): if unknown_keys: raise ValidationError(f"Invalid fields: {unknown_keys}") return data + + +# Integrations + + +class BaseWriteIntegrationSerializer(BaseWriteSerializer): + @staticmethod + def validate_integration_data( + integration_type: str, + providers: list[Provider], # noqa + configuration: dict, + credentials: dict, + ): + if integration_type == Integration.IntegrationChoices.S3: + config_serializer = S3ConfigSerializer + credentials_serializers = [AWSCredentialSerializer] + # TODO: This will be required for AWS Security Hub + # if providers and not all( + # provider.provider == Provider.ProviderChoices.AWS + # for provider in providers + # ): + # raise serializers.ValidationError( + # {"providers": "All providers must be AWS for the S3 integration."} + # ) + else: + raise serializers.ValidationError( + { + "integration_type": f"Integration type not supported yet: {integration_type}" + } + ) + + config_serializer(data=configuration).is_valid(raise_exception=True) + + for cred_serializer in credentials_serializers: + try: + cred_serializer(data=credentials).is_valid(raise_exception=True) + break + except ValidationError: + continue + else: + raise ValidationError( + {"credentials": "Invalid credentials for the integration type."} + ) + + +class IntegrationSerializer(RLSSerializer): + """ + Serializer for the Integration model. + """ + + providers = serializers.ResourceRelatedField( + queryset=Provider.objects.all(), many=True + ) + + class Meta: + model = Integration + fields = [ + "id", + "inserted_at", + "updated_at", + "enabled", + "connected", + "connection_last_checked_at", + "integration_type", + "configuration", + "providers", + "url", + ] + + included_serializers = { + "providers": "api.v1.serializers.ProviderIncludeSerializer", + } + + def to_representation(self, instance): + representation = super().to_representation(instance) + allowed_providers = self.context.get("allowed_providers") + if allowed_providers: + allowed_provider_ids = {str(provider.id) for provider in allowed_providers} + representation["providers"] = [ + provider + for provider in representation["providers"] + if provider["id"] in allowed_provider_ids + ] + return representation + + +class IntegrationCreateSerializer(BaseWriteIntegrationSerializer): + credentials = IntegrationCredentialField(write_only=True) + configuration = IntegrationConfigField() + providers = serializers.ResourceRelatedField( + queryset=Provider.objects.all(), many=True, required=False + ) + + class Meta: + model = Integration + fields = [ + "inserted_at", + "updated_at", + "enabled", + "connected", + "connection_last_checked_at", + "integration_type", + "configuration", + "credentials", + "providers", + ] + extra_kwargs = { + "inserted_at": {"read_only": True}, + "updated_at": {"read_only": True}, + "connected": {"read_only": True}, + "enabled": {"read_only": True}, + "connection_last_checked_at": {"read_only": True}, + } + + def validate(self, attrs): + integration_type = attrs.get("integration_type") + providers = attrs.get("providers") + configuration = attrs.get("configuration") + credentials = attrs.get("credentials") + + validated_attrs = super().validate(attrs) + self.validate_integration_data( + integration_type, providers, configuration, credentials + ) + return validated_attrs + + def create(self, validated_data): + tenant_id = self.context.get("tenant_id") + + providers = validated_data.pop("providers", []) + integration = Integration.objects.create(tenant_id=tenant_id, **validated_data) + + through_model_instances = [ + IntegrationProviderRelationship( + integration=integration, + provider=provider, + tenant_id=tenant_id, + ) + for provider in providers + ] + IntegrationProviderRelationship.objects.bulk_create(through_model_instances) + + return integration + + +class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer): + credentials = IntegrationCredentialField(write_only=True, required=False) + configuration = IntegrationConfigField(required=False) + providers = serializers.ResourceRelatedField( + queryset=Provider.objects.all(), many=True, required=False + ) + + class Meta: + model = Integration + fields = [ + "inserted_at", + "updated_at", + "enabled", + "connected", + "connection_last_checked_at", + "integration_type", + "configuration", + "credentials", + "providers", + ] + extra_kwargs = { + "inserted_at": {"read_only": True}, + "updated_at": {"read_only": True}, + "connected": {"read_only": True}, + "connection_last_checked_at": {"read_only": True}, + "integration_type": {"read_only": True}, + } + + def validate(self, attrs): + integration_type = self.instance.integration_type + providers = attrs.get("providers") + configuration = attrs.get("configuration") or self.instance.configuration + credentials = attrs.get("credentials") or self.instance.credentials + + validated_attrs = super().validate(attrs) + self.validate_integration_data( + integration_type, providers, configuration, credentials + ) + return validated_attrs + + def update(self, instance, validated_data): + tenant_id = self.context.get("tenant_id") + if validated_data.get("providers") is not None: + instance.providers.clear() + new_relationships = [ + IntegrationProviderRelationship( + integration=instance, provider=provider, tenant_id=tenant_id + ) + for provider in validated_data["providers"] + ] + IntegrationProviderRelationship.objects.bulk_create(new_relationships) + + return super().update(instance, validated_data) diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index f26e0cb8f3..c324314f86 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -10,6 +10,7 @@ from api.v1.views import ( FindingViewSet, GithubSocialLoginView, GoogleSocialLoginView, + IntegrationViewSet, InvitationAcceptViewSet, InvitationViewSet, MembershipViewSet, @@ -47,6 +48,7 @@ router.register( ) router.register(r"overviews", OverviewViewSet, basename="overview") router.register(r"schedules", ScheduleViewSet, basename="schedule") +router.register(r"integrations", IntegrationViewSet, basename="integration") tenants_router = routers.NestedSimpleRouter(router, r"tenants", lookup="tenant") tenants_router.register( diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 615fc08c08..df465f3169 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -57,6 +57,7 @@ from api.db_router import MainRouter from api.filters import ( ComplianceOverviewFilter, FindingFilter, + IntegrationFilter, InvitationFilter, MembershipFilter, ProviderFilter, @@ -74,6 +75,7 @@ from api.filters import ( from api.models import ( ComplianceOverview, Finding, + Integration, Invitation, Membership, Provider, @@ -102,6 +104,9 @@ from api.v1.serializers import ( FindingDynamicFilterSerializer, FindingMetadataSerializer, FindingSerializer, + IntegrationCreateSerializer, + IntegrationSerializer, + IntegrationUpdateSerializer, InvitationAcceptSerializer, InvitationCreateSerializer, InvitationSerializer, @@ -239,7 +244,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.5.0" + spectacular_settings.VERSION = "1.6.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -295,6 +300,11 @@ class SchemaView(SpectacularAPIView): "description": "Endpoints for task management, allowing retrieval of task status and " "revoking tasks that have not started.", }, + { + "name": "Integration", + "description": "Endpoints for managing third-party integrations, including registration, configuration," + " retrieval, and deletion of integrations such as S3, JIRA, or other services.", + }, ] return super().get(request, *args, **kwargs) @@ -2433,3 +2443,67 @@ class ScheduleViewSet(BaseRLSViewSet): ) }, ) + + +@extend_schema_view( + list=extend_schema( + tags=["Integration"], + summary="List all integrations", + description="Retrieve a list of all configured integrations with options for filtering by various criteria.", + ), + retrieve=extend_schema( + tags=["Integration"], + summary="Retrieve integration details", + description="Fetch detailed information about a specific integration by its ID.", + ), + create=extend_schema( + tags=["Integration"], + summary="Create a new integration", + description="Register a new integration with the system, providing necessary configuration details.", + ), + partial_update=extend_schema( + tags=["Integration"], + summary="Partially update an integration", + description="Modify certain fields of an existing integration without affecting other settings.", + ), + destroy=extend_schema( + tags=["Integration"], + summary="Delete an integration", + description="Remove an integration from the system by its ID.", + ), +) +@method_decorator(CACHE_DECORATOR, name="list") +@method_decorator(CACHE_DECORATOR, name="retrieve") +class IntegrationViewSet(BaseRLSViewSet): + queryset = Integration.objects.all() + serializer_class = IntegrationSerializer + http_method_names = ["get", "post", "patch", "delete"] + filterset_class = IntegrationFilter + ordering = ["integration_type", "-inserted_at"] + # RBAC required permissions + required_permissions = [Permissions.MANAGE_INTEGRATIONS] + allowed_providers = None + + def get_queryset(self): + user_roles = get_role(self.request.user) + if user_roles.unlimited_visibility: + # User has unlimited visibility, return all integrations + queryset = Integration.objects.filter(tenant_id=self.request.tenant_id) + else: + # User lacks permission, filter providers based on provider groups associated with the role + allowed_providers = get_providers(user_roles) + queryset = Integration.objects.filter(providers__in=allowed_providers) + self.allowed_providers = allowed_providers + return queryset + + def get_serializer_class(self): + if self.action == "create": + return IntegrationCreateSerializer + elif self.action == "partial_update": + return IntegrationUpdateSerializer + return super().get_serializer_class() + + def get_serializer_context(self): + context = super().get_serializer_context() + context["allowed_providers"] = self.allowed_providers + return context diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index 0bffdf4f75..9dc406ab5d 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -15,6 +15,8 @@ from api.db_utils import rls_transaction from api.models import ( ComplianceOverview, Finding, + Integration, + IntegrationProviderRelationship, Invitation, Membership, Provider, @@ -877,6 +879,46 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture): ) +@pytest.fixture +def integrations_fixture(providers_fixture): + provider1, provider2, *_ = providers_fixture + tenant_id = provider1.tenant_id + integration1 = Integration.objects.create( + tenant_id=tenant_id, + enabled=True, + connected=True, + integration_type="amazon_s3", + configuration={"key": "value"}, + credentials={"psswd": "1234"}, + ) + IntegrationProviderRelationship.objects.create( + tenant_id=tenant_id, + integration=integration1, + provider=provider1, + ) + + integration2 = Integration.objects.create( + tenant_id=tenant_id, + enabled=True, + connected=True, + integration_type="amazon_s3", + configuration={"key": "value"}, + credentials={"psswd": "1234"}, + ) + IntegrationProviderRelationship.objects.create( + tenant_id=tenant_id, + integration=integration2, + provider=provider1, + ) + IntegrationProviderRelationship.objects.create( + tenant_id=tenant_id, + integration=integration2, + provider=provider2, + ) + + return integration1, integration2 + + def get_authorization_header(access_token: str) -> dict: return {"Authorization": f"Bearer {access_token}"} diff --git a/dashboard/compliance/iso27001_2022_kubernetes.py b/dashboard/compliance/iso27001_2022_kubernetes.py new file mode 100644 index 0000000000..0d15771106 --- /dev/null +++ b/dashboard/compliance/iso27001_2022_kubernetes.py @@ -0,0 +1,23 @@ +import warnings + +from dashboard.common_methods import get_section_container_iso + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ATTRIBUTES_CATEGORY", + "REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID", + "REQUIREMENTS_ATTRIBUTES_OBJETIVE_NAME", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ] + return get_section_container_iso( + aux, "REQUIREMENTS_ATTRIBUTES_CATEGORY", "REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID" + ) diff --git a/prowler.py b/prowler-cli.py similarity index 100% rename from prowler.py rename to prowler-cli.py diff --git a/prowler/__main__.py b/prowler/__main__.py index 8b37cd5550..d27b7da8e9 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -60,6 +60,9 @@ from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_azure import AzureISO27001 from prowler.lib.outputs.compliance.iso27001.iso27001_gcp import GCPISO27001 +from prowler.lib.outputs.compliance.iso27001.iso27001_kubernetes import ( + KubernetesISO27001, +) from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_aws import AWSMitreAttack from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import ( @@ -631,6 +634,19 @@ def prowler(): ) generated_outputs["compliance"].append(cis) cis.batch_write_data_to_file() + elif compliance_name.startswith("iso27001_"): + # Generate ISO27001 Finding Object + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + iso27001 = KubernetesISO27001( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(iso27001) + iso27001.batch_write_data_to_file() else: filename = ( f"{output_options.output_directory}/compliance/" diff --git a/prowler/compliance/kubernetes/iso27001_2022_kubernetes.json b/prowler/compliance/kubernetes/iso27001_2022_kubernetes.json new file mode 100644 index 0000000000..0a7024a5f9 --- /dev/null +++ b/prowler/compliance/kubernetes/iso27001_2022_kubernetes.json @@ -0,0 +1,1559 @@ +{ + "Framework": "ISO27001", + "Version": "2022", + "Provider": "Kubernetes", + "Description": "ISO (the International Organization for Standardization) and IEC (the International Electrotechnical Commission) form the specialized system for worldwide standardization. National bodies that are members of ISO or IEC participate in the development of International Standards through technical committees established by the respective organization to deal with particular fields of technical activity. ISO and IEC technical committees collaborate in fields of mutual interest. Other international organizations, governmental and non-governmental, in liaison with ISO and IEC, also take part in the work.", + "Requirements": [ + { + "Id": "A.5.1", + "Description": "Information security policy and topic-specific policies should be defined, approved by management, published, communicated to and acknowledged by relevant personnel and relevant interested parties, and reviewed at planned intervals and if significant changes occur.", + "Name": "Policies for information security", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.1", + "Objetive_Name": "Policies for information security", + "Check_Summary": "Information security policy and topic-specific policies should be defined, approved by management, published, communicated to and acknowledged by relevant personnel and relevant interested parties, and reviewed at planned intervals and if significant changes occur." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_audit_log_path_set", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "apiserver_service_account_key_file_set", + "apiserver_service_account_lookup_true", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "etcd_unique_ca", + "kubelet_authorization_mode", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "kubelet_strong_ciphers_only", + "kubelet_tls_cert_and_key", + "rbac_cluster_admin_usage", + "rbac_minimize_pod_creation_access", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "rbac_minimize_wildcard_use_roles" + ] + }, + { + "Id": "A.5.2", + "Description": "Information security roles and responsibilities should be defined and allocated according to the organisation needs.", + "Name": "Roles and Responsibilities", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.2", + "Objetive_Name": "Roles and Responsibilities", + "Check_Summary": "Information security roles and responsibilities should be defined and allocated according to the organisation needs." + } + ], + "Checks": [ + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_service_account_key_file_set", + "apiserver_service_account_lookup_true", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "kubelet_authorization_mode", + "kubelet_client_ca_file_set", + "rbac_cluster_admin_usage", + "rbac_minimize_pod_creation_access", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "rbac_minimize_wildcard_use_roles" + ] + }, + { + "Id": "A.5.3", + "Description": "Conflicting duties and conflicting areas of responsibility should be segregated.", + "Name": "Segregation of Duties", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.3", + "Objetive_Name": "Segregation of Duties", + "Check_Summary": "Conflicting duties and conflicting areas of responsibility should be segregated." + } + ], + "Checks": [ + "rbac_cluster_admin_usage", + "rbac_minimize_csr_approval_access", + "rbac_minimize_node_proxy_subresource_access", + "rbac_minimize_pod_creation_access", + "rbac_minimize_pv_creation_access", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "rbac_minimize_webhook_config_access", + "rbac_minimize_wildcard_use_roles" + ] + }, + { + "Id": "A.5.4", + "Description": "Management should require all personnel to apply information security in accordance with the established information security policy, topic-specific policies and procedures of the organization.", + "Name": "Management Responsibilities", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.4", + "Objetive_Name": "Management Responsibilities", + "Check_Summary": "Management should require all personnel to apply information security in accordance with the established information security policy, topic-specific policies and procedures of the organization." + } + ], + "Checks": [] + }, + { + "Id": "A.5.5", + "Description": "The organisation should establish and maintain contact with relevant authorities.", + "Name": "Contact With Authorities", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.5", + "Objetive_Name": "Contact With Authorities", + "Check_Summary": "The organisation should establish and maintain contact with relevant authorities." + } + ], + "Checks": [] + }, + { + "Id": "A.5.6", + "Description": "The organisation should establish and maintain contact with special interest groups or other specialist security forums and professional associations.", + "Name": "Contact With Special Interest Groups", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.6", + "Objetive_Name": "Contact With Special Interest Groups", + "Check_Summary": "The organisation should establish and maintain contact with special interest groups or other specialist security forums and professional associations." + } + ], + "Checks": [] + }, + { + "Id": "A.5.7", + "Description": "Information relating to information security threats should be collected and analysed to produce threat intelligence.", + "Name": "Threat Intelligence", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.7", + "Objetive_Name": "Threat Intelligence", + "Check_Summary": "Information relating to information security threats should be collected and analysed to produce threat intelligence." + } + ], + "Checks": [] + }, + { + "Id": "A.5.8", + "Description": "Information security should be integrated into project management.", + "Name": "Information Security In Project Management", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.8", + "Objetive_Name": "Information Security In Project Management", + "Check_Summary": "Information security should be integrated into project management." + } + ], + "Checks": [] + }, + { + "Id": "A.5.9", + "Description": "An inventory of information and other associated assets, including owners, should be developed and maintained.", + "Name": "Inventory Of Information And Other Associated Assets", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.9", + "Objetive_Name": "Inventory Of Information And Other Associated Assets", + "Check_Summary": "An inventory of information and other associated assets, including owners, should be developed and maintained." + } + ], + "Checks": [] + }, + { + "Id": "A.5.10", + "Description": "Rules for the acceptable use and procedures for handling information and other associated assets should be identified, documented and implemented.", + "Name": "Acceptable Use Of Information And Other Associated Assets", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.10", + "Objetive_Name": "Acceptable Use Of Information And Other Associated Assets", + "Check_Summary": "Rules for the acceptable use and procedures for handling information and other associated assets should be identified, documented and implemented." + } + ], + "Checks": [] + }, + { + "Id": "A.5.11", + "Description": "Personnel and other interested parties as appropriate should return all the organisation’s assets in their possession upon change or termination of their employment, contract or agreement.", + "Name": "Return Of Assets", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.11", + "Objetive_Name": "Return Of Assets", + "Check_Summary": "Personnel and other interested parties as appropriate should return all the organisation’s assets in their possession upon change or termination of their employment, contract or agreement." + } + ], + "Checks": [] + }, + { + "Id": "A.5.12", + "Description": "Information should be classified according to the information security needs of the organisation based on confidentiality, integrity, availability and relevant interested party requirements.", + "Name": "Classification Of Information", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.12", + "Objetive_Name": "Classification Of Information", + "Check_Summary": "Information should be classified according to the information security needs of the organisation based on confidentiality, integrity, availability and relevant interested party requirements." + } + ], + "Checks": [] + }, + { + "Id": "A.5.13", + "Description": "An appropriate set of procedures for information labelling should be developed and implemented in accordance with the information classification scheme adopted by the organisation.", + "Name": "Labelling Of Information", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.13", + "Objetive_Name": "Labelling Of Information", + "Check_Summary": "An appropriate set of procedures for information labelling should be developed and implemented in accordance with the information classification scheme adopted by the organisation." + } + ], + "Checks": [ + "kubelet_config_yaml_ownership" + ] + }, + { + "Id": "A.5.14", + "Description": "Information transfer rules, procedures, or agreements should be in place for all types of transfer facilities within the organisation and between the organisation and other parties.", + "Name": "Information Transfer", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.14", + "Objetive_Name": "Information Transfer", + "Check_Summary": "Information transfer rules, procedures, or agreements should be in place for all types of transfer facilities within the organisation and between the organisation and other parties." + } + ], + "Checks": [] + }, + { + "Id": "A.5.15", + "Description": "Rules to control physical and logical access to information and other associated assets should be established", + "Name": "Access Control", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.15", + "Objetive_Name": "Access Control", + "Check_Summary": "Rules to control physical and logical access to information and other associated assets should be established" + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_service_account_key_file_set", + "apiserver_service_account_lookup_true", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "kubelet_authorization_mode", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "rbac_cluster_admin_usage", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "core_minimize_allowPrivilegeEscalation_containers", + "controllermanager_root_ca_file_set" + ] + }, + { + "Id": "A.5.16", + "Description": "The full lifecycle of identities should be managed.", + "Name": "Identity Management", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.16", + "Objetive_Name": "Identity Management", + "Check_Summary": "The full lifecycle of identities should be managed." + } + ], + "Checks": [ + "controllermanager_rotate_kubelet_server_cert", + "kubelet_rotate_certificates" + ] + }, + { + "Id": "A.5.17", + "Description": "Allocation and management of authentication information should be controlled by a management process, including advising personnel on the appropriate handling of authentication information.", + "Name": "Authentication Information", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.17", + "Objetive_Name": "Authentication Information", + "Check_Summary": "Allocation and management of authentication information should be controlled by a management process, including advising personnel on the appropriate handling of authentication information." + } + ], + "Checks": [] + }, + { + "Id": "A.5.18", + "Description": "Access rights to information and other associated assets should be provisioned, reviewed, modified and removed in accordance with the organisation’s topic-specific policy on and rules for access control.", + "Name": "Access Rights", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.18", + "Objetive_Name": "Access Rights", + "Check_Summary": "Access rights to information and other associated assets should be provisioned, reviewed, modified and removed in accordance with the organisation’s topic-specific policy on and rules for access control." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_service_account_key_file_set", + "apiserver_service_account_lookup_true", + "kubelet_authorization_mode", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "rbac_cluster_admin_usage", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation" + ] + }, + { + "Id": "A.5.19", + "Description": "Processes and procedures should be defined and implemented to manage the information security risks associated with the use of supplier’s products or services.", + "Name": "Information Security In Supplier Relationships", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.19", + "Objetive_Name": "Information Security In Supplier Relationships", + "Check_Summary": "Processes and procedures should be defined and implemented to manage the information security risks associated with the use of supplier’s products or services." + } + ], + "Checks": [] + }, + { + "Id": "A.5.20", + "Description": "Relevant information security requirements should be established and agreed with each supplier based on the type of supplier relationship.", + "Name": "Addressing Information Security Within Supplier Agreements", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.20", + "Objetive_Name": "Addressing Information Security Within Supplier Agreements", + "Check_Summary": "Relevant information security requirements should be established and agreed with each supplier based on the type of supplier relationship." + } + ], + "Checks": [] + }, + { + "Id": "A.5.21", + "Description": "Processes and procedures should be defined and implemented to manage the information security risks associated with the ICT products and services supply chain.", + "Name": "Managing Information Security In The ICT Supply Chain", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.21", + "Objetive_Name": "Managing Information Security In The ICT Supply Chain", + "Check_Summary": "Processes and procedures should be defined and implemented to manage the information security risks associated with the ICT products and services supply chain." + } + ], + "Checks": [] + }, + { + "Id": "A.5.22", + "Description": "The organisation should regularly monitor, review, evaluate and manage change in supplier information security practices and service delivery.", + "Name": "Monitor, Review And Change Management Of Supplier Services", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.22", + "Objetive_Name": "Monitor, Review And Change Management Of Supplier Services", + "Check_Summary": "The organisation should regularly monitor, review, evaluate and manage change in supplier information security practices and service delivery." + } + ], + "Checks": [] + }, + { + "Id": "A.5.23", + "Description": "Processes for acquisition, use, management and exit from cloud services should be established in accordance with the organisation’s information security requirements.", + "Name": "Information Security For Use Of Cloud Services", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.23", + "Objetive_Name": "Information Security For Use Of Cloud Services", + "Check_Summary": "Processes for acquisition, use, management and exit from cloud services should be established in accordance with the organisation’s information security requirements." + } + ], + "Checks": [] + }, + { + "Id": "A.5.24", + "Description": "The organization should plan and prepare for managing information security incidents by defining, establishing and communicating information security incident management processes, roles and responsibilities.", + "Name": "Information Security Incident Management Planning and Preparation", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.24", + "Objetive_Name": "Information Security Incident Management Planning and Preparation", + "Check_Summary": "The organization should plan and prepare for managing information security incidents by defining, establishing and communicating information security incident management processes, roles and responsibilities." + } + ], + "Checks": [] + }, + { + "Id": "A.5.25", + "Description": "The organisation should assess information security events and decide if they are to be categorised as information security incidents.", + "Name": "Assessment And Decision On Information Security Events", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.25", + "Objetive_Name": "Assessment And Decision On Information Security Events", + "Check_Summary": "The organisation should assess information security events and decide if they are to be categorised as information security incidents." + } + ], + "Checks": [] + }, + { + "Id": "A.5.26", + "Description": "Information security incidents should be responded to in accordance with the documented procedures.", + "Name": "Response To Information Security Incidents", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.26", + "Objetive_Name": "Response To Information Security Incidents", + "Check_Summary": "Information security incidents should be responded to in accordance with the documented procedures." + } + ], + "Checks": [] + }, + { + "Id": "A.5.27", + "Description": "Knowledge gained from information security incidents should be used to strengthen and improve the information security controls.", + "Name": "Learning From Information Security Incidents", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.27", + "Objetive_Name": "Learning From Information Security Incidents", + "Check_Summary": "Knowledge gained from information security incidents should be used to strengthen and improve the information security controls." + } + ], + "Checks": [] + }, + { + "Id": "A.5.28", + "Description": "The organisation should establish and implement procedures for the identification, collection, acquisition and preservation of evidence related to information security events.", + "Name": "Collection Of Evidence", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.28", + "Objetive_Name": "Collection Of Evidence", + "Check_Summary": "The organisation should establish and implement procedures for the identification, collection, acquisition and preservation of evidence related to information security events." + } + ], + "Checks": [] + }, + { + "Id": "A.5.29", + "Description": "The organisation should plan how to maintain information security at an appropriate level during disruption.", + "Name": "Information Security During Disruption", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.29", + "Objetive_Name": "Information Security During Disruption", + "Check_Summary": "The organisation should plan how to maintain information security at an appropriate level during disruption." + } + ], + "Checks": [] + }, + { + "Id": "A.5.30", + "Description": "ICT readiness should be planned, implemented, maintained and tested based on business continuity objectives and ICT continuity requirements. ", + "Name": "Readiness For Business Continuity", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.30", + "Objetive_Name": "Readiness For Business Continuity", + "Check_Summary": "ICT readiness should be planned, implemented, maintained and tested based on business continuity objectives and ICT continuity requirements. " + } + ], + "Checks": [] + }, + { + "Id": "A.5.31", + "Description": "Legal, statutory, regulatory and contractual requirements relevant to information security and the organisations approach to meet these requirements should be identified, documented and kept up to date. ", + "Name": "Legal, statutory, regulatory and contractual requirements", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.31", + "Objetive_Name": "Legal, statutory, regulatory and contractual requirements", + "Check_Summary": "Legal, statutory, regulatory and contractual requirements relevant to information security and the organisations approach to meet these requirements should be identified, documented and kept up to date. " + } + ], + "Checks": [] + }, + { + "Id": "A.5.32", + "Description": "The organisation should implement appropriate procedures to protect intellectual property rights. ", + "Name": "Intellectual Property Rights", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.32", + "Objetive_Name": "Intellectual Property Rights", + "Check_Summary": "The organisation should implement appropriate procedures to protect intellectual property rights. " + } + ], + "Checks": [] + }, + { + "Id": "A.5.33", + "Description": "Records should be protected from loss, destruction, falsification, unauthorised access and unauthorised release.", + "Name": "Protection Of Records", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.33", + "Objetive_Name": "Protection Of Records", + "Check_Summary": "Records should be protected from loss, destruction, falsification, unauthorised access and unauthorised release." + } + ], + "Checks": [] + }, + { + "Id": "A.5.34", + "Description": "The organisation should identify and meet the requirements regarding the preservation of privacy and protection of PII according to applicable laws and regulations and contractual requirements.", + "Name": "Privacy And Protection Of PII", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.34", + "Objetive_Name": "Privacy And Protection Of PII", + "Check_Summary": "The organisation should identify and meet the requirements regarding the preservation of privacy and protection of PII according to applicable laws and regulations and contractual requirements." + } + ], + "Checks": [] + }, + { + "Id": "A.5.35", + "Description": "The organisations approach to managing information security and its implementation including people, processes and technologies should be reviewed independently at planned intervals, or when significant changes occur. ", + "Name": "Independent Review Of Information Security", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.35", + "Objetive_Name": "Independent Review Of Information Security", + "Check_Summary": "The organisations approach to managing information security and its implementation including people, processes and technologies should be reviewed independently at planned intervals, or when significant changes occur. " + } + ], + "Checks": [] + }, + { + "Id": "A.5.36", + "Description": "Compliance with the organisations information security policy, topic-specific policies, rules and standards should be regularly reviewed. ", + "Name": "Compliance With Policies, Rules And Standards For Information Security", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.36", + "Objetive_Name": "Compliance With Policies, Rules And Standards For Information Security", + "Check_Summary": "Compliance with the organisations information security policy, topic-specific policies, rules and standards should be regularly reviewed. " + } + ], + "Checks": [] + }, + { + "Id": "A.5.37", + "Description": "Operating procedures for information processing facilities should be documented and made available to personnel who need them. ", + "Name": "Documented Operating Procedures", + "Attributes": [ + { + "Category": "A.5 Organizational controls", + "Objetive_ID": "A.5.37", + "Objetive_Name": "Documented Operating Procedures", + "Check_Summary": "Operating procedures for information processing facilities should be documented and made available to personnel who need them. " + } + ], + "Checks": [] + }, + { + "Id": "A.6.1", + "Description": "Background verification checks on all candidates to become personnel should be carried out prior to joining the organisation and on an ongoing basis taking into consideration applicable laws, regulations and ethics and be proportional to the business requirements, the classification of the information to be accessed and the perceived risks. ", + "Name": "Screening", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.1", + "Objetive_Name": "Screening", + "Check_Summary": "Background verification checks on all candidates to become personnel should be carried out prior to joining the organisation and on an ongoing basis taking into consideration applicable laws, regulations and ethics and be proportional to the business requirements, the classification of the information to be accessed and the perceived risks. " + } + ], + "Checks": [] + }, + { + "Id": "A.6.2", + "Description": "The employment contractual agreements should state the personnel’s and the organisations responsibilities for information security. ", + "Name": "Terms and Conditions Of Employment", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.2", + "Objetive_Name": "Terms and Conditions Of Employment", + "Check_Summary": "The employment contractual agreements should state the personnel’s and the organisations responsibilities for information security. " + } + ], + "Checks": [] + }, + { + "Id": "A.6.3", + "Description": "Personnel of the organisation and relevant interested parties should receive appropriate information security awareness, education and training and regular updates of the organisations information security policy, topic-specific policies and procedures, as relevant for their job function.", + "Name": "Information Security Awareness, Education And Training", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.3", + "Objetive_Name": "Information Security Awareness, Education And Training", + "Check_Summary": "Personnel of the organisation and relevant interested parties should receive appropriate information security awareness, education and training and regular updates of the organisations information security policy, topic-specific policies and procedures, as relevant for their job function." + } + ], + "Checks": [] + }, + { + "Id": "A.6.4", + "Description": "A disciplinary process should be formalised and communicated to take actions against personnel and other relevant interested parties who have committed an information security policy violation.", + "Name": "Disciplinary Process", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.4", + "Objetive_Name": "Disciplinary Process", + "Check_Summary": "A disciplinary process should be formalised and communicated to take actions against personnel and other relevant interested parties who have committed an information security policy violation." + } + ], + "Checks": [] + }, + { + "Id": "A.6.5", + "Description": "Information security responsibilities and duties that remain valid after termination or change of employment should be defined, enforced and communicated to relevant personnel and other interested parties.", + "Name": "Responsibilities After Termination Or Change Of Employment", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.5", + "Objetive_Name": "Responsibilities After Termination Or Change Of Employment", + "Check_Summary": "Information security responsibilities and duties that remain valid after termination or change of employment should be defined, enforced and communicated to relevant personnel and other interested parties." + } + ], + "Checks": [] + }, + { + "Id": "A.6.6", + "Description": "Confidentiality or non-disclosure agreements reflecting the organisation’s needs for the protection of information should be identified, documented, regularly reviewed and signed by personnel and other relevant interested parties.", + "Name": "Confidentiality Or Non-Disclosure Agreements", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.6", + "Objetive_Name": "Confidentiality Or Non-Disclosure Agreements", + "Check_Summary": "Confidentiality or non-disclosure agreements reflecting the organisation’s needs for the protection of information should be identified, documented, regularly reviewed and signed by personnel and other relevant interested parties." + } + ], + "Checks": [] + }, + { + "Id": "A.6.7", + "Description": "Security measures should be implemented when personnel are working remotely to protect information accessed, processed or stored outside the organisation’s premises.", + "Name": "Remote Working", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.7", + "Objetive_Name": "Remote Working", + "Check_Summary": "Security measures should be implemented when personnel are working remotely to protect information accessed, processed or stored outside the organisation’s premises." + } + ], + "Checks": [] + }, + { + "Id": "A.6.8", + "Description": "The organisation should provide a mechanism for personnel to report observed or suspected information security events through appropriate channels in a timely manner.", + "Name": "Information Security Event Reporting", + "Attributes": [ + { + "Category": "A.6 People controls", + "Objetive_ID": "A.6.8", + "Objetive_Name": "Information Security Event Reporting", + "Check_Summary": "The organisation should provide a mechanism for personnel to report observed or suspected information security events through appropriate channels in a timely manner." + } + ], + "Checks": [] + }, + { + "Id": "A.7.1", + "Description": "To prevent unauthorised physical access, damage and interference to the organisations information and other associated assets.", + "Name": "Physical Security Perimeters", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.1", + "Objetive_Name": "Physical Security Perimeters", + "Check_Summary": "To prevent unauthorised physical access, damage and interference to the organisations information and other associated assets." + } + ], + "Checks": [] + }, + { + "Id": "A.7.2", + "Description": "Secure areas should be protected by appropriate entry controls and access points.", + "Name": "Physical Entry", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.2", + "Objetive_Name": "Physical Entry", + "Check_Summary": "Secure areas should be protected by appropriate entry controls and access points." + } + ], + "Checks": [] + }, + { + "Id": "A.7.3", + "Description": "Physical security for offices, rooms and facilities should be designed and implemented.", + "Name": "Securing Offices, Rooms And Facilities", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.3", + "Objetive_Name": "Securing Offices, Rooms And Facilities", + "Check_Summary": "Physical security for offices, rooms and facilities should be designed and implemented." + } + ], + "Checks": [] + }, + { + "Id": "A.7.4", + "Description": "Premises should be continuously monitored for unauthorised physical access.", + "Name": "Physical Security Monitoring", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.4", + "Objetive_Name": "Physical Security Monitoring", + "Check_Summary": "Premises should be continuously monitored for unauthorised physical access." + } + ], + "Checks": [] + }, + { + "Id": "A.7.5", + "Description": "Protection against physical and environmental threats, such as natural disasters and other intentional or unintentional physical threats to infrastructure should be designed and implemented.", + "Name": "Protecting Against Physical and Environmental Threats", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.5", + "Objetive_Name": "Protecting Against Physical and Environmental Threats", + "Check_Summary": "Protection against physical and environmental threats, such as natural disasters and other intentional or unintentional physical threats to infrastructure should be designed and implemented." + } + ], + "Checks": [] + }, + { + "Id": "A.7.6", + "Description": "Security measures for working in secure areas should be designed and implemented.", + "Name": "Working In Secure Areas", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.6", + "Objetive_Name": "Working In Secure Areas", + "Check_Summary": "Security measures for working in secure areas should be designed and implemented." + } + ], + "Checks": [] + }, + { + "Id": "A.7.7", + "Description": "Clear desk rules for papers and removable storage media and clear screen rules for information processing facilities should be defined and appropriately enforced.", + "Name": "Clear Desk And Clear Screen", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.7", + "Objetive_Name": "Clear Desk And Clear Screen", + "Check_Summary": "Clear desk rules for papers and removable storage media and clear screen rules for information processing facilities should be defined and appropriately enforced." + } + ], + "Checks": [] + }, + { + "Id": "A.7.8", + "Description": "Equipment should be sited securely and protected.", + "Name": "Equipment Siting And Protection", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.8", + "Objetive_Name": "Equipment Siting And Protection", + "Check_Summary": "Equipment should be sited securely and protected." + } + ], + "Checks": [] + }, + { + "Id": "A.7.9", + "Description": "Off-site assets should be protected.", + "Name": "Security Of Assets Off-Premises", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.9", + "Objetive_Name": "Security Of Assets Off-Premises", + "Check_Summary": "Off-site assets should be protected." + } + ], + "Checks": [] + }, + { + "Id": "A.7.10", + "Description": "Storage media should be managed through their life cycle of acquisition, use, transportation and disposal in accordance with the organisations classification scheme and handling requirements.", + "Name": "Storage Media", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.10", + "Objetive_Name": "Storage Media", + "Check_Summary": "Storage media should be managed through their life cycle of acquisition, use, transportation and disposal in accordance with the organisations classification scheme and handling requirements." + } + ], + "Checks": [] + }, + { + "Id": "A.7.11", + "Description": "Information processing facilities should be protected from power failures and other disruptions caused by failures in supporting utilities.", + "Name": "Supporting Utilities", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.11", + "Objetive_Name": "Supporting Utilities", + "Check_Summary": "Information processing facilities should be protected from power failures and other disruptions caused by failures in supporting utilities." + } + ], + "Checks": [] + }, + { + "Id": "A.7.12", + "Description": "Cables carrying power, data or supporting information services should be protected from interception", + "Name": "Cabling Security", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.12", + "Objetive_Name": "Cabling Security", + "Check_Summary": "Cables carrying power, data or supporting information services should be protected from interception" + } + ], + "Checks": [] + }, + { + "Id": "A.7.13", + "Description": "Equipment should be maintained correctly to ensure availability, integrity and confidentiality of information.", + "Name": "Equipment Maintenance", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.13", + "Objetive_Name": "Equipment Maintenance", + "Check_Summary": "Equipment should be maintained correctly to ensure availability, integrity and confidentiality of information." + } + ], + "Checks": [] + }, + { + "Id": "A.7.14", + "Description": "Items of equipment containing storage media should be verified to ensure that any sensitive data and licensed software has been removed or securely overwritten prior to disposal or re-use.", + "Name": "Secure Disposal Or Re-Use Of Equipment", + "Attributes": [ + { + "Category": "A.7 Physical controls", + "Objetive_ID": "A.7.14", + "Objetive_Name": "Secure Disposal Or Re-Use Of Equipment", + "Check_Summary": "Items of equipment containing storage media should be verified to ensure that any sensitive data and licensed software has been removed or securely overwritten prior to disposal or re-use." + } + ], + "Checks": [] + }, + { + "Id": "A.8.1", + "Description": "Information stored on, processed by or accessible via user endpoint devices should be protected.", + "Name": "User Endpoint Devices", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.1", + "Objetive_Name": "User Endpoint Devices", + "Check_Summary": "Information stored on, processed by or accessible via user endpoint devices should be protected." + } + ], + "Checks": [ + "kubelet_strong_ciphers_only", + "apiserver_encryption_provider_config_set", + "apiserver_strong_ciphers_only", + "etcd_tls_encryption" + ] + }, + { + "Id": "A.8.2", + "Description": "The allocation and use of privileged access rights should be restricted and managed.", + "Name": "Privileged Access Rights", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.2", + "Objetive_Name": "Privileged Access Rights", + "Check_Summary": "The allocation and use of privileged access rights should be restricted and managed." + } + ], + "Checks": [ + "controllermanager_root_ca_file_set", + "core_minimize_root_containers_admission", + "kubelet_conf_file_ownership", + "kubelet_service_file_ownership_root", + "controllermanager_service_account_private_key_file", + "core_minimize_privileged_containers", + "kubelet_tls_cert_and_key", + "rbac_minimize_pod_creation_access", + "rbac_minimize_pv_creation_access", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "rbac_minimize_webhook_config_access", + "rbac_minimize_wildcard_use_roles", + "apiserver_anonymous_requests" + ] + }, + { + "Id": "A.8.3", + "Description": "Access to information and other associated assets should be restricted in accordance with the established topic-specific policy on access control.", + "Name": "Information Access Restriction", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.3", + "Objetive_Name": "Information Access Restriction", + "Check_Summary": "Access to information and other associated assets should be restricted in accordance with the established topic-specific policy on access control." + } + ], + "Checks": [ + "core_minimize_privileged_containers", + "rbac_minimize_csr_approval_access", + "rbac_minimize_node_proxy_subresource_access", + "rbac_minimize_pod_creation_access", + "rbac_minimize_pv_creation_access", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation", + "rbac_minimize_webhook_config_access" + ] + }, + { + "Id": "A.8.4", + "Description": "Read and write access to source code, development tools and software libraries should be appropriately managed.", + "Name": "Access To Source Code", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.4", + "Objetive_Name": "Access To Source Code", + "Check_Summary": "Read and write access to source code, development tools and software libraries should be appropriately managed." + } + ], + "Checks": [] + }, + { + "Id": "A.8.5", + "Description": "Secure authentication technologies and procedures should be implemented based on information access restrictions and the topic-specific policy on access control.", + "Name": "Secure Authentication", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.5", + "Objetive_Name": "Secure Authentication", + "Check_Summary": "Secure authentication technologies and procedures should be implemented based on information access restrictions and the topic-specific policy on access control." + } + ], + "Checks": [ + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "apiserver_tls_config", + "controllermanager_rotate_kubelet_server_cert", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_peer_tls_config", + "etcd_tls_encryption", + "kubelet_rotate_certificates", + "kubelet_tls_cert_and_key", + "rbac_minimize_csr_approval_access" + ] + }, + { + "Id": "A.8.6", + "Description": "The use of resources should be monitored and adjusted in line with current and expected capacity requirements.", + "Name": "Capacity Management", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.6", + "Objetive_Name": "Capacity Management", + "Check_Summary": "The use of resources should be monitored and adjusted in line with current and expected capacity requirements." + } + ], + "Checks": [] + }, + { + "Id": "A.8.7", + "Description": "Protection against malware should be implemented and supported by appropriate user awareness.", + "Name": "Protection Against Malware", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.7", + "Objetive_Name": "Protection Against Malware", + "Check_Summary": "Protection against malware should be implemented and supported by appropriate user awareness." + } + ], + "Checks": [] + }, + { + "Id": "A.8.8", + "Description": "Information about technical vulnerabilities of information systems in use should be obtained, the organisations exposure to such vulnerabilities should be evaluated and appropriate measures should be taken.", + "Name": "Management of Technical Vulnerabilities", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.8", + "Objetive_Name": "Management of Technical Vulnerabilities", + "Check_Summary": "Information about technical vulnerabilities of information systems in use should be obtained, the organisations exposure to such vulnerabilities should be evaluated and appropriate measures should be taken." + } + ], + "Checks": [] + }, + { + "Id": "A.8.9", + "Description": "Configurations, including security configurations, of hardware, software, services and networks should be established, documented, implemented, monitored and reviewed.", + "Name": "Configuration Management", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.9", + "Objetive_Name": "Configuration Management", + "Check_Summary": "Configurations, including security configurations, of hardware, software, services and networks should be established, documented, implemented, monitored and reviewed." + } + ], + "Checks": [] + }, + { + "Id": "A.8.10", + "Description": "Information stored in information systems, devices or in any other storage media should be deleted when no longer required.", + "Name": "Information Deletion", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.10", + "Objetive_Name": "Information Deletion", + "Check_Summary": "Information stored in information systems, devices or in any other storage media should be deleted when no longer required." + } + ], + "Checks": [ + "apiserver_audit_log_maxage_set", + "apiserver_audit_log_maxbackup_set", + "apiserver_audit_log_maxsize_set", + "apiserver_audit_log_path_set" + ] + }, + { + "Id": "A.8.11", + "Description": "Data masking should be used in accordance with the organisation’s topic-specific policy on access control and other related topic-specific policies, and business requirements, taking applicable legislation into consideration.", + "Name": "Data Masking", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.11", + "Objetive_Name": "Data Masking", + "Check_Summary": "Data masking should be used in accordance with the organisation’s topic-specific policy on access control and other related topic-specific policies, and business requirements, taking applicable legislation into consideration." + } + ], + "Checks": [ + "apiserver_encryption_provider_config_set", + "etcd_tls_encryption", + "apiserver_etcd_tls_config", + "apiserver_kubelet_tls_auth", + "apiserver_tls_config", + "etcd_no_auto_tls", + "etcd_no_peer_auto_tls", + "etcd_peer_tls_config", + "etcd_tls_encryption", + "kubelet_tls_cert_and_key" + ] + }, + { + "Id": "A.8.12", + "Description": "Data leakage prevention measures should be applied to systems, networks and any other devices that process, store or transmit sensitive information.", + "Name": "Data Leakage Prevention", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.12", + "Objetive_Name": "Data Leakage Prevention", + "Check_Summary": "Data leakage prevention measures should be applied to systems, networks and any other devices that process, store or transmit sensitive information." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_audit_log_path_set", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "apiserver_service_account_key_file_set", + "apiserver_service_account_lookup_true", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "kubelet_authorization_mode", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "kubelet_strong_ciphers_only", + "kubelet_tls_cert_and_key", + "rbac_cluster_admin_usage", + "rbac_minimize_secret_access", + "rbac_minimize_service_account_token_creation" + ] + }, + { + "Id": "A.8.13", + "Description": "Backup copies of information, software and systems should be maintained and regularly tested in accordance with the agreed topic-specific policy on backup.", + "Name": "Information Backup", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.13", + "Objetive_Name": "Information Backup", + "Check_Summary": "Backup copies of information, software and systems should be maintained and regularly tested in accordance with the agreed topic-specific policy on backup." + } + ], + "Checks": [ + "apiserver_audit_log_maxage_set", + "apiserver_audit_log_maxbackup_set", + "apiserver_audit_log_maxsize_set", + "apiserver_audit_log_path_set" + ] + }, + { + "Id": "A.8.14", + "Description": "Information processing facilities should be implemented with redundancy sufficient to meet availability", + "Name": "Redundancy of information processing facilities", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.14", + "Objetive_Name": "Redundancy of information processing facilities", + "Check_Summary": "Information processing facilities should be implemented with redundancy sufficient to meet availability" + } + ], + "Checks": [ + "kubelet_streaming_connection_timeout", + "core_minimize_admission_windows_hostprocess_containers", + "core_minimize_allowPrivilegeEscalation_containers", + "rbac_minimize_webhook_config_access", + "scheduler_bind_address", + "scheduler_profiling" + ] + }, + { + "Id": "A.8.15", + "Description": "Logs that record activities, exceptions, faults and other relevant events should be produced, stored, protected and analysed.", + "Name": "Logging", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.15", + "Objetive_Name": "Logging", + "Check_Summary": "Logs that record activities, exceptions, faults and other relevant events should be produced, stored, protected and analysed." + } + ], + "Checks": [ + "apiserver_audit_log_maxage_set", + "apiserver_audit_log_maxbackup_set", + "apiserver_audit_log_maxsize_set", + "apiserver_audit_log_path_set" + ] + }, + { + "Id": "A.8.16", + "Description": "Networks, systems and applications should be monitored for anomalous behaviour and appropriate actions taken to evaluate potential information security incidents.", + "Name": "Monitoring Activities", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.16", + "Objetive_Name": "Monitoring Activities", + "Check_Summary": "Networks, systems and applications should be monitored for anomalous behaviour and appropriate actions taken to evaluate potential information security incidents." + } + ], + "Checks": [ + "rbac_minimize_node_proxy_subresource_access", + "core_minimize_hostNetwork_containers", + "core_minimize_net_raw_capability_admission" + ] + }, + { + "Id": "A.8.17", + "Description": "The clocks of information processing systems used by the organisation should be synchronised to approved time sources.", + "Name": "Clock Synchronisation", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.17", + "Objetive_Name": "Clock Synchronisation", + "Check_Summary": "The clocks of information processing systems used by the organisation should be synchronised to approved time sources." + } + ], + "Checks": [] + }, + { + "Id": "A.8.18", + "Description": "The use of utility programs that can be capable of overriding system and application controls should be restricted and tightly controlled", + "Name": "Use of Privileged Utility Programs", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.18", + "Objetive_Name": "Use of Privileged Utility Programs", + "Check_Summary": "The use of utility programs that can be capable of overriding system and application controls should be restricted and tightly controlled" + } + ], + "Checks": [] + }, + { + "Id": "A.8.19", + "Description": "Procedures and measures should be implemented to securely manage software installation on operational systems.", + "Name": "Installation of Software on Operational Systems", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.19", + "Objetive_Name": "Installation of Software on Operational Systems", + "Check_Summary": "Procedures and measures should be implemented to securely manage software installation on operational systems." + } + ], + "Checks": [] + }, + { + "Id": "A.8.20", + "Description": "Networks and network devices should be secured, managed and controlled to protect information in systems and applications.", + "Name": "Network Security", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.20", + "Objetive_Name": "Network Security", + "Check_Summary": "Networks and network devices should be secured, managed and controlled to protect information in systems and applications." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_audit_log_path_set", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "kubelet_strong_ciphers_only", + "kubelet_tls_cert_and_key" + ] + }, + { + "Id": "A.8.21", + "Description": "Security mechanisms, service levels and service requirements of network services should be identified, implemented and monitored.", + "Name": "Security of Network Services", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.21", + "Objetive_Name": "Security of Network Services", + "Check_Summary": "Security mechanisms, service levels and service requirements of network services should be identified, implemented and monitored." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_audit_log_path_set", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "kubelet_strong_ciphers_only", + "kubelet_tls_cert_and_key" + ] + }, + { + "Id": "A.8.22", + "Description": "Security mechanisms, service levels and service requirements of network services should be identified, implemented and monitored.", + "Name": "Segregation of Networks", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.22", + "Objetive_Name": "Segregation of Networks", + "Check_Summary": "Security mechanisms, service levels and service requirements of network services should be identified, implemented and monitored." + } + ], + "Checks": [ + "apiserver_anonymous_requests", + "apiserver_audit_log_path_set", + "apiserver_auth_mode_include_rbac", + "apiserver_auth_mode_not_always_allow", + "apiserver_client_ca_file_set", + "apiserver_encryption_provider_config_set", + "apiserver_etcd_cafile_set", + "apiserver_etcd_tls_config", + "apiserver_kubelet_cert_auth", + "apiserver_kubelet_tls_auth", + "etcd_client_cert_auth", + "etcd_peer_client_cert_auth", + "etcd_tls_encryption", + "kubelet_client_ca_file_set", + "kubelet_disable_anonymous_auth", + "kubelet_rotate_certificates", + "kubelet_strong_ciphers_only", + "kubelet_tls_cert_and_key" + ] + }, + { + "Id": "A.8.23", + "Description": "Access to external websites should be managed to reduce exposure to malicious content.", + "Name": "Web Filtering", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.23", + "Objetive_Name": "Web Filtering", + "Check_Summary": "Access to external websites should be managed to reduce exposure to malicious content." + } + ], + "Checks": [] + }, + { + "Id": "A.8.24", + "Description": "Rules for the effective use of cryptography, including cryptographic key management, should be defined and implemented.", + "Name": "Use of Cryptography", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.24", + "Objetive_Name": "Use of Cryptography", + "Check_Summary": "Rules for the effective use of cryptography, including cryptographic key management, should be defined and implemented." + } + ], + "Checks": [ + "apiserver_etcd_tls_config", + "apiserver_kubelet_tls_auth", + "apiserver_service_account_key_file_set", + "apiserver_tls_config", + "controllermanager_service_account_private_key_file", + "etcd_peer_tls_config", + "etcd_tls_encryption", + "kubelet_tls_cert_and_key" + ] + }, + { + "Id": "A.8.25", + "Description": "Rules for the secure development of software and systems should be established and applied.", + "Name": "Secure Development Life Cycle", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.25", + "Objetive_Name": "Secure Development Life Cycle", + "Check_Summary": "Rules for the secure development of software and systems should be established and applied." + } + ], + "Checks": [] + }, + { + "Id": "A.8.26", + "Description": "Information security requirements should be identified, specified and approved when developing or acquiring applications.", + "Name": "Application Security Requirements", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.26", + "Objetive_Name": "Application Security Requirements", + "Check_Summary": "Information security requirements should be identified, specified and approved when developing or acquiring applications." + } + ], + "Checks": [] + }, + { + "Id": "A.8.27", + "Description": "Principles for engineering secure systems should be established, documented, maintained and applied to any information system development activities.", + "Name": "Secure Systems Architecture and Engineering Principles", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.27", + "Objetive_Name": "Secure Systems Architecture and Engineering Principles", + "Check_Summary": "Principles for engineering secure systems should be established, documented, maintained and applied to any information system development activities." + } + ], + "Checks": [] + }, + { + "Id": "A.8.29", + "Description": "Security testing processes should be defined and implemented in the development life cycle.", + "Name": "Security Testing in Development and Acceptance", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.29", + "Objetive_Name": "Security Testing in Development and Acceptance", + "Check_Summary": "Security testing processes should be defined and implemented in the development life cycle." + } + ], + "Checks": [] + }, + { + "Id": "A.8.30", + "Description": "The organisation should direct, monitor and review the activities related to outsourced system development.", + "Name": "Outsourced Development", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.30", + "Objetive_Name": "Outsourced Development", + "Check_Summary": "The organisation should direct, monitor and review the activities related to outsourced system development." + } + ], + "Checks": [] + }, + { + "Id": "A.8.31", + "Description": "Development, testing and production environments should be separated and secured.", + "Name": "Separation of Development, Test and Production Environments", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.31", + "Objetive_Name": "Separation of Development, Test and Production Environments", + "Check_Summary": "Development, testing and production environments should be separated and secured." + } + ], + "Checks": [] + }, + { + "Id": "A.8.32", + "Description": "Changes to information processing facilities and information systems should be subject to change management procedures.", + "Name": "Change Management", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.32", + "Objetive_Name": "Change Management", + "Check_Summary": "Changes to information processing facilities and information systems should be subject to change management procedures." + } + ], + "Checks": [] + }, + { + "Id": "A.8.33", + "Description": "Test information should be appropriately selected, protected and managed.", + "Name": "Test Information", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.33", + "Objetive_Name": "Test Information", + "Check_Summary": "Test information should be appropriately selected, protected and managed." + } + ], + "Checks": [] + }, + { + "Id": "A.8.34", + "Description": "Audit tests and other assurance activities involving assessment of operational systems should be planned and agreed between the tester and appropriate management.", + "Name": "Protection of Information Systems During Audit Testing", + "Attributes": [ + { + "Category": "A.8 Technological controls", + "Objetive_ID": "A.8.34", + "Objetive_Name": "Protection of Information Systems During Audit Testing", + "Check_Summary": "Audit tests and other assurance activities involving assessment of operational systems should be planned and agreed between the tester and appropriate management." + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py index 7ea41f4352..807d559243 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py @@ -13,7 +13,7 @@ class AWSISO27001(ComplianceOutput): - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. Methods: - - transform: Transforms findings into AWS ENS compliance format. + - transform: Transforms findings into AWS ISO 27001 compliance format. """ def transform( @@ -23,7 +23,7 @@ class AWSISO27001(ComplianceOutput): compliance_name: str, ) -> None: """ - Transforms a list of findings into AWS ENS compliance format. + Transforms a list of findings into AWS ISO 27001 compliance format. Parameters: - findings (list): A list of findings. diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py index d467195d61..58aff0d348 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py @@ -13,7 +13,7 @@ class AzureISO27001(ComplianceOutput): - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. Methods: - - transform: Transforms findings into AWS ENS compliance format. + - transform: Transforms findings into Azure ISO 27001 compliance format. """ def transform( @@ -23,7 +23,7 @@ class AzureISO27001(ComplianceOutput): compliance_name: str, ) -> None: """ - Transforms a list of findings into Azure ENS compliance format. + Transforms a list of findings into Azure ISO 27001 compliance format. Parameters: - findings (list): A list of findings. diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py index 4194ec4aae..d7aadb268a 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py @@ -13,7 +13,7 @@ class GCPISO27001(ComplianceOutput): - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. Methods: - - transform: Transforms findings into AWS ENS compliance format. + - transform: Transforms findings into GCP ISO 27001 compliance format. """ def transform( diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py new file mode 100644 index 0000000000..d3890c408c --- /dev/null +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py @@ -0,0 +1,87 @@ +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.iso27001.models import KubernetesISO27001Model +from prowler.lib.outputs.finding import Finding + + +class KubernetesISO27001(ComplianceOutput): + """ + This class represents the Kubernetes ISO 27001 compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Kubernetes ISO 27001 compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Kubernetes ISO 27001 compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = KubernetesISO27001Model( + Provider=finding.provider, + Description=compliance.Description, + Context=finding.account_name, + Namespace=finding.region, + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Category=attribute.Category, + Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, + Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, + Requirements_Attributes_Check_Summary=attribute.Check_Summary, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + CheckId=finding.check_id, + Muted=finding.muted, + ResourceName=finding.resource_name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = KubernetesISO27001Model( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + Context="", + Namespace="", + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Category=attribute.Category, + Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, + Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, + Requirements_Attributes_Check_Summary=attribute.Check_Summary, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/iso27001/models.py b/prowler/lib/outputs/compliance/iso27001/models.py index 956c4e812c..1d84b5c892 100644 --- a/prowler/lib/outputs/compliance/iso27001/models.py +++ b/prowler/lib/outputs/compliance/iso27001/models.py @@ -74,3 +74,28 @@ class GCPISO27001Model(BaseModel): CheckId: str Muted: bool ResourceName: str + + +class KubernetesISO27001Model(BaseModel): + """ + KubernetesISO27001Model generates a finding's output in CSV Kubernetes ISO27001 format. + """ + + Provider: str + Description: str + Context: str + Namespace: str + AssessmentDate: str + Requirements_Id: str + Requirements_Name: str + Requirements_Description: str + Requirements_Attributes_Category: str + Requirements_Attributes_Objetive_ID: str + Requirements_Attributes_Objetive_Name: str + Requirements_Attributes_Check_Summary: str + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured.py b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured.py index c54f842027..6349e926bc 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured.py +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured.py @@ -7,12 +7,15 @@ from prowler.providers.aws.services.cloudwatch.cloudwatch_client import ( class cloudwatch_alarm_actions_alarm_state_configured(Check): def execute(self): findings = [] - for metric_alarm in cloudwatch_client.metric_alarms: - report = Check_Report_AWS(metadata=self.metadata(), resource=metric_alarm) - report.status = "PASS" - report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} has actions configured for the ALARM state." - if not metric_alarm.alarm_actions: - report.status = "FAIL" - report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} does not have actions configured for the ALARM state." - findings.append(report) + if cloudwatch_client.metric_alarms is not None: + for metric_alarm in cloudwatch_client.metric_alarms: + report = Check_Report_AWS( + metadata=self.metadata(), resource=metric_alarm + ) + report.status = "PASS" + report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} has actions configured for the ALARM state." + if not metric_alarm.alarm_actions: + report.status = "FAIL" + report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} does not have actions configured for the ALARM state." + findings.append(report) return findings diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.py b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.py index 1dbe5f084e..f94e0d9429 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.py +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.py @@ -7,14 +7,17 @@ from prowler.providers.aws.services.cloudwatch.cloudwatch_client import ( class cloudwatch_alarm_actions_enabled(Check): def execute(self): findings = [] - for metric_alarm in cloudwatch_client.metric_alarms: - report = Check_Report_AWS(metadata=self.metadata(), resource=metric_alarm) - report.status = "PASS" - report.status_extended = ( - f"CloudWatch metric alarm {metric_alarm.name} has actions enabled." - ) - if not metric_alarm.actions_enabled: - report.status = "FAIL" - report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} does not have actions enabled." - findings.append(report) + if cloudwatch_client.metric_alarms is not None: + for metric_alarm in cloudwatch_client.metric_alarms: + report = Check_Report_AWS( + metadata=self.metadata(), resource=metric_alarm + ) + report.status = "PASS" + report.status_extended = ( + f"CloudWatch metric alarm {metric_alarm.name} has actions enabled." + ) + if not metric_alarm.actions_enabled: + report.status = "FAIL" + report.status_extended = f"CloudWatch metric alarm {metric_alarm.name} does not have actions enabled." + findings.append(report) return findings diff --git a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py index 0202631659..36e650eab4 100644 --- a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py +++ b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py @@ -3,28 +3,45 @@ from prowler.providers.aws.services.rds.rds_client import rds_client class rds_instance_event_subscription_parameter_groups(Check): + """Ensure RDS parameter group event categories of configuration change are subscribed. + + This check is useful to ensure we receive notification of events that may affect the security, availability, and reliability of the RDS database instances associated with these parameter groups. + """ + def execute(self): + """Execute the RDS Parameter Group events are subscribed check. + + Iterates through the RDS DB event subscriptions and checks if the event source is DB parameter group and the event list is empty (so it's suscribe to all categories) or contains only configuration change. + + Returns: + List[Check_Report_AWS]: A list of reports for each RDS DB event subscription. + """ findings = [] if rds_client.provider.scan_unused_services or rds_client.db_instances: for db_event in rds_client.db_event_subscriptions: - report = Check_Report_AWS(metadata=self.metadata(), resource=db_event) + report = Check_Report_AWS(metadata=self.metadata(), resource={}) report.status = "FAIL" report.status_extended = "RDS parameter group event categories of configuration change is not subscribed." report.resource_id = rds_client.audited_account report.resource_arn = rds_client._get_rds_arn_template(db_event.region) report.region = db_event.region - report.resource_tags = [] - if db_event.source_type == "db-parameter-group" and db_event.enabled: - if db_event.event_list == [] or db_event.event_list == [ - "configuration change", - ]: - report = Check_Report_AWS( - metadata=self.metadata(), resource=db_event - ) + if db_event.source_type == "db-parameter-group": + report = Check_Report_AWS( + metadata=self.metadata(), resource=db_event + ) + if db_event.enabled and ( + db_event.event_list == [] + or db_event.event_list + == [ + "configuration change", + ] + ): report.status = "PASS" report.status_extended = ( "RDS parameter group events are subscribed." ) + else: + report.status = "FAIL" + report.status_extended = "RDS parameter group event category of configuration change is not subscribed." findings.append(report) - return findings diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/__init__.py b/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.metadata.json b/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.metadata.json new file mode 100644 index 0000000000..942e1a3c97 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_admin_mfa_enabled_for_administrative_roles", + "CheckTitle": "Ensure multifactor authentication is enabled for all users in administrative roles.", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Conditional Access Policy", + "Description": "Ensure that multifactor authentication (MFA) is enabled for all users in administrative roles to enhance security and reduce the risk of unauthorized access.", + "Risk": "Without MFA enabled for administrative roles, attackers could compromise privileged accounts with only a single authentication factor, increasing the risk of data breaches and unauthorized access to sensitive resources.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com. 2. Expand Protection > Conditional Access and select Policies. 3. Click 'New policy' and configure: Users: Select users and groups > Directory roles (include admin roles). Target resources: Include 'All cloud apps' with no exclusions. Grant: Select 'Grant Access' and check 'Require multifactor authentication'. 4. Set policy to 'Report Only' for testing before full enforcement. 5. Click 'Create'.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable MFA for all users in administrative roles using a Conditional Access policy in Microsoft Entra.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.py b/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.py new file mode 100644 index 0000000000..d69276f135 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles.py @@ -0,0 +1,80 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportMicrosoft365 +from prowler.providers.microsoft365.services.entra.entra_client import entra_client +from prowler.providers.microsoft365.services.entra.entra_service import ( + AdminRoles, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, +) + + +class entra_admin_mfa_enabled_for_administrative_roles(Check): + """ + Ensure multifactor authentication is enabled for all users in administrative roles. + + This check verifies that at least one Conditional Access Policy in Microsoft Entra, which is in an enabled state, + applies to administrative roles and enforces multifactor authentication (MFA). Enforcing MFA for privileged accounts + is critical to reduce the risk of unauthorized access through compromised credentials. + + The check fails if no enabled policy is found that requires MFA for any administrative role. + """ + + def execute(self) -> List[CheckReportMicrosoft365]: + """ + Execute the admin MFA requirement check for administrative roles. + + Iterates over the Conditional Access Policies retrieved from the Entra client and generates a report + indicating whether MFA is enforced for users in administrative roles. + + Returns: + List[CheckReportMicrosoft365]: A list containing a single report with the result of the check. + """ + findings = [] + + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy requiring MFA for administrative roles was found." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if not ({admin_role.value for admin_role in AdminRoles}).issubset( + set(policy.conditions.user_conditions.included_roles) + ): + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + if ( + ConditionalAccessGrantControl.MFA + in policy.grant_controls.built_in_controls + ): + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource=entra_client.conditional_access_policies, + resource_name=policy.display_name, + resource_id=policy.id, + ) + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces MFA for administrative roles." + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' only reports MFA for administrative roles but does not enforce it." + break + + findings.append(report) + return findings diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/__init__.py b/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json b/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json new file mode 100644 index 0000000000..039071202c --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_identity_protection_sign_in_risk_enabled", + "CheckTitle": "Ensure that Identity Protection sign-in risk policies are enabled", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Conditional Access Policy", + "Description": "Ensure that Identity Protection sign-in risk policies are enabled to detect and respond to suspicious high and medium risk login attempts in real time.", + "Risk": "Without Identity Protection sign-in risk policies enabled, suspicious sign-in attempts may go unnoticed, allowing attackers to access accounts using stolen or compromised credentials. This increases the risk of unauthorized access, data breaches, and privilege escalation.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy. Under Users or workload identities choose All users. Under Cloud apps or actions choose All cloud apps. Under Conditions choose Sign-in risk then Yes and check the risk level boxes High and Medium. Under Access Controls select Grant then in the right pane click Grant access then select Require multifactor authentication. Under Session select Sign-in Frequency and set to Every time. Click Select. 5. Under Enable policy set it to Report Only until the organization is ready to enable it. 6. Click Create.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Identity Protection sign-in risk policies to detect and respond to suspicious login attempts in real time. Configure Conditional Access to require MFA for risky sign-ins and ensure all users are enrolled in MFA to prevent account lockouts. Regularly review sign-in risk reports to identify and mitigate potential security threats.", + "Url": "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.py b/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.py new file mode 100644 index 0000000000..29b33cdc3e --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.py @@ -0,0 +1,81 @@ +from prowler.lib.check.models import Check, CheckReportMicrosoft365 +from prowler.providers.microsoft365.services.entra.entra_client import entra_client +from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + RiskLevel, + SignInFrequencyInterval, +) + + +class entra_identity_protection_sign_in_risk_enabled(Check): + """Check if at least one Conditional Access policy is a Identity Protection sign-in risk policy. + + This check ensures that at least one Conditional Access policy is a Identity Protection sign-in risk policy. + """ + + def execute(self) -> list[CheckReportMicrosoft365]: + """Execute the check to ensure that at least one Conditional Access policy is a Identity Protection sign-in risk policy. + + Returns: + list[CheckReportMicrosoft365]: A list containing the results of the check. + """ + findings = [] + + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy is a sign-in risk based Identity Protection Policy." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + if ( + ConditionalAccessGrantControl.MFA + not in policy.grant_controls.built_in_controls + ): + continue + + if ( + SignInFrequencyInterval.EVERY_TIME + != policy.session_controls.sign_in_frequency.interval + ): + continue + + report = CheckReportMicrosoft365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + if ( + RiskLevel.HIGH not in policy.conditions.sign_in_risk_levels + or RiskLevel.MEDIUM not in policy.conditions.sign_in_risk_levels + ): + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a sign-in risk based Identity Protection Policy but does not protect against high and medium sign-in risk attempts." + elif policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a sign-in risk based Identity Protection Policy and reports high and medium risk potential sign-in attempts, but does not protect against them." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a sign-in risk based Identity Protection Policy and does protect against high and medium risk potential sign-in attempts." + break + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index 97d347fba7..9b83f98fc4 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -178,6 +178,14 @@ class Entra(Microsoft365Service): [], ) ], + sign_in_risk_levels=[ + RiskLevel(risk_level) + for risk_level in getattr( + policy.conditions, + "sign_in_risk_levels", + [], + ) + ], ), grant_controls=GrantControls( built_in_controls=( @@ -347,6 +355,7 @@ class Conditions(BaseModel): application_conditions: Optional[ApplicationsConditions] user_conditions: Optional[UsersConditions] user_risk_levels: List[RiskLevel] = [] + sign_in_risk_levels: List[RiskLevel] = [] class PersistentBrowser(BaseModel): diff --git a/pyproject.toml b/pyproject.toml index 8bfb4516bd..44ce5b58af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] build-backend = "poetry.core.masonry.api" -requires = ["poetry-core"] +requires = ["poetry-core>=2.0"] # https://peps.python.org/pep-0621/ [project] diff --git a/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured_test.py b/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured_test.py index f1f1cde85d..ae84723476 100644 --- a/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured_test.py +++ b/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_alarm_state_configured/cloudwatch_alarm_actions_alarm_state_configured_test.py @@ -28,7 +28,6 @@ class Test_cloudwatch_alarm_actions_alarm_state_configured: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_alarm_actions_alarm_state_configured import ( cloudwatch_alarm_actions_alarm_state_configured, ) @@ -38,6 +37,38 @@ class Test_cloudwatch_alarm_actions_alarm_state_configured: assert len(result) == 0 + @mock_aws + def test_none_cloudwatch_alarms(self): + cloudwatch_client = client("cloudwatch", region_name=AWS_REGION_US_EAST_1) + cloudwatch_client.metric_alarms = [] + + from prowler.providers.aws.services.cloudwatch.cloudwatch_service import ( + CloudWatch, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_client", + new=CloudWatch(aws_provider), + ) as cloudwatch_client_mock, + ): + from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_alarm_actions_alarm_state_configured import ( + cloudwatch_alarm_actions_alarm_state_configured, + ) + + cloudwatch_client_mock.metric_alarms = None + + check = cloudwatch_alarm_actions_alarm_state_configured() + result = check.execute() + + assert len(result) == 0 + @mock_aws def test_cloudwatch_alarms_actions_configured(self): cloudwatch_client = client("cloudwatch", region_name=AWS_REGION_US_EAST_1) @@ -66,7 +97,6 @@ class Test_cloudwatch_alarm_actions_alarm_state_configured: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_alarm_actions_alarm_state_configured import ( cloudwatch_alarm_actions_alarm_state_configured, ) @@ -116,7 +146,6 @@ class Test_cloudwatch_alarm_actions_alarm_state_configured: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_alarm_state_configured.cloudwatch_alarm_actions_alarm_state_configured import ( cloudwatch_alarm_actions_alarm_state_configured, ) diff --git a/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled_test.py b/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled_test.py index 0d4d20c01e..178e2f2f86 100644 --- a/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled_test.py +++ b/tests/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled_test.py @@ -28,7 +28,6 @@ class Test_cloudwatch_alarm_actions_enabled: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_enabled.cloudwatch_alarm_actions_enabled import ( cloudwatch_alarm_actions_enabled, ) @@ -38,6 +37,37 @@ class Test_cloudwatch_alarm_actions_enabled: assert len(result) == 0 + def test_none_cloudwatch_alarms(self): + cloudwatch_client = client("cloudwatch", region_name=AWS_REGION_US_EAST_1) + cloudwatch_client.metric_alarms = [] + + from prowler.providers.aws.services.cloudwatch.cloudwatch_service import ( + CloudWatch, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_enabled.cloudwatch_alarm_actions_enabled.cloudwatch_client", + new=CloudWatch(aws_provider), + ) as cloudwatch_client_mock, + ): + from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_enabled.cloudwatch_alarm_actions_enabled import ( + cloudwatch_alarm_actions_enabled, + ) + + cloudwatch_client_mock.metric_alarms = None + + check = cloudwatch_alarm_actions_enabled() + result = check.execute() + + assert len(result) == 0 + @mock_aws def test_cloudwatch_alarms_actions_enabled(self): cloudwatch_client = client("cloudwatch", region_name=AWS_REGION_US_EAST_1) @@ -66,7 +96,6 @@ class Test_cloudwatch_alarm_actions_enabled: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_enabled.cloudwatch_alarm_actions_enabled import ( cloudwatch_alarm_actions_enabled, ) @@ -116,7 +145,6 @@ class Test_cloudwatch_alarm_actions_enabled: new=CloudWatch(aws_provider), ), ): - from prowler.providers.aws.services.cloudwatch.cloudwatch_alarm_actions_enabled.cloudwatch_alarm_actions_enabled import ( cloudwatch_alarm_actions_enabled, ) diff --git a/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py b/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py index 81c2ead211..bfb0987aa4 100644 --- a/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py +++ b/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py @@ -47,6 +47,7 @@ class Test_rds_instance__no_event_subscriptions: assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN assert result[0].resource_tags == [] + assert result[0].resource == {} @mock_aws def test_rds_no_events_ignoring(self): @@ -190,3 +191,61 @@ class Test_rds_instance__no_event_subscriptions: == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) assert result[0].resource_tags == [] + + @mock_aws + def test_rds_parameter_event_subscription_not_parameter_group(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.aurora-postgresql14", + Description="test parameter group", + ) + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="aurora-postgresql", + DBName="aurora-postgres", + DBInstanceClass="db.m1.small", + DBParameterGroupName="test", + DBClusterIdentifier="db-cluster-1", + ) + conn.create_event_subscription( + SubscriptionName="TestSub", + SnsTopicArn=f"arn:aws:sns:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:test", + SourceType="db-instance", + Enabled=True, + Tags=[ + {"Key": "test", "Value": "testing"}, + ], + ) + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_event_subscription_parameter_groups.rds_instance_event_subscription_parameter_groups.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_event_subscription_parameter_groups.rds_instance_event_subscription_parameter_groups import ( + rds_instance_event_subscription_parameter_groups, + ) + + check = rds_instance_event_subscription_parameter_groups() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS parameter group event categories of configuration change is not subscribed." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] + assert result[0].resource == {} diff --git a/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py b/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py new file mode 100644 index 0000000000..ed30be2c7a --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_admin_mfa_enabled_for_administrative_roles/entra_admin_mfa_enabled_for_administrative_roles_test.py @@ -0,0 +1,539 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.microsoft365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicy, + ConditionalAccessPolicyState, + Conditions, + GrantControls, + GrantControlOperator, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_entra_admin_mfa_enabled_for_administrative_roles: + def test_no_conditional_access_policies(self): + """No conditional access policies configured: expected FAIL.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = {} + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requiring MFA for administrative roles was found." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_policy_disabled(self): + """Policy in DISABLED state: expected to be ignored and return FAIL.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="Disabled Policy", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requiring MFA for administrative roles was found." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_policy_missing_admin_roles(self): + """ + Enabled policy that does not apply to administrative roles: + Does not include 'All' in included_users nor administrative roles in included_roles. + Expected FAIL. + """ + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="No Admin Roles Policy", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requiring MFA for administrative roles was found." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_policy_missing_application_all(self): + """ + Enabled policy that includes administrative users (via 'All') + but does not have "All" in included_applications. + Expected FAIL. + """ + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="Missing Application All Policy", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["MicrosoftAdminPortals"], + excluded_applications=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requiring MFA for administrative roles was found." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_policy_valid(self): + """ + Valid policy: + - State enabled for reporting only + - Applies to administrative roles via 'All' in included_users + - Application conditions include "All" + - MFA is configured in grant_controls + + Expected FAIL due to is only for reporting. + """ + policy_id = str(uuid4()) + display_name = "Valid MFA Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + expected_status_extended = f"Conditional Access Policy '{display_name}' only reports MFA for administrative roles but does not enforce it." + assert result[0].status_extended == expected_status_extended + assert result[0].resource == entra_client.conditional_access_policies + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + + def test_policy_valid_through_roles(self): + """ + Valid policy: + - State enabled (ENABLED) + - Applies to administrative roles + - Application conditions include "All" + - MFA is configured in grant_controls + + Expected PASS. + """ + policy_id = str(uuid4()) + display_name = "Valid MFA Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[ + "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", + "c4e39bd9-1100-46d3-8c65-fb160da0071f", + "b0f54661-2d74-4c50-afa3-1ec803f12efe", + "158c047a-c907-4556-b7ef-446551a6b5f7", + "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9", + "29232cdf-9323-42fd-ade2-1d097af3e4de", + "62e90394-69f5-4237-9190-012177145e10", + "f2ef992c-3afb-46b9-b7cf-a126ee74c451", + "729827e3-9c14-49f7-bb1b-9608f156bbb8", + "966707d0-3269-4727-9be2-8c3a10f19b9d", + "7be44c8a-adaf-4e2a-84d6-ab2649e08a13", + "e8611ab8-c189-46e8-94e1-60213ab1f814", + "194ae4cb-b126-40b2-bd5b-6091b380977d", + "f28a1f50-f6e7-4571-818b-6a12f2af6b6c", + "fe930be7-5e62-47db-91af-98c3a49a38b1", + ], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + expected_status_extended = f"Conditional Access Policy '{display_name}' enforces MFA for administrative roles." + assert result[0].status_extended == expected_status_extended + assert result[0].resource == entra_client.conditional_access_policies + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + + def test_policy_valid_one_missing_role(self): + """ + Valid policy: + - State enabled (ENABLED or ENABLED_FOR_REPORTING) + - Applies to administrative roles except one + - Application conditions include "All" + - MFA is configured in grant_controls + + Expected FAIL. + """ + policy_id = str(uuid4()) + display_name = "Valid MFA Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import ( + entra_admin_mfa_enabled_for_administrative_roles, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[ + "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", + "c4e39bd9-1100-46d3-8c65-fb160da0071f", + "b0f54661-2d74-4c50-afa3-1ec803f12efe", + "158c047a-c907-4556-b7ef-446551a6b5f7", + "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9", + "29232cdf-9323-42fd-ade2-1d097af3e4de", + "62e90394-69f5-4237-9190-012177145e10", + "f2ef992c-3afb-46b9-b7cf-a126ee74c451", + "729827e3-9c14-49f7-bb1b-9608f156bbb8", + "966707d0-3269-4727-9be2-8c3a10f19b9d", + "7be44c8a-adaf-4e2a-84d6-ab2649e08a13", + "e8611ab8-c189-46e8-94e1-60213ab1f814", + "194ae4cb-b126-40b2-bd5b-6091b380977d", + "f28a1f50-f6e7-4571-818b-6a12f2af6b6c", + ], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_admin_mfa_enabled_for_administrative_roles() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + expected_status_extended = "No Conditional Access Policy requiring MFA for administrative roles was found." + assert result[0].status_extended == expected_status_extended + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" diff --git a/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py new file mode 100644 index 0000000000..fd6ff82a91 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled_test.py @@ -0,0 +1,375 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.microsoft365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + RiskLevel, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_entra_identity_protection_sign_in_risk_enabled: + def test_entra_no_conditional_access_policies(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled import ( + entra_identity_protection_sign_in_risk_enabled, + ) + + entra_client.conditional_access_policies = {} + + check = entra_identity_protection_sign_in_risk_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy is a sign-in risk based Identity Protection Policy." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_entra_identity_protection_user_risk_policy_disabled(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled import ( + entra_identity_protection_sign_in_risk_enabled, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=[], excluded_applications=[] + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + user_risk_levels=[], + sign_in_risk_levels=[], + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.OR + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_identity_protection_sign_in_risk_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy is a sign-in risk based Identity Protection Policy." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_entra_identity_protection_user_risk_policy_enabled_not_enough_risk(self): + id = str(uuid4()) + display_name = "Test" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled import ( + entra_identity_protection_sign_in_risk_enabled, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + user_risk_levels=[RiskLevel.LOW], + sign_in_risk_levels=[RiskLevel.LOW], + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.PASSWORD_CHANGE, + ], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_identity_protection_sign_in_risk_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' is a sign-in risk based Identity Protection Policy but does not protect against high and medium sign-in risk attempts." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global" + + def test_entra_identity_protection_user_risk_policy_enabled_for_reporting(self): + id = str(uuid4()) + display_name = "Test" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled import ( + entra_identity_protection_sign_in_risk_enabled, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + user_risk_levels=[RiskLevel.HIGH], + sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM], + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.PASSWORD_CHANGE, + ], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_identity_protection_sign_in_risk_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' is a sign-in risk based Identity Protection Policy and reports high and medium risk potential sign-in attempts, but does not protect against them." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global" + + def test_entra_identity_protection_user_risk_policy_enabled(self): + id = str(uuid4()) + display_name = "Test" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_identity_protection_sign_in_risk_enabled.entra_identity_protection_sign_in_risk_enabled import ( + entra_identity_protection_sign_in_risk_enabled, + ) + from prowler.providers.microsoft365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + user_risk_levels=[RiskLevel.HIGH], + sign_in_risk_levels=[RiskLevel.HIGH, RiskLevel.MEDIUM], + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.PASSWORD_CHANGE, + ], + operator=GrantControlOperator.AND, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_identity_protection_sign_in_risk_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' is a sign-in risk based Identity Protection Policy and does protect against high and medium risk potential sign-in attempts." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global"