mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-06-10 21:42:29 +00:00
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc39f3565b | |||
| 926f449ae6 | |||
| 646668c6ae | |||
| 8e6b92792b | |||
| 65c081ce38 | |||
| 5600131d6a | |||
| dbd271980f | |||
| ff532a899e | |||
| 0c9675ec70 | |||
| d45eda2b2b | |||
| 0abcf80d19 | |||
| c0e10fd395 | |||
| a80e9b26a8 | |||
| 2a9cd57fb8 | |||
| a0ad1a5f49 | |||
| dff22dd166 | |||
| 58138810b9 | |||
| 1ecc272fe4 | |||
| b784167006 | |||
| cf0ec8dea0 | |||
| 96cae5e961 | |||
| a48e5cb15f | |||
| 5a9ff007e0 | |||
| 24c45f894c | |||
| 5d03c85629 | |||
| 41dc397a7a | |||
| 237a9adce9 | |||
| a06167f1c2 | |||
| a7d58c40dd | |||
| e260c46389 | |||
| 115169a596 | |||
| 5b19173c1d | |||
| d3dd1644e6 | |||
| 8ff0c59964 | |||
| 285939c389 | |||
| a62ae8af51 | |||
| 5d78b9e439 | |||
| 1056c270ca | |||
| eeef6600b7 | |||
| e142f17abe | |||
| a65d858dac | |||
| 6235a1ba41 | |||
| 05007d03ee | |||
| 102d099947 | |||
| 3194675a5c | |||
| 14e6e4aa68 | |||
| b24c3665b5 | |||
| 1f60878867 | |||
| 2dd18662d8 | |||
| 175360dbe6 | |||
| 80e24b971f | |||
| 78877c470a | |||
| 9c9b100359 | |||
| 10f3232294 | |||
| a2e5f70f36 | |||
| 8d8b31c757 | |||
| cba1e718b9 | |||
| 6c3c37fc26 | |||
| b610cacd0c | |||
| 027a5705cb | |||
| b7fbfb4360 | |||
| 5acf0a7e3d | |||
| 3a25e86e30 | |||
| 50f1592eb3 | |||
| 0f2927cb88 | |||
| b4e1434052 | |||
| 43710783f9 | |||
| 16f767e7b9 | |||
| 42818217a0 | |||
| 13405594b2 | |||
| b5a3852334 | |||
| 181ff1acb3 | |||
| 91e59a3279 | |||
| b3fad1a765 | |||
| 6f90927a79 | |||
| 0bb4a9a3e9 | |||
| 80d9cde60b | |||
| 11196c2f83 | |||
| 55a0d0a1b5 | |||
| 4e5e1d7bd4 | |||
| 06a0a434ab | |||
| 153833fc55 | |||
| fc4877975f | |||
| 0797efd4fd | |||
| fbec99a0b7 | |||
| b2cb1de95e | |||
| 190c2316d7 | |||
| f6c352281a | |||
| 66dfe89936 | |||
| 8b3942ca49 | |||
| 9d35213bd5 | |||
| 3e586e615d | |||
| a4f950e093 | |||
| 7c2441f6ff | |||
| 0a92af3eb2 | |||
| 666f3a0e20 | |||
| 06ef98b5cc | |||
| 79125bdd40 | |||
| e8e8b085ac | |||
| c9b81d003a | |||
| 23fa3c1e38 | |||
| 03fbd0baca | |||
| d4fe24ef47 | |||
| 9c5220ee98 | |||
| 6491bce5a6 | |||
| ca375dd79c | |||
| e807573b54 | |||
| c0f4c9743f | |||
| 5974d0b5da | |||
| 6244a8a5f7 | |||
| 5b9dae4529 | |||
| a424374c44 | |||
| b7fc2542e8 | |||
| 83a1598a1e | |||
| b22b56a06b | |||
| 5020e4713c | |||
| ee534a740e | |||
| 48cb45b7a8 | |||
| 91b74822e9 | |||
| 287eef5085 | |||
| 45d359c84a | |||
| 6049e5d4e8 | |||
| dfd377f89e | |||
| 37e6c52c14 | |||
| d6a7f4d88f | |||
| 239cda0a90 | |||
| 4a821e425b | |||
| e1a2f0c204 | |||
| c70860c733 | |||
| 05e71e033f | |||
| 5164ec2eb9 | |||
| be18dac4f9 | |||
| bb126c242f | |||
| e27780a856 | |||
| 196ec51751 | |||
| 86abf9e64c | |||
| 9d8be578e3 | |||
| b3aa800082 | |||
| 501674a778 | |||
| 5ff6ae79d8 | |||
| e518a869ab | |||
| 43927a62f3 | |||
| 335980c8d8 | |||
| ca3ee378db | |||
| c05bc1068a | |||
| 2e3164636d | |||
| c34e07fc40 | |||
| 6022122a61 | |||
| f65f5e4b46 | |||
| dee17733a0 | |||
| cddda1e64e | |||
| f7b873db03 | |||
| 792bc70d0a | |||
| 185491b061 | |||
| 3af8a43480 | |||
| fd78406b29 | |||
| 4758b258a3 | |||
| 015e2b3b88 | |||
| e184c9cb61 | |||
| 9004a01183 | |||
| dd65ba3d9e | |||
| bba616a18f | |||
| aa0f8d2981 | |||
| 2511d6ffa9 | |||
| 27329457be | |||
| 7189f3d526 | |||
| 58e7589c9d | |||
| d60f4b5ded | |||
| 4c2ec094f6 | |||
| 395ecaff5b | |||
| c39506ef7d | |||
| eb90d479e2 | |||
| b92a73f5ea | |||
| ad121f3059 | |||
| 70e4c5a44e | |||
| b5a46b7b59 | |||
| f1a97cd166 | |||
| 0774508093 | |||
| 0664ce6b94 | |||
| 407c779c52 | |||
| c60f13f23f | |||
| 37d912ef01 | |||
| d3de89c017 | |||
| cb22af25c6 | |||
| a534b94495 | |||
| 6262b4ff0b | |||
| 84ecd7ab2c | |||
| 1a5428445a | |||
| ac8e991ca0 | |||
| 83a0331472 | |||
| cce31e2971 | |||
| 0adf7d6e77 | |||
| 295f8b557e | |||
| bb2c5c3161 | |||
| 0018f36a36 | |||
| 857de84f49 | |||
| 9630f2242a | |||
| 1fe125867c | |||
| 0737893240 | |||
| 282fe3d348 | |||
| b5d83640ae | |||
| 2823d3ad21 | |||
| 00b93bfe86 | |||
| 84c253d887 |
@@ -3,7 +3,7 @@
|
||||
# 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"
|
||||
PROWLER_UI_VERSION="stable"
|
||||
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
|
||||
@@ -30,6 +30,30 @@ VALKEY_HOST=valkey
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_DB=0
|
||||
|
||||
# API scan settings
|
||||
|
||||
# The path to the directory where scan output should be stored
|
||||
DJANGO_TMP_OUTPUT_DIRECTORY = "/tmp/prowler_api_output"
|
||||
|
||||
# The maximum number of findings to process in a single batch
|
||||
DJANGO_FINDINGS_BATCH_SIZE = 1000
|
||||
|
||||
# The AWS access key to be used when uploading scan output to an S3 bucket
|
||||
# If left empty, default AWS credentials resolution behavior will be used
|
||||
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID=""
|
||||
|
||||
# The AWS secret key to be used when uploading scan output to an S3 bucket
|
||||
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY=""
|
||||
|
||||
# An optional AWS session token
|
||||
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN=""
|
||||
|
||||
# The AWS region where your S3 bucket is located (e.g., "us-east-1")
|
||||
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION=""
|
||||
|
||||
# The name of the S3 bucket where scan output should be stored
|
||||
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET=""
|
||||
|
||||
# Django settings
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
|
||||
DJANGO_BIND_ADDRESS=0.0.0.0
|
||||
@@ -92,7 +116,11 @@ jQIDAQAB
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400
|
||||
DJANGO_SENTRY_DSN=
|
||||
|
||||
# Sentry settings
|
||||
SENTRY_ENVIRONMENT=local
|
||||
SENTRY_RELEASE=local
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.4.0
|
||||
|
||||
@@ -92,3 +92,13 @@ component/api:
|
||||
component/ui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "ui/**"
|
||||
|
||||
compliance:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/compliance/**"
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/compliance/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/compliance/**"
|
||||
|
||||
review-django-migrations:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "api/src/backend/api/migrations/**"
|
||||
|
||||
@@ -17,6 +17,11 @@ Please include a summary of the change and which issue is fixed. List any depend
|
||||
- [ ] Review if backport is needed.
|
||||
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
|
||||
|
||||
#### API
|
||||
- [ ] Verify if API specs need to be regenerated.
|
||||
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
|
||||
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
|
||||
@@ -63,6 +63,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
shortSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -82,6 +88,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.SHORT_SHA }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -96,3 +103,12 @@ jobs:
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Trigger deployment
|
||||
if: github.event_name == 'push'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
event-type: prowler-api-deploy
|
||||
client-payload: '{"sha": "${{ github.sha }}", "short_sha": "${{ env.SHORT_SHA }}"}'
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- ".github/workflows/api-pull-request.yml"
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -14,7 +15,6 @@ on:
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
|
||||
env:
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
@@ -26,7 +26,8 @@ env:
|
||||
VALKEY_HOST: localhost
|
||||
VALKEY_PORT: 6379
|
||||
VALKEY_DB: 0
|
||||
|
||||
API_WORKING_DIR: ./api
|
||||
IMAGE_NAME: prowler-api
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -171,3 +172,18 @@ jobs:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
flags: api
|
||||
test-container-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Container
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.API_WORKING_DIR }}
|
||||
push: false
|
||||
tags: ${{ env.IMAGE_NAME }}:latest
|
||||
outputs: type=docker
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Prowler - Conventional Commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- "opened"
|
||||
- "edited"
|
||||
- "synchronize"
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
|
||||
jobs:
|
||||
conventional-commit-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: conventional-commit-check
|
||||
id: conventional-commit-check
|
||||
uses: agenthunt/conventional-commit-checker-action@v2.0.0
|
||||
with:
|
||||
pr-title-regex: '^([^\s(]+)(?:\(([^)]+)\))?: (.+)'
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.88.2
|
||||
uses: trufflesecurity/trufflehog@v3.88.14
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -39,6 +39,8 @@ jobs:
|
||||
.backportrc.json
|
||||
.env
|
||||
docker-compose*
|
||||
examples/**
|
||||
.gitignore
|
||||
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
|
||||
@@ -63,6 +63,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
shortSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -78,10 +84,13 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ env.SHORT_SHA }}
|
||||
# Set push: false for testing
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.SHORT_SHA }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -90,9 +99,20 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${{ env.RELEASE_TAG }}
|
||||
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
|
||||
|
||||
- name: Trigger deployment
|
||||
if: github.event_name == 'push'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
event-type: prowler-ui-deploy
|
||||
client-payload: '{"sha": "${{ github.sha }}", "short_sha": "${{ env.SHORT_SHA }}"}'
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- ".github/workflows/ui-pull-request.yml"
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -13,6 +14,9 @@ on:
|
||||
- "v5.*"
|
||||
paths:
|
||||
- 'ui/**'
|
||||
env:
|
||||
UI_WORKING_DIR: ./ui
|
||||
IMAGE_NAME: prowler-ui
|
||||
|
||||
jobs:
|
||||
test-and-coverage:
|
||||
@@ -39,3 +43,20 @@ jobs:
|
||||
- name: Build the application
|
||||
working-directory: ./ui
|
||||
run: npm run build
|
||||
test-container-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Container
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.UI_WORKING_DIR }}
|
||||
# Always build using `prod` target
|
||||
target: prod
|
||||
push: false
|
||||
tags: ${{ env.IMAGE_NAME }}:latest
|
||||
outputs: type=docker
|
||||
build-args: |
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ tags
|
||||
*.DS_Store
|
||||
|
||||
# Prowler output
|
||||
output/
|
||||
/output
|
||||
|
||||
# Prowler found secrets
|
||||
secrets-*/
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.8-alpine3.20
|
||||
FROM python:3.12.9-alpine3.20
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
|
||||
@@ -71,10 +71,13 @@ 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`|
|
||||
| Azure | 139 | 18 -> `prowler azure --list-services` | 5 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 2 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
| AWS | 564 | 82 | 33 | 10 |
|
||||
| GCP | 77 | 13 | 5 | 3 |
|
||||
| Azure | 140 | 18 | 6 | 3 |
|
||||
| Kubernetes | 83 | 7 | 2 | 7 |
|
||||
| Microsoft365 | 5 | 2 | 1 | 0 |
|
||||
|
||||
> You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
@@ -116,7 +119,7 @@ docker compose up -d
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler/api
|
||||
poetry install
|
||||
poetry shell
|
||||
eval $(poetry env activate)
|
||||
set -a
|
||||
source .env
|
||||
docker compose up postgres valkey -d
|
||||
@@ -124,6 +127,11 @@ cd src/backend
|
||||
python manage.py migrate --database admin
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
@@ -133,7 +141,7 @@ gunicorn -c config/guniconf.py config.wsgi:application
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler/api
|
||||
poetry install
|
||||
poetry shell
|
||||
eval $(poetry env activate)
|
||||
set -a
|
||||
source .env
|
||||
cd src/backend
|
||||
@@ -146,7 +154,7 @@ python -m celery -A config.celery worker -l info -E
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler/api
|
||||
poetry install
|
||||
poetry shell
|
||||
eval $(poetry env activate)
|
||||
set -a
|
||||
source .env
|
||||
cd src/backend
|
||||
@@ -167,7 +175,7 @@ npm start
|
||||
|
||||
## 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:
|
||||
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python > 3.9.1, < 3.13:
|
||||
|
||||
```console
|
||||
pip install prowler
|
||||
@@ -197,15 +205,21 @@ The container images are available here:
|
||||
|
||||
### From GitHub
|
||||
|
||||
Python >= 3.9, < 3.13 is required with pip and poetry:
|
||||
Python > 3.9.1, < 3.13 is required with pip and poetry:
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler
|
||||
poetry shell
|
||||
eval $(poetry env activate)
|
||||
poetry install
|
||||
python prowler.py -v
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
> If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
|
||||
# 📐✏️ High level architecture
|
||||
|
||||
|
||||
+14
-4
@@ -23,10 +23,7 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000
|
||||
DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400
|
||||
DJANGO_SENTRY_DSN=
|
||||
|
||||
# PostgreSQL settings
|
||||
# If running django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
@@ -43,3 +40,16 @@ POSTGRES_DB=prowler_db
|
||||
VALKEY_HOST=[localhost|valkey]
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_DB=0
|
||||
|
||||
# Sentry settings
|
||||
SENTRY_ENVIRONMENT=local
|
||||
SENTRY_RELEASE=local
|
||||
|
||||
# Social login credentials
|
||||
DJANGO_GOOGLE_OAUTH_CLIENT_ID=""
|
||||
DJANGO_GOOGLE_OAUTH_CLIENT_SECRET=""
|
||||
DJANGO_GOOGLE_OAUTH_CALLBACK_URL=""
|
||||
|
||||
DJANGO_GITHUB_OAUTH_CLIENT_ID=""
|
||||
DJANGO_GITHUB_OAUTH_CLIENT_SECRET=""
|
||||
DJANGO_GITHUB_OAUTH_CALLBACK_URL=""
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Prowler API Changelog
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v1.5.0] (Prowler v5.4.0)
|
||||
|
||||
### Added
|
||||
- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906)
|
||||
- Add API scan report system, now all scans launched from the API will generate a compressed file with the report in OCSF, CSV and HTML formats [(#6878)](https://github.com/prowler-cloud/prowler/pull/6878).
|
||||
- Configurable Sentry integration [(#6874)](https://github.com/prowler-cloud/prowler/pull/6874)
|
||||
|
||||
### Changed
|
||||
- Optimized `GET /findings` endpoint to improve response time and size [(#7019)](https://github.com/prowler-cloud/prowler/pull/7019).
|
||||
|
||||
---
|
||||
|
||||
## [v1.4.0] (Prowler v5.3.0)
|
||||
|
||||
### Changed
|
||||
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700).
|
||||
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800).
|
||||
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863).
|
||||
- Increase the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869).
|
||||
|
||||
---
|
||||
@@ -269,3 +269,66 @@ poetry shell
|
||||
cd src/backend
|
||||
pytest
|
||||
```
|
||||
|
||||
# Custom commands
|
||||
|
||||
Django provides a way to create custom commands that can be run from the command line.
|
||||
|
||||
> These commands can be found in: ```prowler/api/src/backend/api/management/commands```
|
||||
|
||||
To run a custom command, you need to be in the `prowler/api/src/backend` directory and run:
|
||||
|
||||
```console
|
||||
poetry shell
|
||||
python manage.py <command_name>
|
||||
```
|
||||
|
||||
## Generate dummy data
|
||||
|
||||
```console
|
||||
python manage.py findings --tenant
|
||||
<TENANT_ID> --findings <NUM_FINDINGS> --re
|
||||
sources <NUM_RESOURCES> --batch <TRANSACTION_BATCH_SIZE> --alias <ALIAS>
|
||||
```
|
||||
|
||||
This command creates, for a given tenant, a provider, scan and a set of findings and resources related altogether.
|
||||
|
||||
> Scan progress and state are updated in real time.
|
||||
> - 0-33%: Create resources.
|
||||
> - 33-66%: Create findings.
|
||||
> - 66%: Create resource-finding mapping.
|
||||
>
|
||||
> The last step is required to access the findings details, since the UI needs that to print all the information.
|
||||
|
||||
### Example
|
||||
|
||||
```console
|
||||
~/backend $ poetry run python manage.py findings --tenant
|
||||
fffb1893-3fc7-4623-a5d9-fae47da1c528 --findings 25000 --re
|
||||
sources 1000 --batch 5000 --alias test-script
|
||||
|
||||
Starting data population
|
||||
Tenant: fffb1893-3fc7-4623-a5d9-fae47da1c528
|
||||
Alias: test-script
|
||||
Resources: 1000
|
||||
Findings: 25000
|
||||
Batch size: 5000
|
||||
|
||||
|
||||
Creating resources...
|
||||
100%|███████████████████████| 1/1 [00:00<00:00, 7.72it/s]
|
||||
Resources created successfully.
|
||||
|
||||
|
||||
Creating findings...
|
||||
100%|███████████████████████| 5/5 [00:05<00:00, 1.09s/it]
|
||||
Findings created successfully.
|
||||
|
||||
|
||||
Creating resource-finding mappings...
|
||||
100%|███████████████████████| 5/5 [00:02<00:00, 1.81it/s]
|
||||
Resource-finding mappings created successfully.
|
||||
|
||||
|
||||
Successfully populated test data.
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ start_prod_server() {
|
||||
|
||||
start_worker() {
|
||||
echo "Starting the worker..."
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion -E --max-tasks-per-child 1
|
||||
}
|
||||
|
||||
start_worker_beat() {
|
||||
|
||||
Generated
+892
-809
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -8,10 +8,11 @@ description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
name = "prowler-api"
|
||||
package-mode = false
|
||||
version = "1.3.0"
|
||||
version = "1.5.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
celery = {extras = ["pytest"], version = "^5.4.0"}
|
||||
dj-rest-auth = {extras = ["with_social", "jwt"], version = "7.0.1"}
|
||||
django = "5.1.5"
|
||||
django-celery-beat = "^2.7.0"
|
||||
django-celery-results = "^2.5.1"
|
||||
@@ -27,16 +28,18 @@ drf-nested-routers = "^0.94.1"
|
||||
drf-spectacular = "0.27.2"
|
||||
drf-spectacular-jsonapi = "0.5.1"
|
||||
gunicorn = "23.0.0"
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "v5.2"}
|
||||
psycopg = {extras = ["pool", "binary"], version = "3.2.3"}
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "v5.4"}
|
||||
psycopg2-binary = "2.9.9"
|
||||
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
|
||||
# Needed for prowler compatibility
|
||||
python = ">=3.11,<3.13"
|
||||
sentry-sdk = {extras = ["django"], version = "^2.20.0"}
|
||||
uuid6 = "2024.7.10"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
bandit = "1.7.9"
|
||||
coverage = "7.5.4"
|
||||
django-silk = "5.3.2"
|
||||
docker = "7.1.0"
|
||||
freezegun = "1.5.1"
|
||||
mypy = "1.10.1"
|
||||
@@ -49,6 +52,7 @@ pytest-randomly = "3.15.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
ruff = "0.5.0"
|
||||
safety = "3.2.9"
|
||||
tqdm = "4.67.1"
|
||||
vulture = "2.14"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from django.db import transaction
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Membership, Role, Tenant, User, UserRoleRelationship
|
||||
|
||||
|
||||
class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
@staticmethod
|
||||
def get_user_by_email(email: str):
|
||||
try:
|
||||
return User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def pre_social_login(self, request, sociallogin):
|
||||
# Link existing accounts with the same email address
|
||||
email = sociallogin.account.extra_data.get("email")
|
||||
if email:
|
||||
existing_user = self.get_user_by_email(email)
|
||||
if existing_user:
|
||||
sociallogin.connect(request, existing_user)
|
||||
|
||||
def save_user(self, request, sociallogin, form=None):
|
||||
"""
|
||||
Called after the user data is fully populated from the provider
|
||||
and is about to be saved to the DB for the first time.
|
||||
"""
|
||||
with transaction.atomic(using=MainRouter.admin_db):
|
||||
user = super().save_user(request, sociallogin, form)
|
||||
user.save(using=MainRouter.admin_db)
|
||||
|
||||
tenant = Tenant.objects.using(MainRouter.admin_db).create(
|
||||
name=f"{user.email.split('@')[0]} default tenant"
|
||||
)
|
||||
with rls_transaction(str(tenant.id)):
|
||||
Membership.objects.using(MainRouter.admin_db).create(
|
||||
user=user, tenant=tenant, role=Membership.RoleChoices.OWNER
|
||||
)
|
||||
role = Role.objects.using(MainRouter.admin_db).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.using(MainRouter.admin_db).create(
|
||||
user=user,
|
||||
role=role,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return user
|
||||
@@ -1,18 +1,29 @@
|
||||
ALLOWED_APPS = ("django", "socialaccount", "account", "authtoken", "silk")
|
||||
|
||||
|
||||
class MainRouter:
|
||||
default_db = "default"
|
||||
admin_db = "admin"
|
||||
|
||||
def db_for_read(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_"):
|
||||
if model_table_name.startswith("django_") or any(
|
||||
model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS
|
||||
):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
def db_for_write(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_"):
|
||||
if any(model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: F841
|
||||
return db == self.admin_db
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints): # noqa: F841
|
||||
# Allow relations if both objects are in either "default" or "admin" db connectors
|
||||
if {obj1._state.db, obj2._state.db} <= {self.default_db, self.admin_db}:
|
||||
return True
|
||||
return None
|
||||
|
||||
@@ -6,10 +6,8 @@ from datetime import datetime, timedelta, timezone
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
from django.db import connection, models, transaction
|
||||
from psycopg import connect as psycopg_connect
|
||||
from psycopg.adapt import Dumper
|
||||
from psycopg.types import TypeInfo
|
||||
from psycopg.types.string import TextLoader
|
||||
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"
|
||||
@@ -22,7 +20,6 @@ DB_PROWLER_USER = (
|
||||
DB_PROWLER_PASSWORD = (
|
||||
settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test"
|
||||
)
|
||||
|
||||
TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
|
||||
POSTGRES_TENANT_VAR = "api.tenant_id"
|
||||
POSTGRES_USER_VAR = "api.user_id"
|
||||
@@ -32,25 +29,21 @@ SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
|
||||
|
||||
@contextmanager
|
||||
def psycopg_connection(database_alias: str):
|
||||
"""
|
||||
Context manager returning a psycopg 3 connection
|
||||
for the specified 'database_alias' in Django settings.
|
||||
"""
|
||||
pg_conn = None
|
||||
psycopg2_connection = None
|
||||
try:
|
||||
admin_db = settings.DATABASES[database_alias]
|
||||
|
||||
pg_conn = psycopg_connect(
|
||||
psycopg2_connection = psycopg2_connect(
|
||||
dbname=admin_db["NAME"],
|
||||
user=admin_db["USER"],
|
||||
password=admin_db["PASSWORD"],
|
||||
host=admin_db["HOST"],
|
||||
port=admin_db["PORT"],
|
||||
)
|
||||
yield pg_conn
|
||||
yield psycopg2_connection
|
||||
finally:
|
||||
if pg_conn is not None:
|
||||
pg_conn.close()
|
||||
if psycopg2_connection is not None:
|
||||
psycopg2_connection.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -66,7 +59,7 @@ def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Just in case the value is a UUID object
|
||||
# just in case the value is an UUID object
|
||||
uuid.UUID(str(value))
|
||||
except ValueError:
|
||||
raise ValidationError("Must be a valid UUID")
|
||||
@@ -194,24 +187,32 @@ class EnumType:
|
||||
return self.value
|
||||
|
||||
|
||||
def register_enum(apps, schema_editor, enum_class):
|
||||
"""
|
||||
psycopg 3 approach: register a loader + dumper for the given enum_class,
|
||||
so we can read/write the custom Postgres ENUM seamlessly.
|
||||
"""
|
||||
with psycopg_connection(schema_editor.connection.alias) as conn:
|
||||
ti = TypeInfo.fetch(conn, enum_class.enum_type_name)
|
||||
def enum_adapter(enum_obj):
|
||||
return AsIs(f"'{enum_obj.value}'::{enum_obj.__class__.enum_type_name}")
|
||||
|
||||
class EnumLoader(TextLoader):
|
||||
def load(self, data):
|
||||
return data
|
||||
|
||||
class EnumDumper(Dumper):
|
||||
def dump(self, obj):
|
||||
return f"'{obj.value}'::{obj.__class__.enum_type_name}"
|
||||
def get_enum_oid(connection, enum_type_name: str):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT oid FROM pg_type WHERE typname = %s;", (enum_type_name,))
|
||||
result = cursor.fetchone()
|
||||
if result is None:
|
||||
raise ValueError(f"Enum type '{enum_type_name}' not found")
|
||||
return result[0]
|
||||
|
||||
conn.adapters.register_loader(ti.oid, EnumLoader)
|
||||
conn.adapters.register_dumper(enum_class, EnumDumper)
|
||||
|
||||
def register_enum(apps, schema_editor, enum_class): # noqa: F841
|
||||
with psycopg_connection(schema_editor.connection.alias) as connection:
|
||||
enum_oid = get_enum_oid(connection, enum_class.enum_type_name)
|
||||
enum_instance = new_type(
|
||||
(enum_oid,),
|
||||
enum_class.enum_type_name,
|
||||
lambda value, cur: value, # noqa: F841
|
||||
)
|
||||
register_type(enum_instance, connection)
|
||||
register_adapter(enum_class, enum_adapter)
|
||||
|
||||
|
||||
# Postgres enum definition for member role
|
||||
|
||||
|
||||
class MemberRoleEnum(EnumType):
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework_json_api.serializers import ValidationError
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
|
||||
|
||||
def set_tenant(func):
|
||||
def set_tenant(func=None, *, keep_tenant=False):
|
||||
"""
|
||||
Decorator to set the tenant context for a Celery task based on the provided tenant_id.
|
||||
|
||||
@@ -40,20 +40,29 @@ def set_tenant(func):
|
||||
# The tenant context will be set before the task logic executes.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
@transaction.atomic
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
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])
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
@transaction.atomic
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
if not keep_tenant:
|
||||
tenant_id = kwargs.pop("tenant_id")
|
||||
else:
|
||||
tenant_id = kwargs["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])
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
if func is None:
|
||||
return decorator
|
||||
else:
|
||||
return decorator(func)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
@@ -319,34 +319,41 @@ class FindingFilter(FilterSet):
|
||||
field_name="resources__type", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_tag_key = CharFilter(field_name="resources__tags__key")
|
||||
resource_tag_key__in = CharInFilter(
|
||||
field_name="resources__tags__key", lookup_expr="in"
|
||||
)
|
||||
resource_tag_key__icontains = CharFilter(
|
||||
field_name="resources__tags__key", lookup_expr="icontains"
|
||||
)
|
||||
resource_tag_value = CharFilter(field_name="resources__tags__value")
|
||||
resource_tag_value__in = CharInFilter(
|
||||
field_name="resources__tags__value", lookup_expr="in"
|
||||
)
|
||||
resource_tag_value__icontains = CharFilter(
|
||||
field_name="resources__tags__value", lookup_expr="icontains"
|
||||
)
|
||||
resource_tags = CharInFilter(
|
||||
method="filter_resource_tag",
|
||||
lookup_expr="in",
|
||||
help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be "
|
||||
"separated by commas.",
|
||||
)
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# resource_tag_key = CharFilter(field_name="resources__tags__key")
|
||||
# resource_tag_key__in = CharInFilter(
|
||||
# field_name="resources__tags__key", lookup_expr="in"
|
||||
# )
|
||||
# resource_tag_key__icontains = CharFilter(
|
||||
# field_name="resources__tags__key", lookup_expr="icontains"
|
||||
# )
|
||||
# resource_tag_value = CharFilter(field_name="resources__tags__value")
|
||||
# resource_tag_value__in = CharInFilter(
|
||||
# field_name="resources__tags__value", lookup_expr="in"
|
||||
# )
|
||||
# resource_tag_value__icontains = CharFilter(
|
||||
# field_name="resources__tags__value", lookup_expr="icontains"
|
||||
# )
|
||||
# resource_tags = CharInFilter(
|
||||
# method="filter_resource_tag",
|
||||
# lookup_expr="in",
|
||||
# help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be "
|
||||
# "separated by commas.",
|
||||
# )
|
||||
|
||||
scan = UUIDFilter(method="filter_scan_id")
|
||||
scan__in = UUIDInFilter(method="filter_scan_id_in")
|
||||
|
||||
inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__gte = DateFilter(method="filter_inserted_at_gte")
|
||||
inserted_at__lte = DateFilter(method="filter_inserted_at_lte")
|
||||
inserted_at__gte = DateFilter(
|
||||
method="filter_inserted_at_gte",
|
||||
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
)
|
||||
inserted_at__lte = DateFilter(
|
||||
method="filter_inserted_at_lte",
|
||||
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Finding
|
||||
@@ -374,11 +381,51 @@ class FindingFilter(FilterSet):
|
||||
},
|
||||
}
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
# Force distinct results to prevent duplicates with many-to-many relationships
|
||||
parent_qs = super().qs
|
||||
return parent_qs.distinct()
|
||||
def filter_queryset(self, queryset):
|
||||
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
|
||||
self.data.get("inserted_at")
|
||||
or self.data.get("inserted_at__date")
|
||||
or self.data.get("inserted_at__gte")
|
||||
or self.data.get("inserted_at__lte")
|
||||
):
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], "
|
||||
"or filter[inserted_at.lte].",
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/attributes/inserted_at"},
|
||||
"code": "required",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
gte_date = (
|
||||
datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date()
|
||||
if self.data.get("inserted_at__gte")
|
||||
else datetime.now(timezone.utc).date()
|
||||
)
|
||||
lte_date = (
|
||||
datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date()
|
||||
if self.data.get("inserted_at__lte")
|
||||
else datetime.now(timezone.utc).date()
|
||||
)
|
||||
|
||||
if abs(lte_date - gte_date) > timedelta(
|
||||
days=settings.FINDINGS_MAX_DAYS_IN_RANGE
|
||||
):
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/attributes/inserted_at"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
# Convert filter values to UUIDv7 values for use with partitioning
|
||||
def filter_scan_id(self, queryset, name, value):
|
||||
@@ -400,9 +447,7 @@ class FindingFilter(FilterSet):
|
||||
)
|
||||
|
||||
return (
|
||||
queryset.filter(id__gte=start)
|
||||
.filter(id__lt=end)
|
||||
.filter(scan__id=value_uuid)
|
||||
queryset.filter(id__gte=start).filter(id__lt=end).filter(scan_id=value_uuid)
|
||||
)
|
||||
|
||||
def filter_scan_id_in(self, queryset, name, value):
|
||||
@@ -427,31 +472,32 @@ class FindingFilter(FilterSet):
|
||||
]
|
||||
)
|
||||
if start == end:
|
||||
return queryset.filter(id__gte=start).filter(scan__id__in=uuid_list)
|
||||
return queryset.filter(id__gte=start).filter(scan_id__in=uuid_list)
|
||||
else:
|
||||
return (
|
||||
queryset.filter(id__gte=start)
|
||||
.filter(id__lt=end)
|
||||
.filter(scan__id__in=uuid_list)
|
||||
.filter(scan_id__in=uuid_list)
|
||||
)
|
||||
|
||||
def filter_inserted_at(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(value))
|
||||
datetime_value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(datetime_value))
|
||||
end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1)))
|
||||
|
||||
return queryset.filter(id__gte=start).filter(inserted_at__date=value)
|
||||
return queryset.filter(id__gte=start, id__lt=end)
|
||||
|
||||
def filter_inserted_at_gte(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(value))
|
||||
datetime_value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(datetime_value))
|
||||
|
||||
return queryset.filter(id__gte=start).filter(inserted_at__gte=value)
|
||||
return queryset.filter(id__gte=start)
|
||||
|
||||
def filter_inserted_at_lte(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
end = uuid7_start(datetime_to_uuid7(value))
|
||||
datetime_value = self.maybe_date_to_datetime(value)
|
||||
end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1)))
|
||||
|
||||
return queryset.filter(id__lte=end).filter(inserted_at__lte=value)
|
||||
return queryset.filter(id__lt=end)
|
||||
|
||||
def filter_resource_tag(self, queryset, name, value):
|
||||
overall_query = Q()
|
||||
|
||||
@@ -122,6 +122,22 @@
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:45:26.352Z",
|
||||
"updated_at": "2024-10-18T11:16:23.533Z",
|
||||
"provider": "kubernetes",
|
||||
"uid": "gke_lucky-coast-419309_us-central1_autopilot-cluster-2",
|
||||
"alias": "k8s_testing_2",
|
||||
"connected": true,
|
||||
"connection_last_checked_at": "2024-10-18T11:16:23.503Z",
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "11491b47-75ae-4f71-ad8d-3e630a72182e",
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 5,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-01T17:25:27.050Z",
|
||||
"started_at": "2024-09-01T17:25:27.050Z",
|
||||
@@ -33,9 +31,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 20,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T17:24:27.050Z",
|
||||
"started_at": "2024-09-02T17:24:27.050Z",
|
||||
@@ -55,9 +51,7 @@
|
||||
"unique_resource_count": 10,
|
||||
"duration": 10,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudsql_instance_automated_backups"
|
||||
]
|
||||
"checks_to_execute": ["cloudsql_instance_automated_backups"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:26:27.050Z",
|
||||
"started_at": "2024-09-02T19:26:27.050Z",
|
||||
@@ -77,9 +71,7 @@
|
||||
"unique_resource_count": 1,
|
||||
"duration": 35,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:27:27.050Z",
|
||||
"started_at": "2024-09-02T19:27:27.050Z",
|
||||
@@ -97,9 +89,7 @@
|
||||
"name": "test scheduled aws scan",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudformation_stack_outputs_find_secrets"
|
||||
]
|
||||
"checks_to_execute": ["cloudformation_stack_outputs_find_secrets"]
|
||||
},
|
||||
"scheduled_at": "2030-09-02T19:20:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:24:27.050Z",
|
||||
@@ -178,9 +168,7 @@
|
||||
"unique_resource_count": 19,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
"checks_to_execute": ["accessanalyzer_enabled"]
|
||||
},
|
||||
"duration": 7,
|
||||
"scheduled_at": null,
|
||||
@@ -190,6 +178,56 @@
|
||||
"completed_at": "2024-10-18T10:46:05.127Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "6dd8925f-a52d-48de-a546-d2d90db30ab1",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan azure",
|
||||
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 20,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"account_security_contact_information_is_registered"
|
||||
]
|
||||
},
|
||||
"duration": 4,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T11:16:21.358Z",
|
||||
"updated_at": "2024-10-18T11:16:26.060Z",
|
||||
"started_at": "2024-10-18T11:16:21.593Z",
|
||||
"completed_at": "2024-10-18T11:16:26.060Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "4ca7ce89-3236-41a8-a369-8937bc152af5",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan k8s",
|
||||
"provider": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 20,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"account_security_contact_information_is_registered"
|
||||
]
|
||||
},
|
||||
"duration": 4,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T11:16:21.358Z",
|
||||
"updated_at": "2024-10-18T11:16:26.060Z",
|
||||
"started_at": "2024-10-18T11:16:21.593Z",
|
||||
"completed_at": "2024-10-18T11:16:26.060Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01929f57-c0ee-7553-be0b-cbde006fb6f7",
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import random
|
||||
from datetime import datetime, timezone
|
||||
from math import ceil
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from tqdm import tqdm
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
Finding,
|
||||
Provider,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
Scan,
|
||||
StatusChoices,
|
||||
)
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Populates the database with test data for performance testing."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--tenant",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Tenant id for which the data will be populated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resources",
|
||||
type=int,
|
||||
required=True,
|
||||
help="The number of resources to create.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--findings",
|
||||
type=int,
|
||||
required=True,
|
||||
help="The number of findings to create.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch", type=int, required=True, help="The batch size for bulk creation."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alias",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Optional alias for the provider and scan",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
tenant_id = options["tenant"]
|
||||
num_resources = options["resources"]
|
||||
num_findings = options["findings"]
|
||||
batch_size = options["batch"]
|
||||
alias = options["alias"] or "Testing"
|
||||
uid_token = str(uuid4())
|
||||
|
||||
self.stdout.write(self.style.NOTICE("Starting data population"))
|
||||
self.stdout.write(self.style.NOTICE(f"\tTenant: {tenant_id}"))
|
||||
self.stdout.write(self.style.NOTICE(f"\tAlias: {alias}"))
|
||||
self.stdout.write(self.style.NOTICE(f"\tResources: {num_resources}"))
|
||||
self.stdout.write(self.style.NOTICE(f"\tFindings: {num_findings}"))
|
||||
self.stdout.write(self.style.NOTICE(f"\tBatch size: {batch_size}\n\n"))
|
||||
|
||||
# Resource metadata
|
||||
possible_regions = [
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"sa-east-1",
|
||||
]
|
||||
possible_services = []
|
||||
possible_types = []
|
||||
|
||||
bulk_check_metadata = CheckMetadata.get_bulk(provider="aws")
|
||||
for check_metadata in bulk_check_metadata.values():
|
||||
if check_metadata.ServiceName not in possible_services:
|
||||
possible_services.append(check_metadata.ServiceName)
|
||||
if (
|
||||
check_metadata.ResourceType
|
||||
and check_metadata.ResourceType not in possible_types
|
||||
):
|
||||
possible_types.append(check_metadata.ResourceType)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
provider, _ = Provider.all_objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
provider="aws",
|
||||
connected=True,
|
||||
uid=str(random.randint(100000000000, 999999999999)),
|
||||
defaults={
|
||||
"alias": alias,
|
||||
},
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scan = Scan.all_objects.create(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
name=alias,
|
||||
trigger="manual",
|
||||
state="executing",
|
||||
progress=0,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
)
|
||||
scan_state = "completed"
|
||||
|
||||
try:
|
||||
# Create resources
|
||||
resources = []
|
||||
|
||||
for i in range(num_resources):
|
||||
resources.append(
|
||||
Resource(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider.id,
|
||||
uid=f"testing-{uid_token}-{i}",
|
||||
name=f"Testing {uid_token}-{i}",
|
||||
region=random.choice(possible_regions),
|
||||
service=random.choice(possible_services),
|
||||
type=random.choice(possible_types),
|
||||
)
|
||||
)
|
||||
|
||||
num_batches = ceil(len(resources) / batch_size)
|
||||
self.stdout.write(self.style.WARNING("Creating resources..."))
|
||||
for i in tqdm(range(0, len(resources), batch_size), total=num_batches):
|
||||
with rls_transaction(tenant_id):
|
||||
Resource.all_objects.bulk_create(resources[i : i + batch_size])
|
||||
self.stdout.write(self.style.SUCCESS("Resources created successfully.\n\n"))
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scan.progress = 33
|
||||
scan.save()
|
||||
|
||||
# Create Findings
|
||||
findings = []
|
||||
possible_deltas = ["new", "changed", None]
|
||||
possible_severities = ["critical", "high", "medium", "low"]
|
||||
findings_resources_mapping = []
|
||||
|
||||
for i in range(num_findings):
|
||||
severity = random.choice(possible_severities)
|
||||
check_id = random.randint(1, 1000)
|
||||
assigned_resource_num = random.randint(0, len(resources) - 1)
|
||||
assigned_resource = resources[assigned_resource_num]
|
||||
findings_resources_mapping.append(assigned_resource_num)
|
||||
|
||||
findings.append(
|
||||
Finding(
|
||||
tenant_id=tenant_id,
|
||||
scan=scan,
|
||||
uid=f"testing-{uid_token}-{i}",
|
||||
delta=random.choice(possible_deltas),
|
||||
check_id=f"check-{check_id}",
|
||||
status=random.choice(list(StatusChoices)),
|
||||
severity=severity,
|
||||
impact=severity,
|
||||
raw_result={},
|
||||
check_metadata={
|
||||
"checktitle": f"Test title for check {check_id}",
|
||||
"risk": f"Testing risk {uid_token}-{i}",
|
||||
"provider": "aws",
|
||||
"severity": severity,
|
||||
"categories": ["category1", "category2", "category3"],
|
||||
"description": "This is a random description that should not matter for testing purposes.",
|
||||
"servicename": assigned_resource.service,
|
||||
"resourcetype": assigned_resource.type,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
num_batches = ceil(len(findings) / batch_size)
|
||||
self.stdout.write(self.style.WARNING("Creating findings..."))
|
||||
for i in tqdm(range(0, len(findings), batch_size), total=num_batches):
|
||||
with rls_transaction(tenant_id):
|
||||
Finding.all_objects.bulk_create(findings[i : i + batch_size])
|
||||
self.stdout.write(self.style.SUCCESS("Findings created successfully.\n\n"))
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scan.progress = 66
|
||||
scan.save()
|
||||
|
||||
# Create ResourceFindingMapping
|
||||
mappings = []
|
||||
for index, f in enumerate(findings):
|
||||
mappings.append(
|
||||
ResourceFindingMapping(
|
||||
tenant_id=tenant_id,
|
||||
resource=resources[findings_resources_mapping[index]],
|
||||
finding=f,
|
||||
)
|
||||
)
|
||||
|
||||
num_batches = ceil(len(mappings) / batch_size)
|
||||
self.stdout.write(
|
||||
self.style.WARNING("Creating resource-finding mappings...")
|
||||
)
|
||||
for i in tqdm(range(0, len(mappings), batch_size), total=num_batches):
|
||||
with rls_transaction(tenant_id):
|
||||
ResourceFindingMapping.objects.bulk_create(
|
||||
mappings[i : i + batch_size]
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Resource-finding mappings created successfully.\n\n"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Failed to populate test data: {e}"))
|
||||
scan_state = "failed"
|
||||
finally:
|
||||
scan.completed_at = datetime.now(timezone.utc)
|
||||
scan.duration = int(
|
||||
(datetime.now(timezone.utc) - scan.started_at).total_seconds()
|
||||
)
|
||||
scan.progress = 100
|
||||
scan.state = scan_state
|
||||
scan.unique_resource_count = num_resources
|
||||
with rls_transaction(tenant_id):
|
||||
scan.save()
|
||||
|
||||
self.stdout.write(self.style.NOTICE("Successfully populated test data."))
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-28 15:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0006_findings_first_seen"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="scan",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_id", "state", "inserted_at"],
|
||||
name="scans_prov_state_insert_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="scansummary",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id"], name="scan_summaries_tenant_scan_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Scan, StateChoices
|
||||
|
||||
|
||||
def migrate_daily_scheduled_scan_tasks(apps, schema_editor):
|
||||
for daily_scheduled_scan_task in PeriodicTask.objects.filter(
|
||||
task="scan-perform-scheduled"
|
||||
):
|
||||
task_kwargs = json.loads(daily_scheduled_scan_task.kwargs)
|
||||
tenant_id = task_kwargs["tenant_id"]
|
||||
provider_id = task_kwargs["provider_id"]
|
||||
|
||||
current_time = datetime.now(timezone.utc)
|
||||
scheduled_time_today = datetime.combine(
|
||||
current_time.date(),
|
||||
daily_scheduled_scan_task.start_time.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
if current_time < scheduled_time_today:
|
||||
next_scan_date = scheduled_time_today
|
||||
else:
|
||||
next_scan_date = scheduled_time_today + timedelta(days=1)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.SCHEDULED,
|
||||
scheduled_at=next_scan_date,
|
||||
scheduler_task_id=daily_scheduled_scan_task.id,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0007_scan_and_scan_summaries_indexes"),
|
||||
("django_celery_beat", "0019_alter_periodictasks_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scan",
|
||||
name="scheduler_task",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="django_celery_beat.periodictask",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_daily_scheduled_scan_tasks),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-07 09:42
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0008_daily_scheduled_tasks_update"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="provider",
|
||||
name="uid",
|
||||
field=models.CharField(
|
||||
max_length=250,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
verbose_name="Unique identifier for the provider, set by the provider",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,109 @@
|
||||
from functools import partial
|
||||
|
||||
from django.db import connection, migrations
|
||||
|
||||
|
||||
def create_index_on_partitions(
|
||||
apps, schema_editor, parent_table: str, index_name: str, index_details: str
|
||||
):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT inhrelid::regclass::text
|
||||
FROM pg_inherits
|
||||
WHERE inhparent = %s::regclass;
|
||||
""",
|
||||
[parent_table],
|
||||
)
|
||||
partitions = [row[0] for row in cursor.fetchall()]
|
||||
# Iterate over partitions and create index concurrently.
|
||||
# Note: PostgreSQL does not allow CONCURRENTLY inside a transaction,
|
||||
# so we need atomic = False for this migration.
|
||||
for partition in partitions:
|
||||
sql = (
|
||||
f"CREATE INDEX CONCURRENTLY IF NOT EXISTS {partition.replace('.', '_')}_{index_name} ON {partition} "
|
||||
f"{index_details};"
|
||||
)
|
||||
schema_editor.execute(sql)
|
||||
|
||||
|
||||
def drop_index_on_partitions(apps, schema_editor, parent_table: str, index_name: str):
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT inhrelid::regclass::text
|
||||
FROM pg_inherits
|
||||
WHERE inhparent = %s::regclass;
|
||||
""",
|
||||
[parent_table],
|
||||
)
|
||||
partitions = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Iterate over partitions and drop index concurrently.
|
||||
for partition in partitions:
|
||||
partition_index = f"{partition.replace('.', '_')}_{index_name}"
|
||||
sql = f"DROP INDEX CONCURRENTLY IF EXISTS {partition_index};"
|
||||
schema_editor.execute(sql)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0009_increase_provider_uid_maximum_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_tenant_and_id_idx",
|
||||
index_details="(tenant_id, id)",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="findings_tenant_and_id_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_tenant_scan_idx",
|
||||
index_details="(tenant_id, scan_id)",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_tenant_scan_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_tenant_scan_id_idx",
|
||||
index_details="(tenant_id, scan_id, id)",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_tenant_scan_id_idx",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_delta_new_idx",
|
||||
index_details="(tenant_id, id) where delta = 'new'",
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="find_delta_new_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0010_findings_performance_indexes_partitions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "id"], name="findings_tenant_and_id_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id"], name="find_tenant_scan_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id", "id"], name="find_tenant_scan_id_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="finding",
|
||||
index=models.Index(
|
||||
condition=models.Q(("delta", "new")),
|
||||
fields=["tenant_id", "id"],
|
||||
name="find_delta_new_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="resourcetagmapping",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "resource_id"], name="resource_tag_tenant_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="resource",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "service", "region", "type"],
|
||||
name="resource_tenant_metadata_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0011_findings_performance_indexes_parent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scan",
|
||||
name="output_location",
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
]
|
||||
@@ -11,6 +11,7 @@ from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
from psqlextra.manager import PostgresManager
|
||||
from psqlextra.models import PostgresPartitionedModel
|
||||
@@ -226,13 +227,13 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
@staticmethod
|
||||
def validate_kubernetes_uid(value):
|
||||
if not re.match(
|
||||
r"(^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$)|(^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$)",
|
||||
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
|
||||
value,
|
||||
):
|
||||
raise ModelValidationError(
|
||||
detail="The value must either be a valid Kubernetes UID (up to 63 characters, "
|
||||
"starting and ending with a lowercase letter or number, containing only "
|
||||
"lowercase alphanumeric characters and hyphens) or a valid EKS ARN.",
|
||||
"lowercase alphanumeric characters and hyphens) or a valid AWS EKS Cluster ARN, GCP GKE Context Name or Azure AKS Cluster Name.",
|
||||
code="kubernetes-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
@@ -246,7 +247,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
)
|
||||
uid = models.CharField(
|
||||
"Unique identifier for the provider, set by the provider",
|
||||
max_length=63,
|
||||
max_length=250,
|
||||
blank=False,
|
||||
validators=[MinLengthValidator(3)],
|
||||
)
|
||||
@@ -410,6 +411,10 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
next_scan_at = models.DateTimeField(null=True, blank=True)
|
||||
scheduler_task = models.ForeignKey(
|
||||
PeriodicTask, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
output_location = models.CharField(blank=True, null=True, max_length=200)
|
||||
# TODO: mutelist foreign key
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
@@ -428,6 +433,10 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
fields=["provider", "state", "trigger", "scheduled_at"],
|
||||
name="scans_prov_state_trig_sche_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_id", "state", "inserted_at"],
|
||||
name="scans_prov_state_insert_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
@@ -544,6 +553,10 @@ class Resource(RowLevelSecurityProtectedModel):
|
||||
fields=["uid", "region", "service", "name"],
|
||||
name="resource_uid_reg_serv_name_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "service", "region", "type"],
|
||||
name="resource_tenant_metadata_idx",
|
||||
),
|
||||
GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
|
||||
]
|
||||
|
||||
@@ -591,6 +604,12 @@ class ResourceTagMapping(RowLevelSecurityProtectedModel):
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["tenant_id", "resource_id"], name="resource_tag_tenant_idx"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
@@ -689,7 +708,17 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
],
|
||||
name="findings_filter_idx",
|
||||
),
|
||||
models.Index(fields=["tenant_id", "id"], name="findings_tenant_and_id_idx"),
|
||||
GinIndex(fields=["text_search"], name="gin_findings_search_idx"),
|
||||
models.Index(fields=["tenant_id", "scan_id"], name="find_tenant_scan_idx"),
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id", "id"], name="find_tenant_scan_id_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "id"],
|
||||
condition=Q(delta="new"),
|
||||
name="find_delta_new_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
@@ -1100,6 +1129,12 @@ class ScanSummary(RowLevelSecurityProtectedModel):
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id"],
|
||||
name="scan_summaries_tenant_scan_idx",
|
||||
)
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "scan-summaries"
|
||||
|
||||
+158
-150
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.2.0
|
||||
version: 1.5.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -346,6 +346,9 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: At least one of the variations of the `filter[inserted_at]` filter
|
||||
must be provided.
|
||||
required: true
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
@@ -356,11 +359,13 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -478,51 +483,6 @@ paths:
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_key]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_value]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tags]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |-
|
||||
Filter by resource tags `key:value` pairs.
|
||||
Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_type]
|
||||
schema:
|
||||
@@ -714,8 +674,6 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
@@ -908,11 +866,13 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -1030,51 +990,6 @@ paths:
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_key]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_value]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tags]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |-
|
||||
Filter by resource tags `key:value` pairs.
|
||||
Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_type]
|
||||
schema:
|
||||
@@ -1242,8 +1157,6 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
@@ -1284,7 +1197,6 @@ paths:
|
||||
- services
|
||||
- regions
|
||||
- resource_types
|
||||
- tags
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
@@ -1370,6 +1282,9 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: At least one of the variations of the `filter[inserted_at]` filter
|
||||
must be provided.
|
||||
required: true
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
@@ -1380,11 +1295,13 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -1502,51 +1419,6 @@ paths:
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_key]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_key__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tag_value]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_tag_value__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_tags]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |-
|
||||
Filter by resource tags `key:value` pairs.
|
||||
Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_type]
|
||||
schema:
|
||||
@@ -1714,8 +1586,6 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
@@ -4235,6 +4105,43 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScanUpdateResponse'
|
||||
description: ''
|
||||
/api/v1/scans/{id}/report:
|
||||
get:
|
||||
operationId: scans_report_retrieve
|
||||
description: Returns a ZIP file containing the requested report
|
||||
summary: Download ZIP report
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[scan-reports]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this scan.
|
||||
required: true
|
||||
tags:
|
||||
- Scan
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report obtained successfully
|
||||
'202':
|
||||
description: The task is in progress
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: The scan has no reports
|
||||
/api/v1/schedules/daily:
|
||||
post:
|
||||
operationId: schedules_daily_create
|
||||
@@ -5126,6 +5033,35 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRefreshResponse'
|
||||
description: ''
|
||||
/api/v1/tokens/switch:
|
||||
post:
|
||||
operationId: tokens_switch_create
|
||||
description: Switch tenant by providing a valid tenant ID. The authenticated
|
||||
user must belong to the tenant.
|
||||
summary: Switch tenant using a valid tenant ID
|
||||
tags:
|
||||
- Token
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenSwitchTenantRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenSwitchTenantRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenSwitchTenantRequest'
|
||||
required: true
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenSwitchTenantResponse'
|
||||
description: ''
|
||||
/api/v1/users:
|
||||
get:
|
||||
operationId: users_list
|
||||
@@ -6087,13 +6023,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
tags:
|
||||
description: Tags are described as key-value pairs.
|
||||
required:
|
||||
- services
|
||||
- regions
|
||||
- resource_types
|
||||
- tags
|
||||
FindingMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -6753,7 +6686,7 @@ components:
|
||||
type: integer
|
||||
fail:
|
||||
type: integer
|
||||
manual:
|
||||
muted:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
@@ -7810,7 +7743,7 @@ components:
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 63
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
alias:
|
||||
type: string
|
||||
@@ -7922,7 +7855,7 @@ components:
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 63
|
||||
maxLength: 250
|
||||
minLength: 3
|
||||
required:
|
||||
- uid
|
||||
@@ -7966,7 +7899,7 @@ components:
|
||||
type: string
|
||||
minLength: 3
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 63
|
||||
maxLength: 250
|
||||
required:
|
||||
- uid
|
||||
required:
|
||||
@@ -10180,6 +10113,81 @@ components:
|
||||
$ref: '#/components/schemas/Token'
|
||||
required:
|
||||
- data
|
||||
TokenSwitchTenant:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/TokenSwitchTenantTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
tenant_id:
|
||||
type: string
|
||||
format: uuid
|
||||
writeOnly: true
|
||||
description: The tenant ID for which to request a new token.
|
||||
access:
|
||||
type: string
|
||||
readOnly: true
|
||||
refresh:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- tenant_id
|
||||
TokenSwitchTenantRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- tokens-switch-tenant
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
tenant_id:
|
||||
type: string
|
||||
format: uuid
|
||||
writeOnly: true
|
||||
description: The tenant ID for which to request a new token.
|
||||
access:
|
||||
type: string
|
||||
readOnly: true
|
||||
minLength: 1
|
||||
refresh:
|
||||
type: string
|
||||
readOnly: true
|
||||
minLength: 1
|
||||
required:
|
||||
- tenant_id
|
||||
required:
|
||||
- data
|
||||
TokenSwitchTenantResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TokenSwitchTenant'
|
||||
required:
|
||||
- data
|
||||
TokenSwitchTenantTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- tokens-switch-tenant
|
||||
TokenTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
@@ -3,6 +3,8 @@ from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.models import Membership, User
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_basic_authentication():
|
||||
@@ -177,3 +179,122 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTokenSwitchTenant:
|
||||
def test_switch_tenant_with_valid_token(self, tenants_fixture, providers_fixture):
|
||||
client = APIClient()
|
||||
|
||||
test_user = "test_email@prowler.com"
|
||||
test_password = "test_password"
|
||||
|
||||
# Check that we can create a new user without any kind of authentication
|
||||
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
|
||||
|
||||
# Create a new relationship between this user and another tenant
|
||||
tenant_id = tenants_fixture[0].id
|
||||
user_instance = User.objects.get(email=test_user)
|
||||
Membership.objects.create(user=user_instance, tenant_id=tenant_id)
|
||||
|
||||
# Check that using our new user's credentials we can authenticate and get the providers
|
||||
access_token, _ = get_api_tokens(client, test_user, test_password)
|
||||
auth_headers = get_authorization_header(access_token)
|
||||
|
||||
user_me_response = client.get(
|
||||
reverse("user-me"),
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert user_me_response.status_code == 200
|
||||
# Assert this user belongs to two tenants
|
||||
assert (
|
||||
user_me_response.json()["data"]["relationships"]["memberships"]["meta"][
|
||||
"count"
|
||||
]
|
||||
== 2
|
||||
)
|
||||
|
||||
provider_response = client.get(
|
||||
reverse("provider-list"),
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert provider_response.status_code == 200
|
||||
# Empty response since there are no providers in this tenant
|
||||
assert not provider_response.json()["data"]
|
||||
|
||||
switch_tenant_response = client.post(
|
||||
reverse("token-switch"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-switch-tenant",
|
||||
"attributes": {"tenant_id": tenant_id},
|
||||
}
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert switch_tenant_response.status_code == 200
|
||||
new_access_token = switch_tenant_response.json()["data"]["attributes"]["access"]
|
||||
new_auth_headers = get_authorization_header(new_access_token)
|
||||
|
||||
provider_response = client.get(
|
||||
reverse("provider-list"),
|
||||
headers=new_auth_headers,
|
||||
)
|
||||
assert provider_response.status_code == 200
|
||||
# Now it must be data because we switched to another tenant with providers
|
||||
assert provider_response.json()["data"]
|
||||
|
||||
def test_switch_tenant_with_invalid_token(self, create_test_user, tenants_fixture):
|
||||
client = APIClient()
|
||||
|
||||
access_token, refresh_token = get_api_tokens(
|
||||
client, create_test_user.email, TEST_PASSWORD
|
||||
)
|
||||
auth_headers = get_authorization_header(access_token)
|
||||
|
||||
invalid_token_response = client.post(
|
||||
reverse("token-switch"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-switch-tenant",
|
||||
"attributes": {"tenant_id": "invalid_tenant_id"},
|
||||
}
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert invalid_token_response.status_code == 400
|
||||
assert invalid_token_response.json()["errors"][0]["code"] == "invalid"
|
||||
assert (
|
||||
invalid_token_response.json()["errors"][0]["detail"]
|
||||
== "Must be a valid UUID."
|
||||
)
|
||||
|
||||
invalid_tenant_response = client.post(
|
||||
reverse("token-switch"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-switch-tenant",
|
||||
"attributes": {"tenant_id": tenants_fixture[-1].id},
|
||||
}
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert invalid_tenant_response.status_code == 400
|
||||
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
|
||||
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
|
||||
"Tenant does not exist or user is not a " "member."
|
||||
)
|
||||
|
||||
@@ -274,9 +274,10 @@ class TestValidateInvitation:
|
||||
expired_time = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
invitation.expires_at = expired_time
|
||||
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using, patch(
|
||||
"api.utils.datetime"
|
||||
) as mock_datetime:
|
||||
with (
|
||||
patch("api.utils.Invitation.objects.using") as mock_using,
|
||||
patch("api.utils.datetime") as mock_datetime,
|
||||
):
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.return_value = invitation
|
||||
mock_datetime.now.return_value = datetime.now(timezone.utc)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import ANY, Mock, patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from botocore.exceptions import NoCredentialsError
|
||||
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@@ -19,6 +24,7 @@ from api.models import (
|
||||
RoleProviderGroupRelationship,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
)
|
||||
@@ -27,6 +33,12 @@ from api.rls import Tenant
|
||||
TODAY = str(datetime.today().date())
|
||||
|
||||
|
||||
def today_after_n_days(n_days: int) -> str:
|
||||
return datetime.strftime(
|
||||
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserViewSet:
|
||||
def test_users_list(self, authenticated_client, create_test_user):
|
||||
@@ -878,6 +890,16 @@ class TestProviderViewSet:
|
||||
"uid": "kubernetes-test-123456789",
|
||||
"alias": "test",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
|
||||
"alias": "EKS",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
|
||||
"alias": "GKE",
|
||||
},
|
||||
{
|
||||
"provider": "azure",
|
||||
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
@@ -2062,9 +2084,9 @@ class TestScanViewSet:
|
||||
("started_at.gte", "2024-01-01", 3),
|
||||
("started_at.lte", "2024-01-01", 0),
|
||||
("trigger", Scan.TriggerChoices.MANUAL, 1),
|
||||
("state", StateChoices.AVAILABLE, 2),
|
||||
("state", StateChoices.AVAILABLE, 1),
|
||||
("state", StateChoices.FAILED, 1),
|
||||
("state.in", f"{StateChoices.FAILED},{StateChoices.AVAILABLE}", 3),
|
||||
("state.in", f"{StateChoices.FAILED},{StateChoices.AVAILABLE}", 2),
|
||||
("trigger", Scan.TriggerChoices.MANUAL, 1),
|
||||
]
|
||||
),
|
||||
@@ -2139,6 +2161,159 @@ class TestScanViewSet:
|
||||
response = authenticated_client.get(reverse("scan-list"), {"sort": "invalid"})
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_report_executing(self, authenticated_client, scans_fixture):
|
||||
"""
|
||||
When the scan is still executing (state == EXECUTING), the view should return
|
||||
the task data with HTTP 202 and a Content-Location header.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
scan.state = StateChoices.EXECUTING
|
||||
scan.save()
|
||||
|
||||
task = Task.objects.create(tenant_id=scan.tenant_id)
|
||||
dummy_task_data = {"id": str(task.id), "state": StateChoices.EXECUTING}
|
||||
|
||||
scan.task = task
|
||||
scan.save()
|
||||
|
||||
with patch(
|
||||
"api.v1.views.TaskSerializer",
|
||||
return_value=type("DummySerializer", (), {"data": dummy_task_data}),
|
||||
):
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert "Content-Location" in response
|
||||
assert dummy_task_data["id"] in response["Content-Location"]
|
||||
|
||||
def test_report_celery_task_executing(self, authenticated_client, scans_fixture):
|
||||
"""
|
||||
When the scan is not executing but a related celery task exists and is running,
|
||||
the view should return that task data with HTTP 202.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.output_location = "dummy"
|
||||
scan.save()
|
||||
|
||||
dummy_task = Task.objects.create(tenant_id=scan.tenant_id)
|
||||
dummy_task.id = "dummy-task-id"
|
||||
dummy_task_data = {"id": dummy_task.id, "state": StateChoices.EXECUTING}
|
||||
|
||||
with patch("api.v1.views.Task.objects.get", return_value=dummy_task), patch(
|
||||
"api.v1.views.TaskSerializer",
|
||||
return_value=type("DummySerializer", (), {"data": dummy_task_data}),
|
||||
):
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert "Content-Location" in response
|
||||
assert dummy_task_data["id"] in response["Content-Location"]
|
||||
|
||||
def test_report_no_output_location(self, authenticated_client, scans_fixture):
|
||||
"""
|
||||
If the scan does not have an output_location, the view should return a 404.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.output_location = ""
|
||||
scan.save()
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json()["errors"]["detail"] == "The scan has no reports."
|
||||
|
||||
def test_report_s3_no_credentials(
|
||||
self, authenticated_client, scans_fixture, monkeypatch
|
||||
):
|
||||
"""
|
||||
When output_location is an S3 URL and get_s3_client() raises a credentials exception,
|
||||
the view should return HTTP 403 with the proper error message.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
bucket = "test-bucket"
|
||||
key = "report.zip"
|
||||
scan.output_location = f"s3://{bucket}/{key}"
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.save()
|
||||
|
||||
def fake_get_s3_client():
|
||||
raise NoCredentialsError()
|
||||
|
||||
monkeypatch.setattr("api.v1.views.get_s3_client", fake_get_s3_client)
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert (
|
||||
response.json()["errors"]["detail"]
|
||||
== "There is a problem with credentials."
|
||||
)
|
||||
|
||||
def test_report_s3_success(self, authenticated_client, scans_fixture, monkeypatch):
|
||||
"""
|
||||
When output_location is an S3 URL and the S3 client returns the file successfully,
|
||||
the view should return the ZIP file with HTTP 200 and proper headers.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
bucket = "test-bucket"
|
||||
key = "report.zip"
|
||||
scan.output_location = f"s3://{bucket}/{key}"
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.save()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"api.v1.views.env", type("env", (), {"str": lambda self, key: bucket})()
|
||||
)
|
||||
|
||||
class FakeS3Client:
|
||||
def get_object(self, Bucket, Key):
|
||||
assert Bucket == bucket
|
||||
assert Key == key
|
||||
return {"Body": io.BytesIO(b"s3 zip content")}
|
||||
|
||||
monkeypatch.setattr("api.v1.views.get_s3_client", lambda: FakeS3Client())
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == 200
|
||||
expected_filename = os.path.basename("report.zip")
|
||||
content_disposition = response.get("Content-Disposition")
|
||||
assert content_disposition.startswith('attachment; filename="')
|
||||
assert f'filename="{expected_filename}"' in content_disposition
|
||||
assert response.content == b"s3 zip content"
|
||||
|
||||
def test_report_local_file(
|
||||
self, authenticated_client, scans_fixture, tmp_path, monkeypatch
|
||||
):
|
||||
"""
|
||||
When output_location is a local file path, the view should read the file from disk
|
||||
and return it with proper headers.
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
file_content = b"local zip file content"
|
||||
file_path = tmp_path / "report.zip"
|
||||
file_path.write_bytes(file_content)
|
||||
|
||||
scan.output_location = str(file_path)
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.save()
|
||||
|
||||
monkeypatch.setattr(
|
||||
glob,
|
||||
"glob",
|
||||
lambda pattern: [str(file_path)] if pattern == str(file_path) else [],
|
||||
)
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.content == file_content
|
||||
content_disposition = response.get("Content-Disposition")
|
||||
assert content_disposition.startswith('attachment; filename="')
|
||||
assert f'filename="{file_path.name}"' in content_disposition
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTaskViewSet:
|
||||
@@ -2379,12 +2554,33 @@ class TestResourceViewSet:
|
||||
@pytest.mark.django_db
|
||||
class TestFindingViewSet:
|
||||
def test_findings_list_none(self, authenticated_client):
|
||||
response = authenticated_client.get(reverse("finding-list"))
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"filter[inserted_at]": TODAY}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 0
|
||||
|
||||
def test_findings_list(self, authenticated_client, findings_fixture):
|
||||
def test_findings_list_no_date_filter(self, authenticated_client):
|
||||
response = authenticated_client.get(reverse("finding-list"))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "required"
|
||||
|
||||
def test_findings_date_range_too_large(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{
|
||||
"filter[inserted_at.lte]": today_after_n_days(
|
||||
-(settings.FINDINGS_MAX_DAYS_IN_RANGE + 1)
|
||||
),
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "invalid"
|
||||
|
||||
def test_findings_list(self, authenticated_client, findings_fixture):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"filter[inserted_at]": TODAY}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(findings_fixture)
|
||||
assert (
|
||||
@@ -2397,14 +2593,15 @@ class TestFindingViewSet:
|
||||
[
|
||||
("resources", ["resources"]),
|
||||
("scan", ["scans"]),
|
||||
("resources.provider,scan", ["resources", "scans", "providers"]),
|
||||
("resources,scan.provider", ["resources", "scans", "providers"]),
|
||||
],
|
||||
)
|
||||
def test_findings_list_include(
|
||||
self, include_values, expected_resources, authenticated_client, findings_fixture
|
||||
):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"include": include_values}
|
||||
reverse("finding-list"),
|
||||
{"include": include_values, "filter[inserted_at]": TODAY},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(findings_fixture)
|
||||
@@ -2439,30 +2636,31 @@ class TestFindingViewSet:
|
||||
("service.icontains", "ec", 1),
|
||||
("inserted_at", "2024-01-01", 0),
|
||||
("inserted_at.date", "2024-01-01", 0),
|
||||
("inserted_at.gte", "2024-01-01", 2),
|
||||
("inserted_at.gte", today_after_n_days(-1), 2),
|
||||
(
|
||||
"inserted_at.lte",
|
||||
"2028-12-31",
|
||||
today_after_n_days(1),
|
||||
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
|
||||
("updated_at.lte", "2024-01-01", 0),
|
||||
),
|
||||
("updated_at.lte", today_after_n_days(-1), 0),
|
||||
("resource_type.icontains", "prowler", 2),
|
||||
# full text search on finding
|
||||
("search", "dev-qa", 1),
|
||||
("search", "orange juice", 1),
|
||||
# full text search on resource
|
||||
("search", "ec2", 2),
|
||||
# full text search on finding tags
|
||||
("search", "value2", 2),
|
||||
("resource_tag_key", "key", 2),
|
||||
("resource_tag_key__in", "key,key2", 2),
|
||||
("resource_tag_key__icontains", "key", 2),
|
||||
("resource_tag_value", "value", 2),
|
||||
("resource_tag_value__in", "value,value2", 2),
|
||||
("resource_tag_value__icontains", "value", 2),
|
||||
("resource_tags", "key:value", 2),
|
||||
("resource_tags", "not:exists", 0),
|
||||
("resource_tags", "not:exists,key:value", 2),
|
||||
# full text search on finding tags (disabled for now)
|
||||
# ("search", "value2", 2),
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# ("resource_tag_key", "key", 2),
|
||||
# ("resource_tag_key__in", "key,key2", 2),
|
||||
# ("resource_tag_key__icontains", "key", 2),
|
||||
# ("resource_tag_value", "value", 2),
|
||||
# ("resource_tag_value__in", "value,value2", 2),
|
||||
# ("resource_tag_value__icontains", "value", 2),
|
||||
# ("resource_tags", "key:value", 2),
|
||||
# ("resource_tags", "not:exists", 0),
|
||||
# ("resource_tags", "not:exists,key:value", 2),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -2474,9 +2672,13 @@ class TestFindingViewSet:
|
||||
filter_value,
|
||||
expected_count,
|
||||
):
|
||||
filters = {f"filter[{filter_name}]": filter_value}
|
||||
if "inserted_at" not in filter_name:
|
||||
filters["filter[inserted_at]"] = TODAY
|
||||
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{f"filter[{filter_name}]": filter_value},
|
||||
filters,
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2485,9 +2687,7 @@ class TestFindingViewSet:
|
||||
def test_finding_filter_by_scan_id(self, authenticated_client, findings_fixture):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{
|
||||
"filter[scan]": findings_fixture[0].scan.id,
|
||||
},
|
||||
{"filter[scan]": findings_fixture[0].scan.id},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2
|
||||
@@ -2510,6 +2710,7 @@ class TestFindingViewSet:
|
||||
reverse("finding-list"),
|
||||
{
|
||||
"filter[provider]": findings_fixture[0].scan.provider.id,
|
||||
"filter[inserted_at]": TODAY,
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2524,7 +2725,8 @@ class TestFindingViewSet:
|
||||
"filter[provider.in]": [
|
||||
findings_fixture[0].scan.provider.id,
|
||||
findings_fixture[1].scan.provider.id,
|
||||
]
|
||||
],
|
||||
"filter[inserted_at]": TODAY,
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2558,13 +2760,13 @@ class TestFindingViewSet:
|
||||
)
|
||||
def test_findings_sort(self, authenticated_client, sort_field):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"sort": sort_field}
|
||||
reverse("finding-list"), {"sort": sort_field, "filter[inserted_at]": TODAY}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_findings_sort_invalid(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"sort": "invalid"}
|
||||
reverse("finding-list"), {"sort": "invalid", "filter[inserted_at]": TODAY}
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "invalid"
|
||||
@@ -2611,7 +2813,8 @@ class TestFindingViewSet:
|
||||
|
||||
expected_services = {"ec2", "s3"}
|
||||
expected_regions = {"eu-west-1", "us-east-1"}
|
||||
expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
expected_resource_types = {"prowler-test"}
|
||||
|
||||
assert data["data"]["type"] == "findings-metadata"
|
||||
@@ -2621,7 +2824,7 @@ class TestFindingViewSet:
|
||||
assert (
|
||||
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
|
||||
)
|
||||
assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
# assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
|
||||
def test_findings_metadata_severity_retrieve(
|
||||
self, authenticated_client, findings_fixture
|
||||
@@ -2631,14 +2834,15 @@ class TestFindingViewSet:
|
||||
reverse("finding-metadata"),
|
||||
{
|
||||
"filter[severity__in]": ["low", "medium"],
|
||||
"filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d"),
|
||||
"filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"),
|
||||
},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
expected_services = {"s3"}
|
||||
expected_regions = {"eu-west-1"}
|
||||
expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
expected_resource_types = {"prowler-test"}
|
||||
|
||||
assert data["data"]["type"] == "findings-metadata"
|
||||
@@ -2648,7 +2852,7 @@ class TestFindingViewSet:
|
||||
assert (
|
||||
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
|
||||
)
|
||||
assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
# assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
|
||||
def test_findings_metadata_future_date(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
@@ -2660,7 +2864,8 @@ class TestFindingViewSet:
|
||||
assert data["data"]["id"] is None
|
||||
assert data["data"]["attributes"]["services"] == []
|
||||
assert data["data"]["attributes"]["regions"] == []
|
||||
assert data["data"]["attributes"]["tags"] == {}
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# assert data["data"]["attributes"]["tags"] == {}
|
||||
assert data["data"]["attributes"]["resource_types"] == []
|
||||
|
||||
def test_findings_metadata_invalid_date(self, authenticated_client):
|
||||
@@ -4280,18 +4485,15 @@ class TestOverviewViewSet:
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
def test_overview_providers_list(
|
||||
self, authenticated_client, findings_fixture, resources_fixture
|
||||
self, authenticated_client, scan_summaries_fixture, resources_fixture
|
||||
):
|
||||
response = authenticated_client.get(reverse("overview-providers"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Only findings from one provider
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == len(
|
||||
findings_fixture
|
||||
)
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 0
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["manual"] == 0
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == 4
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == len(
|
||||
resources_fixture
|
||||
)
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation, Provider
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.common.models import Connection
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Provider, Invitation
|
||||
|
||||
class CustomOAuth2Client(OAuth2Client):
|
||||
def __init__(self, client_id, secret, *args, **kwargs):
|
||||
# Remove any duplicate "scope_delimiter" from kwargs
|
||||
# Bug present in dj-rest-auth after version v7.0.1
|
||||
# https://github.com/iMerica/dj-rest-auth/issues/673
|
||||
kwargs.pop("scope_delimiter", None)
|
||||
super().__init__(client_id, secret, *args, **kwargs)
|
||||
|
||||
|
||||
def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
|
||||
|
||||
@@ -106,7 +106,7 @@ def uuid7_end(uuid_obj: UUID, offset_months: int = 1) -> UUID:
|
||||
|
||||
Args:
|
||||
uuid_obj: A UUIDv7 object.
|
||||
offset_days: Number of months to offset from the given UUID's date. Defaults to 1 to handle if
|
||||
offset_months: Number of months to offset from the given UUID's date. Defaults to 1 to handle if
|
||||
partitions are not being used, if so the value will be the one set at FINDINGS_TABLE_PARTITION_MONTHS.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -38,7 +38,65 @@ from api.rls import Tenant
|
||||
# Tokens
|
||||
|
||||
|
||||
class TokenSerializer(TokenObtainPairSerializer):
|
||||
def generate_tokens(user: User, tenant_id: str) -> dict:
|
||||
try:
|
||||
refresh = RefreshToken.for_user(user)
|
||||
except InvalidKeyError:
|
||||
# Handle invalid key error
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "Token generation failed due to invalid key configuration. Provide valid "
|
||||
"DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment."
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
# Post-process the tokens
|
||||
# Set the tenant_id
|
||||
refresh["tenant_id"] = tenant_id
|
||||
|
||||
# Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a
|
||||
# way to set the nbf claim
|
||||
refresh.payload["nbf"] = refresh["iat"]
|
||||
|
||||
# Get the access token
|
||||
access = refresh.access_token
|
||||
|
||||
if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]:
|
||||
update_last_login(None, user)
|
||||
|
||||
return {"access": str(access), "refresh": str(refresh)}
|
||||
|
||||
|
||||
class BaseTokenSerializer(TokenObtainPairSerializer):
|
||||
def custom_validate(self, attrs, social: bool = False):
|
||||
email = attrs.get("email")
|
||||
password = attrs.get("password")
|
||||
tenant_id = str(attrs.get("tenant_id", ""))
|
||||
|
||||
# Authenticate user
|
||||
user = (
|
||||
User.objects.get(email=email)
|
||||
if social
|
||||
else authenticate(username=email, password=password)
|
||||
)
|
||||
if user is None:
|
||||
raise ValidationError("Invalid credentials")
|
||||
|
||||
if tenant_id:
|
||||
if not user.is_member_of_tenant(tenant_id):
|
||||
raise ValidationError("Tenant does not exist or user is not a member.")
|
||||
else:
|
||||
first_membership = user.memberships.order_by("date_joined").first()
|
||||
if first_membership is None:
|
||||
raise ValidationError("User has no memberships.")
|
||||
tenant_id = str(first_membership.tenant_id)
|
||||
|
||||
return generate_tokens(user, tenant_id)
|
||||
|
||||
|
||||
class TokenSerializer(BaseTokenSerializer):
|
||||
email = serializers.EmailField(write_only=True)
|
||||
password = serializers.CharField(write_only=True)
|
||||
tenant_id = serializers.UUIDField(
|
||||
@@ -56,53 +114,25 @@ class TokenSerializer(TokenObtainPairSerializer):
|
||||
resource_name = "tokens"
|
||||
|
||||
def validate(self, attrs):
|
||||
email = attrs.get("email")
|
||||
password = attrs.get("password")
|
||||
tenant_id = str(attrs.get("tenant_id", ""))
|
||||
return super().custom_validate(attrs)
|
||||
|
||||
# Authenticate user
|
||||
user = authenticate(username=email, password=password)
|
||||
if user is None:
|
||||
raise ValidationError("Invalid credentials")
|
||||
|
||||
if tenant_id:
|
||||
if not user.is_member_of_tenant(tenant_id):
|
||||
raise ValidationError("Tenant does not exist or user is not a member.")
|
||||
else:
|
||||
first_membership = user.memberships.order_by("date_joined").first()
|
||||
if first_membership is None:
|
||||
raise ValidationError("User has no memberships.")
|
||||
tenant_id = str(first_membership.tenant_id)
|
||||
class TokenSocialLoginSerializer(BaseTokenSerializer):
|
||||
email = serializers.EmailField(write_only=True)
|
||||
|
||||
# Generate tokens
|
||||
try:
|
||||
refresh = RefreshToken.for_user(user)
|
||||
except InvalidKeyError:
|
||||
# Handle invalid key error
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "Token generation failed due to invalid key configuration. Provide valid "
|
||||
"DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment."
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
# Output tokens
|
||||
refresh = serializers.CharField(read_only=True)
|
||||
access = serializers.CharField(read_only=True)
|
||||
|
||||
# Post-process the tokens
|
||||
# Set the tenant_id
|
||||
refresh["tenant_id"] = tenant_id
|
||||
class JSONAPIMeta:
|
||||
resource_name = "tokens"
|
||||
|
||||
# Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a
|
||||
# way to set the nbf claim
|
||||
refresh.payload["nbf"] = refresh["iat"]
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields.pop("password", None)
|
||||
|
||||
# Get the access token
|
||||
access = refresh.access_token
|
||||
|
||||
if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]:
|
||||
update_last_login(None, user)
|
||||
|
||||
return {"access": str(access), "refresh": str(refresh)}
|
||||
def validate(self, attrs):
|
||||
return super().custom_validate(attrs, social=True)
|
||||
|
||||
|
||||
# TODO: Check if we can change the parent class to TokenRefreshSerializer from rest_framework_simplejwt.serializers
|
||||
@@ -140,6 +170,30 @@ class TokenRefreshSerializer(serializers.Serializer):
|
||||
raise ValidationError({"refresh": "Invalid or expired token"})
|
||||
|
||||
|
||||
class TokenSwitchTenantSerializer(serializers.Serializer):
|
||||
tenant_id = serializers.UUIDField(
|
||||
write_only=True, help_text="The tenant ID for which to request a new token."
|
||||
)
|
||||
access = serializers.CharField(read_only=True)
|
||||
refresh = serializers.CharField(read_only=True)
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "tokens-switch-tenant"
|
||||
|
||||
def validate(self, attrs):
|
||||
request = self.context["request"]
|
||||
user = request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
raise ValidationError("Invalid or expired token.")
|
||||
|
||||
tenant_id = str(attrs.get("tenant_id"))
|
||||
if not user.is_member_of_tenant(tenant_id):
|
||||
raise ValidationError("Tenant does not exist or user is not a member.")
|
||||
|
||||
return generate_tokens(user, tenant_id)
|
||||
|
||||
|
||||
# Base
|
||||
|
||||
|
||||
@@ -691,6 +745,43 @@ class ProviderSerializer(RLSSerializer):
|
||||
}
|
||||
|
||||
|
||||
class ProviderIncludeSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the Provider model.
|
||||
"""
|
||||
|
||||
provider = ProviderEnumSerializerField()
|
||||
connection = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"provider",
|
||||
"uid",
|
||||
"alias",
|
||||
"connection",
|
||||
# "scanner_args",
|
||||
]
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connected": {"type": "boolean"},
|
||||
"last_checked_at": {"type": "string", "format": "date-time"},
|
||||
},
|
||||
}
|
||||
)
|
||||
def get_connection(self, obj):
|
||||
return {
|
||||
"connected": obj.connected,
|
||||
"last_checked_at": obj.connection_last_checked_at,
|
||||
}
|
||||
|
||||
|
||||
class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
@@ -753,6 +844,35 @@ class ScanSerializer(RLSSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ScanIncludeSerializer(RLSSerializer):
|
||||
trigger = serializers.ChoiceField(
|
||||
choices=Scan.TriggerChoices.choices, read_only=True
|
||||
)
|
||||
state = StateEnumSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Scan
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"trigger",
|
||||
"state",
|
||||
"unique_resource_count",
|
||||
"progress",
|
||||
# "scanner_args",
|
||||
"duration",
|
||||
"inserted_at",
|
||||
"started_at",
|
||||
"completed_at",
|
||||
"scheduled_at",
|
||||
"provider",
|
||||
]
|
||||
|
||||
included_serializers = {
|
||||
"provider": "api.v1.serializers.ProviderIncludeSerializer",
|
||||
}
|
||||
|
||||
|
||||
class ScanCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = Scan
|
||||
@@ -819,6 +939,14 @@ class ScanTaskSerializer(RLSSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ScanReportSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(source="scan")
|
||||
|
||||
class Meta:
|
||||
resource_name = "scan-reports"
|
||||
fields = ["id"]
|
||||
|
||||
|
||||
class ResourceTagSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the ResourceTag model
|
||||
@@ -884,6 +1012,51 @@ class ResourceSerializer(RLSSerializer):
|
||||
return fields
|
||||
|
||||
|
||||
class ResourceIncludeSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the Resource model.
|
||||
"""
|
||||
|
||||
tags = serializers.SerializerMethodField()
|
||||
type_ = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Resource
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"uid",
|
||||
"name",
|
||||
"region",
|
||||
"service",
|
||||
"type_",
|
||||
"tags",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
}
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Tags associated with the resource",
|
||||
"example": {"env": "prod", "owner": "johndoe"},
|
||||
}
|
||||
)
|
||||
def get_tags(self, obj):
|
||||
return obj.get_tags(self.context.get("tenant_id"))
|
||||
|
||||
def get_fields(self):
|
||||
"""`type` is a Python reserved keyword."""
|
||||
fields = super().get_fields()
|
||||
type_ = fields.pop("type_")
|
||||
fields["type"] = type_
|
||||
return fields
|
||||
|
||||
|
||||
class FindingSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the Finding model.
|
||||
@@ -913,8 +1086,8 @@ class FindingSerializer(RLSSerializer):
|
||||
]
|
||||
|
||||
included_serializers = {
|
||||
"scan": ScanSerializer,
|
||||
"resources": ResourceSerializer,
|
||||
"scan": ScanIncludeSerializer,
|
||||
"resources": ResourceIncludeSerializer,
|
||||
}
|
||||
|
||||
|
||||
@@ -933,7 +1106,8 @@ class FindingMetadataSerializer(serializers.Serializer):
|
||||
resource_types = serializers.ListField(
|
||||
child=serializers.CharField(), allow_empty=True
|
||||
)
|
||||
tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
|
||||
|
||||
class Meta:
|
||||
resource_name = "findings-metadata"
|
||||
@@ -1735,7 +1909,7 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
"properties": {
|
||||
"pass": {"type": "integer"},
|
||||
"fail": {"type": "integer"},
|
||||
"manual": {"type": "integer"},
|
||||
"muted": {"type": "integer"},
|
||||
"total": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
@@ -1744,7 +1918,7 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
return {
|
||||
"pass": obj["findings_passed"],
|
||||
"fail": obj["findings_failed"],
|
||||
"manual": obj["findings_manual"],
|
||||
"muted": obj["findings_muted"],
|
||||
"total": obj["total_findings"],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,28 +3,31 @@ from drf_spectacular.views import SpectacularRedocView
|
||||
from rest_framework_nested import routers
|
||||
|
||||
from api.v1.views import (
|
||||
ComplianceOverviewViewSet,
|
||||
CustomTokenObtainView,
|
||||
CustomTokenRefreshView,
|
||||
CustomTokenSwitchTenantView,
|
||||
FindingViewSet,
|
||||
MembershipViewSet,
|
||||
ProviderGroupViewSet,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
ProviderSecretViewSet,
|
||||
InvitationViewSet,
|
||||
GithubSocialLoginView,
|
||||
GoogleSocialLoginView,
|
||||
InvitationAcceptViewSet,
|
||||
RoleViewSet,
|
||||
RoleProviderGroupRelationshipView,
|
||||
UserRoleRelationshipView,
|
||||
InvitationViewSet,
|
||||
MembershipViewSet,
|
||||
OverviewViewSet,
|
||||
ComplianceOverviewViewSet,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
ProviderGroupViewSet,
|
||||
ProviderSecretViewSet,
|
||||
ProviderViewSet,
|
||||
ResourceViewSet,
|
||||
RoleProviderGroupRelationshipView,
|
||||
RoleViewSet,
|
||||
ScanViewSet,
|
||||
ScheduleViewSet,
|
||||
SchemaView,
|
||||
TaskViewSet,
|
||||
TenantMembersViewSet,
|
||||
TenantViewSet,
|
||||
UserRoleRelationshipView,
|
||||
UserViewSet,
|
||||
)
|
||||
|
||||
@@ -56,6 +59,7 @@ users_router.register(r"memberships", MembershipViewSet, basename="user-membersh
|
||||
urlpatterns = [
|
||||
path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"),
|
||||
path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"),
|
||||
path("tokens/switch", CustomTokenSwitchTenantView.as_view(), name="token-switch"),
|
||||
path(
|
||||
"providers/secrets",
|
||||
ProviderSecretViewSet.as_view({"get": "list", "post": "create"}),
|
||||
@@ -106,6 +110,8 @@ urlpatterns = [
|
||||
),
|
||||
name="provider_group-providers-relationship",
|
||||
),
|
||||
path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"),
|
||||
path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"),
|
||||
path("", include(router.urls)),
|
||||
path("", include(tenants_router.urls)),
|
||||
path("", include(users_router.urls)),
|
||||
|
||||
+325
-126
@@ -1,10 +1,23 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
|
||||
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
|
||||
from celery.result import AsyncResult
|
||||
from config.env import env
|
||||
from config.settings.social_login import (
|
||||
GITHUB_OAUTH_CALLBACK_URL,
|
||||
GOOGLE_OAUTH_CALLBACK_URL,
|
||||
)
|
||||
from dj_rest_auth.registration.views import SocialLoginView
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.search import SearchQuery
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum
|
||||
from django.db.models.functions import JSONObject
|
||||
from django.db.models import Count, Exists, F, OuterRef, Prefetch, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
@@ -31,11 +44,11 @@ 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 tasks.beat import schedule_provider_scan
|
||||
from tasks.jobs.export import get_s3_client
|
||||
from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
delete_tenant_task,
|
||||
perform_scan_summary_task,
|
||||
perform_scan_task,
|
||||
)
|
||||
|
||||
@@ -68,13 +81,13 @@ from api.models import (
|
||||
ProviderGroupMembership,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
Task,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
@@ -82,8 +95,7 @@ from api.models import (
|
||||
from api.pagination import ComplianceOverviewPagination
|
||||
from api.rbac.permissions import Permissions, get_providers, get_role
|
||||
from api.rls import Tenant
|
||||
from api.utils import validate_invitation
|
||||
from api.uuid_utils import datetime_to_uuid7
|
||||
from api.utils import CustomOAuth2Client, validate_invitation
|
||||
from api.v1.serializers import (
|
||||
ComplianceOverviewFullSerializer,
|
||||
ComplianceOverviewSerializer,
|
||||
@@ -115,6 +127,7 @@ from api.v1.serializers import (
|
||||
RoleSerializer,
|
||||
RoleUpdateSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanReportSerializer,
|
||||
ScanSerializer,
|
||||
ScanUpdateSerializer,
|
||||
ScheduleDailyCreateSerializer,
|
||||
@@ -122,6 +135,8 @@ from api.v1.serializers import (
|
||||
TenantSerializer,
|
||||
TokenRefreshSerializer,
|
||||
TokenSerializer,
|
||||
TokenSocialLoginSerializer,
|
||||
TokenSwitchTenantSerializer,
|
||||
UserCreateSerializer,
|
||||
UserRoleRelationshipSerializer,
|
||||
UserSerializer,
|
||||
@@ -188,13 +203,43 @@ class CustomTokenRefreshView(GenericAPIView):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["Token"],
|
||||
summary="Switch tenant using a valid tenant ID",
|
||||
description="Switch tenant by providing a valid tenant ID. The authenticated user must belong to the tenant.",
|
||||
)
|
||||
class CustomTokenSwitchTenantView(GenericAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
resource_name = "tokens-switch-tenant"
|
||||
serializer_class = TokenSwitchTenantSerializer
|
||||
http_method_names = ["post"]
|
||||
|
||||
def post(self, request):
|
||||
serializer = TokenSwitchTenantSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except TokenError as e:
|
||||
raise InvalidToken(e.args[0])
|
||||
|
||||
return Response(
|
||||
data={
|
||||
"type": "tokens-switch-tenant",
|
||||
"attributes": serializer.validated_data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
class SchemaView(SpectacularAPIView):
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.2.0"
|
||||
spectacular_settings.VERSION = "1.5.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -254,6 +299,58 @@ class SchemaView(SpectacularAPIView):
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
class GoogleSocialLoginView(SocialLoginView):
|
||||
adapter_class = GoogleOAuth2Adapter
|
||||
client_class = CustomOAuth2Client
|
||||
callback_url = GOOGLE_OAUTH_CALLBACK_URL
|
||||
|
||||
def get_response(self):
|
||||
original_response = super().get_response()
|
||||
|
||||
if self.user and self.user.is_authenticated:
|
||||
serializer = TokenSocialLoginSerializer(data={"email": self.user.email})
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except TokenError as e:
|
||||
raise InvalidToken(e.args[0])
|
||||
return Response(
|
||||
data={
|
||||
"type": "google-social-tokens",
|
||||
"attributes": serializer.validated_data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return original_response
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
class GithubSocialLoginView(SocialLoginView):
|
||||
adapter_class = GitHubOAuth2Adapter
|
||||
client_class = CustomOAuth2Client
|
||||
callback_url = GITHUB_OAUTH_CALLBACK_URL
|
||||
|
||||
def get_response(self):
|
||||
original_response = super().get_response()
|
||||
|
||||
if self.user and self.user.is_authenticated:
|
||||
serializer = TokenSocialLoginSerializer(data={"email": self.user.email})
|
||||
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except TokenError as e:
|
||||
raise InvalidToken(e.args[0])
|
||||
|
||||
return Response(
|
||||
data={
|
||||
"type": "github-social-tokens",
|
||||
"attributes": serializer.validated_data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return original_response
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["User"],
|
||||
@@ -1026,6 +1123,18 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
request=ScanCreateSerializer,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
),
|
||||
report=extend_schema(
|
||||
tags=["Scan"],
|
||||
summary="Download ZIP report",
|
||||
description="Returns a ZIP file containing the requested report",
|
||||
request=ScanReportSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Report obtained successfully"),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(description="The scan has no reports"),
|
||||
},
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
@@ -1074,6 +1183,10 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
return ScanCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return ScanUpdateSerializer
|
||||
elif self.action == "report":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return ScanReportSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
@@ -1091,6 +1204,93 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
)
|
||||
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["get"], url_name="report")
|
||||
def report(self, request, pk=None):
|
||||
scan_instance = self.get_object()
|
||||
|
||||
if scan_instance.state == StateChoices.EXECUTING:
|
||||
# If the scan is still running, return the task
|
||||
prowler_task = Task.objects.get(id=scan_instance.task.id)
|
||||
self.response_serializer_class = TaskSerializer
|
||||
output_serializer = self.get_serializer(prowler_task)
|
||||
return Response(
|
||||
data=output_serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": output_serializer.data["id"]}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
output_celery_task = Task.objects.get(
|
||||
task_runner_task__task_name="scan-report",
|
||||
task_runner_task__task_args__contains=pk,
|
||||
)
|
||||
self.response_serializer_class = TaskSerializer
|
||||
output_serializer = self.get_serializer(output_celery_task)
|
||||
if output_serializer.data["state"] == StateChoices.EXECUTING:
|
||||
# If the task is still running, return the task
|
||||
return Response(
|
||||
data=output_serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": output_serializer.data["id"]}
|
||||
)
|
||||
},
|
||||
)
|
||||
except Task.DoesNotExist:
|
||||
# If the task does not exist, it means that the task is removed from the database
|
||||
pass
|
||||
|
||||
output_location = scan_instance.output_location
|
||||
if not output_location:
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if scan_instance.output_location.startswith("s3://"):
|
||||
try:
|
||||
s3_client = get_s3_client()
|
||||
except (ClientError, NoCredentialsError, ParamValidationError):
|
||||
return Response(
|
||||
{"detail": "There is a problem with credentials."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
bucket_name = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET")
|
||||
key = output_location[len(f"s3://{bucket_name}/") :]
|
||||
try:
|
||||
s3_object = s3_client.get_object(Bucket=bucket_name, Key=key)
|
||||
except ClientError as e:
|
||||
error_code = e.response.get("Error", {}).get("Code")
|
||||
if error_code == "NoSuchKey":
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
return Response(
|
||||
{"detail": "There is a problem with credentials."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
file_content = s3_object["Body"].read()
|
||||
filename = os.path.basename(output_location.split("/")[-1])
|
||||
else:
|
||||
zip_files = glob.glob(output_location)
|
||||
file_path = zip_files[0]
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
response = HttpResponse(
|
||||
file_content, content_type="application/x-zip-compressed"
|
||||
)
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
input_serializer = self.get_serializer(data=request.data)
|
||||
input_serializer.is_valid(raise_exception=True)
|
||||
@@ -1105,10 +1305,6 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
# 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,
|
||||
scan_id=str(scan.id),
|
||||
),
|
||||
)
|
||||
|
||||
scan.task_id = task.id
|
||||
@@ -1272,6 +1468,14 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
tags=["Finding"],
|
||||
summary="List all findings",
|
||||
description="Retrieve a list of all findings with options for filtering by various criteria.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="filter[inserted_at]",
|
||||
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
|
||||
required=True,
|
||||
type=OpenApiTypes.DATE,
|
||||
)
|
||||
],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Finding"],
|
||||
@@ -1289,32 +1493,44 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
tags=["Finding"],
|
||||
summary="Retrieve metadata values from findings",
|
||||
description="Fetch unique metadata values from a set of findings. This is useful for dynamic filtering.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="filter[inserted_at]",
|
||||
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
|
||||
required=True,
|
||||
type=OpenApiTypes.DATE,
|
||||
)
|
||||
],
|
||||
filters=True,
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
class FindingViewSet(BaseRLSViewSet):
|
||||
queryset = Finding.objects.all()
|
||||
queryset = Finding.all_objects.all()
|
||||
serializer_class = FindingSerializer
|
||||
prefetch_for_includes = {
|
||||
"__all__": [],
|
||||
"resources": [
|
||||
Prefetch("resources", queryset=Resource.objects.select_related("findings"))
|
||||
],
|
||||
"scan": [Prefetch("scan", queryset=Scan.objects.select_related("findings"))],
|
||||
}
|
||||
http_method_names = ["get"]
|
||||
filterset_class = FindingFilter
|
||||
ordering = ["-id"]
|
||||
http_method_names = ["get"]
|
||||
ordering = ["-inserted_at"]
|
||||
ordering_fields = [
|
||||
"id",
|
||||
"status",
|
||||
"severity",
|
||||
"check_id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
prefetch_for_includes = {
|
||||
"__all__": [],
|
||||
"resources": [
|
||||
Prefetch(
|
||||
"resources",
|
||||
queryset=Resource.all_objects.prefetch_related("tags", "findings"),
|
||||
)
|
||||
],
|
||||
"scan": [
|
||||
Prefetch("scan", queryset=Scan.all_objects.select_related("findings"))
|
||||
],
|
||||
}
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
|
||||
# the provider through the provider group)
|
||||
required_permissions = []
|
||||
@@ -1328,48 +1544,59 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
tenant_id = self.request.tenant_id
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Finding.objects.filter(tenant_id=self.request.tenant_id)
|
||||
# User has unlimited visibility, return all findings
|
||||
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
queryset = Finding.objects.filter(
|
||||
# User lacks permission, filter findings based on provider groups associated with the role
|
||||
queryset = Finding.all_objects.filter(
|
||||
scan__provider__in=get_providers(user_roles)
|
||||
)
|
||||
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
if search_value:
|
||||
# Django's ORM will build a LEFT JOIN and OUTER JOIN on any "through" tables, resulting in duplicates
|
||||
# The duplicates then require a `distinct` query
|
||||
search_query = SearchQuery(
|
||||
search_value, config="simple", search_type="plain"
|
||||
)
|
||||
|
||||
resource_match = Resource.all_objects.filter(
|
||||
text_search=search_query,
|
||||
id__in=ResourceFindingMapping.objects.filter(
|
||||
resource_id=OuterRef("pk"),
|
||||
tenant_id=tenant_id,
|
||||
).values("resource_id"),
|
||||
)
|
||||
|
||||
queryset = queryset.filter(
|
||||
Q(impact_extended__contains=search_value)
|
||||
| Q(status_extended__contains=search_value)
|
||||
| Q(check_id=search_value)
|
||||
| Q(check_id__icontains=search_value)
|
||||
| Q(text_search=search_query)
|
||||
| Q(resources__uid=search_value)
|
||||
| Q(resources__name=search_value)
|
||||
| Q(resources__region=search_value)
|
||||
| Q(resources__service=search_value)
|
||||
| Q(resources__type=search_value)
|
||||
| Q(resources__uid__contains=search_value)
|
||||
| Q(resources__name__contains=search_value)
|
||||
| Q(resources__region__contains=search_value)
|
||||
| Q(resources__service__contains=search_value)
|
||||
| Q(resources__tags__text_search=search_query)
|
||||
| Q(resources__text_search=search_query)
|
||||
).distinct()
|
||||
Q(text_search=search_query) | Q(Exists(resource_match))
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def inserted_at_to_uuidv7(self, inserted_at):
|
||||
if inserted_at is None:
|
||||
return None
|
||||
return datetime_to_uuid7(inserted_at)
|
||||
def filter_queryset(self, queryset):
|
||||
# Do not apply filters when retrieving specific finding
|
||||
if self.action == "retrieve":
|
||||
return queryset
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
base_qs = self.filter_queryset(self.get_queryset())
|
||||
paginated_ids = self.paginate_queryset(base_qs.values_list("id", flat=True))
|
||||
if paginated_ids is not None:
|
||||
ids = list(paginated_ids)
|
||||
findings = (
|
||||
Finding.all_objects.filter(tenant_id=self.request.tenant_id, id__in=ids)
|
||||
.select_related("scan")
|
||||
.prefetch_related("resources")
|
||||
)
|
||||
# Re-sort in Python to preserve ordering:
|
||||
findings = sorted(findings, key=lambda x: ids.index(x.id))
|
||||
serializer = self.get_serializer(findings, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(base_qs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings_services_regions")
|
||||
def findings_services_regions(self, request):
|
||||
@@ -1394,48 +1621,35 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="metadata")
|
||||
def metadata(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
result = filtered_queryset.aggregate(
|
||||
services=ArrayAgg("resources__service", flat=True, distinct=True),
|
||||
regions=ArrayAgg("resources__region", flat=True, distinct=True),
|
||||
tags=ArrayAgg(
|
||||
JSONObject(
|
||||
key=F("resources__tags__key"), value=F("resources__tags__value")
|
||||
),
|
||||
distinct=True,
|
||||
filter=Q(resources__tags__key__isnull=False),
|
||||
),
|
||||
resource_types=ArrayAgg("resources__type", flat=True, distinct=True),
|
||||
)
|
||||
if result["services"] is None:
|
||||
result["services"] = []
|
||||
if result["regions"] is None:
|
||||
result["regions"] = []
|
||||
if result["regions"] is None:
|
||||
result["regions"] = []
|
||||
if result["resource_types"] is None:
|
||||
result["resource_types"] = []
|
||||
if result["tags"] is None:
|
||||
result["tags"] = []
|
||||
filtered_ids = filtered_queryset.order_by().values("id")
|
||||
|
||||
tags_dict = {}
|
||||
for t in result["tags"]:
|
||||
key, value = t["key"], t["value"]
|
||||
if key not in tags_dict:
|
||||
tags_dict[key] = []
|
||||
tags_dict[key].append(value)
|
||||
relevant_resources = Resource.all_objects.filter(
|
||||
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
|
||||
).only("service", "region", "type")
|
||||
|
||||
result["tags"] = tags_dict
|
||||
|
||||
serializer = self.get_serializer(
|
||||
data=result,
|
||||
aggregation = relevant_resources.aggregate(
|
||||
services=ArrayAgg("service", flat=True),
|
||||
regions=ArrayAgg("region", flat=True),
|
||||
resource_types=ArrayAgg("type", flat=True),
|
||||
)
|
||||
|
||||
services = sorted(set(aggregation["services"] or []))
|
||||
regions = sorted({region for region in aggregation["regions"] or [] if region})
|
||||
resource_types = sorted(set(aggregation["resource_types"] or []))
|
||||
|
||||
result = {
|
||||
"services": services,
|
||||
"regions": regions,
|
||||
"resource_types": resource_types,
|
||||
}
|
||||
|
||||
serializer = self.get_serializer(data=result)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return Response(data=serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
@@ -1999,68 +2213,53 @@ 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(
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
uid=OuterRef("uid"),
|
||||
scan__provider=OuterRef("scan__provider"),
|
||||
state=StateChoices.COMPLETED,
|
||||
)
|
||||
.order_by("-id") # Most recent by id
|
||||
.values("id")[:1]
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
# Aggregate findings by provider
|
||||
findings_aggregated = (
|
||||
recent_findings.values("scan__provider__provider")
|
||||
ScanSummary.objects.filter(tenant_id=tenant_id, scan_id__in=latest_scan_ids)
|
||||
.values("scan__provider__provider")
|
||||
.annotate(
|
||||
findings_passed=Count("id", filter=Q(status=StatusChoices.PASS.value)),
|
||||
findings_failed=Count("id", filter=Q(status=StatusChoices.FAIL.value)),
|
||||
findings_manual=Count(
|
||||
"id", filter=Q(status=StatusChoices.MANUAL.value)
|
||||
),
|
||||
total_findings=Count("id"),
|
||||
findings_passed=Coalesce(Sum("_pass"), 0),
|
||||
findings_failed=Coalesce(Sum("fail"), 0),
|
||||
findings_muted=Coalesce(Sum("muted"), 0),
|
||||
total_findings=Coalesce(Sum("total"), 0),
|
||||
)
|
||||
.order_by("-findings_failed")
|
||||
)
|
||||
|
||||
# Aggregate total resources by provider
|
||||
resources_aggregated = (
|
||||
Resource.objects.filter(tenant_id=tenant_id)
|
||||
.values("provider__provider")
|
||||
.annotate(total_resources=Count("id"))
|
||||
)
|
||||
resources_dict = {
|
||||
row["provider__provider"]: row["total_resources"]
|
||||
for row in resources_aggregated
|
||||
}
|
||||
|
||||
# Combine findings and resources data
|
||||
overview = []
|
||||
for findings in findings_aggregated:
|
||||
provider = findings["scan__provider__provider"]
|
||||
total_resources = next(
|
||||
(
|
||||
res["total_resources"]
|
||||
for res in resources_aggregated
|
||||
if res["provider__provider"] == provider
|
||||
),
|
||||
0,
|
||||
)
|
||||
for row in findings_aggregated:
|
||||
provider_type = row["scan__provider__provider"]
|
||||
overview.append(
|
||||
{
|
||||
"provider": provider,
|
||||
"total_resources": total_resources,
|
||||
"total_findings": findings["total_findings"],
|
||||
"findings_passed": findings["findings_passed"],
|
||||
"findings_failed": findings["findings_failed"],
|
||||
"findings_manual": findings["findings_manual"],
|
||||
"provider": provider_type,
|
||||
"total_resources": resources_dict.get(provider_type, 0),
|
||||
"total_findings": row["total_findings"],
|
||||
"findings_passed": row["findings_passed"],
|
||||
"findings_failed": row["findings_failed"],
|
||||
"findings_muted": row["findings_muted"],
|
||||
}
|
||||
)
|
||||
|
||||
serializer = OverviewProviderSerializer(overview, many=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings")
|
||||
@@ -2075,7 +2274,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-id")
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
@@ -2120,7 +2319,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-id")
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
@@ -2156,7 +2355,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
)
|
||||
.order_by("-id")
|
||||
.order_by("-inserted_at")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ from config.custom_logging import LOGGING # noqa
|
||||
from config.env import BASE_DIR, env # noqa
|
||||
from config.settings.celery import * # noqa
|
||||
from config.settings.partitions import * # noqa
|
||||
from config.settings.sentry import * # noqa
|
||||
from config.settings.social_login import * # noqa
|
||||
|
||||
SECRET_KEY = env("SECRET_KEY", default="secret")
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
@@ -29,6 +31,13 @@ INSTALLED_APPS = [
|
||||
"django_celery_results",
|
||||
"django_celery_beat",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.socialaccount.providers.google",
|
||||
"allauth.socialaccount.providers.github",
|
||||
"dj_rest_auth.registration",
|
||||
"rest_framework.authtoken",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -42,8 +51,11 @@ MIDDLEWARE = [
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"api.middleware.APILoggingMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
CORS_ALLOWED_ORIGINS = ["http://localhost", "http://127.0.0.1"]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
@@ -115,12 +127,6 @@ DJANGO_GUID = {
|
||||
|
||||
DATABASE_ROUTERS = ["api.db_router.MainRouter"]
|
||||
|
||||
# Database connection pool
|
||||
DB_CP_MIN_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MIN_SIZE", 4)
|
||||
DB_CP_MAX_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_SIZE", 10)
|
||||
DB_CP_MAX_IDLE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_IDLE", 36000)
|
||||
DB_CP_MAX_LIFETIME = env.int("DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME", 86400)
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||
@@ -213,3 +219,20 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
|
||||
|
||||
|
||||
TESTING = False
|
||||
|
||||
FINDINGS_MAX_DAYS_IN_RANGE = env.int("DJANGO_FINDINGS_MAX_DAYS_IN_RANGE", 7)
|
||||
|
||||
|
||||
# API export settings
|
||||
DJANGO_TMP_OUTPUT_DIRECTORY = env.str(
|
||||
"DJANGO_TMP_OUTPUT_DIRECTORY", "/tmp/prowler_api_output"
|
||||
)
|
||||
DJANGO_FINDINGS_BATCH_SIZE = env.str("DJANGO_FINDINGS_BATCH_SIZE", 1000)
|
||||
|
||||
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
|
||||
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID = env.str("DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID", "")
|
||||
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY = env.str(
|
||||
"DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY", ""
|
||||
)
|
||||
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN = env.str("DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN", "")
|
||||
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION = env.str("DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION", "")
|
||||
|
||||
@@ -13,14 +13,6 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_PASSWORD", default="prowler"),
|
||||
"HOST": env("POSTGRES_HOST", default="postgres-db"),
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": DB_CP_MIN_SIZE, # noqa: F405
|
||||
"max_size": DB_CP_MAX_SIZE, # noqa: F405
|
||||
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
|
||||
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
|
||||
}
|
||||
},
|
||||
},
|
||||
"admin": {
|
||||
"ENGINE": "psqlextra.backend",
|
||||
@@ -29,14 +21,6 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_ADMIN_PASSWORD", default="S3cret"),
|
||||
"HOST": env("POSTGRES_HOST", default="postgres-db"),
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": DB_CP_MIN_SIZE, # noqa: F405
|
||||
"max_size": DB_CP_MAX_SIZE, # noqa: F405
|
||||
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
|
||||
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
DATABASES["default"] = DATABASES["prowler_user"]
|
||||
|
||||
@@ -14,14 +14,6 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_PASSWORD"),
|
||||
"HOST": env("POSTGRES_HOST"),
|
||||
"PORT": env("POSTGRES_PORT"),
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": DB_CP_MIN_SIZE, # noqa: F405
|
||||
"max_size": DB_CP_MAX_SIZE, # noqa: F405
|
||||
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
|
||||
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
|
||||
}
|
||||
},
|
||||
},
|
||||
"admin": {
|
||||
"ENGINE": "psqlextra.backend",
|
||||
@@ -30,14 +22,6 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_ADMIN_PASSWORD"),
|
||||
"HOST": env("POSTGRES_HOST"),
|
||||
"PORT": env("POSTGRES_PORT"),
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": DB_CP_MIN_SIZE, # noqa: F405
|
||||
"max_size": DB_CP_MAX_SIZE, # noqa: F405
|
||||
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
|
||||
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
DATABASES["default"] = DATABASES["prowler_user"]
|
||||
|
||||
@@ -13,14 +13,6 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_PASSWORD", default="postgres"),
|
||||
"HOST": env("POSTGRES_HOST", default="localhost"),
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": DB_CP_MIN_SIZE, # noqa: F405
|
||||
"max_size": DB_CP_MAX_SIZE, # noqa: F405
|
||||
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
|
||||
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import sentry_sdk
|
||||
from config.env import env
|
||||
|
||||
IGNORED_EXCEPTIONS = [
|
||||
# Authentication Errors from AWS
|
||||
"InvalidToken",
|
||||
"AccessDeniedException",
|
||||
"AuthorizationErrorException",
|
||||
"UnrecognizedClientException",
|
||||
"UnauthorizedOperation",
|
||||
"AuthFailure",
|
||||
"InvalidClientTokenId",
|
||||
"AccessDenied",
|
||||
# Shodan Check
|
||||
"No Shodan API Key",
|
||||
# For now we don't want to log the RequestLimitExceeded errors
|
||||
"RequestLimitExceeded",
|
||||
"ThrottlingException",
|
||||
"Rate exceeded",
|
||||
# The following comes from urllib3
|
||||
# eu-west-1 -- HTTPClientError[126]: An HTTP Client raised an unhandled exception: AWSHTTPSConnectionPool(host='hostname.s3.eu-west-1.amazonaws.com', port=443): Pool is closed.
|
||||
"Pool is closed",
|
||||
]
|
||||
|
||||
|
||||
def before_send(event, hint):
|
||||
"""
|
||||
before_send handles the Sentry events in order to sent them or not
|
||||
"""
|
||||
# Ignore logs with the ignored_exceptions
|
||||
# https://docs.python.org/3/library/logging.html#logrecord-objects
|
||||
if "log_record" in hint:
|
||||
log_msg = hint["log_record"].msg
|
||||
log_lvl = hint["log_record"].levelno
|
||||
|
||||
# Handle Error events and discard the rest
|
||||
if log_lvl == 40 and any(ignored in log_msg for ignored in IGNORED_EXCEPTIONS):
|
||||
return
|
||||
return event
|
||||
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=env.str("DJANGO_SENTRY_DSN", ""),
|
||||
# Add data like request headers and IP for users,
|
||||
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
|
||||
before_send=before_send,
|
||||
send_default_pii=True,
|
||||
_experiments={
|
||||
# Set continuous_profiling_auto_start to True
|
||||
# to automatically start the profiler on when
|
||||
# possible.
|
||||
"continuous_profiling_auto_start": True,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
from config.env import env
|
||||
|
||||
# Google Oauth settings
|
||||
GOOGLE_OAUTH_CLIENT_ID = env("DJANGO_GOOGLE_OAUTH_CLIENT_ID", default="")
|
||||
GOOGLE_OAUTH_CLIENT_SECRET = env("DJANGO_GOOGLE_OAUTH_CLIENT_SECRET", default="")
|
||||
GOOGLE_OAUTH_CALLBACK_URL = env("DJANGO_GOOGLE_OAUTH_CALLBACK_URL", default="")
|
||||
|
||||
GITHUB_OAUTH_CLIENT_ID = env("DJANGO_GITHUB_OAUTH_CLIENT_ID", default="")
|
||||
GITHUB_OAUTH_CLIENT_SECRET = env("DJANGO_GITHUB_OAUTH_CLIENT_SECRET", default="")
|
||||
GITHUB_OAUTH_CALLBACK_URL = env("DJANGO_GITHUB_OAUTH_CALLBACK_URL", default="")
|
||||
|
||||
# Allauth settings
|
||||
ACCOUNT_LOGIN_METHODS = {"email"} # Use Email / Password authentication
|
||||
ACCOUNT_USERNAME_REQUIRED = False
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = "none" # Do not require email confirmation
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
REST_AUTH = {
|
||||
"TOKEN_MODEL": None,
|
||||
"REST_USE_JWT": True,
|
||||
}
|
||||
# django-allauth (social)
|
||||
# Authenticate if local account with this email address already exists
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
|
||||
# Connect local account and social account if local account with that email address already exists
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
|
||||
SOCIALACCOUNT_ADAPTER = "api.adapters.ProwlerSocialAccountAdapter"
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"google": {
|
||||
"APP": {
|
||||
"client_id": GOOGLE_OAUTH_CLIENT_ID,
|
||||
"secret": GOOGLE_OAUTH_CLIENT_SECRET,
|
||||
"key": "",
|
||||
},
|
||||
"SCOPE": [
|
||||
"email",
|
||||
"profile",
|
||||
],
|
||||
"AUTH_PARAMS": {
|
||||
"access_type": "online",
|
||||
},
|
||||
},
|
||||
"github": {
|
||||
"APP": {
|
||||
"client_id": GITHUB_OAUTH_CLIENT_ID,
|
||||
"secret": GITHUB_OAUTH_CLIENT_SECRET,
|
||||
},
|
||||
"SCOPE": [
|
||||
"user",
|
||||
"read:org",
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -486,7 +486,7 @@ def scans_fixture(tenants_fixture, providers_fixture):
|
||||
name="Scan 1",
|
||||
provider=provider,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant_id=tenant.id,
|
||||
started_at="2024-01-02T00:00:00Z",
|
||||
)
|
||||
|
||||
@@ -5,10 +5,14 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from tasks.tasks import perform_scheduled_scan_task
|
||||
|
||||
from api.models import Provider
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Provider, Scan, StateChoices
|
||||
|
||||
|
||||
def schedule_provider_scan(provider_instance: Provider):
|
||||
tenant_id = str(provider_instance.tenant_id)
|
||||
provider_id = str(provider_instance.id)
|
||||
|
||||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
||||
every=24,
|
||||
period=IntervalSchedule.HOURS,
|
||||
@@ -17,23 +21,9 @@ def schedule_provider_scan(provider_instance: Provider):
|
||||
# Create a unique name for the periodic task
|
||||
task_name = f"scan-perform-scheduled-{provider_instance.id}"
|
||||
|
||||
# Schedule the task
|
||||
_, created = PeriodicTask.objects.get_or_create(
|
||||
interval=schedule,
|
||||
name=task_name,
|
||||
task="scan-perform-scheduled",
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"tenant_id": str(provider_instance.tenant_id),
|
||||
"provider_id": str(provider_instance.id),
|
||||
}
|
||||
),
|
||||
one_off=False,
|
||||
defaults={
|
||||
"start_time": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
if PeriodicTask.objects.filter(
|
||||
interval=schedule, name=task_name, task="scan-perform-scheduled"
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
@@ -45,9 +35,36 @@ def schedule_provider_scan(provider_instance: Provider):
|
||||
]
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scheduled_scan = Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.AVAILABLE,
|
||||
scheduled_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Schedule the task
|
||||
periodic_task_instance = PeriodicTask.objects.create(
|
||||
interval=schedule,
|
||||
name=task_name,
|
||||
task="scan-perform-scheduled",
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"provider_id": provider_id,
|
||||
}
|
||||
),
|
||||
one_off=False,
|
||||
start_time=datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
)
|
||||
scheduled_scan.scheduler_task_id = periodic_task_instance.id
|
||||
scheduled_scan.save()
|
||||
|
||||
return perform_scheduled_scan_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": str(provider_instance.tenant_id),
|
||||
"provider_id": str(provider_instance.id),
|
||||
"provider_id": provider_id,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
import boto3
|
||||
import config.django.base as base
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
|
||||
from prowler.config.config import (
|
||||
csv_file_suffix,
|
||||
html_file_suffix,
|
||||
json_ocsf_file_suffix,
|
||||
output_file_timestamp,
|
||||
)
|
||||
from prowler.lib.outputs.csv.csv import CSV
|
||||
from prowler.lib.outputs.html.html import HTML
|
||||
from prowler.lib.outputs.ocsf.ocsf import OCSF
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
# Predefined mapping for output formats and their configurations
|
||||
OUTPUT_FORMATS_MAPPING = {
|
||||
"csv": {
|
||||
"class": CSV,
|
||||
"suffix": csv_file_suffix,
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}},
|
||||
"html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}},
|
||||
}
|
||||
|
||||
|
||||
def _compress_output_files(output_directory: str) -> str:
|
||||
"""
|
||||
Compress output files from all configured output formats into a ZIP archive.
|
||||
Args:
|
||||
output_directory (str): The directory where the output files are located.
|
||||
The function looks up all known suffixes in OUTPUT_FORMATS_MAPPING
|
||||
and compresses those files into a single ZIP.
|
||||
Returns:
|
||||
str: The full path to the newly created ZIP archive.
|
||||
"""
|
||||
zip_path = f"{output_directory}.zip"
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for suffix in [config["suffix"] for config in OUTPUT_FORMATS_MAPPING.values()]:
|
||||
zipf.write(
|
||||
f"{output_directory}{suffix}",
|
||||
f"output/{output_directory.split('/')[-1]}{suffix}",
|
||||
)
|
||||
|
||||
return zip_path
|
||||
|
||||
|
||||
def get_s3_client():
|
||||
"""
|
||||
Create and return a boto3 S3 client using AWS credentials from environment variables.
|
||||
|
||||
This function attempts to initialize an S3 client by reading the AWS access key, secret key,
|
||||
session token, and region from environment variables. It then validates the client by listing
|
||||
available S3 buckets. If an error occurs during this process (for example, due to missing or
|
||||
invalid credentials), it falls back to creating an S3 client without explicitly provided credentials,
|
||||
which may rely on other configuration sources (e.g., IAM roles).
|
||||
|
||||
Returns:
|
||||
boto3.client: A configured S3 client instance.
|
||||
|
||||
Raises:
|
||||
ClientError, NoCredentialsError, or ParamValidationError if both attempts to create a client fail.
|
||||
"""
|
||||
s3_client = None
|
||||
try:
|
||||
s3_client = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY,
|
||||
aws_session_token=settings.DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN,
|
||||
region_name=settings.DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION,
|
||||
)
|
||||
s3_client.list_buckets()
|
||||
except (ClientError, NoCredentialsError, ParamValidationError, ValueError):
|
||||
s3_client = boto3.client("s3")
|
||||
s3_client.list_buckets()
|
||||
|
||||
return s3_client
|
||||
|
||||
|
||||
def _upload_to_s3(tenant_id: str, zip_path: str, scan_id: str) -> str:
|
||||
"""
|
||||
Upload the specified ZIP file to an S3 bucket.
|
||||
If the S3 bucket environment variables are not configured,
|
||||
the function returns None without performing an upload.
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier, used as part of the S3 key prefix.
|
||||
zip_path (str): The local file system path to the ZIP file to be uploaded.
|
||||
scan_id (str): The scan identifier, used as part of the S3 key prefix.
|
||||
Returns:
|
||||
str: The S3 URI of the uploaded file (e.g., "s3://<bucket>/<key>") if successful.
|
||||
None: If the required environment variables for the S3 bucket are not set.
|
||||
Raises:
|
||||
botocore.exceptions.ClientError: If the upload attempt to S3 fails for any reason.
|
||||
"""
|
||||
if not base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET:
|
||||
return
|
||||
|
||||
try:
|
||||
s3 = get_s3_client()
|
||||
s3_key = f"{tenant_id}/{scan_id}/{os.path.basename(zip_path)}"
|
||||
s3.upload_file(
|
||||
Filename=zip_path,
|
||||
Bucket=base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET,
|
||||
Key=s3_key,
|
||||
)
|
||||
return f"s3://{base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET}/{s3_key}"
|
||||
except (ClientError, NoCredentialsError, ParamValidationError, ValueError) as e:
|
||||
logger.error(f"S3 upload failed: {str(e)}")
|
||||
|
||||
|
||||
def _generate_output_directory(
|
||||
output_directory, prowler_provider: object, tenant_id: str, scan_id: str
|
||||
) -> str:
|
||||
"""
|
||||
Generate a file system path for the output directory of a prowler scan.
|
||||
|
||||
This function constructs the output directory path by combining a base
|
||||
temporary output directory, the tenant ID, the scan ID, and details about
|
||||
the prowler provider along with a timestamp. The resulting path is used to
|
||||
store the output files of a prowler scan.
|
||||
|
||||
Note:
|
||||
This function depends on one external variable:
|
||||
- `output_file_timestamp`: A timestamp (as a string) used to uniquely identify the output.
|
||||
|
||||
Args:
|
||||
output_directory (str): The base output directory.
|
||||
prowler_provider (object): An identifier or descriptor for the prowler provider.
|
||||
Typically, this is a string indicating the provider (e.g., "aws").
|
||||
tenant_id (str): The unique identifier for the tenant.
|
||||
scan_id (str): The unique identifier for the scan.
|
||||
|
||||
Returns:
|
||||
str: The constructed file system path for the prowler scan output directory.
|
||||
|
||||
Example:
|
||||
>>> _generate_output_directory("/tmp", "aws", "tenant-1234", "scan-5678")
|
||||
'/tmp/tenant-1234/aws/scan-5678/prowler-output-2023-02-15T12:34:56'
|
||||
"""
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
|
||||
f"{prowler_provider}-{output_file_timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
return path
|
||||
@@ -152,6 +152,9 @@ def perform_prowler_scan(
|
||||
|
||||
for progress, findings in prowler_scan.scan():
|
||||
for finding in findings:
|
||||
if finding is None:
|
||||
logger.error(f"None finding detected on scan {scan_id}.")
|
||||
continue
|
||||
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
@@ -176,7 +179,10 @@ def perform_prowler_scan(
|
||||
|
||||
# Update resource fields if necessary
|
||||
updated_fields = []
|
||||
if resource_instance.region != finding.region:
|
||||
if (
|
||||
finding.region
|
||||
and resource_instance.region != finding.region
|
||||
):
|
||||
resource_instance.region = finding.region
|
||||
updated_fields.append("region")
|
||||
if resource_instance.service != finding.service_name:
|
||||
@@ -222,8 +228,10 @@ def perform_prowler_scan(
|
||||
last_first_seen_at = None
|
||||
if finding_uid not in last_status_cache:
|
||||
most_recent_finding = (
|
||||
Finding.objects.filter(uid=finding_uid)
|
||||
.order_by("-id")
|
||||
Finding.all_objects.filter(
|
||||
tenant_id=tenant_id, uid=finding_uid
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.values("status", "first_seen_at")
|
||||
.first()
|
||||
)
|
||||
@@ -237,8 +245,11 @@ def perform_prowler_scan(
|
||||
|
||||
status = FindingStatus[finding.status]
|
||||
delta = _create_finding_delta(last_status, status)
|
||||
# For the findings prior to the change, when a first finding is found with delta!="new" it will be assigned a current date as first_seen_at and the successive findings with the same UID will always get the date of the previous finding.
|
||||
# For new findings, when a finding (delta="new") is found for the first time, the first_seen_at attribute will be assigned the current date, the following findings will get that date.
|
||||
# For the findings prior to the change, when a first finding is found with delta!="new" it will be
|
||||
# assigned a current date as first_seen_at and the successive findings with the same UID will
|
||||
# always get the date of the previous finding.
|
||||
# For new findings, when a finding (delta="new") is found for the first time, the first_seen_at
|
||||
# attribute will be assigned the current date, the following findings will get that date.
|
||||
if not last_first_seen_at:
|
||||
last_first_seen_at = datetime.now(tz=timezone.utc)
|
||||
|
||||
@@ -333,9 +344,18 @@ def perform_prowler_scan(
|
||||
total_requirements=compliance["total_requirements"],
|
||||
)
|
||||
)
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(
|
||||
compliance_overview_objects, batch_size=100
|
||||
)
|
||||
except Exception as overview_exception:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.capture_exception(overview_exception)
|
||||
logger.error(
|
||||
f"Error storing compliance overview for scan {scan_id}: {overview_exception}"
|
||||
)
|
||||
if exception is not None:
|
||||
raise exception
|
||||
|
||||
@@ -372,7 +392,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
- muted_changed: Muted findings with a delta of 'changed'.
|
||||
"""
|
||||
with rls_transaction(tenant_id):
|
||||
findings = Finding.objects.filter(scan_id=scan_id)
|
||||
findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
aggregation = findings.values(
|
||||
"check_id",
|
||||
|
||||
+165
-27
@@ -1,15 +1,28 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from shutil import rmtree
|
||||
|
||||
from celery import shared_task
|
||||
from celery import chain, shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from config.celery import RLSTask
|
||||
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
from tasks.jobs.export import (
|
||||
OUTPUT_FORMATS_MAPPING,
|
||||
_compress_output_files,
|
||||
_generate_output_directory,
|
||||
_upload_to_s3,
|
||||
)
|
||||
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
|
||||
from tasks.utils import batched, get_next_execution_datetime
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.decorators import set_tenant
|
||||
from api.models import Provider, Scan
|
||||
from api.models import Finding, Provider, Scan, ScanSummary, StateChoices
|
||||
from api.utils import initialize_prowler_provider
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="provider-connection-check")
|
||||
@@ -29,7 +42,7 @@ def check_provider_connection_task(provider_id: str):
|
||||
return check_provider_connection(provider_id=provider_id)
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="provider-deletion")
|
||||
@shared_task(base=RLSTask, name="provider-deletion", queue="deletion")
|
||||
@set_tenant
|
||||
def delete_provider_task(provider_id: str):
|
||||
"""
|
||||
@@ -69,13 +82,22 @@ def perform_scan_task(
|
||||
Returns:
|
||||
dict: The result of the scan execution, typically including the status and results of the performed checks.
|
||||
"""
|
||||
return perform_prowler_scan(
|
||||
result = perform_prowler_scan(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
provider_id=provider_id,
|
||||
checks_to_execute=checks_to_execute,
|
||||
)
|
||||
|
||||
chain(
|
||||
perform_scan_summary_task.si(tenant_id, scan_id),
|
||||
generate_outputs.si(
|
||||
scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id
|
||||
),
|
||||
).apply_async()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, bind=True, name="scan-perform-scheduled", queue="scans")
|
||||
def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
@@ -100,34 +122,49 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
task_id = self.request.id
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=f"scan-perform-scheduled-{provider_id}"
|
||||
)
|
||||
next_scan_date = datetime.combine(
|
||||
datetime.now(timezone.utc), periodic_task_instance.start_time.time()
|
||||
) + timedelta(hours=24)
|
||||
|
||||
scan_instance = Scan.objects.create(
|
||||
next_scan_datetime = get_next_execution_datetime(task_id, provider_id)
|
||||
scan_instance, _ = Scan.objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider=provider_instance,
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
next_scan_at=next_scan_date,
|
||||
task_id=task_id,
|
||||
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
|
||||
scheduler_task_id=periodic_task_instance.id,
|
||||
defaults={"state": StateChoices.SCHEDULED},
|
||||
)
|
||||
|
||||
result = perform_prowler_scan(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=str(scan_instance.id),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
perform_scan_summary_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": tenant_id,
|
||||
"scan_id": str(scan_instance.id),
|
||||
}
|
||||
)
|
||||
scan_instance.task_id = task_id
|
||||
scan_instance.save()
|
||||
|
||||
try:
|
||||
result = perform_prowler_scan(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=str(scan_instance.id),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
with rls_transaction(tenant_id):
|
||||
Scan.objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.SCHEDULED,
|
||||
scheduled_at=next_scan_datetime,
|
||||
scheduler_task_id=periodic_task_instance.id,
|
||||
)
|
||||
|
||||
chain(
|
||||
perform_scan_summary_task.si(tenant_id, scan_instance.id),
|
||||
generate_outputs.si(
|
||||
scan_id=str(scan_instance.id), provider_id=provider_id, tenant_id=tenant_id
|
||||
),
|
||||
).apply_async()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -136,6 +173,107 @@ def perform_scan_summary_task(tenant_id: str, scan_id: str):
|
||||
return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
|
||||
@shared_task(name="tenant-deletion")
|
||||
@shared_task(name="tenant-deletion", queue="deletion")
|
||||
def delete_tenant_task(tenant_id: str):
|
||||
return delete_tenant(pk=tenant_id)
|
||||
|
||||
|
||||
@shared_task(
|
||||
base=RLSTask,
|
||||
name="scan-report",
|
||||
queue="scan-reports",
|
||||
)
|
||||
@set_tenant(keep_tenant=True)
|
||||
def generate_outputs(scan_id: str, provider_id: str, tenant_id: str):
|
||||
"""
|
||||
Process findings in batches and generate output files in multiple formats.
|
||||
|
||||
This function retrieves findings associated with a scan, processes them
|
||||
in batches of 50, and writes each batch to the corresponding output files.
|
||||
It reuses output writer instances across batches, updates them with each
|
||||
batch of transformed findings, and uses a flag to indicate when the final
|
||||
batch is being processed. Finally, the output files are compressed and
|
||||
uploaded to S3.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier.
|
||||
scan_id (str): The scan identifier.
|
||||
provider_id (str): The provider_id id to be used in generating outputs.
|
||||
"""
|
||||
# Initialize the prowler provider
|
||||
prowler_provider = initialize_prowler_provider(Provider.objects.get(id=provider_id))
|
||||
|
||||
# Get the provider UID
|
||||
provider_uid = Provider.objects.get(id=provider_id).uid
|
||||
|
||||
# Generate and ensure the output directory exists
|
||||
output_directory = _generate_output_directory(
|
||||
DJANGO_TMP_OUTPUT_DIRECTORY, provider_uid, tenant_id, scan_id
|
||||
)
|
||||
|
||||
# Define auxiliary variables
|
||||
output_writers = {}
|
||||
scan_summary = FindingOutput._transform_findings_stats(
|
||||
ScanSummary.objects.filter(scan_id=scan_id)
|
||||
)
|
||||
|
||||
# Retrieve findings queryset
|
||||
findings_qs = Finding.all_objects.filter(scan_id=scan_id).order_by("uid")
|
||||
|
||||
# Process findings in batches
|
||||
for batch, is_last_batch in batched(
|
||||
findings_qs.iterator(), DJANGO_FINDINGS_BATCH_SIZE
|
||||
):
|
||||
finding_outputs = [
|
||||
FindingOutput.transform_api_finding(finding, prowler_provider)
|
||||
for finding in batch
|
||||
]
|
||||
|
||||
# Generate output files
|
||||
for mode, config in OUTPUT_FORMATS_MAPPING.items():
|
||||
kwargs = dict(config.get("kwargs", {}))
|
||||
if mode == "html":
|
||||
kwargs["provider"] = prowler_provider
|
||||
kwargs["stats"] = scan_summary
|
||||
|
||||
writer_class = config["class"]
|
||||
if writer_class in output_writers:
|
||||
writer = output_writers[writer_class]
|
||||
writer.transform(finding_outputs)
|
||||
writer.close_file = is_last_batch
|
||||
else:
|
||||
writer = writer_class(
|
||||
findings=finding_outputs,
|
||||
file_path=output_directory,
|
||||
file_extension=config["suffix"],
|
||||
from_cli=False,
|
||||
)
|
||||
writer.close_file = is_last_batch
|
||||
output_writers[writer_class] = writer
|
||||
|
||||
# Write the current batch using the writer
|
||||
writer.batch_write_data_to_file(**kwargs)
|
||||
|
||||
# TODO: Refactor the output classes to avoid this manual reset
|
||||
writer._data = []
|
||||
|
||||
# Compress output files
|
||||
output_directory = _compress_output_files(output_directory)
|
||||
|
||||
# Save to configured storage
|
||||
uploaded = _upload_to_s3(tenant_id, output_directory, scan_id)
|
||||
|
||||
if uploaded:
|
||||
output_directory = uploaded
|
||||
uploaded = True
|
||||
# Remove the local files after upload
|
||||
rmtree(DJANGO_TMP_OUTPUT_DIRECTORY, ignore_errors=True)
|
||||
else:
|
||||
uploaded = False
|
||||
|
||||
# Update the scan instance with the output path
|
||||
Scan.all_objects.filter(id=scan_id).update(output_location=output_directory)
|
||||
|
||||
logger.info(f"Scan output files generated, output location: {output_directory}")
|
||||
|
||||
return {"upload": uploaded}
|
||||
|
||||
@@ -6,6 +6,8 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from tasks.beat import schedule_provider_scan
|
||||
|
||||
from api.models import Scan
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestScheduleProviderScan:
|
||||
@@ -15,9 +17,11 @@ class TestScheduleProviderScan:
|
||||
with patch(
|
||||
"tasks.tasks.perform_scheduled_scan_task.apply_async"
|
||||
) as mock_apply_async:
|
||||
assert Scan.all_objects.count() == 0
|
||||
result = schedule_provider_scan(provider_instance)
|
||||
|
||||
assert result is not None
|
||||
assert Scan.all_objects.count() == 1
|
||||
|
||||
mock_apply_async.assert_called_once_with(
|
||||
kwargs={
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
from tasks.utils import batched, get_next_execution_datetime
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestGetNextExecutionDatetime:
|
||||
@pytest.fixture
|
||||
def setup_periodic_task(self, db):
|
||||
# Create a periodic task with an hourly interval
|
||||
interval = IntervalSchedule.objects.create(
|
||||
every=1, period=IntervalSchedule.HOURS
|
||||
)
|
||||
periodic_task = PeriodicTask.objects.create(
|
||||
name="scan-perform-scheduled-123",
|
||||
task="scan-perform-scheduled",
|
||||
interval=interval,
|
||||
)
|
||||
return periodic_task
|
||||
|
||||
@pytest.fixture
|
||||
def setup_task_result(self, db):
|
||||
# Create a task result record
|
||||
task_result = TaskResult.objects.create(
|
||||
task_id="abc123",
|
||||
task_name="scan-perform-scheduled",
|
||||
status="SUCCESS",
|
||||
date_created=datetime.now(timezone.utc) - timedelta(hours=1),
|
||||
result="Success",
|
||||
)
|
||||
return task_result
|
||||
|
||||
def test_get_next_execution_datetime_success(
|
||||
self, setup_task_result, setup_periodic_task
|
||||
):
|
||||
task_result = setup_task_result
|
||||
periodic_task = setup_periodic_task
|
||||
|
||||
# Mock periodic_task_name on TaskResult
|
||||
with patch.object(
|
||||
TaskResult, "periodic_task_name", return_value=periodic_task.name
|
||||
):
|
||||
next_execution = get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="123"
|
||||
)
|
||||
|
||||
expected_time = task_result.date_created + timedelta(hours=1)
|
||||
assert next_execution == expected_time
|
||||
|
||||
def test_get_next_execution_datetime_fallback_to_provider_id(
|
||||
self, setup_task_result, setup_periodic_task
|
||||
):
|
||||
task_result = setup_task_result
|
||||
|
||||
# Simulate the case where `periodic_task_name` is missing
|
||||
with patch.object(TaskResult, "periodic_task_name", return_value=None):
|
||||
next_execution = get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="123"
|
||||
)
|
||||
|
||||
expected_time = task_result.date_created + timedelta(hours=1)
|
||||
assert next_execution == expected_time
|
||||
|
||||
def test_get_next_execution_datetime_periodic_task_does_not_exist(
|
||||
self, setup_task_result
|
||||
):
|
||||
task_result = setup_task_result
|
||||
|
||||
with pytest.raises(PeriodicTask.DoesNotExist):
|
||||
get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="nonexistent"
|
||||
)
|
||||
|
||||
|
||||
class TestBatchedFunction:
|
||||
def test_empty_iterable(self):
|
||||
result = list(batched([], 3))
|
||||
assert result == [([], True)]
|
||||
|
||||
def test_exact_batches(self):
|
||||
result = list(batched([1, 2, 3, 4], 2))
|
||||
expected = [([1, 2], False), ([3, 4], False), ([], True)]
|
||||
assert result == expected
|
||||
|
||||
def test_inexact_batches(self):
|
||||
result = list(batched([1, 2, 3, 4, 5], 2))
|
||||
expected = [([1, 2], False), ([3, 4], False), ([5], True)]
|
||||
assert result == expected
|
||||
|
||||
def test_batch_size_one(self):
|
||||
result = list(batched([1, 2, 3], 1))
|
||||
expected = [([1], False), ([2], False), ([3], False), ([], True)]
|
||||
assert result == expected
|
||||
|
||||
def test_batch_size_greater_than_length(self):
|
||||
result = list(batched([1, 2, 3], 5))
|
||||
expected = [([1, 2, 3], True)]
|
||||
assert result == expected
|
||||
@@ -0,0 +1,50 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
|
||||
def get_next_execution_datetime(task_id: int, provider_id: str) -> datetime:
|
||||
task_instance = TaskResult.objects.get(task_id=task_id)
|
||||
try:
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=task_instance.periodic_task_name
|
||||
)
|
||||
except PeriodicTask.DoesNotExist:
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=f"scan-perform-scheduled-{provider_id}"
|
||||
)
|
||||
|
||||
interval = periodic_task_instance.interval
|
||||
|
||||
current_scheduled_time = datetime.combine(
|
||||
datetime.now(timezone.utc).date(),
|
||||
task_instance.date_created.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
return current_scheduled_time + timedelta(**{interval.period: interval.every})
|
||||
|
||||
|
||||
def batched(iterable, batch_size):
|
||||
"""
|
||||
Yield successive batches from an iterable.
|
||||
|
||||
Args:
|
||||
iterable: An iterable source of items.
|
||||
batch_size (int): The number of items per batch.
|
||||
|
||||
Yields:
|
||||
tuple: A pair (batch, is_last_batch) where:
|
||||
- batch (list): A list of items (with length equal to batch_size,
|
||||
except possibly for the last batch).
|
||||
- is_last_batch (bool): True if this is the final batch, False otherwise.
|
||||
"""
|
||||
batch = []
|
||||
for item in iterable:
|
||||
batch.append(item)
|
||||
if len(batch) == batch_size:
|
||||
yield batch, False
|
||||
batch = []
|
||||
|
||||
yield batch, True
|
||||
@@ -0,0 +1,24 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_container_iso
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ATTRIBUTES_CATEGORY",
|
||||
"REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_OBJETIVE_NAME",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
return get_section_container_iso(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_CATEGORY", "REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_format3
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_format3(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_format3
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
return get_section_containers_format3(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_format3
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
return get_section_containers_format3(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_format3
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_format3(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -76,7 +76,6 @@ def load_csv_files(csv_files):
|
||||
result = result.replace("_AZURE", " - AZURE")
|
||||
if "KUBERNETES" in result:
|
||||
result = result.replace("_KUBERNETES", " - KUBERNETES")
|
||||
result = result[result.find("CIS_") :]
|
||||
results.append(result)
|
||||
|
||||
unique_results = set(results)
|
||||
@@ -532,8 +531,8 @@ def get_bar_graph(df, column_name):
|
||||
|
||||
# Cut the text if it is too long
|
||||
for i in range(len(colums)):
|
||||
if len(colums[i]) > 15:
|
||||
colums[i] = colums[i][:15] + "..."
|
||||
if len(colums[i]) > 43:
|
||||
colums[i] = colums[i][:43] + "..."
|
||||
|
||||
fig = px.bar(
|
||||
df,
|
||||
|
||||
@@ -165,9 +165,21 @@ else:
|
||||
)
|
||||
|
||||
# For the timestamp, remove the two columns and keep only the date
|
||||
|
||||
data["TIMESTAMP"] = pd.to_datetime(data["TIMESTAMP"])
|
||||
data["ASSESSMENT_TIME"] = data["TIMESTAMP"].dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
# Handle findings from v3 outputs
|
||||
if "FINDING_UNIQUE_ID" in data.columns:
|
||||
data.rename(columns={"FINDING_UNIQUE_ID": "FINDING_UID"}, inplace=True)
|
||||
if "ACCOUNT_ID" in data.columns:
|
||||
data.rename(columns={"ACCOUNT_ID": "ACCOUNT_UID"}, inplace=True)
|
||||
if "ASSESSMENT_START_TIME" in data.columns:
|
||||
data.rename(columns={"ASSESSMENT_START_TIME": "TIMESTAMP"}, inplace=True)
|
||||
if "RESOURCE_ID" in data.columns:
|
||||
data.rename(columns={"RESOURCE_ID": "RESOURCE_UID"}, inplace=True)
|
||||
|
||||
# Remove dupplicates on the finding_uid colummn but keep the last one taking into account the timestamp
|
||||
data = data.sort_values("TIMESTAMP").drop_duplicates("FINDING_UID", keep="last")
|
||||
|
||||
data["ASSESSMENT_TIME"] = data["TIMESTAMP"].dt.strftime("%Y-%m-%d")
|
||||
data_valid = pd.DataFrame()
|
||||
for account in data["ACCOUNT_UID"].unique():
|
||||
all_times = data[data["ACCOUNT_UID"] == account]["ASSESSMENT_TIME"].unique()
|
||||
@@ -1720,7 +1732,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Recomendation: ",
|
||||
"Recommendation: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
@@ -1744,7 +1756,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"RecomendationUrl: ",
|
||||
"RecommendationUrl: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
volumes:
|
||||
- "./api/src/backend:/home/prowler/backend"
|
||||
- "./api/pyproject.toml:/home/prowler/pyproject.toml"
|
||||
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -35,6 +36,9 @@ services:
|
||||
required: false
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- "./ui:/app"
|
||||
- "/app/node_modules"
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine3.20
|
||||
@@ -82,6 +86,8 @@ services:
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
volumes:
|
||||
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -7,6 +7,8 @@ services:
|
||||
required: false
|
||||
ports:
|
||||
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
|
||||
volumes:
|
||||
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -65,6 +67,8 @@ services:
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
volumes:
|
||||
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -19,8 +19,13 @@ For isolation and to avoid conflicts with other environments, we recommend using
|
||||
Then install all dependencies including the ones for developers:
|
||||
```
|
||||
poetry install --with dev
|
||||
poetry shell
|
||||
eval $(poetry env activate) \
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
## Contributing with your code or fixes to Prowler
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ Due to the complexity and differences of each provider use the rest of the provi
|
||||
- [GCP](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/gcp_provider.py)
|
||||
- [Azure](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/azure_provider.py)
|
||||
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
|
||||
- [Microsoft365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/microsoft365/microsoft365_provider.py)
|
||||
|
||||
To facilitate understanding here is a pseudocode of how the most basic provider could be with examples.
|
||||
|
||||
|
||||
@@ -237,3 +237,4 @@ It is really important to check if the current Prowler's permissions for each pr
|
||||
- AWS: https://docs.prowler.cloud/en/latest/getting-started/requirements/#aws-authentication
|
||||
- Azure: https://docs.prowler.cloud/en/latest/getting-started/requirements/#permissions
|
||||
- GCP: https://docs.prowler.cloud/en/latest/getting-started/requirements/#gcp-authentication
|
||||
- Microsoft365: https://docs.prowler.cloud/en/latest/getting-started/requirements/#microsoft365-authentication
|
||||
|
||||
@@ -102,3 +102,32 @@ Those credentials must be associated to a user or service account with proper pe
|
||||
|
||||
???+ note
|
||||
By default, `prowler` will scan all accessible GCP Projects, use flag `--project-ids` to specify the projects to be scanned.
|
||||
|
||||
## Microsoft365
|
||||
|
||||
Prowler for Microsoft365 currently 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.
|
||||
|
||||
|
||||
???+ warning
|
||||
For Prowler App only the Service Principal with an application authentication method is supported.
|
||||
|
||||
### Service Principal authentication
|
||||
|
||||
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"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
```
|
||||
|
||||
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) section to create a service principal.
|
||||
|
||||
### Interactive Browser authentication
|
||||
|
||||
To use `--browser-auth` the user needs to authenticate against Azure using the default browser to start the scan, also `--tenant-id` flag is required.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
+43
-5
@@ -76,7 +76,7 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
poetry shell \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
docker compose up postgres valkey -d \
|
||||
@@ -85,6 +85,12 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
|
||||
???+ important
|
||||
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
|
||||
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
_Commands to run the API Worker_:
|
||||
@@ -93,7 +99,7 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
poetry shell \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
@@ -106,7 +112,7 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
poetry shell \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
@@ -164,7 +170,7 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
* `Python >= 3.9`
|
||||
* `Python pip >= 21.0.0`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
* AWS, GCP, Azure, Microsoft365 and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
@@ -377,6 +383,19 @@ Go to [http://localhost:3000](http://localhost:3000) after installing the app (s
|
||||
<img src="img/sign-up-button.png" alt="Sign Up Button" width="320"/>
|
||||
<img src="img/sign-up.png" alt="Sign Up" width="285"/>
|
||||
|
||||
???+ note "User creation and default tenant behavior"
|
||||
|
||||
When creating a new user, the behavior depends on whether an invitation is provided:
|
||||
|
||||
- **Without an invitation**:
|
||||
|
||||
- A new tenant is automatically created.
|
||||
- The new user is assigned to this tenant.
|
||||
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly created tenant.
|
||||
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
|
||||
|
||||
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
|
||||
|
||||
#### **Log In**
|
||||
Log in with your email and password to start using the Prowler App.
|
||||
|
||||
@@ -404,7 +423,7 @@ While the scan is running, start exploring the findings in these sections:
|
||||
|
||||
### Prowler CLI
|
||||
|
||||
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure` or `kubernetes`):
|
||||
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure`, `microsoft365` or `kubernetes`):
|
||||
|
||||
???+ note
|
||||
If no provider specified, AWS will be used for backward compatibility with most of v2 options.
|
||||
@@ -535,6 +554,7 @@ prowler kubernetes --kubeconfig-file path
|
||||
|
||||
For in-cluster execution, you can use the supplied yaml to run Prowler as a job within a new Prowler namespace:
|
||||
```console
|
||||
kubectl apply -f kubernetes/prowler-sa.yaml
|
||||
kubectl apply -f kubernetes/job.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
@@ -545,5 +565,23 @@ kubectl logs prowler-XXXXX --namespace prowler-ns
|
||||
???+ note
|
||||
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the flag `--context` to specify the context to be scanned and `--namespaces` to specify the namespaces to be scanned.
|
||||
|
||||
#### Microsoft365
|
||||
|
||||
With Microsoft365 you need to specify which auth method is going to be used:
|
||||
|
||||
```console
|
||||
# To use service principal authentication
|
||||
prowler microsoft365 --sp-env-auth
|
||||
|
||||
# To use az cli authentication
|
||||
prowler microsoft365 --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler microsoft365 --browser-auth --tenant-id "XXXXXXXX"
|
||||
|
||||
```
|
||||
|
||||
See more details about Microsoft365 Authentication in [Requirements](getting-started/requirements.md#microsoft365)
|
||||
|
||||
## Prowler v2 Documentation
|
||||
For **Prowler v2 Documentation**, please check it out [here](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md).
|
||||
|
||||
@@ -27,7 +27,12 @@ cd prowler
|
||||
pip install poetry
|
||||
mkdir /tmp/poetry
|
||||
poetry config cache-dir /tmp/poetry
|
||||
poetry shell
|
||||
eval $(poetry env activate)
|
||||
poetry install
|
||||
python prowler.py -v
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
>
|
||||
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
@@ -4,7 +4,7 @@ To allow Prowler assume an identity to start the scan with the required privileg
|
||||
|
||||
To create a Service Principal Application you can use the Azure Portal or the Azure CLI.
|
||||
|
||||
## From Azure Portal
|
||||
## From Azure Portal / Entra Admin Center
|
||||
|
||||
1. Access to Microsoft Entra ID
|
||||
2. In the left menu bar, go to "App registrations"
|
||||
|
||||
@@ -47,6 +47,7 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String |
|
||||
| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String |
|
||||
| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings |
|
||||
| `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer |
|
||||
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer |
|
||||
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer |
|
||||
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
|
||||
|
||||
@@ -36,7 +36,7 @@ In this page you can do multiple functions:
|
||||
* Severity
|
||||
* Service
|
||||
* Status
|
||||
* See wich files has been scanned to generate the dashboard placing your mouse on the `?` icon:
|
||||
* See which files has been scanned to generate the dashboard placing your mouse on the `?` icon:
|
||||
<img src="../img/dashboard/dashboard-files-scanned.png">
|
||||
* Download the `Top Findings by Severity` table using the button `DOWNLOAD THIS TABLE AS CSV` or `DOWNLOAD THIS TABLE AS XLSX`
|
||||
* Click on the provider cards to filter by provider.
|
||||
|
||||
@@ -25,6 +25,9 @@ Prowler will follow the same credentials search as [Google authentication librar
|
||||
|
||||
Those credentials must be associated to a user or service account with proper permissions to do all checks. To make sure, add the `Viewer` role to the member associated with the credentials.
|
||||
|
||||
???+ note
|
||||
Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks.
|
||||
|
||||
## Impersonate Service Account
|
||||
|
||||
If you want to impersonate a GCP service account, you can use the `--impersonate-service-account` argument:
|
||||
@@ -34,23 +37,3 @@ prowler gcp --impersonate-service-account <service-account-email>
|
||||
```
|
||||
|
||||
This argument will use the default credentials to impersonate the service account provided.
|
||||
|
||||
## Service APIs
|
||||
|
||||
Prowler will use the Google Cloud APIs to get the information needed to perform the checks. Make sure that the following APIs are enabled in the project:
|
||||
|
||||
- apikeys.googleapis.com
|
||||
- artifactregistry.googleapis.com
|
||||
- bigquery.googleapis.com
|
||||
- sqladmin.googleapis.com
|
||||
- storage.googleapis.com
|
||||
- compute.googleapis.com
|
||||
- dataproc.googleapis.com
|
||||
- dns.googleapis.com
|
||||
- containerregistry.googleapis.com
|
||||
- container.googleapis.com
|
||||
- iam.googleapis.com
|
||||
- cloudkms.googleapis.com
|
||||
- logging.googleapis.com
|
||||
|
||||
You can enable them automatically using our script [enable_apis_in_projects.sh](https://github.com/prowler-cloud/prowler/blob/master/contrib/gcp/enable_apis_in_projects.sh)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
For in-cluster execution, you can use the supplied yaml files inside `/kubernetes`:
|
||||
|
||||
* [prowler-sa.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-sa.yaml)
|
||||
* [job.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/job.yaml)
|
||||
* [prowler-role.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-role.yaml)
|
||||
* [prowler-rolebinding.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-rolebinding.yaml)
|
||||
@@ -9,6 +10,7 @@ For in-cluster execution, you can use the supplied yaml files inside `/kubernete
|
||||
They can be used to run Prowler as a job within a new Prowler namespace:
|
||||
|
||||
```console
|
||||
kubectl apply -f kubernetes/prowler-sa.yaml
|
||||
kubectl apply -f kubernetes/job.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Microsoft365 authentication
|
||||
|
||||
By default Prowler uses MsGraph Python SDK identity package authentication methods using the class `ClientSecretCredential`.
|
||||
This allows Prowler to authenticate against microsoft365 using the following methods:
|
||||
|
||||
- Service principal authentication by environment variables (Enterprise Application)
|
||||
- Current CLI credentials stored
|
||||
- Interactive browser authentication
|
||||
|
||||
To launch the tool first you need to specify which method is used through the following flags:
|
||||
|
||||
```console
|
||||
# To use service principal authentication
|
||||
prowler microsoft365 --sp-env-auth
|
||||
|
||||
# To use cli authentication
|
||||
prowler microsoft365 --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler microsoft365 --browser-auth --tenant-id "XXXXXXXX"
|
||||
```
|
||||
|
||||
To use Prowler you need to set up also the permissions required to access your resources in your Microsoft365 account, to more details refer to [Requirements](../../getting-started/requirements.md)
|
||||
@@ -71,6 +71,15 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to
|
||||
|
||||
<img src="../../img/aws-role.png" alt="AWS Role" width="700"/>
|
||||
|
||||
???+ note
|
||||
check if your AWS Security Token Service (STS) has the EU (Ireland) endpoint active. If not we will not be able to connect to your AWS account.
|
||||
|
||||
If that is the case your STS configuration may look like this:
|
||||
|
||||
<img src="../../img/sts-configuration.png" alt="AWS Role" width="800"/>
|
||||
|
||||
To solve this issue, please activate the EU (Ireland) STS endpoint.
|
||||
|
||||
---
|
||||
|
||||
### **Step 4.2: Azure Credentials**
|
||||
@@ -99,6 +108,34 @@ By default, the `kubeconfig` file is located at `~/.kube/config`.
|
||||
|
||||
<img src="../../img/kubernetes-credentials.png" alt="Kubernetes Credentials" width="700"/>
|
||||
|
||||
???+ note
|
||||
If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication:
|
||||
|
||||
** Make sure your cluster allow traffic from the Prowler Cloud IP address `52.48.254.174/32` **
|
||||
|
||||
1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
|
||||
```console
|
||||
kubectl apply -f kubernetes/prowler-sa.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
```
|
||||
|
||||
2. Generate a long-lived token for authentication:
|
||||
```console
|
||||
kubectl create token prowler-sa -n prowler-ns --duration=0
|
||||
```
|
||||
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
|
||||
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
|
||||
|
||||
3. Update your `kubeconfig` to use the ServiceAccount token:
|
||||
```console
|
||||
kubectl config set-credentials prowler-sa --token=<SA_TOKEN>
|
||||
kubectl config set-context <CONTEXT_NAME> --user=prowler-sa
|
||||
```
|
||||
Replace <SA_TOKEN> with the generated token and <CONTEXT_NAME> with your KubeConfig Context Name of your EKS, GKE or AKS cluster.
|
||||
|
||||
4. Now you can add the modified `kubeconfig` in Prowler Cloud. Then simply test the connection.
|
||||
|
||||
---
|
||||
|
||||
## **Step 5: Test Connection**
|
||||
@@ -133,3 +170,5 @@ While the scan is running, start exploring the findings in these sections:
|
||||
<img src="../../img/issues.png" alt="Issues" width="300" style="text-align: center;"/>
|
||||
|
||||
- **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more. <img src="../../img/findings.png" alt="Findings" width="700"/>
|
||||
|
||||
To view all `new` findings that have not been seen prior to this scan, click the `Delta` filter and select `new`. To view all `changed` findings that have had a status change (from `PASS` to `FAIL` for example), click the `Delta` filter and select `changed`.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
|
||||
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;accessanalyzer_enabled;Check if IAM Access Analyzer is enabled;IAM;FAIL;IAM Access Analyzer in account <account_uid> is not enabled.;False;accessanalyzer;;low;Other;<resource_uid>;<resource_name>;;;aws;<region>;Check if IAM Access Analyzer is enabled;AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy.;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost).;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;;;aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION>;;CIS-1.4: 1.20 | CIS-1.5: 1.20 | KISA-ISMS-P-2023: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | CIS-2.0: 1.20 | KISA-ISMS-P-2023-korean: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | AWS-Account-Security-Onboarding: Enabled security services, Create analyzers in each active regions, Verify that events are present in SecurityHub aggregated view | CIS-3.0: 1.20;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_current_contact_details;Maintain current contact details.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain current contact details.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;CIS-1.4: 1.1 | CIS-1.5: 1.1 | KISA-ISMS-P-2023: 2.1.3 | CIS-2.0: 1.1 | KISA-ISMS-P-2023-korean: 2.1.3 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.1 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_different_contact_details_to_security_billing_and_operations;Maintain different contact details to security, billing and operations.;IAM;FAIL;SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain different contact details to security, billing and operations.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;KISA-ISMS-P-2023: 2.1.3 | KISA-ISMS-P-2023-korean: 2.1.3;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_security_contact_information_is_registered;Ensure security contact information is registered.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.;False;account;;medium;Other;<resource_uid>:root;<account_uid>;;;aws;<region>;Ensure security contact information is registered.;AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them.;;Go to the My Account section and complete alternate contacts.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console;CIS-1.4: 1.2 | CIS-1.5: 1.2 | AWS-Foundational-Security-Best-Practices: account, acm | KISA-ISMS-P-2023: 2.1.3, 2.2.1 | CIS-2.0: 1.2 | KISA-ISMS-P-2023-korean: 2.1.3, 2.2.1 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.2 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>
|
||||
|
@@ -0,0 +1,625 @@
|
||||
[
|
||||
{
|
||||
"message": "IAM Access Analyzer in account <account_uid> is not enabled.",
|
||||
"metadata": {
|
||||
"event_code": "accessanalyzer_enabled",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "<prowler_version>"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 2,
|
||||
"severity": "Low",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "IAM Access Analyzer in account <account_uid> is not enabled.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-1.4": [
|
||||
"1.20"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"1.20"
|
||||
],
|
||||
"KISA-ISMS-P-2023": [
|
||||
"2.5.6",
|
||||
"2.6.4",
|
||||
"2.8.1",
|
||||
"2.8.2"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"1.20"
|
||||
],
|
||||
"KISA-ISMS-P-2023-korean": [
|
||||
"2.5.6",
|
||||
"2.6.4",
|
||||
"2.8.1",
|
||||
"2.8.2"
|
||||
],
|
||||
"AWS-Account-Security-Onboarding": [
|
||||
"Enabled security services",
|
||||
"Create analyzers in each active regions",
|
||||
"Verify that events are present in SecurityHub aggregated view"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"1.20"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539623,
|
||||
"created_time_dt": "2025-02-14T14:27:03.913874",
|
||||
"desc": "Check if IAM Access Analyzer is enabled",
|
||||
"product_uid": "prowler",
|
||||
"title": "Check if IAM Access Analyzer is enabled",
|
||||
"types": [
|
||||
"IAM"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "<region>",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"arn": "<resource_arn>",
|
||||
"name": "<resource_name>",
|
||||
"status": "NOT_AVAILABLE",
|
||||
"findings": [],
|
||||
"tags": [],
|
||||
"type": "",
|
||||
"region": "<region>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "accessanalyzer"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_name>",
|
||||
"type": "Other",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"type": "AWS Account",
|
||||
"type_id": 10,
|
||||
"uid": "<account_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"region": "<region>"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost).",
|
||||
"references": [
|
||||
"aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION>",
|
||||
"https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy.",
|
||||
"time": 1739539623,
|
||||
"time_dt": "2025-02-14T14:27:03.913874",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.",
|
||||
"metadata": {
|
||||
"event_code": "account_maintain_current_contact_details",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "<prowler_version>"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "MANUAL",
|
||||
"status_detail": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-1.4": [
|
||||
"1.1"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"1.1"
|
||||
],
|
||||
"KISA-ISMS-P-2023": [
|
||||
"2.1.3"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"1.1"
|
||||
],
|
||||
"KISA-ISMS-P-2023-korean": [
|
||||
"2.1.3"
|
||||
],
|
||||
"AWS-Well-Architected-Framework-Security-Pillar": [
|
||||
"SEC03-BP03",
|
||||
"SEC10-BP01"
|
||||
],
|
||||
"AWS-Account-Security-Onboarding": [
|
||||
"Billing, emergency, security contacts"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"1.1"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.ext.7.aws.am.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539623,
|
||||
"created_time_dt": "2025-02-14T14:27:03.913874",
|
||||
"desc": "Maintain current contact details.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Maintain current contact details.",
|
||||
"types": [
|
||||
"IAM"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "<region>",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"type": "PRIMARY",
|
||||
"email": null,
|
||||
"name": "<account_name>",
|
||||
"phone_number": "<value>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "account"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<account_uid>",
|
||||
"type": "Other",
|
||||
"uid": "arn:aws:iam::<account_uid>:root"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"type": "AWS Account",
|
||||
"type_id": 10,
|
||||
"uid": "<account_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"region": "<region>"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Using the Billing and Cost Management console complete contact details.",
|
||||
"references": [
|
||||
"No command available.",
|
||||
"https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
|
||||
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.",
|
||||
"time": 1739539623,
|
||||
"time_dt": "2025-02-14T14:27:03.913874",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.",
|
||||
"metadata": {
|
||||
"event_code": "account_maintain_different_contact_details_to_security_billing_and_operations",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "<prowler_version>"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"KISA-ISMS-P-2023": [
|
||||
"2.1.3"
|
||||
],
|
||||
"KISA-ISMS-P-2023-korean": [
|
||||
"2.1.3"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539623,
|
||||
"created_time_dt": "2025-02-14T14:27:03.913874",
|
||||
"desc": "Maintain different contact details to security, billing and operations.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Maintain different contact details to security, billing and operations.",
|
||||
"types": [
|
||||
"IAM"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "<region>",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"type": "PRIMARY",
|
||||
"email": null,
|
||||
"name": "<account_name>",
|
||||
"phone_number": "<value>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "account"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<account_uid>",
|
||||
"type": "Other",
|
||||
"uid": "arn:aws:iam::<account_uid>:root"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"type": "AWS Account",
|
||||
"type_id": 10,
|
||||
"uid": "<account_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"region": "<region>"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Using the Billing and Cost Management console complete contact details.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
|
||||
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.",
|
||||
"time": 1739539623,
|
||||
"time_dt": "2025-02-14T14:27:03.913874",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.",
|
||||
"metadata": {
|
||||
"event_code": "account_security_contact_information_is_registered",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "<prowler_version>"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "MANUAL",
|
||||
"status_detail": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-1.4": [
|
||||
"1.2"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"1.2"
|
||||
],
|
||||
"AWS-Foundational-Security-Best-Practices": [
|
||||
"account",
|
||||
"acm"
|
||||
],
|
||||
"KISA-ISMS-P-2023": [
|
||||
"2.1.3",
|
||||
"2.2.1"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"1.2"
|
||||
],
|
||||
"KISA-ISMS-P-2023-korean": [
|
||||
"2.1.3",
|
||||
"2.2.1"
|
||||
],
|
||||
"AWS-Well-Architected-Framework-Security-Pillar": [
|
||||
"SEC03-BP03",
|
||||
"SEC10-BP01"
|
||||
],
|
||||
"AWS-Account-Security-Onboarding": [
|
||||
"Billing, emergency, security contacts"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"1.2"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.ext.7.aws.am.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539623,
|
||||
"created_time_dt": "2025-02-14T14:27:03.913874",
|
||||
"desc": "Ensure security contact information is registered.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure security contact information is registered.",
|
||||
"types": [
|
||||
"IAM"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "<region>",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"type": "PRIMARY",
|
||||
"email": null,
|
||||
"name": "<account_name>",
|
||||
"phone_number": "<value>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "account"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<account_uid>",
|
||||
"type": "Other",
|
||||
"uid": "arn:aws:iam::<account_uid>:root"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"type": "AWS Account",
|
||||
"type_id": 10,
|
||||
"uid": "<account_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"region": "<region>"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Go to the My Account section and complete alternate contacts.",
|
||||
"references": [
|
||||
"No command available.",
|
||||
"https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console",
|
||||
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them.",
|
||||
"time": 1739539623,
|
||||
"time_dt": "2025-02-14T14:27:03.913874",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Login to the AWS Console as root. Choose your account name on the top right of the window -> My Account -> Configure Security Challenge Questions.",
|
||||
"metadata": {
|
||||
"event_code": "account_security_questions_are_registered_in_the_aws_account",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "<prowler_version>"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "MANUAL",
|
||||
"status_detail": "Login to the AWS Console as root. Choose your account name on the top right of the window -> My Account -> Configure Security Challenge Questions.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-1.4": [
|
||||
"1.3"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"1.3"
|
||||
],
|
||||
"KISA-ISMS-P-2023": [
|
||||
"2.1.3"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"1.3"
|
||||
],
|
||||
"KISA-ISMS-P-2023-korean": [
|
||||
"2.1.3"
|
||||
],
|
||||
"AWS-Well-Architected-Framework-Security-Pillar": [
|
||||
"SEC03-BP03",
|
||||
"SEC10-BP01"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"1.3"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.ext.7.aws.am.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539623,
|
||||
"created_time_dt": "2025-02-14T14:27:03.913874",
|
||||
"desc": "Ensure security questions are registered in the AWS account.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure security questions are registered in the AWS account.",
|
||||
"types": [
|
||||
"IAM"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "<region>",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"type": "SECURITY",
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone_number": null
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "account"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<account_uid>",
|
||||
"type": "Other",
|
||||
"uid": "arn:aws:iam::<account_uid>:root"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"type": "AWS Account",
|
||||
"type_id": 10,
|
||||
"uid": "<account_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"region": "<region>"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Login as root account and from My Account configure Security questions.",
|
||||
"references": [
|
||||
"No command available.",
|
||||
"https://docs.prowler.com/checks/aws/iam-policies/iam_15",
|
||||
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-security-challenge.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "The AWS support portal allows account owners to establish security questions that can be used to authenticate individuals calling AWS customer service for support. It is recommended that security questions be established. When creating a new AWS account a default super user is automatically created. This account is referred to as the root account. It is recommended that the use of this account be limited and highly controlled. During events in which the root password is no longer accessible or the MFA token associated with root is lost/destroyed it is possible through authentication using secret questions and associated answers to recover root login access.",
|
||||
"time": 1739539623,
|
||||
"time_dt": "2025-02-14T14:27:03.913874",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
|
||||
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_cluster_rbac_enabled;Ensure AKS RBAC is enabled;;PASS;RBAC is enabled for cluster '<resource_name>' in subscription '<account_name>'.;False;aks;;medium;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Azure Kubernetes Service (AKS) can be configured to use Azure Active Directory (AD) for user authentication. In this configuration, you sign in to an AKS cluster using an Azure AD authentication token. You can also configure Kubernetes role-based access control (Kubernetes RBAC) to limit access to cluster resources based a user's identity or group membership.;Kubernetes RBAC and AKS help you secure your cluster access and provide only the minimum required permissions to developers and operators.;https://learn.microsoft.com/en-us/azure/aks/azure-ad-rbac?tabs=portal;;https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle;;https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_2#terraform;;https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-role-based-access-control-for-kubernetes-service.html#;ENS-RD2022: op.acc.2.az.r1.eid.1;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_clusters_created_with_private_nodes;Ensure clusters are created with Private Nodes;;PASS;Cluster '<resource_name>' was created with private nodes in subscription '<account_name>';False;aks;;high;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Disable public IP addresses for cluster nodes, so that they only have private IP addresses. Private Nodes are nodes with no public IP addresses.;Disabling public IP addresses on cluster nodes restricts access to only internal networks, forcing attackers to obtain local network access before attempting to compromise the underlying Kubernetes hosts.;https://learn.microsoft.com/en-us/azure/aks/private-clusters;;https://learn.microsoft.com/en-us/azure/aks/access-private-cluster;;;;;ENS-RD2022: mp.com.4.r2.az.aks.1 | MITRE-ATTACK: T1190, T1530;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_clusters_public_access_disabled;Ensure clusters are created with Private Endpoint Enabled and Public Access Disabled;;FAIL;Public access to nodes is enabled for cluster '<resource_name>' in subscription '<account_name>';False;aks;;high;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Disable access to the Kubernetes API from outside the node network if it is not required.;In a private cluster, the master node has two endpoints, a private and public endpoint. The private endpoint is the internal IP address of the master, behind an internal load balancer in the master's wirtual network. Nodes communicate with the master using the private endpoint. The public endpoint enables the Kubernetes API to be accessed from outside the master's virtual network. Although Kubernetes API requires an authorized token to perform sensitive actions, a vulnerability could potentially expose the Kubernetes publically with unrestricted access. Additionally, an attacker may be able to identify the current cluster and Kubernetes API version and determine whether it is vulnerable to an attack. Unless required, disabling public endpoint will help prevent such threats, and require the attacker to be on the master's virtual network to perform any attack on the Kubernetes API.;https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=azure-portal;To use a private endpoint, create a new private endpoint in your virtual network then create a link between your virtual network and a new private DNS zone;https://learn.microsoft.com/en-us/azure/aks/access-private-cluster?tabs=azure-cli;;;az aks update -n <cluster_name> -g <resource_group> --disable-public-fqdn;;ENS-RD2022: mp.com.4.az.aks.2 | MITRE-ATTACK: T1190, T1530;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_network_policy_enabled;Ensure Network Policy is Enabled and set as appropriate;;PASS;Network policy is enabled for cluster '<resource_name>' in subscription '<account_name>'.;False;aks;;medium;Microsoft.ContainerService/managedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;When you run modern, microservices-based applications in Kubernetes, you often want to control which components can communicate with each other. The principle of least privilege should be applied to how traffic can flow between pods in an Azure Kubernetes Service (AKS) cluster. Let's say you likely want to block traffic directly to back-end applications. The Network Policy feature in Kubernetes lets you define rules for ingress and egress traffic between pods in a cluster.;All pods in an AKS cluster can send and receive traffic without limitations, by default. To improve security, you can define rules that control the flow of traffic. Back-end applications are often only exposed to required front-end services, for example. Or, database components are only accessible to the application tiers that connect to them. Network Policy is a Kubernetes specification that defines access policies for communication between Pods. Using Network Policies, you define an ordered set of rules to send and receive traffic and apply them to a collection of pods that match one or more label selectors. These network policy rules are defined as YAML manifests. Network policies can be included as part of a wider manifest that also creates a deployment or service.;https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-network-security#ns-2-connect-private-networks-together;;https://learn.microsoft.com/en-us/azure/aks/use-network-policies;;https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_4#terraform;;;ENS-RD2022: mp.com.4.r2.az.aks.1;;;;Network Policy requires the Network Policy add-on. This add-on is included automatically when a cluster with Network Policy is created, but for an existing cluster, needs to be added prior to enabling Network Policy. Enabling/Disabling Network Policy causes a rolling update of all cluster nodes, similar to performing a cluster upgrade. This operation is long-running and will block other operations on the cluster (including delete) until it has run to completion. If Network Policy is used, a cluster must have at least 2 nodes of type n1-standard-1 or higher. The recommended minimum size cluster to run Network Policy enforcement is 3 n1-standard-1 instances. Enabling Network Policy enforcement consumes additional resources in nodes. Specifically, it increases the memory footprint of the kube-system process by approximately 128MB, and requires approximately 300 millicores of CPU.;<prowler_version>
|
||||
|
@@ -0,0 +1,552 @@
|
||||
[
|
||||
{
|
||||
"message": "There are no AppInsight configured in subscription <subscription_name>.",
|
||||
"metadata": {
|
||||
"event_code": "appinsights_ensure_is_configured",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 2,
|
||||
"severity": "Low",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "There are no AppInsight configured in subscription <subscription_name>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Because Application Insights relies on a Log Analytics Workspace, an organization will incur additional expenses when using this service.",
|
||||
"compliance": {
|
||||
"CIS-2.1": [
|
||||
"5.3.1"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"mp.s.4.r1.az.nt.2"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"6.3.1"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"5.3.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539650,
|
||||
"created_time_dt": "2025-02-14T14:27:30.710664",
|
||||
"desc": "Application Insights within Azure act as an Application Performance Monitoring solution providing valuable data into how well an application performs and additional information when performing incident response. The types of log data collected include application metrics, telemetry data, and application trace logging data providing organizations with detailed information about application activity and application transactions. Both data sets help organizations adopt a proactive and retroactive means to handle security and performance related metrics within their modern applications.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure Application Insights are Configured.",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "AzureCloud",
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {}
|
||||
},
|
||||
"group": {
|
||||
"name": "appinsights"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "AppInsights",
|
||||
"type": "Microsoft.Insights/components",
|
||||
"uid": "AppInsights"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<subscription_name>",
|
||||
"type": "Azure AD Account",
|
||||
"type_id": 6,
|
||||
"uid": "<subscription_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "<organization_name>",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "azure",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "1. Navigate to Application Insights 2. Under the Basics tab within the PROJECT DETAILS section, select the Subscription 3. Select the Resource group 4. Within the INSTANCE DETAILS, enter a Name 5. Select a Region 6. Next to Resource Mode, select Workspace-based 7. Within the WORKSPACE DETAILS, select the Subscription for the log analytics workspace 8. Select the appropriate Log Analytics Workspace 9. Click Next:Tags > 10. Enter the appropriate Tags as Name, Value pairs. 11. Click Next:Review+Create 12. Click Create.",
|
||||
"references": [
|
||||
"az monitor app-insights component create --app <app name> --resource-group <resource group name> --location <location> --kind 'web' --retention-time <INT days to retain logs> --workspace <log analytics workspace ID> -- subscription <subscription ID>",
|
||||
"https://www.tenable.com/audits/items/CIS_Microsoft_Azure_Foundations_v2.0.0_L2.audit:8a7a608d180042689ad9d3f16aa359f1"
|
||||
]
|
||||
},
|
||||
"risk_details": "Configuring Application Insights provides additional data not found elsewhere within Azure as part of a much larger logging and monitoring program within an organization's Information Security practice. The types and contents of these logs will act as both a potential cost saving measure (application performance) and a means to potentially confirm the source of a potential incident (trace logging). Metrics and Telemetry data provide organizations with a proactive approach to cost savings by monitoring an application's performance, while the trace logging data provides necessary details in a reactive incident response scenario by helping organizations identify the potential source of an incident within their application.",
|
||||
"time": 1739539650,
|
||||
"time_dt": "2025-02-14T14:27:30.710664",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "There is not another correct email configured for subscription <subscription_name>.",
|
||||
"metadata": {
|
||||
"event_code": "defender_additional_email_configured_with_a_security_contact",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "There is not another correct email configured for subscription <subscription_name>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-2.1": [
|
||||
"2.1.18"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.mon.3.r3.az.de.1"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.1.13"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"2.1.19"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539650,
|
||||
"created_time_dt": "2025-02-14T14:27:30.710664",
|
||||
"desc": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure 'Additional email addresses' is Configured with a Security Contact Email",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "AzureCloud",
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"resource_id": "<resource_uid>",
|
||||
"name": "<resource_name>",
|
||||
"emails": "",
|
||||
"phone": "",
|
||||
"alert_notifications_minimal_severity": "High",
|
||||
"alert_notifications_state": "On",
|
||||
"notified_roles": [
|
||||
"Owner"
|
||||
],
|
||||
"notified_roles_state": "On"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "defender"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_name>",
|
||||
"type": "AzureEmailNotifications",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<subscription_name>",
|
||||
"type": "Azure AD Account",
|
||||
"type_id": 6,
|
||||
"uid": "<subscription_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "<organization_name>",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "azure",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "1. From Azure Home select the Portal Menu 2. Select Microsoft Defender for Cloud 3. Click on Environment Settings 4. Click on the appropriate Management Group, Subscription, or Workspace 5. Click on Email notifications 6. Enter a valid security contact email address (or multiple addresses separated by commas) in the Additional email addresses field 7. Click Save",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-security-contact-emails-is-set#terraform",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/security-contact-email.html",
|
||||
"https://learn.microsoft.com/en-us/rest/api/defenderforcloud/security-contacts/list?view=rest-defenderforcloud-2020-01-01-preview&tabs=HTTP"
|
||||
]
|
||||
},
|
||||
"risk_details": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.",
|
||||
"time": 1739539650,
|
||||
"time_dt": "2025-02-14T14:27:30.710664",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Defender Auto Provisioning Log Analytics Agents from subscription <subscription_name> is set to OFF.",
|
||||
"metadata": {
|
||||
"event_code": "defender_auto_provisioning_log_analytics_agent_vms_on",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Defender Auto Provisioning Log Analytics Agents from subscription <subscription_name> is set to OFF.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-data-security",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-2.1": [
|
||||
"2.1.14"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.mon.3.r2.az.de.1",
|
||||
"mp.s.4.r1.az.nt.5"
|
||||
],
|
||||
"MITRE-ATTACK": [
|
||||
"T1190"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.1.1.1"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"2.1.15"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539650,
|
||||
"created_time_dt": "2025-02-14T14:27:30.710664",
|
||||
"desc": "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'. The Microsoft Monitoring Agent scans for various security-related configurations and events such as system updates, OS vulnerabilities, endpoint protection, and provides alerts.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "AzureCloud",
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"resource_id": "<resource_uid>",
|
||||
"resource_name": "<resource_name>",
|
||||
"resource_type": "Microsoft.Security/autoProvisioningSettings",
|
||||
"auto_provision": "Off"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "defender"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_name>",
|
||||
"type": "AzureDefenderPlan",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<subscription_name>",
|
||||
"type": "Azure AD Account",
|
||||
"type_id": 6,
|
||||
"uid": "<subscription_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "<organization_name>",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "azure",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Ensure comprehensive visibility into possible security vulnerabilities, including missing updates, misconfigured operating system security settings, and active threats, allowing for timely mitigation and improved overall security posture",
|
||||
"references": [
|
||||
"https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/SecurityCenter/automatic-provisioning-of-monitoring-agent.html",
|
||||
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/monitoring-components"
|
||||
]
|
||||
},
|
||||
"risk_details": "Missing critical security information about your Azure VMs, such as security alerts, security recommendations, and change tracking.",
|
||||
"time": 1739539650,
|
||||
"time_dt": "2025-02-14T14:27:30.710664",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Container image scan is disabled in subscription <subscription_name>.",
|
||||
"metadata": {
|
||||
"event_code": "defender_container_images_scan_enabled",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Container image scan is disabled in subscription <subscription_name>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-check-health",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "When using an Azure container registry, you might occasionally encounter problems. For example, you might not be able to pull a container image because of an issue with Docker in your local environment. Or, a network issue might prevent you from connecting to the registry.",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1190",
|
||||
"T1525"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539650,
|
||||
"created_time_dt": "2025-02-14T14:27:30.710664",
|
||||
"desc": "Scan images being deployed to Azure (AKS) for vulnerabilities. Vulnerability scanning for images stored in Azure Container Registry is generally available in Azure Security Center. This capability is powered by Qualys, a leading provider of information security. When you push an image to Container Registry, Security Center automatically scans it, then checks for known vulnerabilities in packages or dependencies defined in the file. When the scan completes (after about 10 minutes), Security Center provides details and a security classification for each vulnerability detected, along with guidance on how to remediate issues and protect vulnerable attack surfaces.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure Image Vulnerability Scanning using Azure Defender image scanning or a third party provider",
|
||||
"types": [],
|
||||
"uid": "prowler-azure-defender_container_images_scan_enabled-<subscription_uid>-global-Dender plan for Containers"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "AzureCloud",
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"resource_id": "<resource_uid>",
|
||||
"pricing_tier": "Free",
|
||||
"free_trial_remaining_time": 2592000.0,
|
||||
"extensions": {}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "defender"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_name>",
|
||||
"type": "Microsoft.Security",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<subscription_name>",
|
||||
"type": "Azure AD Account",
|
||||
"type_id": 6,
|
||||
"uid": "<subscription_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "<organization_name>",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "azure",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "",
|
||||
"references": [
|
||||
"https://learn.microsoft.com/en-us/azure/container-registry/scan-images-defender"
|
||||
]
|
||||
},
|
||||
"risk_details": "Vulnerabilities in software packages can be exploited by hackers or malicious users to obtain unauthorized access to local cloud resources. Azure Defender and other third party products allow images to be scanned for known vulnerabilities.",
|
||||
"time": 1739539650,
|
||||
"time_dt": "2025-02-14T14:27:30.710664",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Defender plan Defender for App Services from subscription <subscription_name> is set to OFF (pricing tier not standard).",
|
||||
"metadata": {
|
||||
"event_code": "defender_ensure_defender_for_app_services_is_on",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Defender plan Defender for App Services from subscription <subscription_name> is set to OFF (pricing tier not standard).",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CIS-2.1": [
|
||||
"2.1.2"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"mp.s.4.r1.az.nt.3"
|
||||
],
|
||||
"MITRE-ATTACK": [
|
||||
"T1190",
|
||||
"T1059",
|
||||
"T1204",
|
||||
"T1552",
|
||||
"T1486",
|
||||
"T1499",
|
||||
"T1496",
|
||||
"T1087"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.1.6.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539650,
|
||||
"created_time_dt": "2025-02-14T14:27:30.710664",
|
||||
"desc": "Ensure That Microsoft Defender for App Services Is Set To 'On' ",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure That Microsoft Defender for App Services Is Set To 'On' ",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "AzureCloud",
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"resource_id": "<resource_uid>",
|
||||
"pricing_tier": "Free",
|
||||
"free_trial_remaining_time": 2592000.0,
|
||||
"extensions": {}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "defender"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_name>",
|
||||
"type": "AzureDefenderPlan",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<subscription_name>",
|
||||
"type": "Azure AD Account",
|
||||
"type_id": 6,
|
||||
"uid": "<subscription_uid>",
|
||||
"labels": []
|
||||
},
|
||||
"org": {
|
||||
"name": "<organization_name>",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "azure",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "By <resource_name>, Microsoft Defender for Cloud is not enabled for your App Service instances. Enabling the Defender security service for App Service instances allows for advanced security defense using threat detection capabilities provided by Microsoft Security Response Center.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-azure-defender-is-set-to-on-for-app-service#terraform",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/defender-app-service.html",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/defender-app-service.html"
|
||||
]
|
||||
},
|
||||
"risk_details": "Turning on Microsoft Defender for App Service enables threat detection for App Service, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.",
|
||||
"time": 1739539650,
|
||||
"time_dt": "2025-02-14T14:27:30.710664",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
|
||||
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;apikeys_key_exists;Ensure API Keys Only Exist for Active Services;;PASS;Project <account_uid> does not have active API Keys.;False;apikeys;;medium;API Key;<account_uid>;<account_name>;;;;<region>;API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead.;Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key.;;To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead.;https://cloud.google.com/docs/authentication/api-keys;;;gcloud alpha services api-keys delete;;MITRE-ATTACK: T1098 | CIS-2.0: 1.12 | ENS-RD2022: op.acc.2.gcp.rbak.1 | CIS-3.0: 1.12;;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;artifacts_container_analysis_enabled;Ensure Image Vulnerability Analysis using AR Container Analysis or a third-party provider;Security | Configuration;FAIL;AR Container Analysis is not enabled in project <account_uid>.;False;artifacts;Container Analysis;medium;Service;<resource_uid>;<resource_name>;;;;<region>;Scan images stored in Google Container Registry (GCR) for vulnerabilities using AR Container Analysis or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.;Without image vulnerability scanning, container images stored in Artifact Registry may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.;https://cloud.google.com/artifact-analysis/docs;Enable vulnerability scanning for images stored in Artifact Registry using AR Container Analysis or a third-party provider.;https://cloud.google.com/artifact-analysis/docs/container-scanning-overview;;;gcloud services enable containeranalysis.googleapis.com;;MITRE-ATTACK: T1525 | ENS-RD2022: op.exp.4.r4.gcp.log.1, op.mon.3.gcp.scc.1;;;;By default, AR Container Analysis is disabled.;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;compute_firewall_rdp_access_from_the_internet_allowed;Ensure That RDP Access Is Restricted From the Internet;;PASS;Firewall <resource_name> does not expose port 3389 (RDP) to the internet.;False;networking;;critical;FirewallRule;<resource_uid>;<resource_name>;;;;<region>;GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.;Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.;;Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.;https://cloud.google.com/vpc/docs/using-firewalls;;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command;https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html;MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7;internet-exposed;;;;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;compute_firewall_rdp_access_from_the_internet_allowed;Ensure That RDP Access Is Restricted From the Internet;;PASS;Firewall <resource_name> does not expose port 3389 (RDP) to the internet.;False;networking;;critical;FirewallRule;<resource_uid>;<resource_name>;;;;<region>;GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.;Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.;;Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.;https://cloud.google.com/vpc/docs/using-firewalls;;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command;https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html;MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7;internet-exposed;;;;<prowler_version>
|
||||
|
@@ -0,0 +1,636 @@
|
||||
[
|
||||
{
|
||||
"message": "Project <project_id> does not have active API Keys.",
|
||||
"metadata": {
|
||||
"event_code": "apikeys_key_exists",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "PASS",
|
||||
"status_detail": "Project <project_id> does not have active API Keys.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1098"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"1.12"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.acc.2.gcp.rbak.1"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"1.12"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539640,
|
||||
"created_time_dt": "2025-02-14T14:27:20.697446",
|
||||
"desc": "API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure API Keys Only Exist for Active Services",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"number": "<uid>",
|
||||
"id": "<project_id>",
|
||||
"name": "<project_name>",
|
||||
"organization": {
|
||||
"id": "<tenant_uid>",
|
||||
"name": "organizations/<tenant_uid>",
|
||||
"display_name": "prowler.com"
|
||||
},
|
||||
"labels": {
|
||||
"tag": "test",
|
||||
"tag2": "test2",
|
||||
"generative-language": "enabled"
|
||||
},
|
||||
"lifecycle_state": "ACTIVE"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apikeys"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<project_name>",
|
||||
"type": "API Key",
|
||||
"uid": "<project_id>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<project_name>",
|
||||
"type": "GCP Account",
|
||||
"type_id": 5,
|
||||
"uid": "<project_id>",
|
||||
"labels": [
|
||||
"tag:test"
|
||||
]
|
||||
},
|
||||
"org": {
|
||||
"name": "prowler.com",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "gcp",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead.",
|
||||
"references": [
|
||||
"gcloud alpha services api-keys delete",
|
||||
"https://cloud.google.com/docs/authentication/api-keys"
|
||||
]
|
||||
},
|
||||
"risk_details": "Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key.",
|
||||
"time": 1739539640,
|
||||
"time_dt": "2025-02-14T14:27:20.697446",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "AR Container Analysis is not enabled in project <project_id>.",
|
||||
"metadata": {
|
||||
"event_code": "artifacts_container_analysis_enabled",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "AR Container Analysis is not enabled in project <project_id>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://cloud.google.com/artifact-analysis/docs",
|
||||
"categories": [],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "By default, AR Container Analysis is disabled.",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1525"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"op.exp.4.r4.gcp.log.1",
|
||||
"op.mon.3.gcp.scc.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539640,
|
||||
"created_time_dt": "2025-02-14T14:27:20.697446",
|
||||
"desc": "Scan images stored in Google Container Registry (GCR) for vulnerabilities using AR Container Analysis or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure Image Vulnerability Analysis using AR Container Analysis or a third-party provider",
|
||||
"types": [
|
||||
"Security",
|
||||
"Configuration"
|
||||
],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"number": "538174383574",
|
||||
"id": "<project_id>",
|
||||
"name": "<project_name>",
|
||||
"organization": {
|
||||
"id": "<tenant_uid>",
|
||||
"name": "organizations/<tenant_uid>",
|
||||
"display_name": "prowler.com"
|
||||
},
|
||||
"labels": {
|
||||
"tag": "test",
|
||||
"tag2": "test2",
|
||||
"generative-language": "enabled"
|
||||
},
|
||||
"lifecycle_state": "ACTIVE"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "artifacts"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "AR Container Analysis",
|
||||
"type": "Service",
|
||||
"uid": "containeranalysis.googleapis.com"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<project_name>",
|
||||
"type": "GCP Account",
|
||||
"type_id": 5,
|
||||
"uid": "<project_id>",
|
||||
"labels": [
|
||||
"tag:test"
|
||||
]
|
||||
},
|
||||
"org": {
|
||||
"name": "prowler.com",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "gcp",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Enable vulnerability scanning for images stored in Artifact Registry using AR Container Analysis or a third-party provider.",
|
||||
"references": [
|
||||
"gcloud services enable containeranalysis.googleapis.com",
|
||||
"https://cloud.google.com/artifact-analysis/docs/container-scanning-overview"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without image vulnerability scanning, container images stored in Artifact Registry may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.",
|
||||
"time": 1739539640,
|
||||
"time_dt": "2025-02-14T14:27:20.697446",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
|
||||
"metadata": {
|
||||
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 5,
|
||||
"severity": "Critical",
|
||||
"status": "New",
|
||||
"status_code": "PASS",
|
||||
"status_detail": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1190",
|
||||
"T1199",
|
||||
"T1048",
|
||||
"T1498",
|
||||
"T1046"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"3.7"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"mp.com.1.gcp.fw.1"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.7"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539640,
|
||||
"created_time_dt": "2025-02-14T14:27:20.697446",
|
||||
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure That RDP Access Is Restricted From the Internet",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<resource_id>",
|
||||
"id": "<uid>",
|
||||
"source_ranges": [
|
||||
"<value>"
|
||||
],
|
||||
"direction": "INGRESS",
|
||||
"allowed_rules": [
|
||||
{
|
||||
"IPProtocol": "icmp"
|
||||
}
|
||||
],
|
||||
"project_id": "<project_id>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "networking"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_id>",
|
||||
"type": "FirewallRule",
|
||||
"uid": "<uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<project_name>",
|
||||
"type": "GCP Account",
|
||||
"type_id": 5,
|
||||
"uid": "<project_id>",
|
||||
"labels": [
|
||||
"tag:test",
|
||||
"tag2:test2"
|
||||
]
|
||||
},
|
||||
"org": {
|
||||
"name": "prowler.com",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "gcp",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
|
||||
"https://cloud.google.com/vpc/docs/using-firewalls"
|
||||
]
|
||||
},
|
||||
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
|
||||
"time": 1739539640,
|
||||
"time_dt": "2025-02-14T14:27:20.697446",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
|
||||
"metadata": {
|
||||
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 5,
|
||||
"severity": "Critical",
|
||||
"status": "New",
|
||||
"status_code": "PASS",
|
||||
"status_detail": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1190",
|
||||
"T1199",
|
||||
"T1048",
|
||||
"T1498",
|
||||
"T1046"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"3.7"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"mp.com.1.gcp.fw.1"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.7"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539640,
|
||||
"created_time_dt": "2025-02-14T14:27:20.697446",
|
||||
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure That RDP Access Is Restricted From the Internet",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<resource_id>",
|
||||
"id": "<uid>",
|
||||
"source_ranges": [
|
||||
"<value>"
|
||||
],
|
||||
"direction": "INGRESS",
|
||||
"allowed_rules": [
|
||||
{
|
||||
"IPProtocol": "tcp",
|
||||
"ports": [
|
||||
"0-65535"
|
||||
]
|
||||
},
|
||||
{
|
||||
"IPProtocol": "udp",
|
||||
"ports": [
|
||||
"0-65535"
|
||||
]
|
||||
},
|
||||
{
|
||||
"IPProtocol": "icmp"
|
||||
}
|
||||
],
|
||||
"project_id": "<project_id>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "networking"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_id>",
|
||||
"type": "FirewallRule",
|
||||
"uid": "<uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<project_name>",
|
||||
"type": "GCP Account",
|
||||
"type_id": 5,
|
||||
"uid": "<project_id>",
|
||||
"labels": [
|
||||
"tag:test",
|
||||
"tag2:test2"
|
||||
]
|
||||
},
|
||||
"org": {
|
||||
"name": "prowler.com",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "gcp",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
|
||||
"https://cloud.google.com/vpc/docs/using-firewalls"
|
||||
]
|
||||
},
|
||||
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
|
||||
"time": 1739539640,
|
||||
"time_dt": "2025-02-14T14:27:20.697446",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Firewall <resource_id> does exposes port 3389 (RDP) to the internet.",
|
||||
"metadata": {
|
||||
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"cloud",
|
||||
"datetime"
|
||||
],
|
||||
"tenant_uid": "<tenant_uid>",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 5,
|
||||
"severity": "Critical",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Firewall <resource_id> does exposes port 3389 (RDP) to the internet.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "",
|
||||
"categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"MITRE-ATTACK": [
|
||||
"T1190",
|
||||
"T1199",
|
||||
"T1048",
|
||||
"T1498",
|
||||
"T1046"
|
||||
],
|
||||
"CIS-2.0": [
|
||||
"3.7"
|
||||
],
|
||||
"ENS-RD2022": [
|
||||
"mp.com.1.gcp.fw.1"
|
||||
],
|
||||
"CIS-3.0": [
|
||||
"3.7"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539640,
|
||||
"created_time_dt": "2025-02-14T14:27:20.697446",
|
||||
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure That RDP Access Is Restricted From the Internet",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"region": "global",
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<resource_id>",
|
||||
"id": "<uid>",
|
||||
"source_ranges": [
|
||||
"<value>"
|
||||
],
|
||||
"direction": "INGRESS",
|
||||
"allowed_rules": [
|
||||
{
|
||||
"IPProtocol": "tcp",
|
||||
"ports": [
|
||||
"3389"
|
||||
]
|
||||
}
|
||||
],
|
||||
"project_id": "<project_id>"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "networking"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<resource_id>",
|
||||
"type": "FirewallRule",
|
||||
"uid": "<uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "<project_name>",
|
||||
"type": "GCP Account",
|
||||
"type_id": 5,
|
||||
"uid": "<project_id>",
|
||||
"labels": [
|
||||
"tag:test",
|
||||
"tag2:test2"
|
||||
]
|
||||
},
|
||||
"org": {
|
||||
"name": "prowler.com",
|
||||
"uid": "<tenant_uid>"
|
||||
},
|
||||
"provider": "gcp",
|
||||
"region": "global"
|
||||
},
|
||||
"remediation": {
|
||||
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
|
||||
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
|
||||
"https://cloud.google.com/vpc/docs/using-firewalls"
|
||||
]
|
||||
},
|
||||
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
|
||||
"time": 1739539640,
|
||||
"time_dt": "2025-02-14T14:27:20.697446",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
|
||||
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_always_pull_images_plugin;Ensure that the admission control plugin AlwaysPullImages is set;;FAIL;AlwaysPullImages admission control plugin is not set in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check verifies that the AlwaysPullImages admission control plugin is enabled in the Kubernetes API server. This plugin ensures that every new pod always pulls the required images, enforcing image access control and preventing the use of possibly outdated or altered images.;Without AlwaysPullImages, once an image is pulled to a node, any pod can use it without any authorization check, potentially leading to security risks.;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages;Configure the API server to use the AlwaysPullImages admission control plugin to ensure image security and integrity.;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-admission-control-plugin-alwayspullimages-is-set#kubernetes;;--enable-admission-plugins=...,AlwaysPullImages,...;;CIS-1.10: 1.2.11 | CIS-1.8: 1.2.11;cluster-security;;;Enabling AlwaysPullImages can increase network and registry load and decrease container startup speed. It may not be suitable for all environments.;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_anonymous_requests;Ensure that the --anonymous-auth argument is set to false;;PASS;API Server does not have anonymous-auth enabled in pod <resource_uid>;False;apiserver;;high;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;Disable anonymous requests to the API server. When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests, which are then served by the API server. Disallowing anonymous requests strengthens security by ensuring all access is authenticated.;Enabling anonymous access to the API server can expose the cluster to unauthorized access and potential security vulnerabilities.;https://kubernetes.io/docs/admin/authentication/#anonymous-requests;Ensure the --anonymous-auth argument in the API server is set to false. This will reject all anonymous requests, enforcing authenticated access to the server.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-anonymous-auth-argument-is-set-to-false-1#kubernetes;;--anonymous-auth=false;;CIS-1.10: 1.2.1 | CIS-1.8: 1.2.1;trustboundaries;;;While anonymous access can be useful for health checks and discovery, consider the security implications for your specific environment.;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_audit_log_maxage_set;Ensure that the --audit-log-maxage argument is set to 30 or as appropriate;;FAIL;Audit log max age is not set to 30 or as appropriate in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check ensures that the Kubernetes API server is configured with an appropriate audit log retention period. Setting --audit-log-maxage to 30 or as per business requirements helps in maintaining logs for sufficient time to investigate past events.;Without an adequate log retention period, there may be insufficient audit history to investigate and analyze past events or security incidents.;https://kubernetes.io/docs/concepts/cluster-administration/audit/;Configure the API server audit log retention period to retain logs for at least 30 days or as per your organization's requirements.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxage-argument-is-set-to-30-or-as-appropriate#kubernetes;;--audit-log-maxage=30;;CIS-1.10: 1.2.17 | CIS-1.8: 1.2.18;logging;;;Ensure the audit log retention period is set appropriately to balance between storage constraints and the need for historical data.;<prowler_version>
|
||||
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_audit_log_maxbackup_set;Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate;;FAIL;Audit log max backup is not set to 10 or as appropriate in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check ensures that the Kubernetes API server is configured with an appropriate number of audit log backups. Setting --audit-log-maxbackup to 10 or as per business requirements helps maintain a sufficient log backup for investigations or analysis.;Without an adequate number of audit log backups, there may be insufficient log history to investigate past events or security incidents.;https://kubernetes.io/docs/concepts/cluster-administration/audit/;Configure the API server audit log backup retention to 10 or as per your organization's requirements.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxbackup-argument-is-set-to-10-or-as-appropriate#kubernetes;;--audit-log-maxbackup=10;;CIS-1.10: 1.2.18 | CIS-1.8: 1.2.19;logging;;;Ensure the audit log backup retention period is set appropriately to balance between storage constraints and the need for historical data.;<prowler_version>
|
||||
|
@@ -0,0 +1,800 @@
|
||||
[
|
||||
{
|
||||
"message": "AlwaysPullImages admission control plugin is not set in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_always_pull_images_plugin",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "AlwaysPullImages admission control plugin is not set in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages",
|
||||
"categories": [
|
||||
"cluster-security"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Enabling AlwaysPullImages can increase network and registry load and decrease container startup speed. It may not be suitable for all environments.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.11"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.11"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "This check verifies that the AlwaysPullImages admission control plugin is enabled in the Kubernetes API server. This plugin ensures that every new pod always pulls the required images, enforcing image access control and preventing the use of possibly outdated or altered images.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the admission control plugin AlwaysPullImages is set",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Configure the API server to use the AlwaysPullImages admission control plugin to ensure image security and integrity.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-admission-control-plugin-alwayspullimages-is-set#kubernetes",
|
||||
"--enable-admission-plugins=...,AlwaysPullImages,...",
|
||||
"https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without AlwaysPullImages, once an image is pulled to a node, any pod can use it without any authorization check, potentially leading to security risks.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "API Server does not have anonymous-auth enabled in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_anonymous_requests",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"status": "New",
|
||||
"status_code": "PASS",
|
||||
"status_detail": "API Server does not have anonymous-auth enabled in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/admin/authentication/#anonymous-requests",
|
||||
"categories": [
|
||||
"trustboundaries"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "While anonymous access can be useful for health checks and discovery, consider the security implications for your specific environment.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.1"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "Disable anonymous requests to the API server. When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests, which are then served by the API server. Disallowing anonymous requests strengthens security by ensuring all access is authenticated.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the --anonymous-auth argument is set to false",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<resource_uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Ensure the --anonymous-auth argument in the API server is set to false. This will reject all anonymous requests, enforcing authenticated access to the server.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-anonymous-auth-argument-is-set-to-false-1#kubernetes",
|
||||
"--anonymous-auth=false",
|
||||
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
|
||||
]
|
||||
},
|
||||
"risk_details": "Enabling anonymous access to the API server can expose the cluster to unauthorized access and potential security vulnerabilities.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Audit log max age is not set to 30 or as appropriate in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_audit_log_maxage_set",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Audit log max age is not set to 30 or as appropriate in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
|
||||
"categories": [
|
||||
"logging"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Ensure the audit log retention period is set appropriately to balance between storage constraints and the need for historical data.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.17"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.18"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate audit log retention period. Setting --audit-log-maxage to 30 or as per business requirements helps in maintaining logs for sufficient time to investigate past events.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the --audit-log-maxage argument is set to 30 or as appropriate",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<resource_uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Configure the API server audit log retention period to retain logs for at least 30 days or as per your organization's requirements.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxage-argument-is-set-to-30-or-as-appropriate#kubernetes",
|
||||
"--audit-log-maxage=30",
|
||||
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without an adequate log retention period, there may be insufficient audit history to investigate and analyze past events or security incidents.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Audit log max backup is not set to 10 or as appropriate in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_audit_log_maxbackup_set",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Audit log max backup is not set to 10 or as appropriate in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
|
||||
"categories": [
|
||||
"logging"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Ensure the audit log backup retention period is set appropriately to balance between storage constraints and the need for historical data.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.18"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.19"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate number of audit log backups. Setting --audit-log-maxbackup to 10 or as per business requirements helps maintain a sufficient log backup for investigations or analysis.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<resource_uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Configure the API server audit log backup retention to 10 or as per your organization's requirements.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxbackup-argument-is-set-to-10-or-as-appropriate#kubernetes",
|
||||
"--audit-log-maxbackup=10",
|
||||
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without an adequate number of audit log backups, there may be insufficient log history to investigate past events or security incidents.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Audit log max size is not set to 100 MB or as appropriate in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_audit_log_maxsize_set",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 3,
|
||||
"severity": "Medium",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Audit log max size is not set to 100 MB or as appropriate in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
|
||||
"categories": [
|
||||
"logging"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Adjust the audit log file size limit based on your organization's storage capabilities and logging requirements.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.19"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.20"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate audit log file size limit. Setting --audit-log-maxsize to 100 MB or as per business requirements helps manage the size of log files and prevents them from growing excessively large.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<resource_uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Configure the API server audit log file size limit to 100 MB or as per your organization's requirements.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxsize-argument-is-set-to-100-or-as-appropriate#kubernetes",
|
||||
"--audit-log-maxsize=100",
|
||||
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without an appropriate audit log file size limit, log files can grow excessively large, potentially leading to storage issues and difficulty in log analysis.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
},
|
||||
{
|
||||
"message": "Audit log path is not set in pod <pod>.",
|
||||
"metadata": {
|
||||
"event_code": "apiserver_audit_log_path_set",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"uid": "prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "5.4.0"
|
||||
},
|
||||
"profiles": [
|
||||
"container",
|
||||
"datetime"
|
||||
],
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "Audit log path is not set in pod <pod>.",
|
||||
"status_id": 1,
|
||||
"unmapped": {
|
||||
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
|
||||
"categories": [
|
||||
"logging"
|
||||
],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"notes": "Audit logs are not enabled by default in Kubernetes. Configuring them is essential for security monitoring and forensic analysis.",
|
||||
"compliance": {
|
||||
"CIS-1.10": [
|
||||
"1.2.16"
|
||||
],
|
||||
"CIS-1.8": [
|
||||
"1.2.17"
|
||||
]
|
||||
}
|
||||
},
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": 1739539658,
|
||||
"created_time_dt": "2025-02-14T14:27:38.533897",
|
||||
"desc": "This check verifies that the Kubernetes API server is configured with an audit log path. Enabling audit logs helps in maintaining a chronological record of all activities and operations which can be critical for security analysis and troubleshooting.",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure that the --audit-log-path argument is set",
|
||||
"types": [],
|
||||
"uid": "<finding_uid>"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"data": {
|
||||
"details": "",
|
||||
"metadata": {
|
||||
"name": "<pod>",
|
||||
"uid": "<resource_uid>",
|
||||
"namespace": "<namespace>",
|
||||
"labels": {
|
||||
"component": "kube-apiserver",
|
||||
"tier": "control-plane"
|
||||
},
|
||||
"annotations": {
|
||||
"kubernetes.io/config.source": "file"
|
||||
},
|
||||
"node_name": "<node_name>",
|
||||
"service_account": null,
|
||||
"status_phase": "Running",
|
||||
"pod_ip": "<ip>",
|
||||
"host_ip": "<ip>",
|
||||
"host_pid": null,
|
||||
"host_ipc": null,
|
||||
"host_network": "True",
|
||||
"security_context": {
|
||||
"app_armor_profile": null,
|
||||
"fs_group": null,
|
||||
"fs_group_change_policy": null,
|
||||
"run_as_group": null,
|
||||
"run_as_non_root": null,
|
||||
"run_as_user": null,
|
||||
"se_linux_change_policy": null,
|
||||
"se_linux_options": null,
|
||||
"seccomp_profile": {
|
||||
"localhost_profile": null,
|
||||
"type": "RuntimeDefault"
|
||||
},
|
||||
"supplemental_groups": null,
|
||||
"supplemental_groups_policy": null,
|
||||
"sysctls": null,
|
||||
"windows_options": null
|
||||
},
|
||||
"containers": {
|
||||
"kube-apiserver": {
|
||||
"name": "kube-apiserver",
|
||||
"image": "<image>",
|
||||
"command": [
|
||||
"<command>"
|
||||
],
|
||||
"ports": null,
|
||||
"env": null,
|
||||
"security_context": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"name": "apiserver"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "<pod>",
|
||||
"namespace": "<namespace>",
|
||||
"type": "KubernetesAPIServer",
|
||||
"uid": "<resource_uid>"
|
||||
}
|
||||
],
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "Detection Finding",
|
||||
"class_uid": 2004,
|
||||
"remediation": {
|
||||
"desc": "Enable audit logging in the API server by specifying a valid path for --audit-log-path to ensure comprehensive activity logging within the cluster.",
|
||||
"references": [
|
||||
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-path-argument-is-set#kubernetes",
|
||||
"--audit-log-path=/var/log/apiserver/audit.log",
|
||||
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
|
||||
]
|
||||
},
|
||||
"risk_details": "Without audit logs, it becomes difficult to track changes and activities within the cluster, potentially obscuring the detection of malicious activities or operational issues.",
|
||||
"time": 1739539658,
|
||||
"time_dt": "2025-02-14T14:27:38.533897",
|
||||
"type_uid": 200401,
|
||||
"type_name": "Detection Finding: Create"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup and Configure Logger\n",
|
||||
"This section configures the Python logging system to filter Prowler's output messages during security scans. We set the logging level to `CRITICAL`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import logging\n",
|
||||
"\n",
|
||||
"logging.basicConfig(level=logging.CRITICAL)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Initialize AWS Provider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import the Prowler's provider you want to scan\n",
|
||||
"from prowler.providers.aws.aws_provider import AwsProvider\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"# Path to credentials file\n",
|
||||
"credentials_path = \"./secrets-sdk/credentials.json\"\n",
|
||||
"\n",
|
||||
"# Load credentials from JSON file\n",
|
||||
"try:\n",
|
||||
" with open(credentials_path, \"r\") as f:\n",
|
||||
" aws_credentials = json.load(f)\n",
|
||||
" print(\"AWS credentials loaded successfully from file\")\n",
|
||||
"except (FileNotFoundError, json.JSONDecodeError):\n",
|
||||
" print(\"Invalid or missing JSON credentials file\")\n",
|
||||
" aws_credentials = {\n",
|
||||
" \"aws_access_key_id\": \"YOUR_ACCESS_KEY\",\n",
|
||||
" \"aws_secret_access_key\": \"YOUR_SECRET_KEY\",\n",
|
||||
" \"aws_session_token\": \"YOUR_SESSION_TOKEN\"\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"# Optional: Test the AWS provider credentials before instantiation to verify that credentials work\n",
|
||||
"aws_connection = AwsProvider.test_connection(**aws_credentials)\n",
|
||||
"print(f\"AWS Test Connection:\\n\\t- Connected: {aws_connection.is_connected}\\n\\t- Error (if any): {aws_connection.error}\\n\")\n",
|
||||
"\n",
|
||||
"# Initialize the AWS provider with static credentials\n",
|
||||
"aws = AwsProvider(**aws_credentials)\n",
|
||||
"\n",
|
||||
"# AWS Identity Information\n",
|
||||
"print(f\"AWS Identity Information:\\n\\t- Account Number: {aws.identity.account}\\n\\t- User ID: {aws.identity.user_id}\\n\")\n",
|
||||
"\n",
|
||||
"# Alternative Providers (commented out)\n",
|
||||
"# from prowler.providers.gcp.gcp_provider import GcpProvider\n",
|
||||
"# from prowler.providers.azure.azure_provider import AzureProvider\n",
|
||||
"# from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider\n",
|
||||
"# from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pprint\n",
|
||||
"pprint.pp(aws.identity)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Mutelist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Mutelist\n",
|
||||
"from prowler.providers.aws.lib.mutelist.mutelist import AWSMutelist\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"mutelist_content = {\n",
|
||||
" \"Accounts\": {\n",
|
||||
" \"*\": {\n",
|
||||
" \"Checks\": {\n",
|
||||
" \"s3_account_level_public_access_blocks\": {\n",
|
||||
" \"Tags\": [\"*\"],\n",
|
||||
" \"Regions\": [\"*\"],\n",
|
||||
" \"Resources\": [\"*\"],\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"mutelist_object = AWSMutelist(\n",
|
||||
" mutelist_content=mutelist_content,\n",
|
||||
")\n",
|
||||
"aws._mutelist = mutelist_object"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## List Available Security Checks\n",
|
||||
"Explore different ways to list security checks by provider, service, severity, and category."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import the CheckMetadata class to list security checks\n",
|
||||
"from prowler.lib.check.models import CheckMetadata\n",
|
||||
"\n",
|
||||
"# List all available checks\n",
|
||||
"checks = CheckMetadata.list()\n",
|
||||
"print(f\"# Checks: {len(checks)}\")\n",
|
||||
"\n",
|
||||
"# List all AWS S3 checks\n",
|
||||
"aws_s3_checks = CheckMetadata.list(provider=\"aws\", service=\"s3\")\n",
|
||||
"print(f\"AWS S3 Checks:\\n\\t- {'\\n\\t- '.join(aws_s3_checks)}\")\n",
|
||||
"\n",
|
||||
"# List all critical severity checks\n",
|
||||
"critical_checks = CheckMetadata.list(provider=\"aws\", severity=\"critical\")\n",
|
||||
"print(f\"\\n# Critical Checks: {len(critical_checks)}\")\n",
|
||||
"\n",
|
||||
"# List all checks in the internet-exposed category\n",
|
||||
"internet_exposed_checks = CheckMetadata.list(provider=\"aws\", category=\"internet-exposed\")\n",
|
||||
"print(f\"\\n# Internet-Exposed Category Checks: {len(internet_exposed_checks)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Execute Security Scans\n",
|
||||
"Set up and execute security scans on AWS resources with different filtering options."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import necessary libraries for scanning\n",
|
||||
"from prowler.lib.scan.scan import Scan\n",
|
||||
"# Auxiliary libraries\n",
|
||||
"import itertools\n",
|
||||
"\n",
|
||||
"# Set up the Scan class to scan all checks for the provider\n",
|
||||
"scan = Scan(provider=aws)\n",
|
||||
"\n",
|
||||
"# Parametrize the Scan to execute several checks, services, categories, compliances, etc.\n",
|
||||
"scan_s3 = Scan(provider=aws, services=[\"s3\"], severities=[\"critical\", \"high\"])\n",
|
||||
"# scan_critical = Scan(provider=aws, severities=[\"critical\"])\n",
|
||||
"# scan_internet_exposed = Scan(provider=aws, categories=[\"internet-exposed\"])\n",
|
||||
"\n",
|
||||
"# Start the scan with the `scan` method. This returns a generator with findings and progress.\n",
|
||||
"print(\"\\n##### Scanning AWS #####\")\n",
|
||||
"all_findings = []\n",
|
||||
"total_findings = 0\n",
|
||||
"for progress, findings in scan_s3.scan():\n",
|
||||
" all_findings.extend(findings)\n",
|
||||
" total_findings += len(findings)\n",
|
||||
" print(f\"- Scan Progress: {progress}\\n- # Findings: {total_findings}\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Process and Display Findings\n",
|
||||
"Process the scan results and display detailed information about each finding."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(\"Finding's Details:\")\n",
|
||||
"for finding in all_findings:\n",
|
||||
" print(f\"\"\"\n",
|
||||
" - Check ID: {finding.metadata.CheckID}\n",
|
||||
" - Status: {str(finding.status)}\n",
|
||||
" - Status Extended: {finding.status_extended}\n",
|
||||
" - Resource ID: {finding.resource_uid}\n",
|
||||
" - Resource Metadata: {finding.resource_metadata}\n",
|
||||
" \"\"\")\n",
|
||||
"\n",
|
||||
"# Retrieve all findings in one line\n",
|
||||
"print(\"\\n##### Getting all findings in one line #####\")\n",
|
||||
"scan_s3 = Scan(provider=aws, services=[\"s3\"], severities=[\"critical\"])\n",
|
||||
"all_findings_one_line = list(itertools.chain.from_iterable(findings for _, findings in scan_s3.scan()))\n",
|
||||
"print(f\"Total findings collected in one line: {len(all_findings_one_line)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Check Metatada\n",
|
||||
"```plain\n",
|
||||
"CheckMetadata(\n",
|
||||
" Provider='aws'\n",
|
||||
" CheckID='s3_bucket_policy_public_write_access'\n",
|
||||
" CheckTitle='Check if S3 buckets have policies which allow WRITE access.'\n",
|
||||
" CheckType=['IAM']\n",
|
||||
" CheckAliases=[]\n",
|
||||
" ServiceName='s3'\n",
|
||||
" SubServiceName=''\n",
|
||||
" ResourceIdTemplate='arn:partition:s3:::bucket_name'\n",
|
||||
" Severity=<Severity.critical: 'critical'>\n",
|
||||
" ResourceType='AwsS3Bucket'\n",
|
||||
" Description='Check if S3 buckets have policies which allow WRITE access.'\n",
|
||||
" Risk='Non intended users can put objects in a given bucket.'\n",
|
||||
" RelatedUrl=''\n",
|
||||
" Remediation=\n",
|
||||
" Remediation(\n",
|
||||
" Code=Code(\n",
|
||||
" NativeIaC=''\n",
|
||||
" Terraform=''\n",
|
||||
" CLI=''\n",
|
||||
" Other='https://docs.prowler.com/checks/aws/s3-policies/s3_18-write-permissions-public#aws-console')\n",
|
||||
" Recommendation=Recommendation(Text='Ensure proper bucket policy is in place with the least privilege principle applied.'\n",
|
||||
" Url='https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html'\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" Categories=['internet-exposed']\n",
|
||||
" DependsOn=[]\n",
|
||||
" RelatedTo=[]\n",
|
||||
" Notes=''\n",
|
||||
" # Compliance framework: A list of requirement IDs where the check is present.\n",
|
||||
" Compliance={\n",
|
||||
" \"CIS-1.10\": [\"5.2.13\"],\n",
|
||||
" \"CIS-1.8\": [\"5.2.13\"]\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Output Formats"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import necessary libraries for output\n",
|
||||
"from prowler.lib.outputs.csv.csv import CSV\n",
|
||||
"from prowler.lib.outputs.ocsf.ocsf import OCSF\n",
|
||||
"from prowler.lib.outputs.asff.asff import ASFF # Only for AWS\n",
|
||||
"from prowler.lib.outputs.html.html import HTML\n",
|
||||
"from prowler.lib.outputs.outputs import extract_findings_statistics\n",
|
||||
"import datetime\n",
|
||||
"\n",
|
||||
"# Get current date and time in YYYY-MM-DD_HH-MM-SS format for filenames\n",
|
||||
"current_datetime = datetime.datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\")\n",
|
||||
"\n",
|
||||
"# Write findings to CSV file\n",
|
||||
"print(\"Writing findings to CSV file...\")\n",
|
||||
"csv_filename = f\"./output/findings_{current_datetime}.csv\"\n",
|
||||
"csv = CSV(findings=all_findings, create_file_descriptor=True, file_path=csv_filename)\n",
|
||||
"csv.batch_write_data_to_file()\n",
|
||||
"print(f\"Done! CSV File Path: {csv._file_descriptor.name}\")\n",
|
||||
"\n",
|
||||
"# Write findings to OCSF file\n",
|
||||
"print(\"Writing findings to OCSF file...\")\n",
|
||||
"ocsf_filename = f\"./output/findings_{current_datetime}.ocsf\"\n",
|
||||
"ocsf = OCSF(findings=all_findings, create_file_descriptor=True, file_path=ocsf_filename)\n",
|
||||
"ocsf.batch_write_data_to_file()\n",
|
||||
"print(f\"Done! OCSF File Path: {ocsf._file_descriptor.name}\")\n",
|
||||
"\n",
|
||||
"# Write findings to ASFF file\n",
|
||||
"print(\"Writing findings to ASFF file...\")\n",
|
||||
"asff_filename = f\"./output/findings_{current_datetime}.asff\"\n",
|
||||
"asff = ASFF(findings=all_findings, create_file_descriptor=True, file_path=asff_filename)\n",
|
||||
"asff.batch_write_data_to_file()\n",
|
||||
"print(f\"Done! ASFF File Path: {asff._file_descriptor.name}\")\n",
|
||||
"\n",
|
||||
"# Write findings to HTML file\n",
|
||||
"print(\"Writing findings to HTML file...\")\n",
|
||||
"html_filename = f\"./output/findings_{current_datetime}.html\"\n",
|
||||
"stats = extract_findings_statistics(all_findings)\n",
|
||||
"html = HTML(findings=all_findings, create_file_descriptor=True, file_path=html_filename)\n",
|
||||
"html.batch_write_data_to_file(provider=aws, stats=stats)\n",
|
||||
"print(f\"Done! HTML File Path: {html._file_descriptor.name}\")\n",
|
||||
"\n",
|
||||
"# IMPORTANT: The create_file_descriptor parameter will be removed in 5.4.0\n",
|
||||
"# The file descriptor will be created by default when the Output class is instantiated"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Integrate with AWS S3\n",
|
||||
"Send findings to AWS S3."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import the S3 class to send findings to AWS S3\n",
|
||||
"from prowler.providers.aws.lib.s3.s3 import S3\n",
|
||||
"\n",
|
||||
"print(\"\\n##### Sending findings to S3 bucket #####\")\n",
|
||||
"generated_outputs = {\"regular\": [csv, ocsf, asff, html], \"compliance\": []}\n",
|
||||
"s3_integration = S3(aws.session.current_session, bucket_name=\"sdk-core\", output_directory=\"output\")\n",
|
||||
"s3_integration.send_to_bucket(generated_outputs)\n",
|
||||
"\n",
|
||||
"# This upload the output files to the S3 bucket. In this case:\n",
|
||||
"# sdk-core/output/csv/findings_2025-02-26_16-25-30.csv\n",
|
||||
"# sdk-core/output/ocsf/findings_2025-02-26_16-25-30.ocsf\n",
|
||||
"# sdk-core/output/asff/findings_2025-02-26_16-25-30.asff"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Integrate with AWS Security Hub\n",
|
||||
"Send findings to AWS Security Hub for centralized security monitoring."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Import the SecurityHub class to send findings to AWS Security Hub\n",
|
||||
"from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub\n",
|
||||
"\n",
|
||||
"# Print message indicating the start of the process\n",
|
||||
"print(\"\\n##### Sending findings to AWS Security Hub #####\")\n",
|
||||
"\n",
|
||||
"# Get available AWS regions for Security Hub.\n",
|
||||
"# Each finding can only be sent to Security Hub within its own region.\n",
|
||||
"# Additionally, it verifies that Prowler’s integration is active in\n",
|
||||
"# Security Hub before sending\n",
|
||||
"available_regions = aws.get_available_aws_service_regions(\n",
|
||||
" \"securityhub\",\n",
|
||||
" aws.identity.partition,\n",
|
||||
" aws.identity.audited_regions,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Initialize the SecurityHub class with necessary parameters\n",
|
||||
"security_hub = SecurityHub(\n",
|
||||
" aws_account_id=aws.identity.account,\n",
|
||||
" aws_partition=aws.identity.partition,\n",
|
||||
" aws_session=aws.session.current_session,\n",
|
||||
" findings=asff.data,\n",
|
||||
" send_only_fails=False,\n",
|
||||
" aws_security_hub_available_regions=available_regions,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Send findings to AWS Security Hub\n",
|
||||
"findings_sent_to_security_hub = security_hub.batch_send_to_security_hub()\n",
|
||||
"\n",
|
||||
"# Print the number of findings sent to AWS Security Hub\n",
|
||||
"print(f\"{findings_sent_to_security_hub} findings sent to AWS Security Hub!\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "prowler-HDV3a8VZ-py3.12",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -1,16 +1,3 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: prowler-ns
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: prowler-sa
|
||||
namespace: prowler-ns
|
||||
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: prowler-ns
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: prowler-sa
|
||||
namespace: prowler-ns
|
||||
+4
-1
@@ -94,6 +94,9 @@ nav:
|
||||
- In-Cluster Execution: tutorials/kubernetes/in-cluster.md
|
||||
- Non In-Cluster Execution: tutorials/kubernetes/outside-cluster.md
|
||||
- Miscellaneous: tutorials/kubernetes/misc.md
|
||||
- Microsoft 365:
|
||||
- Authentication: tutorials/microsoft365/authentication.md
|
||||
- Create Prowler Service Principal: tutorials/microsoft365/create-prowler-service-principal.md
|
||||
- Developer Guide:
|
||||
- Introduction: developer-guide/introduction.md
|
||||
- Provider: developer-guide/provider.md
|
||||
@@ -124,7 +127,7 @@ extra:
|
||||
make our documentation better.
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-H5TFH6WJRQ
|
||||
property: G-KBKV70W5Y2
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/prowler-cloud
|
||||
|
||||
Generated
+532
-405
File diff suppressed because it is too large
Load Diff
+48
-33
@@ -51,6 +51,7 @@ from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_microsoft365 import Microsoft365CIS
|
||||
from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
@@ -78,6 +79,7 @@ from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.kubernetes.models import KubernetesOutputOptions
|
||||
from prowler.providers.microsoft365.models import Microsoft365OutputOptions
|
||||
|
||||
|
||||
def prowler():
|
||||
@@ -259,6 +261,10 @@ def prowler():
|
||||
output_options = KubernetesOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "microsoft365":
|
||||
output_options = Microsoft365OutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "quick_inventory") and args.quick_inventory:
|
||||
@@ -299,15 +305,26 @@ def prowler():
|
||||
print(f"{Style.BRIGHT}{Fore.GREEN}\nNo findings to fix!{Style.RESET_ALL}\n")
|
||||
sys.exit()
|
||||
|
||||
# Outputs
|
||||
# TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding
|
||||
# This will be refactored for the outputs generate directly the Finding
|
||||
finding_outputs = []
|
||||
for finding in findings:
|
||||
try:
|
||||
finding_outputs.append(
|
||||
Finding.generate_output(global_provider, finding, output_options)
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Extract findings stats
|
||||
stats = extract_findings_statistics(findings)
|
||||
stats = extract_findings_statistics(finding_outputs)
|
||||
|
||||
if args.slack:
|
||||
# TODO: this should be also in a config file
|
||||
if "SLACK_API_TOKEN" in environ and (
|
||||
"SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ
|
||||
):
|
||||
|
||||
token = environ["SLACK_API_TOKEN"]
|
||||
channel = (
|
||||
environ["SLACK_CHANNEL_NAME"]
|
||||
@@ -324,26 +341,16 @@ def prowler():
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Outputs
|
||||
# TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding
|
||||
# This will be refactored for the outputs generate directly the Finding
|
||||
finding_outputs = [
|
||||
Finding.generate_output(global_provider, finding, output_options)
|
||||
for finding in findings
|
||||
]
|
||||
|
||||
generated_outputs = {"regular": [], "compliance": []}
|
||||
|
||||
if args.output_formats:
|
||||
for mode in args.output_formats:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/"
|
||||
f"{output_options.output_filename}"
|
||||
f"{output_options.output_directory}/{output_options.output_filename}"
|
||||
)
|
||||
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)
|
||||
@@ -353,7 +360,6 @@ def prowler():
|
||||
if mode == "json-asff":
|
||||
asff_output = ASFF(
|
||||
findings=finding_outputs,
|
||||
create_file_descriptor=True,
|
||||
file_path=f"{filename}{json_asff_file_suffix}",
|
||||
)
|
||||
generated_outputs["regular"].append(asff_output)
|
||||
@@ -363,7 +369,6 @@ def prowler():
|
||||
if mode == "json-ocsf":
|
||||
json_output = OCSF(
|
||||
findings=finding_outputs,
|
||||
create_file_descriptor=True,
|
||||
file_path=f"{filename}{json_ocsf_file_suffix}",
|
||||
)
|
||||
generated_outputs["regular"].append(json_output)
|
||||
@@ -371,7 +376,6 @@ def prowler():
|
||||
if mode == "html":
|
||||
html_output = HTML(
|
||||
findings=finding_outputs,
|
||||
create_file_descriptor=True,
|
||||
file_path=f"{filename}{html_file_suffix}",
|
||||
)
|
||||
generated_outputs["regular"].append(html_output)
|
||||
@@ -394,7 +398,6 @@ def prowler():
|
||||
cis = AWSCIS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
@@ -408,7 +411,6 @@ def prowler():
|
||||
mitre_attack = AWSMitreAttack(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(mitre_attack)
|
||||
@@ -422,7 +424,6 @@ def prowler():
|
||||
ens = AWSENS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(ens)
|
||||
@@ -436,7 +437,6 @@ def prowler():
|
||||
aws_well_architected = AWSWellArchitected(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(aws_well_architected)
|
||||
@@ -450,7 +450,6 @@ def prowler():
|
||||
iso27001 = AWSISO27001(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(iso27001)
|
||||
@@ -464,7 +463,6 @@ def prowler():
|
||||
kisa_ismsp = AWSKISAISMSP(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(kisa_ismsp)
|
||||
@@ -477,7 +475,6 @@ def prowler():
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(generic_compliance)
|
||||
@@ -494,7 +491,6 @@ def prowler():
|
||||
cis = AzureCIS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
@@ -508,7 +504,6 @@ def prowler():
|
||||
mitre_attack = AzureMitreAttack(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(mitre_attack)
|
||||
@@ -522,7 +517,6 @@ def prowler():
|
||||
ens = AzureENS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(ens)
|
||||
@@ -535,7 +529,6 @@ def prowler():
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(generic_compliance)
|
||||
@@ -552,7 +545,6 @@ def prowler():
|
||||
cis = GCPCIS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
@@ -566,7 +558,6 @@ def prowler():
|
||||
mitre_attack = GCPMitreAttack(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(mitre_attack)
|
||||
@@ -580,7 +571,6 @@ def prowler():
|
||||
ens = GCPENS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(ens)
|
||||
@@ -593,7 +583,6 @@ def prowler():
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(generic_compliance)
|
||||
@@ -610,7 +599,6 @@ def prowler():
|
||||
cis = KubernetesCIS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
@@ -623,7 +611,34 @@ def prowler():
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(generic_compliance)
|
||||
generic_compliance.batch_write_data_to_file()
|
||||
|
||||
elif provider == "microsoft365":
|
||||
for compliance_name in input_compliance_frameworks:
|
||||
if compliance_name.startswith("cis_"):
|
||||
# Generate CIS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
cis = Microsoft365CIS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
cis.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
generic_compliance = GenericCompliance(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(generic_compliance)
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"Service": "ebs"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
"Checks": [
|
||||
"ec2_ebs_volume_snapshots_exists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "1.0.3",
|
||||
@@ -42,7 +44,8 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"ec2_ebs_default_encryption"
|
||||
"ec2_ebs_default_encryption",
|
||||
"ec2_ebs_volume_encryption"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -87,7 +90,9 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_user_mfa_enabled_console_access"
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_user_hardware_mfa_enabled",
|
||||
"iam_root_mfa_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -102,7 +107,9 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_user_mfa_enabled_console_access"
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_user_hardware_mfa_enabled",
|
||||
"iam_root_mfa_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -117,7 +124,9 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_root_mfa_enabled"
|
||||
"iam_root_mfa_enabled",
|
||||
"iam_root_hardware_mfa_enabled",
|
||||
"iam_user_mfa_enabled_console_access"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -162,7 +171,10 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"rds_instance_no_public_access"
|
||||
"rds_instance_no_public_access",
|
||||
"s3_bucket_public_access",
|
||||
"s3_bucket_public_list_acl",
|
||||
"s3_account_level_public_access_blocks"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -192,7 +204,8 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"rds_instance_storage_encrypted"
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_instance_transport_encrypted"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -584,7 +584,8 @@
|
||||
"Id": "2.3.1",
|
||||
"Description": "Ensure that encryption is enabled for RDS Instances",
|
||||
"Checks": [
|
||||
"rds_instance_storage_encrypted"
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_instance_transport_encrypted"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
@@ -584,7 +584,8 @@
|
||||
"Id": "2.3.1",
|
||||
"Description": "Ensure that encryption is enabled for RDS Instances",
|
||||
"Checks": [
|
||||
"rds_instance_storage_encrypted"
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_instance_transport_encrypted"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
@@ -303,7 +303,9 @@
|
||||
{
|
||||
"Id": "1.22",
|
||||
"Description": "Ensure access to AWSCloudShellFullAccess is restricted",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"iam_policy_cloudshell_admin_not_attached"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "1. Identity and Access Management",
|
||||
@@ -492,7 +494,8 @@
|
||||
"Id": "2.1.2",
|
||||
"Description": "Ensure MFA Delete is enabled on S3 buckets",
|
||||
"Checks": [
|
||||
"s3_bucket_no_mfa_delete"
|
||||
"s3_bucket_no_mfa_delete",
|
||||
"cloudtrail_bucket_requires_mfa_delete"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -581,7 +584,8 @@
|
||||
"Id": "2.3.1",
|
||||
"Description": "Ensure that encryption is enabled for RDS Instances",
|
||||
"Checks": [
|
||||
"rds_instance_storage_encrypted"
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_instance_transport_encrypted"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -1347,7 +1351,8 @@
|
||||
"Id": "5.6",
|
||||
"Description": "Ensure that EC2 Metadata Service only allows IMDSv2",
|
||||
"Checks": [
|
||||
"ec2_instance_imdsv2_enabled"
|
||||
"ec2_instance_imdsv2_enabled",
|
||||
"ec2_instance_account_imdsv2_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user