merge branch 'master' into PRWLR-5989-ensure-that-spf-records-are-published-for-all-exchange
No files matched your search
@@ -30,5 +30,6 @@ jobs:
|
||||
"PROWLER_COMMIT_SHORT_SHA": "${{ env.SHORT_SHA }}",
|
||||
"PROWLER_PR_TITLE": "${{ github.event.pull_request.title }}",
|
||||
"PROWLER_PR_LABELS": ${{ toJson(github.event.pull_request.labels.*.name) }},
|
||||
"PROWLER_PR_BODY": ${{ toJson(github.event.pull_request.body) }}
|
||||
"PROWLER_PR_BODY": ${{ toJson(github.event.pull_request.body) }},
|
||||
"PROWLER_PR_URL":${{ toJson(github.event.pull_request.html_url) }}
|
||||
}'
|
||||
@@ -25,17 +25,31 @@ jobs:
|
||||
MINOR_VERSION=${BASH_REMATCH[2]}
|
||||
FIX_VERSION=${BASH_REMATCH[3]}
|
||||
|
||||
# Export version components to GitHub environment
|
||||
echo "MAJOR_VERSION=${MAJOR_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "MINOR_VERSION=${MINOR_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "FIX_VERSION=${FIX_VERSION}" >> "${GITHUB_ENV}"
|
||||
|
||||
if (( MAJOR_VERSION == 5 )); then
|
||||
if (( FIX_VERSION == 0 )); then
|
||||
echo "Minor Release: $PROWLER_VERSION"
|
||||
|
||||
# Set up next minor version for master
|
||||
BUMP_VERSION_TO=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).${FIX_VERSION}
|
||||
echo "BUMP_VERSION_TO=${BUMP_VERSION_TO}" >> "${GITHUB_ENV}"
|
||||
|
||||
TARGET_BRANCH=${BASE_BRANCH}
|
||||
echo "TARGET_BRANCH=${TARGET_BRANCH}" >> "${GITHUB_ENV}"
|
||||
|
||||
# Set up patch version for version branch
|
||||
PATCH_VERSION_TO=${MAJOR_VERSION}.${MINOR_VERSION}.1
|
||||
echo "PATCH_VERSION_TO=${PATCH_VERSION_TO}" >> "${GITHUB_ENV}"
|
||||
|
||||
VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION}
|
||||
echo "VERSION_BRANCH=${VERSION_BRANCH}" >> "${GITHUB_ENV}"
|
||||
|
||||
echo "Bumping to next minor version: ${BUMP_VERSION_TO} in branch ${TARGET_BRANCH}"
|
||||
echo "Bumping to next patch version: ${PATCH_VERSION_TO} in branch ${VERSION_BRANCH}"
|
||||
else
|
||||
echo "Patch Release: $PROWLER_VERSION"
|
||||
|
||||
@@ -74,7 +88,6 @@ jobs:
|
||||
|
||||
git --no-pager diff
|
||||
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
@@ -92,3 +105,41 @@ jobs:
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
|
||||
- name: Handle patch version for minor release
|
||||
if: env.FIX_VERSION == '0'
|
||||
run: |
|
||||
echo "Using PROWLER_VERSION=$PROWLER_VERSION"
|
||||
echo "Using PATCH_VERSION_TO=$PATCH_VERSION_TO"
|
||||
|
||||
set -e
|
||||
|
||||
echo "Bumping version in pyproject.toml ..."
|
||||
sed -i "s|version = \"${PROWLER_VERSION}\"|version = \"${PATCH_VERSION_TO}\"|" pyproject.toml
|
||||
|
||||
echo "Bumping version in prowler/config/config.py ..."
|
||||
sed -i "s|prowler_version = \"${PROWLER_VERSION}\"|prowler_version = \"${PATCH_VERSION_TO}\"|" prowler/config/config.py
|
||||
|
||||
echo "Bumping version in .env ..."
|
||||
sed -i "s|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${PROWLER_VERSION}|NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${PATCH_VERSION_TO}|" .env
|
||||
|
||||
git --no-pager diff
|
||||
|
||||
- name: Create Pull Request for patch version
|
||||
if: env.FIX_VERSION == '0'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
base: ${{ env.VERSION_BRANCH }}
|
||||
commit-message: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}"
|
||||
branch: "version-bump-to-v${{ env.PATCH_VERSION_TO }}"
|
||||
title: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}"
|
||||
body: |
|
||||
### Description
|
||||
|
||||
Bump Prowler version to v${{ env.PATCH_VERSION_TO }}
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
@@ -3,7 +3,7 @@
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<b><i>Prowler Open Source</b> is as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security.
|
||||
<b><i>Prowler Open Source</b> is as dynamic and adaptable as the environment it secures. It is trusted by the industry leaders to uphold the highest standards in security.
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
|
||||
@@ -43,15 +43,29 @@
|
||||
|
||||
# Description
|
||||
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler Cloud</a>.
|
||||
**Prowler** is an open-source security tool designed to assess and enforce security best practices across AWS, Azure, Google Cloud, and Kubernetes. It supports tasks such as security audits, incident response, continuous monitoring, system hardening, forensic readiness, and remediation processes.
|
||||
|
||||
Prowler includes hundreds of built-in controls to ensure compliance with standards and frameworks, including:
|
||||
|
||||
- **Industry Standards:** CIS, NIST 800, NIST CSF, and CISA
|
||||
- **Regulatory Compliance and Governance:** RBI, FedRAMP, and PCI-DSS
|
||||
- **Frameworks for Sensitive Data and Privacy:** GDPR, HIPAA, and FFIEC
|
||||
- **Frameworks for Organizational Governance and Quality Control:** SOC2 and GXP
|
||||
- **AWS-Specific Frameworks:** AWS Foundational Technical Review (FTR) and AWS Well-Architected Framework (Security Pillar)
|
||||
- **National Security Standards:** ENS (Spanish National Security Scheme)
|
||||
- **Custom Security Frameworks:** Tailored to your needs
|
||||
|
||||
## Prowler CLI and Prowler Cloud
|
||||
|
||||
Prowler offers a Command Line Interface (CLI), known as Prowler Open Source, and an additional service built on top of it, called <a href="https://prowler.com">Prowler Cloud</a>.
|
||||
|
||||
## Prowler App
|
||||
|
||||
Prowler App is a web application that allows you to run Prowler in your cloud provider accounts and visualize the results in a user-friendly interface.
|
||||
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
|
||||
|
||||

|
||||
|
||||
>More details at [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
|
||||
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
@@ -60,6 +74,7 @@ prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
|
||||
## Prowler Dashboard
|
||||
|
||||
```console
|
||||
@@ -67,7 +82,7 @@ prowler dashboard
|
||||
```
|
||||

|
||||
|
||||
It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks.
|
||||
# Prowler at a Glance
|
||||
|
||||
| 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) |
|
||||
|---|---|---|---|---|
|
||||
@@ -75,18 +90,19 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
| GCP | 79 | 13 | 7 | 3 |
|
||||
| Azure | 140 | 18 | 8 | 3 |
|
||||
| Kubernetes | 83 | 7 | 4 | 7 |
|
||||
| M365 | 44 | 2 | 1 | 0 |
|
||||
| M365 | 44 | 2 | 2 | 0 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
|
||||
> You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
|
||||
> Use the following commands to list Prowler's available checks, services, compliance frameworks, and categories: `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
## Prowler App
|
||||
|
||||
Prowler App can be installed in different ways, depending on your environment:
|
||||
Installing Prowler App
|
||||
Prowler App offers flexible installation methods tailored to various environments:
|
||||
|
||||
> See how to use Prowler App in the [Prowler App Usage Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/).
|
||||
> For detailed instructions on using Prowler App, refer to the [Prowler App Usage Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/).
|
||||
|
||||
### Docker Compose
|
||||
|
||||
@@ -102,8 +118,16 @@ curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/mast
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
> Containers are built for `linux/amd64`.
|
||||
|
||||
### Configuring Your Workstation for Prowler App
|
||||
|
||||
If your workstation's architecture is incompatible, you can resolve this by:
|
||||
|
||||
- **Setting the environment variable**: `DOCKER_DEFAULT_PLATFORM=linux/amd64`
|
||||
- **Using the following flag in your Docker command**: `--platform linux/amd64`
|
||||
|
||||
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
|
||||
|
||||
### From GitHub
|
||||
|
||||
@@ -129,12 +153,12 @@ python manage.py migrate --database admin
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment.
|
||||
> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment.
|
||||
|
||||
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
> After completing the setup, access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
**Commands to run the API Worker**
|
||||
|
||||
@@ -172,29 +196,31 @@ npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
|
||||
|
||||
## Prowler CLI
|
||||
### Pip package
|
||||
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python > 3.9.1, < 3.13:
|
||||
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/). Consequently, it can be installed using pip with Python >3.9.1, <3.13:
|
||||
|
||||
```console
|
||||
pip install prowler
|
||||
prowler -v
|
||||
```
|
||||
>More details at [https://docs.prowler.com](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli-installation)
|
||||
>For further guidance, refer to [https://docs.prowler.com](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli-installation)
|
||||
|
||||
### Containers
|
||||
|
||||
The available versions of Prowler CLI are the following:
|
||||
**Available Versions of Prowler CLI**
|
||||
|
||||
- `latest`: in sync with `master` branch (bear in mind that it is not a stable version)
|
||||
- `v4-latest`: in sync with `v4` branch (bear in mind that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (bear in mind that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v4-stable`: this tag always point to the latest release for v4.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
The following versions of Prowler CLI are available, depending on your requirements:
|
||||
|
||||
- `latest`: Synchronizes with the `master` branch. Note that this version is not stable.
|
||||
- `v4-latest`: Synchronizes with the `v4` branch. Note that this version is not stable.
|
||||
- `v3-latest`: Synchronizes with the `v3` branch. Note that this version is not stable.
|
||||
- `<x.y.z>` (release): Stable releases corresponding to specific versions. You can find the complete list of releases [here](https://github.com/prowler-cloud/prowler/releases).
|
||||
- `stable`: Always points to the latest release.
|
||||
- `v4-stable`: Always points to the latest release for v4.
|
||||
- `v3-stable`: Always points to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
- Prowler CLI:
|
||||
@@ -206,7 +232,7 @@ The container images are available here:
|
||||
|
||||
### From GitHub
|
||||
|
||||
Python > 3.9.1, < 3.13 is required with pip and poetry:
|
||||
Python >3.9.1, <3.13 is required with pip and Poetry:
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
@@ -216,25 +242,46 @@ poetry install
|
||||
python prowler-cli.py -v
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
> To clone Prowler on Windows, configure Git to support long file paths by running the following command: `git config core.longpaths true`.
|
||||
|
||||
> If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
|
||||
# 📐✏️ High level architecture
|
||||
> [!IMPORTANT]
|
||||
> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation.
|
||||
>
|
||||
> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment.
|
||||
> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment.
|
||||
|
||||
# ✏️ High level architecture
|
||||
|
||||
## Prowler App
|
||||
The **Prowler App** consists of three main components:
|
||||
**Prowler App** is composed of three key components:
|
||||
|
||||
- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js.
|
||||
- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework.
|
||||
- **Prowler SDK**: A Python SDK that integrates with the Prowler CLI for advanced functionality.
|
||||
- **Prowler UI**: A web-based interface, built with Next.js, providing a user-friendly experience for executing Prowler scans and visualizing results.
|
||||
- **Prowler API**: A backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
|
||||
- **Prowler SDK**: A Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
|
||||
|
||||

|
||||
|
||||
## Prowler CLI
|
||||
You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more.
|
||||
|
||||
**Running Prowler**
|
||||
|
||||
Prowler can be executed across various environments, offering flexibility to meet your needs. It can be run from:
|
||||
|
||||
- Your own workstation
|
||||
|
||||
- A Kubernetes Job
|
||||
|
||||
- Google Compute Engine
|
||||
|
||||
- Azure Virtual Machines (VMs)
|
||||
|
||||
- Amazon EC2 instances
|
||||
|
||||
- AWS Fargate or other container platforms
|
||||
|
||||
- CloudShell
|
||||
|
||||
And many more environments.
|
||||
|
||||

|
||||
|
||||
@@ -242,23 +289,36 @@ You can run Prowler from your workstation, a Kubernetes Job, a Google Compute En
|
||||
|
||||
## General
|
||||
- `Allowlist` now is called `Mutelist`.
|
||||
- The `--quiet` option has been deprecated, now use the `--status` flag to select the finding's status you want to get from PASS, FAIL or MANUAL.
|
||||
- All `INFO` finding's status has changed to `MANUAL`.
|
||||
- The CSV output format is common for all the providers.
|
||||
- The `--quiet` option has been deprecated. Use the `--status` flag to filter findings based on their status: PASS, FAIL, or MANUAL.
|
||||
- All findings with an `INFO` status have been reclassified as `MANUAL`.
|
||||
- The CSV output format is standardized across all providers.
|
||||
|
||||
We have deprecated some of our outputs formats:
|
||||
- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers.
|
||||
**Deprecated Output Formats**
|
||||
|
||||
The following formats are now deprecated:
|
||||
- Native JSON has been replaced with JSON in [OCSF] v1.1.0 format, which is standardized across all providers (https://schema.ocsf.io/).
|
||||
|
||||
## AWS
|
||||
- Deprecate the AWS flag --sts-endpoint-region since we use AWS STS regional tokens.
|
||||
- To send only FAILS to AWS Security Hub, now use either `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
**AWS Flag Deprecation**
|
||||
|
||||
The flag --sts-endpoint-region has been deprecated due to the adoption of AWS STS regional tokens.
|
||||
|
||||
**Sending FAIL Results to AWS Security Hub**
|
||||
|
||||
- To send only FAILS to AWS Security Hub, use one of the following options: `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
|
||||
# 📖 Documentation
|
||||
|
||||
Install, Usage, Tutorials and Developer Guide is at https://docs.prowler.com/
|
||||
**Documentation Resources**
|
||||
|
||||
For installation instructions, usage details, tutorials, and the Developer Guide, visit https://docs.prowler.com/
|
||||
|
||||
# 📃 License
|
||||
|
||||
Prowler is licensed as Apache License 2.0 as specified in each file. You may obtain a copy of the License at
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>
|
||||
**Prowler License Information**
|
||||
|
||||
Prowler is licensed under the Apache License 2.0, as indicated in each file within the repository. Obtaining a Copy of the License
|
||||
|
||||
A copy of the License is available at <http://www.apache.org/licenses/LICENSE-2.0>
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [v1.8.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Added huge improvements to `/findings/metadata` and resource related filters for findings [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
- Added improvements to `/overviews` endpoints [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
- Added new queue to perform backfill background tasks [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
|
||||
---
|
||||
|
||||
## [v1.7.0] (Prowler v5.6.0)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ start_prod_server() {
|
||||
|
||||
start_worker() {
|
||||
echo "Starting the worker..."
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion -E --max-tasks-per-child 1
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill -E --max-tasks-per-child 1
|
||||
}
|
||||
|
||||
start_worker_beat() {
|
||||
|
||||
@@ -35,7 +35,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -227,6 +227,77 @@ def register_enum(apps, schema_editor, enum_class): # noqa: F841
|
||||
register_adapter(enum_class, enum_adapter)
|
||||
|
||||
|
||||
def create_index_on_partitions(
|
||||
apps, # noqa: F841
|
||||
schema_editor,
|
||||
parent_table: str,
|
||||
index_name: str,
|
||||
columns: str,
|
||||
method: str = "BTREE",
|
||||
where: str = "",
|
||||
):
|
||||
"""
|
||||
Create an index on every existing partition of `parent_table`.
|
||||
|
||||
Args:
|
||||
parent_table: The name of the root table (e.g. "findings").
|
||||
index_name: A short name for the index (will be prefixed per-partition).
|
||||
columns: The parenthesized column list, e.g. "tenant_id, scan_id, status".
|
||||
method: The index method—BTREE, GIN, etc. Defaults to BTREE.
|
||||
where: Optional WHERE clause (without the leading "WHERE"), e.g. "status = 'FAIL'".
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT inhrelid::regclass::text
|
||||
FROM pg_inherits
|
||||
WHERE inhparent = %s::regclass
|
||||
""",
|
||||
[parent_table],
|
||||
)
|
||||
partitions = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
where_sql = f" WHERE {where}" if where else ""
|
||||
for partition in partitions:
|
||||
idx_name = f"{partition.replace('.', '_')}_{index_name}"
|
||||
sql = (
|
||||
f"CREATE INDEX CONCURRENTLY IF NOT EXISTS {idx_name} "
|
||||
f"ON {partition} USING {method} ({columns})"
|
||||
f"{where_sql};"
|
||||
)
|
||||
schema_editor.execute(sql)
|
||||
|
||||
|
||||
def drop_index_on_partitions(
|
||||
apps, # noqa: F841
|
||||
schema_editor,
|
||||
parent_table: str,
|
||||
index_name: str,
|
||||
):
|
||||
"""
|
||||
Drop the per-partition indexes that were created by create_index_on_partitions.
|
||||
|
||||
Args:
|
||||
parent_table: The name of the root table (e.g. "findings").
|
||||
index_name: The same short name used when creating them.
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT inhrelid::regclass::text
|
||||
FROM pg_inherits
|
||||
WHERE inhparent = %s::regclass
|
||||
""",
|
||||
[parent_table],
|
||||
)
|
||||
partitions = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
for partition in partitions:
|
||||
idx_name = f"{partition.replace('.', '_')}_{index_name}"
|
||||
sql = f"DROP INDEX CONCURRENTLY IF EXISTS {idx_name};"
|
||||
schema_editor.execute(sql)
|
||||
|
||||
|
||||
# Postgres enum definition for member role
|
||||
|
||||
|
||||
|
||||
@@ -293,16 +293,16 @@ class FindingFilter(FilterSet):
|
||||
|
||||
resources = UUIDInFilter(field_name="resource__id", lookup_expr="in")
|
||||
|
||||
region = CharFilter(field_name="resources__region")
|
||||
region__in = CharInFilter(field_name="resources__region", lookup_expr="in")
|
||||
region = CharFilter(method="filter_resource_region")
|
||||
region__in = CharInFilter(field_name="resource_regions", lookup_expr="overlap")
|
||||
region__icontains = CharFilter(
|
||||
field_name="resources__region", lookup_expr="icontains"
|
||||
field_name="resource_regions", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
service = CharFilter(field_name="resources__service")
|
||||
service__in = CharInFilter(field_name="resources__service", lookup_expr="in")
|
||||
service = CharFilter(method="filter_resource_service")
|
||||
service__in = CharInFilter(field_name="resource_services", lookup_expr="overlap")
|
||||
service__icontains = CharFilter(
|
||||
field_name="resources__service", lookup_expr="icontains"
|
||||
field_name="resource_services", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_uid = CharFilter(field_name="resources__uid")
|
||||
@@ -317,8 +317,8 @@ class FindingFilter(FilterSet):
|
||||
field_name="resources__name", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_type = CharFilter(field_name="resources__type")
|
||||
resource_type__in = CharInFilter(field_name="resources__type", lookup_expr="in")
|
||||
resource_type = CharFilter(method="filter_resource_type")
|
||||
resource_type__in = CharInFilter(field_name="resource_types", lookup_expr="overlap")
|
||||
resource_type__icontains = CharFilter(
|
||||
field_name="resources__type", lookup_expr="icontains"
|
||||
)
|
||||
@@ -385,6 +385,15 @@ class FindingFilter(FilterSet):
|
||||
},
|
||||
}
|
||||
|
||||
def filter_resource_type(self, queryset, name, value):
|
||||
return queryset.filter(resource_types__contains=[value])
|
||||
|
||||
def filter_resource_region(self, queryset, name, value):
|
||||
return queryset.filter(resource_regions__contains=[value])
|
||||
|
||||
def filter_resource_service(self, queryset, name, value):
|
||||
return queryset.filter(resource_services__contains=[value])
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
|
||||
self.data.get("inserted_at")
|
||||
|
||||
@@ -12,6 +12,7 @@ from api.models import (
|
||||
Provider,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
ResourceScanSummary,
|
||||
Scan,
|
||||
StatusChoices,
|
||||
)
|
||||
@@ -133,6 +134,7 @@ class Command(BaseCommand):
|
||||
region=random.choice(possible_regions),
|
||||
service=random.choice(possible_services),
|
||||
type=random.choice(possible_types),
|
||||
inserted_at="2024-10-01T00:00:00Z",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -181,6 +183,10 @@ class Command(BaseCommand):
|
||||
"servicename": assigned_resource.service,
|
||||
"resourcetype": assigned_resource.type,
|
||||
},
|
||||
resource_types=[assigned_resource.type],
|
||||
resource_regions=[assigned_resource.region],
|
||||
resource_services=[assigned_resource.service],
|
||||
inserted_at="2024-10-01T00:00:00Z",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -197,12 +203,22 @@ class Command(BaseCommand):
|
||||
|
||||
# Create ResourceFindingMapping
|
||||
mappings = []
|
||||
for index, f in enumerate(findings):
|
||||
scan_resource_cache: set[tuple] = set()
|
||||
for index, finding_instance in enumerate(findings):
|
||||
resource_instance = resources[findings_resources_mapping[index]]
|
||||
mappings.append(
|
||||
ResourceFindingMapping(
|
||||
tenant_id=tenant_id,
|
||||
resource=resources[findings_resources_mapping[index]],
|
||||
finding=f,
|
||||
resource=resource_instance,
|
||||
finding=finding_instance,
|
||||
)
|
||||
)
|
||||
scan_resource_cache.add(
|
||||
(
|
||||
str(resource_instance.id),
|
||||
resource_instance.service,
|
||||
resource_instance.region,
|
||||
resource_instance.type,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -220,6 +236,38 @@ class Command(BaseCommand):
|
||||
"Resource-finding mappings created successfully.\n\n"
|
||||
)
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scan.progress = 99
|
||||
scan.save()
|
||||
|
||||
self.stdout.write(self.style.WARNING("Creating finding filter values..."))
|
||||
resource_scan_summaries = [
|
||||
ResourceScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=str(scan.id),
|
||||
resource_id=resource_id,
|
||||
service=service,
|
||||
region=region,
|
||||
resource_type=resource_type,
|
||||
)
|
||||
for resource_id, service, region, resource_type in scan_resource_cache
|
||||
]
|
||||
num_batches = ceil(len(resource_scan_summaries) / batch_size)
|
||||
with rls_transaction(tenant_id):
|
||||
for i in tqdm(
|
||||
range(0, len(resource_scan_summaries), batch_size),
|
||||
total=num_batches,
|
||||
):
|
||||
with rls_transaction(tenant_id):
|
||||
ResourceScanSummary.objects.bulk_create(
|
||||
resource_scan_summaries[i : i + batch_size],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Finding filter values created successfully.\n\n")
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Failed to populate test data: {e}"))
|
||||
scan_state = "failed"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-05 10:01
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid6
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0017_m365_provider"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ResourceScanSummary",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("scan_id", models.UUIDField(db_index=True, default=uuid6.uuid7)),
|
||||
("resource_id", models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
("service", models.CharField(max_length=100)),
|
||||
("region", models.CharField(max_length=100)),
|
||||
("resource_type", models.CharField(max_length=100)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "resource_scan_summaries",
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "service"],
|
||||
name="rss_tenant_scan_svc_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region"],
|
||||
name="rss_tenant_scan_reg_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "resource_type"],
|
||||
name="rss_tenant_scan_type_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region", "service"],
|
||||
name="rss_tenant_scan_reg_svc_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "service", "resource_type"],
|
||||
name="rss_tenant_scan_svc_type_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region", "resource_type"],
|
||||
name="rss_tenant_scan_reg_type_idx",
|
||||
),
|
||||
],
|
||||
"unique_together": {("tenant_id", "scan_id", "resource_id")},
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="resourcescansummary",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_resourcescansummary",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.contrib.postgres.indexes
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0018_resource_scan_summaries"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="finding",
|
||||
name="resource_regions",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=100),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="finding",
|
||||
name="resource_services",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=100),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="finding",
|
||||
name="resource_types",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(max_length=100),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
from functools import partial
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0019_finding_denormalize_resource_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_service_idx",
|
||||
columns="resource_services",
|
||||
method="GIN",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_service_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_region_idx",
|
||||
columns="resource_regions",
|
||||
method="GIN",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_region_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_rtype_idx",
|
||||
columns="resource_types",
|
||||
method="GIN",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="gin_find_rtype_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_uid_idx",
|
||||
),
|
||||
reverse_code=partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_uid_idx",
|
||||
columns="uid",
|
||||
method="BTREE",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_filter_idx",
|
||||
),
|
||||
reverse_code=partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_filter_idx",
|
||||
columns="scan_id, impact, severity, status, check_id, delta",
|
||||
method="BTREE",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
import django.contrib.postgres.indexes
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0020_findings_new_performance_indexes_partitions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["resource_services"], name="gin_find_service_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["resource_regions"], name="gin_find_region_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=django.contrib.postgres.indexes.GinIndex(
|
||||
fields=["resource_types"], name="gin_find_rtype_idx"
|
||||
),
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="finding",
|
||||
name="findings_uid_idx",
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="finding",
|
||||
name="findings_filter_idx",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.1.8 on 2025-05-12 10:04
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0021_findings_new_performance_indexes_parent"),
|
||||
("django_celery_beat", "0019_alter_periodictasks_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name="scan",
|
||||
index=models.Index(
|
||||
condition=models.Q(("state", "completed")),
|
||||
fields=["tenant_id", "provider_id", "state", "-inserted_at"],
|
||||
name="scans_prov_state_ins_desc_idx",
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="scansummary",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id", "service"],
|
||||
name="ss_tenant_scan_service_idx",
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="scansummary",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id", "severity"],
|
||||
name="ss_tenant_scan_severity_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.8 on 2025-05-12 10:18
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0022_scan_summaries_performance_indexes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name="resource",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "id"], name="resources_tenant_id_idx"
|
||||
),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name="resource",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_id"],
|
||||
name="resources_tenant_provider_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,7 @@ from uuid import UUID, uuid4
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector, SearchVectorField
|
||||
from django.core.validators import MinLengthValidator
|
||||
@@ -447,6 +448,11 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
fields=["tenant_id", "provider_id", "state", "inserted_at"],
|
||||
name="scans_prov_state_insert_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_id", "state", "-inserted_at"],
|
||||
condition=Q(state=StateChoices.COMPLETED),
|
||||
name="scans_prov_state_ins_desc_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
@@ -573,6 +579,11 @@ class Resource(RowLevelSecurityProtectedModel):
|
||||
name="resource_tenant_metadata_idx",
|
||||
),
|
||||
GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
|
||||
models.Index(fields=["tenant_id", "id"], name="resources_tenant_id_idx"),
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_id"],
|
||||
name="resources_tenant_provider_idx",
|
||||
),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
@@ -673,6 +684,21 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
muted = models.BooleanField(default=False, null=False)
|
||||
compliance = models.JSONField(default=dict, null=True, blank=True)
|
||||
|
||||
# Denormalize resource data for performance
|
||||
resource_regions = ArrayField(
|
||||
models.CharField(max_length=100), blank=True, null=True
|
||||
)
|
||||
resource_services = ArrayField(
|
||||
models.CharField(max_length=100),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
resource_types = ArrayField(
|
||||
models.CharField(max_length=100),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
# Relationships
|
||||
scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE)
|
||||
|
||||
@@ -713,18 +739,6 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["uid"], name="findings_uid_idx"),
|
||||
models.Index(
|
||||
fields=[
|
||||
"scan_id",
|
||||
"impact",
|
||||
"severity",
|
||||
"status",
|
||||
"check_id",
|
||||
"delta",
|
||||
],
|
||||
name="findings_filter_idx",
|
||||
),
|
||||
models.Index(fields=["tenant_id", "id"], name="findings_tenant_and_id_idx"),
|
||||
GinIndex(fields=["text_search"], name="gin_findings_search_idx"),
|
||||
models.Index(fields=["tenant_id", "scan_id"], name="find_tenant_scan_idx"),
|
||||
@@ -736,19 +750,38 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
condition=Q(delta="new"),
|
||||
name="find_delta_new_idx",
|
||||
),
|
||||
GinIndex(fields=["resource_services"], name="gin_find_service_idx"),
|
||||
GinIndex(fields=["resource_regions"], name="gin_find_region_idx"),
|
||||
GinIndex(fields=["resource_types"], name="gin_find_rtype_idx"),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "findings"
|
||||
|
||||
def add_resources(self, resources: list[Resource] | None):
|
||||
# Add new relationships with the tenant_id field
|
||||
if not resources:
|
||||
return
|
||||
|
||||
self.resource_regions = self.resource_regions or []
|
||||
self.resource_services = self.resource_services or []
|
||||
self.resource_types = self.resource_types or []
|
||||
|
||||
# Deduplication
|
||||
regions = set(self.resource_regions)
|
||||
services = set(self.resource_services)
|
||||
types = set(self.resource_types)
|
||||
|
||||
for resource in resources:
|
||||
ResourceFindingMapping.objects.update_or_create(
|
||||
resource=resource, finding=self, tenant_id=self.tenant_id
|
||||
)
|
||||
regions.add(resource.region)
|
||||
services.add(resource.service)
|
||||
types.add(resource.type)
|
||||
|
||||
# Save the instance
|
||||
self.resource_regions = list(regions)
|
||||
self.resource_services = list(services)
|
||||
self.resource_types = list(types)
|
||||
self.save()
|
||||
|
||||
|
||||
@@ -1150,7 +1183,15 @@ class ScanSummary(RowLevelSecurityProtectedModel):
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id"],
|
||||
name="scan_summaries_tenant_scan_idx",
|
||||
)
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "service"],
|
||||
name="ss_tenant_scan_service_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "severity"],
|
||||
name="ss_tenant_scan_severity_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
@@ -1232,3 +1273,52 @@ class IntegrationProviderRelationship(RowLevelSecurityProtectedModel):
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ResourceScanSummary(RowLevelSecurityProtectedModel):
|
||||
scan_id = models.UUIDField(default=uuid7, db_index=True)
|
||||
resource_id = models.UUIDField(default=uuid4, db_index=True)
|
||||
service = models.CharField(max_length=100)
|
||||
region = models.CharField(max_length=100)
|
||||
resource_type = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
db_table = "resource_scan_summaries"
|
||||
unique_together = (("tenant_id", "scan_id", "resource_id"),)
|
||||
|
||||
indexes = [
|
||||
# Single-dimension lookups:
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "service"],
|
||||
name="rss_tenant_scan_svc_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region"],
|
||||
name="rss_tenant_scan_reg_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "resource_type"],
|
||||
name="rss_tenant_scan_type_idx",
|
||||
),
|
||||
# Two-dimension cross-filters:
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region", "service"],
|
||||
name="rss_tenant_scan_reg_svc_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "service", "resource_type"],
|
||||
name="rss_tenant_scan_svc_type_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "region", "resource_type"],
|
||||
name="rss_tenant_scan_reg_type_idx",
|
||||
),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.7.0
|
||||
version: 1.8.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -4505,7 +4505,7 @@ paths:
|
||||
description: ''
|
||||
/api/v1/scans/{id}/compliance/{name}:
|
||||
get:
|
||||
operationId: scan_compliance_download
|
||||
operationId: scans_compliance_retrieve
|
||||
description: Download a specific compliance report (e.g., 'cis_1.4_aws') as
|
||||
a CSV file.
|
||||
summary: Retrieve compliance report as CSV
|
||||
|
||||
@@ -3096,7 +3096,9 @@ class TestFindingViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_findings_metadata_retrieve(self, authenticated_client, findings_fixture):
|
||||
def test_findings_metadata_retrieve(
|
||||
self, authenticated_client, findings_fixture, backfill_scan_metadata_fixture
|
||||
):
|
||||
finding_1, *_ = findings_fixture
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
@@ -3119,14 +3121,14 @@ class TestFindingViewSet:
|
||||
)
|
||||
# assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
|
||||
def test_findings_metadata_severity_retrieve(
|
||||
self, authenticated_client, findings_fixture
|
||||
def test_findings_metadata_resource_filter_retrieve(
|
||||
self, authenticated_client, findings_fixture, backfill_scan_metadata_fixture
|
||||
):
|
||||
finding_1, *_ = findings_fixture
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
{
|
||||
"filter[severity__in]": ["low", "medium"],
|
||||
"filter[region]": "eu-west-1",
|
||||
"filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"),
|
||||
},
|
||||
)
|
||||
@@ -4814,9 +4816,8 @@ class TestOverviewViewSet:
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == len(
|
||||
resources_fixture
|
||||
)
|
||||
# Since we rely on completed scans, there are only 2 resources now
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == 2
|
||||
|
||||
def test_overview_services_list_no_required_filters(
|
||||
self, authenticated_client, scan_summaries_fixture
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import Subquery
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation, Provider
|
||||
from api.models import Invitation, Provider, Resource
|
||||
from api.v1.serializers import FindingMetadataSerializer
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.common.models import Connection
|
||||
@@ -205,3 +208,33 @@ def validate_invitation(
|
||||
)
|
||||
|
||||
return invitation
|
||||
|
||||
|
||||
# ToRemove after removing the fallback mechanism in /findings/metadata
|
||||
def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
|
||||
filtered_ids = filtered_queryset.order_by().values("id")
|
||||
|
||||
relevant_resources = Resource.all_objects.filter(
|
||||
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
|
||||
).only("service", "region", "type")
|
||||
|
||||
aggregation = relevant_resources.aggregate(
|
||||
services=ArrayAgg("service", flat=True),
|
||||
regions=ArrayAgg("region", flat=True),
|
||||
resource_types=ArrayAgg("type", flat=True),
|
||||
)
|
||||
|
||||
services = sorted(set(aggregation["services"] or []))
|
||||
regions = sorted({region for region in aggregation["regions"] or [] if region})
|
||||
resource_types = sorted(set(aggregation["resource_types"] or []))
|
||||
|
||||
result = {
|
||||
"services": services,
|
||||
"regions": regions,
|
||||
"resource_types": resource_types,
|
||||
}
|
||||
|
||||
serializer = FindingMetadataSerializer(data=result)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return serializer.data
|
||||
@@ -1,5 +1,6 @@
|
||||
import glob
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import sentry_sdk
|
||||
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
|
||||
@@ -20,6 +21,7 @@ from django.db.models import Count, Exists, F, OuterRef, Prefetch, Q, Subquery,
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
@@ -48,6 +50,7 @@ from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
from tasks.beat import schedule_provider_scan
|
||||
from tasks.jobs.export import get_s3_client
|
||||
from tasks.tasks import (
|
||||
backfill_scan_resource_summaries_task,
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
delete_tenant_task,
|
||||
@@ -87,6 +90,7 @@ from api.models import (
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
ResourceScanSummary,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
Scan,
|
||||
@@ -100,7 +104,12 @@ from api.models import (
|
||||
from api.pagination import ComplianceOverviewPagination
|
||||
from api.rbac.permissions import Permissions, get_providers, get_role
|
||||
from api.rls import Tenant
|
||||
from api.utils import CustomOAuth2Client, validate_invitation
|
||||
from api.utils import (
|
||||
CustomOAuth2Client,
|
||||
get_findings_metadata_no_aggregations,
|
||||
validate_invitation,
|
||||
)
|
||||
from api.uuid_utils import datetime_to_uuid7, uuid7_start
|
||||
from api.v1.serializers import (
|
||||
ComplianceOverviewFullSerializer,
|
||||
ComplianceOverviewMetadataSerializer,
|
||||
@@ -249,7 +258,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.7.0"
|
||||
spectacular_settings.VERSION = "1.8.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -1780,25 +1789,103 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="metadata")
|
||||
def metadata(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
# Force filter validation
|
||||
filtered_queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
filtered_ids = filtered_queryset.order_by().values("id")
|
||||
tenant_id = request.tenant_id
|
||||
query_params = request.query_params
|
||||
|
||||
relevant_resources = Resource.all_objects.filter(
|
||||
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
|
||||
).only("service", "region", "type")
|
||||
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
|
||||
scan_based_filters = {}
|
||||
|
||||
aggregation = relevant_resources.aggregate(
|
||||
services=ArrayAgg("service", flat=True),
|
||||
regions=ArrayAgg("region", flat=True),
|
||||
resource_types=ArrayAgg("type", flat=True),
|
||||
if scans := query_params.get("filter[scan__in]") or query_params.get(
|
||||
"filter[scan]"
|
||||
):
|
||||
queryset = queryset.filter(scan_id__in=scans.split(","))
|
||||
scan_based_filters = {"id__in": scans.split(",")}
|
||||
else:
|
||||
exact = query_params.get("filter[inserted_at]")
|
||||
gte = query_params.get("filter[inserted_at__gte]")
|
||||
lte = query_params.get("filter[inserted_at__lte]")
|
||||
|
||||
date_filters = {}
|
||||
if exact:
|
||||
date = parse_date(exact)
|
||||
datetime_start = datetime.combine(
|
||||
date, datetime.min.time(), tzinfo=timezone.utc
|
||||
)
|
||||
datetime_end = datetime_start + timedelta(days=1)
|
||||
date_filters["scan_id__gte"] = uuid7_start(
|
||||
datetime_to_uuid7(datetime_start)
|
||||
)
|
||||
date_filters["scan_id__lt"] = uuid7_start(
|
||||
datetime_to_uuid7(datetime_end)
|
||||
)
|
||||
else:
|
||||
if gte:
|
||||
date_start = parse_date(gte)
|
||||
datetime_start = datetime.combine(
|
||||
date_start, datetime.min.time(), tzinfo=timezone.utc
|
||||
)
|
||||
date_filters["scan_id__gte"] = uuid7_start(
|
||||
datetime_to_uuid7(datetime_start)
|
||||
)
|
||||
if lte:
|
||||
date_end = parse_date(lte)
|
||||
datetime_end = datetime.combine(
|
||||
date_end + timedelta(days=1),
|
||||
datetime.min.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
date_filters["scan_id__lt"] = uuid7_start(
|
||||
datetime_to_uuid7(datetime_end)
|
||||
)
|
||||
|
||||
if date_filters:
|
||||
queryset = queryset.filter(**date_filters)
|
||||
scan_based_filters = {
|
||||
key.lstrip("scan_"): value for key, value in date_filters.items()
|
||||
}
|
||||
|
||||
# ToRemove: Temporary fallback mechanism
|
||||
if not queryset.exists():
|
||||
scan_ids = Scan.objects.filter(
|
||||
tenant_id=tenant_id, **scan_based_filters
|
||||
).values_list("id", flat=True)
|
||||
for scan_id in scan_ids:
|
||||
backfill_scan_resource_summaries_task.apply_async(
|
||||
kwargs={"tenant_id": tenant_id, "scan_id": scan_id}
|
||||
)
|
||||
return Response(
|
||||
get_findings_metadata_no_aggregations(tenant_id, filtered_queryset)
|
||||
)
|
||||
|
||||
if service_filter := query_params.get("filter[service]") or query_params.get(
|
||||
"filter[service__in]"
|
||||
):
|
||||
queryset = queryset.filter(service__in=service_filter.split(","))
|
||||
if region_filter := query_params.get("filter[region]") or query_params.get(
|
||||
"filter[region__in]"
|
||||
):
|
||||
queryset = queryset.filter(region__in=region_filter.split(","))
|
||||
if resource_type_filter := query_params.get(
|
||||
"filter[resource_type]"
|
||||
) or query_params.get("filter[resource_type__in]"):
|
||||
queryset = queryset.filter(
|
||||
resource_type__in=resource_type_filter.split(",")
|
||||
)
|
||||
|
||||
services = list(
|
||||
queryset.values_list("service", flat=True).distinct().order_by("service")
|
||||
)
|
||||
regions = list(
|
||||
queryset.values_list("region", flat=True).distinct().order_by("region")
|
||||
)
|
||||
resource_types = list(
|
||||
queryset.values_list("resource_type", flat=True)
|
||||
.distinct()
|
||||
.order_by("resource_type")
|
||||
)
|
||||
|
||||
services = sorted(set(aggregation["services"] or []))
|
||||
regions = sorted({region for region in aggregation["regions"] or [] if region})
|
||||
resource_types = sorted(set(aggregation["resource_types"] or []))
|
||||
|
||||
result = {
|
||||
"services": services,
|
||||
@@ -1808,7 +1895,7 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
serializer = self.get_serializer(data=result)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
@@ -2375,8 +2462,8 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
def _get_filtered_queryset(model):
|
||||
if role.unlimited_visibility:
|
||||
return model.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return model.objects.filter(
|
||||
return model.all_objects.filter(tenant_id=self.request.tenant_id)
|
||||
return model.all_objects.filter(
|
||||
tenant_id=self.request.tenant_id, scan__provider__in=providers
|
||||
)
|
||||
|
||||
@@ -2420,51 +2507,38 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
tenant_id = self.request.tenant_id
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
)
|
||||
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
findings_aggregated = (
|
||||
ScanSummary.objects.filter(tenant_id=tenant_id, scan_id__in=latest_scan_ids)
|
||||
.values("scan__provider__provider")
|
||||
resource_count_queryset = (
|
||||
Resource.all_objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by()
|
||||
.values("provider_id")
|
||||
.annotate(cnt=Count("id"))
|
||||
.values("cnt")
|
||||
)
|
||||
|
||||
overview_queryset = (
|
||||
ScanSummary.all_objects.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
.values(provider=F("scan__provider__provider"))
|
||||
.annotate(
|
||||
findings_passed=Coalesce(Sum("_pass"), 0),
|
||||
findings_failed=Coalesce(Sum("fail"), 0),
|
||||
findings_muted=Coalesce(Sum("muted"), 0),
|
||||
total_findings=Coalesce(Sum("total"), 0),
|
||||
total_resources=Coalesce(Subquery(resource_count_queryset), 0),
|
||||
)
|
||||
)
|
||||
|
||||
resources_aggregated = (
|
||||
Resource.objects.filter(tenant_id=tenant_id)
|
||||
.values("provider__provider")
|
||||
.annotate(total_resources=Count("id"))
|
||||
)
|
||||
resources_dict = {
|
||||
row["provider__provider"]: row["total_resources"]
|
||||
for row in resources_aggregated
|
||||
}
|
||||
|
||||
overview = []
|
||||
for row in findings_aggregated:
|
||||
provider_type = row["scan__provider__provider"]
|
||||
overview.append(
|
||||
{
|
||||
"provider": provider_type,
|
||||
"total_resources": resources_dict.get(provider_type, 0),
|
||||
"total_findings": row["total_findings"],
|
||||
"findings_passed": row["findings_passed"],
|
||||
"findings_failed": row["findings_failed"],
|
||||
"findings_muted": row["findings_muted"],
|
||||
}
|
||||
)
|
||||
|
||||
serializer = OverviewProviderSerializer(overview, many=True)
|
||||
serializer = OverviewProviderSerializer(overview_queryset, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings")
|
||||
@@ -2473,22 +2547,16 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
annotated_queryset = filtered_queryset.annotate(
|
||||
latest_scan_id=Subquery(latest_scan_subquery)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
|
||||
|
||||
aggregated_totals = filtered_queryset.aggregate(
|
||||
_pass=Sum("_pass") or 0,
|
||||
fail=Sum("fail") or 0,
|
||||
@@ -2518,22 +2586,16 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
annotated_queryset = filtered_queryset.annotate(
|
||||
latest_scan_id=Subquery(latest_scan_subquery)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
|
||||
|
||||
severity_counts = (
|
||||
filtered_queryset.values("severity")
|
||||
.annotate(count=Sum("total"))
|
||||
@@ -2554,22 +2616,16 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
annotated_queryset = filtered_queryset.annotate(
|
||||
latest_scan_id=Subquery(latest_scan_subquery)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
|
||||
|
||||
services_data = (
|
||||
filtered_queryset.values("service")
|
||||
.annotate(_pass=Sum("_pass"))
|
||||
|
||||
@@ -111,6 +111,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
|
||||
],
|
||||
"TITLE": "API Reference - Prowler",
|
||||
}
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from tasks.jobs.backfill import backfill_resource_scan_summaries
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
@@ -920,6 +921,14 @@ def integrations_fixture(providers_fixture):
|
||||
return integration1, integration2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backfill_scan_metadata_fixture(scans_fixture, findings_fixture):
|
||||
for scan_instance in scans_fixture:
|
||||
tenant_id = scan_instance.tenant_id
|
||||
scan_id = scan_instance.id
|
||||
backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
|
||||
def get_authorization_header(access_token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
ResourceScanSummary,
|
||||
Scan,
|
||||
StateChoices,
|
||||
)
|
||||
|
||||
|
||||
def backfill_resource_scan_summaries(tenant_id: str, scan_id: str):
|
||||
with rls_transaction(tenant_id):
|
||||
if ResourceScanSummary.objects.filter(
|
||||
tenant_id=tenant_id, scan_id=scan_id
|
||||
).exists():
|
||||
return {"status": "already backfilled"}
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
if not Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
id=scan_id,
|
||||
state__in=(StateChoices.COMPLETED, StateChoices.FAILED),
|
||||
).exists():
|
||||
return {"status": "scan is not completed"}
|
||||
|
||||
resource_ids_qs = (
|
||||
ResourceFindingMapping.objects.filter(
|
||||
tenant_id=tenant_id, finding__scan_id=scan_id
|
||||
)
|
||||
.values_list("resource_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
resource_ids = list(resource_ids_qs)
|
||||
|
||||
if not resource_ids:
|
||||
return {"status": "no resources to backfill"}
|
||||
|
||||
resources_qs = Resource.objects.filter(
|
||||
tenant_id=tenant_id, id__in=resource_ids
|
||||
).only("id", "service", "region", "type")
|
||||
|
||||
summaries = []
|
||||
for resource in resources_qs.iterator():
|
||||
summaries.append(
|
||||
ResourceScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
resource_id=str(resource.id),
|
||||
service=resource.service,
|
||||
region=resource.region,
|
||||
resource_type=resource.type,
|
||||
)
|
||||
)
|
||||
|
||||
for i in range(0, len(summaries), 500):
|
||||
ResourceScanSummary.objects.bulk_create(
|
||||
summaries[i : i + 500], ignore_conflicts=True
|
||||
)
|
||||
|
||||
return {"status": "backfilled", "inserted": len(summaries)}
|
||||
@@ -19,6 +19,7 @@ from api.models import (
|
||||
Finding,
|
||||
Provider,
|
||||
Resource,
|
||||
ResourceScanSummary,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
@@ -121,6 +122,7 @@ def perform_prowler_scan(
|
||||
check_status_by_region = {}
|
||||
exception = None
|
||||
unique_resources = set()
|
||||
scan_resource_cache: set[tuple[str, str, str, str]] = set()
|
||||
start_time = time.time()
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
@@ -295,6 +297,16 @@ def perform_prowler_scan(
|
||||
continue
|
||||
region_dict[finding.check_id] = finding.status.value
|
||||
|
||||
# Update scan resource summaries
|
||||
scan_resource_cache.add(
|
||||
(
|
||||
str(resource_instance.id),
|
||||
resource_instance.service,
|
||||
resource_instance.region,
|
||||
resource_instance.type,
|
||||
)
|
||||
)
|
||||
|
||||
# Update scan progress
|
||||
with rls_transaction(tenant_id):
|
||||
scan_instance.progress = progress
|
||||
@@ -314,66 +326,90 @@ def perform_prowler_scan(
|
||||
scan_instance.unique_resource_count = len(unique_resources)
|
||||
scan_instance.save()
|
||||
|
||||
if exception is None:
|
||||
try:
|
||||
regions = prowler_provider.get_regions()
|
||||
except AttributeError:
|
||||
regions = set()
|
||||
|
||||
compliance_template = PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE[
|
||||
provider_instance.provider
|
||||
]
|
||||
compliance_overview_by_region = {
|
||||
region: deepcopy(compliance_template) for region in regions
|
||||
}
|
||||
|
||||
for region, check_status in check_status_by_region.items():
|
||||
compliance_data = compliance_overview_by_region.setdefault(
|
||||
region, deepcopy(compliance_template)
|
||||
)
|
||||
for check_name, status in check_status.items():
|
||||
generate_scan_compliance(
|
||||
compliance_data,
|
||||
provider_instance.provider,
|
||||
check_name,
|
||||
status,
|
||||
)
|
||||
|
||||
# Prepare compliance overview objects
|
||||
compliance_overview_objects = []
|
||||
for region, compliance_data in compliance_overview_by_region.items():
|
||||
for compliance_id, compliance in compliance_data.items():
|
||||
compliance_overview_objects.append(
|
||||
ComplianceOverview(
|
||||
tenant_id=tenant_id,
|
||||
scan=scan_instance,
|
||||
region=region,
|
||||
compliance_id=compliance_id,
|
||||
framework=compliance["framework"],
|
||||
version=compliance["version"],
|
||||
description=compliance["description"],
|
||||
requirements=compliance["requirements"],
|
||||
requirements_passed=compliance["requirements_status"]["passed"],
|
||||
requirements_failed=compliance["requirements_status"]["failed"],
|
||||
requirements_manual=compliance["requirements_status"]["manual"],
|
||||
total_requirements=compliance["total_requirements"],
|
||||
)
|
||||
)
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(
|
||||
compliance_overview_objects, batch_size=100
|
||||
)
|
||||
except Exception as overview_exception:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(overview_exception)
|
||||
logger.error(
|
||||
f"Error storing compliance overview for scan {scan_id}: {overview_exception}"
|
||||
)
|
||||
if exception is not None:
|
||||
raise exception
|
||||
|
||||
try:
|
||||
regions = prowler_provider.get_regions()
|
||||
except AttributeError:
|
||||
regions = set()
|
||||
|
||||
compliance_template = PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE[
|
||||
provider_instance.provider
|
||||
]
|
||||
compliance_overview_by_region = {
|
||||
region: deepcopy(compliance_template) for region in regions
|
||||
}
|
||||
|
||||
for region, check_status in check_status_by_region.items():
|
||||
compliance_data = compliance_overview_by_region.setdefault(
|
||||
region, deepcopy(compliance_template)
|
||||
)
|
||||
for check_name, status in check_status.items():
|
||||
generate_scan_compliance(
|
||||
compliance_data,
|
||||
provider_instance.provider,
|
||||
check_name,
|
||||
status,
|
||||
)
|
||||
|
||||
# Prepare compliance overview objects
|
||||
compliance_overview_objects = []
|
||||
for region, compliance_data in compliance_overview_by_region.items():
|
||||
for compliance_id, compliance in compliance_data.items():
|
||||
compliance_overview_objects.append(
|
||||
ComplianceOverview(
|
||||
tenant_id=tenant_id,
|
||||
scan=scan_instance,
|
||||
region=region,
|
||||
compliance_id=compliance_id,
|
||||
framework=compliance["framework"],
|
||||
version=compliance["version"],
|
||||
description=compliance["description"],
|
||||
requirements=compliance["requirements"],
|
||||
requirements_passed=compliance["requirements_status"]["passed"],
|
||||
requirements_failed=compliance["requirements_status"]["failed"],
|
||||
requirements_manual=compliance["requirements_status"]["manual"],
|
||||
total_requirements=compliance["total_requirements"],
|
||||
)
|
||||
)
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(
|
||||
compliance_overview_objects, batch_size=500
|
||||
)
|
||||
except Exception as overview_exception:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(overview_exception)
|
||||
logger.error(
|
||||
f"Error storing compliance overview for scan {scan_id}: {overview_exception}"
|
||||
)
|
||||
|
||||
try:
|
||||
resource_scan_summaries = [
|
||||
ResourceScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
resource_id=resource_id,
|
||||
service=service,
|
||||
region=region,
|
||||
resource_type=resource_type,
|
||||
)
|
||||
for resource_id, service, region, resource_type in scan_resource_cache
|
||||
]
|
||||
with rls_transaction(tenant_id):
|
||||
ResourceScanSummary.objects.bulk_create(
|
||||
resource_scan_summaries, batch_size=500, ignore_conflicts=True
|
||||
)
|
||||
except Exception as filter_exception:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(filter_exception)
|
||||
logger.error(
|
||||
f"Error storing filter values for scan {scan_id}: {filter_exception}"
|
||||
)
|
||||
|
||||
serializer = ScanTaskSerializer(instance=scan_instance)
|
||||
return serializer.data
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from celery.utils.log import get_task_logger
|
||||
from config.celery import RLSTask
|
||||
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from tasks.jobs.backfill import backfill_resource_scan_summaries
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
from tasks.jobs.export import (
|
||||
@@ -358,3 +359,15 @@ def generate_outputs(scan_id: str, provider_id: str, tenant_id: str):
|
||||
Scan.all_objects.filter(id=scan_id).update(output_location=final_location)
|
||||
logger.info(f"Scan outputs at {final_location}")
|
||||
return {"upload": did_upload}
|
||||
|
||||
|
||||
@shared_task(name="backfill-scan-resource-summaries", queue="backfill")
|
||||
def backfill_scan_resource_summaries_task(tenant_id: str, scan_id: str):
|
||||
"""
|
||||
Tries to backfill the resource scan summaries table for a given scan.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier.
|
||||
scan_id (str): The scan identifier.
|
||||
"""
|
||||
return backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id)
|
||||
@@ -0,0 +1,79 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from tasks.jobs.backfill import backfill_resource_scan_summaries
|
||||
|
||||
from api.models import ResourceScanSummary, Scan, StateChoices
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestBackfillResourceScanSummaries:
|
||||
@pytest.fixture(scope="function")
|
||||
def resource_scan_summary_data(self, scans_fixture):
|
||||
scan = scans_fixture[0]
|
||||
return ResourceScanSummary.objects.create(
|
||||
tenant_id=scan.tenant_id,
|
||||
scan_id=scan.id,
|
||||
resource_id=str(uuid4()),
|
||||
service="aws",
|
||||
region="us-east-1",
|
||||
resource_type="instance",
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def get_not_completed_scans(self, providers_fixture):
|
||||
provider_id = providers_fixture[0].id
|
||||
tenant_id = providers_fixture[0].tenant_id
|
||||
scan_1 = Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.EXECUTING,
|
||||
provider_id=provider_id,
|
||||
)
|
||||
scan_2 = Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
provider_id=provider_id,
|
||||
)
|
||||
return scan_1, scan_2
|
||||
|
||||
def test_already_backfilled(self, resource_scan_summary_data):
|
||||
tenant_id = resource_scan_summary_data.tenant_id
|
||||
scan_id = resource_scan_summary_data.scan_id
|
||||
|
||||
result = backfill_resource_scan_summaries(tenant_id, scan_id)
|
||||
|
||||
assert result == {"status": "already backfilled"}
|
||||
|
||||
def test_not_completed_scan(self, get_not_completed_scans):
|
||||
for scan_instance in get_not_completed_scans:
|
||||
tenant_id = scan_instance.tenant_id
|
||||
scan_id = scan_instance.id
|
||||
result = backfill_resource_scan_summaries(tenant_id, scan_id)
|
||||
|
||||
assert result == {"status": "scan is not completed"}
|
||||
|
||||
def test_successful_backfill_inserts_one_summary(
|
||||
self, resources_fixture, findings_fixture
|
||||
):
|
||||
tenant_id = findings_fixture[0].tenant_id
|
||||
scan_id = findings_fixture[0].scan_id
|
||||
|
||||
# This scan affects the first two resources
|
||||
resources = resources_fixture[:2]
|
||||
|
||||
result = backfill_resource_scan_summaries(tenant_id, scan_id)
|
||||
assert result == {"status": "backfilled", "inserted": len(resources)}
|
||||
|
||||
# Verify correct values
|
||||
summaries = ResourceScanSummary.objects.filter(
|
||||
tenant_id=tenant_id, scan_id=scan_id
|
||||
)
|
||||
assert summaries.count() == len(resources)
|
||||
for resource in resources:
|
||||
summary = summaries.get(resource_id=resource.id)
|
||||
assert summary.resource_id == resource.id
|
||||
assert summary.service == resource.service
|
||||
assert summary.region == resource.region
|
||||
assert summary.resource_type == resource.type
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -122,8 +123,7 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
|
||||
metrics_dir = Path(args.metrics_dir)
|
||||
if not metrics_dir.is_dir():
|
||||
sys.exit(f"Metrics directory not found: {metrics_dir}")
|
||||
os.makedirs(metrics_dir, exist_ok=True)
|
||||
|
||||
metrics_data: dict[str, pd.DataFrame] = {}
|
||||
for csv_file in sorted(metrics_dir.glob("*.csv")):
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
locust==2.34.1
|
||||
matplotlib==3.10.1
|
||||
pandas==2.2.3
|
||||
@@ -180,13 +180,27 @@ class APIUser(APIUserBase):
|
||||
)
|
||||
|
||||
endpoint = (
|
||||
f"/findings?filter[{filter_name}]={filter_value}"
|
||||
f"/findings/metadata?filter[{filter_name}]={filter_value}"
|
||||
f"&filter[inserted_at]={TARGET_INSERTED_AT}"
|
||||
f"&{get_sort_value(FINDINGS_UI_SORT_VALUES)}"
|
||||
)
|
||||
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
|
||||
|
||||
@task
|
||||
@task(3)
|
||||
def findings_metadata_resource_filter_scan_large(self):
|
||||
name = "/findings/metadata?filter[resource_filter]&filter[scan_id] - 500k"
|
||||
filter_name, filter_value = get_next_resource_filter(
|
||||
self.available_resource_filters
|
||||
)
|
||||
|
||||
endpoint = (
|
||||
f"/findings/metadata?filter[{filter_name}]={filter_value}"
|
||||
f"&filter[scan]={self.l_scan_id}"
|
||||
f"&{get_sort_value(FINDINGS_UI_SORT_VALUES)}"
|
||||
)
|
||||
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
|
||||
|
||||
@task(2)
|
||||
def findings_resource_filter_large_scan_include(self):
|
||||
name = "/findings?filter[resource_filter][scan]&include - 500k"
|
||||
filter_name, filter_value = get_next_resource_filter(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -23,55 +23,7 @@ In order to see which compliance frameworks are cover by Prowler, you can use op
|
||||
prowler <provider> --list-compliance
|
||||
```
|
||||
|
||||
### AWS
|
||||
|
||||
- `aws_account_security_onboarding_aws`
|
||||
- `aws_audit_manager_control_tower_guardrails_aws`
|
||||
- `aws_foundational_security_best_practices_aws`
|
||||
- `aws_foundational_technical_review_aws`
|
||||
- `aws_well_architected_framework_reliability_pillar_aws`
|
||||
- `aws_well_architected_framework_security_pillar_aws`
|
||||
- `cis_1.4_aws`
|
||||
- `cis_1.5_aws`
|
||||
- `cis_2.0_aws`
|
||||
- `cis_3.0_aws`
|
||||
- `cisa_aws`
|
||||
- `ens_rd2022_aws`
|
||||
- `fedramp_low_revision_4_aws`
|
||||
- `fedramp_moderate_revision_4_aws`
|
||||
- `ffiec_aws`
|
||||
- `gdpr_aws`
|
||||
- `gxp_21_cfr_part_11_aws`
|
||||
- `gxp_eu_annex_11_aws`
|
||||
- `hipaa_aws`
|
||||
- `iso27001_2013_aws`
|
||||
- `kisa_isms_p_2023_aws`
|
||||
- `kisa_isms_p_2023_korean_aws`
|
||||
- `mitre_attack_aws`
|
||||
- `nist_800_171_revision_2_aws`
|
||||
- `nist_800_53_revision_4_aws`
|
||||
- `nist_800_53_revision_5_aws`
|
||||
- `nist_csf_1.1_aws`
|
||||
- `pci_3.2.1_aws`
|
||||
- `rbi_cyber_security_framework_aws`
|
||||
- `soc2_aws`
|
||||
|
||||
### Azure
|
||||
|
||||
- `cis_2.0_azure`
|
||||
- `cis_2.1_azure`
|
||||
- `ens_rd2022_azure`
|
||||
- `mitre_attack_azure`
|
||||
|
||||
### GCP
|
||||
|
||||
- `cis_2.0_gcp`
|
||||
- `ens_rd2022_gcp`
|
||||
- `mitre_attack_gcp`
|
||||
|
||||
### Kubernetes
|
||||
|
||||
- `cis_1.8_kubernetes`
|
||||
The full and updated list of supported compliance frameworks for each provider is available at [Prowler Hub](https://hub.prowler.com/compliance).
|
||||
|
||||
## List Requirements of Compliance Frameworks
|
||||
For each compliance framework, you can use option `--list-compliance-requirements` to list its requirements:
|
||||
|
||||
@@ -20,3 +20,19 @@ kubectl logs prowler-XXXXX --namespace prowler-ns
|
||||
|
||||
???+ note
|
||||
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the [`--namespace`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/namespace/) flag to specify the namespace(s) to be scanned.
|
||||
|
||||
???+ tip "Identifying the cluster in reports"
|
||||
When running in in-cluster mode, the Kubernetes API does not expose the actual cluster name by default.
|
||||
|
||||
To uniquely identify the cluster in logs and reports, you can:
|
||||
|
||||
- Use the `--cluster-name` flag to manually set the cluster name:
|
||||
```bash
|
||||
prowler -p kubernetes --cluster-name production-cluster
|
||||
```
|
||||
- Or set the `CLUSTER_NAME` environment variable:
|
||||
```yaml
|
||||
env:
|
||||
- name: CLUSTER_NAME
|
||||
value: production-cluster
|
||||
```
|
||||
@@ -194,15 +194,45 @@ To view all `new` findings that have not been seen prior to this scan, click the
|
||||
|
||||
## **Step 9: Download the Outputs**
|
||||
|
||||
Once the scan is complete, you can download the output files generated by Prowler as a single `zip` file. This archive contains the CSV, JSON-OSCF, and HTML reports detailing the findings.
|
||||
Once a scan is complete, navigate to the Scan Jobs section to download the output files generated by Prowler:
|
||||
|
||||
To download these files, click the **Download** button. This button becomes available only after the scan has finished.
|
||||
<img src="../../img/scan_jobs_section.png" alt="Scan Jobs section" width="700"/>
|
||||
|
||||
These outputs are bundled into a single .zip archive containing:
|
||||
|
||||
- CSV report
|
||||
|
||||
- JSON-OSCF formatted results
|
||||
|
||||
- HTML report
|
||||
|
||||
- A folder with individual compliance reports
|
||||
|
||||
???+ note "Note"
|
||||
The Download button only becomes active after a scan completes successfully.
|
||||
|
||||
<img src="../../img/download_output.png" alt="Download output" width="700"/>
|
||||
|
||||
This action downloads a `zip` file containing an `output` folder, which includes the files mentioned above: CSV, JSON-OSCF, and HTML reports.
|
||||
The `zip` file unpacks into a folder named like `prowler-output-<provider_id>-<timestamp>`, which includes all of the above outputs. In the example below, you can see the `.csv`, .`json`, and `.html` reports alongside a subfolder for detailed compliance checks.
|
||||
|
||||
<img src="../../img/output_folder.png" alt="Output folder" width="700"/>
|
||||
|
||||
???+ note "API Note"
|
||||
To learn more about the API endpoint the UI uses to download ZIP exports, see: [Prowler API Reference - Download Scan Output](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_report_retrieve)
|
||||
For more information about the API endpoint used by the UI to download the ZIP archive, refer to: [Prowler API Reference - Download Scan Output](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_report_retrieve)
|
||||
|
||||
## **Step 10: Download specified compliance report**
|
||||
|
||||
Once your scan has finished, you don’t need to grab the entire ZIP—just pull down the specific compliance report you want:
|
||||
|
||||
- Navigate to the **Compliance** section of the UI.
|
||||
|
||||
<img src="../../img/compliance_section.png" alt="Compliance section" width="700"/>
|
||||
|
||||
- Find the Framework report you need.
|
||||
|
||||
- Click its **Download** icon to retrieve that report’s CSV file with all the detailed findings.
|
||||
|
||||
<img src="../../img/compliance_download.png" alt="Download compliance output" width="700"/>
|
||||
|
||||
???+ note "API Note"
|
||||
To fetch a single compliance report via API, see the Retrieve compliance report as CSV endpoint in the Prowler API Reference.[Prowler API Reference - Retrieve compliance report as CSV](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_compliance_retrieve)
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.7.0] (Prowler v5.7.0)
|
||||
|
||||
### Added
|
||||
- Update the compliance list supported for each provider from docs. [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
|
||||
- Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
|
||||
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
|
||||
### Fixed
|
||||
- Update CIS 4.0 for M365 provider. [(#7699)](https://github.com/prowler-cloud/prowler/pull/7699)
|
||||
|
||||
---
|
||||
|
||||
## [v5.6.0] (Prowler v5.6.0)
|
||||
|
||||
### Added
|
||||
@@ -51,6 +63,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Modified check `exchange_mailbox_properties_auditing_enabled` to make it configurable. [(#7662)](https://github.com/prowler-cloud/prowler/pull/7662)
|
||||
- Add snapshots to m365 documentation. [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
|
||||
- Add support for static credentials for sending findings to Amazon S3 and AWS Security Hub. [(#7322)](https://github.com/prowler-cloud/prowler/pull/7322)
|
||||
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Add Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azur
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
|
||||
ProwlerThreatScoreGCP,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
|
||||
ProwlerThreatScoreM365,
|
||||
)
|
||||
from prowler.lib.outputs.csv.csv import CSV
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.html.html import HTML
|
||||
@@ -726,6 +729,18 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
cis.batch_write_data_to_file()
|
||||
elif compliance_name == "prowler_threatscore_m365":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
prowler_threatscore = ProwlerThreatScoreM365(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(prowler_threatscore)
|
||||
prowler_threatscore.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.6.0"
|
||||
prowler_version = "5.7.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -79,3 +79,29 @@ class ProwlerThreatScoreGCPModel(BaseModel):
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class ProwlerThreatScoreM365Model(BaseModel):
|
||||
"""
|
||||
ProwlerThreatScoreM365Model generates a finding's output in M365 Prowler ThreatScore Compliance format.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
TenantId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Attributes_Title: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_SubSection: Optional[str]
|
||||
Requirements_Attributes_AttributeDescription: str
|
||||
Requirements_Attributes_AdditionalInformation: str
|
||||
Requirements_Attributes_LevelOfRisk: int
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
@@ -0,0 +1,91 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
ProwlerThreatScoreM365Model,
|
||||
)
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class ProwlerThreatScoreM365(ComplianceOutput):
|
||||
"""
|
||||
This class represents the M365 Prowler ThreatScore 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 M365 Prowler ThreatScore compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into M365 Prowler ThreatScore 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 = ProwlerThreatScoreM365Model(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
TenantId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Title=attribute.Title,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_AttributeDescription=attribute.AttributeDescription,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
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 = ProwlerThreatScoreM365Model(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
TenantId="",
|
||||
Location="",
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Title=attribute.Title,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_AttributeDescription=attribute.AttributeDescription,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -581,6 +581,9 @@ class HTML(Output):
|
||||
<li class="list-group-item">
|
||||
<b>M365 Identity ID:</b> {provider.identity.identity_id}
|
||||
</li>
|
||||
{f'''<li class="list-group-item">
|
||||
<b>M365 User:</b> {provider.identity.user}
|
||||
</li>''' if hasattr(provider.identity, 'user') and provider.identity.user is not None else ""}
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
@@ -750,6 +750,7 @@
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -1989,10 +1990,13 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
@@ -2000,6 +2004,7 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -2845,6 +2850,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -2858,6 +2864,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -3342,6 +3349,7 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
@@ -4203,6 +4211,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
@@ -4682,6 +4691,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4695,6 +4705,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -5129,6 +5140,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -5265,6 +5277,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5918,7 +5931,10 @@
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
@@ -8249,6 +8265,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -11133,9 +11150,20 @@
|
||||
},
|
||||
"timestream-query": {
|
||||
"regions": {
|
||||
"aws": [],
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestream-write": {
|
||||
@@ -11893,6 +11921,7 @@
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
|
||||
@@ -207,6 +207,7 @@ class Provider(ABC):
|
||||
kubeconfig_file=arguments.kubeconfig_file,
|
||||
context=arguments.context,
|
||||
namespace=arguments.namespace,
|
||||
cluster_name=arguments.cluster_name,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
|
||||
@@ -70,6 +70,7 @@ class KubernetesProvider(Provider):
|
||||
kubeconfig_file: str = None,
|
||||
context: str = None,
|
||||
namespace: list = None,
|
||||
cluster_name: str = None,
|
||||
config_path: str = None,
|
||||
config_content: dict = {},
|
||||
fixer_config: dict = {},
|
||||
@@ -84,6 +85,7 @@ class KubernetesProvider(Provider):
|
||||
kubeconfig_file (str): Path to the kubeconfig file.
|
||||
kubeconfig_content (str or dict): Content of the kubeconfig file.
|
||||
context (str): Context name.
|
||||
cluster_name (str): Cluster name.
|
||||
namespace (list): List of namespaces.
|
||||
config_content (dict): Audit configuration.
|
||||
config_path (str): Path to the configuration file.
|
||||
@@ -147,7 +149,9 @@ class KubernetesProvider(Provider):
|
||||
"""
|
||||
|
||||
logger.info("Instantiating Kubernetes Provider ...")
|
||||
self._session = self.setup_session(kubeconfig_file, kubeconfig_content, context)
|
||||
self._session = self.setup_session(
|
||||
kubeconfig_file, kubeconfig_content, context, cluster_name
|
||||
)
|
||||
if not namespace:
|
||||
logger.info("Retrieving all namespaces ...")
|
||||
self._namespaces = self.get_all_namespaces()
|
||||
@@ -227,6 +231,7 @@ class KubernetesProvider(Provider):
|
||||
kubeconfig_file: str = None,
|
||||
kubeconfig_content: Union[dict, str] = None,
|
||||
context: str = None,
|
||||
cluster_name: str = None,
|
||||
) -> KubernetesSession:
|
||||
"""
|
||||
Sets up the Kubernetes session.
|
||||
@@ -235,7 +240,7 @@ class KubernetesProvider(Provider):
|
||||
kubeconfig_file (str): Path to the kubeconfig file.
|
||||
kubeconfig_content (str or dict): Content of the kubeconfig file.
|
||||
context (str): Context name.
|
||||
|
||||
cluster_name (str): Cluster name.
|
||||
Returns:
|
||||
Tuple: A tuple containing the API client and the context.
|
||||
|
||||
@@ -270,11 +275,15 @@ class KubernetesProvider(Provider):
|
||||
# If the kubeconfig file is not found, try to use the in-cluster config
|
||||
logger.info("Using in-cluster config")
|
||||
config.load_incluster_config()
|
||||
# Use CLI flag or env var to set cluster name
|
||||
resolved_cluster_name = cluster_name or os.getenv(
|
||||
"CLUSTER_NAME", "in-cluster"
|
||||
)
|
||||
context = {
|
||||
"name": "In-Cluster",
|
||||
"context": {
|
||||
"cluster": "in-cluster", # Placeholder, as the real cluster name is not available
|
||||
"user": "service-account-name", # Also a placeholder
|
||||
"cluster": resolved_cluster_name,
|
||||
"user": "service-account-name",
|
||||
},
|
||||
}
|
||||
return KubernetesSession(
|
||||
|
||||
@@ -27,3 +27,9 @@ def init_parser(self):
|
||||
metavar="NAMESPACES",
|
||||
help="The namespaces where to scan for the Kubernetes resources. By default, Prowler will scan all namespaces available.",
|
||||
)
|
||||
k8s_auth_subparser.add_argument(
|
||||
"--cluster-name",
|
||||
nargs="?",
|
||||
metavar="CLUSTER_NAME",
|
||||
help="Manually specify the cluster name in in-cluster mode, by default it will be 'in-cluster'",
|
||||
)
|
||||
@@ -98,21 +98,21 @@ class M365BaseException(ProwlerException):
|
||||
"message": "User and Password environment variables are needed to use Credentials authentication method.",
|
||||
"remediation": "Ensure your environment variables are properly set up.",
|
||||
},
|
||||
(6023, "M365EnvironmentUserCredentialsError"): {
|
||||
"message": "User or Password environment variables are not correct.",
|
||||
"remediation": "Ensure you are using the right credentials.",
|
||||
(6023, "M365UserCredentialsError"): {
|
||||
"message": "The provided User credentials are not valid.",
|
||||
"remediation": "Check the User credentials and ensure they are valid.",
|
||||
},
|
||||
(6024, "M365NotValidUserError"): {
|
||||
"message": "The provided M365 User is not valid.",
|
||||
"remediation": "Check the M365 User and ensure it is a valid user.",
|
||||
"message": "The provided User is not valid.",
|
||||
"remediation": "Check the User and ensure it is a valid user.",
|
||||
},
|
||||
(6025, "M365NotValidEncryptedPasswordError"): {
|
||||
"message": "The provided M365 Encrypted Password is not valid.",
|
||||
"remediation": "Check the M365 Encrypted Password and ensure it is a valid password.",
|
||||
"message": "The provided Encrypted Password is not valid.",
|
||||
"remediation": "Check the Encrypted Password and ensure it is a valid password.",
|
||||
},
|
||||
(6026, "M365UserNotBelongingToTenantError"): {
|
||||
"message": "The provided M365 User does not belong to the specified tenant.",
|
||||
"remediation": "Check the M365 User email domain and ensure it belongs to the specified tenant.",
|
||||
"message": "The provided User does not belong to the specified tenant.",
|
||||
"remediation": "Check the User email domain and ensure it belongs to the specified tenant.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ class M365MissingEnvironmentCredentialsError(M365CredentialsError):
|
||||
)
|
||||
|
||||
|
||||
class M365EnvironmentUserCredentialsError(M365CredentialsError):
|
||||
class M365UserCredentialsError(M365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6023, file=file, original_exception=original_exception, message=message
|
||||
|
||||
@@ -525,6 +525,26 @@ class M365PowerShell(PowerShellSession):
|
||||
"Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True
|
||||
)
|
||||
|
||||
def get_inbound_spam_filter_rule(self) -> dict:
|
||||
"""
|
||||
Get Inbound Spam Filter Rule.
|
||||
|
||||
Retrieves the current inbound spam filter rule settings for Exchange Online.
|
||||
|
||||
Returns:
|
||||
dict: Inbound spam filter rule settings in JSON format.
|
||||
|
||||
Example:
|
||||
>>> get_inbound_spam_filter_rule()
|
||||
{
|
||||
"Name": "Rule1",
|
||||
"State": "Enabled"
|
||||
}
|
||||
"""
|
||||
return self.execute(
|
||||
"Get-HostedContentFilterRule | ConvertTo-Json", json_parse=True
|
||||
)
|
||||
|
||||
def get_report_submission_policy(self) -> dict:
|
||||
"""
|
||||
Get Exchange Online Report Submission Policy.
|
||||
|
||||
@@ -33,7 +33,6 @@ from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365ConfigCredentialsError,
|
||||
M365CredentialsUnavailableError,
|
||||
M365DefaultAzureCredentialError,
|
||||
M365EnvironmentUserCredentialsError,
|
||||
M365EnvironmentVariableError,
|
||||
M365GetTokenIdentityError,
|
||||
M365HTTPResponseError,
|
||||
@@ -51,6 +50,7 @@ from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365SetUpSessionError,
|
||||
M365TenantIdAndClientIdNotBelongingToClientSecretError,
|
||||
M365TenantIdAndClientSecretNotBelongingToClientIdError,
|
||||
M365UserCredentialsError,
|
||||
)
|
||||
from prowler.providers.m365.lib.mutelist.mutelist import M365Mutelist
|
||||
from prowler.providers.m365.lib.powershell.m365_powershell import (
|
||||
@@ -207,6 +207,7 @@ class M365Provider(Provider):
|
||||
m365_credentials=m365_credentials,
|
||||
provider_id=self.identity.tenant_domain,
|
||||
init_modules=init_modules,
|
||||
identity=self.identity,
|
||||
)
|
||||
|
||||
# Audit Config
|
||||
@@ -382,6 +383,7 @@ class M365Provider(Provider):
|
||||
m365_credentials: dict = {},
|
||||
provider_id: str = None,
|
||||
init_modules: bool = False,
|
||||
identity: M365IdentityInfo = None,
|
||||
) -> M365Credentials:
|
||||
"""Gets the M365 credentials.
|
||||
|
||||
@@ -394,6 +396,7 @@ class M365Provider(Provider):
|
||||
If False, returns empty credentials.
|
||||
"""
|
||||
credentials = None
|
||||
|
||||
if m365_credentials:
|
||||
credentials = M365Credentials(
|
||||
user=m365_credentials.get("user", ""),
|
||||
@@ -428,15 +431,18 @@ class M365Provider(Provider):
|
||||
)
|
||||
|
||||
if credentials:
|
||||
if identity:
|
||||
identity.identity_type = "Service Principal and User Credentials"
|
||||
identity.user = credentials.user
|
||||
test_session = M365PowerShell(credentials)
|
||||
try:
|
||||
if test_session.test_credentials(credentials):
|
||||
if init_modules:
|
||||
initialize_m365_powershell_modules()
|
||||
return credentials
|
||||
raise M365EnvironmentUserCredentialsError(
|
||||
raise M365UserCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message="M365_USER or M365_ENCRYPTED_PASSWORD environment variables are not correct. Please ensure you are using the right credentials.",
|
||||
message="The provided User credentials are not valid.",
|
||||
)
|
||||
finally:
|
||||
test_session.close()
|
||||
@@ -994,7 +1000,7 @@ class M365Provider(Provider):
|
||||
except ValueError:
|
||||
raise M365NotValidTenantIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Tenant ID is not valid.",
|
||||
message="The provided Tenant ID is not valid.",
|
||||
)
|
||||
|
||||
# Validate the Client ID
|
||||
@@ -1003,28 +1009,28 @@ class M365Provider(Provider):
|
||||
except ValueError:
|
||||
raise M365NotValidClientIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Client ID is not valid.",
|
||||
message="The provided Client ID is not valid.",
|
||||
)
|
||||
|
||||
# Validate the Client Secret
|
||||
if not client_secret:
|
||||
raise M365NotValidClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Client Secret is not valid.",
|
||||
message="The provided Client Secret is not valid.",
|
||||
)
|
||||
|
||||
# Validate the User
|
||||
if not user:
|
||||
raise M365NotValidUserError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 User is not valid.",
|
||||
message="The provided User is not valid.",
|
||||
)
|
||||
|
||||
# Validate the Encrypted Password
|
||||
if not encrypted_password:
|
||||
raise M365NotValidEncryptedPasswordError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Encrypted Password is not valid.",
|
||||
message="The provided Encrypted Password is not valid.",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -1042,7 +1048,7 @@ class M365Provider(Provider):
|
||||
)
|
||||
raise M365ClientIdAndClientSecretNotBelongingToTenantIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Client ID and Client Secret do not belong to the specified Tenant ID.",
|
||||
message="The provided Client ID and Client Secret do not belong to the specified Tenant ID.",
|
||||
)
|
||||
except M365NotValidClientIdError as client_id_error:
|
||||
logger.error(
|
||||
@@ -1050,7 +1056,7 @@ class M365Provider(Provider):
|
||||
)
|
||||
raise M365TenantIdAndClientSecretNotBelongingToClientIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Tenant ID and Client Secret do not belong to the specified Client ID.",
|
||||
message="The provided Tenant ID and Client Secret do not belong to the specified Client ID.",
|
||||
)
|
||||
except M365NotValidClientSecretError as client_secret_error:
|
||||
logger.error(
|
||||
@@ -1058,7 +1064,7 @@ class M365Provider(Provider):
|
||||
)
|
||||
raise M365TenantIdAndClientIdNotBelongingToClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Tenant ID and Client ID do not belong to the specified Client Secret.",
|
||||
message="The provided Tenant ID and Client ID do not belong to the specified Client Secret.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -1100,19 +1106,25 @@ class M365Provider(Provider):
|
||||
if f"Tenant '{tenant_id}'" in error_description:
|
||||
raise M365NotValidTenantIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Microsoft 365 Tenant ID is not valid for the specified Client ID and Client Secret.",
|
||||
message="The provided Tenant ID is not valid for the specified Client ID and Client Secret.",
|
||||
)
|
||||
if f"Application with identifier '{client_id}'" in error_description:
|
||||
raise M365NotValidClientIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Microsoft 365 Client ID is not valid for the specified Tenant ID and Client Secret.",
|
||||
message="The provided Client ID is not valid for the specified Tenant ID and Client Secret.",
|
||||
)
|
||||
if "Invalid client secret provided" in error_description:
|
||||
raise M365NotValidClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Microsoft 365 Client Secret is not valid for the specified Tenant ID and Client ID.",
|
||||
message="The provided Client Secret is not valid for the specified Tenant ID and Client ID.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Generic exception handling (if needed)
|
||||
raise RuntimeError(f"An unexpected error occurred: {str(e)}")
|
||||
except (
|
||||
M365NotValidTenantIdError,
|
||||
M365NotValidClientIdError,
|
||||
M365NotValidClientSecretError,
|
||||
) as m365_error:
|
||||
# M365 specific errors already raised
|
||||
raise RuntimeError(f"{m365_error}")
|
||||
except Exception as error:
|
||||
# Generic exception handling for unexpected errors
|
||||
raise RuntimeError(f"An unexpected error occurred: {str(error)}")
|
||||
@@ -10,6 +10,7 @@ class M365IdentityInfo(BaseModel):
|
||||
tenant_id: str = ""
|
||||
tenant_domain: str = "Unknown tenant domain (missing AAD permissions)"
|
||||
location: str = ""
|
||||
user: str = None
|
||||
|
||||
|
||||
class M365RegionConfig(BaseModel):
|
||||
|
||||
@@ -23,37 +23,155 @@ class defender_antiphishing_policy_configured(Check):
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
for policy_name, policy in defender_client.antiphishing_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Anti-Phishing Policy",
|
||||
resource_id=policy_name,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Anti-phishing policy {policy_name} is not properly configured."
|
||||
)
|
||||
|
||||
if (
|
||||
not policy.default
|
||||
and policy_name in defender_client.antiphising_rules
|
||||
and defender_client.antiphising_rules[policy_name].state.lower()
|
||||
== "enabled"
|
||||
) or policy.default:
|
||||
if (
|
||||
policy.spoof_intelligence
|
||||
and policy.spoof_intelligence_action.lower() == "quarantine"
|
||||
and policy.dmarc_reject_action.lower() == "quarantine"
|
||||
and policy.dmarc_quarantine_action.lower() == "quarantine"
|
||||
and policy.safety_tips
|
||||
and policy.unauthenticated_sender_action
|
||||
and policy.show_tag
|
||||
and policy.honor_dmarc_policy
|
||||
):
|
||||
if defender_client.antiphishing_policies:
|
||||
# Only Default Defender Anti-Phishing Policy exists since there are only anti phishing rules when there are custom policies
|
||||
if not defender_client.antiphishing_rules:
|
||||
# Get the only policy in the dictionary since there is only the default policy
|
||||
policy = next(iter(defender_client.antiphishing_policies.values()))
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.name,
|
||||
resource_id=policy.name,
|
||||
)
|
||||
|
||||
if self._is_policy_properly_configured(policy):
|
||||
# Case 1: Default policy exists and is properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Anti-phishing policy {policy_name} is properly configured and enabled."
|
||||
report.status_extended = f"{policy.name} is the only policy and it's properly configured in the default Defender Anti-Phishing Policy."
|
||||
else:
|
||||
# Case 5: Default policy exists but is not properly configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.name} is the only policy and it's not properly configured in the default Defender Anti-Phishing Policy."
|
||||
findings.append(report)
|
||||
|
||||
findings.append(report)
|
||||
# Multiple Defender Anti-Phishing Policies
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for (
|
||||
policy_name,
|
||||
policy,
|
||||
) in defender_client.antiphishing_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy_name,
|
||||
resource_id=policy_name,
|
||||
)
|
||||
if policy.default:
|
||||
if not self._is_policy_properly_configured(policy):
|
||||
# Case 4: Default policy is not properly configured and there are other policies
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy_name} is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy."
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy is properly configured and there are other policies
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{policy_name} is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
if not self._is_policy_properly_configured(policy):
|
||||
included_resources = []
|
||||
|
||||
if defender_client.antiphishing_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.antiphishing_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.antiphishing_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.antiphishing_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.antiphishing_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.antiphishing_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
# Case 3: Default policy is properly configured but other custom policies are not
|
||||
if default_policy_well_configured:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Anti-phishing policy {policy_name} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
# Case 5: Default policy is not properly configured and other custom policies are not
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Anti-phishing policy {policy_name} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.antiphishing_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.antiphishing_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.antiphishing_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.antiphishing_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.antiphishing_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.antiphishing_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
# Case 2: Default policy is properly configured and other custom policies are too
|
||||
if default_policy_well_configured:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Anti-phishing policy {policy_name} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
# Case 6: Default policy is not properly configured but other custom policies are
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Anti-phishing policy {policy_name} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.antiphishing_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _is_policy_properly_configured(self, policy) -> bool:
|
||||
"""
|
||||
Check if a policy is properly configured according to best practices.
|
||||
|
||||
Args:
|
||||
policy: The anti-phishing policy to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the policy is properly configured, False otherwise.
|
||||
"""
|
||||
return (
|
||||
(
|
||||
policy.default
|
||||
or defender_client.antiphishing_rules[policy.name].state.lower()
|
||||
== "enabled"
|
||||
)
|
||||
and policy.spoof_intelligence
|
||||
and policy.spoof_intelligence_action.lower() == "quarantine"
|
||||
and policy.dmarc_reject_action.lower() == "quarantine"
|
||||
and policy.dmarc_quarantine_action.lower() == "quarantine"
|
||||
and policy.safety_tips
|
||||
and policy.unauthenticated_sender_action
|
||||
and policy.show_tag
|
||||
and policy.honor_dmarc_policy
|
||||
)
|
||||
@@ -6,8 +6,7 @@ from prowler.providers.m365.services.defender.defender_client import defender_cl
|
||||
|
||||
class defender_antispam_outbound_policy_configured(Check):
|
||||
"""
|
||||
Check if the Exchange Online Spam Policies are configured to notify administrators
|
||||
when a sender is blocked for sending spam emails.
|
||||
Check if the outbound spam policy is established and properly configured in the Defender service.
|
||||
|
||||
Attributes:
|
||||
metadata: Metadata associated with the check (inherited from Check).
|
||||
@@ -15,40 +14,159 @@ class defender_antispam_outbound_policy_configured(Check):
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""
|
||||
Execute the check to verify if the Exchange Online Spam Policies notify administrators
|
||||
when a sender is blocked for sending spam emails.
|
||||
Execute the check to verify if an outbound spam policy is established and properly configured.
|
||||
|
||||
This method checks the Defender outbound spam policies to ensure they are configured
|
||||
according to best practices.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
for policy_name, policy in defender_client.outbound_spam_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Outbound Spam Policy",
|
||||
resource_id=policy_name,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Outbound Spam Policy {policy_name} is not properly configured."
|
||||
)
|
||||
|
||||
if (
|
||||
not policy.default
|
||||
and policy_name in defender_client.outbound_spam_rules
|
||||
and defender_client.outbound_spam_rules[policy_name].state.lower()
|
||||
== "enabled"
|
||||
) or policy.default:
|
||||
if (
|
||||
policy.notify_limit_exceeded
|
||||
and policy.notify_sender_blocked
|
||||
and policy.notify_limit_exceeded_addresses
|
||||
and policy.notify_sender_blocked_addresses
|
||||
):
|
||||
if defender_client.outbound_spam_policies:
|
||||
# Only Default Defender Outbound Spam Policy
|
||||
if not defender_client.outbound_spam_rules:
|
||||
# Get the only policy in the dictionary
|
||||
policy = next(iter(defender_client.outbound_spam_policies.values()))
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.name,
|
||||
resource_id=policy.name,
|
||||
)
|
||||
|
||||
if self._is_policy_properly_configured(policy):
|
||||
# Case 1: Default policy exists and is properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Outbound Spam Policy {policy_name} is properly configured and enabled."
|
||||
report.status_extended = f"{policy.name} is the only policy and it's properly configured in the default Defender Outbound Spam Policy."
|
||||
else:
|
||||
# Case 5: Default policy exists but is not properly configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.name} is the only policy and it's not properly configured in the default Defender Outbound Spam Policy."
|
||||
findings.append(report)
|
||||
|
||||
findings.append(report)
|
||||
# Multiple Defender Outbound Spam Policies
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for (
|
||||
policy_name,
|
||||
policy,
|
||||
) in defender_client.outbound_spam_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy_name,
|
||||
resource_id=policy_name,
|
||||
)
|
||||
if policy.default:
|
||||
if not self._is_policy_properly_configured(policy):
|
||||
# Case 4: Default policy is not properly configured and there are other policies
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy_name} is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy."
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy is properly configured and there are other policies
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{policy_name} is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
if not self._is_policy_properly_configured(policy):
|
||||
included_resources = []
|
||||
|
||||
if defender_client.outbound_spam_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
# Case 3: Default policy is properly configured but other custom policies are not
|
||||
if default_policy_well_configured:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
# Case 5: Default policy is not properly configured and other custom policies are not
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.outbound_spam_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
# Case 2: Default policy is properly configured and other custom policies are too
|
||||
if default_policy_well_configured:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
# Case 6: Default policy is not properly configured but other custom policies are
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _is_policy_properly_configured(self, policy) -> bool:
|
||||
"""
|
||||
Check if a policy is properly configured according to best practices.
|
||||
|
||||
Args:
|
||||
policy: The outbound spam policy to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the policy is properly configured, False otherwise.
|
||||
"""
|
||||
return (
|
||||
(
|
||||
policy.default
|
||||
or defender_client.outbound_spam_rules[policy.name].state.lower()
|
||||
== "enabled"
|
||||
)
|
||||
and policy.notify_limit_exceeded
|
||||
and policy.notify_sender_blocked
|
||||
and policy.notify_limit_exceeded_addresses
|
||||
and policy.notify_sender_blocked_addresses
|
||||
)
|
||||
@@ -20,28 +20,150 @@ class defender_antispam_outbound_policy_forwarding_disabled(Check):
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
for policy_name, policy in defender_client.outbound_spam_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Outbound Spam Policy",
|
||||
resource_id=policy_name,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Outbound Spam Policy {policy_name} does allow mail forwarding."
|
||||
)
|
||||
|
||||
if (
|
||||
not policy.default
|
||||
and policy_name in defender_client.outbound_spam_rules
|
||||
and defender_client.outbound_spam_rules[policy_name].state.lower()
|
||||
== "enabled"
|
||||
) or policy.default:
|
||||
if not policy.auto_forwarding_mode:
|
||||
if defender_client.outbound_spam_policies:
|
||||
# Only Default Defender Outbound Spam Policy exists
|
||||
if not defender_client.outbound_spam_rules:
|
||||
policy = next(iter(defender_client.outbound_spam_policies.values()))
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.name,
|
||||
resource_id=policy.name,
|
||||
)
|
||||
|
||||
if self._is_forwarding_disabled(policy):
|
||||
# Case 1: Default policy exists and has forwarding disabled
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Outbound Spam Policy {policy_name} does not allow mail forwarding."
|
||||
report.status_extended = f"{policy.name} is the only policy and mail forwarding is disabled."
|
||||
else:
|
||||
# Case 5: Default policy exists but allows forwarding
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.name} is the only policy and mail forwarding is allowed."
|
||||
findings.append(report)
|
||||
|
||||
findings.append(report)
|
||||
# Multiple Defender Outbound Spam Policies exist
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for (
|
||||
policy_name,
|
||||
policy,
|
||||
) in defender_client.outbound_spam_policies.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy_name,
|
||||
resource_id=policy_name,
|
||||
)
|
||||
|
||||
if policy.default:
|
||||
if not self._is_forwarding_disabled(policy):
|
||||
# Case 4: Default policy allows forwarding and there are other policies
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{policy_name} is the default policy and mail forwarding is allowed, "
|
||||
"but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy disables forwarding and there are other policies
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{policy_name} is the default policy and mail forwarding is disabled, "
|
||||
"but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
if not self._is_forwarding_disabled(policy):
|
||||
included_resources = []
|
||||
|
||||
if defender_client.outbound_spam_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 3: Default policy disables forwarding but custom one doesn't
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} allows mail forwarding and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy disables mail forwarding, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 5: Neither default nor custom policies disable forwarding
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} allows mail forwarding and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.outbound_spam_rules[policy.name].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.outbound_spam_rules[policy.name].users)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.outbound_spam_rules[policy.name].groups)}"
|
||||
)
|
||||
if defender_client.outbound_spam_rules[policy.name].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.outbound_spam_rules[policy.name].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 2: Both default and custom policies disable forwarding
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} disables mail forwarding and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"Also, the default policy disables mail forwarding, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 6: Default policy allows forwarding, custom policy disables it
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Outbound Spam policy {policy_name} disables mail forwarding and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.outbound_spam_rules[policy.name].priority} (0 is the highest). "
|
||||
"However, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _is_forwarding_disabled(self, policy) -> bool:
|
||||
"""
|
||||
Check if mail forwarding is disabled in the policy.
|
||||
|
||||
Args:
|
||||
policy: The outbound spam policy to check.
|
||||
|
||||
Returns:
|
||||
bool: True if mail forwarding is disabled, False otherwise.
|
||||
"""
|
||||
return (
|
||||
policy.default
|
||||
or defender_client.outbound_spam_rules[policy.name].state.lower()
|
||||
== "enabled"
|
||||
) and not policy.auto_forwarding_mode
|
||||
@@ -23,20 +23,163 @@ class defender_antispam_policy_inbound_no_allowed_domains(Check):
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
for policy in defender_client.inbound_spam_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Inbound Spam Policy",
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Inbound anti-spam policy {policy.identity} does not contain allowed domains."
|
||||
|
||||
if policy.allowed_sender_domains:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Inbound anti-spam policy {policy.identity} contains allowed domains: {policy.allowed_sender_domains}."
|
||||
if defender_client.inbound_spam_policies:
|
||||
# Only Default Defender Inbound Spam Policy exists
|
||||
if not defender_client.inbound_spam_rules:
|
||||
policy = defender_client.inbound_spam_policies[0]
|
||||
|
||||
findings.append(report)
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if self._has_no_allowed_domains(policy):
|
||||
# Case 1: Default policy exists and has no allowed domains
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{policy.identity} is the only policy and it does not contain allowed domains."
|
||||
else:
|
||||
# Case 5: Default policy exists but contains allowed domains
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.identity} is the only policy and it contains allowed domains: {', '.join(policy.allowed_sender_domains)}."
|
||||
findings.append(report)
|
||||
|
||||
# Multiple Defender Inbound Spam Policies exist
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.inbound_spam_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if policy.default:
|
||||
if not self._has_no_allowed_domains(policy):
|
||||
# Case 4: Default policy contains allowed domains
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and it contains allowed domains: {', '.join(policy.allowed_sender_domains)}, "
|
||||
"but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy has no allowed domains and there are other policies
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and it does not contain allowed domains, "
|
||||
"but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
if not self._has_no_allowed_domains(policy):
|
||||
included_resources = []
|
||||
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.inbound_spam_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.inbound_spam_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.inbound_spam_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
priority = defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].priority
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 3: Default policy has no allowed domains but custom one does
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Inbound Spam policy {policy.identity} contains allowed domains and includes {included_resources_str}, "
|
||||
f"with priority {priority} (0 is the highest). However, the default policy does not contain allowed domains, "
|
||||
"so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
else:
|
||||
# Case 5: Neither default nor custom policies are correctly configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Inbound Spam policy {policy.identity} contains allowed domains and includes {included_resources_str}, "
|
||||
f"with priority {priority} (0 is the highest). Also, the default policy contains allowed domains, "
|
||||
"so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.inbound_spam_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.inbound_spam_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.inbound_spam_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
priority = defender_client.inbound_spam_rules[
|
||||
policy.identity
|
||||
].priority
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 2: Both default and custom policies do not contain allowed domains
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Inbound Spam policy {policy.identity} does not contain allowed domains and includes {included_resources_str}, "
|
||||
f"with priority {priority} (0 is the highest). Also, the default policy does not contain allowed domains, "
|
||||
"so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
else:
|
||||
# Case 6: Default policy contains allowed domains, custom policy does not
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Inbound Spam policy {policy.identity} does not contain allowed domains and includes {included_resources_str}, "
|
||||
f"with priority {priority} (0 is the highest). However, the default policy contains allowed domains, "
|
||||
"so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _has_no_allowed_domains(self, policy) -> bool:
|
||||
"""
|
||||
Check if the policy has no allowed domains.
|
||||
|
||||
Args:
|
||||
policy: The inbound spam policy to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the policy has no allowed domains, False otherwise.
|
||||
"""
|
||||
return (
|
||||
policy.default
|
||||
or defender_client.inbound_spam_rules[policy.identity].state.lower()
|
||||
== "enabled"
|
||||
) and not policy.allowed_sender_domains
|
||||
@@ -16,39 +16,138 @@ class defender_malware_policy_common_attachments_filter_enabled(Check):
|
||||
"""
|
||||
Execute the check to verify if the Common Attachment Types Filter is enabled.
|
||||
|
||||
This method checks the Defender anti-malware policy to determine if the
|
||||
This method checks the Defender anti-malware policies to determine if the
|
||||
Common Attachment Types Filter is enabled.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
if not defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Common Attachment Types Filter is not enabled."
|
||||
findings.append(report)
|
||||
else:
|
||||
for policy in defender_client.malware_policies:
|
||||
|
||||
if defender_client.malware_policies:
|
||||
# Only Default Defender Malware Policy exists
|
||||
if not defender_client.malware_rules:
|
||||
policy = defender_client.malware_policies[0]
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Common Attachment Types Filter is not enabled in anti-malware policy {policy.identity}."
|
||||
|
||||
if policy.enable_file_filter:
|
||||
# Case 1: Default policy exists and has the setting enabled
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}."
|
||||
break
|
||||
report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is enabled."
|
||||
else:
|
||||
# Case 5: Default policy exists but doesn't have the setting enabled
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is not enabled."
|
||||
findings.append(report)
|
||||
|
||||
findings.append(report)
|
||||
# Multiple Defender Malware Policies exist
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if policy.is_default:
|
||||
if not policy.enable_file_filter:
|
||||
# Case 4: Default policy doesn't have the setting enabled and there are other policies
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and Common Attachment Types Filter is not enabled, "
|
||||
"but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy has the setting enabled and there are other policies
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and Common Attachment Types Filter is enabled, "
|
||||
"but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
if not policy.enable_file_filter:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 3: Default policy enables the setting but custom one doesn't
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} does not enable Common Attachment Types Filter and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"However, the default policy enables the filter, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 5: Neither default nor custom policies enable the setting
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} does not enable Common Attachment Types Filter and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"Also, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 2: Both default and custom policies enable the setting
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} enables Common Attachment Types Filter and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"Also, the default policy enables the filter, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 6: Default policy doesn't enable the setting, but custom policy does
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} enables Common Attachment Types Filter and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"However, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -14,17 +14,16 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check):
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""
|
||||
Executes the check to determine if the Common Attachment Types Filter is enabled.
|
||||
Executes the check to determine if the Common Attachment Types Filter is properly configured.
|
||||
|
||||
This method evaluates the Defender anti-malware policy to ensure it is enabled and the
|
||||
Common Attachment Types Filter is active and applied to the recommended file types.
|
||||
This method evaluates the Defender anti-malware policies to ensure the filter is enabled and applied
|
||||
to all recommended file types.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list of reports with the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Default to Microsoft-recommended common file types
|
||||
default_recommended_extensions = [
|
||||
"ace",
|
||||
"ani",
|
||||
@@ -81,63 +80,175 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check):
|
||||
"z",
|
||||
]
|
||||
|
||||
# Load extensions from audit_config (user config), fallback to default
|
||||
recommended_extensions = defender_client.audit_config.get(
|
||||
"recommended_blocked_file_types", default_recommended_extensions
|
||||
)
|
||||
|
||||
if not defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Common Attachment Types Filter is not enabled."
|
||||
findings.append(report)
|
||||
else:
|
||||
for policy in defender_client.malware_policies:
|
||||
if defender_client.malware_policies:
|
||||
# Only Default Defender Malware Policy exists
|
||||
if not defender_client.malware_rules:
|
||||
policy = defender_client.malware_policies[0]
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Common Attachment Types Filter is not properly configured in anti-malware policy {policy.identity}."
|
||||
|
||||
if not policy.enable_file_filter:
|
||||
report.status_extended = f"Common Attachment Types Filter is not enabled in anti-malware policy {policy.identity}."
|
||||
break
|
||||
if self._is_filter_properly_configured(policy, recommended_extensions):
|
||||
# Case 1: Default policy exists and has filter properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is properly configured."
|
||||
else:
|
||||
# Case 5: Default policy exists but doesn't have filter properly configured
|
||||
missing = self._get_missing_extensions(
|
||||
policy, recommended_extensions
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.identity} is the only policy and Common Attachment Types Filter is not properly configured. Missing recommended file types: {', '.join(missing)}."
|
||||
findings.append(report)
|
||||
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity in defender_client.malware_rules
|
||||
) or policy.is_default:
|
||||
if (
|
||||
not policy.is_default
|
||||
and defender_client.malware_rules[policy.identity].state.lower()
|
||||
== "enabled"
|
||||
) or policy.is_default:
|
||||
blocked_extensions = [ext.lower() for ext in policy.file_types]
|
||||
missing = [
|
||||
ext
|
||||
for ext in recommended_extensions
|
||||
if ext.lower() not in blocked_extensions
|
||||
]
|
||||
# Multiple Defender Malware Policies exist
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
if missing:
|
||||
report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, but the following recommended file types are missing: {', '.join(missing)}."
|
||||
break
|
||||
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, the policy is enabled and the filter is applied to the recommended file types."
|
||||
for policy in defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if policy.is_default:
|
||||
if not self._is_filter_properly_configured(
|
||||
policy, recommended_extensions
|
||||
):
|
||||
# Case 4: Default policy is not properly configured
|
||||
missing = self._get_missing_extensions(
|
||||
policy, recommended_extensions
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and Common Attachment Types Filter is not properly configured, "
|
||||
f"but it could be overridden by another well-configured Custom Policy. Missing recommended file types: {', '.join(missing)}."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy is properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and Common Attachment Types Filter is properly configured, "
|
||||
"but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
report.status_extended = f"Common Attachment Types Filter is enabled in anti-malware policy {policy.identity}, but the policy is disabled."
|
||||
break
|
||||
if not self._is_filter_properly_configured(
|
||||
policy, recommended_extensions
|
||||
):
|
||||
included_resources = []
|
||||
|
||||
findings.append(report)
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
missing = self._get_missing_extensions(
|
||||
policy, recommended_extensions
|
||||
)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 3: Default policy is configured, custom one isn't
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
f"Missing recommended file types: {', '.join(missing)}. "
|
||||
"However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 5: Neither default nor custom policy is properly configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
f"Missing recommended file types: {', '.join(missing)}. "
|
||||
"Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 2: Both default and custom policies are properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 6: Default policy not configured, custom policy is
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _is_filter_properly_configured(self, policy, recommended_extensions) -> bool:
|
||||
if not policy.enable_file_filter:
|
||||
return False
|
||||
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity in defender_client.malware_rules
|
||||
and defender_client.malware_rules[policy.identity].state.lower()
|
||||
!= "enabled"
|
||||
):
|
||||
return False
|
||||
|
||||
blocked_extensions = [ext.lower() for ext in policy.file_types]
|
||||
return all(ext.lower() in blocked_extensions for ext in recommended_extensions)
|
||||
|
||||
def _get_missing_extensions(self, policy, recommended_extensions) -> List[str]:
|
||||
if not policy.enable_file_filter:
|
||||
return recommended_extensions
|
||||
|
||||
blocked_extensions = [ext.lower() for ext in policy.file_types]
|
||||
return [
|
||||
ext
|
||||
for ext in recommended_extensions
|
||||
if ext.lower() not in blocked_extensions
|
||||
]
|
||||
@@ -16,47 +16,152 @@ class defender_malware_policy_notifications_internal_users_malware_enabled(Check
|
||||
"""
|
||||
Execute the check to verify if notifications for internal users sending malware are enabled.
|
||||
|
||||
This method checks the Defender anti-malware policy to determine if notifications for internal users
|
||||
sending malware are enabled.
|
||||
This method evaluates the Defender anti-malware policies to ensure internal sender notifications
|
||||
are properly configured.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list of reports containing the result of the check.
|
||||
List[CheckReportM365]: A list of reports with the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
if not defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Notifications for internal users sending malware are not enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
for policy in defender_client.malware_policies:
|
||||
|
||||
if defender_client.malware_policies:
|
||||
# Only Default Defender Malware Policy exists
|
||||
if not defender_client.malware_rules:
|
||||
policy = defender_client.malware_policies[0]
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name="Defender Malware Policy",
|
||||
resource_id="defenderMalwarePolicy",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Notifications for internal users sending malware are not enabled."
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if policy.enable_internal_sender_admin_notifications:
|
||||
if policy.internal_sender_admin_address:
|
||||
report.status = "PASS"
|
||||
report.status_extended = "Notifications for internal users sending malware are enabled."
|
||||
break
|
||||
if self._are_notifications_enabled(policy):
|
||||
# Case 1: Default policy exists and has notifications enabled
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{policy.identity} is the only policy and notifications for internal users sending malware are enabled."
|
||||
else:
|
||||
# Case 5: Default policy exists but doesn't have notifications enabled
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.identity} is the only policy and notifications for internal users sending malware are not enabled."
|
||||
findings.append(report)
|
||||
|
||||
# Multiple Defender Malware Policies exist
|
||||
else:
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.malware_policies:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
resource_name=policy.identity,
|
||||
resource_id=policy.identity,
|
||||
)
|
||||
|
||||
if policy.is_default:
|
||||
if not self._are_notifications_enabled(policy):
|
||||
# Case 4: Default policy not configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and notifications for internal users sending malware are not enabled, "
|
||||
"but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 2: Default policy is properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{policy.identity} is the default policy and notifications for internal users sending malware are enabled, "
|
||||
"but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
default_policy_well_configured = True
|
||||
findings.append(report)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Notifications for internal users sending malware are enabled, but no email addresses are configured."
|
||||
if not self._are_notifications_enabled(policy):
|
||||
included_resources = []
|
||||
|
||||
findings.append(report)
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 3: Default policy is configured, custom one isn't
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 5: Neither default nor custom policy is properly configured
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is not properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
included_resources = []
|
||||
|
||||
if defender_client.malware_rules[policy.identity].users:
|
||||
included_resources.append(
|
||||
f"users: {', '.join(defender_client.malware_rules[policy.identity].users)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].groups:
|
||||
included_resources.append(
|
||||
f"groups: {', '.join(defender_client.malware_rules[policy.identity].groups)}"
|
||||
)
|
||||
if defender_client.malware_rules[policy.identity].domains:
|
||||
included_resources.append(
|
||||
f"domains: {', '.join(defender_client.malware_rules[policy.identity].domains)}"
|
||||
)
|
||||
|
||||
included_resources_str = "; ".join(included_resources)
|
||||
|
||||
if default_policy_well_configured:
|
||||
# Case 2: Both default and custom policies are properly configured
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
else:
|
||||
# Case 6: Default policy not configured, custom policy is
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Custom Malware policy {policy.identity} is properly configured and includes {included_resources_str}, "
|
||||
f"with priority {defender_client.malware_rules[policy.identity].priority} (0 is the highest). "
|
||||
"However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
def _are_notifications_enabled(self, policy) -> bool:
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity in defender_client.malware_rules
|
||||
and defender_client.malware_rules[policy.identity].state.lower()
|
||||
!= "enabled"
|
||||
):
|
||||
return False
|
||||
|
||||
return (
|
||||
policy.enable_internal_sender_admin_notifications
|
||||
and policy.internal_sender_admin_address
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
from asyncio import gather, get_event_loop
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -15,10 +15,11 @@ class Defender(M365Service):
|
||||
self.outbound_spam_policies = {}
|
||||
self.outbound_spam_rules = {}
|
||||
self.antiphishing_policies = {}
|
||||
self.antiphising_rules = {}
|
||||
self.antiphishing_rules = {}
|
||||
self.connection_filter_policy = None
|
||||
self.dkim_configurations = []
|
||||
self.inbound_spam_policies = []
|
||||
self.inbound_spam_rules = {}
|
||||
self.report_submission_policy = None
|
||||
if self.powershell:
|
||||
self.powershell.connect_exchange_online()
|
||||
@@ -26,11 +27,12 @@ class Defender(M365Service):
|
||||
self.malware_rules = self._get_malware_filter_rule()
|
||||
self.outbound_spam_policies = self._get_outbound_spam_filter_policy()
|
||||
self.outbound_spam_rules = self._get_outbound_spam_filter_rule()
|
||||
self.antiphishing_policies = self._get_antiphising_policy()
|
||||
self.antiphising_rules = self._get_antiphising_rules()
|
||||
self.antiphishing_policies = self._get_antiphishing_policy()
|
||||
self.antiphishing_rules = self._get_antiphishing_rules()
|
||||
self.connection_filter_policy = self._get_connection_filter_policy()
|
||||
self.dkim_configurations = self._get_dkim_config()
|
||||
self.inbound_spam_policies = self._get_inbound_spam_filter_policy()
|
||||
self.inbound_spam_rules = self._get_inbound_spam_filter_rule()
|
||||
self.report_submission_policy = self._get_report_submission_policy()
|
||||
self.powershell.close()
|
||||
|
||||
@@ -55,7 +57,7 @@ class Defender(M365Service):
|
||||
if policy:
|
||||
malware_policies.append(
|
||||
MalwarePolicy(
|
||||
enable_file_filter=policy.get("EnableFileFilter", True),
|
||||
enable_file_filter=policy.get("EnableFileFilter", False),
|
||||
identity=policy.get("Identity", ""),
|
||||
enable_internal_sender_admin_notifications=policy.get(
|
||||
"EnableInternalSenderAdminNotifications", False
|
||||
@@ -67,6 +69,7 @@ class Defender(M365Service):
|
||||
is_default=policy.get("IsDefault", False),
|
||||
)
|
||||
)
|
||||
malware_policies.sort(key=lambda x: x.is_default, reverse=True)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -84,6 +87,10 @@ class Defender(M365Service):
|
||||
if rule:
|
||||
malware_rules[rule.get("Name", "")] = MalwareRule(
|
||||
state=rule.get("State", ""),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -91,7 +98,7 @@ class Defender(M365Service):
|
||||
)
|
||||
return malware_rules
|
||||
|
||||
def _get_antiphising_policy(self):
|
||||
def _get_antiphishing_policy(self):
|
||||
logger.info("Microsoft365 - Getting Defender antiphishing policy...")
|
||||
antiphishing_policies = {}
|
||||
try:
|
||||
@@ -101,6 +108,7 @@ class Defender(M365Service):
|
||||
for policy in antiphishing_policy:
|
||||
if policy:
|
||||
antiphishing_policies[policy.get("Name", "")] = AntiphishingPolicy(
|
||||
name=policy.get("Name", ""),
|
||||
spoof_intelligence=policy.get("EnableSpoofIntelligence", True),
|
||||
spoof_intelligence_action=policy.get(
|
||||
"AuthenticationFailAction", ""
|
||||
@@ -115,13 +123,21 @@ class Defender(M365Service):
|
||||
honor_dmarc_policy=policy.get("HonorDmarcPolicy", True),
|
||||
default=policy.get("IsDefault", False),
|
||||
)
|
||||
|
||||
antiphishing_policies = dict(
|
||||
sorted(
|
||||
antiphishing_policies.items(),
|
||||
key=lambda item: item[1].default,
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return antiphishing_policies
|
||||
|
||||
def _get_antiphising_rules(self):
|
||||
def _get_antiphishing_rules(self):
|
||||
logger.info("Microsoft365 - Getting Defender antiphishing rules...")
|
||||
antiphishing_rules = {}
|
||||
try:
|
||||
@@ -132,6 +148,10 @@ class Defender(M365Service):
|
||||
if rule:
|
||||
antiphishing_rules[rule.get("Name", "")] = AntiphishingRule(
|
||||
state=rule.get("State", ""),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -187,6 +207,7 @@ class Defender(M365Service):
|
||||
for policy in outbound_spam_policy:
|
||||
if policy:
|
||||
outbound_spam_policies[policy.get("Name", "")] = OutboundSpamPolicy(
|
||||
name=policy.get("Name", ""),
|
||||
notify_sender_blocked=policy.get("NotifyOutboundSpam", True),
|
||||
notify_limit_exceeded=policy.get(
|
||||
"BccSuspiciousOutboundMail", True
|
||||
@@ -200,6 +221,14 @@ class Defender(M365Service):
|
||||
auto_forwarding_mode=policy.get("AutoForwardingMode", True),
|
||||
default=policy.get("IsDefault", False),
|
||||
)
|
||||
|
||||
outbound_spam_policies = dict(
|
||||
sorted(
|
||||
outbound_spam_policies.items(),
|
||||
key=lambda item: item[1].default,
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -217,6 +246,10 @@ class Defender(M365Service):
|
||||
if rule:
|
||||
outbound_spam_rules[rule.get("Name", "")] = OutboundSpamRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("From", None),
|
||||
groups=rule.get("FromMemberOf", None),
|
||||
domains=rule.get("SenderDomainIs", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -241,14 +274,38 @@ class Defender(M365Service):
|
||||
allowed_sender_domains=policy.get(
|
||||
"AllowedSenderDomains", []
|
||||
),
|
||||
default=policy.get("IsDefault", False),
|
||||
)
|
||||
)
|
||||
inbound_spam_policies.sort(key=lambda x: x.default, reverse=True)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return inbound_spam_policies
|
||||
|
||||
def _get_inbound_spam_filter_rule(self):
|
||||
logger.info("Microsoft365 - Getting Defender inbound spam filter rule...")
|
||||
inbound_spam_rules = {}
|
||||
try:
|
||||
inbound_spam_rule = self.powershell.get_inbound_spam_filter_rule()
|
||||
if isinstance(inbound_spam_rule, dict):
|
||||
inbound_spam_rule = [inbound_spam_rule]
|
||||
for rule in inbound_spam_rule:
|
||||
if rule:
|
||||
inbound_spam_rules[rule.get("Name", "")] = InboundSpamRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return inbound_spam_rules
|
||||
|
||||
def _get_report_submission_policy(self):
|
||||
logger.info("Microsoft365 - Getting Defender report submission policy...")
|
||||
report_submission_policy = None
|
||||
@@ -328,9 +385,14 @@ class MalwarePolicy(BaseModel):
|
||||
|
||||
class MalwareRule(BaseModel):
|
||||
state: str
|
||||
priority: int
|
||||
users: Optional[list[str]]
|
||||
groups: Optional[list[str]]
|
||||
domains: Optional[list[str]]
|
||||
|
||||
|
||||
class AntiphishingPolicy(BaseModel):
|
||||
name: str
|
||||
spoof_intelligence: bool
|
||||
spoof_intelligence_action: str
|
||||
dmarc_reject_action: str
|
||||
@@ -344,6 +406,10 @@ class AntiphishingPolicy(BaseModel):
|
||||
|
||||
class AntiphishingRule(BaseModel):
|
||||
state: str
|
||||
priority: int
|
||||
users: Optional[list[str]]
|
||||
groups: Optional[list[str]]
|
||||
domains: Optional[list[str]]
|
||||
|
||||
|
||||
class ConnectionFilterPolicy(BaseModel):
|
||||
@@ -358,6 +424,7 @@ class DkimConfig(BaseModel):
|
||||
|
||||
|
||||
class OutboundSpamPolicy(BaseModel):
|
||||
name: str
|
||||
notify_sender_blocked: bool
|
||||
notify_limit_exceeded: bool
|
||||
notify_limit_exceeded_addresses: List[str]
|
||||
@@ -368,11 +435,24 @@ class OutboundSpamPolicy(BaseModel):
|
||||
|
||||
class OutboundSpamRule(BaseModel):
|
||||
state: str
|
||||
priority: int
|
||||
users: Optional[list[str]]
|
||||
groups: Optional[list[str]]
|
||||
domains: Optional[list[str]]
|
||||
|
||||
|
||||
class DefenderInboundSpamPolicy(BaseModel):
|
||||
identity: str
|
||||
allowed_sender_domains: list[str] = []
|
||||
default: bool
|
||||
|
||||
|
||||
class InboundSpamRule(BaseModel):
|
||||
state: str
|
||||
priority: int
|
||||
users: Optional[list[str]]
|
||||
groups: Optional[list[str]]
|
||||
domains: Optional[list[str]]
|
||||
|
||||
|
||||
class ReportSubmissionPolicy(BaseModel):
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"CheckID": "entra_legacy_authentication_blocked",
|
||||
"CheckTitle": "Ensure that Conditional Access policy blocks legacy authentication",
|
||||
"CheckType": [],
|
||||
"ServiceName": "critical",
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"Description": "Ensure that Conditional Access policy blocks legacy authentication in Microsoft Entra ID to enforce modern authentication methods and protect against credential-stuffing and brute-force attacks.",
|
||||
"Risk": "Legacy authentication protocols do not support MFA, making them vulnerable to credential-stuffing and brute-force attacks. Attackers commonly exploit these protocols to bypass security controls and gain unauthorized access.",
|
||||
|
||||
@@ -64,7 +64,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">3.9.1,<3.13"
|
||||
version = "5.6.0"
|
||||
version = "5.7.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -12,6 +12,7 @@ from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_prov
|
||||
from tests.providers.kubernetes.kubernetes_fixtures import (
|
||||
set_mocked_kubernetes_provider,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
|
||||
|
||||
html_stats = {
|
||||
"total_pass": 25,
|
||||
@@ -222,6 +223,38 @@ kubernetes_html_assessment_summary = """
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
m365_html_assessment_summary = """
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
M365 Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>M365 Tenant Domain:</b> user.onmicrosoft.com
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
M365 Credentials
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>M365 Identity Type:</b> Application
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>M365 Identity ID:</b> 00000000-0000-0000-0000-000000000000
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>M365 User:</b> user@email.com
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
|
||||
def get_aws_html_header(args: list) -> str:
|
||||
"""
|
||||
@@ -554,3 +587,13 @@ class TestHTML:
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert summary == kubernetes_html_assessment_summary
|
||||
|
||||
def test_m365_get_assessment_summary(self):
|
||||
findings = [generate_finding_output()]
|
||||
output = HTML(findings)
|
||||
provider = set_mocked_m365_provider()
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
expected_summary = m365_html_assessment_summary
|
||||
assert summary == expected_summary
|
||||
@@ -1,6 +1,8 @@
|
||||
from argparse import Namespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from kubernetes.config.config_exception import ConfigException
|
||||
|
||||
from kubernetes import client
|
||||
from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
@@ -305,3 +307,49 @@ class TestKubernetesProvider:
|
||||
|
||||
assert connection.is_connected
|
||||
assert connection.error is None
|
||||
|
||||
def test_kubernetes_provider_incluster_with_env_var(self, monkeypatch):
|
||||
monkeypatch.setenv("CLUSTER_NAME", "env-cluster-name")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"kubernetes.config.load_kube_config",
|
||||
side_effect=ConfigException("No kubeconfig"),
|
||||
),
|
||||
patch("kubernetes.config.load_incluster_config", return_value=None),
|
||||
patch("prowler.providers.kubernetes.kubernetes_provider.client.ApiClient"),
|
||||
patch(
|
||||
"prowler.providers.kubernetes.kubernetes_provider.KubernetesProvider.get_all_namespaces",
|
||||
return_value=["default"],
|
||||
),
|
||||
):
|
||||
session = KubernetesProvider.setup_session(
|
||||
kubeconfig_file=None,
|
||||
kubeconfig_content=None,
|
||||
context=None,
|
||||
cluster_name=None,
|
||||
)
|
||||
assert isinstance(session, KubernetesSession)
|
||||
assert session.context["context"]["cluster"] == "env-cluster-name"
|
||||
|
||||
def test_kubernetes_provider_incluster_with_cli_flag(self):
|
||||
with (
|
||||
patch(
|
||||
"kubernetes.config.load_kube_config",
|
||||
side_effect=ConfigException("No kubeconfig"),
|
||||
),
|
||||
patch("kubernetes.config.load_incluster_config", return_value=None),
|
||||
patch("prowler.providers.kubernetes.kubernetes_provider.client.ApiClient"),
|
||||
patch(
|
||||
"prowler.providers.kubernetes.kubernetes_provider.KubernetesProvider.get_all_namespaces",
|
||||
return_value=["default"],
|
||||
),
|
||||
):
|
||||
session = KubernetesProvider.setup_session(
|
||||
kubeconfig_file=None,
|
||||
kubeconfig_content=None,
|
||||
context=None,
|
||||
cluster_name="cli-cluster-name",
|
||||
)
|
||||
assert isinstance(session, KubernetesSession)
|
||||
assert session.context["context"]["cluster"] == "cli-cluster-name"
|
||||
@@ -29,6 +29,7 @@ def set_mocked_m365_provider(
|
||||
identity_type=IDENTITY_TYPE,
|
||||
tenant_id=TENANT_ID,
|
||||
tenant_domain=DOMAIN,
|
||||
user="user@email.com",
|
||||
),
|
||||
audit_config: dict = None,
|
||||
azure_region_config: M365RegionConfig = M365RegionConfig(),
|
||||
|
||||
@@ -522,7 +522,7 @@ class TestM365Provider:
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
)
|
||||
assert "The provided M365 Tenant ID is not valid." in str(exception.value)
|
||||
assert "The provided Tenant ID is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_client_id(self):
|
||||
with pytest.raises(M365NotValidClientIdError) as exception:
|
||||
@@ -533,7 +533,7 @@ class TestM365Provider:
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
)
|
||||
assert "The provided M365 Client ID is not valid." in str(exception.value)
|
||||
assert "The provided Client ID is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_client_secret(self):
|
||||
with pytest.raises(M365NotValidClientSecretError) as exception:
|
||||
@@ -544,7 +544,7 @@ class TestM365Provider:
|
||||
user="test@example.com",
|
||||
encrypted_password="test_password",
|
||||
)
|
||||
assert "The provided M365 Client Secret is not valid." in str(exception.value)
|
||||
assert "The provided Client Secret is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_user(self):
|
||||
with pytest.raises(M365NotValidUserError) as exception:
|
||||
@@ -555,7 +555,7 @@ class TestM365Provider:
|
||||
user="",
|
||||
encrypted_password="test_password",
|
||||
)
|
||||
assert "The provided M365 User is not valid." in str(exception.value)
|
||||
assert "The provided User is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_encrypted_password(self):
|
||||
with pytest.raises(M365NotValidEncryptedPasswordError) as exception:
|
||||
@@ -566,9 +566,7 @@ class TestM365Provider:
|
||||
user="test@example.com",
|
||||
encrypted_password="",
|
||||
)
|
||||
assert "The provided M365 Encrypted Password is not valid." in str(
|
||||
exception.value
|
||||
)
|
||||
assert "The provided Encrypted Password is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_arguments_missing_env_credentials(self):
|
||||
with pytest.raises(M365MissingEnvironmentCredentialsError) as exception:
|
||||
@@ -583,6 +581,7 @@ class TestM365Provider:
|
||||
user=None,
|
||||
encrypted_password=None,
|
||||
)
|
||||
|
||||
assert (
|
||||
"M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_ENCRYPTED_PASSWORD environment variables to be set when using --env-auth"
|
||||
in str(exception.value)
|
||||
|
||||
@@ -4,7 +4,69 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_antiphishing_policy_configured:
|
||||
def test_properly_configured_custom_policy(self):
|
||||
def test_case_1_default_policy_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
|
||||
defender_antiphishing_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
AntiphishingPolicy,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
dmarc_quarantine_action="Quarantine",
|
||||
safety_tips=True,
|
||||
unauthenticated_sender_action=True,
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.antiphishing_rules = {}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and it's properly configured in the default Defender Anti-Phishing Policy."
|
||||
)
|
||||
assert (
|
||||
result[0].resource_name
|
||||
== defender_client.antiphishing_policies["Default"].name
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== defender_client.antiphishing_policies["Default"].name
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
|
||||
def test_case_2_all_policies_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -31,7 +93,20 @@ class Test_defender_antiphishing_policy_configured:
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
dmarc_quarantine_action="Quarantine",
|
||||
safety_tips=True,
|
||||
unauthenticated_sender_action=True,
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": AntiphishingPolicy(
|
||||
name="Policy1",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
@@ -41,29 +116,59 @@ class Test_defender_antiphishing_policy_configured:
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=False,
|
||||
)
|
||||
),
|
||||
}
|
||||
defender_client.antiphising_rules = {
|
||||
"Policy1": AntiphishingRule(state="Enabled")
|
||||
defender_client.antiphishing_rules = {
|
||||
"Policy1": AntiphishingRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Anti-phishing policy Policy1 is properly configured and enabled."
|
||||
== "Default is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
)
|
||||
assert (
|
||||
result[0].resource_name
|
||||
== defender_client.antiphishing_policies["Default"].name
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== defender_client.antiphishing_policies["Default"].name
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== f"Custom Anti-phishing policy {defender_client.antiphishing_policies['Policy1'].name} is properly configured and includes users: {', '.join(defender_client.antiphishing_rules['Policy1'].users)}; groups: {', '.join(defender_client.antiphishing_rules['Policy1'].groups)}; domains: {', '.join(defender_client.antiphishing_rules['Policy1'].domains)}, "
|
||||
f"with priority {defender_client.antiphishing_rules['Policy1'].priority} (0 is the highest). "
|
||||
"Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert (
|
||||
result[1].resource_name
|
||||
== defender_client.antiphishing_policies["Policy1"].name
|
||||
)
|
||||
assert (
|
||||
result[1].resource_id
|
||||
== defender_client.antiphishing_policies["Policy1"].name
|
||||
)
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.antiphishing_policies["Policy1"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Anti-Phishing Policy"
|
||||
assert result[0].resource_id == "Policy1"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_not_properly_configured_policy(self):
|
||||
def test_case_3_default_ok_others_not(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -89,66 +194,9 @@ class Test_defender_antiphishing_policy_configured:
|
||||
AntiphishingRule,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Policy2": AntiphishingPolicy(
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
dmarc_quarantine_action="None",
|
||||
safety_tips=False,
|
||||
unauthenticated_sender_action=False,
|
||||
show_tag=False,
|
||||
honor_dmarc_policy=False,
|
||||
default=False,
|
||||
)
|
||||
}
|
||||
defender_client.antiphising_rules = {
|
||||
"Policy2": AntiphishingRule(state="Enabled")
|
||||
}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Anti-phishing policy Policy2 is not properly configured."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Policy2"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Anti-Phishing Policy"
|
||||
assert result[0].resource_id == "Policy2"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_properly_configured_default_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
|
||||
defender_antiphishing_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
AntiphishingPolicy,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
@@ -158,27 +206,149 @@ class Test_defender_antiphishing_policy_configured:
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": AntiphishingPolicy(
|
||||
name="Policy1",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
dmarc_quarantine_action="None",
|
||||
safety_tips=False,
|
||||
unauthenticated_sender_action=False,
|
||||
show_tag=False,
|
||||
honor_dmarc_policy=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.antiphishing_rules = {
|
||||
"Policy1": AntiphishingRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
defender_client.antiphising_rules = {}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Anti-phishing policy Default is properly configured and enabled."
|
||||
== "Default is properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Anti-Phishing Policy"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_default_policy_not_properly_configured(self):
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Anti-phishing policy Policy1 is not properly configured and includes users: test@example.com; groups: example_group; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.antiphishing_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_4_default_not_ok_potential_false_positive(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
|
||||
defender_antiphishing_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
AntiphishingPolicy,
|
||||
AntiphishingRule,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
dmarc_quarantine_action="None",
|
||||
safety_tips=False,
|
||||
unauthenticated_sender_action=False,
|
||||
show_tag=False,
|
||||
honor_dmarc_policy=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": AntiphishingPolicy(
|
||||
name="Policy1",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
dmarc_quarantine_action="Quarantine",
|
||||
safety_tips=True,
|
||||
unauthenticated_sender_action=True,
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.antiphishing_rules = {
|
||||
"Policy1": AntiphishingRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Anti-phishing policy Policy1 is properly configured and includes users: test@example.com; groups: example_group; domains: example.com, "
|
||||
f"with priority {defender_client.antiphishing_rules['Policy1'].priority} (0 is the highest). "
|
||||
"However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.antiphishing_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_5_default_policy_not_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -205,6 +375,7 @@ class Test_defender_antiphishing_policy_configured:
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
@@ -216,7 +387,7 @@ class Test_defender_antiphishing_policy_configured:
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.antiphising_rules = {}
|
||||
defender_client.antiphishing_rules = {}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
@@ -224,15 +395,103 @@ class Test_defender_antiphishing_policy_configured:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Anti-phishing policy Default is not properly configured."
|
||||
== "Default is the only policy and it's not properly configured in the default Defender Anti-Phishing Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Anti-Phishing Policy"
|
||||
|
||||
def test_case_6_both_policies_not_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
|
||||
defender_antiphishing_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
AntiphishingPolicy,
|
||||
AntiphishingRule,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"Default": AntiphishingPolicy(
|
||||
name="Default",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
dmarc_quarantine_action="None",
|
||||
safety_tips=False,
|
||||
unauthenticated_sender_action=False,
|
||||
show_tag=False,
|
||||
honor_dmarc_policy=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": AntiphishingPolicy(
|
||||
name="Policy1",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
dmarc_quarantine_action="None",
|
||||
safety_tips=False,
|
||||
unauthenticated_sender_action=False,
|
||||
show_tag=False,
|
||||
honor_dmarc_policy=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.antiphishing_rules = {
|
||||
"Policy1": AntiphishingRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=[],
|
||||
groups=[],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is not properly configured in the default Defender Anti-Phishing Policy, but could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].location == "global"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["Default"].dict()
|
||||
)
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Anti-phishing policy Policy1 is not properly configured and includes domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.antiphishing_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_no_antiphishing_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
@@ -257,64 +516,8 @@ class Test_defender_antiphishing_policy_configured:
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {}
|
||||
defender_client.antiphising_rules = {}
|
||||
defender_client.antiphishing_rules = {}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert result == []
|
||||
|
||||
def test_custom_policy_without_rule(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antiphishing_policy_configured.defender_antiphishing_policy_configured import (
|
||||
defender_antiphishing_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
AntiphishingPolicy,
|
||||
)
|
||||
|
||||
defender_client.antiphishing_policies = {
|
||||
"PolicyX": AntiphishingPolicy(
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Quarantine",
|
||||
dmarc_quarantine_action="Quarantine",
|
||||
safety_tips=True,
|
||||
unauthenticated_sender_action=True,
|
||||
show_tag=True,
|
||||
honor_dmarc_policy=True,
|
||||
default=False,
|
||||
)
|
||||
}
|
||||
defender_client.antiphising_rules = {}
|
||||
|
||||
check = defender_antiphishing_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Anti-phishing policy PolicyX is not properly configured."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.antiphishing_policies["PolicyX"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Anti-Phishing Policy"
|
||||
assert result[0].resource_id == "PolicyX"
|
||||
assert result[0].location == "global"
|
||||
assert len(result) == 0
|
||||
@@ -4,119 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_antispam_outbound_policy_configured:
|
||||
def test_properly_configured_custom_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
default=False,
|
||||
auto_forwarding_mode=False,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(state="Enabled")
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Policy1 is properly configured and enabled."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy1"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_not_properly_configured_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Policy2": OutboundSpamPolicy(
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=False,
|
||||
auto_forwarding_mode=False,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy2": OutboundSpamRule(state="Enabled")
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Policy2 is not properly configured."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Policy2"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy2"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_properly_configured_default_policy(self):
|
||||
def test_case_1_default_policy_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -143,12 +31,13 @@ class Test_defender_antispam_outbound_policy_configured:
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
notify_sender_blocked=True,
|
||||
name="Default",
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
default=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
@@ -159,17 +48,271 @@ class Test_defender_antispam_outbound_policy_configured:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Default is properly configured and enabled."
|
||||
== "Default is the only policy and it's properly configured in the default Defender Outbound Spam Policy."
|
||||
)
|
||||
assert (
|
||||
result[0].resource_name
|
||||
== defender_client.outbound_spam_policies["Default"].name
|
||||
)
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_without_rule(self):
|
||||
def test_case_2_all_policies_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
auto_forwarding_mode=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 is properly configured and includes users: test@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_3_default_ok_others_not(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
auto_forwarding_mode=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is properly configured in the default Defender Outbound Spam Policy, but could be overridden by another bad-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 is not properly configured and includes users: test@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_4_default_not_ok_custom_good(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
auto_forwarding_mode=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 is properly configured and includes users: user1@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_5_only_default_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -195,13 +338,14 @@ class Test_defender_antispam_outbound_policy_configured:
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"PolicyX": OutboundSpamPolicy(
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["admin@org.com"],
|
||||
notify_sender_blocked_addresses=["admin@org.com"],
|
||||
default=False,
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
@@ -212,15 +356,98 @@ class Test_defender_antispam_outbound_policy_configured:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy PolicyX is not properly configured."
|
||||
== "Default is the only policy and it's not properly configured in the default Defender Outbound Spam Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["PolicyX"].dict()
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
def test_case_6_default_and_custom_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
|
||||
defender_antispam_outbound_policy_configured,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
auto_forwarding_mode=False,
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
auto_forwarding_mode=False,
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=5,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_configured()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is not properly configured in the default Defender Outbound Spam Policy, but could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 is not properly configured and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 5 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "PolicyX"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_no_outbound_spam_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
|
||||
@@ -4,6 +4,456 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_antispam_outbound_policy_forwarding_disabled:
|
||||
def test_case_1_default_policy_forwarding_disabled(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=False,
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and mail forwarding is disabled."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
def test_case_2_all_policies_forwarding_disabled(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=False,
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
auto_forwarding_mode=False,
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and mail forwarding is disabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 disables mail forwarding and includes users: test@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy disables mail forwarding, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_3_default_ok_custom_not(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=False,
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
auto_forwarding_mode=True,
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and mail forwarding is disabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 allows mail forwarding and includes users: test@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). However, the default policy disables mail forwarding, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_4_default_not_ok_custom_good(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=True,
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
auto_forwarding_mode=False,
|
||||
notify_limit_exceeded=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded_addresses=["admin@example.com"],
|
||||
notify_sender_blocked_addresses=["admin@example.com"],
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and mail forwarding is allowed, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 disables mail forwarding and includes users: user1@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_case_5_only_default_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=True,
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=True,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and mail forwarding is allowed."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
def test_case_6_default_and_custom_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
name="Default",
|
||||
auto_forwarding_mode=True,
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=True,
|
||||
),
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
auto_forwarding_mode=True,
|
||||
notify_limit_exceeded=False,
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
notify_sender_blocked_addresses=[],
|
||||
default=False,
|
||||
),
|
||||
}
|
||||
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and mail forwarding is allowed, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Outbound Spam policy Policy1 allows mail forwarding and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy allows mail forwarding, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert (
|
||||
result[1].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
|
||||
def test_no_outbound_spam_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
@@ -32,221 +482,3 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled:
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_forwarding_disabled_custom_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
default=False,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
auto_forwarding_mode=False,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy1": OutboundSpamRule(state="Enabled")
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Policy1 does not allow mail forwarding."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Policy1"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy1"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_forwarding_enabled_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
OutboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Policy2": OutboundSpamPolicy(
|
||||
default=False,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
auto_forwarding_mode=True,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {
|
||||
"Policy2": OutboundSpamRule(state="Enabled")
|
||||
}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Policy2 does allow mail forwarding."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Policy2"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy2"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_forwarding_disabled_default_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"Default": OutboundSpamPolicy(
|
||||
default=True,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
auto_forwarding_mode=False,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy Default does not allow mail forwarding."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["Default"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_without_rule(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_forwarding_disabled.defender_antispam_outbound_policy_forwarding_disabled import (
|
||||
defender_antispam_outbound_policy_forwarding_disabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
OutboundSpamPolicy,
|
||||
)
|
||||
|
||||
defender_client.outbound_spam_policies = {
|
||||
"PolicyX": OutboundSpamPolicy(
|
||||
default=False,
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["test@correo.com"],
|
||||
notify_sender_blocked_addresses=["test@correo.com"],
|
||||
auto_forwarding_mode=False,
|
||||
)
|
||||
}
|
||||
defender_client.outbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_outbound_policy_forwarding_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Outbound Spam Policy PolicyX does allow mail forwarding."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== defender_client.outbound_spam_policies["PolicyX"].dict()
|
||||
)
|
||||
assert result[0].resource_name == "Defender Outbound Spam Policy"
|
||||
assert result[0].resource_id == "PolicyX"
|
||||
assert result[0].location == "global"
|
||||
@@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_antispam_policy_inbound_no_allowed_domains:
|
||||
def test_policy_without_allowed_domains(self):
|
||||
def test_case_1_default_policy_no_allowed_domains(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -31,26 +31,240 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains:
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=[],
|
||||
)
|
||||
]
|
||||
defender_client.inbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Inbound anti-spam policy Policy1 does not contain allowed domains."
|
||||
== "Default is the only policy and it does not contain allowed domains."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Inbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy1"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_with_allowed_domains(self):
|
||||
def test_case_2_all_policies_no_allowed_domains(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import (
|
||||
defender_antispam_policy_inbound_no_allowed_domains,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
DefenderInboundSpamPolicy,
|
||||
InboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=[],
|
||||
),
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
default=False,
|
||||
allowed_sender_domains=[],
|
||||
),
|
||||
]
|
||||
defender_client.inbound_spam_rules = {
|
||||
"Policy1": InboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and it does not contain allowed domains, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Inbound Spam policy Policy1 does not contain allowed domains and includes users: user1@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy does not contain allowed domains, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.inbound_spam_policies[1].dict()
|
||||
|
||||
def test_case_3_default_ok_custom_with_allowed_domains(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import (
|
||||
defender_antispam_policy_inbound_no_allowed_domains,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
DefenderInboundSpamPolicy,
|
||||
InboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=[],
|
||||
),
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
default=False,
|
||||
allowed_sender_domains=["spam.com"],
|
||||
),
|
||||
]
|
||||
defender_client.inbound_spam_rules = {
|
||||
"Policy1": InboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=2,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and it does not contain allowed domains, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Inbound Spam policy Policy1 contains allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 2 (0 is the highest). However, the default policy does not contain allowed domains, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.inbound_spam_policies[1].dict()
|
||||
|
||||
def test_case_4_default_with_allowed_domains_custom_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import (
|
||||
defender_antispam_policy_inbound_no_allowed_domains,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
DefenderInboundSpamPolicy,
|
||||
InboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=["example.org"],
|
||||
),
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
default=False,
|
||||
allowed_sender_domains=[],
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.inbound_spam_rules = {
|
||||
"Policy1": InboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and it contains allowed domains: example.org, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Inbound Spam policy Policy1 does not contain allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy contains allowed domains, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.inbound_spam_policies[1].dict()
|
||||
|
||||
def test_case_5_default_with_allowed_domains(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -77,24 +291,96 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains:
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy2",
|
||||
allowed_sender_domains=["bad-domain.com"],
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=["example.com"],
|
||||
)
|
||||
]
|
||||
defender_client.inbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Inbound anti-spam policy Policy2 contains allowed domains: ['bad-domain.com']."
|
||||
== "Default is the only policy and it contains allowed domains: example.com."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Inbound Spam Policy"
|
||||
assert result[0].resource_id == "Policy2"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_case_6_both_default_and_custom_with_allowed_domains(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_antispam_policy_inbound_no_allowed_domains.defender_antispam_policy_inbound_no_allowed_domains import (
|
||||
defender_antispam_policy_inbound_no_allowed_domains,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
DefenderInboundSpamPolicy,
|
||||
InboundSpamRule,
|
||||
)
|
||||
|
||||
defender_client.inbound_spam_policies = [
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Default",
|
||||
default=True,
|
||||
allowed_sender_domains=["example.com"],
|
||||
),
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
default=False,
|
||||
allowed_sender_domains=["spam.com"],
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.inbound_spam_rules = {
|
||||
"Policy1": InboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=5,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and it contains allowed domains: example.com, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.inbound_spam_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Inbound Spam policy Policy1 contains allowed domains and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 5 (0 is the highest). Also, the default policy contains allowed domains, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.inbound_spam_policies[1].dict()
|
||||
|
||||
def test_no_inbound_spam_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
@@ -119,8 +405,8 @@ class Test_defender_antispam_policy_inbound_no_allowed_domains:
|
||||
)
|
||||
|
||||
defender_client.inbound_spam_policies = []
|
||||
defender_client.inbound_spam_rules = {}
|
||||
|
||||
check = defender_antispam_policy_inbound_no_allowed_domains()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
@@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_malware_policy_common_attachments_filter_enabled:
|
||||
def test_enable_file_filter_disabled(self):
|
||||
def test_case_1_only_default_policy_enabled(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -31,30 +31,29 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=False,
|
||||
identity="Policy1",
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "js"],
|
||||
is_default=True,
|
||||
),
|
||||
)
|
||||
]
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is not enabled in anti-malware policy Policy1."
|
||||
== "Default is the only policy and Common Attachment Types Filter is enabled."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_enable_file_filter_enabled(self):
|
||||
def test_case_2_all_policies_enabled(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -77,34 +76,345 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
identity="Policy1",
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "js"],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is enabled in anti-malware policy Policy1."
|
||||
== "Default is the default policy and Common Attachment Types Filter is enabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_no_policy(self):
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 enables Common Attachment Types Filter and includes users: user1@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy enables the filter, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_3_default_ok_custom_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import (
|
||||
defender_malware_policy_common_attachments_filter_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["js"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=2,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is enabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 does not enable Common Attachment Types Filter and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 2 (0 is the highest). However, the default policy enables the filter, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_4_default_not_ok_custom_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import (
|
||||
defender_malware_policy_common_attachments_filter_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is not enabled, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 enables Common Attachment Types Filter and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_5_only_default_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import (
|
||||
defender_malware_policy_common_attachments_filter_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and Common Attachment Types Filter is not enabled."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
def test_case_6_default_and_custom_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_common_attachments_filter_enabled.defender_malware_policy_common_attachments_filter_enabled import (
|
||||
defender_malware_policy_common_attachments_filter_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=5,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is not enabled, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 does not enable Common Attachment Types Filter and includes users: user1@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 5 (0 is the highest). Also, the default policy does not enable the filter, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_no_malware_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -127,16 +437,8 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
|
||||
)
|
||||
|
||||
defender_client.malware_policies = []
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = defender_malware_policy_common_attachments_filter_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is not enabled."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
assert len(result) == 0
|
||||
@@ -1,53 +1,10 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_malware_policy_comprehensive_attachments_filter_applied:
|
||||
def test_no_policy(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
defender_client.malware_policies = []
|
||||
defender_client.audit_config = {}
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is not enabled."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_enabled_all_extensions_blocked(self):
|
||||
def test_case_1_default_policy_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -68,192 +25,430 @@ class Test_defender_malware_policy_comprehensive_attachments_filter_applied:
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
|
||||
valid_extensions = ["exe", "bat", "js"]
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": valid_extensions
|
||||
}
|
||||
defender_client.malware_rules = {"PolicyGood": MalwareRule(state="Enabled")}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="PolicyGood",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=valid_extensions,
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is enabled in anti-malware policy PolicyGood, the policy is enabled and the filter is applied to the recommended file types."
|
||||
)
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_enabled_missing_extensions(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat", "js"]
|
||||
}
|
||||
defender_client.malware_rules = {
|
||||
"PolicyPartial": MalwareRule(state="Enabled")
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
identity="PolicyPartial",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"],
|
||||
file_types=["exe", "bat", "js"],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is enabled in anti-malware policy PolicyPartial, but the following recommended file types are missing: bat, js."
|
||||
)
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_enabled_but_rule_disabled(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
|
||||
valid_extensions = ["exe"]
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": valid_extensions
|
||||
}
|
||||
defender_client.malware_rules = {
|
||||
"PolicyDisabled": MalwareRule(state="Disabled")
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="PolicyDisabled",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=valid_extensions,
|
||||
is_default=False,
|
||||
)
|
||||
]
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is enabled in anti-malware policy PolicyDisabled, but the policy is disabled."
|
||||
)
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_policy_enabled_but_no_rule_and_not_default(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
|
||||
valid_extensions = ["exe"]
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": valid_extensions
|
||||
}
|
||||
defender_client.malware_rules = {}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="PolicyNoRule",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=valid_extensions,
|
||||
is_default=False,
|
||||
)
|
||||
]
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and Common Attachment Types Filter is properly configured."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
def test_case_2_all_policies_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat"]
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "bat"],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Custom1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "bat"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
defender_client.malware_rules = {
|
||||
"Custom1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is properly configured, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Custom1 is properly configured and includes users: user1@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Custom1"
|
||||
assert result[1].resource_id == "Custom1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_3_default_ok_custom_not(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat"]
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "bat"],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Custom1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"], # missing bat
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
defender_client.malware_rules = {
|
||||
"Custom1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is properly configured, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Custom1 is not properly configured and includes users: user1@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 1 (0 is the highest). Missing recommended file types: bat. However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Custom1"
|
||||
assert result[1].resource_id == "Custom1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_4_default_not_ok_custom_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat"]
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Custom1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe", "bat"],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
defender_client.malware_rules = {
|
||||
"Custom1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user1@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is not properly configured, but it could be overridden by another well-configured Custom Policy. Missing recommended file types: exe, bat."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Custom1 is properly configured and includes users: user1@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Custom1"
|
||||
assert result[1].resource_id == "Custom1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_5_only_default_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat"]
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Common Attachment Types Filter is not properly configured in anti-malware policy PolicyNoRule."
|
||||
== "Default is the only policy and Common Attachment Types Filter is not properly configured. Missing recommended file types: exe, bat."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_case_6_default_and_custom_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.audit_config = {
|
||||
"recommended_blocked_file_types": ["exe", "bat"]
|
||||
}
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Custom1",
|
||||
enable_file_filter=True,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=["exe"], # missing bat
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
defender_client.malware_rules = {
|
||||
"Custom1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=2,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["example.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and Common Attachment Types Filter is not properly configured, but it could be overridden by another well-configured Custom Policy. Missing recommended file types: exe, bat."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Custom1 is not properly configured and includes users: user@example.com; groups: group1; domains: example.com, "
|
||||
"with priority 2 (0 is the highest). Missing recommended file types: bat. Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Custom1"
|
||||
assert result[1].resource_id == "Custom1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_no_malware_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
defender_client.malware_policies = []
|
||||
defender_client.malware_rules = {}
|
||||
defender_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_comprehensive_attachments_filter_applied.defender_malware_policy_comprehensive_attachments_filter_applied import (
|
||||
defender_malware_policy_comprehensive_attachments_filter_applied,
|
||||
)
|
||||
|
||||
check = defender_malware_policy_comprehensive_attachments_filter_applied()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
@@ -4,7 +4,7 @@ from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
|
||||
def test_notifications_disabled(self):
|
||||
def test_case_1_default_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -31,32 +31,322 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the only policy and notifications for internal users sending malware are enabled."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
def test_case_2_default_and_custom_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and notifications for internal users sending malware are enabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 is properly configured and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 1 (0 is the highest). Also, the default policy is properly configured, so entities not included by this custom policy could still be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_3_default_ok_custom_not_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=2,
|
||||
users=["internal@example.com"],
|
||||
groups=["group2"],
|
||||
domains=["example.org"],
|
||||
)
|
||||
}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and notifications for internal users sending malware are enabled, but it could be overridden by another misconfigured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 is not properly configured and includes users: internal@example.com; groups: group2; domains: example.org, "
|
||||
"with priority 2 (0 is the highest). However, the default policy is properly configured, so entities not included by this custom policy could be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_4_default_not_ok_custom_properly_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="admin@example.com",
|
||||
file_types=[],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=0,
|
||||
users=["user@example.com"],
|
||||
groups=["group1"],
|
||||
domains=["domain.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Default is the default policy and notifications for internal users sending malware are not enabled, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
|
||||
assert result[1].status == "PASS"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 is properly configured and includes users: user@example.com; groups: group1; domains: domain.com, "
|
||||
"with priority 0 (0 is the highest). However, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_case_5_only_default_not_ok(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
identity="Default",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Notifications for internal users sending malware are not enabled."
|
||||
== "Default is the only policy and notifications for internal users sending malware are not enabled."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_notifications_enabled_without_email(self):
|
||||
def test_case_6_default_and_custom_not_configured(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -79,36 +369,64 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="Default",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
),
|
||||
MalwarePolicy(
|
||||
identity="Policy1",
|
||||
enable_file_filter=False,
|
||||
enable_internal_sender_admin_notifications=False,
|
||||
internal_sender_admin_address="",
|
||||
file_types=[],
|
||||
is_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
defender_client.malware_rules = {
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=3,
|
||||
users=["u@example.com"],
|
||||
groups=["g1"],
|
||||
domains=["d.com"],
|
||||
)
|
||||
}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Notifications for internal users sending malware are enabled, but no email addresses are configured."
|
||||
== "Default is the default policy and notifications for internal users sending malware are not enabled, but it could be overridden by another well-configured Custom Policy."
|
||||
)
|
||||
assert result[0].resource_name == "Default"
|
||||
assert result[0].resource_id == "Default"
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_notifications_enabled_with_email(self):
|
||||
assert result[1].status == "FAIL"
|
||||
assert (
|
||||
result[1].status_extended
|
||||
== "Custom Malware policy Policy1 is not properly configured and includes users: u@example.com; groups: g1; domains: d.com, "
|
||||
"with priority 3 (0 is the highest). Also, the default policy is not properly configured, so entities not included by this custom policy could not be correctly protected."
|
||||
)
|
||||
assert result[1].resource_name == "Policy1"
|
||||
assert result[1].resource_id == "Policy1"
|
||||
assert result[1].resource == defender_client.malware_policies[1].dict()
|
||||
|
||||
def test_no_malware_policies(self):
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.audited_tenant = "audited_tenant"
|
||||
defender_client.audited_domain = DOMAIN
|
||||
@@ -129,33 +447,12 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
|
||||
from prowler.providers.m365.services.defender.defender_malware_policy_notifications_internal_users_malware_enabled.defender_malware_policy_notifications_internal_users_malware_enabled import (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled,
|
||||
)
|
||||
from prowler.providers.m365.services.defender.defender_service import (
|
||||
MalwarePolicy,
|
||||
)
|
||||
|
||||
defender_client.malware_policies = [
|
||||
MalwarePolicy(
|
||||
enable_file_filter=True,
|
||||
identity="Default",
|
||||
enable_internal_sender_admin_notifications=True,
|
||||
internal_sender_admin_address="security@example.com",
|
||||
file_types=[],
|
||||
is_default=True,
|
||||
)
|
||||
]
|
||||
defender_client.malware_policies = []
|
||||
defender_client.malware_rules = {}
|
||||
|
||||
check = (
|
||||
defender_malware_policy_notifications_internal_users_malware_enabled()
|
||||
)
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Notifications for internal users sending malware are enabled."
|
||||
)
|
||||
assert result[0].resource == defender_client.malware_policies[0].dict()
|
||||
assert result[0].resource_name == "Defender Malware Policy"
|
||||
assert result[0].resource_id == "defenderMalwarePolicy"
|
||||
assert result[0].location == "global"
|
||||
assert len(result) == 0
|
||||
@@ -10,6 +10,7 @@ from prowler.providers.m365.services.defender.defender_service import (
|
||||
DefenderInboundSpamPolicy,
|
||||
DkimConfig,
|
||||
DomainServiceConfiguration,
|
||||
InboundSpamRule,
|
||||
MalwarePolicy,
|
||||
MalwareRule,
|
||||
OutboundSpamPolicy,
|
||||
@@ -42,14 +43,27 @@ def mock_defender_get_malware_filter_policy(_):
|
||||
|
||||
def mock_defender_get_malware_filter_rule(_):
|
||||
return {
|
||||
"Policy1": MalwareRule(state="Enabled"),
|
||||
"Policy2": MalwareRule(state="Disabled"),
|
||||
"Policy1": MalwareRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
"Policy2": MalwareRule(
|
||||
state="Disabled",
|
||||
priority=2,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def mock_defender_get_antiphising_policy(_):
|
||||
def mock_defender_get_antiphishing_policy(_):
|
||||
return {
|
||||
"Policy1": AntiphishingPolicy(
|
||||
name="Policy1",
|
||||
spoof_intelligence=True,
|
||||
spoof_intelligence_action="Quarantine",
|
||||
dmarc_reject_action="Reject",
|
||||
@@ -61,6 +75,7 @@ def mock_defender_get_antiphising_policy(_):
|
||||
default=False,
|
||||
),
|
||||
"Policy2": AntiphishingPolicy(
|
||||
name="Policy2",
|
||||
spoof_intelligence=False,
|
||||
spoof_intelligence_action="None",
|
||||
dmarc_reject_action="None",
|
||||
@@ -74,13 +89,21 @@ def mock_defender_get_antiphising_policy(_):
|
||||
}
|
||||
|
||||
|
||||
def mock_defender_get_antiphising_rules(_):
|
||||
def mock_defender_get_antiphishing_rules(_):
|
||||
return {
|
||||
"Policy1": AntiphishingRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
"Policy2": AntiphishingRule(
|
||||
state="Disabled",
|
||||
priority=2,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
}
|
||||
|
||||
@@ -90,14 +113,35 @@ def mock_defender_get_inbound_spam_policy(_):
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy1",
|
||||
allowed_sender_domains=[],
|
||||
default=False,
|
||||
),
|
||||
DefenderInboundSpamPolicy(
|
||||
identity="Policy2",
|
||||
allowed_sender_domains=["example.com"],
|
||||
default=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def mock_defender_get_inbound_spam_rule(_):
|
||||
return {
|
||||
"Policy1": InboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
"Policy2": InboundSpamRule(
|
||||
state="Disabled",
|
||||
priority=2,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def mock_defender_get_connection_filter_policy(_):
|
||||
return ConnectionFilterPolicy(
|
||||
ip_allow_list=[],
|
||||
@@ -132,6 +176,7 @@ def mock_defender_get_report_submission_policy(_):
|
||||
def mock_defender_get_outbound_spam_filter_policy(_):
|
||||
return {
|
||||
"Policy1": OutboundSpamPolicy(
|
||||
name="Policy1",
|
||||
notify_sender_blocked=True,
|
||||
notify_limit_exceeded=True,
|
||||
notify_limit_exceeded_addresses=["security@example.com"],
|
||||
@@ -140,6 +185,7 @@ def mock_defender_get_outbound_spam_filter_policy(_):
|
||||
default=False,
|
||||
),
|
||||
"Policy2": OutboundSpamPolicy(
|
||||
name="Policy2",
|
||||
notify_sender_blocked=False,
|
||||
notify_limit_exceeded=False,
|
||||
notify_limit_exceeded_addresses=[],
|
||||
@@ -154,9 +200,17 @@ def mock_defender_get_outbound_spam_filter_rule(_):
|
||||
return {
|
||||
"Policy1": OutboundSpamRule(
|
||||
state="Enabled",
|
||||
priority=1,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
"Policy2": OutboundSpamRule(
|
||||
state="Disabled",
|
||||
priority=2,
|
||||
users=["test@example.com"],
|
||||
groups=["example_group"],
|
||||
domains=["example.com"],
|
||||
),
|
||||
}
|
||||
|
||||
@@ -251,14 +305,22 @@ class Test_Defender_Service:
|
||||
)
|
||||
malware_rules = defender_client.malware_rules
|
||||
assert malware_rules["Policy1"].state == "Enabled"
|
||||
assert malware_rules["Policy1"].priority == 1
|
||||
assert malware_rules["Policy1"].users == ["test@example.com"]
|
||||
assert malware_rules["Policy1"].groups == ["example_group"]
|
||||
assert malware_rules["Policy1"].domains == ["example.com"]
|
||||
assert malware_rules["Policy2"].state == "Disabled"
|
||||
assert malware_rules["Policy2"].priority == 2
|
||||
assert malware_rules["Policy2"].users == ["test@example.com"]
|
||||
assert malware_rules["Policy2"].groups == ["example_group"]
|
||||
assert malware_rules["Policy2"].domains == ["example.com"]
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_antiphising_policy",
|
||||
new=mock_defender_get_antiphising_policy,
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_antiphishing_policy",
|
||||
new=mock_defender_get_antiphishing_policy,
|
||||
)
|
||||
def test_get_antiphising_policy(self):
|
||||
def test_get_antiphishing_policy(self):
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
@@ -270,6 +332,7 @@ class Test_Defender_Service:
|
||||
)
|
||||
)
|
||||
antiphishing_policies = defender_client.antiphishing_policies
|
||||
assert antiphishing_policies["Policy1"].name == "Policy1"
|
||||
assert antiphishing_policies["Policy1"].spoof_intelligence is True
|
||||
assert (
|
||||
antiphishing_policies["Policy1"].spoof_intelligence_action
|
||||
@@ -286,6 +349,7 @@ class Test_Defender_Service:
|
||||
assert antiphishing_policies["Policy1"].show_tag is True
|
||||
assert antiphishing_policies["Policy1"].honor_dmarc_policy is True
|
||||
assert antiphishing_policies["Policy1"].default is False
|
||||
assert antiphishing_policies["Policy2"].name == "Policy2"
|
||||
assert antiphishing_policies["Policy2"].spoof_intelligence is False
|
||||
assert antiphishing_policies["Policy2"].spoof_intelligence_action == "None"
|
||||
assert antiphishing_policies["Policy2"].dmarc_reject_action == "None"
|
||||
@@ -300,10 +364,10 @@ class Test_Defender_Service:
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_antiphising_rules",
|
||||
new=mock_defender_get_antiphising_rules,
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_antiphishing_rules",
|
||||
new=mock_defender_get_antiphishing_rules,
|
||||
)
|
||||
def test_get_antiphising_rules(self):
|
||||
def test_get_antiphishing_rules(self):
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
@@ -314,9 +378,17 @@ class Test_Defender_Service:
|
||||
identity=M365IdentityInfo(tenant_domain=DOMAIN)
|
||||
)
|
||||
)
|
||||
antiphishing_rules = defender_client.antiphising_rules
|
||||
antiphishing_rules = defender_client.antiphishing_rules
|
||||
assert antiphishing_rules["Policy1"].state == "Enabled"
|
||||
assert antiphishing_rules["Policy1"].priority == 1
|
||||
assert antiphishing_rules["Policy1"].users == ["test@example.com"]
|
||||
assert antiphishing_rules["Policy1"].groups == ["example_group"]
|
||||
assert antiphishing_rules["Policy1"].domains == ["example.com"]
|
||||
assert antiphishing_rules["Policy2"].state == "Disabled"
|
||||
assert antiphishing_rules["Policy2"].priority == 2
|
||||
assert antiphishing_rules["Policy2"].users == ["test@example.com"]
|
||||
assert antiphishing_rules["Policy2"].groups == ["example_group"]
|
||||
assert antiphishing_rules["Policy2"].domains == ["example.com"]
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
@@ -378,6 +450,7 @@ class Test_Defender_Service:
|
||||
)
|
||||
)
|
||||
outbound_spam_policies = defender_client.outbound_spam_policies
|
||||
assert outbound_spam_policies["Policy1"].name == "Policy1"
|
||||
assert outbound_spam_policies["Policy1"].notify_sender_blocked is True
|
||||
assert outbound_spam_policies["Policy1"].notify_limit_exceeded is True
|
||||
assert outbound_spam_policies[
|
||||
@@ -388,6 +461,7 @@ class Test_Defender_Service:
|
||||
].notify_sender_blocked_addresses == ["security@example.com"]
|
||||
assert outbound_spam_policies["Policy1"].auto_forwarding_mode is False
|
||||
assert outbound_spam_policies["Policy1"].default is False
|
||||
assert outbound_spam_policies["Policy2"].name == "Policy2"
|
||||
assert outbound_spam_policies["Policy2"].notify_sender_blocked is False
|
||||
assert outbound_spam_policies["Policy2"].notify_limit_exceeded is False
|
||||
assert (
|
||||
@@ -398,6 +472,7 @@ class Test_Defender_Service:
|
||||
)
|
||||
assert outbound_spam_policies["Policy2"].auto_forwarding_mode is True
|
||||
assert outbound_spam_policies["Policy2"].default is True
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_outbound_spam_filter_rule",
|
||||
@@ -416,7 +491,16 @@ class Test_Defender_Service:
|
||||
)
|
||||
outbound_spam_rules = defender_client.outbound_spam_rules
|
||||
assert outbound_spam_rules["Policy1"].state == "Enabled"
|
||||
assert outbound_spam_rules["Policy1"].priority == 1
|
||||
assert outbound_spam_rules["Policy1"].users == ["test@example.com"]
|
||||
assert outbound_spam_rules["Policy1"].groups == ["example_group"]
|
||||
assert outbound_spam_rules["Policy1"].domains == ["example.com"]
|
||||
assert outbound_spam_rules["Policy2"].state == "Disabled"
|
||||
assert outbound_spam_rules["Policy2"].priority == 2
|
||||
assert outbound_spam_rules["Policy2"].users == ["test@example.com"]
|
||||
assert outbound_spam_rules["Policy2"].groups == ["example_group"]
|
||||
assert outbound_spam_rules["Policy2"].domains == ["example.com"]
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_inbound_spam_filter_policy",
|
||||
@@ -438,6 +522,34 @@ class Test_Defender_Service:
|
||||
assert inbound_spam_policies[1].allowed_sender_domains == ["example.com"]
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_inbound_spam_filter_rule",
|
||||
new=mock_defender_get_inbound_spam_rule,
|
||||
)
|
||||
def test__get_inbound_spam_filter_rule(self):
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
):
|
||||
defender_client = Defender(
|
||||
set_mocked_m365_provider(
|
||||
identity=M365IdentityInfo(tenant_domain=DOMAIN)
|
||||
)
|
||||
)
|
||||
inbound_spam_rules = defender_client.inbound_spam_rules
|
||||
assert inbound_spam_rules["Policy1"].state == "Enabled"
|
||||
assert inbound_spam_rules["Policy1"].priority == 1
|
||||
assert inbound_spam_rules["Policy1"].users == ["test@example.com"]
|
||||
assert inbound_spam_rules["Policy1"].groups == ["example_group"]
|
||||
assert inbound_spam_rules["Policy1"].domains == ["example.com"]
|
||||
assert inbound_spam_rules["Policy2"].state == "Disabled"
|
||||
assert inbound_spam_rules["Policy2"].priority == 2
|
||||
assert inbound_spam_rules["Policy2"].users == ["test@example.com"]
|
||||
assert inbound_spam_rules["Policy2"].groups == ["example_group"]
|
||||
assert inbound_spam_rules["Policy2"].domains == ["example.com"]
|
||||
defender_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.services.defender.defender_service.Defender._get_report_submission_policy",
|
||||
new=mock_defender_get_report_submission_policy,
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [v1.7.0] (Prowler v5.7.0) – Not released
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Add a new chart to show the split between passed and failed findings. [(#7680)](https://github.com/prowler-cloud/prowler/pull/7680)
|
||||
- Added `Accordion` component. [(#7700)](https://github.com/prowler-cloud/prowler/pull/7700)
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix form validation in launch scan workflow. [(#7693)](https://github.com/prowler-cloud/prowler/pull/7693)
|
||||
- Moved ProviderType to a shared types file and replaced all occurrences across the codebase. [(#7710)](https://github.com/prowler-cloud/prowler/pull/7710)
|
||||
|
||||
---
|
||||
|
||||
## [v1.6.0] (Prowler v5.6.0)
|
||||
|
||||
@@ -14,12 +14,8 @@ import {
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import {
|
||||
FindingProps,
|
||||
ProviderProps,
|
||||
ScanProps,
|
||||
SearchParamsProps,
|
||||
} from "@/types/components";
|
||||
import { ProviderProps } from "@/types";
|
||||
import { FindingProps, ScanProps, SearchParamsProps } from "@/types/components";
|
||||
|
||||
export default async function Findings({
|
||||
searchParams,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Divider } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
|
||||
import { ProviderType } from "@/types";
|
||||
interface ComplianceScanInfoProps {
|
||||
scan: {
|
||||
providerInfo: {
|
||||
provider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
provider: ProviderType;
|
||||
alias?: string;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ export const filterScans = [
|
||||
{
|
||||
key: "provider_type__in",
|
||||
labelCheckboxGroup: "Cloud Provider",
|
||||
values: ["aws", "azure", "gcp", "kubernetes"],
|
||||
values: ["aws", "azure", "m365", "gcp", "kubernetes"],
|
||||
},
|
||||
{
|
||||
key: "state__in",
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SeverityBadge,
|
||||
StatusFindingBadge,
|
||||
} from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
import { FindingProps, ProviderType } from "@/types";
|
||||
|
||||
import { Muted } from "../muted";
|
||||
import { DeltaIndicator } from "./delta-indicator";
|
||||
@@ -215,7 +215,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
return (
|
||||
<>
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
/>
|
||||
|
||||
@@ -6,12 +6,9 @@ import Link from "next/link";
|
||||
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
|
||||
import { InfoField } from "@/components/ui/entities";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import {
|
||||
getProviderLogo,
|
||||
type ProviderType,
|
||||
} from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderLogo } from "@/components/ui/entities/get-provider-logo";
|
||||
import { SeverityBadge } from "@/components/ui/table/severity-badge";
|
||||
import { FindingProps } from "@/types";
|
||||
import { FindingProps, ProviderType } from "@/types";
|
||||
|
||||
import { Muted } from "../muted";
|
||||
import { DeltaIndicator } from "./delta-indicator";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { InfoIcon } from "@/components/icons";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
import { FindingProps, ProviderType } from "@/types";
|
||||
|
||||
import { Muted } from "../../../findings/muted";
|
||||
|
||||
@@ -162,7 +162,7 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
return (
|
||||
<>
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
import { ConnectionFalse, ConnectionPending, ConnectionTrue } from "../icons";
|
||||
import { getProviderLogo, ProviderType } from "../ui/entities";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { ConnectionFalse, ConnectionPending, ConnectionTrue } from "../icons";
|
||||
import { getProviderLogo } from "../ui/entities";
|
||||
interface ProviderInfoProps {
|
||||
connected: boolean | null;
|
||||
provider: ProviderType;
|
||||
|
||||
@@ -9,8 +9,8 @@ import * as z from "zod";
|
||||
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { ProviderType } from "@/components/ui/entities";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { addProvider } from "../../../../actions/providers/providers";
|
||||
import { addProviderFormSchema, ApiError } from "../../../../types";
|
||||
|
||||
@@ -18,9 +18,9 @@ import { getTask } from "@/actions/task/tasks";
|
||||
import { CheckIcon, RocketIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { ProviderType } from "@/components/ui/entities";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { checkTaskStatus } from "@/lib/helper";
|
||||
import { ProviderType } from "@/types";
|
||||
import { ApiError, testConnectionFormSchema } from "@/types";
|
||||
|
||||
import { ProviderInfo } from "../..";
|
||||
|
||||
@@ -10,8 +10,8 @@ import * as z from "zod";
|
||||
import { updateCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { ProviderType } from "@/components/ui/entities";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderType } from "@/types";
|
||||
import {
|
||||
addCredentialsFormSchema,
|
||||
ApiError,
|
||||
|
||||
@@ -10,8 +10,8 @@ import * as z from "zod";
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { ProviderType } from "@/components/ui/entities";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderType } from "@/types";
|
||||
import {
|
||||
addCredentialsFormSchema,
|
||||
ApiError,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { getProviderName } from "@/components/ui/entities/get-provider-logo";
|
||||
import { ProviderType } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderLogo } from "@/components/ui/entities/get-provider-logo";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
export const ProviderTitleDocs = ({
|
||||
providerType,
|
||||
|
||||
@@ -26,8 +26,16 @@ export const LaunchScanWorkflow = ({
|
||||
}: {
|
||||
providers: ProviderInfo[];
|
||||
}) => {
|
||||
const formSchema = onDemandScanFormSchema();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
const formSchema = z.object({
|
||||
...onDemandScanFormSchema().shape,
|
||||
scanName: z
|
||||
.string()
|
||||
.min(3, "Must have at least 3 characters")
|
||||
.or(z.literal(""))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId: "",
|
||||
@@ -77,25 +85,25 @@ export const LaunchScanWorkflow = ({
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
className="flex flex-wrap justify-start gap-4"
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
<div className="col-span-3">
|
||||
<SelectScanProvider
|
||||
providers={providers}
|
||||
control={form.control}
|
||||
name="providerId"
|
||||
/>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
<>
|
||||
<div className="w-72">
|
||||
<SelectScanProvider
|
||||
providers={providers}
|
||||
control={form.control}
|
||||
name="providerId"
|
||||
/>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-6 md:gap-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="col-span-2 self-end"
|
||||
className="min-w-48 self-end"
|
||||
>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
@@ -115,36 +123,35 @@ export const LaunchScanWorkflow = ({
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="col-span-4 flex items-end gap-4"
|
||||
className="flex items-end gap-4"
|
||||
>
|
||||
<div className="flex flex-row items-end gap-4">
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Start scan now"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="sm"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <RocketIcon size={16} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
onPress={() => form.reset()}
|
||||
className="w-fit border-gray-200 bg-transparent"
|
||||
ariaLabel="Clear form"
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
</div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Start scan now"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="sm"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <RocketIcon size={16} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
onPress={() => form.reset()}
|
||||
className="w-fit border-gray-200 bg-transparent"
|
||||
ariaLabel="Clear form"
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/*
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/*
|
||||
<div className="flex flex-col justify-start">
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
@@ -169,7 +176,6 @@ export const LaunchScanWorkflow = ({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div> */}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
|
||||
import { downloadScanZip } from "@/lib/helper";
|
||||
import { ScanProps } from "@/types";
|
||||
import { ProviderType, ScanProps } from "@/types";
|
||||
|
||||
import { LinkToFindingsFromScan } from "../../link-to-findings-from-scan";
|
||||
import { TriggerIcon } from "../../trigger-icon";
|
||||
@@ -60,7 +60,7 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
|
||||
return (
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias}
|
||||
entityId={uid}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Accordion as NextUIAccordion,
|
||||
AccordionItem,
|
||||
Selection,
|
||||
} from "@nextui-org/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import React, { ReactNode, useCallback, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface AccordionItemProps {
|
||||
key: string;
|
||||
title: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
content: ReactNode;
|
||||
items?: AccordionItemProps[];
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AccordionProps {
|
||||
items: AccordionItemProps[];
|
||||
variant?: "light" | "shadow" | "bordered" | "splitted";
|
||||
className?: string;
|
||||
defaultExpandedKeys?: string[];
|
||||
selectionMode?: "single" | "multiple";
|
||||
isCompact?: boolean;
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
const AccordionContent = ({
|
||||
content,
|
||||
items,
|
||||
}: {
|
||||
content: ReactNode;
|
||||
items?: AccordionItemProps[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{content}
|
||||
{items && items.length > 0 && (
|
||||
<div className="ml-2 mt-4 border-l-2 border-gray-200 pl-4 dark:border-gray-700">
|
||||
<Accordion
|
||||
items={items}
|
||||
variant="light"
|
||||
isCompact
|
||||
selectionMode="multiple"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Accordion = ({
|
||||
items,
|
||||
variant = "light",
|
||||
className,
|
||||
defaultExpandedKeys = [],
|
||||
selectionMode = "single",
|
||||
isCompact = false,
|
||||
showDivider = true,
|
||||
}: AccordionProps) => {
|
||||
const [expandedKeys, setExpandedKeys] = useState<Selection>(
|
||||
new Set(defaultExpandedKeys),
|
||||
);
|
||||
|
||||
const handleSelectionChange = useCallback((keys: Selection) => {
|
||||
setExpandedKeys(keys);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NextUIAccordion
|
||||
className={cn("w-full", className)}
|
||||
variant={variant}
|
||||
selectionMode={selectionMode}
|
||||
selectedKeys={expandedKeys}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
isCompact={isCompact}
|
||||
showDivider={showDivider}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<AccordionItem
|
||||
key={item.key}
|
||||
aria-label={
|
||||
typeof item.title === "string" ? item.title : `Item ${item.key}`
|
||||
}
|
||||
title={item.title}
|
||||
subtitle={item.subtitle}
|
||||
isDisabled={item.isDisabled}
|
||||
indicator={<ChevronDown className="text-gray-500" />}
|
||||
classNames={{
|
||||
base: index === 0 || index === 1 ? "my-2" : "my-1",
|
||||
title: "text-sm font-medium",
|
||||
subtitle: "text-xs text-gray-500",
|
||||
trigger:
|
||||
"p-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50",
|
||||
content: "p-2",
|
||||
}}
|
||||
>
|
||||
<AccordionContent content={item.content} items={item.items} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</NextUIAccordion>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.displayName = "Accordion";
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HorizontalSplitBarProps {
|
||||
/**
|
||||
* First value (left)
|
||||
*/
|
||||
valueA: number;
|
||||
/**
|
||||
* Second value (right)
|
||||
*/
|
||||
valueB: number;
|
||||
/**
|
||||
* Additional CSS classes for the main container
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Color for value A (Tailwind classes)
|
||||
* @default "bg-system-success"
|
||||
*/
|
||||
colorA?: string;
|
||||
/**
|
||||
* Color for value B (Tailwind classes)
|
||||
* @default "bg-system-error"
|
||||
*/
|
||||
colorB?: string;
|
||||
/**
|
||||
* Value format suffix (like "%", "$", etc.)
|
||||
* Will be appended to the values when displayed
|
||||
* @example "%"
|
||||
*/
|
||||
valueSuffix?: string;
|
||||
/**
|
||||
* Bar height
|
||||
* @default "h-4"
|
||||
*/
|
||||
barHeight?: string;
|
||||
/**
|
||||
* Color for the empty state (when both values are 0)
|
||||
* @default "bg-gray-300"
|
||||
*/
|
||||
emptyColor?: string;
|
||||
/**
|
||||
* Text to display when there is no data
|
||||
* @default "No data available"
|
||||
*/
|
||||
emptyText?: string;
|
||||
/**
|
||||
* Minimum width for small values (in pixels)
|
||||
* @default 25
|
||||
*/
|
||||
minBarWidth?: number;
|
||||
/**
|
||||
* Custom tooltip content for value A (optional)
|
||||
* If not provided, the formatted value will be used
|
||||
*/
|
||||
tooltipContentA?: string;
|
||||
/**
|
||||
* Custom tooltip content for value B (optional)
|
||||
* If not provided, the formatted value will be used
|
||||
*/
|
||||
tooltipContentB?: string;
|
||||
/**
|
||||
* Text color for labels
|
||||
* @default "text-gray-700"
|
||||
*/
|
||||
labelColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal split bar chart component that displays two values
|
||||
* with bars growing from a central separator.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <HorizontalSplitBar
|
||||
* valueA={54}
|
||||
* valueB={38}
|
||||
* valueSuffix="%"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const HorizontalSplitBar = ({
|
||||
valueA,
|
||||
valueB,
|
||||
className,
|
||||
colorA = "bg-system-success",
|
||||
colorB = "bg-system-error",
|
||||
valueSuffix = "",
|
||||
barHeight = "h-4",
|
||||
emptyColor = "bg-gray-300",
|
||||
emptyText = "No data available",
|
||||
minBarWidth = 25,
|
||||
tooltipContentA,
|
||||
tooltipContentB,
|
||||
labelColor = "text-gray-700",
|
||||
}: HorizontalSplitBarProps) => {
|
||||
// Reference to the container to measure its width
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [maxContainerWidth, setMaxContainerWidth] = useState(0);
|
||||
|
||||
// Effect to measure the container width
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const updateWidth = () => {
|
||||
const containerWidth = containerRef.current?.clientWidth || 0;
|
||||
setMaxContainerWidth(containerWidth);
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => window.removeEventListener("resize", updateWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Ensure values are positive
|
||||
const valA = Math.max(0, valueA);
|
||||
const valB = Math.max(0, valueB);
|
||||
|
||||
const hasNoData = valA === 0 && valB === 0;
|
||||
const formattedValueA = `${valA}${valueSuffix}`;
|
||||
const formattedValueB = `${valB}${valueSuffix}`;
|
||||
|
||||
if (hasNoData) {
|
||||
return (
|
||||
<div ref={containerRef} className={cn("flex w-full flex-col", className)}>
|
||||
<div className="flex w-full justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
`w-full ${barHeight} flex items-center justify-center rounded-full`,
|
||||
emptyColor,
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium text-gray-600">
|
||||
{emptyText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const availableWidth = Math.max(0, maxContainerWidth);
|
||||
const halfWidth = availableWidth / 2;
|
||||
const separatorWidth = 1;
|
||||
|
||||
let rawWidthA = valA;
|
||||
let rawWidthB = valB;
|
||||
|
||||
// Determine if we need to scale to fit in available space
|
||||
const maxSideWidth = halfWidth - separatorWidth / 2;
|
||||
const needsScaling = rawWidthA > maxSideWidth || rawWidthB > maxSideWidth;
|
||||
|
||||
if (needsScaling) {
|
||||
// Calculate scale factor based on the largest value
|
||||
const maxRawWidth = Math.max(rawWidthA, rawWidthB);
|
||||
const scaleFactor = maxSideWidth / maxRawWidth;
|
||||
|
||||
// Apply the scale factor to both sides
|
||||
rawWidthA = rawWidthA * scaleFactor;
|
||||
rawWidthB = rawWidthB * scaleFactor;
|
||||
}
|
||||
|
||||
// Apply minimum width if needed
|
||||
const barWidthA = Math.max(rawWidthA, valA > 0 ? minBarWidth : 0);
|
||||
const barWidthB = Math.max(rawWidthB, valB > 0 ? minBarWidth : 0);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn("flex w-full flex-col", className)}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div
|
||||
className="flex items-center justify-end gap-2"
|
||||
style={{ width: `${halfWidth}px` }}
|
||||
>
|
||||
{/* Left label */}
|
||||
<div
|
||||
className={cn("text-xs font-medium", labelColor)}
|
||||
aria-label={`${formattedValueA} ${tooltipContentA ? tooltipContentA : ""}`}
|
||||
>
|
||||
{valA > 0 ? formattedValueA : "0"}
|
||||
</div>
|
||||
{/* Left bar */}
|
||||
{valA > 0 && (
|
||||
<Tooltip
|
||||
content={`${formattedValueA} ${tooltipContentA ? tooltipContentA : ""}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<div
|
||||
className={cn(`${barHeight} rounded-l-full`, colorA)}
|
||||
style={{
|
||||
width: `${barWidthA}px`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Central separator */}
|
||||
<div
|
||||
className="flex-shrink-0 bg-background"
|
||||
style={{ width: `${separatorWidth}px` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-start gap-2"
|
||||
style={{ width: `${halfWidth}px` }}
|
||||
>
|
||||
{/* Right bar */}
|
||||
{valB > 0 && (
|
||||
<Tooltip
|
||||
content={`${formattedValueB} ${tooltipContentB ? tooltipContentB : ""}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<div
|
||||
className={cn(`${barHeight} rounded-r-full`, colorB)}
|
||||
style={{
|
||||
width: `${barWidthB}px`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Right label */}
|
||||
<div
|
||||
className={cn("text-xs font-medium", labelColor)}
|
||||
aria-label={`${formattedValueB} ${tooltipContentB ? tooltipContentB : ""}`}
|
||||
>
|
||||
{valB > 0 ? formattedValueB : "0"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HorizontalSplitBar;
|
||||
@@ -120,7 +120,7 @@ export const CustomInput = <T extends FieldValues>({
|
||||
/>
|
||||
</FormControl>
|
||||
{showFormMessage && (
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
<FormMessage className="max-w-full text-xs text-system-error dark:text-system-error" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { getProviderLogo } from "./get-provider-logo";
|
||||
import { SnippetId } from "./snippet-id";
|
||||
|
||||
interface EntityInfoProps {
|
||||
cloudProvider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
cloudProvider: ProviderType;
|
||||
entityAlias?: string;
|
||||
entityId?: string;
|
||||
hideCopyButton?: boolean;
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
|
||||
export type ProviderType = "aws" | "azure" | "gcp" | "kubernetes" | "m365";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
export const getProviderLogo = (provider: ProviderType) => {
|
||||
switch (provider) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./accordion/Accordion";
|
||||
export * from "./action-card/ActionCard";
|
||||
export * from "./alert/Alert";
|
||||
export * from "./alert-dialog/AlertDialog";
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"recharts": "^2.13.0-alpha.4",
|
||||
"recharts": "^2.15.2",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn-ui": "^0.2.3",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.23.8",
|
||||
@@ -9420,6 +9420,7 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -10402,9 +10403,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
@@ -13363,17 +13365,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
|
||||
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
@@ -13419,6 +13422,7 @@
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
@@ -13463,15 +13467,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.13.0-alpha.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz",
|
||||
"integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==",
|
||||
"version": "2.15.3",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
|
||||
"integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.0",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
@@ -13480,8 +13485,8 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts-scale": {
|
||||
@@ -14470,9 +14475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz",
|
||||
"integrity": "sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
|
||||
"integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"recharts": "^2.13.0-alpha.4",
|
||||
"recharts": "^2.15.2",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn-ui": "^0.2.3",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.23.8",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
import { ProviderType } from "./providers";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
};
|
||||
@@ -45,7 +47,7 @@ export interface CollapseMenuButtonProps {
|
||||
export interface SelectScanComplianceDataProps {
|
||||
scans: (ScanProps & {
|
||||
providerInfo: {
|
||||
provider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
@@ -174,27 +176,6 @@ export interface FindingsSeverityOverview {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProviderOverviewProps {
|
||||
data: {
|
||||
type: "provider-overviews";
|
||||
id: "aws" | "gcp" | "azure" | "kubernetes";
|
||||
attributes: {
|
||||
findings: {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
total: number;
|
||||
};
|
||||
resources: {
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskDetails {
|
||||
attributes: {
|
||||
state: string;
|
||||
@@ -509,51 +490,6 @@ export interface UserProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ProviderProps {
|
||||
id: string;
|
||||
type: "providers";
|
||||
attributes: {
|
||||
provider: "aws" | "azure" | "m365" | "gcp" | "kubernetes";
|
||||
uid: string;
|
||||
alias: string;
|
||||
status: "completed" | "pending" | "cancelled";
|
||||
resources: number;
|
||||
connection: {
|
||||
connected: boolean;
|
||||
last_checked_at: string;
|
||||
};
|
||||
scanner_args: {
|
||||
only_logs: boolean;
|
||||
excluded_checks: string[];
|
||||
aws_retries_max_attempts: number;
|
||||
};
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
created_by: {
|
||||
object: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
relationships: {
|
||||
secret: {
|
||||
data: {
|
||||
type: string;
|
||||
id: string;
|
||||
} | null;
|
||||
};
|
||||
provider_groups: {
|
||||
meta: {
|
||||
count: number;
|
||||
};
|
||||
data: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
groupNames?: string[];
|
||||
}
|
||||
|
||||
export interface ScanProps {
|
||||
type: "scans";
|
||||
id: string;
|
||||
@@ -596,7 +532,7 @@ export interface ScanProps {
|
||||
};
|
||||
};
|
||||
providerInfo?: {
|
||||
provider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||