Compare commits

..
Author SHA1 Message Date
Pablo Lara a75755c8c5 WIP: add change role to the user's invitations 2024-12-15 10:37:51 +01:00
Pablo Lara 3e0568f381 chore: add change role to the user's invitations 2024-12-13 12:38:08 +01:00
Pablo Lara fec66a3685 Merge branch 'master' into PRWLR-4669-Roles-Page-API-UI 2024-12-13 06:02:29 +01:00
Pablo Lara ba335de6b3 chore: fix error with exports 2024-12-11 08:30:12 +01:00
Pablo Lara 93051d55d5 feat: add role when invite an user 2024-12-10 18:14:57 +01:00
Pablo Lara 161c56ffe4 feat: add permission column to roles table 2024-12-10 11:28:10 +01:00
Pablo Lara e306322630 Merge branch 'PRWLR-5688-Implement-permission-status-filter' into PRWLR-4669-Roles-Page-API-UI 2024-12-10 10:47:14 +01:00
Adrián Jesús Peña Rodríguez b4eb6e8076 feat(rbac): add permission_state field to role serializer 2024-12-10 10:45:09 +01:00
Pablo Lara b54e9334b9 chore: add and edit roles is working now 2024-12-10 10:44:34 +01:00
Adrián Jesús Peña Rodríguez 5fd1af7559 feat(rbac): add permission_state filter 2024-12-10 10:33:26 +01:00
Pablo Lara 83c7ced6ff Merge branch 'feat-rbac' into PRWLR-4669-Roles-Page-API-UI 2024-12-10 09:29:24 +01:00
Adrián Jesús Peña Rodríguez 67d9ff2419 ref(provider_groups): ref the provider_groups relationships endpoint (#6033) 2024-12-10 09:28:22 +01:00
Pablo Lara 130fddae1e feat: edit role feature 2024-12-10 09:08:25 +01:00
Pablo Lara 04b9f81e26 feat: add new role feature 2024-12-10 07:24:01 +01:00
Pablo Lara 29bc697487 feat: add roles page 2024-12-05 15:25:07 +01:00
Pablo Lara 381aa93f55 chore: add roles item to the sidebar 2024-12-05 09:12:31 +01:00
Adrián Jesús Peña Rodríguez 2bee4b986f Merge branch 'master' into feat-rbac 2024-12-04 15:42:47 +01:00
Adrián Jesús Peña Rodríguez 9723b8fac1 Merge branch 'master' into feat-rbac 2024-12-04 12:51:39 +01:00
Adrián Jesús Peña Rodríguez 67ef67add9 feat(api-rbac): RBAC system (#5903) 2024-11-26 12:32:55 +01:00
388 changed files with 3754 additions and 21793 deletions

No files matched your search

+4 -9
View File
@@ -6,14 +6,13 @@
PROWLER_UI_VERSION="latest"
SITE_URL=http://localhost:3000
API_BASE_URL=http://prowler-api:8080/api/v1
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST=true
UI_PORT=3000
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
#### Prowler API Configuration ####
PROWLER_API_VERSION="stable"
PROWLER_API_VERSION="latest"
# PostgreSQL settings
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
POSTGRES_HOST=postgres-db
@@ -41,12 +40,9 @@ DJANGO_LOGGING_FORMATTER=human_readable
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
# Applies to both Django and Celery Workers
DJANGO_LOGGING_LEVEL=INFO
# Defaults to the maximum available based on CPU cores if not set.
DJANGO_WORKERS=4
# Token lifetime is in minutes
DJANGO_ACCESS_TOKEN_LIFETIME=30
# Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME=1440
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
DJANGO_ACCESS_TOKEN_LIFETIME=30 # Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME=1440 # Token lifetime is in minutes
DJANGO_CACHE_MAX_AGE=3600
DJANGO_STALE_WHILE_REVALIDATE=60
DJANGO_MANAGE_DB_PARTITIONS=True
@@ -91,4 +87,3 @@ jQIDAQAB
-----END PUBLIC KEY-----"
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
+1 -34
View File
@@ -16,17 +16,6 @@ updates:
- "dependencies"
- "pip"
- package-ecosystem: "pip"
directory: "/api"
schedule:
interval: "daily"
open-pull-requests-limit: 10
target-branch: master
labels:
- "dependencies"
- "pip"
- "component/api"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
@@ -38,7 +27,7 @@ updates:
- "github_actions"
- package-ecosystem: "npm"
directory: "/ui"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
@@ -46,17 +35,6 @@ updates:
labels:
- "dependencies"
- "npm"
- "component/ui"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: master
labels:
- "dependencies"
- "docker"
# v4.6
- package-ecosystem: "pip"
@@ -81,17 +59,6 @@ updates:
- "github_actions"
- "v4"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: v4.6
labels:
- "dependencies"
- "docker"
- "v4"
# v3
- package-ecosystem: "pip"
directory: "/"
-5
View File
@@ -22,11 +22,6 @@ provider/kubernetes:
- any-glob-to-any-file: "prowler/providers/kubernetes/**"
- any-glob-to-any-file: "tests/providers/kubernetes/**"
provider/github:
- changed-files:
- any-glob-to-any-file: "prowler/providers/github/**"
- any-glob-to-any-file: "tests/providers/github/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
@@ -23,7 +23,6 @@ env:
# Tags
LATEST_TAG: latest
RELEASE_TAG: ${{ github.event.release.tag_name }}
STABLE_TAG: stable
WORKING_DIRECTORY: ./api
@@ -32,34 +31,19 @@ env:
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
# Build Prowler OSS container
container-build-push:
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIRECTORY }}
steps:
- name: Repository check
working-directory: /tmp
run: |
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
- name: Checkout
uses: actions/checkout@v4
@@ -93,6 +77,5 @@ jobs:
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
+4
View File
@@ -15,12 +15,16 @@ on:
push:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths:
- "api/**"
pull_request:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths:
- "api/**"
+2 -4
View File
@@ -4,13 +4,11 @@ on:
push:
branches:
- "master"
- "v5.*"
paths:
- "api/**"
pull_request:
branches:
- "master"
- "v5.*"
paths:
- "api/**"
@@ -89,7 +87,7 @@ jobs:
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
python -m pip install --upgrade pip
pipx install poetry==1.8.5
pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -114,7 +112,7 @@ jobs:
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry check --lock
poetry lock --check
- name: Lint with ruff
working-directory: ./api
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.88.2
uses: trufflesecurity/trufflehog@v3.86.1
with:
path: ./
base: ${{ github.event.repository.default_branch }}
@@ -68,7 +68,7 @@ jobs:
- name: Install Poetry
run: |
pipx install poetry==1.8.5
pipx install poetry
pipx inject poetry poetry-bumpversion
- name: Get Prowler version
-2
View File
@@ -17,7 +17,6 @@ on:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths-ignore:
- 'ui/**'
- 'api/**'
@@ -26,7 +25,6 @@ on:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths-ignore:
- 'ui/**'
- 'api/**'
+2 -4
View File
@@ -37,14 +37,12 @@ jobs:
README.md
mkdocs.yml
.backportrc.json
.env
docker-compose*
- name: Install poetry
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
python -m pip install --upgrade pip
pipx install poetry==1.8.5
pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -67,7 +65,7 @@ jobs:
- name: Poetry check
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry check --lock
poetry lock --check
- name: Lint with flake8
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
+1 -29
View File
@@ -10,40 +10,12 @@ env:
CACHE: "poetry"
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
release-prowler-job:
runs-on: ubuntu-latest
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
env:
POETRY_VIRTUALENVS_CREATE: "false"
name: Release Prowler to PyPI
steps:
- name: Repository check
working-directory: /tmp
run: |
if [[ "${{ github.repository }}" != "prowler-cloud/prowler" ]]; then
echo "This action only runs for prowler-cloud/prowler"
exit 1
fi
- name: Get Prowler version
run: |
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
@@ -68,7 +40,7 @@ jobs:
- name: Install dependencies
run: |
pipx install poetry==1.8.5
pipx install poetry
- name: Setup Python
uses: actions/setup-python@v5
@@ -23,7 +23,6 @@ env:
# Tags
LATEST_TAG: latest
RELEASE_TAG: ${{ github.event.release.tag_name }}
STABLE_TAG: stable
WORKING_DIRECTORY: ./ui
@@ -32,34 +31,19 @@ env:
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
# Build Prowler OSS container
container-build-push:
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIRECTORY }}
steps:
- name: Repository check
working-directory: /tmp
run: |
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
- name: Checkout
uses: actions/checkout@v4
@@ -93,6 +77,5 @@ jobs:
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
+2
View File
@@ -15,12 +15,14 @@ on:
push:
branches:
- "master"
- "v4.*"
- "v5.*"
paths:
- "ui/**"
pull_request:
branches:
- "master"
- "v4.*"
- "v5.*"
paths:
- "ui/**"
+1 -8
View File
@@ -1,16 +1,9 @@
name: UI - Pull Request
on:
push:
branches:
- "master"
- "v5.*"
paths:
- "ui/**"
pull_request:
branches:
- master
- "v5.*"
paths:
- 'ui/**'
@@ -27,7 +20,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
+3 -2
View File
@@ -90,7 +90,7 @@ repos:
- id: bandit
name: bandit
description: "Bandit is a tool for finding common security issues in Python code"
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/' -r .'
language: system
files: '.*\.py'
@@ -103,6 +103,7 @@ repos:
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .'
exclude: 'api/src/backend/'
language: system
files: '.*\.py'
+1 -2
View File
@@ -72,7 +72,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|---|---|---|---|---|
| AWS | 561 | 81 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
| GCP | 77 | 13 -> `prowler gcp --list-services` | 4 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
| GCP | 77 | 13 -> `prowler gcp --list-services` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
| Azure | 139 | 18 -> `prowler azure --list-services` | 4 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
@@ -98,7 +98,6 @@ 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.
### From GitHub
-1
View File
@@ -22,7 +22,6 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
# Decide whether to allow Django manage database table partitions
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
# PostgreSQL settings
# If running django and celery on host, use 'localhost', else use 'postgres-db'
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.12.8-alpine3.20 AS build
FROM python:3.12-alpine AS build
LABEL maintainer="https://github.com/prowler-cloud/api"
+363 -362
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -8,11 +8,11 @@ description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
name = "prowler-api"
package-mode = false
version = "1.1.0"
version = "1.0.0"
[tool.poetry.dependencies]
celery = {extras = ["pytest"], version = "^5.4.0"}
django = "5.1.4"
django = "5.1.1"
django-celery-beat = "^2.7.0"
django-celery-results = "^2.5.1"
django-cors-headers = "4.4.0"
@@ -27,7 +27,7 @@ drf-nested-routers = "^0.94.1"
drf-spectacular = "0.27.2"
drf-spectacular-jsonapi = "0.5.1"
gunicorn = "23.0.0"
prowler = "^5.0"
prowler = {git = "https://github.com/prowler-cloud/prowler.git", tag = "5.0.0"}
psycopg2-binary = "2.9.9"
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
# Needed for prowler compatibility
@@ -48,8 +48,8 @@ pytest-env = "1.1.3"
pytest-randomly = "3.15.0"
pytest-xdist = "3.6.1"
ruff = "0.5.0"
safety = "3.2.9"
vulture = "2.14"
safety = "3.2.3"
vulture = "2.11"
[tool.poetry.scripts]
celery = "src.backend.config.settings.celery"
+29 -21
View File
@@ -1,23 +1,23 @@
import uuid
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db import connection, transaction
from rest_framework import permissions
from rest_framework.exceptions import NotAuthenticated
from rest_framework.filters import SearchFilter
from rest_framework_json_api import filters
from rest_framework_json_api.serializers import ValidationError
from rest_framework_json_api.views import ModelViewSet
from rest_framework_simplejwt.authentication import JWTAuthentication
from api.db_router import MainRouter
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
from api.filters import CustomDjangoFilterBackend
from api.models import Role, Tenant
from api.rbac.permissions import HasPermissions
from api.db_router import MainRouter
class BaseViewSet(ModelViewSet):
authentication_classes = [JWTAuthentication]
required_permissions = []
permission_classes = [permissions.IsAuthenticated, HasPermissions]
permission_classes = [permissions.IsAuthenticated]
filter_backends = [
filters.QueryParameterValidationFilter,
filters.OrderingFilter,
@@ -31,17 +31,6 @@ class BaseViewSet(ModelViewSet):
ordering_fields = "__all__"
ordering = ["id"]
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.set_required_permissions()
super().initial(request, *args, **kwargs)
def set_required_permissions(self):
"""This is an abstract method that must be implemented by subclasses."""
NotImplemented
def get_queryset(self):
raise NotImplementedError
@@ -61,7 +50,13 @@ class BaseRLSViewSet(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
@@ -115,7 +110,8 @@ class BaseTenantViewset(BaseViewSet):
):
user_id = str(request.user.id)
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
return super().initial(request, *args, **kwargs)
# TODO: DRY this when we have time
@@ -126,7 +122,13 @@ class BaseTenantViewset(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
@@ -147,6 +149,12 @@ class BaseUserViewset(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
+8 -27
View File
@@ -1,14 +1,13 @@
import secrets
import uuid
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from django.conf import settings
from django.contrib.auth.models import BaseUserManager
from django.core.paginator import Paginator
from django.db import connection, models, transaction
from psycopg2 import connect as psycopg2_connect
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
from rest_framework_json_api.serializers import ValidationError
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
DB_PASSWORD = (
@@ -24,8 +23,6 @@ TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
POSTGRES_TENANT_VAR = "api.tenant_id"
POSTGRES_USER_VAR = "api.user_id"
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
@contextmanager
def psycopg_connection(database_alias: str):
@@ -47,23 +44,10 @@ def psycopg_connection(database_alias: str):
@contextmanager
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
"""
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
if the value is a valid UUID.
Args:
value (str): Database configuration parameter value.
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
"""
def tenant_transaction(tenant_id: str):
with transaction.atomic():
with connection.cursor() as cursor:
try:
# just in case the value is an UUID object
uuid.UUID(str(value))
except ValueError:
raise ValidationError("Must be a valid UUID")
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
yield cursor
@@ -119,18 +103,15 @@ def batch_delete(queryset, batch_size=5000):
total_deleted = 0
deletion_summary = {}
while True:
# Get a batch of IDs to delete
batch_ids = set(
queryset.values_list("id", flat=True).order_by("id")[:batch_size]
)
if not batch_ids:
# No more objects to delete
break
paginator = Paginator(queryset.order_by("id").only("id"), batch_size)
for page_num in paginator.page_range:
batch_ids = [obj.id for obj in paginator.page(page_num).object_list]
deleted_count, deleted_info = queryset.filter(id__in=batch_ids).delete()
total_deleted += deleted_count
for model_label, count in deleted_info.items():
deletion_summary[model_label] = deletion_summary.get(model_label, 0) + count
+3 -10
View File
@@ -1,10 +1,6 @@
import uuid
from functools import wraps
from django.db import connection, transaction
from rest_framework_json_api.serializers import ValidationError
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
def set_tenant(func):
@@ -35,7 +31,7 @@ def set_tenant(func):
pass
# When calling the task
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
some_task.delay(arg1, tenant_id="1234-abcd-5678")
# The tenant context will be set before the task logic executes.
"""
@@ -47,12 +43,9 @@ def set_tenant(func):
tenant_id = kwargs.pop("tenant_id")
except KeyError:
raise KeyError("This task requires the tenant_id")
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
return func(*args, **kwargs)
+25 -31
View File
@@ -22,22 +22,21 @@ from api.db_utils import (
StatusEnumField,
)
from api.models import (
ComplianceOverview,
Finding,
Invitation,
Membership,
PermissionChoices,
Provider,
ProviderGroup,
ProviderSecret,
Resource,
ResourceTag,
Role,
Scan,
ScanSummary,
SeverityChoices,
StateChoices,
StatusChoices,
ProviderSecret,
Invitation,
Role,
ComplianceOverview,
Task,
User,
)
@@ -486,12 +485,29 @@ class UserFilter(FilterSet):
class RoleFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
permission_state = ChoiceFilter(
choices=PermissionChoices.choices, method="filter_permission_state"
)
permission_state = CharFilter(method="filter_permission_state")
def filter_permission_state(self, queryset, name, value):
return Role.filter_by_permission_state(queryset, value)
permission_fields = [
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
]
q_all_true = Q(**{field: True for field in permission_fields})
q_all_false = Q(**{field: False for field in permission_fields})
if value == "unlimited":
return queryset.filter(q_all_true)
elif value == "none":
return queryset.filter(q_all_false)
elif value == "limited":
return queryset.exclude(q_all_true | q_all_false)
else:
return queryset.none()
class Meta:
model = Role
@@ -543,25 +559,3 @@ class ScanSummaryFilter(FilterSet):
"inserted_at": ["date", "gte", "lte"],
"region": ["exact", "icontains", "in"],
}
class ServiceOverviewFilter(ScanSummaryFilter):
muted_findings = None
def is_valid(self):
# Check if at least one of the inserted_at filters is present
inserted_at_filters = [
self.data.get("inserted_at"),
self.data.get("inserted_at__gte"),
self.data.get("inserted_at__lte"),
]
if not any(inserted_at_filters):
raise ValidationError(
{
"inserted_at": [
"At least one of filter[inserted_at], filter[inserted_at__gte], or "
"filter[inserted_at__lte] is required."
]
}
)
return super().is_valid()
@@ -64,7 +64,7 @@
"pk": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "admin_test",
"name": "admin",
"manage_users": true,
"manage_account": true,
"manage_billing": true,
@@ -552,7 +552,7 @@ class Migration(migrations.Migration):
migrations.AddConstraint(
model_name="providergroupmembership",
constraint=models.UniqueConstraint(
fields=("provider_id", "provider_group"),
fields=("provider_id", "provider_group_id"),
name="unique_provider_group_membership",
),
),
@@ -1,17 +1,15 @@
# Generated by Django 5.1.1 on 2024-12-05 12:29
import uuid
import api.rls
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0003_update_provider_unique_constraint_with_is_deleted"),
("api", "0002_token_migrations"),
]
operations = [
@@ -1,23 +0,0 @@
# Generated by Django 5.1.1 on 2024-12-20 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0002_token_migrations"),
]
operations = [
migrations.RemoveConstraint(
model_name="provider",
name="unique_provider_uids",
),
migrations.AddConstraint(
model_name="provider",
constraint=models.UniqueConstraint(
fields=("tenant_id", "provider", "uid", "is_deleted"),
name="unique_provider_uids",
),
),
]
@@ -1,44 +0,0 @@
from django.db import migrations
from api.db_router import MainRouter
def create_admin_role(apps, schema_editor):
Tenant = apps.get_model("api", "Tenant")
Role = apps.get_model("api", "Role")
User = apps.get_model("api", "User")
UserRoleRelationship = apps.get_model("api", "UserRoleRelationship")
for tenant in Tenant.objects.using(MainRouter.admin_db).all():
admin_role, _ = Role.objects.using(MainRouter.admin_db).get_or_create(
name="admin",
tenant=tenant,
defaults={
"manage_users": True,
"manage_account": True,
"manage_billing": True,
"manage_providers": True,
"manage_integrations": True,
"manage_scans": True,
"unlimited_visibility": True,
},
)
users = User.objects.using(MainRouter.admin_db).filter(
membership__tenant=tenant
)
for user in users:
UserRoleRelationship.objects.using(MainRouter.admin_db).get_or_create(
user=user,
role=admin_role,
tenant=tenant,
)
class Migration(migrations.Migration):
dependencies = [
("api", "0004_rbac"),
]
operations = [
migrations.RunPython(create_admin_role),
]
+6 -53
View File
@@ -69,21 +69,6 @@ class StateChoices(models.TextChoices):
CANCELLED = "cancelled", _("Cancelled")
class PermissionChoices(models.TextChoices):
"""
Represents the different permission states that a role can have.
Attributes:
UNLIMITED: Indicates that the role possesses all permissions.
LIMITED: Indicates that the role has some permissions but not all.
NONE: Indicates that the role does not have any permissions.
"""
UNLIMITED = "unlimited", _("Unlimited permissions")
LIMITED = "limited", _("Limited permissions")
NONE = "none", _("No permissions")
class ActiveProviderManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(self.active_provider_filter())
@@ -271,7 +256,7 @@ class Provider(RowLevelSecurityProtectedModel):
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "provider", "uid", "is_deleted"),
fields=("tenant_id", "provider", "uid"),
name="unique_provider_uids",
),
RowLevelSecurityConstraint(
@@ -309,7 +294,7 @@ class ProviderGroup(RowLevelSecurityProtectedModel):
]
class JSONAPIMeta:
resource_name = "provider-groups"
resource_name = "provider-group"
class ProviderGroupMembership(RowLevelSecurityProtectedModel):
@@ -322,7 +307,7 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel):
db_table = "provider_group_memberships"
constraints = [
models.UniqueConstraint(
fields=["provider_id", "provider_group"],
fields=["provider_id", "provider_group_id"],
name="unique_provider_group_membership",
),
RowLevelSecurityConstraint(
@@ -515,8 +500,8 @@ class Resource(RowLevelSecurityProtectedModel):
through="ResourceTagMapping",
)
def get_tags(self, tenant_id: str) -> dict:
return {tag.key: tag.value for tag in self.tags.filter(tenant_id=tenant_id)}
def get_tags(self) -> dict:
return {tag.key: tag.value for tag in self.tags.all()}
def clear_tags(self):
self.tags.clear()
@@ -879,38 +864,6 @@ class Role(RowLevelSecurityProtectedModel):
Invitation, through="InvitationRoleRelationship", related_name="roles"
)
# Filter permission_state
PERMISSION_FIELDS = [
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
]
@property
def permission_state(self):
values = [getattr(self, field) for field in self.PERMISSION_FIELDS]
if all(values):
return PermissionChoices.UNLIMITED
elif not any(values):
return PermissionChoices.NONE
else:
return PermissionChoices.LIMITED
@classmethod
def filter_by_permission_state(cls, queryset, value):
q_all_true = Q(**{field: True for field in cls.PERMISSION_FIELDS})
q_all_false = Q(**{field: False for field in cls.PERMISSION_FIELDS})
if value == PermissionChoices.UNLIMITED:
return queryset.filter(q_all_true)
elif value == PermissionChoices.NONE:
return queryset.filter(q_all_false)
else:
return queryset.exclude(q_all_true | q_all_false)
class Meta:
db_table = "roles"
constraints = [
@@ -926,7 +879,7 @@ class Role(RowLevelSecurityProtectedModel):
]
class JSONAPIMeta:
resource_name = "roles"
resource_name = "role"
class RoleProviderGroupRelationship(RowLevelSecurityProtectedModel):
+7 -42
View File
@@ -1,12 +1,8 @@
from config.django.base import DISABLE_RBAC
from enum import Enum
from typing import Optional
from django.db.models import QuerySet
from rest_framework.permissions import BasePermission
from api.db_router import MainRouter
from api.models import Provider, Role, User
class Permissions(Enum):
MANAGE_USERS = "manage_users"
@@ -25,13 +21,15 @@ class HasPermissions(BasePermission):
"""
def has_permission(self, request, view):
# This is for testing/demo purposes only
if DISABLE_RBAC:
return True
required_permissions = getattr(view, "required_permissions", [])
if not required_permissions:
return True
user_roles = (
User.objects.using(MainRouter.admin_db).get(id=request.user.id).roles.all()
)
user_roles = request.user.roles.all()
if not user_roles:
return False
@@ -40,36 +38,3 @@ class HasPermissions(BasePermission):
return False
return True
def get_role(user: User) -> Optional[Role]:
"""
Retrieve the first role assigned to the given user.
Returns:
The user's first Role instance if the user has any roles, otherwise None.
"""
return user.roles.first()
def get_providers(role: Role) -> QuerySet[Provider]:
"""
Return a distinct queryset of Providers accessible by the given role.
If the role has no associated provider groups, an empty queryset is returned.
Args:
role: A Role instance.
Returns:
A QuerySet of Provider objects filtered by the role's provider groups.
If the role has no provider groups, returns an empty queryset.
"""
tenant = role.tenant
provider_groups = role.provider_groups.all()
if not provider_groups.exists():
return Provider.objects.none()
return Provider.objects.filter(
tenant=tenant, provider_groups__in=provider_groups
).distinct()
+3 -3
View File
@@ -2,7 +2,7 @@ from contextlib import nullcontext
from rest_framework_json_api.renderers import JSONRenderer
from api.db_utils import rls_transaction
from api.db_utils import tenant_transaction
class APIJSONRenderer(JSONRenderer):
@@ -13,9 +13,9 @@ class APIJSONRenderer(JSONRenderer):
tenant_id = getattr(request, "tenant_id", None) if request else None
include_param_present = "include" in request.query_params if request else False
# Use rls_transaction if needed for included resources, otherwise do nothing
# Use tenant_transaction if needed for included resources, otherwise do nothing
context_manager = (
rls_transaction(tenant_id)
tenant_transaction(tenant_id)
if tenant_id and include_param_present
else nullcontext()
)
File diff suppressed because it is too large. Load diff
@@ -1,9 +1,12 @@
import pytest
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
from django.urls import reverse
from unittest.mock import patch
from rest_framework.test import APIClient
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
@patch("api.v1.views.MainRouter.admin_db", new="default")
@pytest.mark.django_db
def test_basic_authentication():
client = APIClient()
@@ -95,85 +98,3 @@ def test_refresh_token(create_test_user, tenants_fixture):
format="vnd.api+json",
)
assert new_refresh_response.status_code == 200
@pytest.mark.django_db
def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fixture):
client = APIClient()
role = roles_fixture[0]
user1_email = "user1@testing.com"
user2_email = "user2@testing.com"
password = "thisisapassword123"
user1_response = client.post(
reverse("user-list"),
data={
"data": {
"type": "users",
"attributes": {
"name": "user1",
"email": user1_email,
"password": password,
},
}
},
format="vnd.api+json",
)
assert user1_response.status_code == 201
user1_access_token, _ = get_api_tokens(client, user1_email, password)
user1_headers = get_authorization_header(user1_access_token)
user2_invitation = client.post(
reverse("invitation-list"),
data={
"data": {
"type": "invitations",
"attributes": {"email": user2_email},
"relationships": {
"roles": {
"data": [
{
"type": "roles",
"id": str(role.id),
}
]
}
},
}
},
format="vnd.api+json",
headers=user1_headers,
)
assert user2_invitation.status_code == 201
invitation_token = user2_invitation.json()["data"]["attributes"]["token"]
user2_response = client.post(
reverse("user-list") + f"?invitation_token={invitation_token}",
data={
"data": {
"type": "users",
"attributes": {
"name": "user2",
"email": user2_email,
"password": password,
},
}
},
format="vnd.api+json",
)
assert user2_response.status_code == 201
user2_access_token, _ = get_api_tokens(client, user2_email, password)
user2_headers = get_authorization_header(user2_access_token)
user1_me = client.get(reverse("user-me"), headers=user1_headers)
assert user1_me.status_code == 200
assert user1_me.json()["data"]["attributes"]["email"] == user1_email
user2_me = client.get(reverse("user-me"), headers=user2_headers)
assert user2_me.status_code == 200
assert user2_me.json()["data"]["attributes"]["email"] == user2_email
@@ -1,85 +0,0 @@
from unittest.mock import Mock, patch
import pytest
from conftest import get_api_tokens, get_authorization_header
from django.urls import reverse
from rest_framework.test import APIClient
from api.models import Provider
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.delete_provider_task.delay")
@pytest.mark.django_db
def test_delete_provider_without_executing_task(
mock_delete_task, mock_task_get, create_test_user, tenants_fixture, tasks_fixture
):
client = APIClient()
test_user = "test_email@prowler.com"
test_password = "test_password"
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
mock_delete_task.return_value = task_mock
mock_task_get.return_value = prowler_task
user_creation_response = client.post(
reverse("user-list"),
data={
"data": {
"type": "users",
"attributes": {
"name": "test",
"email": test_user,
"password": test_password,
},
}
},
format="vnd.api+json",
)
assert user_creation_response.status_code == 201
access_token, _ = get_api_tokens(client, test_user, test_password)
auth_headers = get_authorization_header(access_token)
create_provider_response = client.post(
reverse("provider-list"),
data={
"data": {
"type": "providers",
"attributes": {
"provider": Provider.ProviderChoices.AWS,
"uid": "123456789012",
},
}
},
format="vnd.api+json",
headers=auth_headers,
)
assert create_provider_response.status_code == 201
provider_id = create_provider_response.json()["data"]["id"]
provider_uid = create_provider_response.json()["data"]["attributes"]["uid"]
remove_provider = client.delete(
reverse("provider-detail", kwargs={"pk": provider_id}),
headers=auth_headers,
)
assert remove_provider.status_code == 202
recreate_provider_response = client.post(
reverse("provider-list"),
data={
"data": {
"type": "providers",
"attributes": {
"provider": Provider.ProviderChoices.AWS,
"uid": provider_uid,
},
}
},
format="vnd.api+json",
headers=auth_headers,
)
assert recreate_provider_response.status_code == 201
@@ -11,9 +11,9 @@ from conftest import TEST_USER, TEST_PASSWORD, get_api_tokens, get_authorization
def test_check_resources_between_different_tenants(
schedule_mock,
enforce_test_user_db_connection,
patch_testing_flag,
authenticated_api_client,
tenants_fixture,
set_user_admin_roles_fixture,
):
client = authenticated_api_client
@@ -1,3 +1,5 @@
# TODO: Enable this tests
import pytest
from django.urls import reverse
from rest_framework import status
@@ -29,14 +31,6 @@ class TestUserViewSet:
== create_test_user_rbac.email
)
def test_retrieve_user_with_no_roles(
self, authenticated_client_rbac_noroles, create_test_user_rbac_no_roles
):
response = authenticated_client_rbac_noroles.get(
reverse("user-detail", kwargs={"pk": create_test_user_rbac_no_roles.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_retrieve_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac, create_test_user
):
@@ -45,6 +39,7 @@ class TestUserViewSet:
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_create_user_with_all_permissions(self, authenticated_client_rbac):
valid_user_payload = {
"name": "test",
@@ -57,6 +52,7 @@ class TestUserViewSet:
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["data"]["attributes"]["email"] == "new_user@test.com"
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_create_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac
):
@@ -6,10 +6,8 @@ from django.db.utils import ConnectionRouter
from api.db_router import MainRouter
from api.rls import Tenant
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
from unittest.mock import patch
@patch("api.db_router.MainRouter.admin_db", new="admin")
class TestMainDatabaseRouter:
@pytest.fixture(scope="module")
def router(self):
+1 -32
View File
@@ -2,15 +2,7 @@ from datetime import datetime, timezone
from enum import Enum
from unittest.mock import patch
import pytest
from api.db_utils import (
batch_delete,
enum_to_choices,
generate_random_token,
one_week_from_now,
)
from api.models import Provider
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
class TestEnumToChoices:
@@ -114,26 +106,3 @@ class TestGenerateRandomToken:
token = generate_random_token(length=5, symbols="")
# Default symbols
assert len(token) == 5
class TestBatchDelete:
@pytest.fixture
def create_test_providers(self, tenants_fixture):
tenant = tenants_fixture[0]
provider_id = 123456789012
provider_count = 10
for i in range(provider_count):
Provider.objects.create(
tenant=tenant,
uid=f"{provider_id + i}",
provider=Provider.ProviderChoices.AWS,
)
return provider_count
@pytest.mark.django_db
def test_batch_delete(self, create_test_providers):
_, summary = batch_delete(
Provider.objects.all(), batch_size=create_test_providers // 2
)
assert Provider.objects.all().count() == 0
assert summary == {"api.Provider": create_test_providers}
+3 -5
View File
@@ -1,9 +1,7 @@
import uuid
from unittest.mock import call, patch
from unittest.mock import patch, call
import pytest
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
from api.decorators import set_tenant
@@ -17,12 +15,12 @@ class TestSetTenantDecorator:
def random_func(arg):
return arg
tenant_id = str(uuid.uuid4())
tenant_id = "1234-abcd-5678"
result = random_func("test_arg", tenant_id=tenant_id)
assert (
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
call(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
in mock_cursor.execute.mock_calls
)
assert result == "test_arg"
+16 -21
View File
@@ -7,10 +7,9 @@ from api.models import Resource, ResourceTag
class TestResourceModel:
def test_setting_tags(self, providers_fixture):
provider, *_ = providers_fixture
tenant_id = provider.tenant_id
resource = Resource.objects.create(
tenant_id=tenant_id,
tenant_id=provider.tenant_id,
provider=provider,
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
name="My Instance 1",
@@ -21,12 +20,12 @@ class TestResourceModel:
tags = [
ResourceTag.objects.create(
tenant_id=tenant_id,
tenant_id=provider.tenant_id,
key="key",
value="value",
),
ResourceTag.objects.create(
tenant_id=tenant_id,
tenant_id=provider.tenant_id,
key="key2",
value="value2",
),
@@ -34,9 +33,9 @@ class TestResourceModel:
resource.upsert_or_delete_tags(tags)
assert len(tags) == len(resource.tags.filter(tenant_id=tenant_id))
assert len(tags) == len(resource.tags.all())
tags_dict = resource.get_tags(tenant_id=tenant_id)
tags_dict = resource.get_tags()
for tag in tags:
assert tag.key in tags_dict
@@ -44,51 +43,47 @@ class TestResourceModel:
def test_adding_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
tags = [
ResourceTag.objects.create(
tenant_id=tenant_id,
tenant_id=resource.tenant_id,
key="env",
value="test",
),
]
before_count = len(resource.tags.filter(tenant_id=tenant_id))
before_count = len(resource.tags.all())
resource.upsert_or_delete_tags(tags)
assert before_count + 1 == len(resource.tags.filter(tenant_id=tenant_id))
assert before_count + 1 == len(resource.tags.all())
tags_dict = resource.get_tags(tenant_id=tenant_id)
tags_dict = resource.get_tags()
assert "env" in tags_dict
assert tags_dict["env"] == "test"
def test_adding_duplicate_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
tags = resource.tags.filter(tenant_id=tenant_id)
tags = resource.tags.all()
before_count = len(resource.tags.filter(tenant_id=tenant_id))
before_count = len(resource.tags.all())
resource.upsert_or_delete_tags(tags)
# should be the same number of tags
assert before_count == len(resource.tags.filter(tenant_id=tenant_id))
assert before_count == len(resource.tags.all())
def test_add_tags_none(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
resource.upsert_or_delete_tags(None)
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
assert resource.get_tags(tenant_id=tenant_id) == {}
assert len(resource.tags.all()) == 0
assert resource.get_tags() == {}
def test_clear_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
resource.clear_tags()
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
assert resource.get_tags(tenant_id=tenant_id) == {}
assert len(resource.tags.all()) == 0
assert resource.get_tags() == {}
+73 -466
View File
@@ -9,24 +9,32 @@ from django.urls import reverse
from rest_framework import status
from api.models import (
Invitation,
Membership,
Provider,
ProviderGroup,
ProviderGroupMembership,
ProviderSecret,
Role,
RoleProviderGroupRelationship,
Invitation,
UserRoleRelationship,
ProviderSecret,
Scan,
StateChoices,
User,
UserRoleRelationship,
)
from api.rls import Tenant
TODAY = str(datetime.today().date())
@pytest.fixture(autouse=True)
def enable_testing_flag(patch_testing_flag):
"""
Automatically applies the patch_testing_flag fixture to all tests in this file.
"""
pass
@pytest.mark.django_db
class TestUserViewSet:
def test_users_list(self, authenticated_client, create_test_user):
@@ -53,6 +61,7 @@ class TestUserViewSet:
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["email"] == create_test_user.email
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_users_create(self, client):
valid_user_payload = {
"name": "test",
@@ -69,6 +78,7 @@ class TestUserViewSet:
== valid_user_payload["email"].lower()
)
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_users_create_duplicated_email(self, client):
# Create a user
self.test_users_create(client)
@@ -123,6 +133,7 @@ class TestUserViewSet:
"NonExistentEmail@prowler.com",
],
)
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_users_create_used_email(self, authenticated_client, email):
# First user created; no errors should occur
user_payload = {
@@ -340,7 +351,7 @@ class TestTenantViewSet:
def test_tenants_list(self, authenticated_client, tenants_fixture):
response = authenticated_client.get(reverse("tenant-list"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2 # Test user belongs to 2 tenants
assert len(response.json()["data"]) == len(tenants_fixture)
def test_tenants_retrieve(self, authenticated_client, tenants_fixture):
tenant1, *_ = tenants_fixture
@@ -418,6 +429,7 @@ class TestTenantViewSet:
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@patch("api.db_router.MainRouter.admin_db", new="default")
@patch("api.v1.views.delete_tenant_task.apply_async")
def test_tenants_delete(
self, delete_tenant_mock, authenticated_client, tenants_fixture
@@ -470,11 +482,11 @@ class TestTenantViewSet:
(
[
("name", "Tenant One", 1),
("name.icontains", "Tenant", 2),
("inserted_at", TODAY, 2),
("inserted_at.gte", "2024-01-01", 2),
("name.icontains", "Tenant", 3),
("inserted_at", TODAY, 3),
("inserted_at.gte", "2024-01-01", 3),
("inserted_at.lte", "2024-01-01", 0),
("updated_at.gte", "2024-01-01", 2),
("updated_at.gte", "2024-01-01", 3),
("updated_at.lte", "2024-01-01", 0),
]
),
@@ -510,9 +522,7 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == page_size
assert response.json()["meta"]["pagination"]["page"] == 1
assert (
response.json()["meta"]["pagination"]["pages"] == 2
) # Test user belongs to 2 tenants
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
def test_tenants_list_page_number(self, authenticated_client, tenants_fixture):
page_size = 1
@@ -525,13 +535,13 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == page_size
assert response.json()["meta"]["pagination"]["page"] == page_number
assert response.json()["meta"]["pagination"]["pages"] == 2
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
def test_tenants_list_sort_name(self, authenticated_client, tenants_fixture):
_, tenant2, _ = tenants_fixture
response = authenticated_client.get(reverse("tenant-list"), {"sort": "-name"})
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
assert len(response.json()["data"]) == 3
assert response.json()["data"][0]["attributes"]["name"] == tenant2.name
def test_tenants_list_memberships_as_owner(
@@ -545,7 +555,6 @@ class TestTenantViewSet:
# Test user + 2 extra users for tenant 2
assert len(response.json()["data"]) == 3
@patch("api.v1.views.TenantMembersViewSet.required_permissions", [])
def test_tenants_list_memberships_as_member(
self, authenticated_client, tenants_fixture, extra_users
):
@@ -1202,7 +1211,7 @@ class TestProviderGroupViewSet:
def test_provider_group_create(self, authenticated_client):
data = {
"data": {
"type": "provider-groups",
"type": "provider-group",
"attributes": {
"name": "Test Provider Group",
},
@@ -1221,7 +1230,7 @@ class TestProviderGroupViewSet:
def test_provider_group_create_invalid(self, authenticated_client):
data = {
"data": {
"type": "provider-groups",
"type": "provider-group",
"attributes": {
# Name is missing
},
@@ -1243,7 +1252,7 @@ class TestProviderGroupViewSet:
data = {
"data": {
"id": str(provider_group.id),
"type": "provider-groups",
"type": "provider-group",
"attributes": {
"name": "Updated Provider Group Name",
},
@@ -1265,7 +1274,7 @@ class TestProviderGroupViewSet:
data = {
"data": {
"id": str(provider_group.id),
"type": "provider-groups",
"type": "provider-group",
"attributes": {
"name": "", # Invalid name
},
@@ -1329,166 +1338,6 @@ class TestProviderGroupViewSet:
response = authenticated_client.put(reverse("providergroup-list"))
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_provider_group_create_with_relationships(
self, authenticated_client, providers_fixture, roles_fixture
):
provider1, provider2, *_ = providers_fixture
role1, role2, *_ = roles_fixture
data = {
"data": {
"type": "provider-groups",
"attributes": {"name": "Test Provider Group with relationships"},
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider1.id)},
{"type": "providers", "id": str(provider2.id)},
]
},
"roles": {
"data": [
{"type": "roles", "id": str(role1.id)},
{"type": "roles", "id": str(role2.id)},
]
},
},
}
}
response = authenticated_client.post(
reverse("providergroup-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED
response_data = response.json()["data"]
group = ProviderGroup.objects.get(id=response_data["id"])
assert group.name == "Test Provider Group with relationships"
assert set(group.providers.all()) == {provider1, provider2}
assert set(group.roles.all()) == {role1, role2}
def test_provider_group_update_relationships(
self,
authenticated_client,
provider_groups_fixture,
providers_fixture,
roles_fixture,
):
group = provider_groups_fixture[0]
provider3 = providers_fixture[2]
provider4 = providers_fixture[3]
role3 = roles_fixture[2]
role4 = roles_fixture[3]
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider3.id)},
{"type": "providers", "id": str(provider4.id)},
]
},
"roles": {
"data": [
{"type": "roles", "id": str(role3.id)},
{"type": "roles", "id": str(role4.id)},
]
},
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
group.refresh_from_db()
assert set(group.providers.all()) == {provider3, provider4}
assert set(group.roles.all()) == {role3, role4}
def test_provider_group_clear_relationships(
self, authenticated_client, providers_fixture, provider_groups_fixture
):
group = provider_groups_fixture[0]
provider3 = providers_fixture[2]
provider4 = providers_fixture[3]
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider3.id)},
{"type": "providers", "id": str(provider4.id)},
]
}
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {"data": []}, # Removing all providers
"roles": {"data": []}, # Removing all roles
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
group.refresh_from_db()
assert group.providers.count() == 0
assert group.roles.count() == 0
def test_provider_group_create_with_invalid_relationships(
self, authenticated_client
):
invalid_provider_id = "non-existent-id"
data = {
"data": {
"type": "provider-groups",
"attributes": {"name": "Invalid relationships test"},
"relationships": {
"providers": {
"data": [{"type": "providers", "id": invalid_provider_id}]
}
},
}
}
response = authenticated_client.post(
reverse("providergroup-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code in [status.HTTP_400_BAD_REQUEST]
@pytest.mark.django_db
class TestProviderSecretViewSet:
@@ -2341,10 +2190,7 @@ class TestResourceViewSet:
response.json()["errors"][0]["detail"] == "invalid sort parameter: invalid"
)
def test_resources_retrieve(
self, authenticated_client, tenants_fixture, resources_fixture
):
tenant = tenants_fixture[0]
def test_resources_retrieve(self, authenticated_client, resources_fixture):
resource_1, *_ = resources_fixture
response = authenticated_client.get(
reverse("resource-detail", kwargs={"pk": resource_1.id}),
@@ -2355,9 +2201,7 @@ class TestResourceViewSet:
assert response.json()["data"]["attributes"]["region"] == resource_1.region
assert response.json()["data"]["attributes"]["service"] == resource_1.service
assert response.json()["data"]["attributes"]["type"] == resource_1.type
assert response.json()["data"]["attributes"]["tags"] == resource_1.get_tags(
tenant_id=str(tenant.id)
)
assert response.json()["data"]["attributes"]["tags"] == resource_1.get_tags()
def test_resources_invalid_retrieve(self, authenticated_client):
response = authenticated_client.get(
@@ -2430,11 +2274,7 @@ class TestFindingViewSet:
("inserted_at", "2024-01-01", 0),
("inserted_at.date", "2024-01-01", 0),
("inserted_at.gte", "2024-01-01", 2),
(
"inserted_at.lte",
"2028-12-31",
2,
), # TODO: To avoid having to modify this value and to ensure that the tests always work, we should set the time before the fixtures are inserted
("inserted_at.lte", "2024-12-31", 2),
("updated_at.lte", "2024-01-01", 0),
("resource_type.icontains", "prowler", 2),
# full text search on finding
@@ -2742,7 +2582,7 @@ class TestInvitationViewSet:
},
"relationships": {
"roles": {
"data": [{"type": "roles", "id": str(roles_fixture[0].id)}]
"data": [{"type": "role", "id": str(roles_fixture[0].id)}]
}
},
}
@@ -2841,10 +2681,9 @@ class TestInvitationViewSet:
)
def test_invitations_partial_update_valid(
self, authenticated_client, invitations_fixture, roles_fixture
self, authenticated_client, invitations_fixture
):
invitation, *_ = invitations_fixture
role1, role2, *_ = roles_fixture
new_email = "new_email@prowler.com"
new_expires_at = datetime.now(timezone.utc) + timedelta(days=7)
new_expires_at_iso = new_expires_at.isoformat()
@@ -2856,14 +2695,6 @@ class TestInvitationViewSet:
"email": new_email,
"expires_at": new_expires_at_iso,
},
"relationships": {
"roles": {
"data": [
{"type": "roles", "id": str(role1.id)},
{"type": "roles", "id": str(role2.id)},
]
},
},
}
}
assert invitation.email != new_email
@@ -2882,7 +2713,6 @@ class TestInvitationViewSet:
assert invitation.email == new_email
assert invitation.expires_at == new_expires_at
assert invitation.roles.count() == 2
@pytest.mark.parametrize(
"email",
@@ -3036,6 +2866,7 @@ class TestInvitationViewSet:
== "This invitation cannot be revoked."
)
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_new_user(self, client, invitations_fixture):
invitation, *_ = invitations_fixture
@@ -3061,6 +2892,7 @@ class TestInvitationViewSet:
user__email__iexact=invitation.email, tenant=invitation.tenant
).exists()
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_existing_user(
self, authenticated_client, create_test_user, tenants_fixture
):
@@ -3092,6 +2924,7 @@ class TestInvitationViewSet:
).exists()
assert invitation.state == Invitation.State.ACCEPTED.value
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_invalid_token(self, authenticated_client):
data = {
"invitation_token": "invalid_token",
@@ -3104,6 +2937,7 @@ class TestInvitationViewSet:
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["errors"][0]["code"] == "not_found"
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_invalid_token_expired(
self, authenticated_client, invitations_fixture
):
@@ -3122,6 +2956,7 @@ class TestInvitationViewSet:
assert response.status_code == status.HTTP_410_GONE
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_invalid_token_expired_new_user(
self, client, invitations_fixture
):
@@ -3145,6 +2980,7 @@ class TestInvitationViewSet:
assert response.status_code == status.HTTP_410_GONE
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_invalid_token_accepted(
self, authenticated_client, invitations_fixture
):
@@ -3168,6 +3004,7 @@ class TestInvitationViewSet:
== "This invitation is no longer valid."
)
@patch("api.db_router.MainRouter.admin_db", new="default")
def test_invitations_accept_invitation_invalid_token_revoked(
self, authenticated_client, invitations_fixture
):
@@ -3267,9 +3104,7 @@ class TestRoleViewSet:
def test_role_list(self, authenticated_client, roles_fixture):
response = authenticated_client.get(reverse("role-list"))
assert response.status_code == status.HTTP_200_OK
assert (
len(response.json()["data"]) == len(roles_fixture) + 1
) # 1 default admin role
assert len(response.json()["data"]) == len(roles_fixture)
def test_role_retrieve(self, authenticated_client, roles_fixture):
role = roles_fixture[0]
@@ -3281,33 +3116,17 @@ class TestRoleViewSet:
assert data["id"] == str(role.id)
assert data["attributes"]["name"] == role.name
@pytest.mark.parametrize(
("permission_state", "index"),
[("limited", 0), ("unlimited", 2), ("none", 3)],
)
def test_role_retrieve_permission_state(
self, authenticated_client, roles_fixture, permission_state, index
):
role = roles_fixture[index]
response = authenticated_client.get(
reverse("role-detail", kwargs={"pk": role.id}),
{"filter[permission_state]": permission_state},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert data["id"] == str(role.id)
assert data["attributes"]["name"] == role.name
assert data["attributes"]["permission_state"] == permission_state
def test_role_create(self, authenticated_client):
data = {
"data": {
"type": "roles",
"type": "role",
"attributes": {
"name": "Test Role",
"manage_users": "false",
"manage_account": "false",
"manage_billing": "false",
"manage_providers": "true",
"manage_integrations": "true",
"manage_scans": "true",
"unlimited_visibility": "true",
},
@@ -3329,19 +3148,21 @@ class TestRoleViewSet:
):
data = {
"data": {
"type": "roles",
"type": "role",
"attributes": {
"name": "Test Role",
"manage_users": "false",
"manage_account": "false",
"manage_billing": "false",
"manage_providers": "true",
"manage_integrations": "true",
"manage_scans": "true",
"unlimited_visibility": "true",
},
"relationships": {
"provider_groups": {
"data": [
{"type": "provider-groups", "id": str(provider_group.id)}
{"type": "provider-group", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3367,7 +3188,7 @@ class TestRoleViewSet:
def test_role_create_invalid(self, authenticated_client):
data = {
"data": {
"type": "roles",
"type": "role",
"attributes": {
# Name is missing
},
@@ -3382,34 +3203,14 @@ class TestRoleViewSet:
errors = response.json()["errors"]
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
def test_admin_role_partial_update(self, authenticated_client, admin_role_fixture):
role = admin_role_fixture
data = {
"data": {
"id": str(role.id),
"type": "roles",
"attributes": {
"name": "Updated Role",
},
}
}
response = authenticated_client.patch(
reverse("role-detail", kwargs={"pk": role.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
role.refresh_from_db()
assert role.name != "Updated Role"
def test_role_partial_update(self, authenticated_client, roles_fixture):
role = roles_fixture[1]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"type": "role",
"attributes": {
"name": "Updated Role",
"name": "Updated Provider Group Name",
},
}
}
@@ -3420,14 +3221,14 @@ class TestRoleViewSet:
)
assert response.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.name == "Updated Role"
assert role.name == "Updated Provider Group Name"
def test_role_partial_update_invalid(self, authenticated_client, roles_fixture):
role = roles_fixture[2]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"type": "role",
"attributes": {
"name": "", # Invalid name
},
@@ -3442,14 +3243,6 @@ class TestRoleViewSet:
errors = response.json()["errors"]
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
def test_role_destroy_admin(self, authenticated_client, admin_role_fixture):
role = admin_role_fixture
response = authenticated_client.delete(
reverse("role-detail", kwargs={"pk": role.id})
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert Role.objects.filter(id=role.id).exists()
def test_role_destroy(self, authenticated_client, roles_fixture):
role = roles_fixture[2]
response = authenticated_client.delete(
@@ -3484,165 +3277,13 @@ class TestRoleViewSet:
response = authenticated_client.get(reverse("role-list"), {"sort": "name"})
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
names = [
item["attributes"]["name"]
for item in data
if item["attributes"]["name"] != "admin"
]
assert names == sorted(names, key=lambda v: v.lower())
names = [item["attributes"]["name"] for item in data]
assert names == sorted(names)
def test_role_invalid_method(self, authenticated_client):
response = authenticated_client.put(reverse("role-list"))
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_role_create_with_users_and_provider_groups(
self, authenticated_client, users_fixture, provider_groups_fixture
):
user1, user2, *_ = users_fixture
pg1, pg2, *_ = provider_groups_fixture
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Role with Users and PGs",
"manage_users": "true",
"manage_account": "false",
"manage_providers": "true",
"manage_scans": "false",
"unlimited_visibility": "false",
},
"relationships": {
"users": {
"data": [
{"type": "users", "id": str(user1.id)},
{"type": "users", "id": str(user2.id)},
]
},
"provider_groups": {
"data": [
{"type": "provider-groups", "id": str(pg1.id)},
{"type": "provider-groups", "id": str(pg2.id)},
]
},
},
}
}
response = authenticated_client.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED
created_role = Role.objects.get(name="Role with Users and PGs")
assert created_role.users.count() == 2
assert set(created_role.users.all()) == {user1, user2}
assert created_role.provider_groups.count() == 2
assert set(created_role.provider_groups.all()) == {pg1, pg2}
def test_role_update_relationships(
self,
authenticated_client,
roles_fixture,
users_fixture,
provider_groups_fixture,
):
role = roles_fixture[0]
user3 = users_fixture[2]
pg3 = provider_groups_fixture[2]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"relationships": {
"users": {
"data": [
{"type": "users", "id": str(user3.id)},
]
},
"provider_groups": {
"data": [
{"type": "provider-groups", "id": str(pg3.id)},
]
},
},
}
}
response = authenticated_client.patch(
reverse("role-detail", kwargs={"pk": role.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.users.count() == 1
assert role.users.first() == user3
assert role.provider_groups.count() == 1
assert role.provider_groups.first() == pg3
def test_role_clear_relationships(self, authenticated_client, roles_fixture):
role = roles_fixture[0]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"relationships": {
"users": {"data": []}, # Clearing all users
"provider_groups": {"data": []}, # Clearing all provider groups
},
}
}
response = authenticated_client.patch(
reverse("role-detail", kwargs={"pk": role.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.users.count() == 0
assert role.provider_groups.count() == 0
def test_role_create_with_invalid_user_relationship(
self, authenticated_client, provider_groups_fixture
):
invalid_user_id = "non-existent-user-id"
pg = provider_groups_fixture[0]
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Invalid Users Role",
"manage_users": "false",
"manage_account": "false",
"manage_providers": "true",
"manage_scans": "true",
"unlimited_visibility": "true",
},
"relationships": {
"users": {"data": [{"type": "users", "id": invalid_user_id}]},
"provider_groups": {
"data": [{"type": "provider-groups", "id": str(pg.id)}]
},
},
}
}
response = authenticated_client.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code in [status.HTTP_400_BAD_REQUEST]
@pytest.mark.django_db
class TestUserRoleRelationshipViewSet:
@@ -3650,9 +3291,7 @@ class TestUserRoleRelationshipViewSet:
self, authenticated_client, roles_fixture, create_test_user
):
data = {
"data": [
{"type": "roles", "id": str(role.id)} for role in roles_fixture[:2]
]
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
}
response = authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
@@ -3661,17 +3300,15 @@ class TestUserRoleRelationshipViewSet:
)
assert response.status_code == status.HTTP_204_NO_CONTENT
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
assert relationships.count() == 4
for relationship in relationships[2:]: # Skip admin role
assert relationships.count() == 2
for relationship in relationships[1:]: # Skip admin role
assert relationship.role.id in [r.id for r in roles_fixture[:2]]
def test_create_relationship_already_exists(
self, authenticated_client, roles_fixture, create_test_user
):
data = {
"data": [
{"type": "roles", "id": str(role.id)} for role in roles_fixture[:2]
]
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
}
authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
@@ -3681,7 +3318,7 @@ class TestUserRoleRelationshipViewSet:
data = {
"data": [
{"type": "roles", "id": str(roles_fixture[0].id)},
{"type": "role", "id": str(roles_fixture[0].id)},
]
}
response = authenticated_client.post(
@@ -3698,7 +3335,7 @@ class TestUserRoleRelationshipViewSet:
):
data = {
"data": [
{"type": "roles", "id": str(roles_fixture[2].id)},
{"type": "role", "id": str(roles_fixture[1].id)},
]
}
response = authenticated_client.patch(
@@ -3709,12 +3346,12 @@ class TestUserRoleRelationshipViewSet:
assert response.status_code == status.HTTP_204_NO_CONTENT
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
assert relationships.count() == 1
assert {rel.role.id for rel in relationships} == {roles_fixture[2].id}
assert {rel.role.id for rel in relationships} == {roles_fixture[1].id}
data = {
"data": [
{"type": "roles", "id": str(roles_fixture[1].id)},
{"type": "roles", "id": str(roles_fixture[2].id)},
{"type": "role", "id": str(roles_fixture[1].id)},
{"type": "role", "id": str(roles_fixture[2].id)},
]
}
response = authenticated_client.patch(
@@ -3742,7 +3379,7 @@ class TestUserRoleRelationshipViewSet:
def test_invalid_provider_group_id(self, authenticated_client, create_test_user):
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
response = authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
data=data,
@@ -3760,7 +3397,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-groups", "id": str(provider_group.id)}
{"type": "provider-group", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3786,7 +3423,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-groups", "id": str(provider_group.id)}
{"type": "provider-group", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3800,7 +3437,7 @@ class TestRoleProviderGroupRelationshipViewSet:
data = {
"data": [
{"type": "provider-groups", "id": str(provider_groups_fixture[0].id)},
{"type": "provider-group", "id": str(provider_groups_fixture[0].id)},
]
}
response = authenticated_client.post(
@@ -3819,7 +3456,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-groups", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
]
}
response = authenticated_client.patch(
@@ -3840,8 +3477,8 @@ class TestRoleProviderGroupRelationshipViewSet:
data = {
"data": [
{"type": "provider-groups", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-groups", "id": str(provider_groups_fixture[2].id)},
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-group", "id": str(provider_groups_fixture[2].id)},
]
}
response = authenticated_client.patch(
@@ -3877,7 +3514,7 @@ class TestRoleProviderGroupRelationshipViewSet:
def test_invalid_provider_group_id(self, authenticated_client, roles_fixture):
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
response = authenticated_client.post(
reverse(
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[1].id}
@@ -4038,7 +3675,7 @@ class TestProviderGroupMembershipViewSet:
):
provider_group, *_ = provider_groups_fixture
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
response = authenticated_client.post(
reverse(
"provider_group-providers-relationship",
@@ -4265,37 +3902,7 @@ class TestOverviewViewSet:
resources_fixture
)
def test_overview_services_list_no_required_filters(
self, authenticated_client, scan_summaries_fixture
):
response = authenticated_client.get(reverse("overview-services"))
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_overview_services_list(self, authenticated_client, scan_summaries_fixture):
response = authenticated_client.get(
reverse("overview-services"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
# Only two different services
assert len(response.json()["data"]) == 2
# Fixed data from the fixture, TODO improve this at some point with something more dynamic
service1_data = response.json()["data"][0]
service2_data = response.json()["data"][1]
assert service1_data["id"] == "service1"
assert service2_data["id"] == "service2"
# TODO fix numbers when muted_findings filter is fixed
assert service1_data["attributes"]["total"] == 3
assert service2_data["attributes"]["total"] == 1
assert service1_data["attributes"]["pass"] == 1
assert service2_data["attributes"]["pass"] == 1
assert service1_data["attributes"]["fail"] == 1
assert service2_data["attributes"]["fail"] == 0
assert service1_data["attributes"]["muted"] == 1
assert service2_data["attributes"]["muted"] == 0
# TODO Add more tests for the rest of overviews
@pytest.mark.django_db
+59 -196
View File
@@ -14,24 +14,24 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken
from api.models import (
ComplianceOverview,
Finding,
Invitation,
InvitationRoleRelationship,
Membership,
Provider,
ProviderGroup,
ProviderGroupMembership,
ProviderSecret,
Resource,
ResourceTag,
Finding,
ProviderSecret,
Invitation,
InvitationRoleRelationship,
Role,
RoleProviderGroupRelationship,
UserRoleRelationship,
ComplianceOverview,
Scan,
StateChoices,
Task,
User,
UserRoleRelationship,
)
from api.rls import Tenant
@@ -445,12 +445,7 @@ class MembershipSerializer(serializers.ModelSerializer):
# Provider Groups
class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
providers = serializers.ResourceRelatedField(many=True, read_only=True)
def validate(self, attrs):
if ProviderGroup.objects.filter(name=attrs.get("name")).exists():
@@ -480,93 +475,21 @@ class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
}
class ProviderGroupIncludedSerializer(ProviderGroupSerializer):
class ProviderGroupIncludedSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = ProviderGroup
fields = ["id", "name"]
class ProviderGroupCreateSerializer(ProviderGroupSerializer):
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
class ProviderGroupUpdateSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for updating the ProviderGroup model.
Only allows "name" field to be updated.
"""
class Meta:
model = ProviderGroup
fields = [
"id",
"name",
"inserted_at",
"updated_at",
"providers",
"roles",
]
def create(self, validated_data):
providers = validated_data.pop("providers", [])
roles = validated_data.pop("roles", [])
tenant_id = self.context.get("tenant_id")
provider_group = ProviderGroup.objects.create(
tenant_id=tenant_id, **validated_data
)
through_model_instances = [
ProviderGroupMembership(
provider_group=provider_group,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=provider_group,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return provider_group
class ProviderGroupUpdateSerializer(ProviderGroupSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "providers" in validated_data:
providers = validated_data.pop("providers")
instance.providers.clear()
through_model_instances = [
ProviderGroupMembership(
provider_group=instance,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=instance,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
fields = ["id", "name"]
class ProviderResourceIdentifierSerializer(serializers.Serializer):
@@ -874,7 +797,7 @@ class ResourceSerializer(RLSSerializer):
}
)
def get_tags(self, obj):
return obj.get_tags(self.context.get("tenant_id"))
return obj.get_tags()
def get_fields(self):
"""`type` is a Python reserved keyword."""
@@ -1233,12 +1156,6 @@ class InvitationSerializer(RLSSerializer):
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
class Meta:
model = Invitation
fields = [
@@ -1258,12 +1175,6 @@ class InvitationSerializer(RLSSerializer):
class InvitationBaseWriteSerializer(BaseWriteSerializer):
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
def validate_email(self, value):
user = User.objects.filter(email=value).first()
tenant_id = self.context["tenant_id"]
@@ -1324,10 +1235,6 @@ class InvitationCreateSerializer(InvitationBaseWriteSerializer, RLSSerializer):
class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
roles = serializers.ResourceRelatedField(
required=False, many=True, queryset=Role.objects.all()
)
class Meta:
model = Invitation
fields = ["id", "email", "expires_at", "state", "token", "roles"]
@@ -1340,19 +1247,15 @@ class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
}
def update(self, instance, validated_data):
roles = validated_data.pop("roles", [])
tenant_id = self.context.get("tenant_id")
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
new_relationships = [
InvitationRoleRelationship(
role=r, invitation=instance, tenant_id=tenant_id
)
for r in roles
]
InvitationRoleRelationship.objects.bulk_create(new_relationships)
invitation = super().update(instance, validated_data)
if roles:
instance.roles.clear()
for role in roles:
InvitationRoleRelationship.objects.create(
role=role, invitation=invitation, tenant_id=tenant_id
)
return invitation
@@ -1371,27 +1274,30 @@ class InvitationAcceptSerializer(RLSSerializer):
class RoleSerializer(RLSSerializer, BaseWriteSerializer):
permission_state = serializers.SerializerMethodField()
users = serializers.ResourceRelatedField(
queryset=User.objects.all(), many=True, required=False
)
provider_groups = serializers.ResourceRelatedField(
queryset=ProviderGroup.objects.all(), many=True, required=False
many=True, queryset=ProviderGroup.objects.all()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["users"].queryset = User.objects.filter(
membership__tenant__id=tenant_id
)
self.fields["provider_groups"].queryset = ProviderGroup.objects.filter(
tenant_id=self.context.get("tenant_id")
)
permission_state = serializers.SerializerMethodField()
def get_permission_state(self, obj) -> str:
return obj.permission_state
def get_permission_state(self, obj):
permission_fields = [
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
]
values = [getattr(obj, field) for field in permission_fields]
if all(values):
return "unlimited"
elif not any(values):
return "none"
else:
return "limited"
def validate(self, attrs):
if Role.objects.filter(name=attrs.get("name")).exists():
@@ -1417,11 +1323,9 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer):
"name",
"manage_users",
"manage_account",
# Disable for the first release
# "manage_billing",
# "manage_integrations",
# /Disable for the first release
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
"permission_state",
"unlimited_visibility",
@@ -1436,18 +1340,12 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer):
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"users": {"read_only": True},
"url": {"read_only": True},
}
class RoleCreateSerializer(RoleSerializer):
provider_groups = serializers.ResourceRelatedField(
many=True, queryset=ProviderGroup.objects.all(), required=False
)
users = serializers.ResourceRelatedField(
many=True, queryset=User.objects.all(), required=False
)
def create(self, validated_data):
provider_groups = validated_data.pop("provider_groups", [])
users = validated_data.pop("users", [])
@@ -1466,7 +1364,7 @@ class RoleCreateSerializer(RoleSerializer):
through_model_instances = [
UserRoleRelationship(
role=role,
role=user,
user=user,
tenant_id=tenant_id,
)
@@ -1478,36 +1376,19 @@ class RoleCreateSerializer(RoleSerializer):
class RoleUpdateSerializer(RoleSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "provider_groups" in validated_data:
provider_groups = validated_data.pop("provider_groups")
instance.provider_groups.clear()
through_model_instances = [
RoleProviderGroupRelationship(
role=instance,
provider_group=provider_group,
tenant_id=tenant_id,
)
for provider_group in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
if "users" in validated_data:
users = validated_data.pop("users")
instance.users.clear()
through_model_instances = [
UserRoleRelationship(
role=instance,
user=user,
tenant_id=tenant_id,
)
for user in users
]
UserRoleRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
class Meta:
model = Role
fields = [
"id",
"name",
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
"unlimited_visibility",
]
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):
@@ -1791,24 +1672,6 @@ class OverviewSeveritySerializer(serializers.Serializer):
return {"version": "v1"}
class OverviewServiceSerializer(serializers.Serializer):
id = serializers.CharField(source="service")
total = serializers.IntegerField()
_pass = serializers.IntegerField()
fail = serializers.IntegerField()
muted = serializers.IntegerField()
class JSONAPIMeta:
resource_name = "services-overview"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["pass"] = self.fields.pop("_pass")
def get_root_meta(self, _resource, _many):
return {"version": "v1"}
# Schedules
+218 -260
View File
@@ -8,6 +8,7 @@ from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from drf_spectacular.settings import spectacular_settings
from drf_spectacular_jsonapi.schemas.openapi import JsonApiAutoSchema
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
@@ -16,7 +17,6 @@ from drf_spectacular.utils import (
extend_schema_view,
)
from drf_spectacular.views import SpectacularAPIView
from drf_spectacular_jsonapi.schemas.openapi import JsonApiAutoSchema
from rest_framework import permissions, status
from rest_framework.decorators import action
from rest_framework.exceptions import (
@@ -26,9 +26,10 @@ from rest_framework.exceptions import (
ValidationError,
)
from rest_framework.generics import GenericAPIView, get_object_or_404
from rest_framework.permissions import SAFE_METHODS
from rest_framework_json_api.views import RelationshipView, Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework.permissions import SAFE_METHODS
from tasks.beat import schedule_provider_scan
from tasks.tasks import (
check_provider_connection_task,
@@ -49,15 +50,17 @@ from api.filters import (
ProviderGroupFilter,
ProviderSecretFilter,
ResourceFilter,
RoleFilter,
ScanFilter,
ScanSummaryFilter,
ServiceOverviewFilter,
TaskFilter,
TenantFilter,
UserFilter,
RoleFilter,
)
from api.models import (
StatusChoices,
User,
UserRoleRelationship,
ComplianceOverview,
Finding,
Invitation,
@@ -66,24 +69,27 @@ from api.models import (
ProviderGroup,
ProviderGroupMembership,
ProviderSecret,
Resource,
Role,
RoleProviderGroupRelationship,
Resource,
Scan,
ScanSummary,
SeverityChoices,
StateChoices,
StatusChoices,
Task,
User,
UserRoleRelationship,
)
from api.pagination import ComplianceOverviewPagination
from api.rbac.permissions import Permissions, get_providers, get_role
from api.rbac.permissions import DISABLE_RBAC, HasPermissions, Permissions
from api.rls import Tenant
from api.utils import validate_invitation
from api.uuid_utils import datetime_to_uuid7
from api.v1.serializers import (
TokenSerializer,
TokenRefreshSerializer,
UserSerializer,
UserCreateSerializer,
UserUpdateSerializer,
UserRoleRelationshipSerializer,
ComplianceOverviewFullSerializer,
ComplianceOverviewSerializer,
FindingDynamicFilterSerializer,
@@ -95,37 +101,30 @@ from api.v1.serializers import (
MembershipSerializer,
OverviewFindingSerializer,
OverviewProviderSerializer,
OverviewServiceSerializer,
OverviewSeveritySerializer,
ProviderCreateSerializer,
ProviderGroupCreateSerializer,
ProviderGroupMembershipSerializer,
ProviderGroupSerializer,
ProviderGroupUpdateSerializer,
ProviderSecretCreateSerializer,
ProviderSecretSerializer,
ProviderSecretUpdateSerializer,
RoleProviderGroupRelationshipSerializer,
ProviderSerializer,
ProviderUpdateSerializer,
ResourceSerializer,
RoleCreateSerializer,
RoleProviderGroupRelationshipSerializer,
RoleSerializer,
RoleUpdateSerializer,
ScanCreateSerializer,
ScanSerializer,
ScanUpdateSerializer,
ScheduleDailyCreateSerializer,
TaskSerializer,
TenantSerializer,
TokenRefreshSerializer,
TokenSerializer,
UserCreateSerializer,
UserRoleRelationshipSerializer,
UserSerializer,
UserUpdateSerializer,
TaskSerializer,
ScanSerializer,
ScanCreateSerializer,
ScanUpdateSerializer,
ResourceSerializer,
ProviderSecretSerializer,
ProviderSecretUpdateSerializer,
ProviderSecretCreateSerializer,
RoleSerializer,
RoleCreateSerializer,
RoleUpdateSerializer,
ScheduleDailyCreateSerializer,
)
CACHE_DECORATOR = cache_control(
max_age=django_settings.CACHE_MAX_AGE,
stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE,
@@ -192,7 +191,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.1.1"
spectacular_settings.VERSION = "1.0.1"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -291,30 +290,32 @@ class UserViewSet(BaseUserViewset):
filterset_class = UserFilter
ordering = ["-date_joined"]
ordering_fields = ["name", "email", "company_name", "date_joined", "is_active"]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_USERS]
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def set_required_permissions(self):
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = self.get_required_permissions()
super().initial(request, *args, **kwargs)
def get_required_permissions(self):
"""
Returns the required permissions based on the request method.
"""
if self.action == "me":
# No permissions required for me request
self.required_permissions = []
return []
else:
# Require permission for the rest of the requests
self.required_permissions = [Permissions.MANAGE_USERS]
return [Permissions.MANAGE_USERS]
def get_queryset(self):
# If called during schema generation, return an empty queryset
if getattr(self, "swagger_fake_view", False):
return User.objects.none()
queryset = (
User.objects.filter(membership__tenant__id=self.request.tenant_id)
if hasattr(self.request, "tenant_id")
else User.objects.all()
)
return queryset.prefetch_related("memberships", "roles")
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
def get_permissions(self):
if self.action == "create":
@@ -333,7 +334,7 @@ class UserViewSet(BaseUserViewset):
@action(detail=False, methods=["get"], url_name="me")
def me(self, request):
user = self.request.user
user = self.get_queryset().first()
serializer = UserSerializer(user, context=self.get_serializer_context())
return Response(
data=serializer.data,
@@ -385,6 +386,7 @@ class UserViewSet(BaseUserViewset):
user=user, tenant=tenant, role=role
)
if invitation:
# TODO: Add roles to output relationships
user_role = []
for role in invitation.roles.all():
user_role.append(
@@ -453,11 +455,9 @@ class UserRoleRelationshipView(RelationshipView, BaseRLSViewSet):
resource_name = "roles"
http_method_names = ["post", "patch", "delete"]
schema = RelationshipViewSchema()
# RBAC required permissions
required_permissions = [Permissions.MANAGE_USERS]
def get_queryset(self):
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
return User.objects.all()
def create(self, request, *args, **kwargs):
user = self.get_object()
@@ -541,12 +541,11 @@ class TenantViewSet(BaseTenantViewset):
search_fields = ["name"]
ordering = ["-inserted_at"]
ordering_fields = ["name", "inserted_at", "updated_at"]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def get_queryset(self):
queryset = Tenant.objects.filter(membership__user=self.request.user)
return queryset.prefetch_related("memberships")
return Tenant.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@@ -601,13 +600,10 @@ class MembershipViewSet(BaseTenantViewset):
"role",
"date_joined",
]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
def get_queryset(self):
user = self.request.user
queryset = Membership.objects.filter(user_id=user.id)
return queryset.select_related("user", "tenant")
return Membership.objects.filter(user_id=user.id)
@extend_schema_view(
@@ -637,8 +633,6 @@ class TenantMembersViewSet(BaseTenantViewset):
http_method_names = ["get", "delete"]
serializer_class = MembershipSerializer
queryset = Membership.objects.none()
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
def get_queryset(self):
tenant = self.get_tenant()
@@ -724,34 +718,49 @@ class ProviderGroupViewSet(BaseRLSViewSet):
filterset_class = ProviderGroupFilter
http_method_names = ["get", "post", "patch", "delete"]
ordering = ["inserted_at"]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_PROVIDERS]
required_permissions = []
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def set_required_permissions(self):
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = self.get_required_permissions()
super().initial(request, *args, **kwargs)
def get_required_permissions(self):
"""
Returns the required permissions based on the request method.
"""
if self.request.method in SAFE_METHODS:
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
# No permissions required for GET requests
self.required_permissions = []
return []
else:
# Require permission for non-GET requests
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
return [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = get_role(self.request.user)
user = self.request.user
user_roles = user.roles.all()
# Check if any of the user's roles have UNLIMITED_VISIBILITY
if user_roles.unlimited_visibility:
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all provider groups
return ProviderGroup.objects.prefetch_related("providers", "roles")
return ProviderGroup.objects.prefetch_related("providers")
# Collect provider groups associated with the user's roles
return user_roles.provider_groups.all().prefetch_related("providers", "roles")
provider_groups = (
ProviderGroup.objects.filter(roles__in=user_roles)
.distinct()
.prefetch_related("providers")
)
return provider_groups
def get_serializer_class(self):
if self.action == "create":
return ProviderGroupCreateSerializer
elif self.action == "partial_update":
if self.action == "partial_update":
return ProviderGroupUpdateSerializer
return super().get_serializer_class()
@@ -793,11 +802,9 @@ class ProviderGroupProvidersRelationshipView(RelationshipView, BaseRLSViewSet):
resource_name = "providers"
http_method_names = ["post", "patch", "delete"]
schema = RelationshipViewSchema()
# RBAC required permissions
required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
return ProviderGroup.objects.filter(tenant_id=self.request.tenant_id)
return ProviderGroup.objects.all()
def create(self, request, *args, **kwargs):
provider_group = self.get_object()
@@ -893,29 +900,43 @@ class ProviderViewSet(BaseRLSViewSet):
"inserted_at",
"updated_at",
]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_PROVIDERS]
required_permissions = []
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def set_required_permissions(self):
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = self.get_required_permissions()
super().initial(request, *args, **kwargs)
def get_required_permissions(self):
"""
Returns the required permissions based on the request method.
"""
if self.request.method in SAFE_METHODS:
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
# No permissions required for GET requests
self.required_permissions = []
return []
else:
# Require permission for non-GET requests
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
return [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
user = self.request.user
user_roles = user.roles.all()
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all providers
queryset = Provider.objects.filter(tenant_id=self.request.tenant_id)
else:
# User lacks permission, filter providers based on provider groups associated with the role
queryset = get_providers(user_roles)
return queryset.select_related("secret").prefetch_related("provider_groups")
return Provider.objects.all()
# User lacks permission, filter providers based on provider groups associated with the role
provider_groups = user_roles[0].provider_groups.all()
providers = Provider.objects.filter(
provider_groups__in=provider_groups
).distinct()
return providers
def get_serializer_class(self):
if self.action == "create":
@@ -953,7 +974,7 @@ class ProviderViewSet(BaseRLSViewSet):
get_object_or_404(Provider, pk=pk)
with transaction.atomic():
task = check_provider_connection_task.delay(
provider_id=pk, tenant_id=self.request.tenant_id
provider_id=pk, tenant_id=request.tenant_id
)
prowler_task = Task.objects.get(id=task.id)
serializer = TaskSerializer(prowler_task)
@@ -974,7 +995,7 @@ class ProviderViewSet(BaseRLSViewSet):
with transaction.atomic():
task = delete_provider_task.delay(
provider_id=pk, tenant_id=self.request.tenant_id
provider_id=pk, tenant_id=request.tenant_id
)
prowler_task = Task.objects.get(id=task.id)
serializer = TaskSerializer(prowler_task)
@@ -1035,29 +1056,42 @@ class ScanViewSet(BaseRLSViewSet):
"inserted_at",
"updated_at",
]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_SCANS]
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def set_required_permissions(self):
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = self.get_required_permissions()
super().initial(request, *args, **kwargs)
def get_required_permissions(self):
"""
Returns the required permissions based on the request method.
"""
if self.request.method in SAFE_METHODS:
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
# No permissions required for GET requests
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
return []
else:
# Require permission for non-GET requests
self.required_permissions = [Permissions.MANAGE_SCANS]
return [Permissions.MANAGE_SCANS]
def get_queryset(self):
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
user = self.request.user
user_roles = user.roles.all()
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all scans
queryset = Scan.objects.filter(tenant_id=self.request.tenant_id)
else:
# User lacks permission, filter providers based on provider groups associated with the role
queryset = Scan.objects.filter(provider__in=get_providers(user_roles))
return queryset.select_related("provider", "task")
return Scan.objects.all()
# User lacks permission, filter providers based on provider groups associated with the role
provider_groups = user_roles[0].provider_groups.all()
providers = Provider.objects.filter(
provider_groups__in=provider_groups
).distinct()
return Scan.objects.filter(provider__in=providers).distinct()
def get_serializer_class(self):
if self.action == "create":
@@ -1091,14 +1125,14 @@ class ScanViewSet(BaseRLSViewSet):
with transaction.atomic():
task = perform_scan_task.apply_async(
kwargs={
"tenant_id": self.request.tenant_id,
"tenant_id": request.tenant_id,
"scan_id": str(scan.id),
"provider_id": str(scan.provider_id),
# Disabled for now
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
},
link=perform_scan_summary_task.si(
tenant_id=self.request.tenant_id,
tenant_id=request.tenant_id,
scan_id=str(scan.id),
),
)
@@ -1147,14 +1181,28 @@ class TaskViewSet(BaseRLSViewSet):
search_fields = ["name"]
ordering = ["-inserted_at"]
ordering_fields = ["inserted_at", "completed_at", "name", "state"]
# RBAC required permissions
required_permissions = []
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def get_queryset(self):
return Task.objects.annotate(
name=F("task_runner_task__task_name"),
state=F("task_runner_task__status"),
).select_related("task_runner_task")
user = self.request.user
user_roles = user.roles.all()
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all tasks
return Task.objects.annotate(
name=F("task_runner_task__task_name"),
state=F("task_runner_task__status"),
)
# User lacks permission, filter tasks based on provider groups associated with the role
provider_groups = user_roles[0].provider_groups.all()
providers = Provider.objects.filter(
provider_groups__in=provider_groups
).distinct()
scans = Scan.objects.filter(provider__in=providers).distinct()
return Task.objects.filter(scan__in=scans).distinct()
def destroy(self, request, *args, pk=None, **kwargs):
task = get_object_or_404(Task, pk=pk)
@@ -1215,20 +1263,31 @@ class ResourceViewSet(BaseRLSViewSet):
"inserted_at",
"updated_at",
]
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
# the provider through the provider group)
required_permissions = []
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = ResourceViewSet.required_permissions
super().initial(request, *args, **kwargs)
def get_queryset(self):
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
user = self.request.user
user_roles = user.roles.all()
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all scans
queryset = Resource.objects.filter(tenant_id=self.request.tenant_id)
queryset = Resource.objects.all()
else:
# User lacks permission, filter providers based on provider groups associated with the role
queryset = Resource.objects.filter(
tenant_id=self.request.tenant_id, provider__in=get_providers(user_roles)
)
provider_groups = user_roles[0].provider_groups.all()
providers = Provider.objects.filter(
provider_groups__in=provider_groups
).distinct()
queryset = Resource.objects.filter(provider__in=providers).distinct()
search_value = self.request.query_params.get("filter[search]", None)
if search_value:
@@ -1301,9 +1360,15 @@ class FindingViewSet(BaseRLSViewSet):
"inserted_at",
"updated_at",
]
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
# the provider through the provider group)
required_permissions = []
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.required_permissions = ResourceViewSet.required_permissions
super().initial(request, *args, **kwargs)
def get_serializer_class(self):
if self.action == "findings_services_regions":
@@ -1312,15 +1377,21 @@ class FindingViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def get_queryset(self):
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
user = self.request.user
user_roles = user.roles.all()
if DISABLE_RBAC or getattr(
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
):
# User has unlimited visibility, return all scans
queryset = Finding.objects.filter(tenant_id=self.request.tenant_id)
queryset = Finding.objects.all()
else:
# User lacks permission, filter providers based on provider groups associated with the role
queryset = Finding.objects.filter(
scan__provider__in=get_providers(user_roles)
)
provider_groups = user_roles[0].provider_groups.all()
providers = Provider.objects.filter(
provider_groups__in=provider_groups
).distinct()
scans = Scan.objects.filter(provider__in=providers).distinct()
queryset = Finding.objects.filter(scan__in=scans).distinct()
search_value = self.request.query_params.get("filter[search]", None)
if search_value:
@@ -1418,11 +1489,9 @@ class ProviderSecretViewSet(BaseRLSViewSet):
"inserted_at",
"updated_at",
]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
return ProviderSecret.objects.filter(tenant_id=self.request.tenant_id)
return ProviderSecret.objects.all()
def get_serializer_class(self):
if self.action == "create":
@@ -1477,11 +1546,11 @@ class InvitationViewSet(BaseRLSViewSet):
"state",
"inviter",
]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def get_queryset(self):
return Invitation.objects.filter(tenant_id=self.request.tenant_id)
return Invitation.objects.all()
def get_serializer_class(self):
if self.action == "create":
@@ -1528,7 +1597,7 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
http_method_names = ["post"]
def get_queryset(self):
return Invitation.objects.filter(tenant_id=self.request.tenant_id)
return Invitation.objects.all()
def get_serializer_class(self):
if hasattr(self, "response_serializer_class"):
@@ -1566,6 +1635,7 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
user=user,
tenant=invitation.tenant,
)
# TODO: Add roles to output relationships
user_role = []
for role in invitation.roles.all():
user_role.append(
@@ -1616,11 +1686,11 @@ class RoleViewSet(BaseRLSViewSet):
filterset_class = RoleFilter
http_method_names = ["get", "post", "patch", "delete"]
ordering = ["inserted_at"]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
def get_queryset(self):
return Role.objects.filter(tenant_id=self.request.tenant_id)
return Role.objects.all()
def get_serializer_class(self):
if self.action == "create":
@@ -1630,28 +1700,19 @@ class RoleViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def partial_update(self, request, *args, **kwargs):
user_role = get_role(request.user)
user = request.user
user_role = user.roles.all().first()
# If the user is the owner of the role, the manage_account field is not editable
if user_role and kwargs["pk"] == str(user_role.id):
request.data["manage_account"] = str(user_role.manage_account).lower()
return super().partial_update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if (
instance.name == "admin"
): # TODO: Move to a constant/enum (in case other roles are created by default)
raise ValidationError(detail="The admin role cannot be deleted.")
return super().destroy(request, *args, **kwargs)
@extend_schema_view(
create=extend_schema(
tags=["Role"],
summary="Create a new role-provider_groups relationship",
description="Add a new role-provider_groups relationship to the system by providing the required "
"role-provider_groups details.",
description="Add a new role-provider_groups relationship to the system by providing the required role-provider_groups details.",
responses={
204: OpenApiResponse(description="Relationship created successfully"),
400: OpenApiResponse(
@@ -1686,11 +1747,9 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
resource_name = "provider_groups"
http_method_names = ["post", "patch", "delete"]
schema = RelationshipViewSchema()
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
def get_queryset(self):
return Role.objects.filter(tenant_id=self.request.tenant_id)
return Role.objects.all()
def create(self, request, *args, **kwargs):
role = self.get_object()
@@ -1773,41 +1832,12 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
search_fields = ["compliance_id"]
ordering = ["compliance_id"]
ordering_fields = ["inserted_at", "compliance_id", "framework", "region"]
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
# the provider through the provider group)
required_permissions = []
def get_queryset(self):
role = get_role(self.request.user)
unlimited_visibility = getattr(
role, Permissions.UNLIMITED_VISIBILITY.value, False
)
if self.action == "retrieve":
if unlimited_visibility:
# User has unlimited visibility, return all compliance
return ComplianceOverview.objects.filter(
tenant_id=self.request.tenant_id
)
return ComplianceOverview.objects.all()
providers = get_providers(role)
return ComplianceOverview.objects.filter(
tenant_id=self.request.tenant_id, scan__provider__in=providers
)
if unlimited_visibility:
base_queryset = self.filter_queryset(
ComplianceOverview.objects.filter(tenant_id=self.request.tenant_id)
)
else:
providers = Provider.objects.filter(
provider_groups__in=role.provider_groups.all()
).distinct()
base_queryset = self.filter_queryset(
ComplianceOverview.objects.filter(
tenant_id=self.request.tenant_id, scan__provider__in=providers
)
)
base_queryset = self.filter_queryset(ComplianceOverview.objects.all())
max_failed_ids = (
base_queryset.filter(compliance_id=OuterRef("compliance_id"))
@@ -1815,10 +1845,12 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
.values("id")[:1]
)
return base_queryset.filter(id__in=Subquery(max_failed_ids)).order_by(
queryset = base_queryset.filter(id__in=Subquery(max_failed_ids)).order_by(
"compliance_id"
)
return queryset
def get_serializer_class(self):
if self.action == "retrieve":
return ComplianceOverviewFullSerializer
@@ -1870,40 +1902,20 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
),
filters=True,
),
services=extend_schema(
summary="Get findings data by service",
description=(
"Retrieve an aggregated summary of findings grouped by service. The response includes the total count "
"of findings for each service, as long as there are at least one finding for that service. At least "
"one of the `inserted_at` filters must be provided."
),
filters=True,
),
)
@method_decorator(CACHE_DECORATOR, name="list")
class OverviewViewSet(BaseRLSViewSet):
queryset = ComplianceOverview.objects.all()
http_method_names = ["get"]
ordering = ["-id"]
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
# the provider through the provider group)
required_permissions = []
def get_queryset(self):
role = get_role(self.request.user)
providers = get_providers(role)
def _get_filtered_queryset(model):
if role.unlimited_visibility:
return model.objects.filter(tenant_id=self.request.tenant_id)
return model.objects.filter(
tenant_id=self.request.tenant_id, scan__provider__in=providers
)
if self.action == "providers":
return _get_filtered_queryset(Finding)
elif self.action in ("findings", "findings_severity", "services"):
return _get_filtered_queryset(ScanSummary)
return Finding.objects.all()
elif self.action == "findings":
return ScanSummary.objects.all()
elif self.action == "findings_severity":
return ScanSummary.objects.all()
else:
return super().get_queryset()
@@ -1914,8 +1926,6 @@ class OverviewViewSet(BaseRLSViewSet):
return OverviewFindingSerializer
elif self.action == "findings_severity":
return OverviewSeveritySerializer
elif self.action == "services":
return OverviewServiceSerializer
return super().get_serializer_class()
def get_filterset_class(self):
@@ -1923,8 +1933,6 @@ class OverviewViewSet(BaseRLSViewSet):
return None
elif self.action in ["findings", "findings_severity"]:
return ScanSummaryFilter
elif self.action == "services":
return ServiceOverviewFilter
return None
@extend_schema(exclude=True)
@@ -1937,22 +1945,17 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="providers")
def providers(self, request):
tenant_id = self.request.tenant_id
# Subquery to get the most recent finding for each uid
latest_finding_ids = (
Finding.objects.filter(
tenant_id=tenant_id,
uid=OuterRef("uid"),
scan__provider=OuterRef("scan__provider"),
uid=OuterRef("uid"), scan__provider=OuterRef("scan__provider")
)
.order_by("-id") # Most recent by id
.values("id")[:1]
)
# Filter findings to only include the most recent for each uid
recent_findings = Finding.objects.filter(
tenant_id=tenant_id, id__in=Subquery(latest_finding_ids)
)
recent_findings = Finding.objects.filter(id__in=Subquery(latest_finding_ids))
# Aggregate findings by provider
findings_aggregated = (
@@ -1969,10 +1972,8 @@ class OverviewViewSet(BaseRLSViewSet):
)
# Aggregate total resources by provider
resources_aggregated = (
Resource.objects.filter(tenant_id=tenant_id)
.values("provider__provider")
.annotate(total_resources=Count("id"))
resources_aggregated = Resource.objects.values("provider__provider").annotate(
total_resources=Count("id")
)
# Combine findings and resources data
@@ -2004,15 +2005,12 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="findings")
def findings(self, request):
tenant_id = self.request.tenant_id
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"),
state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id")
)
.order_by("-id")
.values("id")[:1]
@@ -2049,15 +2047,12 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="findings_severity")
def findings_severity(self, request):
tenant_id = self.request.tenant_id
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"),
state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id")
)
.order_by("-id")
.values("id")[:1]
@@ -2083,41 +2078,6 @@ class OverviewViewSet(BaseRLSViewSet):
serializer = OverviewSeveritySerializer(severity_data)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"], url_name="services")
def services(self, request):
tenant_id = self.request.tenant_id
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("-id")
.values("id")[:1]
)
annotated_queryset = filtered_queryset.annotate(
latest_scan_id=Subquery(latest_scan_subquery)
)
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
services_data = (
filtered_queryset.values("service")
.annotate(_pass=Sum("_pass"))
.annotate(fail=Sum("fail"))
.annotate(muted=Sum("muted"))
.annotate(total=Sum("total"))
.order_by("service")
)
serializer = OverviewServiceSerializer(services_data, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(tags=["Schedule"])
@extend_schema_view(
@@ -2133,8 +2093,6 @@ class ScheduleViewSet(BaseRLSViewSet):
# TODO: change to Schedule when implemented
queryset = Task.objects.none()
http_method_names = ["post"]
# RBAC required permissions
required_permissions = [Permissions.MANAGE_SCANS]
def get_queryset(self):
return super().get_queryset()
+2 -13
View File
@@ -1,21 +1,10 @@
from celery import Celery, Task
from config.env import env
BROKER_VISIBILITY_TIMEOUT = env.int("DJANGO_BROKER_VISIBILITY_TIMEOUT", default=86400)
celery_app = Celery("tasks")
celery_app.config_from_object("django.conf:settings", namespace="CELERY")
celery_app.conf.update(result_extended=True, result_expires=None)
celery_app.conf.broker_transport_options = {
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
}
celery_app.conf.result_backend_transport_options = {
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
}
celery_app.conf.visibility_timeout = BROKER_VISIBILITY_TIMEOUT
celery_app.autodiscover_tasks(["api"])
@@ -46,10 +35,10 @@ class RLSTask(Task):
**options,
)
task_result_instance = TaskResult.objects.get(task_id=result.task_id)
from api.db_utils import rls_transaction
from api.db_utils import tenant_transaction
tenant_id = kwargs.get("tenant_id")
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
APITask.objects.create(
id=task_result_instance.task_id,
tenant_id=tenant_id,
+3
View File
@@ -207,3 +207,6 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
TESTING = False
# Disable RBAC during tests/demos
DISABLE_RBAC = False
+2 -2
View File
@@ -10,8 +10,8 @@ DATABASES = {
"default": {
"ENGINE": "psqlextra.backend",
"NAME": "prowler_db_test",
"USER": env("POSTGRES_USER", default="prowler_admin"),
"PASSWORD": env("POSTGRES_PASSWORD", default="postgres"),
"USER": env("POSTGRES_USER", default="prowler"),
"PASSWORD": env("POSTGRES_PASSWORD", default="S3cret"),
"HOST": env("POSTGRES_HOST", default="localhost"),
"PORT": env("POSTGRES_PORT", default="5432"),
},
+33 -228
View File
@@ -1,39 +1,38 @@
import logging
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from django.conf import settings
from django.db import connection as django_connection
from django.db import connections as django_connections
from datetime import datetime, timezone, timedelta
from django.db import connections as django_connections, connection as django_connection
from django.urls import reverse
from django_celery_results.models import TaskResult
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
from rest_framework import status
from rest_framework.test import APIClient
from unittest.mock import patch
from api.db_utils import rls_transaction
from api.models import (
ComplianceOverview,
Finding,
Invitation,
Membership,
)
from api.models import (
User,
Provider,
ProviderGroup,
ProviderSecret,
Resource,
ResourceTag,
Role,
Scan,
ScanSummary,
StateChoices,
Task,
User,
Membership,
ProviderSecret,
Invitation,
ComplianceOverview,
UserRoleRelationship,
)
from api.rls import Tenant
from api.v1.serializers import TokenSerializer
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
@@ -76,6 +75,16 @@ def disable_logging():
logging.disable(logging.CRITICAL)
@pytest.fixture(scope="function")
def patch_testing_flag():
"""
Fixture to patch the TESTING flag to True during tests.
"""
with patch("api.rbac.permissions.DISABLE_RBAC", True):
with patch("api.v1.views.DISABLE_RBAC", True):
yield
@pytest.fixture(scope="session", autouse=True)
def create_test_user(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
@@ -88,14 +97,16 @@ def create_test_user(django_db_setup, django_db_blocker):
@pytest.fixture(scope="function")
def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
def create_test_user_rbac(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing",
email="rbac@rbac.com",
password=TEST_PASSWORD,
)
tenant = tenants_fixture[0]
tenant = Tenant.objects.create(
name="Tenant Test",
)
Membership.objects.create(
user=user,
tenant=tenant,
@@ -120,24 +131,6 @@ def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
return user
@pytest.fixture(scope="function")
def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker, tenants_fixture):
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing",
email="rbac_noroles@rbac.com",
password=TEST_PASSWORD,
)
tenant = tenants_fixture[0]
Membership.objects.create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.OWNER,
)
return user
@pytest.fixture(scope="function")
def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
@@ -176,32 +169,8 @@ def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
@pytest.fixture
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
client.user = create_test_user_rbac
tenant_id = tenants_fixture[0].id
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "rbac@rbac.com",
"password": TEST_PASSWORD,
"tenant_id": tenant_id,
}
)
serializer.is_valid(raise_exception=True)
access_token = serializer.validated_data["access"]
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
return client
@pytest.fixture
def authenticated_client_rbac_noroles(
create_test_user_rbac_no_roles, tenants_fixture, client
):
client.user = create_test_user_rbac_no_roles
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "rbac_noroles@rbac.com",
"password": TEST_PASSWORD,
}
data={"type": "tokens", "email": "rbac@rbac.com", "password": TEST_PASSWORD}
)
serializer.is_valid()
access_token = serializer.validated_data["access"]
@@ -228,9 +197,7 @@ def authenticated_client_no_permissions_rbac(
@pytest.fixture
def authenticated_client(
create_test_user, tenants_fixture, set_user_admin_roles_fixture, client
):
def authenticated_client(create_test_user, tenants_fixture, client):
client.user = create_test_user
serializer = TokenSerializer(
data={"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD}
@@ -279,33 +246,10 @@ def tenants_fixture(create_test_user):
return tenant1, tenant2, tenant3
@pytest.fixture
def set_user_admin_roles_fixture(create_test_user, tenants_fixture):
user = create_test_user
for tenant in tenants_fixture[:2]:
with rls_transaction(str(tenant.id)):
role = Role.objects.create(
name="admin",
tenant_id=tenant.id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)
UserRoleRelationship.objects.create(
user=user,
role=role,
tenant_id=tenant.id,
)
@pytest.fixture
def invitations_fixture(create_test_user, tenants_fixture):
user = create_test_user
tenant = tenants_fixture[0]
*_, tenant = tenants_fixture
valid_invitation = Invitation.objects.create(
email="testing@prowler.com",
state=Invitation.State.PENDING,
@@ -324,20 +268,6 @@ def invitations_fixture(create_test_user, tenants_fixture):
return valid_invitation, expired_invitation
@pytest.fixture
def users_fixture(django_user_model):
user1 = User.objects.create_user(
name="user1", email="test_unit0@prowler.com", password="S3cret"
)
user2 = User.objects.create_user(
name="user2", email="test_unit1@prowler.com", password="S3cret"
)
user3 = User.objects.create_user(
name="user3", email="test_unit2@prowler.com", password="S3cret"
)
return user1, user2, user3
@pytest.fixture
def providers_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
@@ -395,23 +325,6 @@ def provider_groups_fixture(tenants_fixture):
return pgroup1, pgroup2, pgroup3
@pytest.fixture
def admin_role_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
return Role.objects.get_or_create(
name="admin",
tenant_id=tenant.id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)[0]
@pytest.fixture
def roles_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
@@ -448,19 +361,8 @@ def roles_fixture(tenants_fixture):
manage_scans=True,
unlimited_visibility=True,
)
role4 = Role.objects.create(
name="Role Four",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
return role1, role2, role3, role4
return role1, role2, role3
@pytest.fixture
@@ -790,107 +692,10 @@ def get_api_tokens(
data=json_body,
format="vnd.api+json",
)
return (
response.json()["data"]["attributes"]["access"],
response.json()["data"]["attributes"]["refresh"],
)
@pytest.fixture
def scan_summaries_fixture(tenants_fixture, providers_fixture):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="overview scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check1",
service="service1",
severity="high",
region="region1",
_pass=1,
fail=0,
muted=0,
total=1,
new=1,
changed=0,
unchanged=0,
fail_new=0,
fail_changed=0,
pass_new=1,
pass_changed=0,
muted_new=0,
muted_changed=0,
scan=scan,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check1",
service="service1",
severity="high",
region="region2",
_pass=0,
fail=1,
muted=1,
total=2,
new=2,
changed=0,
unchanged=0,
fail_new=1,
fail_changed=0,
pass_new=0,
pass_changed=0,
muted_new=1,
muted_changed=0,
scan=scan,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check2",
service="service2",
severity="critical",
region="region1",
_pass=1,
fail=0,
muted=0,
total=1,
new=1,
changed=0,
unchanged=0,
fail_new=0,
fail_changed=0,
pass_new=1,
pass_changed=0,
muted_new=0,
muted_changed=0,
scan=scan,
)
return response.json()["data"]["attributes"]["access"], response.json()["data"][
"attributes"
]["refresh"]
def get_authorization_header(access_token: str) -> dict:
return {"Authorization": f"Bearer {access_token}"}
def pytest_collection_modifyitems(items):
"""Ensure test_rbac.py is executed first."""
items.sort(key=lambda item: 0 if "test_rbac.py" in item.nodeid else 1)
def pytest_configure(config):
# Apply the mock before the test session starts. This is necessary to avoid admin error when running the
# 0004_rbac_missing_admin_roles migration
patch("api.db_router.MainRouter.admin_db", new="default").start()
def pytest_unconfigure(config):
# Stop all patches after the test session ends. This is necessary to avoid admin error when running the
# 0004_rbac_missing_admin_roles migration
patch.stopall()
+2 -2
View File
@@ -2,7 +2,7 @@ from celery.utils.log import get_task_logger
from django.db import transaction
from api.db_router import MainRouter
from api.db_utils import batch_delete, rls_transaction
from api.db_utils import batch_delete, tenant_transaction
from api.models import Finding, Provider, Resource, Scan, ScanSummary, Tenant
logger = get_task_logger(__name__)
@@ -66,7 +66,7 @@ def delete_tenant(pk: str):
deletion_summary = {}
for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk):
with rls_transaction(pk):
with tenant_transaction(pk):
summary = delete_provider(provider.id)
deletion_summary.update(summary)
+18 -16
View File
@@ -11,7 +11,7 @@ from api.compliance import (
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
generate_scan_compliance,
)
from api.db_utils import rls_transaction
from api.db_utils import tenant_transaction
from api.models import (
ComplianceOverview,
Finding,
@@ -69,7 +69,7 @@ def _store_resources(
- tuple[str, str]: A tuple containing the resource UID and region.
"""
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
resource_instance, created = Resource.objects.get_or_create(
tenant_id=tenant_id,
provider=provider_instance,
@@ -86,7 +86,7 @@ def _store_resources(
resource_instance.service = finding.service_name
resource_instance.type = finding.resource_type
resource_instance.save()
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
tags = [
ResourceTag.objects.get_or_create(
tenant_id=tenant_id, key=key, value=value
@@ -116,12 +116,13 @@ def perform_prowler_scan(
ValueError: If the provider cannot be connected.
"""
generate_compliance = False
check_status_by_region = {}
exception = None
unique_resources = set()
start_time = time.time()
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
scan_instance = Scan.objects.get(pk=scan_id)
scan_instance.state = StateChoices.EXECUTING
@@ -129,7 +130,7 @@ def perform_prowler_scan(
scan_instance.save()
try:
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
try:
prowler_provider = initialize_prowler_provider(provider_instance)
provider_instance.connected = True
@@ -144,6 +145,7 @@ def perform_prowler_scan(
)
provider_instance.save()
generate_compliance = provider_instance.provider != Provider.ProviderChoices.GCP
prowler_scan = ProwlerScan(provider=prowler_provider, checks=checks_to_execute)
resource_cache = {}
@@ -154,7 +156,7 @@ def perform_prowler_scan(
for finding in findings:
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
try:
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
# Process resource
resource_uid = finding.resource_uid
if resource_uid not in resource_cache:
@@ -186,7 +188,7 @@ def perform_prowler_scan(
resource_instance.type = finding.resource_type
updated_fields.append("type")
if updated_fields:
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
resource_instance.save(update_fields=updated_fields)
except (OperationalError, IntegrityError) as db_err:
if attempt < CELERY_DEADLOCK_ATTEMPTS - 1:
@@ -201,7 +203,7 @@ def perform_prowler_scan(
# Update tags
tags = []
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
for key, value in finding.resource_tags.items():
tag_key = (key, value)
if tag_key not in tag_cache:
@@ -217,7 +219,7 @@ def perform_prowler_scan(
unique_resources.add((resource_instance.uid, resource_instance.region))
# Process finding
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
finding_uid = finding.uid
if finding_uid not in last_status_cache:
most_recent_finding = (
@@ -255,7 +257,7 @@ def perform_prowler_scan(
finding_instance.add_resources([resource_instance])
# Update compliance data if applicable
if finding.status.value == "MUTED":
if not generate_compliance or finding.status.value == "MUTED":
continue
region_dict = check_status_by_region.setdefault(finding.region, {})
@@ -265,7 +267,7 @@ def perform_prowler_scan(
region_dict[finding.check_id] = finding.status.value
# Update scan progress
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
scan_instance.progress = progress
scan_instance.save()
@@ -277,13 +279,13 @@ def perform_prowler_scan(
scan_instance.state = StateChoices.FAILED
finally:
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
scan_instance.duration = time.time() - start_time
scan_instance.completed_at = datetime.now(tz=timezone.utc)
scan_instance.unique_resource_count = len(unique_resources)
scan_instance.save()
if exception is None:
if exception is None and generate_compliance:
try:
regions = prowler_provider.get_regions()
except AttributeError:
@@ -328,7 +330,7 @@ def perform_prowler_scan(
total_requirements=compliance["total_requirements"],
)
)
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
if exception is not None:
@@ -366,7 +368,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
- muted_new: Muted findings with a delta of 'new'.
- muted_changed: Muted findings with a delta of 'changed'.
"""
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
findings = Finding.objects.filter(scan_id=scan_id)
aggregation = findings.values(
@@ -462,7 +464,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
),
)
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
scan_aggregations = {
ScanSummary(
tenant_id=tenant_id,
+2 -2
View File
@@ -7,7 +7,7 @@ from tasks.jobs.connection import check_provider_connection
from tasks.jobs.deletion import delete_provider, delete_tenant
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
from api.db_utils import rls_transaction
from api.db_utils import tenant_transaction
from api.decorators import set_tenant
from api.models import Provider, Scan
@@ -99,7 +99,7 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
"""
task_id = self.request.id
with rls_transaction(tenant_id):
with tenant_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
periodic_task_instance = PeriodicTask.objects.get(
name=f"scan-perform-scheduled-{provider_id}"
@@ -1,3 +1,5 @@
from unittest.mock import patch
import pytest
from django.core.exceptions import ObjectDoesNotExist
from tasks.jobs.deletion import delete_provider, delete_tenant
@@ -22,6 +24,7 @@ class TestDeleteProvider:
delete_provider(non_existent_pk)
@patch("api.db_router.MainRouter.admin_db", new="default")
@pytest.mark.django_db
class TestDeleteTenant:
def test_delete_tenant_success(self, tenants_fixture, providers_fixture):
+12 -13
View File
@@ -1,4 +1,3 @@
import uuid
from unittest.mock import MagicMock, patch
import pytest
@@ -27,7 +26,7 @@ class TestPerformScan:
providers_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("api.db_utils.tenant_transaction"),
patch(
"tasks.jobs.scan.initialize_prowler_provider"
) as mock_initialize_prowler_provider,
@@ -166,10 +165,10 @@ class TestPerformScan:
"tasks.jobs.scan.initialize_prowler_provider",
side_effect=Exception("Connection error"),
)
@patch("api.db_utils.rls_transaction")
@patch("api.db_utils.tenant_transaction")
def test_perform_prowler_scan_no_connection(
self,
mock_rls_transaction,
mock_tenant_transaction,
mock_initialize_prowler_provider,
mock_prowler_scan_class,
tenants_fixture,
@@ -206,14 +205,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.rls_transaction")
@patch("api.db_utils.tenant_transaction")
def test_store_resources_new_resource(
self,
mock_rls_transaction,
mock_tenant_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = uuid.uuid4()
tenant_id = "tenant123"
provider_instance = MagicMock()
provider_instance.id = "provider456"
@@ -254,14 +253,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.rls_transaction")
@patch("api.db_utils.tenant_transaction")
def test_store_resources_existing_resource(
self,
mock_rls_transaction,
mock_tenant_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = uuid.uuid4()
tenant_id = "tenant123"
provider_instance = MagicMock()
provider_instance.id = "provider456"
@@ -311,14 +310,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.rls_transaction")
@patch("api.db_utils.tenant_transaction")
def test_store_resources_with_tags(
self,
mock_rls_transaction,
mock_tenant_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = uuid.uuid4()
tenant_id = "tenant123"
provider_instance = MagicMock()
provider_instance.id = "provider456"
-301
View File
@@ -1,301 +0,0 @@
# AWS SSO to Prowler Automation Script
## Table of Contents
- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Script Overview](#script-overview)
- [Usage](#usage)
- [Troubleshooting](#troubleshooting)
- [Customization](#customization)
- [Security Considerations](#security-considerations)
- [License](#license)
## Introduction
This repository provides a Bash script that automates the process of logging into AWS Single Sign-On (SSO), extracting temporary AWS credentials, and running **Prowler**—a security tool that performs AWS security best practices assessments—inside a Docker container using those credentials.
By following this guide, you can streamline your AWS security assessments, ensuring that you consistently apply best practices across your AWS accounts.
## Prerequisites
Before you begin, ensure that you have the following tools installed and properly configured on your system:
1. **AWS CLI v2**
- AWS SSO support is available from AWS CLI version 2 onwards.
- [Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
2. **jq**
- A lightweight and flexible command-line JSON processor.
- **macOS (Homebrew):**
```bash
brew install jq
```
- **Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install -y jq
```
- **Windows:**
- [Download jq](https://stedolan.github.io/jq/download/)
3. **Docker**
- Ensure Docker is installed and running on your system.
- [Docker Installation Guide](https://docs.docker.com/get-docker/)
4. **AWS SSO Profile Configuration**
- Ensure that you have configured an AWS CLI profile with SSO.
- [Configuring AWS CLI with SSO](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html)
## Setup
1. **Clone the Repository**
```bash
git clone https://github.com/your-username/aws-sso-prowler-automation.git
cd aws-sso-prowler-automation
```
2. **Create the Automation Script**
Create a new Bash script named `run_prowler_sso.sh` and make it executable.
```bash
nano run_prowler_sso.sh
chmod +x run_prowler_sso.sh
```
3. **Add the Script Content**
Paste the following content into `run_prowler_sso.sh`:
4. **Configure AWS SSO Profile**
Ensure that your AWS CLI profile (`twodragon` in this case) is correctly configured for SSO.
```bash
aws configure sso --profile twodragon
```
**Example Configuration Prompts:**
```
SSO session name (Recommended): [twodragon]
SSO start URL [None]: https://twodragon.awsapps.com/start
SSO region [None]: ap-northeast-2
SSO account ID [None]: 123456789012
SSO role name [None]: ReadOnlyAccess
CLI default client region [None]: ap-northeast-2
CLI default output format [None]: json
CLI profile name [twodragon]: twodragon
```
## Script Overview
The `run_prowler_sso.sh` script performs the following actions:
1. **AWS SSO Login:**
- Initiates AWS SSO login for the specified profile.
- Opens the SSO authorization page in the default browser for user authentication.
2. **Extract Temporary Credentials:**
- Locates the most recent SSO cache file containing the `accessToken`.
- Uses `jq` to parse and extract the `accessToken` from the cache file.
- Retrieves the `sso_role_name` and `sso_account_id` from the AWS CLI configuration.
- Obtains temporary AWS credentials (`AccessKeyId`, `SecretAccessKey`, `SessionToken`) using the extracted `accessToken`.
3. **Set Environment Variables:**
- Exports the extracted AWS credentials as environment variables to be used by the Docker container.
4. **Run Prowler:**
- Executes the **Prowler** Docker container, passing the AWS credentials as environment variables for security assessments.
## Usage
1. **Make the Script Executable**
Ensure the script has execute permissions.
```bash
chmod +x run_prowler_sso.sh
```
2. **Run the Script**
Execute the script to start the AWS SSO login process and run Prowler.
```bash
./run_prowler_sso.sh
```
3. **Follow the Prompts**
- A browser window will open prompting you to authenticate via AWS SSO.
- Complete the authentication process in the browser.
- Upon successful login, the script will extract temporary credentials and run Prowler.
4. **Review Prowler Output**
- Prowler will analyze your AWS environment based on the specified checks and output the results directly in the terminal.
## Troubleshooting
If you encounter issues during the script execution, follow these steps to diagnose and resolve them.
### 1. Verify AWS CLI Version
Ensure you are using AWS CLI version 2 or later.
```bash
aws --version
```
**Expected Output:**
```
aws-cli/2.11.10 Python/3.9.12 Darwin/20.3.0 exe/x86_64 prompt/off
```
If you are not using version 2, [install or update AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).
### 2. Confirm AWS SSO Profile Configuration
Check that the `twodragon` profile is correctly configured.
```bash
aws configure list-profiles
```
**Expected Output:**
```
default
twodragon
```
Review the profile details:
```bash
aws configure get sso_start_url --profile twodragon
aws configure get sso_region --profile twodragon
aws configure get sso_account_id --profile twodragon
aws configure get sso_role_name --profile twodragon
```
Ensure all fields return the correct values.
### 3. Check SSO Cache File
Ensure that the SSO cache file contains a valid `accessToken`.
```bash
cat ~/.aws/sso/cache/*.json
```
**Example Content:**
```json
{
"accessToken": "eyJz93a...k4laUWw",
"expiresAt": "2024-12-22T14:07:55Z",
"clientId": "example-client-id",
"clientSecret": "example-client-secret",
"startUrl": "https://twodragon.awsapps.com/start#"
}
```
If `accessToken` is `null` or missing, retry the AWS SSO login:
```bash
aws sso login --profile twodragon
```
### 4. Validate `jq` Installation
Ensure that `jq` is installed and functioning correctly.
```bash
jq --version
```
**Expected Output:**
```
jq-1.6
```
If `jq` is not installed, install it using the instructions in the [Prerequisites](#prerequisites) section.
### 5. Test Docker Environment Variables
Verify that the Docker container receives the AWS credentials correctly.
```bash
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
toniblyx/prowler /bin/bash -c 'echo $AWS_ACCESS_KEY_ID; echo $AWS_SECRET_ACCESS_KEY; echo $AWS_SESSION_TOKEN'
```
**Expected Output:**
```
ASIA...
wJalrFEMI/K7MDENG/bPxRfiCY...
IQoJb3JpZ2luX2VjEHwaCXVz...
```
Ensure that none of the environment variables are empty.
### 6. Review Script Output
Run the script with debugging enabled to get detailed output.
1. **Enable Debugging in Script**
Add `set -x` for verbose output.
```bash
#!/bin/bash
set -e
set -x
# ... rest of the script ...
```
2. **Run the Script**
```bash
./run_prowler_sso.sh
```
3. **Analyze Output**
Look for any errors or unexpected values in the output to identify where the script is failing.
## Customization
You can modify the script to suit your specific needs, such as:
- **Changing the AWS Profile Name:**
Update the `PROFILE` variable at the top of the script.
```bash
PROFILE="your-profile-name"
```
- **Adding Prowler Options:**
Pass additional options to Prowler for customized checks or output formats.
```bash
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
toniblyx/prowler -c check123 -M json
```
## Security Considerations
- **Handle Credentials Securely:**
- Avoid sharing or exposing your AWS credentials.
- Do not include sensitive information in logs or version control.
- **Script Permissions:**
- Ensure the script file has appropriate permissions to prevent unauthorized access.
```bash
chmod 700 run_prowler_sso.sh
```
- **Environment Variables:**
- Be cautious when exporting credentials as environment variables.
- Consider using more secure methods for credential management if necessary.
## License
This project is licensed under the [MIT License](LICENSE).
@@ -1,136 +0,0 @@
#!/bin/bash
set -e
# Set the profile name
PROFILE="twodragon"
# Set the Prowler output directory
OUTPUT_DIR=~/prowler-output
mkdir -p "$OUTPUT_DIR"
# Set the port for the local web server
WEB_SERVER_PORT=8000
# ----------------------------------------------
# Functions
# ----------------------------------------------
# Function to open the HTML report in the default browser
open_report() {
local report_path="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
open "$report_path"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
xdg-open "$report_path"
elif [[ "$OSTYPE" == "msys" ]]; then
start "" "$report_path"
else
echo "Automatic method to open Prowler HTML report is not supported on this OS."
echo "Please open the report manually at: $report_path"
fi
}
# Function to start a simple HTTP server to host the Prowler reports
start_web_server() {
local directory="$1"
local port="$2"
echo "Starting local web server to host Prowler reports at http://localhost:$port"
echo "Press Ctrl+C to stop the web server."
# Change to the output directory
cd "$directory"
# Start the HTTP server in the foreground
# Python 3 is required
python3 -m http.server "$port"
}
# ----------------------------------------------
# Main Script
# ----------------------------------------------
# AWS SSO Login
echo "Logging into AWS SSO..."
aws sso login --profile "$PROFILE"
# Extract temporary credentials
echo "Extracting temporary credentials..."
# Find the most recently modified SSO cache file
CACHE_FILE=$(ls -t ~/.aws/sso/cache/*.json 2>/dev/null | head -n 1)
echo "Cache File: $CACHE_FILE"
if [ -z "$CACHE_FILE" ]; then
echo "SSO cache file not found. Please ensure AWS SSO login was successful."
exit 1
fi
# Extract accessToken using jq
ACCESS_TOKEN=$(jq -r '.accessToken' "$CACHE_FILE")
echo "Access Token: $ACCESS_TOKEN"
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then
echo "Unable to extract accessToken. Please check your SSO login and cache file."
exit 1
fi
# Extract role name and account ID from AWS CLI configuration
ROLE_NAME=$(aws configure get sso_role_name --profile "$PROFILE")
ACCOUNT_ID=$(aws configure get sso_account_id --profile "$PROFILE")
echo "Role Name: $ROLE_NAME"
echo "Account ID: $ACCOUNT_ID"
if [ -z "$ROLE_NAME" ] || [ -z "$ACCOUNT_ID" ]; then
echo "Unable to extract sso_role_name or sso_account_id. Please check your profile configuration."
exit 1
fi
# Obtain temporary credentials using AWS SSO
TEMP_CREDS=$(aws sso get-role-credentials \
--role-name "$ROLE_NAME" \
--account-id "$ACCOUNT_ID" \
--access-token "$ACCESS_TOKEN" \
--profile "$PROFILE")
echo "TEMP_CREDS: $TEMP_CREDS"
# Extract credentials from the JSON response
AWS_ACCESS_KEY_ID=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.accessKeyId')
AWS_SECRET_ACCESS_KEY=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.secretAccessKey')
AWS_SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.sessionToken')
# Verify that all credentials were extracted successfully
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$AWS_SESSION_TOKEN" ]; then
echo "Unable to extract temporary credentials."
exit 1
fi
# Export AWS credentials as environment variables
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN
echo "AWS credentials have been set."
# Run Prowler in Docker container
echo "Running Prowler Docker container..."
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
-e AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \
-v "$OUTPUT_DIR":/home/prowler/output \
toniblyx/prowler -M html -M csv -M json-ocsf --output-directory /home/prowler/output --output-filename prowler-output
echo "Prowler has finished running. Reports are saved in $OUTPUT_DIR."
# Open the HTML report in the default browser
REPORT_PATH="$OUTPUT_DIR/prowler-output.html"
echo "Opening Prowler HTML report..."
open_report "$REPORT_PATH" &
# Start the local web server to host the Prowler dashboard
# This will run in the foreground. To run it in the background, append an ampersand (&) at the end of the command.
start_web_server "$OUTPUT_DIR" "$WEB_SERVER_PORT"
-24
View File
@@ -1,24 +0,0 @@
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"
)
+4 -4
View File
@@ -1,7 +1,7 @@
services:
api:
hostname: "prowler-api"
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
env_file:
- path: .env
required: false
@@ -17,7 +17,7 @@ services:
- "prod"
ui:
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-stable}
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-latest}
env_file:
- path: .env
required: false
@@ -61,7 +61,7 @@ services:
retries: 3
worker:
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
env_file:
- path: .env
required: false
@@ -75,7 +75,7 @@ services:
- "worker"
worker-beat:
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
env_file:
- path: ./.env
required: false
-3
View File
@@ -279,9 +279,6 @@ Each Prowler check has metadata associated which is stored at the same level of
"Severity": "critical",
# ResourceType only for AWS, holds the type from here
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html
# In case of not existing, use CloudFormation type but removing the "::" and using capital letters only at the beginning of each word. Example: "AWS::EC2::Instance" -> "AwsEc2Instance"
# CloudFormation type reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
# If the resource type does not exist in the CloudFormation types, use "Other".
"ResourceType": "Other",
# Description holds the title of the check, for now is the same as CheckTitle
"Description": "Ensure there are no EC2 AMIs set as Public.",
+2 -335
View File
@@ -1,336 +1,3 @@
# Creating a New Integration
# Create a new integration
## Introduction
Integrating Prowler with external tools enhances its functionality and seamlessly embeds it into your workflows. Prowler supports a wide range of integrations to streamline security assessments and reporting. Common integration targets include messaging platforms like Slack, project management tools like Jira, and cloud services such as AWS Security Hub.
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can integrate it with the desired product!
* Identify the best approach for the specific platform you’re targeting.
## Steps to Create an Integration
### Identify the Integration Purpose
* Clearly define the objective of the integration. For example:
* Sending Prowler findings to a platform for alerts, tracking, or further analysis.
* Review existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder for inspiration and implementation examples.
### Develop the Integration
* Script Development:
* Write a script to process Prowler’s output and interact with the target platform’s API.
* For example, to send findings, parse Prowler’s results and use the platform’s API to create entries or notifications.
* Configuration:
* Ensure your script includes configurable options for environment-specific settings, such as API endpoints and authentication tokens.
### Fundamental Structure
* Integration Class:
* Create a class that encapsulates attributes and methods for the integration.
Here is an example with Jira integration:
```python title="Jira Class"
class Jira:
"""
Jira class to interact with the Jira API
[Note]
This integration is limited to a single Jira Cloud, therefore all the issues will be created for same Jira Cloud ID. We will need to work on the ability of providing a Jira Cloud ID if the user is present in more than one.
Attributes:
- _redirect_uri: The redirect URI
- _client_id: The client ID
- _client_secret: The client secret
- _access_token: The access token
- _refresh_token: The refresh token
- _expiration_date: The authentication expiration
- _cloud_id: The cloud ID
- _scopes: The scopes needed to authenticate, read:jira-user read:jira-work write:jira-work
- AUTH_URL: The URL to authenticate with Jira
- PARAMS_TEMPLATE: The template for the parameters to authenticate with Jira
- TOKEN_URL: The URL to get the access token from Jira
- API_TOKEN_URL: The URL to get the accessible resources from Jira
Methods:
- __init__: Initialize the Jira object
- input_authorization_code: Input the authorization code
- auth_code_url: Generate the URL to authorize the application
- get_auth: Get the access token and refresh token
- get_cloud_id: Get the cloud ID from Jira
- get_access_token: Get the access token
- refresh_access_token: Refresh the access token from Jira
- test_connection: Test the connection to Jira and return a Connection object
- get_projects: Get the projects from Jira
- get_available_issue_types: Get the available issue types for a project
- send_findings: Send the findings to Jira and create an issue
Raises:
- JiraGetAuthResponseError: Failed to get the access token and refresh token
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
- JiraAuthenticationError: Failed to authenticate
- JiraRefreshTokenError: Failed to refresh the access token
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
- JiraGetAccessTokenError: Failed to get the access token
- JiraNoProjectsError: No projects found in Jira
- JiraGetProjectsError: Failed to get projects from Jira
- JiraGetProjectsResponseError: Failed to get projects from Jira, response code did not match 200
- JiraInvalidIssueTypeError: The issue type is invalid
- JiraGetAvailableIssueTypesError: Failed to get available issue types from Jira
- JiraGetAvailableIssueTypesResponseError: Failed to get available issue types from Jira, response code did not match 200
- JiraCreateIssueError: Failed to create an issue in Jira
- JiraSendFindingsResponseError: Failed to send the findings to Jira
- JiraTestConnectionError: Failed to test the connection
Usage:
jira = Jira(
redirect_uri="http://localhost:8080",
client_id="client_id",
client_secret="client_secret
)
jira.send_findings(findings=findings, project_key="KEY")
"""
_redirect_uri: str = None
_client_id: str = None
_client_secret: str = None
_access_token: str = None
_refresh_token: str = None
_expiration_date: int = None
_cloud_id: str = None
_scopes: list[str] = None
AUTH_URL = "https://auth.atlassian.com/authorize"
PARAMS_TEMPLATE = {
"audience": "api.atlassian.com",
"client_id": None,
"scope": None,
"redirect_uri": None,
"state": None,
"response_type": "code",
"prompt": "consent",
}
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
def __init__(
self,
redirect_uri: str = None,
client_id: str = None,
client_secret: str = None,
):
self._redirect_uri = redirect_uri
self._client_id = client_id
self._client_secret = client_secret
self._scopes = ["read:jira-user", "read:jira-work", "write:jira-work"]
auth_url = self.auth_code_url()
authorization_code = self.input_authorization_code(auth_url)
self.get_auth(authorization_code)
# More properties and methods
```
* Test Connection Method:
* Implement a method to validate credentials or tokens, ensuring the connection to the target platform is successful.
The following is the code for the `test_connection` method for the `Jira` class:
```python title="Test connection"
@staticmethod
def test_connection(
redirect_uri: str = None,
client_id: str = None,
client_secret: str = None,
raise_on_exception: bool = True,
) -> Connection:
"""Test the connection to Jira
Args:
- redirect_uri: The redirect URI
- client_id: The client ID
- client_secret: The client secret
- raise_on_exception: Whether to raise an exception or not
Returns:
- Connection: The connection object
Raises:
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
- JiraAuthenticationError: Failed to authenticate
- JiraTestConnectionError: Failed to test the connection
"""
try:
jira = Jira(
redirect_uri=redirect_uri,
client_id=client_id,
client_secret=client_secret,
)
access_token = jira.get_access_token()
if not access_token:
return ValueError("Failed to get access token")
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(
f"https://api.atlassian.com/ex/jira/{jira.cloud_id}/rest/api/3/myself",
headers=headers,
)
if response.status_code == 200:
return Connection(is_connected=True)
else:
return Connection(is_connected=False, error=response.json())
except JiraGetCloudIDNoResourcesError as no_resources_error:
logger.error(
f"{no_resources_error.__class__.__name__}[{no_resources_error.__traceback__.tb_lineno}]: {no_resources_error}"
)
if raise_on_exception:
raise no_resources_error
return Connection(error=no_resources_error)
except JiraGetCloudIDResponseError as response_error:
logger.error(
f"{response_error.__class__.__name__}[{response_error.__traceback__.tb_lineno}]: {response_error}"
)
if raise_on_exception:
raise response_error
return Connection(error=response_error)
except JiraGetCloudIDError as cloud_id_error:
logger.error(
f"{cloud_id_error.__class__.__name__}[{cloud_id_error.__traceback__.tb_lineno}]: {cloud_id_error}"
)
if raise_on_exception:
raise cloud_id_error
return Connection(error=cloud_id_error)
except JiraAuthenticationError as auth_error:
logger.error(
f"{auth_error.__class__.__name__}[{auth_error.__traceback__.tb_lineno}]: {auth_error}"
)
if raise_on_exception:
raise auth_error
return Connection(error=auth_error)
except Exception as error:
logger.error(f"Failed to test connection: {error}")
if raise_on_exception:
raise JiraTestConnectionError(
message="Failed to test connection on the Jira integration",
file=os.path.basename(__file__),
)
return Connection(is_connected=False, error=error)
```
* Send Findings Method:
* Add a method to send Prowler findings to the target platform, adhering to its API specifications.
The following is the code for the `send_findings` method for the `Jira` class:
```python title="Send findings method"
def send_findings(
self,
findings: list[Finding] = None,
project_key: str = None,
issue_type: str = None,
):
"""
Send the findings to Jira
Args:
- findings: The findings to send
- project_key: The project key
- issue_type: The issue type
Raises:
- JiraRefreshTokenError: Failed to refresh the access token
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
- JiraCreateIssueError: Failed to create an issue in Jira
- JiraSendFindingsResponseError: Failed to send the findings to Jira
"""
try:
access_token = self.get_access_token()
if not access_token:
raise JiraNoTokenError(
message="No token was found",
file=os.path.basename(__file__),
)
projects = self.get_projects()
if project_key not in projects:
logger.error("The project key is invalid")
raise JiraInvalidProjectKeyError(
message="The project key is invalid",
file=os.path.basename(__file__),
)
available_issue_types = self.get_available_issue_types(project_key)
if issue_type not in available_issue_types:
logger.error("The issue type is invalid")
raise JiraInvalidIssueTypeError(
message="The issue type is invalid", file=os.path.basename(__file__)
)
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
for finding in findings:
status_color = self.get_color_from_status(finding.status.value)
adf_description = self.get_adf_description(
check_id=finding.metadata.CheckID,
check_title=finding.metadata.CheckTitle,
severity=finding.metadata.Severity.value.upper(),
status=finding.status.value,
status_color=status_color,
status_extended=finding.status_extended,
provider=finding.metadata.Provider,
region=finding.region,
resource_uid=finding.resource_uid,
resource_name=finding.resource_name,
risk=finding.metadata.Risk,
recommendation_text=finding.metadata.Remediation.Recommendation.Text,
recommendation_url=finding.metadata.Remediation.Recommendation.Url,
)
payload = {
"fields": {
"project": {"key": project_key},
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
"description": adf_description,
"issuetype": {"name": issue_type},
}
}
response = requests.post(
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
json=payload,
headers=headers,
)
if response.status_code != 201:
response_error = f"Failed to send finding: {response.status_code} - {response.json()}"
logger.warning(response_error)
raise JiraSendFindingsResponseError(
message=response_error, file=os.path.basename(__file__)
)
else:
logger.info(f"Finding sent successfully: {response.json()}")
except JiraRefreshTokenError as refresh_error:
raise refresh_error
except JiraRefreshTokenResponseError as response_error:
raise response_error
except Exception as e:
logger.error(f"Failed to send findings: {e}")
raise JiraCreateIssueError(
message="Failed to create an issue in Jira",
file=os.path.basename(__file__),
)
```
### Testing
* Test the integration in a controlled environment to confirm it behaves as expected.
* Verify that Prowler’s findings are accurately transmitted and correctly processed by the target platform.
* Simulate edge cases to ensure robust error handling.
### Documentation
* Provide clear, detailed documentation for your integration:
* Setup instructions, including any required dependencies.
* Configuration details, such as environment variables or authentication steps.
* Example use cases and troubleshooting tips.
* Good documentation ensures maintainability and simplifies onboarding for team members.
Coming soon ...
+2 -165
View File
@@ -1,166 +1,3 @@
# Create a Custom Output Format
# Create a custom output format
## Introduction
Prowler can generate outputs in multiple formats, allowing users to customize the way findings are presented. This is particularly useful when integrating Prowler with third-party tools, creating specialized reports, or simply tailoring the data to meet specific requirements. A custom output format gives you the flexibility to extract and display only the most relevant information in the way you need it.
* Prowler organizes its outputs in the `/lib/outputs` directory. Each format (e.g., JSON, CSV, HTML) is implemented as a Python class.
* Outputs are generated based on findings collected during a scan. Each finding is represented as a structured dictionary containing details like resource IDs, severities, descriptions, and more.
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can create it with the desired output!
* Identify the best approach for the specific output you’re targeting.
## Steps to Create a Custom Output Format
### Schema
* Output Class:
* The class must inherit from `Output`. Review the [Output Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/output.py).
* Create a class that encapsulates attributes and methods for the output.
The following is the code for the `CSV` class:
```python title="CSV Class"
class CSV(Output):
def transform(self, findings: List[Finding]) -> None:
"""Transforms the findings into the CSV format.
Args:
findings (list[Finding]): a list of Finding objects
"""
...
```
* Transform Method:
* This method will transform the findings provided by Prowler to a specific format.
The following is the code for the `transform` method for the `CSV` class:
```python title="Transform"
def transform(self, findings: List[Finding]) -> None:
"""Transforms the findings into the CSV format.
Args:
findings (list[Finding]): a list of Finding objects
"""
try:
for finding in findings:
finding_dict = {}
finding_dict["AUTH_METHOD"] = finding.auth_method
finding_dict["TIMESTAMP"] = finding.timestamp
finding_dict["ACCOUNT_UID"] = finding.account_uid
finding_dict["ACCOUNT_NAME"] = finding.account_name
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
finding.account_organization_uid
)
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
finding.account_organization_name
)
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
finding.account_tags, separator=":"
)
finding_dict["FINDING_UID"] = finding.uid
finding_dict["PROVIDER"] = finding.metadata.Provider
finding_dict["CHECK_ID"] = finding.metadata.CheckID
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
finding_dict["STATUS"] = finding.status.value
finding_dict["STATUS_EXTENDED"] = finding.status_extended
finding_dict["MUTED"] = finding.muted
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
finding_dict["SEVERITY"] = finding.metadata.Severity.value
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
finding_dict["RESOURCE_UID"] = finding.resource_uid
finding_dict["RESOURCE_NAME"] = finding.resource_name
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
finding_dict["PARTITION"] = finding.partition
finding_dict["REGION"] = finding.region
finding_dict["DESCRIPTION"] = finding.metadata.Description
finding_dict["RISK"] = finding.metadata.Risk
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
finding.metadata.Remediation.Recommendation.Text
)
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
finding.metadata.Remediation.Recommendation.Url
)
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
finding.metadata.Remediation.Code.NativeIaC
)
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
finding.metadata.Remediation.Code.Terraform
)
finding_dict["REMEDIATION_CODE_CLI"] = (
finding.metadata.Remediation.Code.CLI
)
finding_dict["REMEDIATION_CODE_OTHER"] = (
finding.metadata.Remediation.Code.Other
)
finding_dict["COMPLIANCE"] = unroll_dict(
finding.compliance, separator=": "
)
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
finding_dict["NOTES"] = finding.metadata.Notes
finding_dict["PROWLER_VERSION"] = finding.prowler_version
self._data.append(finding_dict)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
* Batch Write Data To File Method:
* This method will write the modeled object to a file.
The following is the code for the `batch_write_data_to_file` method for the `CSV` class:
```python title="Batch Write Data To File"
def batch_write_data_to_file(self) -> None:
"""Writes the findings to a file using the CSV format using the `Output._file_descriptor`."""
try:
if (
getattr(self, "_file_descriptor", None)
and not self._file_descriptor.closed
and self._data
):
csv_writer = DictWriter(
self._file_descriptor,
fieldnames=self._data[0].keys(),
delimiter=";",
)
csv_writer.writeheader()
for finding in self._data:
csv_writer.writerow(finding)
self._file_descriptor.close()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
### Integration With The Current Code
Once that the desired output format is created it has to be integrated with Prowler. Take a look at the the usage from the current supported output in order to add the new one.
Here is an example of the CSV output creation inside [prowler code](https://github.com/prowler-cloud/prowler/blob/master/prowler/__main__.py):
```python title="CSV creation"
if mode == "csv":
csv_output = CSV(
findings=finding_outputs,
create_file_descriptor=True,
file_path=f"{filename}{csv_file_suffix}",
)
generated_outputs["regular"].append(csv_output)
# Write CSV Finding Object to file
csv_output.batch_write_data_to_file()
```
### Testing
* Verify that Prowler’s findings are accurately writed in the desired output format.
* Simulate edge cases to ensure robust error handling.
### Documentation
* Provide clear, detailed documentation for your output:
* Setup instructions, including any required dependencies.
* Configuration details.
* Example use cases and troubleshooting tips.
* Good documentation ensures maintainability and simplifies onboarding for new users.
Coming soon ...
+15 -20
View File
@@ -38,19 +38,16 @@ If your IAM entity enforces MFA you can use `--mfa` and Prowler will ask you to
## Azure
Prowler for Azure supports the following authentication types. To use each one you need to pass the proper flag to the execution:
Prowler for Azure supports the following authentication types:
- [Service principal application](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (recommended).
- Current az cli credentials stored.
- Interactive browser authentication.
- [Managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication.
- [Service principal application](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) by environment variables (recommended)
- Current az cli credentials stored
- Interactive browser authentication
- [Managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication
???+ warning
For Prowler App only the Service Principal authentication method is supported.
### Service Principal authentication
### Service Principal Application authentication
To allow Prowler assume the service principal application identity to start the scan it is needed to configure the following environment variables:
To allow Prowler assume the service principal identity to start the scan it is needed to configure the following environment variables:
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
@@ -59,31 +56,29 @@ export AZURE_CLIENT_SECRET="XXXXXXX"
```
If you try to execute Prowler with the `--sp-env-auth` flag and those variables are empty or not exported, the execution is going to fail.
Follow the instructions in the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md#how-to-create-prowler-service-principal) section to create a service principal.
Follow the instructions in the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md) section to create a service principal.
### AZ CLI / Browser / Managed Identity authentication
The other three cases does not need additional configuration, `--az-cli-auth` and `--managed-identity-auth` are automated options. To use `--browser-auth` the user needs to authenticate against Azure using the default browser to start the scan, also `tenant-id` is required.
### Needed permissions
### Permissions
Prowler for Azure needs two types of permission scopes to be set:
To use each one you need to pass the proper flag to the execution. Prowler for Azure handles two types of permission scopes, which are:
- **Microsoft Entra ID permissions**: used to retrieve metadata from the identity assumed by Prowler and specific Entra checks (not mandatory to have access to execute the tool). The permissions required by the tool are the following:
- **Microsoft Entra ID permissions**: Used to retrieve metadata from the identity assumed by Prowler and specific Entra checks (not mandatory to have access to execute the tool). The permissions required by the tool are the following:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
- **Subscription scope permissions**: required to launch the checks against your resources, mandatory to launch the tool. It is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
- `UserAuthenticationMethod.Read.All`
- **Subscription scope permissions**: Required to launch the checks against your resources, mandatory to launch the tool. It is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
- `Reader`
- `ProwlerRole` (custom role with minimal permissions defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json))
???+ note
Please, notice that the field `assignableScopes` in the JSON custom role file must be changed to be the subscription or management group where the role is going to be assigned. The valid formats for the field are `/subscriptions/<subscription-id>` or `/providers/Microsoft.Management/managementGroups/<management-group-id>`.
- `ProwlerRole` (custom role defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json))
To assign the permissions, follow the instructions in the [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions) section and the [Azure subscriptions permissions](../tutorials/azure/subscriptions.md#assigning-proper-permissions) section, respectively.
#### Checks that require ProwlerRole
The following checks require the `ProwlerRole` permissions to be executed, if you want to run them, make sure you have assigned the role to the identity that is going to be assumed by Prowler:
The following checks require the `ProwlerRole` custom role to be executed, if you want to run them, make sure you have assigned the role to the identity that is going to be assumed by Prowler:
- `app_function_access_keys_configured`
- `app_function_ftps_deployment_disabled`
-8
View File
@@ -45,8 +45,6 @@ Prowler App can be installed in different ways, depending on your environment:
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.
???+ note
@@ -187,8 +185,6 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
* In the command below, change `-v` to your local directory path in order to access the reports.
* AWS, GCP, Azure and/or Kubernetes credentials
> 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.
_Commands_:
``` bash
@@ -319,14 +315,10 @@ The available versions of Prowler CLI are the following:
- `v3-stable`: this tag always point to the latest release for v3.
The container images are available here:
- Prowler CLI:
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
- Prowler App:
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
@@ -1,10 +1,6 @@
# How to create Prowler Service Principal Application
# How to create Prowler Service Principal
To allow Prowler assume an identity to start the scan with the required privileges is necesary to create a Service Principal. This Service Principal is going to be used to authenticate against Azure and retrieve the metadata needed to perform the checks.
To create a Service Principal Application you can use the Azure Portal or the Azure CLI.
## From Azure Portal
To allow Prowler assume an identity to start the scan with the required privileges is necesary to create a Service Principal. To create one follow the next steps:
1. Access to Microsoft Entra ID
2. In the left menu bar, go to "App registrations"
@@ -17,39 +13,9 @@ To create a Service Principal Application you can use the Azure Portal or the Az
![Register an Application page](../img/create-sp.gif)
## From Azure CLI
## Assigning the proper permissions
To create a Service Principal using the Azure CLI, follow the next steps:
1. Open a terminal and execute the following command to create a new Service Principal application:
```console
az ad sp create-for-rbac --name "ProwlerApp"
```
2. The output of the command is going to be similar to the following:
```json
{
"appId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"displayName": "ProwlerApp",
"password": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"tenant": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
```
3. Save the values of `appId`, `password` and `tenant` to be used as credentials in Prowler.
# Assigning the proper permissions
To allow Prowler to retrieve metadata from the identity assumed and run specific Entra checks, it is needed to assign the following permissions:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
To assign the permissions you can make it from the Azure Portal or using the Azure CLI.
???+ note
Once you have created and assigned the proper Entra permissions to the application, you can go to this [tutorial](../azure/subscriptions.md) to add the subscription permissions to the application and start scanning your resources.
## From Azure Portal
To allow Prowler to retrieve metadata from the identity assumed and specific Entra checks, it is needed to assign the following permissions:
1. Access to Microsoft Entra ID
2. In the left menu bar, go to "App registrations"
@@ -62,18 +28,7 @@ To assign the permissions you can make it from the Azure Portal or using the Azu
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All`
8. Click on "Add permissions" to apply the new permissions.
9. Finally, an admin should click on "Grant admin consent for [your tenant]" to apply the permissions.
9. Finally, click on "Grant admin consent for [your tenant]" to apply the permissions.
![EntraID Permissions](../../img/AAD-permissions.png)
## From Azure CLI
1. Open a terminal and execute the following command to assign the permissions to the Service Principal:
```console
az ad app permission add --id {appId} --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role 246dd0d5-5bd0-4def-940b-0421030a5b68=Role 38d9df27-64da-44fd-b7c5-a6fbac20248f=Role
```
2. The admin consent is needed to apply the permissions, an admin should execute the following command:
```console
az ad app permission admin-consent --id {appId}
```
+21 -112
View File
@@ -1,8 +1,6 @@
# Azure subscriptions scope
The main target for performing the scans in Azure is the subscription scope. Prowler needs to have the proper permissions to access the subscription and retrieve the metadata needed to perform the checks.
By default, Prowler is multi-subscription, which means that is going to scan all the subscriptions is able to list. If you only assign permissions to one subscription, it is going to scan a single one.
By default, Prowler is multisubscription, which means that is going to scan all the subscriptions is able to list. If you only assign permissions to one subscription, it is going to scan a single one.
Prowler also has the ability to limit the subscriptions to scan to a set passed as input argument, to do so:
```console
@@ -11,124 +9,35 @@ prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription
Where you can pass from 1 up to N subscriptions to be scanned.
???+ warning
The multi-subscription feature is only available for the CLI, in the case of Prowler App is only possible to scan one subscription per scan.
## Assigning proper permissions
## Assign the appropriate permissions to the identity that is going to be assumed by Prowler
Regarding the subscription scope, Prowler by default scans all subscriptions that it is able to list, so it is necessary to add the `Reader` RBAC built-in roles per subscription or management group (recommended for multiple subscriptions, see it in the [next section](#recommendation-for-multiple-subscriptions)) to the entity that will be adopted by the tool:
To assign this roles, follow the instructions:
Regarding the subscription scope, Prowler, by default, scans all subscriptions it can access. Therefore, it is necessary to add a `Reader` role assignment for each subscription you want to audit. To make it easier and less repetitive to assign roles in environments with multiple subscriptions check the [following section](#recommendation-for-multiple-subscriptions).
### From Azure Portal
1. Access to the subscription you want to scan with Prowler.
2. Select "Access control (IAM)" in the left menu.
3. Click on "+ Add" and select "Add role assignment".
4. In the search bar, type `Reader`, select it and click on "Next".
5. In the Members tab, click on "+ Select members" and add the members you want to assign this role.
6. Click on "Review + assign" to apply the new role.
1. Access your subscription, then select your subscription.
2. Select "Access control (IAM)".
3. In the overview, select "Roles".
4. Click on "+ Add" and select "Add role assignment".
5. In the search bar, type `Reader`, select it and click on "Next".
6. In the Members tab, click on "+ Select members" and add the members you want to assign this role.
7. Click on "Review + assign" to apply the new role.
![Add reader role to subscription](../../img/add-reader-role.gif)
### From Azure CLI
1. Open a terminal and execute the following command to assign the `Reader` role to the identity that is going to be assumed by Prowler:
```console
az role assignment create --role "Reader" --assignee <user, group, or service principal> --scope /subscriptions/<subscription-id>
```
2. If the command is executed successfully, the output is going to be similar to the following:
```json
{
"condition": null,
"conditionVersion": null,
"createdBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"createdOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00",
"delegatedManagedIdentityResourceId": null,
"description": null,
"id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleAssignments/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"name": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"principalId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"principalName": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"principalType": "ServicePrincipal",
"roleDefinitionId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleDefinitions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"roleDefinitionName": "Reader",
"scope": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"type": "Microsoft.Authorization/roleAssignments",
"updatedBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"updatedOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00"
}
```
### Prowler Custom Role
Moreover, some additional read-only permissions not included in the built-in reader role are needed for some checks, for this kind of checks we use a custom role. This role is defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once the custom role is created you can assign it in the same way as the `Reader` role.
The checks that needs the `ProwlerRole` can be consulted in the [requirements section](../../getting-started/requirements.md#checks-that-require-prowlerrole).
#### Create ProwlerRole from Azure Portal
1. Download the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json) file and modify the `assignableScopes` field to be the subscription ID where the role assignment is going to be made, it should be shomething like `/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`.
2. Access your subscription.
3. Select "Access control (IAM)".
4. Click on "+ Add" and select "Add custom role".
5. In the "Baseline permissions" select "Start from JSON" and upload the file downloaded and modified in the step 1.
7. Click on "Review + create" to create the new role.
#### Create ProwlerRole from Azure CLI
1. Open a terminal and execute the following command to create a new custom role:
```console
az role definition create --role-definition '{ 640ms  lun 16 dic 17:04:17 2024
"Name": "ProwlerRole",
"IsCustom": true,
"Description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
"AssignableScopes": [
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" // USE YOUR SUBSCRIPTION ID
],
"Actions": [
"Microsoft.Web/sites/host/listkeys/action",
"Microsoft.Web/sites/config/list/Action"
]
}'
```
3. If the command is executed successfully, the output is going to be similar to the following:
```json
{
"assignableScopes": [
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
],
"createdBy": null,
"createdOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00",
"description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
"id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleDefinitions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"name": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"permissions": [
{
"actions": [
"Microsoft.Web/sites/host/listkeys/action",
"Microsoft.Web/sites/config/list/Action"
],
"condition": null,
"conditionVersion": null,
"dataActions": [],
"notActions": [],
"notDataActions": []
}
],
"roleName": "ProwlerRole",
"roleType": "CustomRole",
"type": "Microsoft.Authorization/roleDefinitions",
"updatedBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"updatedOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00"
}
```
Moreover, some additional read-only permissions are needed for some checks, for this kind of checks that are not covered by built-in roles we use a custom role. This role is defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once the cusotm role is created, repeat the steps mentioned above to assign the new `ProwlerRole` to an identity.
## Recommendation for multiple subscriptions
Scanning multiple subscriptions can be tedious due to the need to create and assign roles for each one. To simplify this process, we recommend using management groups to organize and audit subscriptions collectively with Prowler.
While scanning multiple subscriptions could be tedious to create and assign roles for each one. For this reason in Prowler we recommend the usage of *[management groups](https://learn.microsoft.com/en-us/azure/governance/management-groups/overview)* to group all subscriptions that are going to be audited by Prowler.
To do this in a proper way you have to [create a new management group](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) and add all roles in the same way that have been done for subscription scope.
1. **Create a Management Group**: Follow the [official guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) to create a new management group.
![Create management group](../../img/create-management-group.gif)
2. **Add all roles**: Assign roles at to the new management group like in the [past section](#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) but at the management group level instead of the subscription level.
3. **Add subscriptions**: Add all the subscriptions you want to audit to the management group.
Once the management group is properly set you can add all the subscription that you want to audit.
![Add subscription to management group](../../img/add-sub-to-management-group.gif)
???+ note
By default, `prowler` will scan all subscriptions in the Azure tenant, use the flag `--subscription-id` to specify the subscriptions to be scanned.
-5
View File
@@ -42,7 +42,6 @@ Mutelist:
Resources:
- "user-1" # Will mute user-1 in check iam_user_hardware_mfa_enabled
- "user-2" # Will mute user-2 in check iam_user_hardware_mfa_enabled
Description: "Findings related with the check iam_user_hardware_mfa_enabled will be muted for us-east-1 region and user-1, user-2 resources"
"ec2_*":
Regions:
- "*"
@@ -141,9 +140,6 @@ Mutelist:
| `resource` | The resource identifier. Use `*` to apply the mutelist to all resources. | `ANDed` |
| `tag` | The tag value. | `ORed` |
### Description
This field can be used to add information or some hints for the Mutelist rule.
## How to Use the Mutelist
@@ -175,7 +171,6 @@ If you want to mute failed findings only in specific regions, create a file with
- "ap-southeast-2"
Resources:
- "*"
Description: "Description related with the muted findings for the check"
### Default Mutelist
For the AWS Provider, Prowler is executed with a default AWS Mutelist with the AWS Resources that should be muted such as all resources created by AWS Control Tower when setting up a landing zone that can be found in [AWS Documentation](https://docs.aws.amazon.com/controltower/latest/userguide/shared-account-resources.html).
+2 -2
View File
@@ -45,7 +45,7 @@ Once you’ve selected a provider, you need to provide the Provider UID:
- **AWS**: Enter your AWS Account ID.
- **GCP**: Enter your GCP Project ID.
- **Azure**: Enter your Azure Subscription ID.
- **Kubernetes**: Enter your Kubernetes Cluster context of your kubeconfig file.
- **Kubernetes**: Enter your Kubernetes Cluster name.
Optionally, provide a **Provider Alias** for easier identification. Follow the instructions provided to add your credentials:
@@ -74,7 +74,7 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to
---
### **Step 4.2: Azure Credentials**
For Azure, Prowler App uses a service principal application to authenticate, for more information about the process of creating and adding permissions to a service principal check this [section](../getting-started/requirements.md#azure). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-the-proper-permissions) and [Subscription](./azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
For Azure, Prowler App uses a Service Principal to authenticate. See the steps in https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/azure/create-prowler-service-principal/ to create a Service Principal. Then, enter the `Tenant ID`, `Client ID` and `Client Secret` of the Service Principal.
<img src="../../img/azure-credentials.png" alt="Azure Credentials" width="700"/>
@@ -95,7 +95,6 @@ Resources:
- 'servicecatalog:List*'
- 'ssm:GetDocument'
- 'ssm-incidents:List*'
- 'states:ListTagsForResource'
- 'support:Describe*'
- 'tag:GetTagKeys'
- 'wellarchitected:List*'
@@ -45,7 +45,6 @@
"servicecatalog:List*",
"ssm:GetDocument",
"ssm-incidents:List*",
"states:ListTagsForResource",
"support:Describe*",
"tag:GetTagKeys",
"wellarchitected:List*"
+1 -1
View File
@@ -3,7 +3,7 @@
"roleName": "ProwlerRole",
"description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
"assignableScopes": [
"/{'subscriptions', 'providers/Microsoft.Management/managementGroups'}/{Your Subscription or Management Group ID}"
"/"
],
"permissions": [
{
Generated
+105 -105
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -198,13 +198,13 @@ trio = ["trio (>=0.26.1)"]
[[package]]
name = "astroid"
version = "3.3.8"
version = "3.3.5"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.9.0"
files = [
{file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"},
{file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
{file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"},
{file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"},
]
[package.dependencies]
@@ -399,13 +399,13 @@ isodate = ">=0.6.1,<1.0.0"
[[package]]
name = "azure-mgmt-compute"
version = "33.1.0"
version = "33.0.0"
description = "Microsoft Azure Compute Management Client Library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "azure_mgmt_compute-33.1.0-py3-none-any.whl", hash = "sha256:9b119a1b7f621aee951074ef110b16d545d7b45c9cfb303becf04210bd772c91"},
{file = "azure_mgmt_compute-33.1.0.tar.gz", hash = "sha256:f5a5e18a5a7a0354562bbfa589b5db4aaf1e8ac3a194a2a910db55900d2535e9"},
{file = "azure-mgmt-compute-33.0.0.tar.gz", hash = "sha256:a3cc0fe4f09c8e1d3523c1bfb92620dfe263a0a893b0ac13a33d7057e9ddddd2"},
{file = "azure_mgmt_compute-33.0.0-py3-none-any.whl", hash = "sha256:155f8d78a1fdedcea1725fd12b85b2d87fbcb6b53f8e77451c644f45701e3bcf"},
]
[package.dependencies]
@@ -513,13 +513,13 @@ isodate = ">=0.6.1,<1.0.0"
[[package]]
name = "azure-mgmt-network"
version = "28.1.0"
version = "28.0.0"
description = "Microsoft Azure Network Management Client Library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658"},
{file = "azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87"},
{file = "azure_mgmt_network-28.0.0-py3-none-any.whl", hash = "sha256:2ee23c1f2ba75752187bd7f4c3e94ad172282cbf8153694feadc7886ef88493c"},
{file = "azure_mgmt_network-28.0.0.tar.gz", hash = "sha256:40356d348ef4838324f19a41cd80340b4f8dd4ac2f0a18a4cbd5cc95ef2974f3"},
]
[package.dependencies]
@@ -775,17 +775,17 @@ files = [
[[package]]
name = "boto3"
version = "1.35.94"
version = "1.35.78"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "boto3-1.35.94-py3-none-any.whl", hash = "sha256:516c514fb447d6f216833d06a0781c003fcf43099a4ca2f5a363a8afe0942070"},
{file = "boto3-1.35.94.tar.gz", hash = "sha256:5aa606239f0fe0dca0506e0ad6bbe4c589048e7e6c2486cee5ec22b6aa7ec2f8"},
{file = "boto3-1.35.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"},
{file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"},
]
[package.dependencies]
botocore = ">=1.35.94,<1.36.0"
botocore = ">=1.35.78,<1.36.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.35.94"
version = "1.35.79"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
{file = "botocore-1.35.94-py3-none-any.whl", hash = "sha256:d784d944865d8279c79d2301fc09ac28b5221d4e7328fb4e23c642c253b9932c"},
{file = "botocore-1.35.94.tar.gz", hash = "sha256:2b3309b356541faa4d88bb957dcac1d8004aa44953c0b7d4521a6cc5d3d5d6ba"},
{file = "botocore-1.35.79-py3-none-any.whl", hash = "sha256:e6b10bb9a357e3f5ca2e60f6dd15a85d311b9a476eb21b3c0c2a3b364a2897c8"},
{file = "botocore-1.35.79.tar.gz", hash = "sha256:245bfdda1b1508539ddd1819c67a8a2cc81780adf0715d3de418d64c4247f346"},
]
[package.dependencies]
@@ -1099,73 +1099,73 @@ files = [
[[package]]
name = "coverage"
version = "7.6.10"
version = "7.6.9"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
{file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
{file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
{file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
{file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
{file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
{file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
{file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
{file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
{file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
{file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
{file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
{file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
{file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
{file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
{file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
{file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
{file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
{file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
{file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
{file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
{file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
{file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
{file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
{file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
{file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
{file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"},
{file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"},
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"},
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"},
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"},
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"},
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"},
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"},
{file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"},
{file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"},
{file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"},
{file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"},
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"},
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"},
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"},
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"},
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"},
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"},
{file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"},
{file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"},
{file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"},
{file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"},
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"},
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"},
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"},
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"},
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"},
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"},
{file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"},
{file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"},
{file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"},
{file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"},
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"},
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"},
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"},
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"},
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"},
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"},
{file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"},
{file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"},
{file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"},
{file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"},
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"},
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"},
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"},
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"},
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"},
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"},
{file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"},
{file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"},
{file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"},
{file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"},
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"},
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"},
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"},
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"},
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"},
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"},
{file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"},
{file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"},
{file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"},
{file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"},
]
[package.dependencies]
@@ -1719,13 +1719,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.158.0"
version = "2.155.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "google_api_python_client-2.158.0-py2.py3-none-any.whl", hash = "sha256:36f8c8d2e79e50f76790ca5946d2f3f8333e210dc8539a6c88e0742416474ad2"},
{file = "google_api_python_client-2.158.0.tar.gz", hash = "sha256:b6664597a9955e04977a62752e33fe44cb35c580e190c1cb08a041893172bd67"},
{file = "google_api_python_client-2.155.0-py2.py3-none-any.whl", hash = "sha256:83fe9b5aa4160899079d7c93a37be306546a17e6686e2549bcc9584f1a229747"},
{file = "google_api_python_client-2.155.0.tar.gz", hash = "sha256:25529f89f0d13abcf3c05c089c423fb2858ac16e0b3727543393468d0d7af67c"},
]
[package.dependencies]
@@ -2403,13 +2403,13 @@ files = [
[[package]]
name = "microsoft-kiota-abstractions"
version = "1.6.8"
version = "1.6.6"
description = "Core abstractions for kiota generated libraries in Python"
optional = false
python-versions = "<4.0,>=3.8"
files = [
{file = "microsoft_kiota_abstractions-1.6.8-py3-none-any.whl", hash = "sha256:12819dee24d5aaa31e99683d938f65e50cbc446de087df244cd26c3326ec4e15"},
{file = "microsoft_kiota_abstractions-1.6.8.tar.gz", hash = "sha256:7070affabfa7182841646a0c8491cbb240af366aff2b9132f0caa45c4837dd78"},
{file = "microsoft_kiota_abstractions-1.6.6-py3-none-any.whl", hash = "sha256:29071715baf0d604c381c5d17be47f35e6e63a441dcfb5e9141963406b469d50"},
{file = "microsoft_kiota_abstractions-1.6.6.tar.gz", hash = "sha256:2554495b00c9c25b43f6964a71b65c89a277bd6b50f4d0028a7febcec6c4fd67"},
]
[package.dependencies]
@@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd
[[package]]
name = "mkdocs-material"
version = "9.5.49"
version = "9.5.48"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"},
{file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"},
{file = "mkdocs_material-9.5.48-py3-none-any.whl", hash = "sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8"},
{file = "mkdocs_material-9.5.48.tar.gz", hash = "sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db"},
]
[package.dependencies]
@@ -2769,13 +2769,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
[[package]]
name = "msgraph-sdk"
version = "1.16.0"
version = "1.14.0"
description = "The Microsoft Graph Python SDK"
optional = false
python-versions = ">=3.8"
files = [
{file = "msgraph_sdk-1.16.0-py3-none-any.whl", hash = "sha256:1dd26ece74c43167818e2ff58b062180233ce7187ad2a061057af1195395c56c"},
{file = "msgraph_sdk-1.16.0.tar.gz", hash = "sha256:980d19617d8d8b20545ef77fa5629fef768ce4ea1f2d1a124c5a9dd88d77940c"},
{file = "msgraph_sdk-1.14.0-py3-none-any.whl", hash = "sha256:1a2f327dc8fbe5a5e6d0d84cf71d605e7b118b3066b1e16f011ccd8fd927bb03"},
{file = "msgraph_sdk-1.14.0.tar.gz", hash = "sha256:5bbda80941c5d1794682753b8b291bd2ebed719a43d6de949fd0cd613b6dfbbd"},
]
[package.dependencies]
@@ -3043,18 +3043,18 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "openapi-schema-validator"
version = "0.6.3"
version = "0.6.2"
description = "OpenAPI schema validation for Python"
optional = false
python-versions = "<4.0.0,>=3.8.0"
python-versions = ">=3.8.0,<4.0.0"
files = [
{file = "openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3"},
{file = "openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee"},
{file = "openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8"},
{file = "openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804"},
]
[package.dependencies]
jsonschema = ">=4.19.1,<5.0.0"
jsonschema-specifications = ">=2023.5.2"
jsonschema-specifications = ">=2023.5.2,<2024.0.0"
rfc3339-validator = "*"
[[package]]
@@ -3796,17 +3796,17 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pylint"
version = "3.3.3"
version = "3.3.2"
description = "python code static checker"
optional = false
python-versions = ">=3.9.0"
files = [
{file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"},
{file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"},
{file = "pylint-3.3.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a"},
{file = "pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01"},
]
[package.dependencies]
astroid = ">=3.3.8,<=3.4.0-dev0"
astroid = ">=3.3.5,<=3.4.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -4643,13 +4643,13 @@ files = [
[[package]]
name = "slack-sdk"
version = "3.34.0"
version = "3.33.5"
description = "The Slack API Platform SDK for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "slack_sdk-3.34.0-py2.py3-none-any.whl", hash = "sha256:c61f57f310d85be83466db5a98ab6ae3bb2e5587437b54fa0daa8fae6a0feffa"},
{file = "slack_sdk-3.34.0.tar.gz", hash = "sha256:ff61db7012160eed742285ea91f11c72b7a38a6500a7f6c5335662b4bc6b853d"},
{file = "slack_sdk-3.33.5-py2.py3-none-any.whl", hash = "sha256:b8cccadfa3d4005a5e6529f52000d25c583f46173fda8e9136fdd2bc58923ff6"},
{file = "slack_sdk-3.33.5.tar.gz", hash = "sha256:a5e74c00c99dc844ad93e501ab764a20d86fa8184bbc9432af217496f632c4ee"},
]
[package.extras]
@@ -5199,4 +5199,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.13"
content-hash = "08f577e54ba3af2dba39393521c3596c614cc6432278be4e0b001fa706c6dccc"
content-hash = "e00da6013a01923ac8e79017e7fdb221e09a3dcf581ad8d74e39550be64cc2f3"
File diff suppressed because it is too large. Load diff
-1
View File
@@ -10,7 +10,6 @@ Mutelist:
- "*"
Resources:
- "aws-controltower-NotificationForwarder"
Description: "Checks from AWS lambda functions muted by default"
"cloudformation_stack*":
Regions:
- "*"
-1
View File
@@ -14,7 +14,6 @@ Mutelist:
Resources:
- "user-1" # Will ignore user-1 in check iam_user_hardware_mfa_enabled
- "user-2" # Will ignore user-2 in check iam_user_hardware_mfa_enabled
Description: "Check iam_user_hardware_mfa_enabled muted for region us-east-1 and resources user-1, user-2"
"ec2_*":
Regions:
- "*"
@@ -15,7 +15,6 @@ Mutelist:
Resources:
- "sqlserver1" # Will ignore sqlserver1 in check sqlserver_tde_encryption_enabled located in westeurope
- "sqlserver2" # Will ignore sqlserver2 in check sqlserver_tde_encryption_enabled located in westeurope
Description: "Findings related with the check sqlserver_tde_encryption_enabled is muted for westeurope region and sqlserver1, sqlserver2 resources"
"defender_*":
Regions:
- "*"
+1 -1
View File
@@ -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.1.1"
prowler_version = "5.1.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"
+1 -7
View File
@@ -73,10 +73,6 @@ aws:
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
log_group_retention_days: 365
# AWS CloudFormation Configuration
# cloudformation_stack_cdktoolkit_bootstrap_version --> by default is 21
recommended_cdk_bootstrap_version: 21
# AWS AppStream Session Configuration
# aws.appstream_fleet_session_idle_disconnect_timeout
max_idle_disconnect_timeout_in_seconds: 600 # 10 Minutes
@@ -318,12 +314,10 @@ aws:
# AWS ACM Configuration
# aws.acm_certificates_expiration_check
days_to_expire_threshold: 7
# aws.acm_certificates_with_secure_key_algorithms
# aws.acm_certificates_rsa_key_length
insecure_key_algorithms:
[
"RSA-1024",
"P-192",
"SHA-1",
]
# AWS EKS Configuration
-1
View File
@@ -15,7 +15,6 @@ Mutelist:
Resources:
- "instance1" # Will ignore instance1 in check compute_instance_public_ip located in europe-southwest1
- "instance2" # Will ignore instance2 in check compute_instance_public_ip located in europe-southwest1
Description: "Findings related with the check compute_instance_public_ip will be muted for europe-southwest1 region and instance1, instance2 resources"
"iam_*":
Regions:
- "*"
@@ -15,7 +15,6 @@ Mutelist:
Resources:
- "prowler-pod1" # Will ignore prowler-pod1 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1
- "prowler-pod2" # Will ignore prowler-pod2 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1
Description: "Findings related with the check core_minimize_allowPrivilegeEscalation_containers will be muted for namespace1 region and prowler-pod1, prowler-pod2 resources"
"kubelet_*":
Regions:
- "*"
+7 -18
View File
@@ -405,18 +405,16 @@ class Check_Report:
status: str
status_extended: str
check_metadata: CheckMetadata
resource_metadata: dict
resource_details: str
resource_tags: list
muted: bool
def __init__(self, metadata, resource=None):
def __init__(self, metadata):
self.status = ""
self.check_metadata = CheckMetadata.parse_raw(metadata)
self.resource_metadata = resource.dict() if resource else {}
self.status_extended = ""
self.resource_details = ""
self.resource_tags = getattr(resource, "tags", []) if resource else []
self.resource_tags = []
self.muted = False
@@ -428,20 +426,11 @@ class Check_Report_AWS(Check_Report):
resource_arn: str
region: str
def __init__(self, metadata, resource_metadata=None):
super().__init__(metadata, resource_metadata)
if resource_metadata:
self.resource_id = (
getattr(resource_metadata, "id", None)
or getattr(resource_metadata, "name", None)
or ""
)
self.resource_arn = getattr(resource_metadata, "arn", "")
self.region = getattr(resource_metadata, "region", "")
else:
self.resource_id = ""
self.resource_arn = ""
self.region = ""
def __init__(self, metadata):
super().__init__(metadata)
self.resource_id = ""
self.resource_arn = ""
self.region = ""
@dataclass
+23
View File
@@ -0,0 +1,23 @@
from schema import Optional, Schema
mutelist_schema = Schema(
{
"Accounts": {
str: {
"Checks": {
str: {
"Regions": list,
"Resources": list,
Optional("Tags"): list,
Optional("Exceptions"): {
Optional("Accounts"): list,
Optional("Regions"): list,
Optional("Resources"): list,
Optional("Tags"): list,
},
}
}
}
}
}
)
+2 -77
View File
@@ -2,86 +2,12 @@ import re
from abc import ABC, abstractmethod
import yaml
from jsonschema import validate
from prowler.lib.logger import logger
from prowler.lib.mutelist.models import mutelist_schema
from prowler.lib.outputs.common import Status
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
mutelist_schema = {
"type": "object",
"properties": {
"Accounts": {
"type": "object",
"patternProperties": {
".*": { # Match any account
"type": "object",
"properties": {
"Checks": {
"type": "object",
"patternProperties": {
".*": { # Match any check
"type": "object",
"properties": {
"Regions": {
"type": "array",
"items": {"type": "string"},
},
"Resources": {
"type": "array",
"items": {"type": "string"},
},
"Tags": { # Optional field
"type": "array",
"items": {"type": "string"},
},
"Exceptions": { # Optional field
"type": "object",
"properties": {
"Accounts": { # Optional field
"type": "array",
"items": {"type": "string"},
},
"Regions": { # Optional field
"type": "array",
"items": {"type": "string"},
},
"Resources": { # Optional field
"type": "array",
"items": {"type": "string"},
},
"Tags": { # Optional field
"type": "array",
"items": {"type": "string"},
},
},
"additionalProperties": False,
},
"Description": { # Optional field
"type": "string",
},
},
"required": [
"Regions",
"Resources",
], # Mandatory within a check
"additionalProperties": False,
}
},
"additionalProperties": False,
},
},
"required": ["Checks"], # Mandatory within an account
"additionalProperties": False,
}
},
"additionalProperties": False,
}
},
"required": ["Accounts"], # Accounts is mandatory at the root level
"additionalProperties": False,
}
class Mutelist(ABC):
"""
@@ -144,7 +70,7 @@ class Mutelist(ABC):
def validate_mutelist(self) -> bool:
try:
validate(self._mutelist, schema=mutelist_schema)
self._mutelist = mutelist_schema.validate(self._mutelist)
return True
except Exception as error:
logger.error(
@@ -180,7 +106,6 @@ class Mutelist(ABC):
- 'i-123456789'
Tags:
- 'Name=AdminInstance | Environment=Prod'
Description: 'Field to describe why the findings associated with these values are muted'
```
The check `ec2_instance_detailed_monitoring_enabled` will be muted for all accounts and regions and for the resource_id 'i-123456789' with at least one of the tags 'Name=AdminInstance' or 'Environment=Prod'.
-1
View File
@@ -14,7 +14,6 @@ def fill_common_finding_data(finding: dict, unix_timestamp: bool) -> dict:
"status_extended": finding.status_extended,
"muted": finding.muted,
"resource_details": finding.resource_details,
# "resource_metadata": finding.resource_metadata, TODO: add resource_metadata to the finding
"resource_tags": unroll_tags(finding.resource_tags),
}
return finding_data
@@ -45,8 +45,6 @@ class AWSISO27001(ComplianceOutput):
AccountId=finding.account_uid,
Region=finding.region,
AssessmentDate=str(finding.timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Attributes_Category=attribute.Category,
Requirements_Attributes_Objetive_ID=attribute.Objetive_ID,
Requirements_Attributes_Objetive_Name=attribute.Objetive_Name,
@@ -69,8 +67,6 @@ class AWSISO27001(ComplianceOutput):
AccountId="",
Region="",
AssessmentDate=str(finding.timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Attributes_Category=attribute.Category,
Requirements_Attributes_Objetive_ID=attribute.Objetive_ID,
Requirements_Attributes_Objetive_Name=attribute.Objetive_Name,
@@ -11,8 +11,6 @@ class AWSISO27001Model(BaseModel):
AccountId: str
Region: str
AssessmentDate: str
Requirements_Id: str
Requirements_Description: str
Requirements_Attributes_Category: str
Requirements_Attributes_Objetive_ID: str
Requirements_Attributes_Objetive_Name: str
-1
View File
@@ -35,7 +35,6 @@ class Finding(BaseModel):
status_extended: str
muted: bool = False
resource_uid: str
# resource_metadata: dict = Field(default_factory=dict) TODO: add resource_metadata to the finding
resource_name: str
resource_details: str
resource_tags: dict = Field(default_factory=dict)
+2 -8
View File
@@ -111,10 +111,7 @@ class OCSF(Output):
# TODO: this should be included only if using the Cloud profile
cloud_partition=finding.partition,
region=finding.region,
data={
"details": finding.resource_details,
# "metadata": finding.resource_metadata, TODO: add the resource_metadata to the finding
},
data={"details": finding.resource_details},
)
]
if finding.metadata.Provider != "kubernetes"
@@ -125,10 +122,7 @@ class OCSF(Output):
uid=finding.resource_uid,
group=Group(name=finding.metadata.ServiceName),
type=finding.metadata.ResourceType,
data={
"details": finding.resource_details,
# "metadata": finding.resource_metadata, TODO: add the resource_metadata to the finding
},
data={"details": finding.resource_details},
namespace=finding.region.replace("namespace: ", ""),
)
]
+1 -8
View File
@@ -789,14 +789,7 @@ class AwsProvider(Provider):
# Handle if there are audit resources so only their services are executed
if self._audit_resources:
# TODO: this should be retrieved automatically
services_without_subservices = [
"guardduty",
"kms",
"s3",
"elb",
"efs",
"sqs",
]
services_without_subservices = ["guardduty", "kms", "s3", "elb", "efs"]
service_list = set()
sub_service_list = set()
for resource in self._audit_resources:
+26 -194
View File
@@ -15,7 +15,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -98,7 +97,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -222,7 +220,6 @@
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-5",
"ca-central-1",
"eu-central-1",
"eu-north-1",
@@ -337,7 +334,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -458,7 +454,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -503,7 +498,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -830,7 +824,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -875,7 +868,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -916,7 +908,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1001,7 +992,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1046,7 +1036,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1102,7 +1091,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1234,7 +1222,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1273,15 +1260,6 @@
"aws-us-gov": []
}
},
"bcm-pricing-calculator": {
"regions": {
"aws": [
"us-east-1"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"bedrock": {
"regions": {
"aws": [
@@ -1793,7 +1771,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1838,7 +1815,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1970,7 +1946,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -2003,21 +1978,7 @@
"cloudtrail-data": {
"regions": {
"aws": [
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-southeast-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"me-central-1",
"me-south-1",
"us-east-1",
"us-east-2",
"us-west-1"
"me-central-1"
],
"aws-cn": [],
"aws-us-gov": []
@@ -2038,7 +1999,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -2597,7 +2557,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -2927,7 +2886,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3118,7 +3076,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3178,7 +3135,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3223,7 +3179,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3385,7 +3340,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3430,7 +3384,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3475,7 +3428,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3520,7 +3472,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3565,7 +3516,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3620,7 +3570,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3665,7 +3614,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3710,7 +3658,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3755,7 +3702,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3811,7 +3757,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3909,7 +3854,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3954,7 +3898,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -3999,7 +3942,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4141,7 +4083,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4186,7 +4127,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4231,7 +4171,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4293,7 +4232,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4585,7 +4523,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4629,7 +4566,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4673,7 +4609,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4716,7 +4651,6 @@
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -4760,7 +4694,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5075,7 +5008,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5162,7 +5094,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5207,7 +5138,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5432,6 +5362,30 @@
]
}
},
"iot1click-devices": {
"regions": {
"aws": [
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"iot1click-projects": {
"regions": {
"aws": [
"ap-northeast-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"iotanalytics": {
"regions": {
"aws": [
@@ -5758,7 +5712,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5864,7 +5817,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5982,7 +5934,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -6071,7 +6022,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -6257,7 +6207,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -6408,7 +6357,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -6680,7 +6628,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7398,31 +7345,6 @@
]
}
},
"networkflowmonitor": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"networkmanager": {
"regions": {
"aws": [
@@ -7520,15 +7442,6 @@
"aws-us-gov": []
}
},
"notificationscontacts": {
"regions": {
"aws": [
"us-east-1"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"oam": {
"regions": {
"aws": [
@@ -7544,7 +7457,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7574,23 +7486,6 @@
]
}
},
"observabilityadmin": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"omics": {
"regions": {
"aws": [
@@ -7621,7 +7516,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7760,7 +7654,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7958,7 +7851,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8174,7 +8066,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8347,7 +8238,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8392,7 +8282,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8437,7 +8326,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8513,7 +8401,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8746,7 +8633,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8791,7 +8677,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8934,7 +8819,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -8976,7 +8860,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9079,7 +8962,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9158,7 +9040,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9329,7 +9210,6 @@
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-south-1",
"sa-east-1",
"us-east-1",
@@ -9367,7 +9247,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9448,7 +9327,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9583,7 +9461,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9613,26 +9490,6 @@
]
}
},
"security-ir": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"securityhub": {
"regions": {
"aws": [
@@ -9752,7 +9609,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9879,7 +9735,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10193,7 +10048,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10226,14 +10080,7 @@
"socialmessaging": {
"regions": {
"aws": [
"ap-south-1",
"ap-southeast-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
"eu-central-1"
],
"aws-cn": [],
"aws-us-gov": []
@@ -10254,7 +10101,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10299,7 +10145,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10495,7 +10340,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10584,7 +10428,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10617,7 +10460,6 @@
"support": {
"regions": {
"aws": [
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
@@ -10629,7 +10471,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10685,7 +10526,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10730,7 +10570,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10983,7 +10822,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -11054,7 +10892,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -11219,7 +11056,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -11264,12 +11100,10 @@
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
@@ -11300,7 +11134,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -11648,7 +11481,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -17,7 +17,7 @@ class acm_certificates_with_secure_key_algorithms(Check):
report.status = "PASS"
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} uses a secure key algorithm ({certificate.key_algorithm})."
if certificate.key_algorithm in acm_client.audit_config.get(
"insecure_key_algorithms", ["RSA-1024", "P-192", "SHA-1"]
"insecure_algorithms", ["RSA-1024"]
):
report.status = "FAIL"
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} does not use a secure key algorithm ({certificate.key_algorithm})."
@@ -1,32 +1,6 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client
default_obsolete_lambda_runtimes = [
"java8",
"go1.x",
"provided",
"python3.6",
"python2.7",
"python3.7",
"nodejs4.3",
"nodejs4.3-edge",
"nodejs6.10",
"nodejs",
"nodejs8.10",
"nodejs10.x",
"nodejs12.x",
"nodejs14.x",
"nodejs16.x",
"dotnet5.0",
"dotnet7",
"dotnetcore1.0",
"dotnetcore2.0",
"dotnetcore2.1",
"dotnetcore3.1",
"ruby2.5",
"ruby2.7",
]
class awslambda_function_using_supported_runtimes(Check):
def execute(self):
@@ -40,7 +14,7 @@ class awslambda_function_using_supported_runtimes(Check):
report.resource_tags = function.tags
if function.runtime in awslambda_client.audit_config.get(
"obsolete_lambda_runtimes", default_obsolete_lambda_runtimes
"obsolete_lambda_runtimes", []
):
report.status = "FAIL"
report.status_extended = f"Lambda function {function.name} is using {function.runtime} which is obsolete."
@@ -8,14 +8,14 @@ class backup_recovery_point_encrypted(Check):
for recovery_point in backup_client.recovery_points:
report = Check_Report_AWS(self.metadata())
report.region = recovery_point.backup_vault_region
report.resource_id = recovery_point.id
report.resource_id = recovery_point.backup_vault_name
report.resource_arn = recovery_point.arn
report.resource_tags = recovery_point.tags
report.status = "FAIL"
report.status_extended = f"Backup Recovery Point {recovery_point.id} for Backup Vault {recovery_point.backup_vault_name} is not encrypted at rest."
report.status_extended = f"Backup Recovery Point {recovery_point.arn} for Backup Vault {recovery_point.backup_vault_name} is not encrypted at rest."
if recovery_point.encrypted:
report.status = "PASS"
report.status_extended = f"Backup Recovery Point {recovery_point.id} for Backup Vault {recovery_point.backup_vault_name} is encrypted at rest."
report.status_extended = f"Backup Recovery Point {recovery_point.arn} for Backup Vault {recovery_point.backup_vault_name} is encrypted at rest."
findings.append(report)
@@ -18,8 +18,7 @@ class Backup(AWSService):
self.backup_vault_arn_template = f"arn:{self.audited_partition}:backup:{self.region}:{self.audited_account}:backup-vault"
self.backup_vaults = []
self.__threading_call__(self._list_backup_vaults)
if self.backup_vaults is not None:
self.__threading_call__(self._list_tags, self.backup_vaults)
self.__threading_call__(self._list_tags, self.backup_vaults)
self.backup_plans = []
self.__threading_call__(self._list_backup_plans)
self.__threading_call__(self._list_tags, self.backup_plans)
@@ -29,7 +28,6 @@ class Backup(AWSService):
self.__threading_call__(self._list_backup_selections)
self.recovery_points = []
self.__threading_call__(self._list_recovery_points)
self.__threading_call__(self._list_tags, self.recovery_points)
def _list_backup_vaults(self, regional_client):
logger.info("Backup - Listing Backup Vaults...")
@@ -173,11 +171,10 @@ class Backup(AWSService):
def _list_tags(self, resource):
try:
if getattr(resource, "arn", None):
tags = self.regional_clients[resource.region].list_tags(
ResourceArn=resource.arn
)["Tags"]
resource.tags = [tags] if tags else []
tags = self.regional_clients[resource.region].list_tags(
ResourceArn=resource.arn
)["Tags"]
resource.tags = [tags] if tags else []
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -198,13 +195,11 @@ class Backup(AWSService):
self.recovery_points.append(
RecoveryPoint(
arn=arn,
id=arn.split(":")[-1],
backup_vault_name=backup_vault.name,
encrypted=recovery_point.get(
"IsEncrypted", False
),
backup_vault_region=backup_vault.region,
region=regional_client.region,
tags=[],
)
)
@@ -251,8 +246,6 @@ class BackupReportPlan(BaseModel):
class RecoveryPoint(BaseModel):
arn: str
id: str
region: str
backup_vault_name: str
encrypted: bool
backup_vault_region: str
@@ -23,7 +23,7 @@
"Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html"
}
},
"Categories": ["gen-ai"],
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -23,7 +23,7 @@
"Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html"
}
},
"Categories": ["gen-ai"],
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Ensure that prompt attack protection is set to the highest strength to minimize the risk of prompt injection attacks."
@@ -23,7 +23,7 @@
"Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html"
}
},
"Categories": ["gen-ai"],
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -25,8 +25,7 @@
},
"Categories": [
"logging",
"forensics-ready",
"gen-ai"
"forensics-ready"
],
"DependsOn": [],
"RelatedTo": [],
@@ -25,8 +25,7 @@
},
"Categories": [
"encryption",
"logging",
"gen-ai"
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,30 +0,0 @@
{
"Provider": "aws",
"CheckID": "cloudformation_stack_cdktoolkit_bootstrap_version",
"CheckTitle": "Ensure that CDKToolkit stacks have a Bootstrap version of 21 or higher to mitigate security risks.",
"CheckType": [],
"ServiceName": "cloudformation",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:cloudformation:region:account-id:stack/resource-id",
"Severity": "high",
"ResourceType": "AwsCloudFormationStack",
"Description": "Ensure that CDKToolkit stacks have a Bootstrap version of 21 or higher to mitigate security risks.",
"Risk": "Using outdated CDKToolkit Bootstrap versions can expose accounts to risks such as bucket takeover or privilege escalation.",
"RelatedUrl": "https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Update the CDKToolkit stack Bootstrap version to 21 or later by running the cdk bootstrap command with the latest CDK version.",
"Url": "https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -1,39 +0,0 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudformation.cloudformation_client import (
cloudformation_client,
)
class cloudformation_stack_cdktoolkit_bootstrap_version(Check):
"""Check if a CDKToolkit CloudFormation Stack has a Bootstrap version less than recommended"""
def execute(self):
"""Execute the cloudformation_stack_cdktoolkit_bootstrap_version check"""
findings = []
recommended_cdk_bootstrap_version = cloudformation_client.audit_config.get(
"recommended_cdk_bootstrap_version", 21
)
for stack in cloudformation_client.stacks:
# Only check stacks named CDKToolkit
if stack.name == "CDKToolkit":
bootstrap_version = None
if stack.outputs:
for output in stack.outputs:
if output.startswith("BootstrapVersion:"):
bootstrap_version = int(output.split(":")[1])
break
if bootstrap_version:
report = Check_Report_AWS(self.metadata())
report.region = stack.region
report.resource_id = stack.name
report.resource_arn = stack.arn
report.resource_tags = stack.tags
report.status = "PASS"
report.status_extended = f"CloudFormation Stack CDKToolkit has a Bootstrap version {bootstrap_version}, which meets the recommended version."
if bootstrap_version < recommended_cdk_bootstrap_version:
report.status = "FAIL"
report.status_extended = f"CloudFormation Stack CDKToolkit has a Bootstrap version {bootstrap_version}, which is less than the recommended version {recommended_cdk_bootstrap_version}."
findings.append(report)
return findings
Loaded 100 of 388 files, more files were not shown because too many files have changed in this diff. Show more