mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-29 11:29:20 +00:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c172f75f1a | ||
|
|
ec492fa13a | ||
|
|
702659959c | ||
|
|
fef332a591 | ||
|
|
a65ca72177 | ||
|
|
1108d90768 | ||
|
|
6715aa351f | ||
|
|
851497eb0a | ||
|
|
3bb4663e3e | ||
|
|
6953fcf6b5 | ||
|
|
ab844eee3f | ||
|
|
708e06aa3b | ||
|
|
aa8b8bbcae | ||
|
|
0ce1e15c2c | ||
|
|
105a83d946 | ||
|
|
e9a885a54d | ||
|
|
0a8759ee06 | ||
|
|
33ec21bbac | ||
|
|
7c00f65ecc | ||
|
|
7777c8f135 | ||
|
|
2386490002 | ||
|
|
b620f12027 | ||
|
|
00722181ad | ||
|
|
15e888a939 | ||
|
|
43fa600f1c | ||
|
|
2e4b5399c9 | ||
|
|
62cbb442e8 | ||
|
|
b0fe696935 | ||
|
|
42dbefbb31 | ||
|
|
f3dbe28681 | ||
|
|
6a5f1a7839 | ||
|
|
3b70f9fed4 | ||
|
|
7eb01aaa5c | ||
|
|
1e27e52fba | ||
|
|
16d73619e4 | ||
|
|
bc82696f15 | ||
|
|
fdb90623fc | ||
|
|
5fa62a9770 | ||
|
|
8f3df7e45d | ||
|
|
bb417587ae | ||
|
|
6b6e12cea3 | ||
|
|
65e70b2ca4 | ||
|
|
94d25f6f6a | ||
|
|
4bcf036831 | ||
|
|
901bc69a7d | ||
|
|
465217442b | ||
|
|
e6b40358aa | ||
|
|
9d48f7286a | ||
|
|
80311d3837 | ||
|
|
f501149068 | ||
|
|
750de62828 | ||
|
|
d2f338ceb6 | ||
|
|
e8d66979b3 | ||
|
|
b5180389f8 | ||
|
|
fbd5235e15 | ||
|
|
afd2267c26 | ||
|
|
9e798ababd | ||
|
|
e9f2fc8ee1 | ||
|
|
12198b4f06 | ||
|
|
15fae4d8f8 | ||
|
|
3de3fed858 | ||
|
|
1bf4255d93 | ||
|
|
b91a132e61 | ||
|
|
39302c9e93 | ||
|
|
65e21c4268 | ||
|
|
3d6a6a9fec | ||
|
|
d185902c86 | ||
|
|
8ce4ad83ed | ||
|
|
89620a96bc | ||
|
|
f1c008f934 | ||
|
|
4d688c9b47 | ||
|
|
db5481cc9c | ||
|
|
ce9a5e6484 | ||
|
|
550165b42b | ||
|
|
080551132a | ||
|
|
0a61848365 | ||
|
|
fcb9ca7795 | ||
|
|
71c58cee9e | ||
|
|
c811b6715d | ||
|
|
231829d8cd | ||
|
|
dbd2f8becb | ||
|
|
cc04e6614e | ||
|
|
a5c5ed614c | ||
|
|
ea13241317 | ||
|
|
a377a9ff6a | ||
|
|
f7e510b333 | ||
|
|
4472b80f1c | ||
|
|
577eb3eec9 | ||
|
|
1ed6a1a40f | ||
|
|
fe4cd1cddf | ||
|
|
6d7a8c8130 | ||
|
|
3057aeeacf | ||
|
|
bb5b63f62f | ||
|
|
58cd944618 | ||
|
|
5964b68c86 | ||
|
|
c87aaeba04 | ||
|
|
6e361005dc | ||
|
|
f5ab254bc5 | ||
|
|
298392b409 | ||
|
|
74a2bf0721 | ||
|
|
ddc5dc0316 | ||
|
|
d3af947553 | ||
|
|
36bb2509ac | ||
|
|
e4c2b0c2d3 | ||
|
|
ac5260ad43 | ||
|
|
33857109c9 | ||
|
|
8cc8f76204 | ||
|
|
8f3229928e | ||
|
|
2551992fd8 | ||
|
|
eb1decfce1 | ||
|
|
fd5e7b809f | ||
|
|
1ac681226d | ||
|
|
366940298d | ||
|
|
fa400ded7d | ||
|
|
ec9455ff75 | ||
|
|
2183f31ff5 | ||
|
|
67257a4212 | ||
|
|
001fa60a11 | ||
|
|
0ec3ed8be7 | ||
|
|
3ed0b8a464 | ||
|
|
fd610d44c0 | ||
|
|
b8cc4b4f0f | ||
|
|
396e51c27d | ||
|
|
36e61cb7a2 | ||
|
|
78c6484ddb | ||
|
|
3f1e90a5b3 | ||
|
|
e1bfec898f | ||
|
|
b5b816dac9 | ||
|
|
57854f23b7 | ||
|
|
9d7499b74f | ||
|
|
5b0b85c0f8 | ||
|
|
f7e8df618b | ||
|
|
d00d254c90 | ||
|
|
f9fbde6637 | ||
|
|
7b1a0474db | ||
|
|
da4f9b8e5f | ||
|
|
32f69d24b6 | ||
|
|
d032a61a9e | ||
|
|
07e0dc2ef5 | ||
|
|
9e175e8504 | ||
|
|
6b8a434cda | ||
|
|
554491a642 | ||
|
|
dc4e2f3c85 | ||
|
|
7d2c50991b | ||
|
|
83c204e010 | ||
|
|
316eb049dd | ||
|
|
be347b2428 | ||
|
|
a90c772827 | ||
|
|
26c70976c0 | ||
|
|
657310dc25 | ||
|
|
6e595eaf92 | ||
|
|
997831e33d | ||
|
|
5920cdc48f | ||
|
|
971e73f9cb | ||
|
|
bd9673c9de | ||
|
|
eded97d735 | ||
|
|
fdb1956b0b | ||
|
|
a915c04e9e | ||
|
|
07178ac69a | ||
|
|
9b434d4856 | ||
|
|
0758e97628 | ||
|
|
b486007f95 | ||
|
|
0c0887afef | ||
|
|
805ed81031 | ||
|
|
ec3fddf5b1 | ||
|
|
d7b0bc02ba | ||
|
|
4d1c8eae8f | ||
|
|
989ccf4ae3 | ||
|
|
9c089756c3 | ||
|
|
8d4b0914a8 | ||
|
|
1ae3f89aab | ||
|
|
b984f0423a | ||
|
|
f2f196cfcd | ||
|
|
6471d936bb | ||
|
|
21bbdccc41 | ||
|
|
48946fa4f7 | ||
|
|
9312dda7c2 | ||
|
|
e3013329ee | ||
|
|
38a0d2d740 | ||
|
|
5c2adf1e14 | ||
|
|
7ddd2c04c8 | ||
|
|
9a55632d8e | ||
|
|
f8b4427505 | ||
|
|
f1efc1456d | ||
|
|
2ea5851b67 | ||
|
|
a3051bc4e3 | ||
|
|
d454427b8b | ||
|
|
4b41bd6adf | ||
|
|
cdd044d120 | ||
|
|
213a793fbc | ||
|
|
a8a567c588 | ||
|
|
fefe89a1ed | ||
|
|
493fe2d523 | ||
|
|
d8fc830f1d | ||
|
|
b6c3ba0f0d | ||
|
|
32cd39d158 | ||
|
|
203275817f | ||
|
|
c05c3396b5 | ||
|
|
8f172aec8a | ||
|
|
263a7e2134 | ||
|
|
a2ea216604 | ||
|
|
77c572f990 | ||
|
|
bb0c346c4d | ||
|
|
2ce8e1fd21 | ||
|
|
ecfd94aeb1 | ||
|
|
eddc672264 | ||
|
|
8c71a39487 | ||
|
|
ff0ac27723 | ||
|
|
ad7134d283 | ||
|
|
58723ae52e |
13
.env
13
.env
@@ -6,13 +6,14 @@
|
||||
PROWLER_UI_VERSION="latest"
|
||||
SITE_URL=http://localhost:3000
|
||||
API_BASE_URL=http://prowler-api:8080/api/v1
|
||||
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
|
||||
AUTH_TRUST_HOST=true
|
||||
UI_PORT=3000
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
|
||||
|
||||
#### Prowler API Configuration ####
|
||||
PROWLER_API_VERSION="latest"
|
||||
PROWLER_API_VERSION="stable"
|
||||
# PostgreSQL settings
|
||||
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
POSTGRES_HOST=postgres-db
|
||||
@@ -40,9 +41,12 @@ DJANGO_LOGGING_FORMATTER=human_readable
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL=INFO
|
||||
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30 # Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440 # Token lifetime is in minutes
|
||||
# Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_WORKERS=4
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_MANAGE_DB_PARTITIONS=True
|
||||
@@ -87,3 +91,4 @@ jQIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
4
.github/codeql/api-codeql-config.yml
vendored
4
.github/codeql/api-codeql-config.yml
vendored
@@ -1,3 +1,3 @@
|
||||
name: "Custom CodeQL Config for API"
|
||||
name: "API - CodeQL Config"
|
||||
paths:
|
||||
- 'api/'
|
||||
- "api/"
|
||||
|
||||
4
.github/codeql/codeql-config.yml
vendored
4
.github/codeql/codeql-config.yml
vendored
@@ -1,4 +0,0 @@
|
||||
name: "Custom CodeQL Config"
|
||||
paths-ignore:
|
||||
- 'api/'
|
||||
- 'ui/'
|
||||
4
.github/codeql/sdk-codeql-config.yml
vendored
Normal file
4
.github/codeql/sdk-codeql-config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
name: "SDK - CodeQL Config"
|
||||
paths-ignore:
|
||||
- "api/"
|
||||
- "ui/"
|
||||
2
.github/codeql/ui-codeql-config.yml
vendored
2
.github/codeql/ui-codeql-config.yml
vendored
@@ -1,3 +1,3 @@
|
||||
name: "Custom CodeQL Config for UI"
|
||||
name: "UI - CodeQL Config"
|
||||
paths:
|
||||
- "ui/"
|
||||
|
||||
74
.github/dependabot.yml
vendored
74
.github/dependabot.yml
vendored
@@ -5,6 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# v5
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -14,6 +15,18 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/api"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "component/api"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -24,20 +37,77 @@ updates:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/ui"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "npm"
|
||||
- "component/ui"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
|
||||
# v4.6
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v4.6
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "v4"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v4.6
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
- "v4"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v4.6
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
- "v4"
|
||||
|
||||
# v3
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "v3"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
|
||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -22,6 +22,11 @@ provider/kubernetes:
|
||||
- any-glob-to-any-file: "prowler/providers/kubernetes/**"
|
||||
- any-glob-to-any-file: "tests/providers/kubernetes/**"
|
||||
|
||||
provider/github:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/github/**"
|
||||
- any-glob-to-any-file: "tests/providers/github/**"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ".github/workflows/*"
|
||||
|
||||
98
.github/workflows/api-build-lint-push-containers.yml
vendored
Normal file
98
.github/workflows/api-build-lint-push-containers.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: API - Build and Push containers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "api/**"
|
||||
- ".github/workflows/api-build-lint-push-containers.yml"
|
||||
|
||||
# Uncomment the code below to test this action on PRs
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - "master"
|
||||
# paths:
|
||||
# - "api/**"
|
||||
# - ".github/workflows/api-build-lint-push-containers.yml"
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
|
||||
WORKING_DIRECTORY: ./api
|
||||
|
||||
# Container Registries
|
||||
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push container image (latest)
|
||||
# Comment the following line for testing
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
# Set push: false for testing
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
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
|
||||
10
.github/workflows/api-codeql.yml
vendored
10
.github/workflows/api-codeql.yml
vendored
@@ -9,22 +9,18 @@
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "API - CodeQL"
|
||||
name: API - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
23
.github/workflows/api-pull-request.yml
vendored
23
.github/workflows/api-pull-request.yml
vendored
@@ -1,14 +1,16 @@
|
||||
name: "API - Pull Request"
|
||||
name: API - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
@@ -69,6 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v45
|
||||
@@ -80,18 +83,21 @@ jobs:
|
||||
api/permissions/**
|
||||
api/README.md
|
||||
api/mkdocs.yml
|
||||
|
||||
- name: Install poetry
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry
|
||||
pipx install poetry==1.8.5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
@@ -108,49 +114,60 @@ jobs:
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry lock --check
|
||||
poetry check --lock
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run ruff check . --exclude contrib
|
||||
|
||||
- name: Check Format with ruff
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run ruff format --check . --exclude contrib
|
||||
|
||||
- name: Lint with pylint
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/
|
||||
|
||||
- name: Bandit
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run bandit -q -lll -x '*_test.py,./contrib/' -r .
|
||||
|
||||
- name: Safety
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run safety check --ignore 70612,66963
|
||||
|
||||
- name: Vulture
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 .
|
||||
|
||||
- name: Hadolint
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
/tmp/hadolint Dockerfile --ignore=DL3013
|
||||
|
||||
- name: Test with pytest
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest --cov=./src/backend --cov-report=xml src/backend
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
flags: api
|
||||
|
||||
37
.github/workflows/backport.yml
vendored
37
.github/workflows/backport.yml
vendored
@@ -1,42 +1,47 @@
|
||||
name: Automatic Backport
|
||||
name: Prowler - Automatic Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: ['master']
|
||||
types: ['labeled', 'closed']
|
||||
|
||||
env:
|
||||
# The prefix of the label that triggers the backport must not contain the branch name
|
||||
# so, for example, if the branch is 'master', the label should be 'backport-to-<branch>'
|
||||
BACKPORT_LABEL_PREFIX: backport-to-
|
||||
BACKPORT_LABEL_IGNORE: was-backported
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport PR
|
||||
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport'))
|
||||
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'was-backported'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
# Workaround not to fail the workflow if the PR does not need a backport
|
||||
# https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2258561266
|
||||
- name: Check for backport labels
|
||||
id: check_labels
|
||||
run: |-
|
||||
labels='${{ toJSON(github.event.pull_request.labels.*.name) }}'
|
||||
echo "$labels"
|
||||
matched=$(echo "${labels}" | jq '. | map(select(startswith("backport-to-"))) | length')
|
||||
echo "matched=$matched"
|
||||
echo "matched=$matched" >> $GITHUB_OUTPUT
|
||||
- name: Check labels
|
||||
id: preview_label_check
|
||||
uses: docker://agilepathway/pull-request-label-checker:v1.6.55
|
||||
with:
|
||||
allow_failure: true
|
||||
prefix_mode: true
|
||||
any_of: ${{ env.BACKPORT_LABEL_PREFIX }}
|
||||
none_of: ${{ env.BACKPORT_LABEL_IGNORE }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Backport Action
|
||||
if: fromJSON(steps.check_labels.outputs.matched) > 0
|
||||
if: steps.preview_label_check.outputs.label_check == 'success'
|
||||
uses: sorenlouv/backport-github-action@v9.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }}
|
||||
|
||||
- name: Info log
|
||||
if: ${{ success() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
if: ${{ success() && steps.preview_label_check.outputs.label_check == 'success' }}
|
||||
run: cat ~/.backport/backport.info.log
|
||||
|
||||
- name: Debug log
|
||||
if: ${{ failure() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
if: ${{ failure() && steps.preview_label_check.outputs.label_check == 'success' }}
|
||||
run: cat ~/.backport/backport.debug.log
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Pull Request Documentation Link
|
||||
name: Prowler - Pull Request Documentation Link
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
6
.github/workflows/find-secrets.yml
vendored
6
.github/workflows/find-secrets.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Find secrets
|
||||
name: Prowler - Find secrets
|
||||
|
||||
on: pull_request
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.84.1
|
||||
uses: trufflesecurity/trufflehog@v3.88.2
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
head: HEAD
|
||||
extra_args: --only-verified
|
||||
extra_args: --only-verified
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Pull Request Labeler"
|
||||
name: Prowler - PR Labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
name: Build and Push containers
|
||||
name: SDK - Build and Push containers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# For `v3-latest`
|
||||
- "v3"
|
||||
# For `v4-latest`
|
||||
- "v4.6"
|
||||
# For `latest`
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
@@ -64,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
pipx install poetry==1.8.5
|
||||
pipx inject poetry poetry-bumpversion
|
||||
|
||||
- name: Get Prowler version
|
||||
@@ -85,8 +89,8 @@ jobs:
|
||||
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
|
||||
4)
|
||||
|
||||
4)
|
||||
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
@@ -9,22 +9,24 @@
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: SDK - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
pull_request:
|
||||
branches:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
@@ -55,7 +57,7 @@ jobs:
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Pull Request"
|
||||
name: SDK - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v45
|
||||
@@ -36,17 +37,22 @@ jobs:
|
||||
README.md
|
||||
mkdocs.yml
|
||||
.backportrc.json
|
||||
.env
|
||||
docker-compose*
|
||||
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry
|
||||
pipx install poetry==1.8.5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
@@ -57,44 +63,56 @@ jobs:
|
||||
sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||
) && curl -L -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64" \
|
||||
&& chmod +x /tmp/hadolint
|
||||
|
||||
- name: Poetry check
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry lock --check
|
||||
poetry check --lock
|
||||
|
||||
- name: Lint with flake8
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api
|
||||
|
||||
- name: Checking format with black
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run black --exclude api ui --check .
|
||||
|
||||
- name: Lint with pylint
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/
|
||||
|
||||
- name: Bandit
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r .
|
||||
|
||||
- name: Safety
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run safety check --ignore 70612 -r pyproject.toml
|
||||
|
||||
- name: Vulture
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
|
||||
|
||||
- name: Hadolint
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
/tmp/hadolint Dockerfile --ignore=DL3013
|
||||
|
||||
- name: Test with pytest
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
flags: prowler
|
||||
@@ -1,4 +1,4 @@
|
||||
name: PyPI release
|
||||
name: SDK - PyPI release
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -10,12 +10,40 @@ env:
|
||||
CACHE: "poetry"
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
release-prowler-job:
|
||||
runs-on: ubuntu-latest
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
name: Release Prowler to PyPI
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ "${{ github.repository }}" != "prowler-cloud/prowler" ]]; then
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get Prowler version
|
||||
run: |
|
||||
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
|
||||
@@ -40,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pipx install poetry
|
||||
pipx install poetry==1.8.5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -1,6 +1,6 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Refresh regions of AWS services
|
||||
name: SDK - Refresh AWS services' regions
|
||||
|
||||
on:
|
||||
schedule:
|
||||
98
.github/workflows/ui-build-lint-push-containers.yml
vendored
Normal file
98
.github/workflows/ui-build-lint-push-containers.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: UI - Build and Push containers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "ui/**"
|
||||
- ".github/workflows/ui-build-lint-push-containers.yml"
|
||||
|
||||
# Uncomment the below code to test this action on PRs
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - "master"
|
||||
# paths:
|
||||
# - "ui/**"
|
||||
# - ".github/workflows/ui-build-lint-push-containers.yml"
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
|
||||
WORKING_DIRECTORY: ./ui
|
||||
|
||||
# Container Registries
|
||||
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push container image (latest)
|
||||
# Comment the following line for testing
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
# Set push: false for testing
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
4
.github/workflows/ui-codeql.yml
vendored
4
.github/workflows/ui-codeql.yml
vendored
@@ -9,20 +9,18 @@
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "UI - CodeQL"
|
||||
name: UI - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
name: "UI - Pull Request"
|
||||
name: UI - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- "v5.*"
|
||||
paths:
|
||||
- 'ui/**'
|
||||
|
||||
@@ -20,7 +27,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
@@ -31,4 +38,4 @@ jobs:
|
||||
run: npm run healthcheck
|
||||
- name: Build the application
|
||||
working-directory: ./ui
|
||||
run: npm run build
|
||||
run: npm run build
|
||||
@@ -90,7 +90,7 @@ repos:
|
||||
- id: bandit
|
||||
name: bandit
|
||||
description: "Bandit is a tool for finding common security issues in Python code"
|
||||
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/' -r .'
|
||||
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
@@ -103,7 +103,6 @@ repos:
|
||||
- id: vulture
|
||||
name: vulture
|
||||
description: "Vulture finds unused code in Python programs."
|
||||
entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .'
|
||||
exclude: 'api/src/backend/'
|
||||
entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-alpine
|
||||
FROM python:3.12.8-alpine3.20
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -3,7 +3,7 @@
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<b><i>Prowler SaaS </b> and <b>Prowler Open Source</b> are as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security.
|
||||
<b><i>Prowler Open Source</b> is as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security.
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
|
||||
@@ -29,7 +29,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler?include_prereleases"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/release-date/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="Contributors" src="https://img.shields.io/github/contributors-anon/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="License" src="https://img.shields.io/github/license/prowler-cloud/prowler"></a>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
# Description
|
||||
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler Cloud</a>.
|
||||
|
||||
## Prowler App
|
||||
|
||||
@@ -72,7 +72,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 561 | 81 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --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` | 4 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
@@ -98,6 +98,7 @@ curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/mast
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
### From GitHub
|
||||
@@ -139,6 +140,19 @@ cd src/backend
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
**Commands to run the API Scheduler**
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler/api
|
||||
poetry install
|
||||
poetry shell
|
||||
set -a
|
||||
source .env
|
||||
cd src/backend
|
||||
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
```
|
||||
|
||||
**Commands to run the UI**
|
||||
|
||||
``` console
|
||||
|
||||
@@ -22,6 +22,7 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
# PostgreSQL settings
|
||||
# If running django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-alpine AS build
|
||||
FROM python:3.12.8-alpine3.20 AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/api"
|
||||
|
||||
|
||||
2083
api/poetry.lock
generated
2083
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
name = "prowler-api"
|
||||
package-mode = false
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
celery = {extras = ["pytest"], version = "^5.4.0"}
|
||||
django = "5.1.1"
|
||||
django = "5.1.4"
|
||||
django-celery-beat = "^2.7.0"
|
||||
django-celery-results = "^2.5.1"
|
||||
django-cors-headers = "4.4.0"
|
||||
@@ -27,7 +27,7 @@ drf-nested-routers = "^0.94.1"
|
||||
drf-spectacular = "0.27.2"
|
||||
drf-spectacular-jsonapi = "0.5.1"
|
||||
gunicorn = "23.0.0"
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "master"}
|
||||
prowler = "^5.0"
|
||||
psycopg2-binary = "2.9.9"
|
||||
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
|
||||
# Needed for prowler compatibility
|
||||
@@ -48,8 +48,8 @@ pytest-env = "1.1.3"
|
||||
pytest-randomly = "3.15.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
ruff = "0.5.0"
|
||||
safety = "3.2.3"
|
||||
vulture = "2.11"
|
||||
safety = "3.2.9"
|
||||
vulture = "2.14"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import uuid
|
||||
|
||||
from django.db import transaction, connection
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework_json_api import filters
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from rest_framework_json_api.views import ModelViewSet
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
|
||||
from api.filters import CustomDjangoFilterBackend
|
||||
from api.models import Role, Tenant
|
||||
from api.rbac.permissions import HasPermissions
|
||||
|
||||
|
||||
class BaseViewSet(ModelViewSet):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
required_permissions = []
|
||||
permission_classes = [permissions.IsAuthenticated, HasPermissions]
|
||||
filter_backends = [
|
||||
filters.QueryParameterValidationFilter,
|
||||
filters.OrderingFilter,
|
||||
@@ -28,6 +31,17 @@ class BaseViewSet(ModelViewSet):
|
||||
ordering_fields = "__all__"
|
||||
ordering = ["id"]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.set_required_permissions()
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def set_required_permissions(self):
|
||||
"""This is an abstract method that must be implemented by subclasses."""
|
||||
NotImplemented
|
||||
|
||||
def get_queryset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -47,13 +61,7 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -66,13 +74,60 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
class BaseTenantViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
tenant = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
# If the request is a POST, create the admin role
|
||||
if request.method == "POST":
|
||||
isinstance(tenant, dict) and self._create_admin_role(tenant.data["id"])
|
||||
except Exception as e:
|
||||
self._handle_creation_error(e, tenant)
|
||||
raise
|
||||
|
||||
return tenant
|
||||
|
||||
def _create_admin_role(self, tenant_id):
|
||||
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,
|
||||
)
|
||||
|
||||
def _handle_creation_error(self, error, tenant):
|
||||
if tenant.data.get("id"):
|
||||
try:
|
||||
Tenant.objects.using(MainRouter.admin_db).filter(
|
||||
id=tenant.data["id"]
|
||||
).delete()
|
||||
except ObjectDoesNotExist:
|
||||
pass # Tenant might not exist, handle gracefully
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
user_id = str(request.user.id)
|
||||
if (
|
||||
request.resolver_match.url_name != "tenant-detail"
|
||||
and request.method != "DELETE"
|
||||
):
|
||||
user_id = str(request.user.id)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
# TODO: DRY this when we have time
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
tenant_id = request.auth.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -92,12 +147,6 @@ class BaseUserViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import secrets
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import connection, models, transaction
|
||||
from psycopg2 import connect as psycopg2_connect
|
||||
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
|
||||
DB_PASSWORD = (
|
||||
@@ -23,6 +24,8 @@ TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
|
||||
POSTGRES_TENANT_VAR = "api.tenant_id"
|
||||
POSTGRES_USER_VAR = "api.user_id"
|
||||
|
||||
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def psycopg_connection(database_alias: str):
|
||||
@@ -44,10 +47,23 @@ def psycopg_connection(database_alias: str):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def tenant_transaction(tenant_id: str):
|
||||
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
|
||||
"""
|
||||
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
|
||||
if the value is a valid UUID.
|
||||
|
||||
Args:
|
||||
value (str): Database configuration parameter value.
|
||||
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
|
||||
"""
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
try:
|
||||
# just in case the value is an UUID object
|
||||
uuid.UUID(str(value))
|
||||
except ValueError:
|
||||
raise ValidationError("Must be a valid UUID")
|
||||
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
|
||||
yield cursor
|
||||
|
||||
|
||||
@@ -103,15 +119,18 @@ def batch_delete(queryset, batch_size=5000):
|
||||
total_deleted = 0
|
||||
deletion_summary = {}
|
||||
|
||||
paginator = Paginator(queryset.order_by("id").only("id"), batch_size)
|
||||
|
||||
for page_num in paginator.page_range:
|
||||
batch_ids = [obj.id for obj in paginator.page(page_num).object_list]
|
||||
while True:
|
||||
# Get a batch of IDs to delete
|
||||
batch_ids = set(
|
||||
queryset.values_list("id", flat=True).order_by("id")[:batch_size]
|
||||
)
|
||||
if not batch_ids:
|
||||
# No more objects to delete
|
||||
break
|
||||
|
||||
deleted_count, deleted_info = queryset.filter(id__in=batch_ids).delete()
|
||||
|
||||
total_deleted += deleted_count
|
||||
|
||||
for model_label, count in deleted_info.items():
|
||||
deletion_summary[model_label] = deletion_summary.get(model_label, 0) + count
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from django.db import connection, transaction
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
|
||||
|
||||
def set_tenant(func):
|
||||
@@ -31,7 +35,7 @@ def set_tenant(func):
|
||||
pass
|
||||
|
||||
# When calling the task
|
||||
some_task.delay(arg1, tenant_id="1234-abcd-5678")
|
||||
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
|
||||
|
||||
# The tenant context will be set before the task logic executes.
|
||||
"""
|
||||
@@ -43,9 +47,12 @@ def set_tenant(func):
|
||||
tenant_id = kwargs.pop("tenant_id")
|
||||
except KeyError:
|
||||
raise KeyError("This task requires the tenant_id")
|
||||
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -26,11 +26,13 @@ from api.models import (
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
PermissionChoices,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Role,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
@@ -481,6 +483,26 @@ class UserFilter(FilterSet):
|
||||
}
|
||||
|
||||
|
||||
class RoleFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
permission_state = ChoiceFilter(
|
||||
choices=PermissionChoices.choices, method="filter_permission_state"
|
||||
)
|
||||
|
||||
def filter_permission_state(self, queryset, name, value):
|
||||
return Role.filter_by_permission_state(queryset, value)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = {
|
||||
"id": ["exact", "in"],
|
||||
"name": ["exact", "in"],
|
||||
"inserted_at": ["gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class ComplianceOverviewFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
provider_type = ChoiceFilter(choices=Provider.ProviderChoices.choices)
|
||||
@@ -521,3 +543,25 @@ class ScanSummaryFilter(FilterSet):
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
|
||||
class ServiceOverviewFilter(ScanSummaryFilter):
|
||||
muted_findings = None
|
||||
|
||||
def is_valid(self):
|
||||
# Check if at least one of the inserted_at filters is present
|
||||
inserted_at_filters = [
|
||||
self.data.get("inserted_at"),
|
||||
self.data.get("inserted_at__gte"),
|
||||
self.data.get("inserted_at__lte"),
|
||||
]
|
||||
if not any(inserted_at_filters):
|
||||
raise ValidationError(
|
||||
{
|
||||
"inserted_at": [
|
||||
"At least one of filter[inserted_at], filter[inserted_at__gte], or "
|
||||
"filter[inserted_at__lte] is required."
|
||||
]
|
||||
}
|
||||
)
|
||||
return super().is_valid()
|
||||
|
||||
@@ -58,5 +58,96 @@
|
||||
"provider_group": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
|
||||
"inserted_at": "2024-11-13T11:55:41.237Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.role",
|
||||
"pk": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "admin_test",
|
||||
"manage_users": true,
|
||||
"manage_account": true,
|
||||
"manage_billing": true,
|
||||
"manage_providers": true,
|
||||
"manage_integrations": true,
|
||||
"manage_scans": true,
|
||||
"unlimited_visibility": true,
|
||||
"inserted_at": "2024-11-20T15:32:42.402Z",
|
||||
"updated_at": "2024-11-20T15:32:42.402Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.role",
|
||||
"pk": "845ff03a-87ef-42ba-9786-6577c70c4df0",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "first_role",
|
||||
"manage_users": true,
|
||||
"manage_account": true,
|
||||
"manage_billing": true,
|
||||
"manage_providers": true,
|
||||
"manage_integrations": false,
|
||||
"manage_scans": false,
|
||||
"unlimited_visibility": true,
|
||||
"inserted_at": "2024-11-20T15:31:53.239Z",
|
||||
"updated_at": "2024-11-20T15:31:53.239Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.role",
|
||||
"pk": "902d726c-4bd5-413a-a2a4-f7b4754b6b20",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "third_role",
|
||||
"manage_users": false,
|
||||
"manage_account": false,
|
||||
"manage_billing": false,
|
||||
"manage_providers": false,
|
||||
"manage_integrations": false,
|
||||
"manage_scans": true,
|
||||
"unlimited_visibility": false,
|
||||
"inserted_at": "2024-11-20T15:34:05.440Z",
|
||||
"updated_at": "2024-11-20T15:34:05.440Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.roleprovidergrouprelationship",
|
||||
"pk": "57fd024a-0a7f-49b4-a092-fa0979a07aaf",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
|
||||
"provider_group": "3fe28fb8-e545-424c-9b8f-69aff638f430",
|
||||
"inserted_at": "2024-11-20T15:32:42.402Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.roleprovidergrouprelationship",
|
||||
"pk": "a3cd0099-1c13-4df1-a5e5-ecdfec561b35",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
|
||||
"provider_group": "481769f5-db2b-447b-8b00-1dee18db90ec",
|
||||
"inserted_at": "2024-11-20T15:32:42.402Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.roleprovidergrouprelationship",
|
||||
"pk": "cfd84182-a058-40c2-af3c-0189b174940f",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
|
||||
"provider_group": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
|
||||
"inserted_at": "2024-11-20T15:32:42.402Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.userrolerelationship",
|
||||
"pk": "92339663-e954-4fd8-98fb-8bfe15949975",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
|
||||
"user": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
|
||||
"inserted_at": "2024-11-20T15:36:14.302Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-20 13:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0002_token_migrations"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="provider",
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="provider",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "uid", "is_deleted"),
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
),
|
||||
]
|
||||
248
api/src/backend/api/migrations/0004_rbac.py
Normal file
248
api/src/backend/api/migrations/0004_rbac.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-05 12:29
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0003_update_provider_unique_constraint_with_is_deleted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Role",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("manage_users", models.BooleanField(default=False)),
|
||||
("manage_account", models.BooleanField(default=False)),
|
||||
("manage_billing", models.BooleanField(default=False)),
|
||||
("manage_providers", models.BooleanField(default=False)),
|
||||
("manage_integrations", models.BooleanField(default=False)),
|
||||
("manage_scans", models.BooleanField(default=False)),
|
||||
("unlimited_visibility", models.BooleanField(default=False)),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "roles",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RoleProviderGroupRelationship",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "role_provider_group_relationship",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserRoleRelationship",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "role_user_relationship",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="roleprovidergrouprelationship",
|
||||
name="provider_group",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.providergroup"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="roleprovidergrouprelationship",
|
||||
name="role",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.role"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="provider_groups",
|
||||
field=models.ManyToManyField(
|
||||
related_name="roles",
|
||||
through="api.RoleProviderGroupRelationship",
|
||||
to="api.providergroup",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userrolerelationship",
|
||||
name="role",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.role"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userrolerelationship",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="users",
|
||||
field=models.ManyToManyField(
|
||||
related_name="roles",
|
||||
through="api.UserRoleRelationship",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="roleprovidergrouprelationship",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("role_id", "provider_group_id"),
|
||||
name="unique_role_provider_group_relationship",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="roleprovidergrouprelationship",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_roleprovidergrouprelationship",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="userrolerelationship",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("role_id", "user_id"), name="unique_role_user_relationship"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="userrolerelationship",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_userrolerelationship",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="role",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "name"), name="unique_role_per_tenant"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="role",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_role",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="InvitationRoleRelationship",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"invitation",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.invitation"
|
||||
),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.role"
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "role_invitation_relationship",
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="invitationrolerelationship",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("role_id", "invitation_id"),
|
||||
name="unique_role_invitation_relationship",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="invitationrolerelationship",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_invitationrolerelationship",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="invitations",
|
||||
field=models.ManyToManyField(
|
||||
related_name="roles",
|
||||
through="api.InvitationRoleRelationship",
|
||||
to="api.invitation",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_router import MainRouter
|
||||
|
||||
|
||||
def create_admin_role(apps, schema_editor):
|
||||
Tenant = apps.get_model("api", "Tenant")
|
||||
Role = apps.get_model("api", "Role")
|
||||
User = apps.get_model("api", "User")
|
||||
UserRoleRelationship = apps.get_model("api", "UserRoleRelationship")
|
||||
|
||||
for tenant in Tenant.objects.using(MainRouter.admin_db).all():
|
||||
admin_role, _ = Role.objects.using(MainRouter.admin_db).get_or_create(
|
||||
name="admin",
|
||||
tenant=tenant,
|
||||
defaults={
|
||||
"manage_users": True,
|
||||
"manage_account": True,
|
||||
"manage_billing": True,
|
||||
"manage_providers": True,
|
||||
"manage_integrations": True,
|
||||
"manage_scans": True,
|
||||
"unlimited_visibility": True,
|
||||
},
|
||||
)
|
||||
users = User.objects.using(MainRouter.admin_db).filter(
|
||||
membership__tenant=tenant
|
||||
)
|
||||
for user in users:
|
||||
UserRoleRelationship.objects.using(MainRouter.admin_db).get_or_create(
|
||||
user=user,
|
||||
role=admin_role,
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0004_rbac"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_admin_role),
|
||||
]
|
||||
@@ -69,6 +69,21 @@ class StateChoices(models.TextChoices):
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
|
||||
|
||||
class PermissionChoices(models.TextChoices):
|
||||
"""
|
||||
Represents the different permission states that a role can have.
|
||||
|
||||
Attributes:
|
||||
UNLIMITED: Indicates that the role possesses all permissions.
|
||||
LIMITED: Indicates that the role has some permissions but not all.
|
||||
NONE: Indicates that the role does not have any permissions.
|
||||
"""
|
||||
|
||||
UNLIMITED = "unlimited", _("Unlimited permissions")
|
||||
LIMITED = "limited", _("Limited permissions")
|
||||
NONE = "none", _("No permissions")
|
||||
|
||||
|
||||
class ActiveProviderManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(self.active_provider_filter())
|
||||
@@ -256,7 +271,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "uid"),
|
||||
fields=("tenant_id", "provider", "uid", "is_deleted"),
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
@@ -298,19 +313,10 @@ class ProviderGroup(RowLevelSecurityProtectedModel):
|
||||
|
||||
|
||||
class ProviderGroupMembership(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
provider = models.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
provider_group = models.ForeignKey(
|
||||
ProviderGroup,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
provider_group = models.ForeignKey(ProviderGroup, on_delete=models.CASCADE)
|
||||
provider = models.ForeignKey(Provider, on_delete=models.CASCADE)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "provider_group_memberships"
|
||||
@@ -327,7 +333,7 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel):
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-group-memberships"
|
||||
resource_name = "provider_groups-provider"
|
||||
|
||||
|
||||
class Task(RowLevelSecurityProtectedModel):
|
||||
@@ -509,8 +515,8 @@ class Resource(RowLevelSecurityProtectedModel):
|
||||
through="ResourceTagMapping",
|
||||
)
|
||||
|
||||
def get_tags(self) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.all()}
|
||||
def get_tags(self, tenant_id: str) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.filter(tenant_id=tenant_id)}
|
||||
|
||||
def clear_tags(self):
|
||||
self.tags.clear()
|
||||
@@ -851,6 +857,150 @@ class Invitation(RowLevelSecurityProtectedModel):
|
||||
resource_name = "invitations"
|
||||
|
||||
|
||||
class Role(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
name = models.CharField(max_length=255)
|
||||
manage_users = models.BooleanField(default=False)
|
||||
manage_account = models.BooleanField(default=False)
|
||||
manage_billing = models.BooleanField(default=False)
|
||||
manage_providers = models.BooleanField(default=False)
|
||||
manage_integrations = models.BooleanField(default=False)
|
||||
manage_scans = models.BooleanField(default=False)
|
||||
unlimited_visibility = models.BooleanField(default=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
provider_groups = models.ManyToManyField(
|
||||
ProviderGroup, through="RoleProviderGroupRelationship", related_name="roles"
|
||||
)
|
||||
users = models.ManyToManyField(
|
||||
User, through="UserRoleRelationship", related_name="roles"
|
||||
)
|
||||
invitations = models.ManyToManyField(
|
||||
Invitation, through="InvitationRoleRelationship", related_name="roles"
|
||||
)
|
||||
|
||||
# Filter permission_state
|
||||
PERMISSION_FIELDS = [
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"manage_scans",
|
||||
]
|
||||
|
||||
@property
|
||||
def permission_state(self):
|
||||
values = [getattr(self, field) for field in self.PERMISSION_FIELDS]
|
||||
if all(values):
|
||||
return PermissionChoices.UNLIMITED
|
||||
elif not any(values):
|
||||
return PermissionChoices.NONE
|
||||
else:
|
||||
return PermissionChoices.LIMITED
|
||||
|
||||
@classmethod
|
||||
def filter_by_permission_state(cls, queryset, value):
|
||||
q_all_true = Q(**{field: True for field in cls.PERMISSION_FIELDS})
|
||||
q_all_false = Q(**{field: False for field in cls.PERMISSION_FIELDS})
|
||||
|
||||
if value == PermissionChoices.UNLIMITED:
|
||||
return queryset.filter(q_all_true)
|
||||
elif value == PermissionChoices.NONE:
|
||||
return queryset.filter(q_all_false)
|
||||
else:
|
||||
return queryset.exclude(q_all_true | q_all_false)
|
||||
|
||||
class Meta:
|
||||
db_table = "roles"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id", "name"],
|
||||
name="unique_role_per_tenant",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "roles"
|
||||
|
||||
|
||||
class RoleProviderGroupRelationship(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
provider_group = models.ForeignKey(ProviderGroup, on_delete=models.CASCADE)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "role_provider_group_relationship"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["role_id", "provider_group_id"],
|
||||
name="unique_role_provider_group_relationship",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "role-provider_groups"
|
||||
|
||||
|
||||
class UserRoleRelationship(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "role_user_relationship"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["role_id", "user_id"],
|
||||
name="unique_role_user_relationship",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "user-roles"
|
||||
|
||||
|
||||
class InvitationRoleRelationship(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
invitation = models.ForeignKey(Invitation, on_delete=models.CASCADE)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "role_invitation_relationship"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["role_id", "invitation_id"],
|
||||
name="unique_role_invitation_relationship",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "invitation-roles"
|
||||
|
||||
|
||||
class ComplianceOverview(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
75
api/src/backend/api/rbac/permissions.py
Normal file
75
api/src/backend/api/rbac/permissions.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.models import Provider, Role, User
|
||||
|
||||
|
||||
class Permissions(Enum):
|
||||
MANAGE_USERS = "manage_users"
|
||||
MANAGE_ACCOUNT = "manage_account"
|
||||
MANAGE_BILLING = "manage_billing"
|
||||
MANAGE_PROVIDERS = "manage_providers"
|
||||
MANAGE_INTEGRATIONS = "manage_integrations"
|
||||
MANAGE_SCANS = "manage_scans"
|
||||
UNLIMITED_VISIBILITY = "unlimited_visibility"
|
||||
|
||||
|
||||
class HasPermissions(BasePermission):
|
||||
"""
|
||||
Custom permission to check if the user's role has the required permissions.
|
||||
The required permissions should be specified in the view as a list in `required_permissions`.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
required_permissions = getattr(view, "required_permissions", [])
|
||||
if not required_permissions:
|
||||
return True
|
||||
|
||||
user_roles = (
|
||||
User.objects.using(MainRouter.admin_db).get(id=request.user.id).roles.all()
|
||||
)
|
||||
if not user_roles:
|
||||
return False
|
||||
|
||||
for perm in required_permissions:
|
||||
if not getattr(user_roles[0], perm.value, False):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_role(user: User) -> Optional[Role]:
|
||||
"""
|
||||
Retrieve the first role assigned to the given user.
|
||||
|
||||
Returns:
|
||||
The user's first Role instance if the user has any roles, otherwise None.
|
||||
"""
|
||||
return user.roles.first()
|
||||
|
||||
|
||||
def get_providers(role: Role) -> QuerySet[Provider]:
|
||||
"""
|
||||
Return a distinct queryset of Providers accessible by the given role.
|
||||
|
||||
If the role has no associated provider groups, an empty queryset is returned.
|
||||
|
||||
Args:
|
||||
role: A Role instance.
|
||||
|
||||
Returns:
|
||||
A QuerySet of Provider objects filtered by the role's provider groups.
|
||||
If the role has no provider groups, returns an empty queryset.
|
||||
"""
|
||||
tenant = role.tenant
|
||||
provider_groups = role.provider_groups.all()
|
||||
if not provider_groups.exists():
|
||||
return Provider.objects.none()
|
||||
|
||||
return Provider.objects.filter(
|
||||
tenant=tenant, provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
@@ -2,7 +2,7 @@ from contextlib import nullcontext
|
||||
|
||||
from rest_framework_json_api.renderers import JSONRenderer
|
||||
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.db_utils import rls_transaction
|
||||
|
||||
|
||||
class APIJSONRenderer(JSONRenderer):
|
||||
@@ -13,9 +13,9 @@ class APIJSONRenderer(JSONRenderer):
|
||||
tenant_id = getattr(request, "tenant_id", None) if request else None
|
||||
include_param_present = "include" in request.query_params if request else False
|
||||
|
||||
# Use tenant_transaction if needed for included resources, otherwise do nothing
|
||||
# Use rls_transaction if needed for included resources, otherwise do nothing
|
||||
context_manager = (
|
||||
tenant_transaction(tenant_id)
|
||||
rls_transaction(tenant_id)
|
||||
if tenant_id and include_param_present
|
||||
else nullcontext()
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,9 @@
|
||||
import pytest
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
|
||||
|
||||
@patch("api.v1.views.MainRouter.admin_db", new="default")
|
||||
@pytest.mark.django_db
|
||||
def test_basic_authentication():
|
||||
client = APIClient()
|
||||
@@ -98,3 +95,85 @@ def test_refresh_token(create_test_user, tenants_fixture):
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert new_refresh_response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fixture):
|
||||
client = APIClient()
|
||||
|
||||
role = roles_fixture[0]
|
||||
|
||||
user1_email = "user1@testing.com"
|
||||
user2_email = "user2@testing.com"
|
||||
|
||||
password = "thisisapassword123"
|
||||
|
||||
user1_response = client.post(
|
||||
reverse("user-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "user1",
|
||||
"email": user1_email,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user1_response.status_code == 201
|
||||
|
||||
user1_access_token, _ = get_api_tokens(client, user1_email, password)
|
||||
user1_headers = get_authorization_header(user1_access_token)
|
||||
|
||||
user2_invitation = client.post(
|
||||
reverse("invitation-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "invitations",
|
||||
"attributes": {"email": user2_email},
|
||||
"relationships": {
|
||||
"roles": {
|
||||
"data": [
|
||||
{
|
||||
"type": "roles",
|
||||
"id": str(role.id),
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=user1_headers,
|
||||
)
|
||||
assert user2_invitation.status_code == 201
|
||||
invitation_token = user2_invitation.json()["data"]["attributes"]["token"]
|
||||
|
||||
user2_response = client.post(
|
||||
reverse("user-list") + f"?invitation_token={invitation_token}",
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "user2",
|
||||
"email": user2_email,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user2_response.status_code == 201
|
||||
|
||||
user2_access_token, _ = get_api_tokens(client, user2_email, password)
|
||||
user2_headers = get_authorization_header(user2_access_token)
|
||||
|
||||
user1_me = client.get(reverse("user-me"), headers=user1_headers)
|
||||
assert user1_me.status_code == 200
|
||||
assert user1_me.json()["data"]["attributes"]["email"] == user1_email
|
||||
|
||||
user2_me = client.get(reverse("user-me"), headers=user2_headers)
|
||||
assert user2_me.status_code == 200
|
||||
assert user2_me.json()["data"]["attributes"]["email"] == user2_email
|
||||
|
||||
85
api/src/backend/api/tests/integration/test_providers.py
Normal file
85
api/src/backend/api/tests/integration/test_providers.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from conftest import get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.models import Provider
|
||||
|
||||
|
||||
@patch("api.v1.views.Task.objects.get")
|
||||
@patch("api.v1.views.delete_provider_task.delay")
|
||||
@pytest.mark.django_db
|
||||
def test_delete_provider_without_executing_task(
|
||||
mock_delete_task, mock_task_get, create_test_user, tenants_fixture, tasks_fixture
|
||||
):
|
||||
client = APIClient()
|
||||
|
||||
test_user = "test_email@prowler.com"
|
||||
test_password = "test_password"
|
||||
|
||||
prowler_task = tasks_fixture[0]
|
||||
task_mock = Mock()
|
||||
task_mock.id = prowler_task.id
|
||||
mock_delete_task.return_value = task_mock
|
||||
mock_task_get.return_value = prowler_task
|
||||
|
||||
user_creation_response = client.post(
|
||||
reverse("user-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "test",
|
||||
"email": test_user,
|
||||
"password": test_password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user_creation_response.status_code == 201
|
||||
|
||||
access_token, _ = get_api_tokens(client, test_user, test_password)
|
||||
auth_headers = get_authorization_header(access_token)
|
||||
|
||||
create_provider_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": Provider.ProviderChoices.AWS,
|
||||
"uid": "123456789012",
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert create_provider_response.status_code == 201
|
||||
provider_id = create_provider_response.json()["data"]["id"]
|
||||
provider_uid = create_provider_response.json()["data"]["attributes"]["uid"]
|
||||
|
||||
remove_provider = client.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider_id}),
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert remove_provider.status_code == 202
|
||||
|
||||
recreate_provider_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": Provider.ProviderChoices.AWS,
|
||||
"uid": provider_uid,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert recreate_provider_response.status_code == 201
|
||||
@@ -13,6 +13,7 @@ def test_check_resources_between_different_tenants(
|
||||
enforce_test_user_db_connection,
|
||||
authenticated_api_client,
|
||||
tenants_fixture,
|
||||
set_user_admin_roles_fixture,
|
||||
):
|
||||
client = authenticated_api_client
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ from django.db.utils import ConnectionRouter
|
||||
from api.db_router import MainRouter
|
||||
from api.rls import Tenant
|
||||
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@patch("api.db_router.MainRouter.admin_db", new="admin")
|
||||
class TestMainDatabaseRouter:
|
||||
@pytest.fixture(scope="module")
|
||||
def router(self):
|
||||
|
||||
@@ -2,7 +2,15 @@ from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
|
||||
import pytest
|
||||
|
||||
from api.db_utils import (
|
||||
batch_delete,
|
||||
enum_to_choices,
|
||||
generate_random_token,
|
||||
one_week_from_now,
|
||||
)
|
||||
from api.models import Provider
|
||||
|
||||
|
||||
class TestEnumToChoices:
|
||||
@@ -106,3 +114,26 @@ class TestGenerateRandomToken:
|
||||
token = generate_random_token(length=5, symbols="")
|
||||
# Default symbols
|
||||
assert len(token) == 5
|
||||
|
||||
|
||||
class TestBatchDelete:
|
||||
@pytest.fixture
|
||||
def create_test_providers(self, tenants_fixture):
|
||||
tenant = tenants_fixture[0]
|
||||
provider_id = 123456789012
|
||||
provider_count = 10
|
||||
for i in range(provider_count):
|
||||
Provider.objects.create(
|
||||
tenant=tenant,
|
||||
uid=f"{provider_id + i}",
|
||||
provider=Provider.ProviderChoices.AWS,
|
||||
)
|
||||
return provider_count
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_batch_delete(self, create_test_providers):
|
||||
_, summary = batch_delete(
|
||||
Provider.objects.all(), batch_size=create_test_providers // 2
|
||||
)
|
||||
assert Provider.objects.all().count() == 0
|
||||
assert summary == {"api.Provider": create_test_providers}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from unittest.mock import patch, call
|
||||
import uuid
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
from api.decorators import set_tenant
|
||||
|
||||
|
||||
@@ -15,12 +17,12 @@ class TestSetTenantDecorator:
|
||||
def random_func(arg):
|
||||
return arg
|
||||
|
||||
tenant_id = "1234-abcd-5678"
|
||||
tenant_id = str(uuid.uuid4())
|
||||
|
||||
result = random_func("test_arg", tenant_id=tenant_id)
|
||||
|
||||
assert (
|
||||
call(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
in mock_cursor.execute.mock_calls
|
||||
)
|
||||
assert result == "test_arg"
|
||||
|
||||
@@ -7,9 +7,10 @@ from api.models import Resource, ResourceTag
|
||||
class TestResourceModel:
|
||||
def test_setting_tags(self, providers_fixture):
|
||||
provider, *_ = providers_fixture
|
||||
tenant_id = provider.tenant_id
|
||||
|
||||
resource = Resource.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
|
||||
name="My Instance 1",
|
||||
@@ -20,12 +21,12 @@ class TestResourceModel:
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
tenant_id=tenant_id,
|
||||
key="key",
|
||||
value="value",
|
||||
),
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
tenant_id=tenant_id,
|
||||
key="key2",
|
||||
value="value2",
|
||||
),
|
||||
@@ -33,9 +34,9 @@ class TestResourceModel:
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert len(tags) == len(resource.tags.all())
|
||||
assert len(tags) == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
|
||||
tags_dict = resource.get_tags()
|
||||
tags_dict = resource.get_tags(tenant_id=tenant_id)
|
||||
|
||||
for tag in tags:
|
||||
assert tag.key in tags_dict
|
||||
@@ -43,47 +44,51 @@ class TestResourceModel:
|
||||
|
||||
def test_adding_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=resource.tenant_id,
|
||||
tenant_id=tenant_id,
|
||||
key="env",
|
||||
value="test",
|
||||
),
|
||||
]
|
||||
before_count = len(resource.tags.all())
|
||||
before_count = len(resource.tags.filter(tenant_id=tenant_id))
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert before_count + 1 == len(resource.tags.all())
|
||||
assert before_count + 1 == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
|
||||
tags_dict = resource.get_tags()
|
||||
tags_dict = resource.get_tags(tenant_id=tenant_id)
|
||||
|
||||
assert "env" in tags_dict
|
||||
assert tags_dict["env"] == "test"
|
||||
|
||||
def test_adding_duplicate_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
|
||||
tags = resource.tags.all()
|
||||
tags = resource.tags.filter(tenant_id=tenant_id)
|
||||
|
||||
before_count = len(resource.tags.all())
|
||||
before_count = len(resource.tags.filter(tenant_id=tenant_id))
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
# should be the same number of tags
|
||||
assert before_count == len(resource.tags.all())
|
||||
assert before_count == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
|
||||
def test_add_tags_none(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
resource.upsert_or_delete_tags(None)
|
||||
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
|
||||
assert resource.get_tags(tenant_id=tenant_id) == {}
|
||||
|
||||
def test_clear_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
resource.clear_tags()
|
||||
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
|
||||
assert resource.get_tags(tenant_id=tenant_id) == {}
|
||||
|
||||
306
api/src/backend/api/tests/test_rbac.py
Normal file
306
api/src/backend/api/tests/test_rbac.py
Normal file
@@ -0,0 +1,306 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from unittest.mock import patch, ANY, Mock
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserViewSet:
|
||||
def test_list_users_with_all_permissions(self, authenticated_client_rbac):
|
||||
response = authenticated_client_rbac.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert isinstance(response.json()["data"], list)
|
||||
|
||||
def test_list_users_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_retrieve_user_with_all_permissions(
|
||||
self, authenticated_client_rbac, create_test_user_rbac
|
||||
):
|
||||
response = authenticated_client_rbac.get(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
response.json()["data"]["attributes"]["email"]
|
||||
== create_test_user_rbac.email
|
||||
)
|
||||
|
||||
def test_retrieve_user_with_no_roles(
|
||||
self, authenticated_client_rbac_noroles, create_test_user_rbac_no_roles
|
||||
):
|
||||
response = authenticated_client_rbac_noroles.get(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user_rbac_no_roles.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_retrieve_user_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, create_test_user
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.get(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_create_user_with_all_permissions(self, authenticated_client_rbac):
|
||||
valid_user_payload = {
|
||||
"name": "test",
|
||||
"password": "newpassword123",
|
||||
"email": "new_user@test.com",
|
||||
}
|
||||
response = authenticated_client_rbac.post(
|
||||
reverse("user-list"), data=valid_user_payload, format="vnd.api+json"
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["data"]["attributes"]["email"] == "new_user@test.com"
|
||||
|
||||
def test_create_user_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac
|
||||
):
|
||||
valid_user_payload = {
|
||||
"name": "test",
|
||||
"password": "newpassword123",
|
||||
"email": "new_user@test.com",
|
||||
}
|
||||
response = authenticated_client_no_permissions_rbac.post(
|
||||
reverse("user-list"), data=valid_user_payload, format="vnd.api+json"
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["data"]["attributes"]["email"] == "new_user@test.com"
|
||||
|
||||
def test_partial_update_user_with_all_permissions(
|
||||
self, authenticated_client_rbac, create_test_user_rbac
|
||||
):
|
||||
updated_data = {
|
||||
"data": {
|
||||
"type": "users",
|
||||
"id": str(create_test_user_rbac.id),
|
||||
"attributes": {"name": "Updated Name"},
|
||||
},
|
||||
}
|
||||
response = authenticated_client_rbac.patch(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id}),
|
||||
data=updated_data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["name"] == "Updated Name"
|
||||
|
||||
def test_partial_update_user_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, create_test_user
|
||||
):
|
||||
updated_data = {
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {"name": "Updated Name"},
|
||||
}
|
||||
}
|
||||
response = authenticated_client_no_permissions_rbac.patch(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user.id}),
|
||||
data=updated_data,
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_delete_user_with_all_permissions(
|
||||
self, authenticated_client_rbac, create_test_user_rbac
|
||||
):
|
||||
response = authenticated_client_rbac.delete(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
def test_delete_user_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, create_test_user
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.delete(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_me_with_all_permissions(
|
||||
self, authenticated_client_rbac, create_test_user_rbac
|
||||
):
|
||||
response = authenticated_client_rbac.get(reverse("user-me"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
response.json()["data"]["attributes"]["email"]
|
||||
== create_test_user_rbac.email
|
||||
)
|
||||
|
||||
def test_me_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, create_test_user
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.get(reverse("user-me"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["email"] == "rbac_limited@rbac.com"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderViewSet:
|
||||
def test_list_providers_with_all_permissions(
|
||||
self, authenticated_client_rbac, providers_fixture
|
||||
):
|
||||
response = authenticated_client_rbac.get(reverse("provider-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(providers_fixture)
|
||||
|
||||
def test_list_providers_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.get(
|
||||
reverse("provider-list")
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 0
|
||||
|
||||
def test_retrieve_provider_with_all_permissions(
|
||||
self, authenticated_client_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
response = authenticated_client_rbac.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["alias"] == provider.alias
|
||||
|
||||
def test_retrieve_provider_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
response = authenticated_client_no_permissions_rbac.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_create_provider_with_all_permissions(self, authenticated_client_rbac):
|
||||
payload = {"provider": "aws", "uid": "111111111111", "alias": "new_alias"}
|
||||
response = authenticated_client_rbac.post(
|
||||
reverse("provider-list"), data=payload, format="json"
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["data"]["attributes"]["alias"] == "new_alias"
|
||||
|
||||
def test_create_provider_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac
|
||||
):
|
||||
payload = {"provider": "aws", "uid": "111111111111", "alias": "new_alias"}
|
||||
response = authenticated_client_no_permissions_rbac.post(
|
||||
reverse("provider-list"), data=payload, format="json"
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_partial_update_provider_with_all_permissions(
|
||||
self, authenticated_client_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": provider.id,
|
||||
"attributes": {"alias": "updated_alias"},
|
||||
},
|
||||
}
|
||||
response = authenticated_client_rbac.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider.id}),
|
||||
data=payload,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["alias"] == "updated_alias"
|
||||
|
||||
def test_partial_update_provider_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
update_payload = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {"alias": "updated_alias"},
|
||||
}
|
||||
}
|
||||
response = authenticated_client_no_permissions_rbac.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider.id}),
|
||||
data=update_payload,
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@patch("api.v1.views.Task.objects.get")
|
||||
@patch("api.v1.views.delete_provider_task.delay")
|
||||
def test_delete_provider_with_all_permissions(
|
||||
self,
|
||||
mock_delete_task,
|
||||
mock_task_get,
|
||||
authenticated_client_rbac,
|
||||
providers_fixture,
|
||||
tasks_fixture,
|
||||
):
|
||||
prowler_task = tasks_fixture[0]
|
||||
task_mock = Mock()
|
||||
task_mock.id = prowler_task.id
|
||||
mock_delete_task.return_value = task_mock
|
||||
mock_task_get.return_value = prowler_task
|
||||
|
||||
provider1, *_ = providers_fixture
|
||||
response = authenticated_client_rbac.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
mock_delete_task.assert_called_once_with(
|
||||
provider_id=str(provider1.id), tenant_id=ANY
|
||||
)
|
||||
assert "Content-Location" in response.headers
|
||||
assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}"
|
||||
|
||||
def test_delete_provider_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
response = authenticated_client_no_permissions_rbac.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@patch("api.v1.views.Task.objects.get")
|
||||
@patch("api.v1.views.check_provider_connection_task.delay")
|
||||
def test_connection_with_all_permissions(
|
||||
self,
|
||||
mock_provider_connection,
|
||||
mock_task_get,
|
||||
authenticated_client_rbac,
|
||||
providers_fixture,
|
||||
tasks_fixture,
|
||||
):
|
||||
prowler_task = tasks_fixture[0]
|
||||
task_mock = Mock()
|
||||
task_mock.id = prowler_task.id
|
||||
task_mock.status = "PENDING"
|
||||
mock_provider_connection.return_value = task_mock
|
||||
mock_task_get.return_value = prowler_task
|
||||
|
||||
provider1, *_ = providers_fixture
|
||||
assert provider1.connected is None
|
||||
assert provider1.connection_last_checked_at is None
|
||||
|
||||
response = authenticated_client_rbac.post(
|
||||
reverse("provider-connection", kwargs={"pk": provider1.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
mock_provider_connection.assert_called_once_with(
|
||||
provider_id=str(provider1.id), tenant_id=ANY
|
||||
)
|
||||
assert "Content-Location" in response.headers
|
||||
assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}"
|
||||
|
||||
def test_connection_with_no_permissions(
|
||||
self, authenticated_client_no_permissions_rbac, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
response = authenticated_client_no_permissions_rbac.post(
|
||||
reverse("provider-connection", kwargs={"pk": provider.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
InvitationRoleRelationship,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
@@ -24,10 +25,13 @@ from api.models import (
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
|
||||
@@ -176,10 +180,26 @@ class UserSerializer(BaseSerializerV1):
|
||||
"""
|
||||
|
||||
memberships = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
roles = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "name", "email", "company_name", "date_joined", "memberships"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"company_name",
|
||||
"date_joined",
|
||||
"memberships",
|
||||
"roles",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"roles": {"read_only": True},
|
||||
}
|
||||
|
||||
included_serializers = {
|
||||
"roles": "api.v1.serializers.RoleSerializer",
|
||||
}
|
||||
|
||||
|
||||
class UserCreateSerializer(BaseWriteSerializer):
|
||||
@@ -235,6 +255,73 @@ class UserUpdateSerializer(BaseWriteSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class RoleResourceIdentifierSerializer(serializers.Serializer):
|
||||
resource_type = serializers.CharField(source="type")
|
||||
id = serializers.UUIDField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "role-identifier"
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Ensure 'type' is used in the output instead of 'resource_type'.
|
||||
"""
|
||||
representation = super().to_representation(instance)
|
||||
representation["type"] = representation.pop("resource_type", None)
|
||||
return representation
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Map 'type' back to 'resource_type' during input.
|
||||
"""
|
||||
data["resource_type"] = data.pop("type", None)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for modifying user memberships
|
||||
"""
|
||||
|
||||
roles = serializers.ListField(
|
||||
child=RoleResourceIdentifierSerializer(),
|
||||
help_text="List of resource identifier objects representing roles.",
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
role_ids = [item["id"] for item in validated_data["roles"]]
|
||||
roles = Role.objects.filter(id__in=role_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
new_relationships = [
|
||||
UserRoleRelationship(
|
||||
user=self.context.get("user"), role=r, tenant_id=tenant_id
|
||||
)
|
||||
for r in roles
|
||||
]
|
||||
UserRoleRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
return self.context.get("user")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
role_ids = [item["id"] for item in validated_data["roles"]]
|
||||
roles = Role.objects.filter(id__in=role_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
instance.roles.clear()
|
||||
new_relationships = [
|
||||
UserRoleRelationship(user=instance, role=r, tenant_id=tenant_id)
|
||||
for r in roles
|
||||
]
|
||||
UserRoleRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = UserRoleRelationship
|
||||
fields = ["id", "roles"]
|
||||
|
||||
|
||||
# Tasks
|
||||
class TaskBase(serializers.ModelSerializer):
|
||||
state_mapping = {
|
||||
@@ -358,89 +445,199 @@ class MembershipSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Provider Groups
|
||||
class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
providers = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
providers = serializers.ResourceRelatedField(
|
||||
queryset=Provider.objects.all(), many=True, required=False
|
||||
)
|
||||
roles = serializers.ResourceRelatedField(
|
||||
queryset=Role.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant = self.context["tenant_id"]
|
||||
name = attrs.get("name", self.instance.name if self.instance else None)
|
||||
|
||||
# Exclude the current instance when checking for uniqueness during updates
|
||||
queryset = ProviderGroup.objects.filter(tenant=tenant, name=name)
|
||||
if self.instance:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
|
||||
if queryset.exists():
|
||||
if ProviderGroup.objects.filter(name=attrs.get("name")).exists():
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"name": "A provider group with this name already exists for this tenant."
|
||||
}
|
||||
{"name": "A provider group with this name already exists."}
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
model = ProviderGroup
|
||||
fields = ["id", "name", "inserted_at", "updated_at", "providers", "url"]
|
||||
read_only_fields = ["id", "inserted_at", "updated_at"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"providers",
|
||||
"roles",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"roles": {"read_only": True},
|
||||
"url": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class ProviderGroupIncludedSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
class ProviderGroupIncludedSerializer(ProviderGroupSerializer):
|
||||
class Meta:
|
||||
model = ProviderGroup
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class ProviderGroupUpdateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for updating the ProviderGroup model.
|
||||
Only allows "name" field to be updated.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ProviderGroup
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class ProviderGroupMembershipUpdateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for modifying provider group memberships
|
||||
"""
|
||||
|
||||
provider_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of provider UUIDs to add to the group",
|
||||
class ProviderGroupCreateSerializer(ProviderGroupSerializer):
|
||||
providers = serializers.ResourceRelatedField(
|
||||
queryset=Provider.objects.all(), many=True, required=False
|
||||
)
|
||||
roles = serializers.ResourceRelatedField(
|
||||
queryset=Role.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant_id = self.context["tenant_id"]
|
||||
provider_ids = attrs.get("provider_ids", [])
|
||||
class Meta:
|
||||
model = ProviderGroup
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"providers",
|
||||
"roles",
|
||||
]
|
||||
|
||||
existing_provider_ids = set(
|
||||
Provider.objects.filter(
|
||||
id__in=provider_ids, tenant_id=tenant_id
|
||||
).values_list("id", flat=True)
|
||||
def create(self, validated_data):
|
||||
providers = validated_data.pop("providers", [])
|
||||
roles = validated_data.pop("roles", [])
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
provider_group = ProviderGroup.objects.create(
|
||||
tenant_id=tenant_id, **validated_data
|
||||
)
|
||||
provided_provider_ids = set(provider_ids)
|
||||
|
||||
missing_provider_ids = provided_provider_ids - existing_provider_ids
|
||||
|
||||
if missing_provider_ids:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"provider_ids": f"The following provider IDs do not exist: {', '.join(str(id) for id in missing_provider_ids)}"
|
||||
}
|
||||
through_model_instances = [
|
||||
ProviderGroupMembership(
|
||||
provider_group=provider_group,
|
||||
provider=provider,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for provider in providers
|
||||
]
|
||||
ProviderGroupMembership.objects.bulk_create(through_model_instances)
|
||||
|
||||
return super().validate(attrs)
|
||||
through_model_instances = [
|
||||
RoleProviderGroupRelationship(
|
||||
provider_group=provider_group,
|
||||
role=role,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for role in roles
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
return provider_group
|
||||
|
||||
|
||||
class ProviderGroupUpdateSerializer(ProviderGroupSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
if "providers" in validated_data:
|
||||
providers = validated_data.pop("providers")
|
||||
instance.providers.clear()
|
||||
through_model_instances = [
|
||||
ProviderGroupMembership(
|
||||
provider_group=instance,
|
||||
provider=provider,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for provider in providers
|
||||
]
|
||||
ProviderGroupMembership.objects.bulk_create(through_model_instances)
|
||||
|
||||
if "roles" in validated_data:
|
||||
roles = validated_data.pop("roles")
|
||||
instance.roles.clear()
|
||||
through_model_instances = [
|
||||
RoleProviderGroupRelationship(
|
||||
provider_group=instance,
|
||||
role=role,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for role in roles
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ProviderResourceIdentifierSerializer(serializers.Serializer):
|
||||
resource_type = serializers.CharField(source="type")
|
||||
id = serializers.UUIDField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-identifier"
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Ensure 'type' is used in the output instead of 'resource_type'.
|
||||
"""
|
||||
representation = super().to_representation(instance)
|
||||
representation["type"] = representation.pop("resource_type", None)
|
||||
return representation
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Map 'type' back to 'resource_type' during input.
|
||||
"""
|
||||
data["resource_type"] = data.pop("type", None)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class ProviderGroupMembershipSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for modifying provider_group memberships
|
||||
"""
|
||||
|
||||
providers = serializers.ListField(
|
||||
child=ProviderResourceIdentifierSerializer(),
|
||||
help_text="List of resource identifier objects representing providers.",
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
provider_ids = [item["id"] for item in validated_data["providers"]]
|
||||
providers = Provider.objects.filter(id__in=provider_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
new_relationships = [
|
||||
ProviderGroupMembership(
|
||||
provider_group=self.context.get("provider_group"),
|
||||
provider=p,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for p in providers
|
||||
]
|
||||
ProviderGroupMembership.objects.bulk_create(new_relationships)
|
||||
|
||||
return self.context.get("provider_group")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
provider_ids = [item["id"] for item in validated_data["providers"]]
|
||||
providers = Provider.objects.filter(id__in=provider_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
instance.providers.clear()
|
||||
new_relationships = [
|
||||
ProviderGroupMembership(
|
||||
provider_group=instance, provider=p, tenant_id=tenant_id
|
||||
)
|
||||
for p in providers
|
||||
]
|
||||
ProviderGroupMembership.objects.bulk_create(new_relationships)
|
||||
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = ProviderGroupMembership
|
||||
fields = ["id", "provider_ids"]
|
||||
fields = ["id", "providers"]
|
||||
|
||||
|
||||
# Providers
|
||||
@@ -677,7 +874,7 @@ class ResourceSerializer(RLSSerializer):
|
||||
}
|
||||
)
|
||||
def get_tags(self, obj):
|
||||
return obj.get_tags()
|
||||
return obj.get_tags(self.context.get("tenant_id"))
|
||||
|
||||
def get_fields(self):
|
||||
"""`type` is a Python reserved keyword."""
|
||||
@@ -1034,6 +1231,14 @@ class InvitationSerializer(RLSSerializer):
|
||||
Serializer for the Invitation model.
|
||||
"""
|
||||
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = [
|
||||
@@ -1043,6 +1248,7 @@ class InvitationSerializer(RLSSerializer):
|
||||
"email",
|
||||
"state",
|
||||
"token",
|
||||
"roles",
|
||||
"expires_at",
|
||||
"inviter",
|
||||
"url",
|
||||
@@ -1050,6 +1256,14 @@ class InvitationSerializer(RLSSerializer):
|
||||
|
||||
|
||||
class InvitationBaseWriteSerializer(BaseWriteSerializer):
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
def validate_email(self, value):
|
||||
user = User.objects.filter(email=value).first()
|
||||
tenant_id = self.context["tenant_id"]
|
||||
@@ -1086,31 +1300,62 @@ class InvitationCreateSerializer(InvitationBaseWriteSerializer, RLSSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = ["email", "expires_at", "state", "token", "inviter"]
|
||||
fields = ["email", "expires_at", "state", "token", "inviter", "roles"]
|
||||
extra_kwargs = {
|
||||
"token": {"read_only": True},
|
||||
"state": {"read_only": True},
|
||||
"inviter": {"read_only": True},
|
||||
"expires_at": {"required": False},
|
||||
"roles": {"required": False},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
inviter = self.context.get("request").user
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
validated_data["inviter"] = inviter
|
||||
return super().create(validated_data)
|
||||
roles = validated_data.pop("roles", [])
|
||||
invitation = super().create(validated_data)
|
||||
for role in roles:
|
||||
InvitationRoleRelationship.objects.create(
|
||||
role=role, invitation=invitation, tenant_id=tenant_id
|
||||
)
|
||||
|
||||
return invitation
|
||||
|
||||
|
||||
class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
|
||||
roles = serializers.ResourceRelatedField(
|
||||
required=False, many=True, queryset=Role.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = ["id", "email", "expires_at", "state", "token"]
|
||||
fields = ["id", "email", "expires_at", "state", "token", "roles"]
|
||||
extra_kwargs = {
|
||||
"token": {"read_only": True},
|
||||
"state": {"read_only": True},
|
||||
"expires_at": {"required": False},
|
||||
"email": {"required": False},
|
||||
"roles": {"required": False},
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if "roles" in validated_data:
|
||||
roles = validated_data.pop("roles")
|
||||
instance.roles.clear()
|
||||
new_relationships = [
|
||||
InvitationRoleRelationship(
|
||||
role=r, invitation=instance, tenant_id=tenant_id
|
||||
)
|
||||
for r in roles
|
||||
]
|
||||
InvitationRoleRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
invitation = super().update(instance, validated_data)
|
||||
|
||||
return invitation
|
||||
|
||||
|
||||
class InvitationAcceptSerializer(RLSSerializer):
|
||||
"""Serializer for accepting an invitation."""
|
||||
@@ -1122,6 +1367,218 @@ class InvitationAcceptSerializer(RLSSerializer):
|
||||
fields = ["invitation_token"]
|
||||
|
||||
|
||||
# Roles
|
||||
|
||||
|
||||
class RoleSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
permission_state = serializers.SerializerMethodField()
|
||||
users = serializers.ResourceRelatedField(
|
||||
queryset=User.objects.all(), many=True, required=False
|
||||
)
|
||||
provider_groups = serializers.ResourceRelatedField(
|
||||
queryset=ProviderGroup.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["users"].queryset = User.objects.filter(
|
||||
membership__tenant__id=tenant_id
|
||||
)
|
||||
self.fields["provider_groups"].queryset = ProviderGroup.objects.filter(
|
||||
tenant_id=self.context.get("tenant_id")
|
||||
)
|
||||
|
||||
def get_permission_state(self, obj) -> str:
|
||||
return obj.permission_state
|
||||
|
||||
def validate(self, attrs):
|
||||
if Role.objects.filter(name=attrs.get("name")).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"name": "A role with this name already exists."}
|
||||
)
|
||||
|
||||
if attrs.get("manage_providers"):
|
||||
attrs["unlimited_visibility"] = True
|
||||
|
||||
# Prevent updates to the admin role
|
||||
if getattr(self.instance, "name", None) == "admin":
|
||||
raise serializers.ValidationError(
|
||||
{"name": "The admin role cannot be updated."}
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
# Disable for the first release
|
||||
# "manage_billing",
|
||||
# "manage_integrations",
|
||||
# /Disable for the first release
|
||||
"manage_providers",
|
||||
"manage_scans",
|
||||
"permission_state",
|
||||
"unlimited_visibility",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"provider_groups",
|
||||
"users",
|
||||
"invitations",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"url": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class RoleCreateSerializer(RoleSerializer):
|
||||
provider_groups = serializers.ResourceRelatedField(
|
||||
many=True, queryset=ProviderGroup.objects.all(), required=False
|
||||
)
|
||||
users = serializers.ResourceRelatedField(
|
||||
many=True, queryset=User.objects.all(), required=False
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
provider_groups = validated_data.pop("provider_groups", [])
|
||||
users = validated_data.pop("users", [])
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
role = Role.objects.create(tenant_id=tenant_id, **validated_data)
|
||||
|
||||
through_model_instances = [
|
||||
RoleProviderGroupRelationship(
|
||||
role=role,
|
||||
provider_group=provider_group,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for provider_group in provider_groups
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
through_model_instances = [
|
||||
UserRoleRelationship(
|
||||
role=role,
|
||||
user=user,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
UserRoleRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
return role
|
||||
|
||||
|
||||
class RoleUpdateSerializer(RoleSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
if "provider_groups" in validated_data:
|
||||
provider_groups = validated_data.pop("provider_groups")
|
||||
instance.provider_groups.clear()
|
||||
through_model_instances = [
|
||||
RoleProviderGroupRelationship(
|
||||
role=instance,
|
||||
provider_group=provider_group,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for provider_group in provider_groups
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
if "users" in validated_data:
|
||||
users = validated_data.pop("users")
|
||||
instance.users.clear()
|
||||
through_model_instances = [
|
||||
UserRoleRelationship(
|
||||
role=instance,
|
||||
user=user,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
UserRoleRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):
|
||||
resource_type = serializers.CharField(source="type")
|
||||
id = serializers.UUIDField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-group-identifier"
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""
|
||||
Ensure 'type' is used in the output instead of 'resource_type'.
|
||||
"""
|
||||
representation = super().to_representation(instance)
|
||||
representation["type"] = representation.pop("resource_type", None)
|
||||
return representation
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Map 'type' back to 'resource_type' during input.
|
||||
"""
|
||||
data["resource_type"] = data.pop("type", None)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class RoleProviderGroupRelationshipSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for modifying role memberships
|
||||
"""
|
||||
|
||||
provider_groups = serializers.ListField(
|
||||
child=ProviderGroupResourceIdentifierSerializer(),
|
||||
help_text="List of resource identifier objects representing provider groups.",
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
provider_group_ids = [item["id"] for item in validated_data["provider_groups"]]
|
||||
provider_groups = ProviderGroup.objects.filter(id__in=provider_group_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
new_relationships = [
|
||||
RoleProviderGroupRelationship(
|
||||
role=self.context.get("role"), provider_group=pg, tenant_id=tenant_id
|
||||
)
|
||||
for pg in provider_groups
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
return self.context.get("role")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
provider_group_ids = [item["id"] for item in validated_data["provider_groups"]]
|
||||
provider_groups = ProviderGroup.objects.filter(id__in=provider_group_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
instance.provider_groups.clear()
|
||||
new_relationships = [
|
||||
RoleProviderGroupRelationship(
|
||||
role=instance, provider_group=pg, tenant_id=tenant_id
|
||||
)
|
||||
for pg in provider_groups
|
||||
]
|
||||
RoleProviderGroupRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = RoleProviderGroupRelationship
|
||||
fields = ["id", "provider_groups"]
|
||||
|
||||
|
||||
# Compliance overview
|
||||
|
||||
|
||||
@@ -1334,6 +1791,24 @@ class OverviewSeveritySerializer(serializers.Serializer):
|
||||
return {"version": "v1"}
|
||||
|
||||
|
||||
class OverviewServiceSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(source="service")
|
||||
total = serializers.IntegerField()
|
||||
_pass = serializers.IntegerField()
|
||||
fail = serializers.IntegerField()
|
||||
muted = serializers.IntegerField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "services-overview"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["pass"] = self.fields.pop("_pass")
|
||||
|
||||
def get_root_meta(self, _resource, _many):
|
||||
return {"version": "v1"}
|
||||
|
||||
|
||||
# Schedules
|
||||
|
||||
|
||||
|
||||
@@ -3,16 +3,20 @@ from drf_spectacular.views import SpectacularRedocView
|
||||
from rest_framework_nested import routers
|
||||
|
||||
from api.v1.views import (
|
||||
ComplianceOverviewViewSet,
|
||||
CustomTokenObtainView,
|
||||
CustomTokenRefreshView,
|
||||
FindingViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
InvitationViewSet,
|
||||
MembershipViewSet,
|
||||
OverviewViewSet,
|
||||
ProviderGroupViewSet,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
ProviderSecretViewSet,
|
||||
InvitationViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
RoleViewSet,
|
||||
RoleProviderGroupRelationshipView,
|
||||
UserRoleRelationshipView,
|
||||
OverviewViewSet,
|
||||
ComplianceOverviewViewSet,
|
||||
ProviderViewSet,
|
||||
ResourceViewSet,
|
||||
ScanViewSet,
|
||||
@@ -29,11 +33,12 @@ router = routers.DefaultRouter(trailing_slash=False)
|
||||
router.register(r"users", UserViewSet, basename="user")
|
||||
router.register(r"tenants", TenantViewSet, basename="tenant")
|
||||
router.register(r"providers", ProviderViewSet, basename="provider")
|
||||
router.register(r"provider_groups", ProviderGroupViewSet, basename="providergroup")
|
||||
router.register(r"provider-groups", ProviderGroupViewSet, basename="providergroup")
|
||||
router.register(r"scans", ScanViewSet, basename="scan")
|
||||
router.register(r"tasks", TaskViewSet, basename="task")
|
||||
router.register(r"resources", ResourceViewSet, basename="resource")
|
||||
router.register(r"findings", FindingViewSet, basename="finding")
|
||||
router.register(r"roles", RoleViewSet, basename="role")
|
||||
router.register(
|
||||
r"compliance-overviews", ComplianceOverviewViewSet, basename="complianceoverview"
|
||||
)
|
||||
@@ -80,6 +85,27 @@ urlpatterns = [
|
||||
InvitationAcceptViewSet.as_view({"post": "accept"}),
|
||||
name="invitation-accept",
|
||||
),
|
||||
path(
|
||||
"roles/<uuid:pk>/relationships/provider_groups",
|
||||
RoleProviderGroupRelationshipView.as_view(
|
||||
{"post": "create", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="role-provider-groups-relationship",
|
||||
),
|
||||
path(
|
||||
"users/<uuid:pk>/relationships/roles",
|
||||
UserRoleRelationshipView.as_view(
|
||||
{"post": "create", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="user-roles-relationship",
|
||||
),
|
||||
path(
|
||||
"provider-groups/<uuid:pk>/relationships/providers",
|
||||
ProviderGroupProvidersRelationshipView.as_view(
|
||||
{"post": "create", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
name="provider_group-providers-relationship",
|
||||
),
|
||||
path("", include(router.urls)),
|
||||
path("", include(tenants_router.urls)),
|
||||
path("", include(users_router.urls)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,21 @@
|
||||
from celery import Celery, Task
|
||||
from config.env import env
|
||||
|
||||
BROKER_VISIBILITY_TIMEOUT = env.int("DJANGO_BROKER_VISIBILITY_TIMEOUT", default=86400)
|
||||
|
||||
celery_app = Celery("tasks")
|
||||
|
||||
celery_app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
celery_app.conf.update(result_extended=True, result_expires=None)
|
||||
|
||||
celery_app.conf.broker_transport_options = {
|
||||
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
|
||||
}
|
||||
celery_app.conf.result_backend_transport_options = {
|
||||
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
|
||||
}
|
||||
celery_app.conf.visibility_timeout = BROKER_VISIBILITY_TIMEOUT
|
||||
|
||||
celery_app.autodiscover_tasks(["api"])
|
||||
|
||||
|
||||
@@ -35,10 +46,10 @@ class RLSTask(Task):
|
||||
**options,
|
||||
)
|
||||
task_result_instance = TaskResult.objects.get(task_id=result.task_id)
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.db_utils import rls_transaction
|
||||
|
||||
tenant_id = kwargs.get("tenant_id")
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
APITask.objects.create(
|
||||
id=task_result_instance.task_id,
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -10,8 +10,8 @@ DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "psqlextra.backend",
|
||||
"NAME": "prowler_db_test",
|
||||
"USER": env("POSTGRES_USER", default="prowler"),
|
||||
"PASSWORD": env("POSTGRES_PASSWORD", default="S3cret"),
|
||||
"USER": env("POSTGRES_USER", default="prowler_admin"),
|
||||
"PASSWORD": env("POSTGRES_PASSWORD", default="postgres"),
|
||||
"HOST": env("POSTGRES_HOST", default="localhost"),
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
},
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from django.db import connections as django_connections, connection as django_connection
|
||||
from django.db import connection as django_connection
|
||||
from django.db import connections as django_connections
|
||||
from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Role,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
StateChoices,
|
||||
Task,
|
||||
Membership,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.v1.serializers import TokenSerializer
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
|
||||
@@ -83,8 +87,150 @@ def create_test_user(django_db_setup, django_db_blocker):
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing",
|
||||
email="rbac@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = tenants_fixture[0]
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
role=Membership.RoleChoices.OWNER,
|
||||
)
|
||||
Role.objects.create(
|
||||
name="admin",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user,
|
||||
role=Role.objects.get(name="admin"),
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker, tenants_fixture):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing",
|
||||
email="rbac_noroles@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = tenants_fixture[0]
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
role=Membership.RoleChoices.OWNER,
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing_limited",
|
||||
email="rbac_limited@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = Tenant.objects.create(
|
||||
name="Tenant Test",
|
||||
)
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
role=Membership.RoleChoices.OWNER,
|
||||
)
|
||||
Role.objects.create(
|
||||
name="limited",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
manage_billing=False,
|
||||
manage_providers=False,
|
||||
manage_integrations=False,
|
||||
manage_scans=False,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user,
|
||||
role=Role.objects.get(name="limited"),
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client(create_test_user, tenants_fixture, client):
|
||||
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
|
||||
client.user = create_test_user_rbac
|
||||
tenant_id = tenants_fixture[0].id
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client_rbac_noroles(
|
||||
create_test_user_rbac_no_roles, tenants_fixture, client
|
||||
):
|
||||
client.user = create_test_user_rbac_no_roles
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac_noroles@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client_no_permissions_rbac(
|
||||
create_test_user_rbac_limited, tenants_fixture, client
|
||||
):
|
||||
client.user = create_test_user_rbac_limited
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac_limited@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client(
|
||||
create_test_user, tenants_fixture, set_user_admin_roles_fixture, client
|
||||
):
|
||||
client.user = create_test_user
|
||||
serializer = TokenSerializer(
|
||||
data={"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD}
|
||||
@@ -104,6 +250,7 @@ def authenticated_api_client(create_test_user, tenants_fixture):
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
|
||||
return client
|
||||
|
||||
|
||||
@@ -128,13 +275,37 @@ def tenants_fixture(create_test_user):
|
||||
tenant3 = Tenant.objects.create(
|
||||
name="Tenant Three",
|
||||
)
|
||||
|
||||
return tenant1, tenant2, tenant3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_user_admin_roles_fixture(create_test_user, tenants_fixture):
|
||||
user = create_test_user
|
||||
for tenant in tenants_fixture[:2]:
|
||||
with rls_transaction(str(tenant.id)):
|
||||
role = Role.objects.create(
|
||||
name="admin",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user,
|
||||
role=role,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def invitations_fixture(create_test_user, tenants_fixture):
|
||||
user = create_test_user
|
||||
*_, tenant = tenants_fixture
|
||||
tenant = tenants_fixture[0]
|
||||
valid_invitation = Invitation.objects.create(
|
||||
email="testing@prowler.com",
|
||||
state=Invitation.State.PENDING,
|
||||
@@ -153,6 +324,20 @@ def invitations_fixture(create_test_user, tenants_fixture):
|
||||
return valid_invitation, expired_invitation
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def users_fixture(django_user_model):
|
||||
user1 = User.objects.create_user(
|
||||
name="user1", email="test_unit0@prowler.com", password="S3cret"
|
||||
)
|
||||
user2 = User.objects.create_user(
|
||||
name="user2", email="test_unit1@prowler.com", password="S3cret"
|
||||
)
|
||||
user3 = User.objects.create_user(
|
||||
name="user3", email="test_unit2@prowler.com", password="S3cret"
|
||||
)
|
||||
return user1, user2, user3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def providers_fixture(tenants_fixture):
|
||||
tenant, *_ = tenants_fixture
|
||||
@@ -210,6 +395,74 @@ def provider_groups_fixture(tenants_fixture):
|
||||
return pgroup1, pgroup2, pgroup3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_role_fixture(tenants_fixture):
|
||||
tenant, *_ = tenants_fixture
|
||||
|
||||
return Role.objects.get_or_create(
|
||||
name="admin",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def roles_fixture(tenants_fixture):
|
||||
tenant, *_ = tenants_fixture
|
||||
role1 = Role.objects.create(
|
||||
name="Role One",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=False,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
role2 = Role.objects.create(
|
||||
name="Role Two",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
manage_billing=False,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)
|
||||
role3 = Role.objects.create(
|
||||
name="Role Three",
|
||||
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,
|
||||
)
|
||||
role4 = Role.objects.create(
|
||||
name="Role Four",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
manage_billing=False,
|
||||
manage_providers=False,
|
||||
manage_integrations=False,
|
||||
manage_scans=False,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
|
||||
return role1, role2, role3, role4
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider_secret_fixture(providers_fixture):
|
||||
return tuple(
|
||||
@@ -537,10 +790,107 @@ def get_api_tokens(
|
||||
data=json_body,
|
||||
format="vnd.api+json",
|
||||
)
|
||||
return response.json()["data"]["attributes"]["access"], response.json()["data"][
|
||||
"attributes"
|
||||
]["refresh"]
|
||||
return (
|
||||
response.json()["data"]["attributes"]["access"],
|
||||
response.json()["data"]["attributes"]["refresh"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
scan = Scan.objects.create(
|
||||
name="overview scan",
|
||||
provider=provider,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
check_id="check1",
|
||||
service="service1",
|
||||
severity="high",
|
||||
region="region1",
|
||||
_pass=1,
|
||||
fail=0,
|
||||
muted=0,
|
||||
total=1,
|
||||
new=1,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
fail_new=0,
|
||||
fail_changed=0,
|
||||
pass_new=1,
|
||||
pass_changed=0,
|
||||
muted_new=0,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
check_id="check1",
|
||||
service="service1",
|
||||
severity="high",
|
||||
region="region2",
|
||||
_pass=0,
|
||||
fail=1,
|
||||
muted=1,
|
||||
total=2,
|
||||
new=2,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
fail_new=1,
|
||||
fail_changed=0,
|
||||
pass_new=0,
|
||||
pass_changed=0,
|
||||
muted_new=1,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
check_id="check2",
|
||||
service="service2",
|
||||
severity="critical",
|
||||
region="region1",
|
||||
_pass=1,
|
||||
fail=0,
|
||||
muted=0,
|
||||
total=1,
|
||||
new=1,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
fail_new=0,
|
||||
fail_changed=0,
|
||||
pass_new=1,
|
||||
pass_changed=0,
|
||||
muted_new=0,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
|
||||
|
||||
def get_authorization_header(access_token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""Ensure test_rbac.py is executed first."""
|
||||
items.sort(key=lambda item: 0 if "test_rbac.py" in item.nodeid else 1)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
# Apply the mock before the test session starts. This is necessary to avoid admin error when running the
|
||||
# 0004_rbac_missing_admin_roles migration
|
||||
patch("api.db_router.MainRouter.admin_db", new="default").start()
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
# Stop all patches after the test session ends. This is necessary to avoid admin error when running the
|
||||
# 0004_rbac_missing_admin_roles migration
|
||||
patch.stopall()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.db import transaction
|
||||
|
||||
from api.db_utils import batch_delete
|
||||
from api.models import Finding, Provider, Resource, Scan, ScanSummary
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import batch_delete, rls_transaction
|
||||
from api.models import Finding, Provider, Resource, Scan, ScanSummary, Tenant
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
@@ -49,3 +50,26 @@ def delete_provider(pk: str):
|
||||
deletion_summary.update(provider_summary)
|
||||
|
||||
return deletion_summary
|
||||
|
||||
|
||||
def delete_tenant(pk: str):
|
||||
"""
|
||||
Gracefully deletes an instance of a tenant along with its related data.
|
||||
|
||||
Args:
|
||||
pk (str): The primary key of the Tenant instance to delete.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with the count of deleted objects per model,
|
||||
including related models.
|
||||
"""
|
||||
deletion_summary = {}
|
||||
|
||||
for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk):
|
||||
with rls_transaction(pk):
|
||||
summary = delete_provider(provider.id)
|
||||
deletion_summary.update(summary)
|
||||
|
||||
Tenant.objects.using(MainRouter.admin_db).filter(id=pk).delete()
|
||||
|
||||
return deletion_summary
|
||||
|
||||
@@ -11,7 +11,7 @@ from api.compliance import (
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
|
||||
generate_scan_compliance,
|
||||
)
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
@@ -69,7 +69,7 @@ def _store_resources(
|
||||
- tuple[str, str]: A tuple containing the resource UID and region.
|
||||
|
||||
"""
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
resource_instance, created = Resource.objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider_instance,
|
||||
@@ -86,7 +86,7 @@ def _store_resources(
|
||||
resource_instance.service = finding.service_name
|
||||
resource_instance.type = finding.resource_type
|
||||
resource_instance.save()
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
tags = [
|
||||
ResourceTag.objects.get_or_create(
|
||||
tenant_id=tenant_id, key=key, value=value
|
||||
@@ -116,13 +116,12 @@ def perform_prowler_scan(
|
||||
ValueError: If the provider cannot be connected.
|
||||
|
||||
"""
|
||||
generate_compliance = False
|
||||
check_status_by_region = {}
|
||||
exception = None
|
||||
unique_resources = set()
|
||||
start_time = time.time()
|
||||
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
scan_instance = Scan.objects.get(pk=scan_id)
|
||||
scan_instance.state = StateChoices.EXECUTING
|
||||
@@ -130,7 +129,7 @@ def perform_prowler_scan(
|
||||
scan_instance.save()
|
||||
|
||||
try:
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
try:
|
||||
prowler_provider = initialize_prowler_provider(provider_instance)
|
||||
provider_instance.connected = True
|
||||
@@ -145,7 +144,6 @@ def perform_prowler_scan(
|
||||
)
|
||||
provider_instance.save()
|
||||
|
||||
generate_compliance = provider_instance.provider != Provider.ProviderChoices.GCP
|
||||
prowler_scan = ProwlerScan(provider=prowler_provider, checks=checks_to_execute)
|
||||
|
||||
resource_cache = {}
|
||||
@@ -156,7 +154,7 @@ def perform_prowler_scan(
|
||||
for finding in findings:
|
||||
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
|
||||
try:
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
# Process resource
|
||||
resource_uid = finding.resource_uid
|
||||
if resource_uid not in resource_cache:
|
||||
@@ -188,7 +186,7 @@ def perform_prowler_scan(
|
||||
resource_instance.type = finding.resource_type
|
||||
updated_fields.append("type")
|
||||
if updated_fields:
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
resource_instance.save(update_fields=updated_fields)
|
||||
except (OperationalError, IntegrityError) as db_err:
|
||||
if attempt < CELERY_DEADLOCK_ATTEMPTS - 1:
|
||||
@@ -203,7 +201,7 @@ def perform_prowler_scan(
|
||||
|
||||
# Update tags
|
||||
tags = []
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
for key, value in finding.resource_tags.items():
|
||||
tag_key = (key, value)
|
||||
if tag_key not in tag_cache:
|
||||
@@ -219,7 +217,7 @@ def perform_prowler_scan(
|
||||
unique_resources.add((resource_instance.uid, resource_instance.region))
|
||||
|
||||
# Process finding
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
finding_uid = finding.uid
|
||||
if finding_uid not in last_status_cache:
|
||||
most_recent_finding = (
|
||||
@@ -257,7 +255,7 @@ def perform_prowler_scan(
|
||||
finding_instance.add_resources([resource_instance])
|
||||
|
||||
# Update compliance data if applicable
|
||||
if not generate_compliance or finding.status.value == "MUTED":
|
||||
if finding.status.value == "MUTED":
|
||||
continue
|
||||
|
||||
region_dict = check_status_by_region.setdefault(finding.region, {})
|
||||
@@ -267,7 +265,7 @@ def perform_prowler_scan(
|
||||
region_dict[finding.check_id] = finding.status.value
|
||||
|
||||
# Update scan progress
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
scan_instance.progress = progress
|
||||
scan_instance.save()
|
||||
|
||||
@@ -279,13 +277,13 @@ def perform_prowler_scan(
|
||||
scan_instance.state = StateChoices.FAILED
|
||||
|
||||
finally:
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
scan_instance.duration = time.time() - start_time
|
||||
scan_instance.completed_at = datetime.now(tz=timezone.utc)
|
||||
scan_instance.unique_resource_count = len(unique_resources)
|
||||
scan_instance.save()
|
||||
|
||||
if exception is None and generate_compliance:
|
||||
if exception is None:
|
||||
try:
|
||||
regions = prowler_provider.get_regions()
|
||||
except AttributeError:
|
||||
@@ -330,7 +328,7 @@ def perform_prowler_scan(
|
||||
total_requirements=compliance["total_requirements"],
|
||||
)
|
||||
)
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
|
||||
|
||||
if exception is not None:
|
||||
@@ -368,7 +366,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
- muted_new: Muted findings with a delta of 'new'.
|
||||
- muted_changed: Muted findings with a delta of 'changed'.
|
||||
"""
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
findings = Finding.objects.filter(scan_id=scan_id)
|
||||
|
||||
aggregation = findings.values(
|
||||
@@ -464,7 +462,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
),
|
||||
)
|
||||
|
||||
with tenant_transaction(tenant_id):
|
||||
with rls_transaction(tenant_id):
|
||||
scan_aggregations = {
|
||||
ScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -4,10 +4,10 @@ from celery import shared_task
|
||||
from config.celery import RLSTask
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_provider
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
|
||||
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.db_utils import rls_transaction
|
||||
from api.decorators import set_tenant
|
||||
from api.models import Provider, Scan
|
||||
|
||||
@@ -99,7 +99,7 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
"""
|
||||
task_id = self.request.id
|
||||
|
||||
with tenant_transaction(tenant_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}"
|
||||
@@ -134,3 +134,8 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
@shared_task(name="scan-summary")
|
||||
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")
|
||||
def delete_tenant_task(tenant_id: str):
|
||||
return delete_tenant(pk=tenant_id)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import pytest
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from tasks.jobs.deletion import delete_provider
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
|
||||
from api.models import Provider
|
||||
from api.models import Provider, Tenant
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestDeleteInstance:
|
||||
def test_delete_instance_success(self, providers_fixture):
|
||||
class TestDeleteProvider:
|
||||
def test_delete_provider_success(self, providers_fixture):
|
||||
instance = providers_fixture[0]
|
||||
result = delete_provider(instance.id)
|
||||
|
||||
@@ -15,8 +15,46 @@ class TestDeleteInstance:
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
Provider.objects.get(pk=instance.id)
|
||||
|
||||
def test_delete_instance_does_not_exist(self):
|
||||
def test_delete_provider_does_not_exist(self):
|
||||
non_existent_pk = "babf6796-cfcc-4fd3-9dcf-88d012247645"
|
||||
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
delete_provider(non_existent_pk)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestDeleteTenant:
|
||||
def test_delete_tenant_success(self, tenants_fixture, providers_fixture):
|
||||
"""
|
||||
Test successful deletion of a tenant and its related data.
|
||||
"""
|
||||
tenant = tenants_fixture[0]
|
||||
providers = Provider.objects.filter(tenant_id=tenant.id)
|
||||
|
||||
# Ensure the tenant and related providers exist before deletion
|
||||
assert Tenant.objects.filter(id=tenant.id).exists()
|
||||
assert providers.exists()
|
||||
|
||||
# Call the function and validate the result
|
||||
deletion_summary = delete_tenant(tenant.id)
|
||||
|
||||
assert deletion_summary is not None
|
||||
assert not Tenant.objects.filter(id=tenant.id).exists()
|
||||
assert not Provider.objects.filter(tenant_id=tenant.id).exists()
|
||||
|
||||
def test_delete_tenant_with_no_providers(self, tenants_fixture):
|
||||
"""
|
||||
Test deletion of a tenant with no related providers.
|
||||
"""
|
||||
tenant = tenants_fixture[1] # Assume this tenant has no providers
|
||||
providers = Provider.objects.filter(tenant_id=tenant.id)
|
||||
|
||||
# Ensure the tenant exists but has no related providers
|
||||
assert Tenant.objects.filter(id=tenant.id).exists()
|
||||
assert not providers.exists()
|
||||
|
||||
# Call the function and validate the result
|
||||
deletion_summary = delete_tenant(tenant.id)
|
||||
|
||||
assert deletion_summary == {} # No providers, so empty summary
|
||||
assert not Tenant.objects.filter(id=tenant.id).exists()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -26,7 +27,7 @@ class TestPerformScan:
|
||||
providers_fixture,
|
||||
):
|
||||
with (
|
||||
patch("api.db_utils.tenant_transaction"),
|
||||
patch("api.db_utils.rls_transaction"),
|
||||
patch(
|
||||
"tasks.jobs.scan.initialize_prowler_provider"
|
||||
) as mock_initialize_prowler_provider,
|
||||
@@ -165,10 +166,10 @@ class TestPerformScan:
|
||||
"tasks.jobs.scan.initialize_prowler_provider",
|
||||
side_effect=Exception("Connection error"),
|
||||
)
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
def test_perform_prowler_scan_no_connection(
|
||||
self,
|
||||
mock_tenant_transaction,
|
||||
mock_rls_transaction,
|
||||
mock_initialize_prowler_provider,
|
||||
mock_prowler_scan_class,
|
||||
tenants_fixture,
|
||||
@@ -205,14 +206,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
def test_store_resources_new_resource(
|
||||
self,
|
||||
mock_tenant_transaction,
|
||||
mock_rls_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = "tenant123"
|
||||
tenant_id = uuid.uuid4()
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
@@ -253,14 +254,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
def test_store_resources_existing_resource(
|
||||
self,
|
||||
mock_tenant_transaction,
|
||||
mock_rls_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = "tenant123"
|
||||
tenant_id = uuid.uuid4()
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
@@ -310,14 +311,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
def test_store_resources_with_tags(
|
||||
self,
|
||||
mock_tenant_transaction,
|
||||
mock_rls_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = "tenant123"
|
||||
tenant_id = uuid.uuid4()
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
|
||||
11
codecov.yml
Normal file
11
codecov.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
component_management:
|
||||
individual_components:
|
||||
- component_id: "prowler"
|
||||
paths:
|
||||
- "prowler/**"
|
||||
- component_id: "api"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
comment:
|
||||
layout: "header, diff, flags, components"
|
||||
301
contrib/aws/aws-sso-docker/readme.md
Normal file
301
contrib/aws/aws-sso-docker/readme.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# AWS SSO to Prowler Automation Script
|
||||
|
||||
## Table of Contents
|
||||
- [Introduction](#introduction)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Script Overview](#script-overview)
|
||||
- [Usage](#usage)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Customization](#customization)
|
||||
- [Security Considerations](#security-considerations)
|
||||
- [License](#license)
|
||||
|
||||
## Introduction
|
||||
|
||||
This repository provides a Bash script that automates the process of logging into AWS Single Sign-On (SSO), extracting temporary AWS credentials, and running **Prowler**—a security tool that performs AWS security best practices assessments—inside a Docker container using those credentials.
|
||||
|
||||
By following this guide, you can streamline your AWS security assessments, ensuring that you consistently apply best practices across your AWS accounts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure that you have the following tools installed and properly configured on your system:
|
||||
|
||||
1. **AWS CLI v2**
|
||||
- AWS SSO support is available from AWS CLI version 2 onwards.
|
||||
- [Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
|
||||
|
||||
2. **jq**
|
||||
- A lightweight and flexible command-line JSON processor.
|
||||
- **macOS (Homebrew):**
|
||||
```bash
|
||||
brew install jq
|
||||
```
|
||||
- **Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
```
|
||||
- **Windows:**
|
||||
- [Download jq](https://stedolan.github.io/jq/download/)
|
||||
|
||||
3. **Docker**
|
||||
- Ensure Docker is installed and running on your system.
|
||||
- [Docker Installation Guide](https://docs.docker.com/get-docker/)
|
||||
|
||||
4. **AWS SSO Profile Configuration**
|
||||
- Ensure that you have configured an AWS CLI profile with SSO.
|
||||
- [Configuring AWS CLI with SSO](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html)
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/your-username/aws-sso-prowler-automation.git
|
||||
cd aws-sso-prowler-automation
|
||||
```
|
||||
|
||||
2. **Create the Automation Script**
|
||||
Create a new Bash script named `run_prowler_sso.sh` and make it executable.
|
||||
|
||||
```bash
|
||||
nano run_prowler_sso.sh
|
||||
chmod +x run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Add the Script Content**
|
||||
Paste the following content into `run_prowler_sso.sh`:
|
||||
|
||||
4. **Configure AWS SSO Profile**
|
||||
Ensure that your AWS CLI profile (`twodragon` in this case) is correctly configured for SSO.
|
||||
|
||||
```bash
|
||||
aws configure sso --profile twodragon
|
||||
```
|
||||
|
||||
**Example Configuration Prompts:**
|
||||
```
|
||||
SSO session name (Recommended): [twodragon]
|
||||
SSO start URL [None]: https://twodragon.awsapps.com/start
|
||||
SSO region [None]: ap-northeast-2
|
||||
SSO account ID [None]: 123456789012
|
||||
SSO role name [None]: ReadOnlyAccess
|
||||
CLI default client region [None]: ap-northeast-2
|
||||
CLI default output format [None]: json
|
||||
CLI profile name [twodragon]: twodragon
|
||||
```
|
||||
|
||||
## Script Overview
|
||||
|
||||
The `run_prowler_sso.sh` script performs the following actions:
|
||||
|
||||
1. **AWS SSO Login:**
|
||||
- Initiates AWS SSO login for the specified profile.
|
||||
- Opens the SSO authorization page in the default browser for user authentication.
|
||||
|
||||
2. **Extract Temporary Credentials:**
|
||||
- Locates the most recent SSO cache file containing the `accessToken`.
|
||||
- Uses `jq` to parse and extract the `accessToken` from the cache file.
|
||||
- Retrieves the `sso_role_name` and `sso_account_id` from the AWS CLI configuration.
|
||||
- Obtains temporary AWS credentials (`AccessKeyId`, `SecretAccessKey`, `SessionToken`) using the extracted `accessToken`.
|
||||
|
||||
3. **Set Environment Variables:**
|
||||
- Exports the extracted AWS credentials as environment variables to be used by the Docker container.
|
||||
|
||||
4. **Run Prowler:**
|
||||
- Executes the **Prowler** Docker container, passing the AWS credentials as environment variables for security assessments.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Make the Script Executable**
|
||||
Ensure the script has execute permissions.
|
||||
|
||||
```bash
|
||||
chmod +x run_prowler_sso.sh
|
||||
```
|
||||
|
||||
2. **Run the Script**
|
||||
Execute the script to start the AWS SSO login process and run Prowler.
|
||||
|
||||
```bash
|
||||
./run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Follow the Prompts**
|
||||
- A browser window will open prompting you to authenticate via AWS SSO.
|
||||
- Complete the authentication process in the browser.
|
||||
- Upon successful login, the script will extract temporary credentials and run Prowler.
|
||||
|
||||
4. **Review Prowler Output**
|
||||
- Prowler will analyze your AWS environment based on the specified checks and output the results directly in the terminal.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues during the script execution, follow these steps to diagnose and resolve them.
|
||||
|
||||
### 1. Verify AWS CLI Version
|
||||
|
||||
Ensure you are using AWS CLI version 2 or later.
|
||||
|
||||
```bash
|
||||
aws --version
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
aws-cli/2.11.10 Python/3.9.12 Darwin/20.3.0 exe/x86_64 prompt/off
|
||||
```
|
||||
|
||||
If you are not using version 2, [install or update AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).
|
||||
|
||||
### 2. Confirm AWS SSO Profile Configuration
|
||||
|
||||
Check that the `twodragon` profile is correctly configured.
|
||||
|
||||
```bash
|
||||
aws configure list-profiles
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
default
|
||||
twodragon
|
||||
```
|
||||
|
||||
Review the profile details:
|
||||
|
||||
```bash
|
||||
aws configure get sso_start_url --profile twodragon
|
||||
aws configure get sso_region --profile twodragon
|
||||
aws configure get sso_account_id --profile twodragon
|
||||
aws configure get sso_role_name --profile twodragon
|
||||
```
|
||||
|
||||
Ensure all fields return the correct values.
|
||||
|
||||
### 3. Check SSO Cache File
|
||||
|
||||
Ensure that the SSO cache file contains a valid `accessToken`.
|
||||
|
||||
```bash
|
||||
cat ~/.aws/sso/cache/*.json
|
||||
```
|
||||
|
||||
**Example Content:**
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJz93a...k4laUWw",
|
||||
"expiresAt": "2024-12-22T14:07:55Z",
|
||||
"clientId": "example-client-id",
|
||||
"clientSecret": "example-client-secret",
|
||||
"startUrl": "https://twodragon.awsapps.com/start#"
|
||||
}
|
||||
```
|
||||
|
||||
If `accessToken` is `null` or missing, retry the AWS SSO login:
|
||||
|
||||
```bash
|
||||
aws sso login --profile twodragon
|
||||
```
|
||||
|
||||
### 4. Validate `jq` Installation
|
||||
|
||||
Ensure that `jq` is installed and functioning correctly.
|
||||
|
||||
```bash
|
||||
jq --version
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
jq-1.6
|
||||
```
|
||||
|
||||
If `jq` is not installed, install it using the instructions in the [Prerequisites](#prerequisites) section.
|
||||
|
||||
### 5. Test Docker Environment Variables
|
||||
|
||||
Verify that the Docker container receives the AWS credentials correctly.
|
||||
|
||||
```bash
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
|
||||
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
|
||||
toniblyx/prowler /bin/bash -c 'echo $AWS_ACCESS_KEY_ID; echo $AWS_SECRET_ACCESS_KEY; echo $AWS_SESSION_TOKEN'
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
ASIA...
|
||||
wJalrFEMI/K7MDENG/bPxRfiCY...
|
||||
IQoJb3JpZ2luX2VjEHwaCXVz...
|
||||
```
|
||||
|
||||
Ensure that none of the environment variables are empty.
|
||||
|
||||
### 6. Review Script Output
|
||||
|
||||
Run the script with debugging enabled to get detailed output.
|
||||
|
||||
1. **Enable Debugging in Script**
|
||||
Add `set -x` for verbose output.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
# ... rest of the script ...
|
||||
```
|
||||
|
||||
2. **Run the Script**
|
||||
|
||||
```bash
|
||||
./run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Analyze Output**
|
||||
Look for any errors or unexpected values in the output to identify where the script is failing.
|
||||
|
||||
## Customization
|
||||
|
||||
You can modify the script to suit your specific needs, such as:
|
||||
|
||||
- **Changing the AWS Profile Name:**
|
||||
Update the `PROFILE` variable at the top of the script.
|
||||
|
||||
```bash
|
||||
PROFILE="your-profile-name"
|
||||
```
|
||||
|
||||
- **Adding Prowler Options:**
|
||||
Pass additional options to Prowler for customized checks or output formats.
|
||||
|
||||
```bash
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
|
||||
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
|
||||
toniblyx/prowler -c check123 -M json
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Handle Credentials Securely:**
|
||||
- Avoid sharing or exposing your AWS credentials.
|
||||
- Do not include sensitive information in logs or version control.
|
||||
|
||||
- **Script Permissions:**
|
||||
- Ensure the script file has appropriate permissions to prevent unauthorized access.
|
||||
|
||||
```bash
|
||||
chmod 700 run_prowler_sso.sh
|
||||
```
|
||||
|
||||
- **Environment Variables:**
|
||||
- Be cautious when exporting credentials as environment variables.
|
||||
- Consider using more secure methods for credential management if necessary.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
136
contrib/aws/aws-sso-docker/run_prowler_sso.sh
Executable file
136
contrib/aws/aws-sso-docker/run_prowler_sso.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Set the profile name
|
||||
PROFILE="twodragon"
|
||||
|
||||
# Set the Prowler output directory
|
||||
OUTPUT_DIR=~/prowler-output
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Set the port for the local web server
|
||||
WEB_SERVER_PORT=8000
|
||||
|
||||
# ----------------------------------------------
|
||||
# Functions
|
||||
# ----------------------------------------------
|
||||
|
||||
# Function to open the HTML report in the default browser
|
||||
open_report() {
|
||||
local report_path="$1"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
open "$report_path"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
xdg-open "$report_path"
|
||||
elif [[ "$OSTYPE" == "msys" ]]; then
|
||||
start "" "$report_path"
|
||||
else
|
||||
echo "Automatic method to open Prowler HTML report is not supported on this OS."
|
||||
echo "Please open the report manually at: $report_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start a simple HTTP server to host the Prowler reports
|
||||
start_web_server() {
|
||||
local directory="$1"
|
||||
local port="$2"
|
||||
|
||||
echo "Starting local web server to host Prowler reports at http://localhost:$port"
|
||||
echo "Press Ctrl+C to stop the web server."
|
||||
|
||||
# Change to the output directory
|
||||
cd "$directory"
|
||||
|
||||
# Start the HTTP server in the foreground
|
||||
# Python 3 is required
|
||||
python3 -m http.server "$port"
|
||||
}
|
||||
|
||||
# ----------------------------------------------
|
||||
# Main Script
|
||||
# ----------------------------------------------
|
||||
|
||||
# AWS SSO Login
|
||||
echo "Logging into AWS SSO..."
|
||||
aws sso login --profile "$PROFILE"
|
||||
|
||||
# Extract temporary credentials
|
||||
echo "Extracting temporary credentials..."
|
||||
|
||||
# Find the most recently modified SSO cache file
|
||||
CACHE_FILE=$(ls -t ~/.aws/sso/cache/*.json 2>/dev/null | head -n 1)
|
||||
echo "Cache File: $CACHE_FILE"
|
||||
|
||||
if [ -z "$CACHE_FILE" ]; then
|
||||
echo "SSO cache file not found. Please ensure AWS SSO login was successful."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract accessToken using jq
|
||||
ACCESS_TOKEN=$(jq -r '.accessToken' "$CACHE_FILE")
|
||||
echo "Access Token: $ACCESS_TOKEN"
|
||||
|
||||
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then
|
||||
echo "Unable to extract accessToken. Please check your SSO login and cache file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract role name and account ID from AWS CLI configuration
|
||||
ROLE_NAME=$(aws configure get sso_role_name --profile "$PROFILE")
|
||||
ACCOUNT_ID=$(aws configure get sso_account_id --profile "$PROFILE")
|
||||
echo "Role Name: $ROLE_NAME"
|
||||
echo "Account ID: $ACCOUNT_ID"
|
||||
|
||||
if [ -z "$ROLE_NAME" ] || [ -z "$ACCOUNT_ID" ]; then
|
||||
echo "Unable to extract sso_role_name or sso_account_id. Please check your profile configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Obtain temporary credentials using AWS SSO
|
||||
TEMP_CREDS=$(aws sso get-role-credentials \
|
||||
--role-name "$ROLE_NAME" \
|
||||
--account-id "$ACCOUNT_ID" \
|
||||
--access-token "$ACCESS_TOKEN" \
|
||||
--profile "$PROFILE")
|
||||
|
||||
echo "TEMP_CREDS: $TEMP_CREDS"
|
||||
|
||||
# Extract credentials from the JSON response
|
||||
AWS_ACCESS_KEY_ID=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.accessKeyId')
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.secretAccessKey')
|
||||
AWS_SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.sessionToken')
|
||||
|
||||
# Verify that all credentials were extracted successfully
|
||||
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$AWS_SESSION_TOKEN" ]; then
|
||||
echo "Unable to extract temporary credentials."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Export AWS credentials as environment variables
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
export AWS_SESSION_TOKEN
|
||||
|
||||
echo "AWS credentials have been set."
|
||||
|
||||
# Run Prowler in Docker container
|
||||
echo "Running Prowler Docker container..."
|
||||
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
|
||||
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
|
||||
-e AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \
|
||||
-v "$OUTPUT_DIR":/home/prowler/output \
|
||||
toniblyx/prowler -M html -M csv -M json-ocsf --output-directory /home/prowler/output --output-filename prowler-output
|
||||
|
||||
echo "Prowler has finished running. Reports are saved in $OUTPUT_DIR."
|
||||
|
||||
# Open the HTML report in the default browser
|
||||
REPORT_PATH="$OUTPUT_DIR/prowler-output.html"
|
||||
echo "Opening Prowler HTML report..."
|
||||
open_report "$REPORT_PATH" &
|
||||
|
||||
# Start the local web server to host the Prowler dashboard
|
||||
# This will run in the foreground. To run it in the background, append an ampersand (&) at the end of the command.
|
||||
start_web_server "$OUTPUT_DIR" "$WEB_SERVER_PORT"
|
||||
24
dashboard/compliance/cis_3_0_gcp.py
Normal file
24
dashboard/compliance/cis_3_0_gcp.py
Normal file
@@ -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"
|
||||
)
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
- 3000:3000
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine
|
||||
image: postgres:16.3-alpine3.20
|
||||
hostname: "postgres-db"
|
||||
volumes:
|
||||
- ./_data/postgres:/var/lib/postgresql/data
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
api:
|
||||
hostname: "prowler-api"
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
- "prod"
|
||||
|
||||
ui:
|
||||
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-latest}
|
||||
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-stable}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
- ${UI_PORT:-3000}:${UI_PORT:-3000}
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine
|
||||
image: postgres:16.3-alpine3.20
|
||||
hostname: "postgres-db"
|
||||
volumes:
|
||||
- ./_data/postgres:/var/lib/postgresql/data
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
- "worker"
|
||||
|
||||
worker-beat:
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
|
||||
@@ -279,6 +279,9 @@ Each Prowler check has metadata associated which is stored at the same level of
|
||||
"Severity": "critical",
|
||||
# ResourceType only for AWS, holds the type from here
|
||||
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html
|
||||
# In case of not existing, use CloudFormation type but removing the "::" and using capital letters only at the beginning of each word. Example: "AWS::EC2::Instance" -> "AwsEc2Instance"
|
||||
# CloudFormation type reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
|
||||
# If the resource type does not exist in the CloudFormation types, use "Other".
|
||||
"ResourceType": "Other",
|
||||
# Description holds the title of the check, for now is the same as CheckTitle
|
||||
"Description": "Ensure there are no EC2 AMIs set as Public.",
|
||||
|
||||
@@ -1,3 +1,336 @@
|
||||
# Create a new integration
|
||||
# Creating a New Integration
|
||||
|
||||
Coming soon ...
|
||||
## Introduction
|
||||
|
||||
Integrating Prowler with external tools enhances its functionality and seamlessly embeds it into your workflows. Prowler supports a wide range of integrations to streamline security assessments and reporting. Common integration targets include messaging platforms like Slack, project management tools like Jira, and cloud services such as AWS Security Hub.
|
||||
|
||||
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can integrate it with the desired product!
|
||||
* Identify the best approach for the specific platform you’re targeting.
|
||||
|
||||
## Steps to Create an Integration
|
||||
|
||||
### Identify the Integration Purpose
|
||||
|
||||
* Clearly define the objective of the integration. For example:
|
||||
* Sending Prowler findings to a platform for alerts, tracking, or further analysis.
|
||||
* Review existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder for inspiration and implementation examples.
|
||||
|
||||
### Develop the Integration
|
||||
|
||||
* Script Development:
|
||||
* Write a script to process Prowler’s output and interact with the target platform’s API.
|
||||
* For example, to send findings, parse Prowler’s results and use the platform’s API to create entries or notifications.
|
||||
* Configuration:
|
||||
* Ensure your script includes configurable options for environment-specific settings, such as API endpoints and authentication tokens.
|
||||
|
||||
### Fundamental Structure
|
||||
|
||||
* Integration Class:
|
||||
* Create a class that encapsulates attributes and methods for the integration.
|
||||
Here is an example with Jira integration:
|
||||
```python title="Jira Class"
|
||||
class Jira:
|
||||
"""
|
||||
Jira class to interact with the Jira API
|
||||
|
||||
[Note]
|
||||
This integration is limited to a single Jira Cloud, therefore all the issues will be created for same Jira Cloud ID. We will need to work on the ability of providing a Jira Cloud ID if the user is present in more than one.
|
||||
|
||||
Attributes:
|
||||
- _redirect_uri: The redirect URI
|
||||
- _client_id: The client ID
|
||||
- _client_secret: The client secret
|
||||
- _access_token: The access token
|
||||
- _refresh_token: The refresh token
|
||||
- _expiration_date: The authentication expiration
|
||||
- _cloud_id: The cloud ID
|
||||
- _scopes: The scopes needed to authenticate, read:jira-user read:jira-work write:jira-work
|
||||
- AUTH_URL: The URL to authenticate with Jira
|
||||
- PARAMS_TEMPLATE: The template for the parameters to authenticate with Jira
|
||||
- TOKEN_URL: The URL to get the access token from Jira
|
||||
- API_TOKEN_URL: The URL to get the accessible resources from Jira
|
||||
|
||||
Methods:
|
||||
- __init__: Initialize the Jira object
|
||||
- input_authorization_code: Input the authorization code
|
||||
- auth_code_url: Generate the URL to authorize the application
|
||||
- get_auth: Get the access token and refresh token
|
||||
- get_cloud_id: Get the cloud ID from Jira
|
||||
- get_access_token: Get the access token
|
||||
- refresh_access_token: Refresh the access token from Jira
|
||||
- test_connection: Test the connection to Jira and return a Connection object
|
||||
- get_projects: Get the projects from Jira
|
||||
- get_available_issue_types: Get the available issue types for a project
|
||||
- send_findings: Send the findings to Jira and create an issue
|
||||
|
||||
Raises:
|
||||
- JiraGetAuthResponseError: Failed to get the access token and refresh token
|
||||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||||
- JiraAuthenticationError: Failed to authenticate
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||||
- JiraGetAccessTokenError: Failed to get the access token
|
||||
- JiraNoProjectsError: No projects found in Jira
|
||||
- JiraGetProjectsError: Failed to get projects from Jira
|
||||
- JiraGetProjectsResponseError: Failed to get projects from Jira, response code did not match 200
|
||||
- JiraInvalidIssueTypeError: The issue type is invalid
|
||||
- JiraGetAvailableIssueTypesError: Failed to get available issue types from Jira
|
||||
- JiraGetAvailableIssueTypesResponseError: Failed to get available issue types from Jira, response code did not match 200
|
||||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||||
- JiraTestConnectionError: Failed to test the connection
|
||||
|
||||
Usage:
|
||||
jira = Jira(
|
||||
redirect_uri="http://localhost:8080",
|
||||
client_id="client_id",
|
||||
client_secret="client_secret
|
||||
)
|
||||
jira.send_findings(findings=findings, project_key="KEY")
|
||||
"""
|
||||
|
||||
_redirect_uri: str = None
|
||||
_client_id: str = None
|
||||
_client_secret: str = None
|
||||
_access_token: str = None
|
||||
_refresh_token: str = None
|
||||
_expiration_date: int = None
|
||||
_cloud_id: str = None
|
||||
_scopes: list[str] = None
|
||||
AUTH_URL = "https://auth.atlassian.com/authorize"
|
||||
PARAMS_TEMPLATE = {
|
||||
"audience": "api.atlassian.com",
|
||||
"client_id": None,
|
||||
"scope": None,
|
||||
"redirect_uri": None,
|
||||
"state": None,
|
||||
"response_type": "code",
|
||||
"prompt": "consent",
|
||||
}
|
||||
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||||
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redirect_uri: str = None,
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
):
|
||||
self._redirect_uri = redirect_uri
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scopes = ["read:jira-user", "read:jira-work", "write:jira-work"]
|
||||
auth_url = self.auth_code_url()
|
||||
authorization_code = self.input_authorization_code(auth_url)
|
||||
self.get_auth(authorization_code)
|
||||
|
||||
# More properties and methods
|
||||
```
|
||||
* Test Connection Method:
|
||||
* Implement a method to validate credentials or tokens, ensuring the connection to the target platform is successful.
|
||||
The following is the code for the `test_connection` method for the `Jira` class:
|
||||
```python title="Test connection"
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
redirect_uri: str = None,
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
raise_on_exception: bool = True,
|
||||
) -> Connection:
|
||||
"""Test the connection to Jira
|
||||
|
||||
Args:
|
||||
- redirect_uri: The redirect URI
|
||||
- client_id: The client ID
|
||||
- client_secret: The client secret
|
||||
- raise_on_exception: Whether to raise an exception or not
|
||||
|
||||
Returns:
|
||||
- Connection: The connection object
|
||||
|
||||
Raises:
|
||||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||||
- JiraAuthenticationError: Failed to authenticate
|
||||
- JiraTestConnectionError: Failed to test the connection
|
||||
"""
|
||||
try:
|
||||
jira = Jira(
|
||||
redirect_uri=redirect_uri,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
access_token = jira.get_access_token()
|
||||
|
||||
if not access_token:
|
||||
return ValueError("Failed to get access token")
|
||||
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
response = requests.get(
|
||||
f"https://api.atlassian.com/ex/jira/{jira.cloud_id}/rest/api/3/myself",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return Connection(is_connected=True)
|
||||
else:
|
||||
return Connection(is_connected=False, error=response.json())
|
||||
except JiraGetCloudIDNoResourcesError as no_resources_error:
|
||||
logger.error(
|
||||
f"{no_resources_error.__class__.__name__}[{no_resources_error.__traceback__.tb_lineno}]: {no_resources_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise no_resources_error
|
||||
return Connection(error=no_resources_error)
|
||||
except JiraGetCloudIDResponseError as response_error:
|
||||
logger.error(
|
||||
f"{response_error.__class__.__name__}[{response_error.__traceback__.tb_lineno}]: {response_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise response_error
|
||||
return Connection(error=response_error)
|
||||
except JiraGetCloudIDError as cloud_id_error:
|
||||
logger.error(
|
||||
f"{cloud_id_error.__class__.__name__}[{cloud_id_error.__traceback__.tb_lineno}]: {cloud_id_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise cloud_id_error
|
||||
return Connection(error=cloud_id_error)
|
||||
except JiraAuthenticationError as auth_error:
|
||||
logger.error(
|
||||
f"{auth_error.__class__.__name__}[{auth_error.__traceback__.tb_lineno}]: {auth_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise auth_error
|
||||
return Connection(error=auth_error)
|
||||
except Exception as error:
|
||||
logger.error(f"Failed to test connection: {error}")
|
||||
if raise_on_exception:
|
||||
raise JiraTestConnectionError(
|
||||
message="Failed to test connection on the Jira integration",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
return Connection(is_connected=False, error=error)
|
||||
```
|
||||
* Send Findings Method:
|
||||
* Add a method to send Prowler findings to the target platform, adhering to its API specifications.
|
||||
The following is the code for the `send_findings` method for the `Jira` class:
|
||||
```python title="Send findings method"
|
||||
def send_findings(
|
||||
self,
|
||||
findings: list[Finding] = None,
|
||||
project_key: str = None,
|
||||
issue_type: str = None,
|
||||
):
|
||||
"""
|
||||
Send the findings to Jira
|
||||
|
||||
Args:
|
||||
- findings: The findings to send
|
||||
- project_key: The project key
|
||||
- issue_type: The issue type
|
||||
|
||||
Raises:
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||||
"""
|
||||
try:
|
||||
access_token = self.get_access_token()
|
||||
|
||||
if not access_token:
|
||||
raise JiraNoTokenError(
|
||||
message="No token was found",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
projects = self.get_projects()
|
||||
|
||||
if project_key not in projects:
|
||||
logger.error("The project key is invalid")
|
||||
raise JiraInvalidProjectKeyError(
|
||||
message="The project key is invalid",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
available_issue_types = self.get_available_issue_types(project_key)
|
||||
|
||||
if issue_type not in available_issue_types:
|
||||
logger.error("The issue type is invalid")
|
||||
raise JiraInvalidIssueTypeError(
|
||||
message="The issue type is invalid", file=os.path.basename(__file__)
|
||||
)
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
for finding in findings:
|
||||
status_color = self.get_color_from_status(finding.status.value)
|
||||
adf_description = self.get_adf_description(
|
||||
check_id=finding.metadata.CheckID,
|
||||
check_title=finding.metadata.CheckTitle,
|
||||
severity=finding.metadata.Severity.value.upper(),
|
||||
status=finding.status.value,
|
||||
status_color=status_color,
|
||||
status_extended=finding.status_extended,
|
||||
provider=finding.metadata.Provider,
|
||||
region=finding.region,
|
||||
resource_uid=finding.resource_uid,
|
||||
resource_name=finding.resource_name,
|
||||
risk=finding.metadata.Risk,
|
||||
recommendation_text=finding.metadata.Remediation.Recommendation.Text,
|
||||
recommendation_url=finding.metadata.Remediation.Recommendation.Url,
|
||||
)
|
||||
payload = {
|
||||
"fields": {
|
||||
"project": {"key": project_key},
|
||||
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
|
||||
"description": adf_description,
|
||||
"issuetype": {"name": issue_type},
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
response_error = f"Failed to send finding: {response.status_code} - {response.json()}"
|
||||
logger.warning(response_error)
|
||||
raise JiraSendFindingsResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
else:
|
||||
logger.info(f"Finding sent successfully: {response.json()}")
|
||||
except JiraRefreshTokenError as refresh_error:
|
||||
raise refresh_error
|
||||
except JiraRefreshTokenResponseError as response_error:
|
||||
raise response_error
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send findings: {e}")
|
||||
raise JiraCreateIssueError(
|
||||
message="Failed to create an issue in Jira",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
* Test the integration in a controlled environment to confirm it behaves as expected.
|
||||
* Verify that Prowler’s findings are accurately transmitted and correctly processed by the target platform.
|
||||
* Simulate edge cases to ensure robust error handling.
|
||||
|
||||
### Documentation
|
||||
|
||||
* Provide clear, detailed documentation for your integration:
|
||||
* Setup instructions, including any required dependencies.
|
||||
* Configuration details, such as environment variables or authentication steps.
|
||||
* Example use cases and troubleshooting tips.
|
||||
* Good documentation ensures maintainability and simplifies onboarding for team members.
|
||||
|
||||
@@ -1,3 +1,166 @@
|
||||
# Create a custom output format
|
||||
# Create a Custom Output Format
|
||||
|
||||
Coming soon ...
|
||||
## Introduction
|
||||
|
||||
Prowler can generate outputs in multiple formats, allowing users to customize the way findings are presented. This is particularly useful when integrating Prowler with third-party tools, creating specialized reports, or simply tailoring the data to meet specific requirements. A custom output format gives you the flexibility to extract and display only the most relevant information in the way you need it.
|
||||
|
||||
* Prowler organizes its outputs in the `/lib/outputs` directory. Each format (e.g., JSON, CSV, HTML) is implemented as a Python class.
|
||||
* Outputs are generated based on findings collected during a scan. Each finding is represented as a structured dictionary containing details like resource IDs, severities, descriptions, and more.
|
||||
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can create it with the desired output!
|
||||
* Identify the best approach for the specific output you’re targeting.
|
||||
|
||||
## Steps to Create a Custom Output Format
|
||||
|
||||
### Schema
|
||||
|
||||
* Output Class:
|
||||
* The class must inherit from `Output`. Review the [Output Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/output.py).
|
||||
* Create a class that encapsulates attributes and methods for the output.
|
||||
The following is the code for the `CSV` class:
|
||||
```python title="CSV Class"
|
||||
class CSV(Output):
|
||||
def transform(self, findings: List[Finding]) -> None:
|
||||
"""Transforms the findings into the CSV format.
|
||||
|
||||
Args:
|
||||
findings (list[Finding]): a list of Finding objects
|
||||
|
||||
"""
|
||||
...
|
||||
```
|
||||
* Transform Method:
|
||||
* This method will transform the findings provided by Prowler to a specific format.
|
||||
The following is the code for the `transform` method for the `CSV` class:
|
||||
```python title="Transform"
|
||||
def transform(self, findings: List[Finding]) -> None:
|
||||
"""Transforms the findings into the CSV format.
|
||||
|
||||
Args:
|
||||
findings (list[Finding]): a list of Finding objects
|
||||
|
||||
"""
|
||||
try:
|
||||
for finding in findings:
|
||||
finding_dict = {}
|
||||
finding_dict["AUTH_METHOD"] = finding.auth_method
|
||||
finding_dict["TIMESTAMP"] = finding.timestamp
|
||||
finding_dict["ACCOUNT_UID"] = finding.account_uid
|
||||
finding_dict["ACCOUNT_NAME"] = finding.account_name
|
||||
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
|
||||
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
|
||||
finding.account_organization_uid
|
||||
)
|
||||
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
|
||||
finding.account_organization_name
|
||||
)
|
||||
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
|
||||
finding.account_tags, separator=":"
|
||||
)
|
||||
finding_dict["FINDING_UID"] = finding.uid
|
||||
finding_dict["PROVIDER"] = finding.metadata.Provider
|
||||
finding_dict["CHECK_ID"] = finding.metadata.CheckID
|
||||
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
|
||||
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
|
||||
finding_dict["STATUS"] = finding.status.value
|
||||
finding_dict["STATUS_EXTENDED"] = finding.status_extended
|
||||
finding_dict["MUTED"] = finding.muted
|
||||
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
|
||||
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
|
||||
finding_dict["SEVERITY"] = finding.metadata.Severity.value
|
||||
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
|
||||
finding_dict["RESOURCE_UID"] = finding.resource_uid
|
||||
finding_dict["RESOURCE_NAME"] = finding.resource_name
|
||||
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
|
||||
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
|
||||
finding_dict["PARTITION"] = finding.partition
|
||||
finding_dict["REGION"] = finding.region
|
||||
finding_dict["DESCRIPTION"] = finding.metadata.Description
|
||||
finding_dict["RISK"] = finding.metadata.Risk
|
||||
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
|
||||
finding.metadata.Remediation.Recommendation.Text
|
||||
)
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
|
||||
finding.metadata.Remediation.Recommendation.Url
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
|
||||
finding.metadata.Remediation.Code.NativeIaC
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
|
||||
finding.metadata.Remediation.Code.Terraform
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_CLI"] = (
|
||||
finding.metadata.Remediation.Code.CLI
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_OTHER"] = (
|
||||
finding.metadata.Remediation.Code.Other
|
||||
)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(
|
||||
finding.compliance, separator=": "
|
||||
)
|
||||
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
|
||||
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
|
||||
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
|
||||
finding_dict["NOTES"] = finding.metadata.Notes
|
||||
finding_dict["PROWLER_VERSION"] = finding.prowler_version
|
||||
self._data.append(finding_dict)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
```
|
||||
* Batch Write Data To File Method:
|
||||
* This method will write the modeled object to a file.
|
||||
The following is the code for the `batch_write_data_to_file` method for the `CSV` class:
|
||||
```python title="Batch Write Data To File"
|
||||
def batch_write_data_to_file(self) -> None:
|
||||
"""Writes the findings to a file using the CSV format using the `Output._file_descriptor`."""
|
||||
try:
|
||||
if (
|
||||
getattr(self, "_file_descriptor", None)
|
||||
and not self._file_descriptor.closed
|
||||
and self._data
|
||||
):
|
||||
csv_writer = DictWriter(
|
||||
self._file_descriptor,
|
||||
fieldnames=self._data[0].keys(),
|
||||
delimiter=";",
|
||||
)
|
||||
csv_writer.writeheader()
|
||||
for finding in self._data:
|
||||
csv_writer.writerow(finding)
|
||||
self._file_descriptor.close()
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
```
|
||||
|
||||
### Integration With The Current Code
|
||||
|
||||
Once that the desired output format is created it has to be integrated with Prowler. Take a look at the the usage from the current supported output in order to add the new one.
|
||||
Here is an example of the CSV output creation inside [prowler code](https://github.com/prowler-cloud/prowler/blob/master/prowler/__main__.py):
|
||||
```python title="CSV creation"
|
||||
if mode == "csv":
|
||||
csv_output = CSV(
|
||||
findings=finding_outputs,
|
||||
create_file_descriptor=True,
|
||||
file_path=f"{filename}{csv_file_suffix}",
|
||||
)
|
||||
generated_outputs["regular"].append(csv_output)
|
||||
# Write CSV Finding Object to file
|
||||
csv_output.batch_write_data_to_file()
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
* Verify that Prowler’s findings are accurately writed in the desired output format.
|
||||
* Simulate edge cases to ensure robust error handling.
|
||||
|
||||
### Documentation
|
||||
|
||||
* Provide clear, detailed documentation for your output:
|
||||
* Setup instructions, including any required dependencies.
|
||||
* Configuration details.
|
||||
* Example use cases and troubleshooting tips.
|
||||
* Good documentation ensures maintainability and simplifies onboarding for new users.
|
||||
|
||||
@@ -51,14 +51,14 @@ For the AWS provider we have ways to test a Prowler check based on the following
|
||||
We use and contribute to the [Moto](https://github.com/getmoto/moto) library which allows us to easily mock out tests based on AWS infrastructure. **It's awesome!**
|
||||
|
||||
- AWS API calls covered by [Moto](https://github.com/getmoto/moto):
|
||||
- Service tests with `@mock_<service>`
|
||||
- Checks tests with `@mock_<service>`
|
||||
- Service tests with `@mock_aws`
|
||||
- Checks tests with `@mock_aws`
|
||||
- AWS API calls not covered by Moto:
|
||||
- Service test with `mock_make_api_call`
|
||||
- Checks tests with [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock)
|
||||
- AWS API calls partially covered by Moto:
|
||||
- Service test with `@mock_<service>` and `mock_make_api_call`
|
||||
- Checks tests with `@mock_<service>` and `mock_make_api_call`
|
||||
- Service test with `@mock_aws` and `mock_make_api_call`
|
||||
- Checks tests with `@mock_aws` and `mock_make_api_call`
|
||||
|
||||
In the following section we are going to explain all of the above scenarios with examples. The main difference between those scenarios comes from if the [Moto](https://github.com/getmoto/moto) library covers the AWS API calls made by the service. You can check the covered API calls [here](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
|
||||
|
||||
@@ -70,7 +70,7 @@ This section is going to be divided based on the API coverage of the [Moto](http
|
||||
|
||||
#### API calls covered
|
||||
|
||||
If the [Moto](https://github.com/getmoto/moto) library covers the API calls we want to test, we can use the `@mock_<service>` decorator. This will mocked out all the API calls made to AWS keeping the state within the code decorated, in this case the test function.
|
||||
If the [Moto](https://github.com/getmoto/moto) library covers the API calls we want to test, we can use the `@mock_aws` decorator. This will mocked out all the API calls made to AWS keeping the state within the code decorated, in this case the test function.
|
||||
|
||||
```python
|
||||
# We need to import the unittest.mock to allow us to patch some objects
|
||||
@@ -80,8 +80,8 @@ from unittest import mock
|
||||
# Boto3 client and session to call the AWS APIs
|
||||
from boto3 import client, session
|
||||
|
||||
# Moto decorator for the IAM service we want to mock
|
||||
from moto import mock_iam
|
||||
# Moto decorator
|
||||
from moto import mock_aws
|
||||
|
||||
# Constants used
|
||||
AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
@@ -91,10 +91,8 @@ AWS_REGION = "us-east-1"
|
||||
# We always name the test classes like Test_<check_name>
|
||||
class Test_iam_password_policy_uppercase:
|
||||
|
||||
# We include the Moto decorator for the service we want to use
|
||||
# You can include more than one if two or more services are
|
||||
# involved in test
|
||||
@mock_iam
|
||||
# We include the Moto decorator
|
||||
@mock_aws
|
||||
# We name the tests with test_<service>_<check_name>_<test_action>
|
||||
def test_iam_password_policy_no_uppercase_flag(self):
|
||||
# First, we have to create an IAM client
|
||||
@@ -238,7 +236,7 @@ To do so, you need to mock the `botocore.client.BaseClient._make_api_call` funct
|
||||
import boto3
|
||||
import botocore
|
||||
from unittest.mock import patch
|
||||
from moto import mock_iam
|
||||
from moto import mock_aws
|
||||
|
||||
# Original botocore _make_api_call function
|
||||
orig = botocore.client.BaseClient._make_api_call
|
||||
|
||||
@@ -38,16 +38,19 @@ If your IAM entity enforces MFA you can use `--mfa` and Prowler will ask you to
|
||||
|
||||
## Azure
|
||||
|
||||
Prowler for Azure supports the following authentication types:
|
||||
Prowler for Azure supports the following authentication types. To use each one you need to pass the proper flag to the execution:
|
||||
|
||||
- [Service principal application](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) by environment variables (recommended)
|
||||
- Current az cli credentials stored
|
||||
- Interactive browser authentication
|
||||
- [Managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication
|
||||
- [Service principal application](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (recommended).
|
||||
- Current az cli credentials stored.
|
||||
- Interactive browser authentication.
|
||||
- [Managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication.
|
||||
|
||||
### Service Principal authentication
|
||||
???+ warning
|
||||
For Prowler App only the Service Principal authentication method is supported.
|
||||
|
||||
To allow Prowler assume the service principal identity to start the scan it is needed to configure the following environment variables:
|
||||
### Service Principal Application authentication
|
||||
|
||||
To allow Prowler assume the service principal application identity to start the scan it is needed to configure the following environment variables:
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
@@ -56,29 +59,31 @@ export AZURE_CLIENT_SECRET="XXXXXXX"
|
||||
```
|
||||
|
||||
If you try to execute Prowler with the `--sp-env-auth` flag and those variables are empty or not exported, the execution is going to fail.
|
||||
Follow the instructions in the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md) section to create a service principal.
|
||||
Follow the instructions in the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md#how-to-create-prowler-service-principal) section to create a service principal.
|
||||
|
||||
### AZ CLI / Browser / Managed Identity authentication
|
||||
|
||||
The other three cases does not need additional configuration, `--az-cli-auth` and `--managed-identity-auth` are automated options. To use `--browser-auth` the user needs to authenticate against Azure using the default browser to start the scan, also `tenant-id` is required.
|
||||
|
||||
### Permissions
|
||||
### Needed permissions
|
||||
|
||||
To use each one you need to pass the proper flag to the execution. Prowler for Azure handles two types of permission scopes, which are:
|
||||
Prowler for Azure needs two types of permission scopes to be set:
|
||||
|
||||
- **Microsoft Entra ID permissions**: Used to retrieve metadata from the identity assumed by Prowler and specific Entra checks (not mandatory to have access to execute the tool). The permissions required by the tool are the following:
|
||||
- **Microsoft Entra ID permissions**: used to retrieve metadata from the identity assumed by Prowler and specific Entra checks (not mandatory to have access to execute the tool). The permissions required by the tool are the following:
|
||||
- `Directory.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All`
|
||||
- **Subscription scope permissions**: Required to launch the checks against your resources, mandatory to launch the tool. It is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
|
||||
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
|
||||
- **Subscription scope permissions**: required to launch the checks against your resources, mandatory to launch the tool. It is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
|
||||
- `Reader`
|
||||
- `ProwlerRole` (custom role defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json))
|
||||
- `ProwlerRole` (custom role with minimal permissions defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json))
|
||||
???+ note
|
||||
Please, notice that the field `assignableScopes` in the JSON custom role file must be changed to be the subscription or management group where the role is going to be assigned. The valid formats for the field are `/subscriptions/<subscription-id>` or `/providers/Microsoft.Management/managementGroups/<management-group-id>`.
|
||||
|
||||
To assign the permissions, follow the instructions in the [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions) section and the [Azure subscriptions permissions](../tutorials/azure/subscriptions.md#assigning-proper-permissions) section, respectively.
|
||||
|
||||
#### Checks that require ProwlerRole
|
||||
|
||||
The following checks require the `ProwlerRole` custom role to be executed, if you want to run them, make sure you have assigned the role to the identity that is going to be assumed by Prowler:
|
||||
The following checks require the `ProwlerRole` permissions to be executed, if you want to run them, make sure you have assigned the role to the identity that is going to be assumed by Prowler:
|
||||
|
||||
- `app_function_access_keys_configured`
|
||||
- `app_function_ftps_deployment_disabled`
|
||||
|
||||
BIN
docs/img/compliance.png
Normal file
BIN
docs/img/compliance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
@@ -1,4 +1,4 @@
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler Cloud</a>.
|
||||
|
||||
## Prowler App
|
||||
|
||||
@@ -29,7 +29,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
Prowler App can be installed in different ways, depending on your environment:
|
||||
|
||||
> See how to use Prowler App in the [Prowler App](tutorials/prowler-app.md) section.
|
||||
> See how to use Prowler App in the [Prowler App Tutorial](tutorials/prowler-app.md) section.
|
||||
|
||||
=== "Docker Compose"
|
||||
|
||||
@@ -45,6 +45,8 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ note
|
||||
@@ -65,6 +67,9 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
???+ warning
|
||||
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
|
||||
|
||||
_Commands to run the API_:
|
||||
|
||||
``` bash
|
||||
@@ -95,6 +100,19 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
_Commands to run the API Scheduler_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
poetry shell \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
```
|
||||
|
||||
_Commands to run the UI_:
|
||||
|
||||
``` bash
|
||||
@@ -107,9 +125,6 @@ Prowler App can be installed in different ways, depending on your environment:
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ warning
|
||||
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
|
||||
|
||||
???+ warning
|
||||
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
|
||||
|
||||
@@ -172,6 +187,8 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
@@ -302,10 +319,14 @@ The available versions of Prowler CLI are the following:
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
- Prowler CLI:
|
||||
|
||||
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
|
||||
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
|
||||
|
||||
- Prowler App:
|
||||
|
||||
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
|
||||
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
|
||||
|
||||
@@ -373,8 +394,8 @@ After successfully adding and testing your credentials, Prowler will start scann
|
||||
#### **View Results**
|
||||
While the scan is running, start exploring the findings in these sections:
|
||||
|
||||
- **Overview**: High-level summary of the scans. <img src="../../img/overview.png" alt="Overview" width="700"/>
|
||||
- **Compliance**: Insights into compliance status. <img src="../../img/compliance.png" alt="Compliance" width="700"/>
|
||||
- **Overview**: High-level summary of the scans. <img src="img/overview.png" alt="Overview" width="700"/>
|
||||
- **Compliance**: Insights into compliance status. <img src="img/compliance.png" alt="Compliance" width="700"/>
|
||||
|
||||
> See more details about the Prowler App usage in the [Prowler App](tutorials/prowler-app.md) section.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (F
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler SaaS service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com).
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler Cloud service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com).
|
||||
|
||||
The information you share with the Prowler team as part of this process is kept confidential within Prowler. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# How to create Prowler Service Principal
|
||||
# How to create Prowler Service Principal Application
|
||||
|
||||
To allow Prowler assume an identity to start the scan with the required privileges is necesary to create a Service Principal. To create one follow the next steps:
|
||||
To allow Prowler assume an identity to start the scan with the required privileges is necesary to create a Service Principal. This Service Principal is going to be used to authenticate against Azure and retrieve the metadata needed to perform the checks.
|
||||
|
||||
To create a Service Principal Application you can use the Azure Portal or the Azure CLI.
|
||||
|
||||
## From Azure Portal
|
||||
|
||||
1. Access to Microsoft Entra ID
|
||||
2. In the left menu bar, go to "App registrations"
|
||||
@@ -13,9 +17,39 @@ To allow Prowler assume an identity to start the scan with the required privileg
|
||||
|
||||

|
||||
|
||||
## Assigning the proper permissions
|
||||
## From Azure CLI
|
||||
|
||||
To allow Prowler to retrieve metadata from the identity assumed and specific Entra checks, it is needed to assign the following permissions:
|
||||
To create a Service Principal using the Azure CLI, follow the next steps:
|
||||
|
||||
1. Open a terminal and execute the following command to create a new Service Principal application:
|
||||
```console
|
||||
az ad sp create-for-rbac --name "ProwlerApp"
|
||||
```
|
||||
2. The output of the command is going to be similar to the following:
|
||||
```json
|
||||
{
|
||||
"appId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"displayName": "ProwlerApp",
|
||||
"password": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"tenant": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
}
|
||||
```
|
||||
3. Save the values of `appId`, `password` and `tenant` to be used as credentials in Prowler.
|
||||
|
||||
# Assigning the proper permissions
|
||||
|
||||
To allow Prowler to retrieve metadata from the identity assumed and run specific Entra checks, it is needed to assign the following permissions:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
|
||||
|
||||
To assign the permissions you can make it from the Azure Portal or using the Azure CLI.
|
||||
|
||||
???+ note
|
||||
Once you have created and assigned the proper Entra permissions to the application, you can go to this [tutorial](../azure/subscriptions.md) to add the subscription permissions to the application and start scanning your resources.
|
||||
|
||||
## From Azure Portal
|
||||
|
||||
1. Access to Microsoft Entra ID
|
||||
2. In the left menu bar, go to "App registrations"
|
||||
@@ -28,7 +62,18 @@ To allow Prowler to retrieve metadata from the identity assumed and specific Ent
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All`
|
||||
8. Click on "Add permissions" to apply the new permissions.
|
||||
9. Finally, click on "Grant admin consent for [your tenant]" to apply the permissions.
|
||||
9. Finally, an admin should click on "Grant admin consent for [your tenant]" to apply the permissions.
|
||||
|
||||
|
||||

|
||||
|
||||
## From Azure CLI
|
||||
|
||||
1. Open a terminal and execute the following command to assign the permissions to the Service Principal:
|
||||
```console
|
||||
az ad app permission add --id {appId} --api 00000003-0000-0000-c000-000000000000 --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role 246dd0d5-5bd0-4def-940b-0421030a5b68=Role 38d9df27-64da-44fd-b7c5-a6fbac20248f=Role
|
||||
```
|
||||
2. The admin consent is needed to apply the permissions, an admin should execute the following command:
|
||||
```console
|
||||
az ad app permission admin-consent --id {appId}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Azure subscriptions scope
|
||||
|
||||
By default, Prowler is multisubscription, which means that is going to scan all the subscriptions is able to list. If you only assign permissions to one subscription, it is going to scan a single one.
|
||||
The main target for performing the scans in Azure is the subscription scope. Prowler needs to have the proper permissions to access the subscription and retrieve the metadata needed to perform the checks.
|
||||
|
||||
By default, Prowler is multi-subscription, which means that is going to scan all the subscriptions is able to list. If you only assign permissions to one subscription, it is going to scan a single one.
|
||||
Prowler also has the ability to limit the subscriptions to scan to a set passed as input argument, to do so:
|
||||
|
||||
```console
|
||||
@@ -9,35 +11,124 @@ prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription
|
||||
|
||||
Where you can pass from 1 up to N subscriptions to be scanned.
|
||||
|
||||
## Assigning proper permissions
|
||||
???+ warning
|
||||
The multi-subscription feature is only available for the CLI, in the case of Prowler App is only possible to scan one subscription per scan.
|
||||
|
||||
Regarding the subscription scope, Prowler by default scans all subscriptions that it is able to list, so it is necessary to add the `Reader` RBAC built-in roles per subscription or management group (recommended for multiple subscriptions, see it in the [next section](#recommendation-for-multiple-subscriptions)) to the entity that will be adopted by the tool:
|
||||
## Assign the appropriate permissions to the identity that is going to be assumed by Prowler
|
||||
|
||||
To assign this roles, follow the instructions:
|
||||
|
||||
1. Access your subscription, then select your subscription.
|
||||
2. Select "Access control (IAM)".
|
||||
3. In the overview, select "Roles".
|
||||
4. Click on "+ Add" and select "Add role assignment".
|
||||
5. In the search bar, type `Reader`, select it and click on "Next".
|
||||
6. In the Members tab, click on "+ Select members" and add the members you want to assign this role.
|
||||
7. Click on "Review + assign" to apply the new role.
|
||||
Regarding the subscription scope, Prowler, by default, scans all subscriptions it can access. Therefore, it is necessary to add a `Reader` role assignment for each subscription you want to audit. To make it easier and less repetitive to assign roles in environments with multiple subscriptions check the [following section](#recommendation-for-multiple-subscriptions).
|
||||
|
||||
### From Azure Portal
|
||||
|
||||
1. Access to the subscription you want to scan with Prowler.
|
||||
2. Select "Access control (IAM)" in the left menu.
|
||||
3. Click on "+ Add" and select "Add role assignment".
|
||||
4. In the search bar, type `Reader`, select it and click on "Next".
|
||||
5. In the Members tab, click on "+ Select members" and add the members you want to assign this role.
|
||||
6. Click on "Review + assign" to apply the new role.
|
||||
|
||||

|
||||
|
||||
Moreover, some additional read-only permissions are needed for some checks, for this kind of checks that are not covered by built-in roles we use a custom role. This role is defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once the cusotm role is created, repeat the steps mentioned above to assign the new `ProwlerRole` to an identity.
|
||||
### From Azure CLI
|
||||
|
||||
1. Open a terminal and execute the following command to assign the `Reader` role to the identity that is going to be assumed by Prowler:
|
||||
```console
|
||||
az role assignment create --role "Reader" --assignee <user, group, or service principal> --scope /subscriptions/<subscription-id>
|
||||
```
|
||||
2. If the command is executed successfully, the output is going to be similar to the following:
|
||||
```json
|
||||
{
|
||||
"condition": null,
|
||||
"conditionVersion": null,
|
||||
"createdBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"createdOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00",
|
||||
"delegatedManagedIdentityResourceId": null,
|
||||
"description": null,
|
||||
"id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleAssignments/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"name": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"principalId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"principalName": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"principalType": "ServicePrincipal",
|
||||
"roleDefinitionId": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleDefinitions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"roleDefinitionName": "Reader",
|
||||
"scope": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"type": "Microsoft.Authorization/roleAssignments",
|
||||
"updatedBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"updatedOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Prowler Custom Role
|
||||
|
||||
Moreover, some additional read-only permissions not included in the built-in reader role are needed for some checks, for this kind of checks we use a custom role. This role is defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once the custom role is created you can assign it in the same way as the `Reader` role.
|
||||
|
||||
The checks that needs the `ProwlerRole` can be consulted in the [requirements section](../../getting-started/requirements.md#checks-that-require-prowlerrole).
|
||||
|
||||
#### Create ProwlerRole from Azure Portal
|
||||
|
||||
1. Download the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json) file and modify the `assignableScopes` field to be the subscription ID where the role assignment is going to be made, it should be shomething like `/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`.
|
||||
2. Access your subscription.
|
||||
3. Select "Access control (IAM)".
|
||||
4. Click on "+ Add" and select "Add custom role".
|
||||
5. In the "Baseline permissions" select "Start from JSON" and upload the file downloaded and modified in the step 1.
|
||||
7. Click on "Review + create" to create the new role.
|
||||
|
||||
#### Create ProwlerRole from Azure CLI
|
||||
|
||||
1. Open a terminal and execute the following command to create a new custom role:
|
||||
```console
|
||||
az role definition create --role-definition '{ 640ms lun 16 dic 17:04:17 2024
|
||||
"Name": "ProwlerRole",
|
||||
"IsCustom": true,
|
||||
"Description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
|
||||
"AssignableScopes": [
|
||||
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" // USE YOUR SUBSCRIPTION ID
|
||||
],
|
||||
"Actions": [
|
||||
"Microsoft.Web/sites/host/listkeys/action",
|
||||
"Microsoft.Web/sites/config/list/Action"
|
||||
]
|
||||
}'
|
||||
```
|
||||
3. If the command is executed successfully, the output is going to be similar to the following:
|
||||
```json
|
||||
{
|
||||
"assignableScopes": [
|
||||
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
],
|
||||
"createdBy": null,
|
||||
"createdOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00",
|
||||
"description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
|
||||
"id": "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/providers/Microsoft.Authorization/roleDefinitions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"name": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"permissions": [
|
||||
{
|
||||
"actions": [
|
||||
"Microsoft.Web/sites/host/listkeys/action",
|
||||
"Microsoft.Web/sites/config/list/Action"
|
||||
],
|
||||
"condition": null,
|
||||
"conditionVersion": null,
|
||||
"dataActions": [],
|
||||
"notActions": [],
|
||||
"notDataActions": []
|
||||
}
|
||||
],
|
||||
"roleName": "ProwlerRole",
|
||||
"roleType": "CustomRole",
|
||||
"type": "Microsoft.Authorization/roleDefinitions",
|
||||
"updatedBy": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||
"updatedOn": "YYYY-MM-DDTHH:MM:SS.SSSSSS+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Recommendation for multiple subscriptions
|
||||
|
||||
While scanning multiple subscriptions could be tedious to create and assign roles for each one. For this reason in Prowler we recommend the usage of *[management groups](https://learn.microsoft.com/en-us/azure/governance/management-groups/overview)* to group all subscriptions that are going to be audited by Prowler.
|
||||
|
||||
To do this in a proper way you have to [create a new management group](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) and add all roles in the same way that have been done for subscription scope.
|
||||
Scanning multiple subscriptions can be tedious due to the need to create and assign roles for each one. To simplify this process, we recommend using management groups to organize and audit subscriptions collectively with Prowler.
|
||||
|
||||
1. **Create a Management Group**: Follow the [official guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) to create a new management group.
|
||||

|
||||
|
||||
Once the management group is properly set you can add all the subscription that you want to audit.
|
||||
|
||||
2. **Add all roles**: Assign roles at to the new management group like in the [past section](#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) but at the management group level instead of the subscription level.
|
||||
3. **Add subscriptions**: Add all the subscriptions you want to audit to the management group.
|
||||

|
||||
|
||||
???+ note
|
||||
By default, `prowler` will scan all subscriptions in the Azure tenant, use the flag `--subscription-id` to specify the subscriptions to be scanned.
|
||||
|
||||
@@ -22,32 +22,31 @@ In order to see which compliance frameworks are cover by Prowler, you can use op
|
||||
```sh
|
||||
prowler <provider> --list-compliance
|
||||
```
|
||||
Currently, the available frameworks are:
|
||||
|
||||
### AWS
|
||||
|
||||
- `aws_account_security_onboarding_aws`
|
||||
- `aws_audit_manager_control_tower_guardrails_aws`
|
||||
- `aws_foundational_security_best_practices_aws`
|
||||
- `aws_foundational_technical_review_aws`
|
||||
- `aws_well_architected_framework_reliability_pillar_aws`
|
||||
- `aws_well_architected_framework_security_pillar_aws`
|
||||
- `cis_1.4_aws`
|
||||
- `cis_1.5_aws`
|
||||
- `cis_2.0_aws`
|
||||
- `cis_2.0_gcp`
|
||||
- `cis_2.0_azure`
|
||||
- `cis_2.1_azure`
|
||||
- `cis_3.0_aws`
|
||||
- `cis_1.8_kubernetes`
|
||||
- `cisa_aws`
|
||||
- `ens_rd2022_aws`
|
||||
- `fedramp_low_revision_4_aws`
|
||||
- `fedramp_moderate_revision_4_aws`
|
||||
- `ffiec_aws`
|
||||
- `aws_foundational_technical_review_aws`
|
||||
- `gdpr_aws`
|
||||
- `gxp_21_cfr_part_11_aws`
|
||||
- `gxp_eu_annex_11_aws`
|
||||
- `hipaa_aws`
|
||||
- `iso27001_2013_aws`
|
||||
- `kisa_isms_p_2023_aws`
|
||||
- `kisa_isms_p_2023_korean_aws`
|
||||
- `mitre_attack_aws`
|
||||
- `nist_800_171_revision_2_aws`
|
||||
- `nist_800_53_revision_4_aws`
|
||||
@@ -57,6 +56,23 @@ Currently, the available frameworks are:
|
||||
- `rbi_cyber_security_framework_aws`
|
||||
- `soc2_aws`
|
||||
|
||||
### Azure
|
||||
|
||||
- `cis_2.0_azure`
|
||||
- `cis_2.1_azure`
|
||||
- `ens_rd2022_azure`
|
||||
- `mitre_attack_azure`
|
||||
|
||||
### GCP
|
||||
|
||||
- `cis_2.0_gcp`
|
||||
- `ens_rd2022_gcp`
|
||||
- `mitre_attack_gcp`
|
||||
|
||||
### Kubernetes
|
||||
|
||||
- `cis_1.8_kubernetes`
|
||||
|
||||
## List Requirements of Compliance Frameworks
|
||||
For each compliance framework, you can use option `--list-compliance-requirements` to list its requirements:
|
||||
```sh
|
||||
|
||||
@@ -75,6 +75,7 @@ The following list includes all the Azure checks with configurable variables tha
|
||||
| `app_ensure_php_version_is_latest` | `php_latest_version` | String |
|
||||
| `app_ensure_python_version_is_latest` | `python_latest_version` | String |
|
||||
| `app_ensure_java_version_is_latest` | `java_latest_version` | String |
|
||||
| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings |
|
||||
|
||||
|
||||
## GCP
|
||||
@@ -447,6 +448,14 @@ azure:
|
||||
# azure.app_ensure_java_version_is_latest
|
||||
java_latest_version: "17"
|
||||
|
||||
# Azure SQL Server
|
||||
# azure.sqlserver_minimal_tls_version
|
||||
recommended_minimal_tls_versions:
|
||||
[
|
||||
"1.2",
|
||||
"1.3"
|
||||
]
|
||||
|
||||
# GCP Configuration
|
||||
gcp:
|
||||
# GCP Compute Configuration
|
||||
|
||||
@@ -42,6 +42,7 @@ Mutelist:
|
||||
Resources:
|
||||
- "user-1" # Will mute user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will mute user-2 in check iam_user_hardware_mfa_enabled
|
||||
Description: "Findings related with the check iam_user_hardware_mfa_enabled will be muted for us-east-1 region and user-1, user-2 resources"
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
@@ -140,6 +141,9 @@ Mutelist:
|
||||
| `resource` | The resource identifier. Use `*` to apply the mutelist to all resources. | `ANDed` |
|
||||
| `tag` | The tag value. | `ORed` |
|
||||
|
||||
### Description
|
||||
|
||||
This field can be used to add information or some hints for the Mutelist rule.
|
||||
|
||||
## How to Use the Mutelist
|
||||
|
||||
@@ -171,6 +175,7 @@ If you want to mute failed findings only in specific regions, create a file with
|
||||
- "ap-southeast-2"
|
||||
Resources:
|
||||
- "*"
|
||||
Description: "Description related with the muted findings for the check"
|
||||
|
||||
### Default Mutelist
|
||||
For the AWS Provider, Prowler is executed with a default AWS Mutelist with the AWS Resources that should be muted such as all resources created by AWS Control Tower when setting up a landing zone that can be found in [AWS Documentation](https://docs.aws.amazon.com/controltower/latest/userguide/shared-account-resources.html).
|
||||
|
||||
@@ -5,6 +5,9 @@ The **Prowler App** is a user-friendly interface for the Prowler CLI, providing
|
||||
After [installing](../index.md#prowler-app-installation) the **Prowler App**, access it at [http://localhost:3000](http://localhost:3000).
|
||||
You can also access to the auto-generated **Prowler API** documentation at [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs) to see all the available endpoints, parameters and responses.
|
||||
|
||||
???+ note
|
||||
If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user you can see API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs)
|
||||
|
||||
## **Step 1: Sign Up**
|
||||
To get started, sign up using your email and password:
|
||||
|
||||
@@ -42,7 +45,7 @@ Once you’ve selected a provider, you need to provide the Provider UID:
|
||||
- **AWS**: Enter your AWS Account ID.
|
||||
- **GCP**: Enter your GCP Project ID.
|
||||
- **Azure**: Enter your Azure Subscription ID.
|
||||
- **Kubernetes**: Enter your Kubernetes Cluster name.
|
||||
- **Kubernetes**: Enter your Kubernetes Cluster context of your kubeconfig file.
|
||||
|
||||
Optionally, provide a **Provider Alias** for easier identification. Follow the instructions provided to add your credentials:
|
||||
|
||||
@@ -71,7 +74,7 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to
|
||||
---
|
||||
|
||||
### **Step 4.2: Azure Credentials**
|
||||
For Azure, Prowler App uses a Service Principal to authenticate. See the steps in https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/azure/create-prowler-service-principal/ to create a Service Principal. Then, enter the `Tenant ID`, `Client ID` and `Client Secret` of the Service Principal.
|
||||
For Azure, Prowler App uses a service principal application to authenticate, for more information about the process of creating and adding permissions to a service principal check this [section](../getting-started/requirements.md#azure). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-the-proper-permissions) and [Subscription](./azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
|
||||
|
||||
<img src="../../img/azure-credentials.png" alt="Azure Credentials" width="700"/>
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ theme:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- git-revision-date-localized:
|
||||
@@ -112,7 +111,7 @@ nav:
|
||||
- Contact Us: contact.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- About: about.md
|
||||
- Prowler SaaS: https://prowler.com
|
||||
- Prowler Cloud: https://prowler.com
|
||||
|
||||
# Customization
|
||||
extra:
|
||||
|
||||
@@ -95,6 +95,7 @@ Resources:
|
||||
- 'servicecatalog:List*'
|
||||
- 'ssm:GetDocument'
|
||||
- 'ssm-incidents:List*'
|
||||
- 'states:ListTagsForResource'
|
||||
- 'support:Describe*'
|
||||
- 'tag:GetTagKeys'
|
||||
- 'wellarchitected:List*'
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"servicecatalog:List*",
|
||||
"ssm:GetDocument",
|
||||
"ssm-incidents:List*",
|
||||
"states:ListTagsForResource",
|
||||
"support:Describe*",
|
||||
"tag:GetTagKeys",
|
||||
"wellarchitected:List*"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"roleName": "ProwlerRole",
|
||||
"description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
|
||||
"assignableScopes": [
|
||||
"/"
|
||||
"/{'subscriptions', 'providers/Microsoft.Management/managementGroups'}/{Your Subscription or Management Group ID}"
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
229
poetry.lock
generated
229
poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "about-time"
|
||||
@@ -198,13 +198,13 @@ trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "3.3.5"
|
||||
version = "3.3.8"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
optional = false
|
||||
python-versions = ">=3.9.0"
|
||||
files = [
|
||||
{file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"},
|
||||
{file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"},
|
||||
{file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"},
|
||||
{file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -399,13 +399,13 @@ isodate = ">=0.6.1,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-compute"
|
||||
version = "33.0.0"
|
||||
version = "33.1.0"
|
||||
description = "Microsoft Azure Compute Management Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "azure-mgmt-compute-33.0.0.tar.gz", hash = "sha256:a3cc0fe4f09c8e1d3523c1bfb92620dfe263a0a893b0ac13a33d7057e9ddddd2"},
|
||||
{file = "azure_mgmt_compute-33.0.0-py3-none-any.whl", hash = "sha256:155f8d78a1fdedcea1725fd12b85b2d87fbcb6b53f8e77451c644f45701e3bcf"},
|
||||
{file = "azure_mgmt_compute-33.1.0-py3-none-any.whl", hash = "sha256:9b119a1b7f621aee951074ef110b16d545d7b45c9cfb303becf04210bd772c91"},
|
||||
{file = "azure_mgmt_compute-33.1.0.tar.gz", hash = "sha256:f5a5e18a5a7a0354562bbfa589b5db4aaf1e8ac3a194a2a910db55900d2535e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -513,13 +513,13 @@ isodate = ">=0.6.1,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-network"
|
||||
version = "28.0.0"
|
||||
version = "28.1.0"
|
||||
description = "Microsoft Azure Network Management Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "azure_mgmt_network-28.0.0-py3-none-any.whl", hash = "sha256:2ee23c1f2ba75752187bd7f4c3e94ad172282cbf8153694feadc7886ef88493c"},
|
||||
{file = "azure_mgmt_network-28.0.0.tar.gz", hash = "sha256:40356d348ef4838324f19a41cd80340b4f8dd4ac2f0a18a4cbd5cc95ef2974f3"},
|
||||
{file = "azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658"},
|
||||
{file = "azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -775,17 +775,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.35.71"
|
||||
version = "1.35.94"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "boto3-1.35.71-py3-none-any.whl", hash = "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16"},
|
||||
{file = "boto3-1.35.71.tar.gz", hash = "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f"},
|
||||
{file = "boto3-1.35.94-py3-none-any.whl", hash = "sha256:516c514fb447d6f216833d06a0781c003fcf43099a4ca2f5a363a8afe0942070"},
|
||||
{file = "boto3-1.35.94.tar.gz", hash = "sha256:5aa606239f0fe0dca0506e0ad6bbe4c589048e7e6c2486cee5ec22b6aa7ec2f8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.35.71,<1.36.0"
|
||||
botocore = ">=1.35.94,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.71"
|
||||
version = "1.35.94"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1"},
|
||||
{file = "botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba"},
|
||||
{file = "botocore-1.35.94-py3-none-any.whl", hash = "sha256:d784d944865d8279c79d2301fc09ac28b5221d4e7328fb4e23c642c253b9932c"},
|
||||
{file = "botocore-1.35.94.tar.gz", hash = "sha256:2b3309b356541faa4d88bb957dcac1d8004aa44953c0b7d4521a6cc5d3d5d6ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1099,73 +1099,73 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.8"
|
||||
version = "7.6.10"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"},
|
||||
{file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"},
|
||||
{file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"},
|
||||
{file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"},
|
||||
{file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"},
|
||||
{file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"},
|
||||
{file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"},
|
||||
{file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"},
|
||||
{file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
|
||||
{file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
|
||||
{file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
|
||||
{file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
|
||||
{file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
|
||||
{file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
|
||||
{file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
|
||||
{file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
|
||||
{file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1719,13 +1719,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.154.0"
|
||||
version = "2.158.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
|
||||
{file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
|
||||
{file = "google_api_python_client-2.158.0-py2.py3-none-any.whl", hash = "sha256:36f8c8d2e79e50f76790ca5946d2f3f8333e210dc8539a6c88e0742416474ad2"},
|
||||
{file = "google_api_python_client-2.158.0.tar.gz", hash = "sha256:b6664597a9955e04977a62752e33fe44cb35c580e190c1cb08a041893172bd67"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2403,13 +2403,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "microsoft-kiota-abstractions"
|
||||
version = "1.6.2"
|
||||
version = "1.6.8"
|
||||
description = "Core abstractions for kiota generated libraries in Python"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "microsoft_kiota_abstractions-1.6.2-py3-none-any.whl", hash = "sha256:8c2c777748e80f17dba3809b5d149585d9918198f0f94125e87432f7165ba80e"},
|
||||
{file = "microsoft_kiota_abstractions-1.6.2.tar.gz", hash = "sha256:dec30f0fb427a051003e94b5c6fcf266f4702ecbd9d6961e3966124b9cbe41bf"},
|
||||
{file = "microsoft_kiota_abstractions-1.6.8-py3-none-any.whl", hash = "sha256:12819dee24d5aaa31e99683d938f65e50cbc446de087df244cd26c3326ec4e15"},
|
||||
{file = "microsoft_kiota_abstractions-1.6.8.tar.gz", hash = "sha256:7070affabfa7182841646a0c8491cbb240af366aff2b9132f0caa45c4837dd78"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.46"
|
||||
version = "9.5.49"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.46-py3-none-any.whl", hash = "sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83"},
|
||||
{file = "mkdocs_material-9.5.46.tar.gz", hash = "sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"},
|
||||
{file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"},
|
||||
{file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2769,13 +2769,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-sdk"
|
||||
version = "1.12.0"
|
||||
version = "1.16.0"
|
||||
description = "The Microsoft Graph Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "msgraph_sdk-1.12.0-py3-none-any.whl", hash = "sha256:ac298b546b240391b0e407379d039db32862a56d6fe15cf7c5f7e77631fc6771"},
|
||||
{file = "msgraph_sdk-1.12.0.tar.gz", hash = "sha256:fbb5a8a9f6eed89b496f207eb35b6b4cfc7fefa75608aeef07477a3b2276d4fa"},
|
||||
{file = "msgraph_sdk-1.16.0-py3-none-any.whl", hash = "sha256:1dd26ece74c43167818e2ff58b062180233ce7187ad2a061057af1195395c56c"},
|
||||
{file = "msgraph_sdk-1.16.0.tar.gz", hash = "sha256:980d19617d8d8b20545ef77fa5629fef768ce4ea1f2d1a124c5a9dd88d77940c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3043,18 +3043,18 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-schema-validator"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "OpenAPI schema validation for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0,<4.0.0"
|
||||
python-versions = "<4.0.0,>=3.8.0"
|
||||
files = [
|
||||
{file = "openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8"},
|
||||
{file = "openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804"},
|
||||
{file = "openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3"},
|
||||
{file = "openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonschema = ">=4.19.1,<5.0.0"
|
||||
jsonschema-specifications = ">=2023.5.2,<2024.0.0"
|
||||
jsonschema-specifications = ">=2023.5.2"
|
||||
rfc3339-validator = "*"
|
||||
|
||||
[[package]]
|
||||
@@ -3796,17 +3796,17 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "3.3.1"
|
||||
version = "3.3.3"
|
||||
description = "python code static checker"
|
||||
optional = false
|
||||
python-versions = ">=3.9.0"
|
||||
files = [
|
||||
{file = "pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9"},
|
||||
{file = "pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e"},
|
||||
{file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"},
|
||||
{file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=3.3.4,<=3.4.0-dev0"
|
||||
astroid = ">=3.3.8,<=3.4.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||
@@ -3858,13 +3858,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4458,6 +4458,7 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
|
||||
@@ -4466,6 +4467,7 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
|
||||
@@ -4474,6 +4476,7 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
|
||||
@@ -4482,6 +4485,7 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
|
||||
@@ -4490,6 +4494,7 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
|
||||
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
|
||||
@@ -4638,17 +4643,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "slack-sdk"
|
||||
version = "3.33.4"
|
||||
version = "3.34.0"
|
||||
description = "The Slack API Platform SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "slack_sdk-3.33.4-py2.py3-none-any.whl", hash = "sha256:9f30cb3c9c07b441c49d53fc27f9f1837ad1592a7e9d4ca431f53cdad8826cc6"},
|
||||
{file = "slack_sdk-3.33.4.tar.gz", hash = "sha256:5e109847f6b6a22d227609226ba4ed936109dc00675bddeb7e0bee502d3ee7e0"},
|
||||
{file = "slack_sdk-3.34.0-py2.py3-none-any.whl", hash = "sha256:c61f57f310d85be83466db5a98ab6ae3bb2e5587437b54fa0daa8fae6a0feffa"},
|
||||
{file = "slack_sdk-3.34.0.tar.gz", hash = "sha256:ff61db7012160eed742285ea91f11c72b7a38a6500a7f6c5335662b4bc6b853d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<14)"]
|
||||
optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<15)"]
|
||||
|
||||
[[package]]
|
||||
name = "smmap"
|
||||
@@ -4888,13 +4893,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "vulture"
|
||||
version = "2.13"
|
||||
version = "2.14"
|
||||
description = "Find dead code"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"},
|
||||
{file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"},
|
||||
{file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"},
|
||||
{file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5194,4 +5199,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<3.13"
|
||||
content-hash = "1acc901866ecfc2c0f3576b9e442d7a3b6e6522cac3d4d1b9301ed4232755cba"
|
||||
content-hash = "08f577e54ba3af2dba39393521c3596c614cc6432278be4e0b001fa706c6dccc"
|
||||
|
||||
1830
prowler/compliance/gcp/cis_3.0_gcp.json
Normal file
1830
prowler/compliance/gcp/cis_3.0_gcp.json
Normal file
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@ Mutelist:
|
||||
- "*"
|
||||
Resources:
|
||||
- "aws-controltower-NotificationForwarder"
|
||||
Description: "Checks from AWS lambda functions muted by default"
|
||||
"cloudformation_stack*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -14,6 +14,7 @@ Mutelist:
|
||||
Resources:
|
||||
- "user-1" # Will ignore user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will ignore user-2 in check iam_user_hardware_mfa_enabled
|
||||
Description: "Check iam_user_hardware_mfa_enabled muted for region us-east-1 and resources user-1, user-2"
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -15,6 +15,7 @@ Mutelist:
|
||||
Resources:
|
||||
- "sqlserver1" # Will ignore sqlserver1 in check sqlserver_tde_encryption_enabled located in westeurope
|
||||
- "sqlserver2" # Will ignore sqlserver2 in check sqlserver_tde_encryption_enabled located in westeurope
|
||||
Description: "Findings related with the check sqlserver_tde_encryption_enabled is muted for westeurope region and sqlserver1, sqlserver2 resources"
|
||||
"defender_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.0.0"
|
||||
prowler_version = "5.1.1"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -73,6 +73,10 @@ aws:
|
||||
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
|
||||
log_group_retention_days: 365
|
||||
|
||||
# AWS CloudFormation Configuration
|
||||
# cloudformation_stack_cdktoolkit_bootstrap_version --> by default is 21
|
||||
recommended_cdk_bootstrap_version: 21
|
||||
|
||||
# AWS AppStream Session Configuration
|
||||
# aws.appstream_fleet_session_idle_disconnect_timeout
|
||||
max_idle_disconnect_timeout_in_seconds: 600 # 10 Minutes
|
||||
@@ -314,10 +318,12 @@ aws:
|
||||
# AWS ACM Configuration
|
||||
# aws.acm_certificates_expiration_check
|
||||
days_to_expire_threshold: 7
|
||||
# aws.acm_certificates_rsa_key_length
|
||||
# aws.acm_certificates_with_secure_key_algorithms
|
||||
insecure_key_algorithms:
|
||||
[
|
||||
"RSA-1024",
|
||||
"P-192",
|
||||
"SHA-1",
|
||||
]
|
||||
|
||||
# AWS EKS Configuration
|
||||
@@ -388,6 +394,14 @@ azure:
|
||||
# azure.app_ensure_java_version_is_latest
|
||||
java_latest_version: "17"
|
||||
|
||||
# Azure SQL Server
|
||||
# azure.sqlserver_minimal_tls_version
|
||||
recommended_minimal_tls_versions:
|
||||
[
|
||||
"1.2",
|
||||
"1.3",
|
||||
]
|
||||
|
||||
# GCP Configuration
|
||||
gcp:
|
||||
# GCP Compute Configuration
|
||||
|
||||
@@ -15,6 +15,7 @@ Mutelist:
|
||||
Resources:
|
||||
- "instance1" # Will ignore instance1 in check compute_instance_public_ip located in europe-southwest1
|
||||
- "instance2" # Will ignore instance2 in check compute_instance_public_ip located in europe-southwest1
|
||||
Description: "Findings related with the check compute_instance_public_ip will be muted for europe-southwest1 region and instance1, instance2 resources"
|
||||
"iam_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -15,6 +15,7 @@ Mutelist:
|
||||
Resources:
|
||||
- "prowler-pod1" # Will ignore prowler-pod1 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1
|
||||
- "prowler-pod2" # Will ignore prowler-pod2 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1
|
||||
Description: "Findings related with the check core_minimize_allowPrivilegeEscalation_containers will be muted for namespace1 region and prowler-pod1, prowler-pod2 resources"
|
||||
"kubelet_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -405,16 +405,18 @@ class Check_Report:
|
||||
status: str
|
||||
status_extended: str
|
||||
check_metadata: CheckMetadata
|
||||
resource_metadata: dict
|
||||
resource_details: str
|
||||
resource_tags: list
|
||||
muted: bool
|
||||
|
||||
def __init__(self, metadata):
|
||||
def __init__(self, metadata, resource=None):
|
||||
self.status = ""
|
||||
self.check_metadata = CheckMetadata.parse_raw(metadata)
|
||||
self.resource_metadata = resource.dict() if resource else {}
|
||||
self.status_extended = ""
|
||||
self.resource_details = ""
|
||||
self.resource_tags = []
|
||||
self.resource_tags = getattr(resource, "tags", []) if resource else []
|
||||
self.muted = False
|
||||
|
||||
|
||||
@@ -426,11 +428,20 @@ class Check_Report_AWS(Check_Report):
|
||||
resource_arn: str
|
||||
region: str
|
||||
|
||||
def __init__(self, metadata):
|
||||
super().__init__(metadata)
|
||||
self.resource_id = ""
|
||||
self.resource_arn = ""
|
||||
self.region = ""
|
||||
def __init__(self, metadata, resource_metadata=None):
|
||||
super().__init__(metadata, resource_metadata)
|
||||
if resource_metadata:
|
||||
self.resource_id = (
|
||||
getattr(resource_metadata, "id", None)
|
||||
or getattr(resource_metadata, "name", None)
|
||||
or ""
|
||||
)
|
||||
self.resource_arn = getattr(resource_metadata, "arn", "")
|
||||
self.region = getattr(resource_metadata, "region", "")
|
||||
else:
|
||||
self.resource_id = ""
|
||||
self.resource_arn = ""
|
||||
self.region = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from schema import Optional, Schema
|
||||
|
||||
mutelist_schema = Schema(
|
||||
{
|
||||
"Accounts": {
|
||||
str: {
|
||||
"Checks": {
|
||||
str: {
|
||||
"Regions": list,
|
||||
"Resources": list,
|
||||
Optional("Tags"): list,
|
||||
Optional("Exceptions"): {
|
||||
Optional("Accounts"): list,
|
||||
Optional("Regions"): list,
|
||||
Optional("Resources"): list,
|
||||
Optional("Tags"): list,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -2,12 +2,86 @@ import re
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import yaml
|
||||
from jsonschema import validate
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.models import mutelist_schema
|
||||
from prowler.lib.outputs.common import Status
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
|
||||
mutelist_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Accounts": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": { # Match any account
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Checks": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": { # Match any check
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Regions": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Resources": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Tags": { # Optional field
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Exceptions": { # Optional field
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Accounts": { # Optional field
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Regions": { # Optional field
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Resources": { # Optional field
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"Tags": { # Optional field
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"Description": { # Optional field
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"Regions",
|
||||
"Resources",
|
||||
], # Mandatory within a check
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
"required": ["Checks"], # Mandatory within an account
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
"required": ["Accounts"], # Accounts is mandatory at the root level
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
class Mutelist(ABC):
|
||||
"""
|
||||
@@ -70,7 +144,7 @@ class Mutelist(ABC):
|
||||
|
||||
def validate_mutelist(self) -> bool:
|
||||
try:
|
||||
self._mutelist = mutelist_schema.validate(self._mutelist)
|
||||
validate(self._mutelist, schema=mutelist_schema)
|
||||
return True
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -106,6 +180,7 @@ class Mutelist(ABC):
|
||||
- 'i-123456789'
|
||||
Tags:
|
||||
- 'Name=AdminInstance | Environment=Prod'
|
||||
Description: 'Field to describe why the findings associated with these values are muted'
|
||||
```
|
||||
The check `ec2_instance_detailed_monitoring_enabled` will be muted for all accounts and regions and for the resource_id 'i-123456789' with at least one of the tags 'Name=AdminInstance' or 'Environment=Prod'.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ def fill_common_finding_data(finding: dict, unix_timestamp: bool) -> dict:
|
||||
"status_extended": finding.status_extended,
|
||||
"muted": finding.muted,
|
||||
"resource_details": finding.resource_details,
|
||||
# "resource_metadata": finding.resource_metadata, TODO: add resource_metadata to the finding
|
||||
"resource_tags": unroll_tags(finding.resource_tags),
|
||||
}
|
||||
return finding_data
|
||||
|
||||
@@ -94,11 +94,12 @@ def get_cis_table(
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -95,11 +95,12 @@ def get_ens_table(
|
||||
print(
|
||||
f"\nEstado de Cumplimiento de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user