diff --git a/.env b/.env new file mode 100644 index 0000000000..14317089d0 --- /dev/null +++ b/.env @@ -0,0 +1,89 @@ +#### Important Note #### +# This file is used to store environment variables for the Prowler App. +# For production, it is recommended to use a secure method to store these variables and change the default secret keys. + +#### Prowler UI Configuration #### +PROWLER_UI_VERSION="latest" +SITE_URL=http://localhost:3000 +API_BASE_URL=http://prowler-api:8080/api/v1 +AUTH_TRUST_HOST=true +UI_PORT=3000 +# openssl rand -base64 32 +AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8=" + +#### Prowler API Configuration #### +PROWLER_API_VERSION="latest" +# PostgreSQL settings +# If running Django and celery on host, use 'localhost', else use 'postgres-db' +POSTGRES_HOST=postgres-db +POSTGRES_PORT=5432 +POSTGRES_ADMIN_USER=prowler_admin +POSTGRES_ADMIN_PASSWORD=postgres +POSTGRES_USER=prowler +POSTGRES_PASSWORD=postgres +POSTGRES_DB=prowler_db + +# Valkey settings +# If running Valkey and celery on host, use localhost, else use 'valkey' +VALKEY_HOST=valkey +VALKEY_PORT=6379 +VALKEY_DB=0 + +# Django settings +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api +DJANGO_BIND_ADDRESS=0.0.0.0 +DJANGO_PORT=8080 +DJANGO_DEBUG=False +DJANGO_SETTINGS_MODULE=config.django.production +# Select one of [ndjson|human_readable] +DJANGO_LOGGING_FORMATTER=human_readable +# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL] +# Applies to both Django and Celery Workers +DJANGO_LOGGING_LEVEL=INFO +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 +# openssl genrsa -out private.pem 2048 +DJANGO_TOKEN_SIGNING_KEY="-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDs4e+kt7SnUJek +6V5r9zMGzXCoU5qnChfPiqu+BgANyawz+MyVZPs6RCRfeo6tlCknPQtOziyXYM2I +7X+qckmuzsjqp8+u+o1mw3VvUuJew5k2SQLPYwsiTzuFNVJEOgRo3hywGiGwS2iv +/5nh2QAl7fq2qLqZEXQa5+/xJlQggS1CYxOJgggvLyra50QZlBvPve/AxKJ/EV/Q +irWTZU5lLNI8sH2iZR05vQeBsxZ0dCnGMT+vGl+cGkqrvzQzKsYbDmabMcfTYhYi +78fpv6A4uharJFHayypYBjE39PwhMyyeycrNXlpm1jpq+03HgmDuDMHydk1tNwuT +nEC7m7iNAgMBAAECggEAA2m48nJcJbn9SVi8bclMwKkWmbJErOnyEGEy2sTK3Of+ +NWx9BB0FmqAPNxn0ss8K7cANKOhDD7ZLF9E2MO4/HgfoMKtUzHRbM7MWvtEepldi +nnvcUMEgULD8Dk4HnqiIVjt3BdmGiTv46OpBnRWrkSBV56pUL+7msZmMZTjUZvh2 +ZWv0+I3gtDIjo2Zo/FiwDV7CfwRjJarRpYUj/0YyuSA4FuOUYl41WAX1I301FKMH +xo3jiAYi1s7IneJ16OtPpOA34Wg5F6ebm/UO0uNe+iD4kCXKaZmxYQPh5tfB0Qa3 +qj1T7GNpFNyvtG7VVdauhkb8iu8X/wl6PCwbg0RCKQKBgQD9HfpnpH0lDlHMRw9K +X7Vby/1fSYy1BQtlXFEIPTN/btJ/asGxLmAVwJ2HAPXWlrfSjVAH7CtVmzN7v8oj +HeIHfeSgoWEu1syvnv2AMaYSo03UjFFlfc/GUxF7DUScRIhcJUPCP8jkAROz9nFv +DByNjUL17Q9r43DmDiRsy0IFqQKBgQDvlJ9Uhl+Sp7gRgKYwa/IG0+I4AduAM+Gz +Dxbm52QrMGMTjaJFLmLHBUZ/ot+pge7tZZGws8YR8ufpyMJbMqPjxhIvRRa/p1Tf +E3TQPW93FMsHUvxAgY3MV5MzXFPhlNAKb+akP/RcXUhetGAuZKLubtDCWa55ZQuL +wj2OS+niRQKBgE7K8zUqNi6/22S8xhy/2GPgB1qPObbsABUofK0U6CAGLo6te+gc +6Jo84IyzFtQbDNQFW2Fr+j1m18rw9AqkdcUhQndiZS9AfG07D+zFB86LeWHt4DS4 +ymIRX8Kvaak/iDcu/n3Mf0vCrhB6aetImObTj4GgrwlFvtJOmrYnO8EpAoGAIXXP +Xt25gWD9OyyNiVu6HKwA/zN7NYeJcRmdaDhO7B1A6R0x2Zml4AfjlbXoqOLlvLAf +zd79vcoAC82nH1eOPiSOq51plPDI0LMF8IN0CtyTkn1Lj7LIXA6rF1RAvtOqzppc +SvpHpZK9pcRpXnFdtBE0BMDDtl6fYzCIqlP94UUCgYEAnhXbAQMF7LQifEm34Dx8 +BizRMOKcqJGPvbO2+Iyt50O5X6onU2ITzSV1QHtOvAazu+B1aG9pEuBFDQ+ASxEu +L9ruJElkOkb/o45TSF6KCsHd55ReTZ8AqnRjf5R+lyzPqTZCXXb8KTcRvWT4zQa3 +VxyT2PnaSqEcexWUy4+UXoQ= +-----END PRIVATE KEY-----" +# openssl rsa -in private.pem -pubout -out public.pem +DJANGO_TOKEN_VERIFYING_KEY="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7OHvpLe0p1CXpOlea/cz +Bs1wqFOapwoXz4qrvgYADcmsM/jMlWT7OkQkX3qOrZQpJz0LTs4sl2DNiO1/qnJJ +rs7I6qfPrvqNZsN1b1LiXsOZNkkCz2MLIk87hTVSRDoEaN4csBohsEtor/+Z4dkA +Je36tqi6mRF0Gufv8SZUIIEtQmMTiYIILy8q2udEGZQbz73vwMSifxFf0Iq1k2VO +ZSzSPLB9omUdOb0HgbMWdHQpxjE/rxpfnBpKq780MyrGGw5mmzHH02IWIu/H6b+g +OLoWqyRR2ssqWAYxN/T8ITMsnsnKzV5aZtY6avtNx4Jg7gzB8nZNbTcLk5xAu5u4 +jQIDAQAB +-----END PUBLIC KEY-----" +# openssl rand -base64 32 +DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc=" diff --git a/.github/workflows/api-pull-request.yml b/.github/workflows/api-pull-request.yml index 3da5fed6af..8143ca7f41 100644 --- a/.github/workflows/api-pull-request.yml +++ b/.github/workflows/api-pull-request.yml @@ -1,4 +1,4 @@ -name: "API - Pull Request" +name: "API - Pull Request" on: push: @@ -148,7 +148,7 @@ jobs: working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | - poetry run pytest -n auto --cov=./src/backend --cov-report=xml src/backend + poetry run pytest --cov=./src/backend --cov-report=xml src/backend - name: Upload coverage reports to Codecov if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index f115325102..5ed64d4658 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build/ /dist/ *.egg-info/ */__pycache__/*.pyc +.idea/ # Session Session.vim @@ -46,7 +47,8 @@ junit-reports/ *.tfstate # .env -.env* +ui/.env* +api/.env* # Coverage .coverage* @@ -55,3 +57,6 @@ coverage* # Node node_modules + +# Persistent data +_data/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f871bf6de1..2fcb4d4c6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,12 +97,13 @@ repos: - id: safety name: safety description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities" - entry: bash -c 'safety check --ignore 70612' + entry: bash -c 'safety check --ignore 70612,66963' language: system - id: vulture name: vulture description: "Vulture finds unused code in Python programs." entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .' + exclude: 'api/src/backend/' language: system files: '.*\.py' diff --git a/README.md b/README.md index 3f1ed03f66..1a0944fb96 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,14 @@ **Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler SaaS. +## Prowler App + +Prowler App is a web application that allows you to run Prowler in your cloud provider accounts and visualize the results in a user-friendly interface. + +![Prowler App](docs/img/overview.png) + +>More details at [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation) + ## Prowler CLI ```console @@ -63,42 +71,121 @@ 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 | 553 | 77 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-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` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`| -| Azure | 138 | 17 -> `prowler azure --list-services` | 4 -> `prowler azure --list-compliance` | 2 -> `prowler azure --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` | # 💻 Installation -## Pip package -Prowler is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9, < 3.13: +## Prowler App + +Prowler App can be installed in different ways, depending on your environment: + +> See how to use Prowler App in the [Prowler App Usage Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/). + +### Docker Compose + +**Requirements** + +* `Docker Compose` installed: https://docs.docker.com/compose/install/. + +**Commands** + +``` console +curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml +curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env +docker compose up -d +``` + +> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + +### From GitHub + +**Requirements** + +* `git` installed. +* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). +* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). +* `Docker Compose` installed: https://docs.docker.com/compose/install/. + +**Commands to run the API** + +``` console +git clone https://github.com/prowler-cloud/prowler +cd prowler/api +poetry install +poetry shell +set -a +source .env +docker compose up postgres valkey -d +cd src/backend +python manage.py migrate --database admin +gunicorn -c config/guniconf.py config.wsgi:application +``` + +> Now, you can access the API documentation at http://localhost:8080/api/v1/docs. + +**Commands to run the API Worker** + +``` console +git clone https://github.com/prowler-cloud/prowler +cd prowler/api +poetry install +poetry shell +set -a +source .env +cd src/backend +python -m celery -A config.celery worker -l info -E +``` + +**Commands to run the UI** + +``` console +git clone https://github.com/prowler-cloud/prowler +cd prowler/ui +npm install +npm run build +npm start +``` + +> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + +## Prowler CLI +### Pip package +Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9, < 3.13: ```console pip install prowler prowler -v ``` ->More details at [https://docs.prowler.com](https://docs.prowler.com/projects/prowler-open-source/en/latest/) +>More details at [https://docs.prowler.com](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli-installation) -## Containers +### Containers -The available versions of Prowler are the following: +The available versions of Prowler CLI are the following: - `latest`: in sync with `master` branch (bear in mind that it is not a stable version) +- `v4-latest`: in sync with `v4` branch (bear in mind that it is not a stable version) - `v3-latest`: in sync with `v3` branch (bear in mind that it is not a stable version) - `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases. - `stable`: this tag always point to the latest release. +- `v4-stable`: this tag always point to the latest release for v4. - `v3-stable`: this tag always point to the latest release for v3. The 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) -- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags) -- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler) - -## From GitHub +### From GitHub Python >= 3.9, < 3.13 is required with pip and poetry: -``` +``` console git clone https://github.com/prowler-cloud/prowler cd prowler poetry shell @@ -108,6 +195,16 @@ python prowler.py -v > If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths. # 📐✏️ High level architecture +## Prowler App +The **Prowler App** consists of three main components: + +- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js. +- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework. +- **Prowler SDK**: A Python SDK that integrates with the Prowler CLI for advanced functionality. + +![Prowler App Architecture](docs/img/prowler-app-architecture.png) + +## Prowler CLI You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more. ![Architecture](docs/img/architecture.png) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000000..2c39e2d9d2 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,111 @@ +services: + api-dev: + hostname: "prowler-api" + build: + context: ./api + dockerfile: Dockerfile + target: dev + environment: + - DJANGO_SETTINGS_MODULE=config.django.devel + - DJANGO_LOGGING_FORMATTER=${LOGGING_FORMATTER:-human_readable} + env_file: + - path: .env + required: false + ports: + - "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}" + volumes: + - "./api/src/backend:/home/prowler/backend" + - "./api/pyproject.toml:/home/prowler/pyproject.toml" + depends_on: + postgres: + condition: service_healthy + valkey: + condition: service_healthy + entrypoint: + - "/home/prowler/docker-entrypoint.sh" + - "dev" + + ui-dev: + build: + context: ./ui + dockerfile: Dockerfile + target: dev + env_file: + - path: .env + required: false + ports: + - 3000:3000 + + postgres: + image: postgres:16.3-alpine + hostname: "postgres-db" + volumes: + - ./_data/postgres:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_ADMIN_USER} + - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + env_file: + - path: .env + required: false + ports: + - "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}" + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'"] + interval: 5s + timeout: 5s + retries: 5 + + valkey: + image: valkey/valkey:7-alpine3.19 + hostname: "valkey" + volumes: + - ./api/_data/valkey:/data + env_file: + - path: .env + required: false + ports: + - "${VALKEY_PORT:-6379}:6379" + healthcheck: + test: ["CMD-SHELL", "sh -c 'valkey-cli ping'"] + interval: 10s + timeout: 5s + retries: 3 + + worker-dev: + build: + context: ./api + dockerfile: Dockerfile + target: dev + environment: + - DJANGO_SETTINGS_MODULE=config.django.devel + env_file: + - path: .env + required: false + depends_on: + valkey: + condition: service_healthy + postgres: + condition: service_healthy + entrypoint: + - "/home/prowler/docker-entrypoint.sh" + - "worker" + + worker-beat: + build: + context: ./api + dockerfile: Dockerfile + target: dev + environment: + - DJANGO_SETTINGS_MODULE=config.django.devel + env_file: + - path: ./.env + required: false + depends_on: + valkey: + condition: service_healthy + postgres: + condition: service_healthy + entrypoint: + - "../docker-entrypoint.sh" + - "beat" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..281c79ddf8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,89 @@ +services: + api: + hostname: "prowler-api" + image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest} + env_file: + - path: .env + required: false + ports: + - "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}" + depends_on: + postgres: + condition: service_healthy + valkey: + condition: service_healthy + entrypoint: + - "/home/prowler/docker-entrypoint.sh" + - "prod" + + ui: + image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-latest} + env_file: + - path: .env + required: false + ports: + - ${UI_PORT:-3000}:${UI_PORT:-3000} + + postgres: + image: postgres:16.3-alpine + hostname: "postgres-db" + volumes: + - ./_data/postgres:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_ADMIN_USER} + - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + env_file: + - path: .env + required: false + ports: + - "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}" + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'"] + interval: 5s + timeout: 5s + retries: 5 + + valkey: + image: valkey/valkey:7-alpine3.19 + hostname: "valkey" + volumes: + - ./_data/valkey:/data + env_file: + - path: .env + required: false + ports: + - "${VALKEY_PORT:-6379}:6379" + healthcheck: + test: ["CMD-SHELL", "sh -c 'valkey-cli ping'"] + interval: 10s + timeout: 5s + retries: 3 + + worker: + image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest} + env_file: + - path: .env + required: false + depends_on: + valkey: + condition: service_healthy + postgres: + condition: service_healthy + entrypoint: + - "/home/prowler/docker-entrypoint.sh" + - "worker" + + worker-beat: + image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest} + env_file: + - path: ./.env + required: false + depends_on: + valkey: + condition: service_healthy + postgres: + condition: service_healthy + entrypoint: + - "../docker-entrypoint.sh" + - "beat" diff --git a/docs/img/add-account.png b/docs/img/add-account.png new file mode 100644 index 0000000000..35333883b5 Binary files /dev/null and b/docs/img/add-account.png differ diff --git a/docs/img/add-provider.png b/docs/img/add-provider.png new file mode 100644 index 0000000000..4e986e3f1a Binary files /dev/null and b/docs/img/add-provider.png differ diff --git a/docs/img/aws-credentials.png b/docs/img/aws-credentials.png new file mode 100644 index 0000000000..71098075db Binary files /dev/null and b/docs/img/aws-credentials.png differ diff --git a/docs/img/aws-role.png b/docs/img/aws-role.png new file mode 100644 index 0000000000..bd63511fb6 Binary files /dev/null and b/docs/img/aws-role.png differ diff --git a/docs/img/azure-credentials.png b/docs/img/azure-credentials.png new file mode 100644 index 0000000000..2f5448d0b9 Binary files /dev/null and b/docs/img/azure-credentials.png differ diff --git a/docs/img/connect-aws-credentials.png b/docs/img/connect-aws-credentials.png new file mode 100644 index 0000000000..7ea1f193c3 Binary files /dev/null and b/docs/img/connect-aws-credentials.png differ diff --git a/docs/img/connect-aws-role.png b/docs/img/connect-aws-role.png new file mode 100644 index 0000000000..78c8f6f390 Binary files /dev/null and b/docs/img/connect-aws-role.png differ diff --git a/docs/img/findings.png b/docs/img/findings.png new file mode 100644 index 0000000000..e2ea9c56ce Binary files /dev/null and b/docs/img/findings.png differ diff --git a/docs/img/gcp-credentials.png b/docs/img/gcp-credentials.png new file mode 100644 index 0000000000..4f6dae3b1e Binary files /dev/null and b/docs/img/gcp-credentials.png differ diff --git a/docs/img/issues.png b/docs/img/issues.png new file mode 100644 index 0000000000..009bc0d447 Binary files /dev/null and b/docs/img/issues.png differ diff --git a/docs/img/kubernetes-credentials.png b/docs/img/kubernetes-credentials.png new file mode 100644 index 0000000000..b461ed218a Binary files /dev/null and b/docs/img/kubernetes-credentials.png differ diff --git a/docs/img/log-in.png b/docs/img/log-in.png new file mode 100644 index 0000000000..9e6d410abe Binary files /dev/null and b/docs/img/log-in.png differ diff --git a/docs/img/overview.png b/docs/img/overview.png new file mode 100644 index 0000000000..a4975bb234 Binary files /dev/null and b/docs/img/overview.png differ diff --git a/docs/img/provider-added.png b/docs/img/provider-added.png new file mode 100644 index 0000000000..cfb94d2cab Binary files /dev/null and b/docs/img/provider-added.png differ diff --git a/docs/img/prowler-app-architecture.png b/docs/img/prowler-app-architecture.png new file mode 100644 index 0000000000..889bc0da88 Binary files /dev/null and b/docs/img/prowler-app-architecture.png differ diff --git a/docs/img/scan-progress.png b/docs/img/scan-progress.png new file mode 100644 index 0000000000..3378775dcd Binary files /dev/null and b/docs/img/scan-progress.png differ diff --git a/docs/img/select-provider.png b/docs/img/select-provider.png new file mode 100644 index 0000000000..f738d04fb7 Binary files /dev/null and b/docs/img/select-provider.png differ diff --git a/docs/img/services.png b/docs/img/services.png new file mode 100644 index 0000000000..e82a441576 Binary files /dev/null and b/docs/img/services.png differ diff --git a/docs/img/sign-up-button.png b/docs/img/sign-up-button.png new file mode 100644 index 0000000000..b7006e72e5 Binary files /dev/null and b/docs/img/sign-up-button.png differ diff --git a/docs/img/sign-up.png b/docs/img/sign-up.png new file mode 100644 index 0000000000..56e8901b24 Binary files /dev/null and b/docs/img/sign-up.png differ diff --git a/docs/img/test-connection-button.png b/docs/img/test-connection-button.png new file mode 100644 index 0000000000..261cb035c7 Binary files /dev/null and b/docs/img/test-connection-button.png differ diff --git a/docs/index.md b/docs/index.md index 2b06f269bf..6f21f75023 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,14 @@ **Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call Prowler SaaS. -## Prowler CLI +## Prowler App + +![Prowler App](img/overview.png) + +Prowler App is a web application that allows you to run Prowler in a simple way. It provides a user-friendly interface to configure and run scans, view results, and manage your security findings. + +See how to install the Prowler App in the [Quick Start](#prowler-app-installation) section. + +## Prowler CLI ```console prowler @@ -17,7 +25,95 @@ prowler dashboard It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks. ## Quick Start -### Installation +### Prowler App Installation + +Prowler App can be installed in different ways, depending on your environment: + +> See how to use Prowler App in the [Prowler App](tutorials/prowler-app.md) section. + +=== "Docker Compose" + + _Requirements_: + + * `Docker Compose` installed: https://docs.docker.com/compose/install/. + + _Commands_: + + ``` bash + curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml + curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env + docker compose up -d + ``` + + > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + + ???+ note + You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments. + + ???+ note + There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose.dev.yml to run the app in development mode. + + ???+ warning + Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). + +=== "GitHub" + + _Requirements_: + + * `git` installed. + * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). + * `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + * `Docker Compose` installed: https://docs.docker.com/compose/install/. + + _Commands to run the API_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + poetry shell \ + set -a \ + source .env \ + docker compose up postgres valkey -d \ + cd src/backend \ + python manage.py migrate --database admin \ + gunicorn -c config/guniconf.py config.wsgi:application + ``` + + > Now, you can access the API documentation at http://localhost:8080/api/v1/docs. + + _Commands to run the API Worker_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/api \ + poetry install \ + poetry shell \ + set -a \ + source .env \ + cd src/backend \ + python -m celery -A config.celery worker -l info -E + ``` + + _Commands to run the UI_: + + ``` bash + git clone https://github.com/prowler-cloud/prowler \ + cd prowler/ui \ + npm install \ + npm run build \ + npm start + ``` + + > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password. + + ???+ warning + Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files. + + ???+ warning + Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com). + +### Prowler CLI Installation Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), thus can be installed as Python package with `Python >= 3.9`: @@ -195,18 +291,23 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), ## Prowler container versions -The available versions of Prowler are the following: +The available versions of Prowler CLI are the following: - `latest`: in sync with `master` branch (bear in mind that it is not a stable version) +- `v4-latest`: in sync with `v4` branch (bear in mind that it is not a stable version) - `v3-latest`: in sync with `v3` branch (bear in mind that it is not a stable version) - `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases. - `stable`: this tag always point to the latest release. +- `v4-stable`: this tag always point to the latest release for v4. - `v3-stable`: this tag always point to the latest release for v3. The container images are available here: - -- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags) -- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler) +- 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) ## High level architecture @@ -214,6 +315,22 @@ You can run Prowler from your workstation, a Kubernetes Job, a Google Compute En ![Architecture](img/architecture.png) +### Prowler App + +The **Prowler App** consists of three main components: + +- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js. +- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework. +- **Prowler SDK**: A Python SDK that integrates with the Prowler CLI for advanced functionality. + +The app leverages the following supporting infrastructure: + +- **PostgreSQL**: Used for persistent storage of scan results. +- **Celery Workers**: Facilitate asynchronous execution of Prowler scans. +- **Valkey**: An in-memory database serving as a message broker for the Celery workers. + +![Prowler App Architecture](img/prowler-app-architecture.png) + ## Deprecations from v3 ### General @@ -231,6 +348,40 @@ We have deprecated some of our outputs formats: - To send only FAILS to AWS Security Hub, now use either `--send-sh-only-fails` or `--security-hub --status FAIL`. ## Basic Usage +### Prowler App + +#### **Access the App** +Go to [http://localhost:3000](http://localhost:3000) after installing the app (see [Quick Start](#prowler-app-installation)). Sign up with your email and password. + +Sign Up Button +Sign Up + +#### **Log In** +Log in with your email and password to start using the Prowler App. + +Log In + +#### **Add a Provider** +- Go to `Settings > Cloud Providers` and click `Add Account`. +- Select the provider you want to scan (AWS, GCP, Azure, Kubernetes). +- Enter the provider's ID (AWS Account ID, GCP Project ID, Azure Subscription ID, Kubernetes Cluster) and optional alias. +- Follow the instructions to add your credentials. + +#### **Start a Scan** +After successfully adding and testing your credentials, Prowler will start scanning your cloud environment, click on the `Go to Scans` button to see the progress. + +#### **View Results** +While the scan is running, start exploring the findings in these sections: + +- **Overview**: High-level summary of the scans. Overview +- **Compliance**: Insights into compliance status. Compliance + +> See more details about the Prowler App usage in the [Prowler App](tutorials/prowler-app.md) section. + +???+ note + Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored. + +### Prowler CLI To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure` or `kubernetes`): @@ -289,7 +440,7 @@ You can always use `-h`/`--help` to access to the usage information and all the prowler --help ``` -### AWS +#### AWS Use a custom AWS profile with `-p`/`--profile` and/or AWS regions which you want to audit with `-f`/`--filter-region`: @@ -302,7 +453,7 @@ prowler aws --profile custom-profile -f us-east-1 eu-south-2 See more details about AWS Authentication in [Requirements](getting-started/requirements.md#aws) -### Azure +#### Azure With Azure you need to specify which auth method is going to be used: @@ -327,7 +478,7 @@ Prowler by default scans all the subscriptions that is allowed to scan, if you w prowler azure --az-cli-auth --subscription-ids ... ``` -### Google Cloud +#### Google Cloud Prowler will use by default your User Account credentials, you can configure it using: @@ -349,7 +500,7 @@ prowler gcp --project-ids ... See more details about GCP Authentication in [Requirements](getting-started/requirements.md#google-cloud) -### Kubernetes +#### Kubernetes Prowler allows you to scan your Kubernetes Cluster either from within the cluster or from outside the cluster. diff --git a/docs/tutorials/azure/create-prowler-service-principal.md b/docs/tutorials/azure/create-prowler-service-principal.md index 7cb8c30a6b..84d620d0d2 100644 --- a/docs/tutorials/azure/create-prowler-service-principal.md +++ b/docs/tutorials/azure/create-prowler-service-principal.md @@ -11,7 +11,7 @@ To allow Prowler assume an identity to start the scan with the required privileg 7. Fill the "Description" and "Expires" fields and click on "Add" 8. Copy the value of the secret, it is going to be used as `AZURE_CLIENT_SECRET` environment variable. -![Register an Application page](../../img/create-sp.gif) +![Register an Application page](../img/create-sp.gif) ## Assigning the proper permissions diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md new file mode 100644 index 0000000000..9133c8fe7b --- /dev/null +++ b/docs/tutorials/prowler-app.md @@ -0,0 +1,132 @@ +# Prowler App + +The **Prowler App** is a user-friendly interface for the Prowler CLI, providing a visual dashboard to monitor your cloud security posture. This tutorial will guide you through setting up and using the Prowler App. + +After [installing](../index.md#prowler-app-installation) the **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). +You can also access to the auto-generated **Prowler API** documentation at [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs) to see all the available endpoints, parameters and responses. + +## **Step 1: Sign Up** +To get started, sign up using your email and password: + +Sign Up Button +Sign Up + +--- + +## **Step 2: Log In** +Once you’ve signed up, log in with your email and password to start using the Prowler App. + +Log In + +You will see the Overview page with no data yet, so let's start adding a provider to scan your cloud environment. + +--- + +## **Step 3: Add a Provider** +To run your first scan, you need to add a cloud provider account. Prowler App supports AWS, Azure, GCP, and Kubernetes. + +1. Navigate to `Settings > Cloud Providers`. +2. Click `Add Account` to set up a new provider and provide your credentials: + +Add Provider + +--- + +## **Step 4: Configure the Provider** +Choose the provider you want to scan from the following options: + +Select a Provider + +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 name. + +Optionally, provide a **Provider Alias** for easier identification. Follow the instructions provided to add your credentials: + +--- +### **Step 4.1: AWS Credentials** +For AWS, enter your `AWS Account ID` and choose one of the following methods to connect: + +#### **Step 4.1.1: IAM Access Keys** +1. Select `Connect via Credentials`. + + AWS Credentials + +2. Enter your `Access Key ID`, `Secret Access Key` and optionally a `Session Token`: + + AWS Credentials + +#### **Step 4.1.2: IAM Role** +1. Select `Connect assuming IAM Role`. + + AWS Role + +2. Enter the `Role ARN` and any optional field like the AWS Access Keys to assume the role, the `External ID`, the `Role Session Name` or the `Session Duration`: + + AWS Role + +--- + +### **Step 4.2: Azure Credentials** +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. + +Azure Credentials + +--- +### **Step 4.3: GCP Credentials** +To connect your GCP Project, you need to use the Application Default Credentials (ADC) returned by the `gcloud` CLI. Here’s how to set up: + +1. Run the following command in your terminal to authenticate with GCP: +```bash +gcloud auth application-default login +``` +2. Once authenticated, get the `Client ID`, `Client Secret` and `Refresh Token` from `~/.config/gcloud/application_default_credentials`. +3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into the Prowler App. + +GCP Credentials + +--- +### **Step 4.4: Kubernetes Credentials** +For Kubernetes, Prowler App uses a `kubeconfig` file to authenticate, paste the contents of your `kubeconfig` file into the `Kubeconfig content` field. + +By default, the `kubeconfig` file is located at `~/.kube/config`. + +Kubernetes Credentials + +--- + +## **Step 5: Test Connection** +After adding your credentials of your cloud account, click the `Launch` button to verify that the Prowler App can successfully connect to your provider: + +Test Connection + + +## **Step 6: Scan started** +After successfully adding and testing your credentials, Prowler will start scanning your cloud environment, click on the `Go to Scans` button to see the progress: + +Start Now + +???+ note + Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored. +--- + +## **Step 7: Monitor Scan Progress** +Track the progress of your scan in the `Scans` section: + +Scan Progress + +--- + +## **Step 8: Analyze the Findings** +While the scan is running, start exploring the findings in these sections: + +- **Overview**: High-level summary of the scans. Overview +- **Compliance**: Insights into compliance status. Compliance +- **Issues**: Types of issues detected. + +Issues + +- **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more. Findings diff --git a/mkdocs.yml b/mkdocs.yml index 170a9c3d9d..e311146609 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ theme: name: material favicon: favicon.ico features: + - content.code.copy - navigation.tabs - navigation.tabs.sticky - navigation.sections @@ -49,6 +50,7 @@ nav: - Overview: index.md - Requirements: getting-started/requirements.md - Tutorials: + - Prowler App: tutorials/prowler-app.md - Miscellaneous: tutorials/misc.md - Reporting: tutorials/reporting.md - Compliance: tutorials/compliance.md diff --git a/permissions/prowler-additions-policy.json b/permissions/prowler-additions-policy.json index 74c6ba8e6d..aa8e01afa7 100644 --- a/permissions/prowler-additions-policy.json +++ b/permissions/prowler-additions-policy.json @@ -7,6 +7,7 @@ "appstream:Describe*", "appstream:List*", "backup:List*", + "backup:Get*", "bedrock:List*", "bedrock:Get*", "cloudtrail:GetInsightSelectors", @@ -29,6 +30,7 @@ "glue:GetConnections", "glue:GetSecurityConfiguration*", "glue:SearchTables", + "glue:GetMLTransforms", "lambda:GetFunction*", "logs:FilterLogEvents", "lightsail:GetRelationalDatabases", diff --git a/poetry.lock b/poetry.lock index 364e772a53..23daa623e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -680,13 +680,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "bandit" -version = "1.7.10" +version = "1.8.0" description = "Security oriented static analyser for python code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, - {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, + {file = "bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38"}, + {file = "bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e"}, ] [package.dependencies] @@ -761,17 +761,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.66" +version = "1.35.71" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.66-py3-none-any.whl", hash = "sha256:09a610f8cf4d3c22d4ca69c1f89079e3a1c82805ce94fa0eb4ecdd4d2ba6c4bc"}, - {file = "boto3-1.35.66.tar.gz", hash = "sha256:c392b9168b65e9c23483eaccb5b68d1f960232d7f967a1e00a045ba065ce050d"}, + {file = "boto3-1.35.71-py3-none-any.whl", hash = "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16"}, + {file = "boto3-1.35.71.tar.gz", hash = "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f"}, ] [package.dependencies] -botocore = ">=1.35.66,<1.36.0" +botocore = ">=1.35.71,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -780,13 +780,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.70" +version = "1.35.71" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.70-py3-none-any.whl", hash = "sha256:ba8a4797cf7c5d9c237e67a62692f5146e895613fd3e6a43b00b66f3a8c7fc73"}, - {file = "botocore-1.35.70.tar.gz", hash = "sha256:18d1bb505722d9efd50c50719ed8de7284bfe6d3908a9e08756a7646e549da21"}, + {file = "botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1"}, + {file = "botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba"}, ] [package.dependencies] @@ -5290,4 +5290,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "3e674bb39019458d469da43301c0ee7607277bd38ab294b6190935882eb280b8" +content-hash = "c6497888031fcb8ddece4848532980baa412943e90fa8298dd0e9671fbfb5391" diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index fc648ffc04..d760702fa8 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -556,19 +556,6 @@ def execute_checks( bar() bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}" - # Custom report interface - if os.environ.get("PROWLER_REPORT_LIB_PATH"): - try: - logger.info("Using custom report interface ...") - lib = os.environ["PROWLER_REPORT_LIB_PATH"] - outputs_module = importlib.import_module(lib) - custom_report_interface = getattr(outputs_module, "report") - - # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider - custom_report_interface(check_findings, output_options, global_provider) - except Exception: - sys.exit(1) - return all_findings diff --git a/prowler/lib/check/checks_loader.py b/prowler/lib/check/checks_loader.py index 8d5ac97ac4..ff54ccc8d1 100644 --- a/prowler/lib/check/checks_loader.py +++ b/prowler/lib/check/checks_loader.py @@ -111,7 +111,7 @@ def load_checks_to_execute( ): checks_to_execute.add(check_name) # Only execute threat detection checks if threat-detection category is set - if categories and categories != [] and "threat-detection" not in categories: + if not categories or "threat-detection" not in categories: for threat_detection_check in check_categories.get("threat-detection", []): checks_to_execute.discard(threat_detection_check) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index b3801ed8d1..5af9d64441 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -322,8 +322,9 @@ class CheckMetadata(BaseModel): checks = set() if service: - if service == "lambda": - service = "awslambda" + # This is a special case for the AWS provider since `lambda` is a reserved keyword in Python + if service == "awslambda": + service = "lambda" checks = { check_name for check_name, check_metadata in bulk_checks_metadata.items() diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index f8ec725446..cfc75a2e06 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -9265,10 +9265,7 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [ - "us-gov-east-1", - "us-gov-west-1" - ] + "aws-us-gov": [] } }, "sagemaker-runtime": { diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index b4dd5d66ce..60d24b1cb1 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -446,7 +446,7 @@ class RDS(AWSService): arn=arn, sns_topic_arn=event["SnsTopicArn"], status=event["Status"], - source_type=event["SourceType"], + source_type=event.get("SourceType", ""), source_id=event.get("SourceIdsList", []), event_list=event.get("EventCategoriesList", []), enabled=event["Enabled"], diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py b/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py index d8a6a41da4..d63d1a5e28 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible.py @@ -18,12 +18,7 @@ class containerregistry_not_publicly_accessible(Check): report.status = "FAIL" report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} allows unrestricted network access." - if ( - getattr( - container_registry_info.network_rule_set, "default_action", "" - ).lower() - == "deny" - ): + if not container_registry_info.public_network_access: report.status = "PASS" report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} does not allow unrestricted network access." diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_service.py b/prowler/providers/azure/services/containerregistry/containerregistry_service.py index 6c3840febc..189c89c642 100644 --- a/prowler/providers/azure/services/containerregistry/containerregistry_service.py +++ b/prowler/providers/azure/services/containerregistry/containerregistry_service.py @@ -37,8 +37,13 @@ class ContainerRegistry(AzureService): resource_group=resource_group, sku=getattr(registry.sku, "name", ""), login_server=getattr(registry, "login_server", ""), - public_network_access=getattr( - registry, "public_network_access", "" + public_network_access=( + False + if getattr( + registry, "public_network_access" "Enabled" + ) + == "Disabled" + else True ), admin_user_enabled=getattr( registry, "admin_user_enabled", False @@ -93,7 +98,7 @@ class ContainerRegistryInfo: resource_group: str sku: str login_server: str - public_network_access: str + public_network_access: bool admin_user_enabled: bool network_rule_set: NetworkRuleSet monitor_diagnostic_settings: list[DiagnosticSetting] diff --git a/prowler/providers/gcp/lib/service/service.py b/prowler/providers/gcp/lib/service/service.py index dbb7f22638..9763230153 100644 --- a/prowler/providers/gcp/lib/service/service.py +++ b/prowler/providers/gcp/lib/service/service.py @@ -55,7 +55,9 @@ class GCPService: project_ids = [] for project_id in audited_project_ids: try: - client = discovery.build("serviceusage", "v1") + client = discovery.build( + "serviceusage", "v1", credentials=self.credentials + ) request = client.services().get( name=f"projects/{project_id}/services/{self.service}.googleapis.com" ) diff --git a/prowler/providers/kubernetes/kubernetes_provider.py b/prowler/providers/kubernetes/kubernetes_provider.py index 9182d3a4ca..0f11a41fb0 100644 --- a/prowler/providers/kubernetes/kubernetes_provider.py +++ b/prowler/providers/kubernetes/kubernetes_provider.py @@ -1,4 +1,5 @@ import os +from typing import Union from colorama import Fore, Style from kubernetes.client.exceptions import ApiException @@ -74,14 +75,14 @@ class KubernetesProvider(Provider): fixer_config: dict = {}, mutelist_path: str = None, mutelist_content: dict = {}, - kubeconfig_content: dict = None, + kubeconfig_content: Union[dict, str] = None, ): """ Initializes the KubernetesProvider instance. Args: kubeconfig_file (str): Path to the kubeconfig file. - kubeconfig_content (dict): Content of the kubeconfig file. + kubeconfig_content (str or dict): Content of the kubeconfig file. context (str): Context name. namespace (list): List of namespaces. config_content (dict): Audit configuration. @@ -224,7 +225,7 @@ class KubernetesProvider(Provider): @staticmethod def setup_session( kubeconfig_file: str = None, - kubeconfig_content: dict = None, + kubeconfig_content: Union[dict, str] = None, context: str = None, ) -> KubernetesSession: """ @@ -232,7 +233,7 @@ class KubernetesProvider(Provider): Args: kubeconfig_file (str): Path to the kubeconfig file. - kubeconfig_content (dict): Content of the kubeconfig file. + kubeconfig_content (str or dict): Content of the kubeconfig file. context (str): Context name. Returns: @@ -243,14 +244,20 @@ class KubernetesProvider(Provider): KubernetesInvalidProviderIdError: If the provider ID is invalid. KubernetesSetUpSessionError: If an error occurs while setting up the session. """ - logger.info(f"Using kubeconfig file: {kubeconfig_file}") try: if kubeconfig_content: - config.load_kube_config_from_dict( - safe_load(kubeconfig_content), context=context - ) - + logger.info("Using kubeconfig content...") + config_data = safe_load(kubeconfig_content) + config.load_kube_config_from_dict(config_data, context=context) + if context: + contexts = config_data.get("contexts", []) + for context_item in contexts: + if context_item["name"] == context: + context = context_item + else: + context = config_data.get("contexts", [])[0] else: + logger.info(f"Using kubeconfig file: {kubeconfig_file}...") kubeconfig_file = ( kubeconfig_file if kubeconfig_file else "~/.kube/config" ) @@ -273,17 +280,19 @@ class KubernetesProvider(Provider): return KubernetesSession( api_client=client.ApiClient(), context=context ) - if context: - contexts = config.list_kube_config_contexts( - config_file=kubeconfig_file - )[0] - for context_item in contexts: - if context_item["name"] == context: - context = context_item - else: - context = config.list_kube_config_contexts(config_file=kubeconfig_file)[ - 1 - ] + if context: + contexts = config.list_kube_config_contexts( + config_file=kubeconfig_file + )[0] + for context_item in contexts: + if context_item["name"] == context: + context = context_item + else: + # If no context is provided, use the active context in the kubeconfig file + # The first element is the list of contexts, the second is the active context + context = config.list_kube_config_contexts( + config_file=kubeconfig_file + )[1] return KubernetesSession(api_client=client.ApiClient(), context=context) except parser.ParserError as parser_error: @@ -318,7 +327,7 @@ class KubernetesProvider(Provider): @staticmethod def test_connection( kubeconfig_file: str = "~/.kube/config", - kubeconfig_content: dict = None, + kubeconfig_content: Union[dict, str] = None, namespace: str = None, provider_id: str = None, raise_on_exception: bool = True, @@ -328,7 +337,7 @@ class KubernetesProvider(Provider): Args: kubeconfig_file (str): Path to the kubeconfig file. - kubeconfig_content (dict): Content of the kubeconfig file. + kubeconfig_content (str or dict): Content of the kubeconfig file. namespace (str): Namespace name. provider_id (str): Provider ID to use, in this case, the Kubernetes context. raise_on_exception (bool): Whether to raise an exception on error. @@ -352,7 +361,7 @@ class KubernetesProvider(Provider): ... ) - Using the kubeconfig content: >>> connection = KubernetesProvider.test_connection( - ... kubeconfig_content={"kubecofig": "content"}, + ... kubeconfig_content="kubeconfig content", ... namespace="default", ... provider_id="my-context", ... raise_on_exception=True, diff --git a/pyproject.toml b/pyproject.toml index 2599dbf58d..1ec503488f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,8 +48,8 @@ azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.66" -botocore = "1.35.70" +boto3 = "1.35.71" +botocore = "1.35.71" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" @@ -76,7 +76,7 @@ tabulate = "0.9.0" tzlocal = "5.2" [tool.poetry.group.dev.dependencies] -bandit = "1.7.10" +bandit = "1.8.0" black = "24.10.0" coverage = "7.6.8" docker = "7.1.0" diff --git a/tests/lib/check/check_loader_test.py b/tests/lib/check/check_loader_test.py index 72080fc560..a122d76ff5 100644 --- a/tests/lib/check/check_loader_test.py +++ b/tests/lib/check/check_loader_test.py @@ -14,11 +14,13 @@ S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_CUSTOM_ALIAS = ( S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY = "medium" S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE = "s3" +CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME = "cloudtrail_threat_detection_enumeration" + class TestCheckLoader: provider = "aws" - def get_custom_check_metadata(self): + def get_custom_check_s3_metadata(self): return CheckMetadata( Provider="aws", CheckID=S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, @@ -52,9 +54,37 @@ class TestCheckLoader: Compliance=[], ) + def get_threat_detection_check_metadata(self): + return CheckMetadata( + Provider="aws", + CheckID=CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME, + CheckTitle="Ensure there are no potential enumeration threats in CloudTrail", + CheckType=[], + ServiceName="cloudtrail", + SubServiceName="", + ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", + Severity="critical", + ResourceType="AwsCloudTrailTrail", + Description="This check ensures that there are no potential enumeration threats in CloudTrail.", + Risk="Potential enumeration threats in CloudTrail can lead to unauthorized access to resources.", + RelatedUrl="", + Remediation=Remediation( + Code=Code(CLI="", NativeIaC="", Other="", Terraform=""), + Recommendation=Recommendation( + Text="To remediate this issue, ensure that there are no potential enumeration threats in CloudTrail.", + Url="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-logging-data-events", + ), + ), + Categories=["threat-detection"], + DependsOn=[], + RelatedTo=[], + Notes="", + Compliance=[], + ) + def test_load_checks_to_execute(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( @@ -64,7 +94,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_check_list(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } check_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME] @@ -76,7 +106,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_severities(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -88,7 +118,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_severities_and_services(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -104,7 +134,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = ["ec2"] severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -120,7 +150,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } checks_file = "path/to/test_file" with patch( @@ -137,7 +167,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] @@ -178,7 +208,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } categories = {"internet-exposed"} @@ -190,7 +220,7 @@ class TestCheckLoader: def test_load_checks_to_execute_no_bulk_checks_metadata(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", @@ -221,7 +251,7 @@ class TestCheckLoader: compliance_frameworks = ["soc2_aws"] bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", @@ -248,3 +278,27 @@ class TestCheckLoader: assert {"check1_name", "check2_name"} == update_checks_to_execute_with_aliases( checks_to_execute, check_aliases ) + + def test_threat_detection_category(self): + bulk_checks_metatada = { + CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() + } + categories = {"threat-detection"} + + assert {CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME} == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metatada, + categories=categories, + provider=self.provider, + ) + + def test_discard_threat_detection_checks(self): + bulk_checks_metatada = { + CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() + } + categories = {} + + assert set() == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metatada, + categories=categories, + provider=self.provider, + ) diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index 7cf470c7e6..414de90288 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -32,6 +32,35 @@ mock_metadata = CheckMetadata( Compliance=[], ) +mock_metadata_lambda = CheckMetadata( + Provider="aws", + CheckID="awslambda_function_url_public", + CheckTitle="Check 1", + CheckType=["type1"], + ServiceName="lambda", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + Compliance=[], +) + class TestCheckMetada: @@ -188,6 +217,46 @@ class TestCheckMetada: # Assertions assert result == {"accessanalyzer_enabled"} + @mock.patch("prowler.lib.check.models.load_check_metadata") + @mock.patch("prowler.lib.check.models.recover_checks_from_provider") + def test_list_by_service_lambda(self, mock_recover_checks, mock_load_metadata): + # Mock the return value of recover_checks_from_provider + mock_recover_checks.return_value = [ + ("awslambda_function_url_public", "/path/to/awslambda_function_url_public") + ] + + # Mock the return value of load_check_metadata + mock_load_metadata.return_value = mock_metadata_lambda + + bulk_metadata = CheckMetadata.get_bulk(provider="aws") + + result = CheckMetadata.list( + bulk_checks_metadata=bulk_metadata, service="lambda" + ) + + # Assertions + assert result == {"awslambda_function_url_public"} + + @mock.patch("prowler.lib.check.models.load_check_metadata") + @mock.patch("prowler.lib.check.models.recover_checks_from_provider") + def test_list_by_service_awslambda(self, mock_recover_checks, mock_load_metadata): + # Mock the return value of recover_checks_from_provider + mock_recover_checks.return_value = [ + ("awslambda_function_url_public", "/path/to/awslambda_function_url_public") + ] + + # Mock the return value of load_check_metadata + mock_load_metadata.return_value = mock_metadata_lambda + + bulk_metadata = CheckMetadata.get_bulk(provider="aws") + + result = CheckMetadata.list( + bulk_checks_metadata=bulk_metadata, service="awslambda" + ) + + # Assertions + assert result == {"awslambda_function_url_public"} + @mock.patch("prowler.lib.check.models.load_check_metadata") @mock.patch("prowler.lib.check.models.recover_checks_from_provider") def test_list_by_service_invalid(self, mock_recover_checks, mock_load_metadata): diff --git a/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py b/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py index 827c889b73..8239aa5b70 100644 --- a/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py +++ b/tests/providers/azure/services/containerregistry/containerregistry_not_publicly_accessible/containerregistry_not_publicly_accessible_test.py @@ -57,7 +57,7 @@ class Test_containerregistry_not_publicly_accessible: resource_group="mock_resource_group", sku="Basic", login_server="mock_login_server.azurecr.io", - public_network_access="Enabled", + public_network_access=True, admin_user_enabled=True, network_rule_set=NetworkRuleSet(default_action="Allow"), private_endpoint_connections=[], @@ -131,7 +131,7 @@ class Test_containerregistry_not_publicly_accessible: resource_group="mock_resource_group", sku="Basic", login_server="mock_login_server.azurecr.io", - public_network_access="Enabled", + public_network_access=False, admin_user_enabled=False, network_rule_set=NetworkRuleSet(default_action="Deny"), private_endpoint_connections=[], diff --git a/tests/providers/azure/services/containerregistry/containerregistry_service_test.py b/tests/providers/azure/services/containerregistry/containerregistry_service_test.py index 31ca7cea9a..b6ff17fb16 100644 --- a/tests/providers/azure/services/containerregistry/containerregistry_service_test.py +++ b/tests/providers/azure/services/containerregistry/containerregistry_service_test.py @@ -32,7 +32,7 @@ class TestContainerRegistryService: resource_group="mock_resource_group", sku="Basic", login_server="mock_login_server.azurecr.io", - public_network_access="Enabled", + public_network_access=False, admin_user_enabled=True, network_rule_set=None, private_endpoint_connections=[], @@ -71,7 +71,7 @@ class TestContainerRegistryService: assert registry_info.resource_group == "mock_resource_group" assert registry_info.sku == "Basic" assert registry_info.login_server == "mock_login_server.azurecr.io" - assert registry_info.public_network_access == "Enabled" + assert not registry_info.public_network_access assert registry_info.admin_user_enabled is True assert isinstance(registry_info.monitor_diagnostic_settings, list) diff --git a/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py b/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py index 0f1311cf09..f79e7ad725 100644 --- a/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py +++ b/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py @@ -1,3 +1,4 @@ +import datetime from unittest import mock from tests.providers.gcp.gcp_fixtures import ( @@ -239,7 +240,10 @@ class Test_kms_key_rotation_enabled: project_id=GCP_PROJECT_ID, key_ring=keyring.name, location=keylocation.name, - next_rotation_time="2025-09-01T00:00:00Z", + # Next rotation time of now + 100 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+100) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), members=["user:jane@example.com"], ) ] @@ -296,7 +300,10 @@ class Test_kms_key_rotation_enabled: project_id=GCP_PROJECT_ID, key_ring=keyring.name, location=keylocation.name, - next_rotation_time="2024-09-01T00:00:00Z", + # Next rotation time of now + 30 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+30) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), members=["user:jane@example.com"], ) ] @@ -352,7 +359,10 @@ class Test_kms_key_rotation_enabled: id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", project_id=GCP_PROJECT_ID, rotation_period="8776000s", - next_rotation_time="2025-09-01T00:00:00Z", + # Next rotation time of now + 100 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+100) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), key_ring=keyring.name, location=keylocation.name, members=["user:jane@example.com"], @@ -412,7 +422,10 @@ class Test_kms_key_rotation_enabled: id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", project_id=GCP_PROJECT_ID, rotation_period="8776000s", - next_rotation_time="2024-09-01T00:00:00Z", + # Next rotation time of now + 30 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+30) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), key_ring=keyring.name, location=keylocation.name, members=["user:jane@example.com"], @@ -470,7 +483,10 @@ class Test_kms_key_rotation_enabled: id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", project_id=GCP_PROJECT_ID, rotation_period="7776000s", - next_rotation_time="2025-09-01T00:00:00Z", + # Next rotation time of now + 100 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+100) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), key_ring=keyring.name, location=keylocation.name, members=["user:jane@example.com"], @@ -530,7 +546,10 @@ class Test_kms_key_rotation_enabled: id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", project_id=GCP_PROJECT_ID, rotation_period="7776000s", - next_rotation_time="2024-09-01T00:00:00Z", + # Next rotation time of now + 30 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+30) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), key_ring=keyring.name, location=keylocation.name, members=["user:jane@example.com"], @@ -588,7 +607,10 @@ class Test_kms_key_rotation_enabled: id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", project_id=GCP_PROJECT_ID, rotation_period="7776000s", - next_rotation_time="2025-07-06T22:00:00.561275Z", + # Next rotation time of now + 100 days + next_rotation_time=( + datetime.datetime.now() - datetime.timedelta(days=+100) + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), key_ring=keyring.name, location=keylocation.name, members=["user:jane@example.com"],