mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-21 21:32:13 +00:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75755c8c5 | ||
|
|
3e0568f381 | ||
|
|
fec66a3685 | ||
|
|
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 | ||
|
|
ba335de6b3 | ||
|
|
9c089756c3 | ||
|
|
8d4b0914a8 | ||
|
|
1ae3f89aab | ||
|
|
93051d55d5 | ||
|
|
b984f0423a | ||
|
|
f2f196cfcd | ||
|
|
6471d936bb | ||
|
|
21bbdccc41 | ||
|
|
48946fa4f7 | ||
|
|
9312dda7c2 | ||
|
|
e3013329ee | ||
|
|
161c56ffe4 | ||
|
|
e306322630 | ||
|
|
b4eb6e8076 | ||
|
|
b54e9334b9 | ||
|
|
5fd1af7559 | ||
|
|
83c7ced6ff | ||
|
|
67d9ff2419 | ||
|
|
130fddae1e | ||
|
|
38a0d2d740 | ||
|
|
5c2adf1e14 | ||
|
|
04b9f81e26 | ||
|
|
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 | ||
|
|
29bc697487 | ||
|
|
eddc672264 | ||
|
|
8c71a39487 | ||
|
|
ff0ac27723 | ||
|
|
381aa93f55 | ||
|
|
2bee4b986f | ||
|
|
ad7134d283 | ||
|
|
9723b8fac1 | ||
|
|
58723ae52e | ||
|
|
67ef67add9 |
@@ -40,12 +40,9 @@ DJANGO_LOGGING_FORMATTER=human_readable
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL=INFO
|
||||
# Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_WORKERS=4
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30 # Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440 # Token lifetime is in minutes
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_MANAGE_DB_PARTITIONS=True
|
||||
@@ -90,4 +87,3 @@ jQIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
+39
-2
@@ -5,6 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# v5
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -14,6 +15,7 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -24,20 +26,55 @@ updates:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "npm"
|
||||
|
||||
# v4.6
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
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"
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -31,34 +31,19 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -69,6 +69,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 +81,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
|
||||
|
||||
- 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'
|
||||
@@ -109,48 +113,59 @@ jobs:
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry lock --check
|
||||
|
||||
- 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
|
||||
|
||||
@@ -5,38 +5,43 @@ on:
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.84.1
|
||||
uses: trufflesecurity/trufflehog@v3.86.1
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -3,7 +3,11 @@ name: SDK - Build and Push containers
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# For `v3-latest`
|
||||
- "v3"
|
||||
# For `v4-latest`
|
||||
- "v4.6"
|
||||
# For `latest`
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
|
||||
@@ -17,7 +17,6 @@ on:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
@@ -26,7 +25,6 @@ on:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
|
||||
@@ -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,19 +37,20 @@ jobs:
|
||||
README.md
|
||||
mkdocs.yml
|
||||
.backportrc.json
|
||||
.env
|
||||
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry
|
||||
|
||||
- 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: |
|
||||
@@ -59,44 +61,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
|
||||
|
||||
- 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
|
||||
|
||||
@@ -10,37 +10,12 @@ env:
|
||||
CACHE: "poetry"
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
release-prowler-job:
|
||||
runs-on: ubuntu-latest
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
name: Release Prowler to PyPI
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 1
|
||||
|
||||
- name: Get Prowler version
|
||||
run: |
|
||||
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
|
||||
|
||||
@@ -31,34 +31,19 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -139,6 +139,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,7 +22,6 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
# PostgreSQL settings
|
||||
# If running django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
|
||||
Generated
+326
-316
@@ -24,87 +24,87 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.11"
|
||||
version = "3.11.9"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"},
|
||||
{file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"},
|
||||
{file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"},
|
||||
{file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"},
|
||||
{file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"},
|
||||
{file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"},
|
||||
{file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"},
|
||||
{file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"},
|
||||
{file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"},
|
||||
{file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"},
|
||||
{file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"},
|
||||
{file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -121,13 +121,13 @@ speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.3.2"
|
||||
version = "1.3.1"
|
||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
|
||||
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
|
||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -164,23 +164,22 @@ vine = ">=5.0.0,<6.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.7.0"
|
||||
version = "4.6.2.post1"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
|
||||
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
|
||||
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
|
||||
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -221,19 +220,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.3.0"
|
||||
version = "24.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"},
|
||||
{file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"},
|
||||
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
|
||||
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||
@@ -795,13 +794,13 @@ zstd = ["zstandard (==0.22.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.12.14"
|
||||
version = "2024.8.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
|
||||
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1283,37 +1282,37 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
version = "1.8.11"
|
||||
version = "1.8.9"
|
||||
description = "An implementation of the Debug Adapter Protocol for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"},
|
||||
{file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"},
|
||||
{file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"},
|
||||
{file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"},
|
||||
{file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"},
|
||||
{file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"},
|
||||
{file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"},
|
||||
{file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"},
|
||||
{file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"},
|
||||
{file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"},
|
||||
{file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"},
|
||||
{file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"},
|
||||
{file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"},
|
||||
{file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"},
|
||||
{file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"},
|
||||
{file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"},
|
||||
{file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"},
|
||||
{file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"},
|
||||
{file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"},
|
||||
{file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"},
|
||||
{file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"},
|
||||
{file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"},
|
||||
{file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"},
|
||||
{file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"},
|
||||
{file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"},
|
||||
{file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"},
|
||||
{file = "debugpy-1.8.9-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:cfe1e6c6ad7178265f74981edf1154ffce97b69005212fbc90ca22ddfe3d017e"},
|
||||
{file = "debugpy-1.8.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada7fb65102a4d2c9ab62e8908e9e9f12aed9d76ef44880367bc9308ebe49a0f"},
|
||||
{file = "debugpy-1.8.9-cp310-cp310-win32.whl", hash = "sha256:c36856343cbaa448171cba62a721531e10e7ffb0abff838004701454149bc037"},
|
||||
{file = "debugpy-1.8.9-cp310-cp310-win_amd64.whl", hash = "sha256:17c5e0297678442511cf00a745c9709e928ea4ca263d764e90d233208889a19e"},
|
||||
{file = "debugpy-1.8.9-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:b74a49753e21e33e7cf030883a92fa607bddc4ede1aa4145172debc637780040"},
|
||||
{file = "debugpy-1.8.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d22dacdb0e296966d7d74a7141aaab4bec123fa43d1a35ddcb39bf9fd29d70"},
|
||||
{file = "debugpy-1.8.9-cp311-cp311-win32.whl", hash = "sha256:8138efff315cd09b8dcd14226a21afda4ca582284bf4215126d87342bba1cc66"},
|
||||
{file = "debugpy-1.8.9-cp311-cp311-win_amd64.whl", hash = "sha256:ff54ef77ad9f5c425398efb150239f6fe8e20c53ae2f68367eba7ece1e96226d"},
|
||||
{file = "debugpy-1.8.9-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:957363d9a7a6612a37458d9a15e72d03a635047f946e5fceee74b50d52a9c8e2"},
|
||||
{file = "debugpy-1.8.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e565fc54b680292b418bb809f1386f17081d1346dca9a871bf69a8ac4071afe"},
|
||||
{file = "debugpy-1.8.9-cp312-cp312-win32.whl", hash = "sha256:3e59842d6c4569c65ceb3751075ff8d7e6a6ada209ceca6308c9bde932bcef11"},
|
||||
{file = "debugpy-1.8.9-cp312-cp312-win_amd64.whl", hash = "sha256:66eeae42f3137eb428ea3a86d4a55f28da9bd5a4a3d369ba95ecc3a92c1bba53"},
|
||||
{file = "debugpy-1.8.9-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:957ecffff80d47cafa9b6545de9e016ae8c9547c98a538ee96ab5947115fb3dd"},
|
||||
{file = "debugpy-1.8.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1efbb3ff61487e2c16b3e033bc8595aea578222c08aaf3c4bf0f93fadbd662ee"},
|
||||
{file = "debugpy-1.8.9-cp313-cp313-win32.whl", hash = "sha256:7c4d65d03bee875bcb211c76c1d8f10f600c305dbd734beaed4077e902606fee"},
|
||||
{file = "debugpy-1.8.9-cp313-cp313-win_amd64.whl", hash = "sha256:e46b420dc1bea64e5bbedd678148be512442bc589b0111bd799367cde051e71a"},
|
||||
{file = "debugpy-1.8.9-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:472a3994999fe6c0756945ffa359e9e7e2d690fb55d251639d07208dbc37caea"},
|
||||
{file = "debugpy-1.8.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365e556a4772d7d0d151d7eb0e77ec4db03bcd95f26b67b15742b88cacff88e9"},
|
||||
{file = "debugpy-1.8.9-cp38-cp38-win32.whl", hash = "sha256:54a7e6d3014c408eb37b0b06021366ee985f1539e12fe49ca2ee0d392d9ceca5"},
|
||||
{file = "debugpy-1.8.9-cp38-cp38-win_amd64.whl", hash = "sha256:8e99c0b1cc7bf86d83fb95d5ccdc4ad0586d4432d489d1f54e4055bcc795f693"},
|
||||
{file = "debugpy-1.8.9-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:7e8b079323a56f719977fde9d8115590cb5e7a1cba2fcee0986ef8817116e7c1"},
|
||||
{file = "debugpy-1.8.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6953b335b804a41f16a192fa2e7851bdcfd92173cbb2f9f777bb934f49baab65"},
|
||||
{file = "debugpy-1.8.9-cp39-cp39-win32.whl", hash = "sha256:7e646e62d4602bb8956db88b1e72fe63172148c1e25c041e03b103a25f36673c"},
|
||||
{file = "debugpy-1.8.9-cp39-cp39-win_amd64.whl", hash = "sha256:3d9755e77a2d680ce3d2c5394a444cf42be4a592caaf246dbfbdd100ffcf7ae5"},
|
||||
{file = "debugpy-1.8.9-py2.py3-none-any.whl", hash = "sha256:cc37a6c9987ad743d9c3a14fa1b1a14b7e4e6041f9dd0c8abf8895fe7a97b899"},
|
||||
{file = "debugpy-1.8.9.zip", hash = "sha256:1339e14c7d980407248f09824d1b25ff5c5616651689f1e0f0e51bdead3ea13e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1901,13 +1900,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.24.0"
|
||||
version = "2.23.0"
|
||||
description = "Google API client core library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"},
|
||||
{file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"},
|
||||
{file = "google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f"},
|
||||
{file = "google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1943,13 +1942,13 @@ uritemplate = ">=3.0.1,<5"
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.37.0"
|
||||
version = "2.36.0"
|
||||
description = "Google Authentication Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"},
|
||||
{file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"},
|
||||
{file = "google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb"},
|
||||
{file = "google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1960,7 +1959,6 @@ rsa = ">=3.1.4,<5"
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"]
|
||||
enterprise-cert = ["cryptography", "pyopenssl"]
|
||||
pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"]
|
||||
pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
|
||||
reauth = ["pyu2f (>=0.1.5)"]
|
||||
requests = ["requests (>=2.20.0,<3.0.0.dev0)"]
|
||||
@@ -2454,13 +2452,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.23.2"
|
||||
version = "3.23.1"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "marshmallow-3.23.2-py3-none-any.whl", hash = "sha256:bcaf2d6fd74fb1459f8450e85d994997ad3e70036452cbfa4ab685acb19479b3"},
|
||||
{file = "marshmallow-3.23.2.tar.gz", hash = "sha256:c448ac6455ca4d794773f00bae22c2f351d62d739929f761dce5eacb5c468d7f"},
|
||||
{file = "marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491"},
|
||||
{file = "marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2641,13 +2639,13 @@ portalocker = ">=1.4,<3"
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-core"
|
||||
version = "1.1.8"
|
||||
version = "1.1.7"
|
||||
description = "Core component of the Microsoft Graph Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "msgraph_core-1.1.8-py3-none-any.whl", hash = "sha256:5b8d28ec16f6b2b9ef328b01368aa4500166afb0fa3748c1255b4340b344197f"},
|
||||
{file = "msgraph_core-1.1.8.tar.gz", hash = "sha256:58c50f1cfdf0098dc9120b8565988ecf7d7e0be6ae9e8a3c1b4805415469159a"},
|
||||
{file = "msgraph_core-1.1.7-py3-none-any.whl", hash = "sha256:7068f5ef59237d77f39cf6235996329d7fb8c8b56da7f16cd2fb820704ced1b3"},
|
||||
{file = "msgraph_core-1.1.7.tar.gz", hash = "sha256:9f975f476eebf8ad039bbfd177151ed23772a7f745996455497f3817a75a1d05"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2946,13 +2944,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.29.0"
|
||||
version = "1.28.2"
|
||||
description = "OpenTelemetry Python API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"},
|
||||
{file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"},
|
||||
{file = "opentelemetry_api-1.28.2-py3-none-any.whl", hash = "sha256:6fcec89e265beb258fe6b1acaaa3c8c705a934bd977b9f534a2b7c0d2d4275a6"},
|
||||
{file = "opentelemetry_api-1.28.2.tar.gz", hash = "sha256:ecdc70c7139f17f9b0cf3742d57d7020e3e8315d6cffcdf1a12a905d45b19cc0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2961,34 +2959,34 @@ importlib-metadata = ">=6.0,<=8.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.29.0"
|
||||
version = "1.28.2"
|
||||
description = "OpenTelemetry Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"},
|
||||
{file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"},
|
||||
{file = "opentelemetry_sdk-1.28.2-py3-none-any.whl", hash = "sha256:93336c129556f1e3ccd21442b94d3521759541521861b2214c499571b85cb71b"},
|
||||
{file = "opentelemetry_sdk-1.28.2.tar.gz", hash = "sha256:5fed24c5497e10df30282456fe2910f83377797511de07d14cec0d3e0a1a3110"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
opentelemetry-api = "1.29.0"
|
||||
opentelemetry-semantic-conventions = "0.50b0"
|
||||
opentelemetry-api = "1.28.2"
|
||||
opentelemetry-semantic-conventions = "0.49b2"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.50b0"
|
||||
version = "0.49b2"
|
||||
description = "OpenTelemetry Semantic Conventions"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"},
|
||||
{file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"},
|
||||
{file = "opentelemetry_semantic_conventions-0.49b2-py3-none-any.whl", hash = "sha256:51e7e1d0daa958782b6c2a8ed05e5f0e7dd0716fc327ac058777b8659649ee54"},
|
||||
{file = "opentelemetry_semantic_conventions-0.49b2.tar.gz", hash = "sha256:44e32ce6a5bb8d7c0c617f84b9dc1c8deda1045a07dc16a688cc7cbeab679997"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
deprecated = ">=1.2.6"
|
||||
opentelemetry-api = "1.29.0"
|
||||
opentelemetry-api = "1.28.2"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
@@ -3385,34 +3383,32 @@ testing = ["google-api-core (>=1.31.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "5.29.2"
|
||||
version = "5.29.0"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851"},
|
||||
{file = "protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9"},
|
||||
{file = "protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb"},
|
||||
{file = "protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e"},
|
||||
{file = "protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e"},
|
||||
{file = "protobuf-5.29.2-cp38-cp38-win32.whl", hash = "sha256:e621a98c0201a7c8afe89d9646859859be97cb22b8bf1d8eacfd90d5bda2eb19"},
|
||||
{file = "protobuf-5.29.2-cp38-cp38-win_amd64.whl", hash = "sha256:13d6d617a2a9e0e82a88113d7191a1baa1e42c2cc6f5f1398d3b054c8e7e714a"},
|
||||
{file = "protobuf-5.29.2-cp39-cp39-win32.whl", hash = "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9"},
|
||||
{file = "protobuf-5.29.2-cp39-cp39-win_amd64.whl", hash = "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355"},
|
||||
{file = "protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181"},
|
||||
{file = "protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e"},
|
||||
{file = "protobuf-5.29.0-cp310-abi3-win32.whl", hash = "sha256:ea7fb379b257911c8c020688d455e8f74efd2f734b72dc1ea4b4d7e9fd1326f2"},
|
||||
{file = "protobuf-5.29.0-cp310-abi3-win_amd64.whl", hash = "sha256:34a90cf30c908f47f40ebea7811f743d360e202b6f10d40c02529ebd84afc069"},
|
||||
{file = "protobuf-5.29.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c931c61d0cc143a2e756b1e7f8197a508de5365efd40f83c907a9febf36e6b43"},
|
||||
{file = "protobuf-5.29.0-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:85286a47caf63b34fa92fdc1fd98b649a8895db595cfa746c5286eeae890a0b1"},
|
||||
{file = "protobuf-5.29.0-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:0d10091d6d03537c3f902279fcf11e95372bdd36a79556311da0487455791b20"},
|
||||
{file = "protobuf-5.29.0-cp38-cp38-win32.whl", hash = "sha256:0cd67a1e5c2d88930aa767f702773b2d054e29957432d7c6a18f8be02a07719a"},
|
||||
{file = "protobuf-5.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:e467f81fdd12ded9655cea3e9b83dc319d93b394ce810b556fb0f421d8613e86"},
|
||||
{file = "protobuf-5.29.0-cp39-cp39-win32.whl", hash = "sha256:17d128eebbd5d8aee80300aed7a43a48a25170af3337f6f1333d1fac2c6839ac"},
|
||||
{file = "protobuf-5.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:6c3009e22717c6cc9e6594bb11ef9f15f669b19957ad4087214d69e08a213368"},
|
||||
{file = "protobuf-5.29.0-py3-none-any.whl", hash = "sha256:88c4af76a73183e21061881360240c0cdd3c39d263b4e8fb570aaf83348d608f"},
|
||||
{file = "protobuf-5.29.0.tar.gz", hash = "sha256:445a0c02483869ed8513a585d80020d012c6dc60075f96fa0563a724987b1001"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.0.2"
|
||||
version = "5.0.0"
|
||||
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.9"
|
||||
files = [
|
||||
{file = "prowler-5.0.2-py3-none-any.whl", hash = "sha256:752c23fcba0ef1f6b2e14557683a9f1040de278bf525020471b28397e06e477c"},
|
||||
{file = "prowler-5.0.2.tar.gz", hash = "sha256:32f6fcaaa6921715bda38e19b0782f07f717a7f02b7065f7ea08e83216bcf39f"},
|
||||
]
|
||||
python-versions = ">=3.9,<3.13"
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
alive-progress = "3.2.0"
|
||||
@@ -3454,7 +3450,7 @@ numpy = "2.0.2"
|
||||
pandas = "2.2.3"
|
||||
py-ocsf-models = "0.2.0"
|
||||
pydantic = "1.10.18"
|
||||
python-dateutil = ">=2.9.0.post0,<3.0.0"
|
||||
python-dateutil = "^2.9.0.post0"
|
||||
pytz = "2024.2"
|
||||
schema = "0.7.7"
|
||||
shodan = "1.31.0"
|
||||
@@ -3462,6 +3458,12 @@ slack-sdk = "3.33.4"
|
||||
tabulate = "0.9.0"
|
||||
tzlocal = "5.2"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "5.0.0"
|
||||
resolved_reference = "52723eda6e17eca40d08b0c1a51e99ec935a0b6c"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "6.1.0"
|
||||
@@ -3525,6 +3527,7 @@ files = [
|
||||
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
|
||||
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
|
||||
@@ -3533,6 +3536,8 @@ files = [
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
|
||||
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
|
||||
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
|
||||
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
|
||||
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
|
||||
@@ -3624,42 +3629,47 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pycurl"
|
||||
version = "7.45.4"
|
||||
version = "7.45.3"
|
||||
description = "PycURL -- A Python Interface To The cURL library"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "pycurl-7.45.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:247b4af8eab7d04137a7f1a98391930e04ea93dc669b64db5625070fe15f80a3"},
|
||||
{file = "pycurl-7.45.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:561f88697f7540634b1c750146f37bdc0da367b15f6b4ab2bb780871ee6ab005"},
|
||||
{file = "pycurl-7.45.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b485fdaf78553f0b8e1c2803bb7dcbe47a7b47594f846fc7e9d3b94d794cfc89"},
|
||||
{file = "pycurl-7.45.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e7ae49b88a5d57485fbabef004534225dfe04dc15716a61fae1a0c7f46f2279e"},
|
||||
{file = "pycurl-7.45.4-cp310-cp310-win32.whl", hash = "sha256:d14f954ecd21a070038d65ef1c6d1d3ab220f952ff703d48313123222097615c"},
|
||||
{file = "pycurl-7.45.4-cp310-cp310-win_amd64.whl", hash = "sha256:2548c3291a33c821f0f80bf9989fc43b5d90fb78b534a7015c8419b83c6f5803"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6c0e22052946bbfa25be67f9d1d6639eff10781c89f0cf6f3ff2099273d1bad"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acf25cfdaf914db21a2a6e9e274b6d95e3fa2b6018c38f2c58c94b5d8ac3d1b7"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a39f28f031885485325034918386be352036c220ca45625c7e286d3938eb579d"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9940e3234c1ca3d30f27a2202d325dbc25291605c98e9585100a351cacd935e8"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-win32.whl", hash = "sha256:ffd3262f98b8997ad04940061d5ebd8bab2362169b9440939c397e24a4a135b0"},
|
||||
{file = "pycurl-7.45.4-cp311-cp311-win_amd64.whl", hash = "sha256:1324a859b50bdb0abdbd5620e42f74240d0b7daf2d5925fa303695d9fc3ece18"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:731c46e7c0acffaab19f7c2ecc3d9e7ee337500e87b260b4e0b9fae2d90fa133"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13eb1643ab0bf4fdc539a2cdf1021029b07095d3196c5cee5a4271af268d3d31"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:df5f94c051c5a163fa85064559ca94979575e2da26740ff91c078c50c541c465"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:688d09ba2c6a0d4a749d192c43422839d73c40c85143c50cc65c944258fe0ba8"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-win32.whl", hash = "sha256:236600bfe2cd72efe47333add621286667e8fa027dadf1247349afbf30333e95"},
|
||||
{file = "pycurl-7.45.4-cp312-cp312-win_amd64.whl", hash = "sha256:26745c6c5ebdccfe8a828ac3fd4e6da6f5d2245696604f04529eb7894a02f4db"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bd493ce598f1dc76c8e50043c47debec27c583fa313a836b2d3667640f875d5"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f25d52c97dbca6ebea786f0961b49c1998fa05178abf1964a977c825b3d8ae6"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:13c4b18f44637859f34639493efd297a08670f45e4eec34ab2dcba724e3cb5fc"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0470bff6cc24d8c2f63c80931aa239463800871609dafc6bcc9ca10f5a12a04e"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-win32.whl", hash = "sha256:3452459668bd01d646385482362b021834a31c036aa1c02acd88924ddeff7d0d"},
|
||||
{file = "pycurl-7.45.4-cp313-cp313-win_amd64.whl", hash = "sha256:fd167f73d34beb0cb8064334aee76d9bdd13167b30be6d5d36fb07d0c8223b71"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b0e38e3eb83b0c891f391853f798fc6a97cb5a86a4a731df0b6320e539ae54ae"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d192a48b3cec2e13ad432196b65c22e99620db92feae39c0476635354eff68c6"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:57971d7215fc6fdedcfc092f880a59f04f52fcaf2fd329151b931623d7b59a9c"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73df3eb5940a7fbf4cf62f7271e9f23a8e9f80e352c838ee9a8448a70c01d3f5"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-win32.whl", hash = "sha256:587a4891039803b5f48392066f97b7cd5e7e9a166187abb5cb4b4806fdb8fbef"},
|
||||
{file = "pycurl-7.45.4-cp39-cp39-win_amd64.whl", hash = "sha256:caec8b634763351dd4e1b729a71542b1e2de885d39710ba8e7202817a381b453"},
|
||||
{file = "pycurl-7.45.4.tar.gz", hash = "sha256:32c8e237069273f4260b6ae13d1e0f99daae938977016021565dc6e11050e803"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86f66d334deaaab20a576fb785587566081407adc703318203fe26e43277ef12"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:205983e87d6aa0b6e93ec7320060de44efaa905ecc5d13f70cbe38c65684c5c4"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbd4a6b8654b779089c5a44af1c65c1419c2cd60718780df6d8f354eb35d6d55"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5ebc6a0ac60c371a9efaf7d55dec5820f76fdafb43a3be1e390011339dc329ae"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2facab1c35600088cb82b5b093bd700bfbd1e3191deab24f7d1803d9dc5b76fc"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-win32.whl", hash = "sha256:7cfca02d70579853041063e53ca713d31161b8831b98d4f68c3554dc0448beec"},
|
||||
{file = "pycurl-7.45.3-cp310-cp310-win_amd64.whl", hash = "sha256:8451e8475051f16eb4776380384699cb8ddd10ea8410bcbfaee5a6fc4c046de6"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1610cc45b5bc8b39bc18b981d0473e59ef41226ee467eaa8fbfc7276603ef5af"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c854885398410fa6e88fc29f7a420a3c13b88bae9b4e10a804437b582e24f58b"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:921c9db0c3128481954f625b3b1bc10c730100aa944d54643528f716676439ee"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:483f3aa5d1bc8cff5657ad96f68e1d89281f971a7b6aa93408a31e3199981ea9"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1e0d32d6ed3a7ba13dbbd3a6fb50ca76c40c70e6bc6fe347f90677478d3422c7"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-win32.whl", hash = "sha256:beaaa4450e23d41dd0c2f2f47a4f8a171210271543550c2c556090c7eeea88f5"},
|
||||
{file = "pycurl-7.45.3-cp311-cp311-win_amd64.whl", hash = "sha256:dd33fd9de8907a6275c70113124aeb7eea672c1324f5d5423f203738b341697d"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0c41a172d5e8a5cdd8328cc8134f47b2a57960ac677f7cda8520eaa9fbe7d990"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13006b62c157bb4483c58e1abdced6df723c9399255a4f5f6bb7f8e425106679"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27f4c5c20c86a9a823677316724306fb1ce3b25ec568efd52026dc6c563e5b29"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c2c246bc29e8762ff4c8a833ac5b4da4c797d16ab138286e8aec9b0c0a0da2d4"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3d07c5daef2d0d85949e32ec254ee44232bb57febb0634194379dd14d1ff4f87"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-win32.whl", hash = "sha256:9f7afe5ef0e4750ac4515baebc251ee94aaefe5de6e2e8a24668473128d69904"},
|
||||
{file = "pycurl-7.45.3-cp312-cp312-win_amd64.whl", hash = "sha256:3648ed9a57a6b704673faeab3dc64d1469cc69f2bc1ed8227ffa0f84e147c500"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c0915ea139f66a289edc4f9de10cb45078af1bb950491c5612969864236a2e7e"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43c5e61a58783ddf78ef84949f6bb6e52e092a13ec67678e9a9e21071ecf5b80"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bf613844a1647fe3d2bba1f5c9c96a62a85280123a57a8a0c8d2f37d518bc10a"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:936afd9c5ff7fe7457065e878a279811787778f472f9a4e8c5df79e7728358e2"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:dbf816a6d0cb71e7fd06609246bbea4eaf100649d9decf49e4eb329594f70be7"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-win32.whl", hash = "sha256:2c8a2ce568193f9f84763717d8961cec0db4ec1aa08c6bcf4d90da5eb72bec86"},
|
||||
{file = "pycurl-7.45.3-cp38-cp38-win_amd64.whl", hash = "sha256:80ac7c17e69ca6b76ccccb4255f7c29a2a36e5b69eb10c2adba82135d43afe8c"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fa7751b614d9aa82d7a0f49ca90924c29c6cedf85a2f8687fb6a772dbfe48711"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b129e9ee07f80b4af957607917af46ab517b0c4e746692f6d9e50e973edba8d8"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a0f920582b8713ca87d5a288a7532607bc4454275d733fc880650d602dbe3c67"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c7c13e4268550cde14a6f4743cc8bd8c035d4cd36514d58eff70276d68954b6f"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0f0e1251a608ffd75fc502f4014442e554c67d3d7a1b0a839c35efb6ad2f8bf8"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-win32.whl", hash = "sha256:51a40a56c58e63dac6145829f9e9bd66e5867a9f0741bcb9ffefab619851d44f"},
|
||||
{file = "pycurl-7.45.3-cp39-cp39-win_amd64.whl", hash = "sha256:e08a06802c8c8a9d04cf3319f9230ec09062c55d2550bd48f8ada1df1431adcf"},
|
||||
{file = "pycurl-7.45.3.tar.gz", hash = "sha256:8c2471af9079ad798e1645ec0b0d3d4223db687379d17dd36a70637449f81d6b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4095,13 +4105,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "5.2.1"
|
||||
version = "5.2.0"
|
||||
description = "Python client for Redis database and key-value store"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
|
||||
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
|
||||
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
|
||||
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4213,114 +4223,114 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.22.3"
|
||||
version = "0.22.1"
|
||||
description = "Python bindings to Rust's persistent data structures (rpds)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"},
|
||||
{file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"},
|
||||
{file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"},
|
||||
{file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"},
|
||||
{file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"},
|
||||
{file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"},
|
||||
{file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"},
|
||||
{file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"},
|
||||
{file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ab27dd4edd84b13309f268ffcdfc07aef8339135ffab7b6d43f16884307a2a48"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9d5b925156a746dc1f5f52376fdd1fbdd3f6ffe1fcd6f5e06f77ca79abb940a3"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201650b309c419143775c15209c620627de3c09a27c7fb58375325aec5cce260"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31264187fc934ff1024a4f56775f33c9252d3f4f3e27ec07d1995a26b52702c3"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c5ffe47ccf92d8b17e10f8a5ce28d015aa1196edc3359684cf31504eae6a14"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9ac7280bd045f472b50306d7efeee051b69e3a2dd1b90f46bd7e86e63b1efa2"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f941fb86195f97be7f6efe04a21b223f05dfe4d1dfb159999e2f8d101e44cc4"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f91bfc39f7a64168e08ab831fa497ec5438c1d6c6e2f9e12848d95ad11ac8523"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:effcae2152afe7937a28376dbabb25c770ef99ed4e16a4ffeb8e6a4f7c4f06aa"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2177e59c033bf0d1bf7de1ced561205963583caf3242c6c700a723034bfb5f8e"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66f4f48a89cdd30ab3a47335df81c76e9a63799d0d84b29c0618371c66fa37b0"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-win32.whl", hash = "sha256:b07fa9e634234e84096adfa4be3828c8f26e238679c122824b2b3d7131bec578"},
|
||||
{file = "rpds_py-0.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:ca4657e9fd0b1b5376942d403d634ce188f79064f0873aa853ab05b10185ceec"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:608c84699b2db09c6a8743845b1a3dad36fae53eaaecb241d45b13dff74405fb"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dae4eb9b5534e09ba6c6ab496a757e5e394b7e7b08767d25ca37e8d36491114"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a1f000c5f6e08b298275bae00921e9fbbf2a35dae0a86db2821c058c2201a9"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:580ccbf11f02f948add4cb641843030a89f1463d7c0740cbfc9aca91e9dc34b3"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96559e05bdf938b2048353e10a7920b98f853cefe4482c2064a718d7d0a50bd7"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128cbaed7ba26116820bcb992405d6a13ea18c8fca1b8c4f59906d858e91e979"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:734783dd7da58f76222f458346ddebdb3621686a1a2a667db5049caf0c9956b9"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c9ce6b83597d45bec44a2690857ede62fc98223772135f8a7fa90884eb726501"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bca4428c4a957b78ded3e6e62884ab03f029dce8fa8d34818da0f80f61332b49"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ded65691a1d3fd7d2aa89d2c91aa51f941601bb2ce099739909034d957fef4b"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72407065ad459db9f3d052ea8c51e02534f02533fc61e51cbab3bd94166f086c"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-win32.whl", hash = "sha256:eb013aa01b404219f28dc973d9e6310fd4db216d7299253dd355629952e0564e"},
|
||||
{file = "rpds_py-0.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:8bd9ec1db79a664f4cbb12878693b73416f4d2cb425d3e27eccc1bdfbdc826ef"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8ec41049c90d204a6561238a9ad6c7263ebb7009d9759c98b58078d9d2fec9ba"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:102be79c4cc47a4aeb5912401185c404cd2601c15a7163bbecff7f1bfe20b669"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a603155db408f773637f9e3a712c6e3cbc521aaa8fa2b99f9ba6106c59a2496"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dbff9402c2bdf00bf0df9905694b3c292a3847c725651938a72f554351a5fcb"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96b3759d8ab2323324e0a92b2f44834f9d88089b8d1ab6f533b61f4be3411cef"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3029f481b31f329b1fdb4ec4b56935d82210ddd9c6f86ea5a87c06f1e97b161"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d280b4bf09f719b89fd9aab3b71067acc0d0449b7d1eba99a2ade4939cef8296"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8e97e19aa7b0b0d801a159f932ce4435f1049c8c38e2bb372bb5bee559ce50"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:50e4b5d291105f7063259fe0125b1af902fb34499444d7c5c521dd8328b00939"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d3777c446bb1c5fcd82dc3f8776e1a146cd91e80cc1892f8634575ace438d22f"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:447ae1104fb32197b9262f772d565d38e834cc2e9edd89350b37b88fed636e70"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-win32.whl", hash = "sha256:55d371b9d8b0c2a68a50413a8cb01c3c3ce1ea4f768bf77b66669a9a486e101e"},
|
||||
{file = "rpds_py-0.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:413a30a99d8683dace3765885920ed27ab662efbb6c98d81db76c397ad1ffd71"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa2ba0176037c915d8660a4e46581d645e2c22b5373e466bc8640a794d45861a"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4ba6c66fbc6015b2f99e7176fec41793cecb00c4cc357cad038dff85e6ac42ab"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15fa4ca658f8ad22645d3531682b17e5580832efbfa87304c3e62214c79c1e8a"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7833ef6f5d6cb634f296abfd93452fb3eb44c4e9a6ae95c1021eab704c1cee2"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0467838c90435b80793cde486a318fc916ee57f2af54e4b10c72b20cbdcbaa9"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d962e2e89b3a95e3597a34b8c93ced1e98958502c5b8096c9fd69deff279f561"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ce729f1dc8a4a190c34b69f75377bddc004079b2963ab722ab91fafe040be6d"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8080467df22feca0fc9c46567001777c6fbc2b4a2683a7137420896051874ca1"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f9eb37d3a60b262a98ab51ee899cac039de9ca0ce68dcf1a6518a09719020b0"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:153248f48d6f90a295a502f53ec544a3ffbd21b0bb32f5dca39c4b93a764d6a2"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a53592cdf98cec3dfcdb24ffec8a4797e7656b65700099af43ec7df023b6de4"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-win32.whl", hash = "sha256:e8056adcefa2dcb67e8bc91ea5eee26df66e8b297a8cd6ff0903f85c70908fa0"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:a451dba533be77454ebcffc85189108fc05f279100835ac76e7989edacb89156"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:2ea23f1525d4f64286dbe0947c929d45c3ffe963b2dbed1d3844a2e4938bda42"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3aaa22487477de9618ce3b37f99fbe81219ba96f3c2ca84f576f0ab451b83aba"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8954b9ffe60f479a0c0ba40987db2546c735ab02a725ea7fd89342152d4d821d"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8502a02ae3ae67084f5a0bf5a8253b19fa7a887f824e41e016cdb0ac532a06f"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a083221b6a4ecdef38a60c95d8d3223d99449cb4da2544e9644958dc16664eb9"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:542eb246d5be31b5e0a9c8ddb9539416f9b31f58f75bd4ee328bff2b5c58d6fd"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffae97d28ea4f2c613a751d087b75a97fb78311b38cc2e9a2f4587e473ace167"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0ff8d5b13ce2357fa8b33a0a2e3775aa71df5bf7c8ba060634c9d15ab12f357"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f057a0c546c42964836b209d8de9ea1a4f4b0432006c6343cbe633d8ca14571"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:48ee97c7c6027fd423058675b5a39d0b5f7a1648250b671563d5c9f74ff13ff0"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:babec324e8654a59122aaa66936a9a483faa03276db9792f51332475c2dddc4a"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-win32.whl", hash = "sha256:e69acdbc132c9592c8dc393af85e38e206ca847c7019a953ff625191c3a12312"},
|
||||
{file = "rpds_py-0.22.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c783e4ed68200f4e03c125690d23158b1c49c4b186d458a18debc109bbdc3c2e"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2143c3aed85992604d758bbe67da839fb4aab3dd2e1c6dddab5b3ca7162b34a2"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f57e2d0f8022783426121b586d7c842ea40ea832a29e28ca36c881b54c74fb28"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0c324879d483504b07f7b18eb1b50567c434263bbe4866ecce33056162668a"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c40e02cc4f3e18fd39344edb10eebe04bd11cfd13119606b5771e5ea51630d3"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f76c6f319e57007ad52e671ec741d801324760a377e3d4992c9bb8200333ebac"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5cae9b415ea8a6a563566dbf46650222eccc5971c7daa16fbee63aef92ae543"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b09209cdfcacf5eba9cf80367130532e6c02e695252e1f64d3cfcc2356e6e19f"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbe428d0ac6eacaf05402adbaf137f59ad6063848182d1ff294f95ce0f24005b"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:626b9feb01bff049a5aec4804f0c58db12585778b4902e5376a95b01f80a7a16"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec1ccc2a9f764cd632fb8ab28fdde166250df54fc8d97315a4a6948dc5367639"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ef92b1fbe6aa2e7885eb90853cc016b1fc95439a8cc8da6d526880e9e2148695"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-win32.whl", hash = "sha256:c88535f83f7391cf3a45af990237e3939a6fdfbedaed2571633bfdd0bceb36b0"},
|
||||
{file = "rpds_py-0.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:7839b7528faa4d134c183b1f2dd1ee4dc2ca2f899f4f0cfdf00fc04c255262a7"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a0ed14a4162c2c2b21a162c9fcf90057e3e7da18cd171ab344c1e1664f75090e"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fdeae9010533e47715c37df83264df0122584e40d691d50cf3607c060952a3"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4659b2e4a5008715099e216050f5c6976e5a4329482664411789968b82e3f17d"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a18aedc032d6468b73ebbe4437129cb30d54fe543cde2f23671ecad76c3aea24"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149b4d875ef9b12a8f5e303e86a32a58f8ef627e57ec97a7d0e4be819069d141"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdaee3947eaaa52dae3ceb9d9f66329e13d8bae35682b1e5dd54612938693934"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ce951800ed2acc6772fd9f42150f29d567f0423989748052fdb39d9e2b5795"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab784621d3e2a41916e21f13a483602cc989fd45fff637634b9231ba43d4383b"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c2a214bf5b79bd39a9de1c991353aaaacafda83ba1374178309e92be8e67d411"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:85060e96953647871957d41707adb8d7bff4e977042fd0deb4fc1881b98dd2fe"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c6f3fd617db422c9d4e12cb8d84c984fe07d6d9cb0950cbf117f3bccc6268d05"},
|
||||
{file = "rpds_py-0.22.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f2d1b58a0c3a73f0361759642e80260a6d28eee6501b40fe25b82af33ef83f21"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:76eaa4c087a061a2c8a0a92536405069878a8f530c00e84a9eaf332e70f5561f"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:959ae04ed30cde606f3a0320f0a1f4167a107e685ef5209cce28c5080590bd31"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:198067aa6f3d942ff5d0d655bb1e91b59ae85279d47590682cba2834ac1b97d2"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e7e99e2af59c56c59b6c964d612511b8203480d39d1ef83edc56f2cb42a3f5d"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0545928bdf53dfdfcab284468212efefb8a6608ca3b6910c7fb2e5ed8bdc2dc0"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef7282d8a14b60dd515e47060638687710b1d518f4b5e961caad43fb3a3606f9"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3f245c2f39a5692d9123c174bc48f6f9fe3e96407e67c6d04541a767d99e72"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efb2ad60ca8637d5f9f653f9a9a8d73964059972b6b95036be77e028bffc68a3"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d8306f27418361b788e3fca9f47dec125457f80122e7e31ba7ff5cdba98343f8"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:4c8dc7331e8cbb1c0ea2bcb550adb1777365944ffd125c69aa1117fdef4887f5"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:776a06cb5720556a549829896a49acebb5bdd96c7bba100191a994053546975a"},
|
||||
{file = "rpds_py-0.22.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e4f91d702b9ce1388660b3d4a28aa552614a1399e93f718ed0dacd68f23b3d32"},
|
||||
{file = "rpds_py-0.22.1.tar.gz", hash = "sha256:157a023bded0618a1eea54979fe2e0f9309e9ddc818ef4b8fc3b884ff38fedd5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4562,13 +4572,13 @@ XlsxWriter = "*"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4598,13 +4608,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.3"
|
||||
version = "0.5.2"
|
||||
description = "A non-validating SQL parser."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"},
|
||||
{file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"},
|
||||
{file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"},
|
||||
{file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -4699,13 +4709,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
version = "0.15.0"
|
||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"},
|
||||
{file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"},
|
||||
{file = "typer-0.15.0-py3-none-any.whl", hash = "sha256:bd16241db7e0f989ce1a0d8faa5aa1e43b9b9ac3fd1d4b8bcff91503d6717e38"},
|
||||
{file = "typer-0.15.0.tar.gz", hash = "sha256:8995452a598922ed8d8ad8c06ca63a218881ab601f5fa6fb0c511f7776497c7e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5061,4 +5071,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "0793fc3f1c84f4b94253b3e6cf12bb12fb872d93fc62c4903da6ef097add3530"
|
||||
content-hash = "b507ad23e204aaefd60ddc6f95d3c6c4857915a519b5d06a2c36e56f24c42095"
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ drf-nested-routers = "^0.94.1"
|
||||
drf-spectacular = "0.27.2"
|
||||
drf-spectacular-jsonapi = "0.5.1"
|
||||
gunicorn = "23.0.0"
|
||||
prowler = "^5.0"
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", tag = "5.0.0"}
|
||||
psycopg2-binary = "2.9.9"
|
||||
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
|
||||
# Needed for prowler compatibility
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from django.db import transaction
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import connection, transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework_json_api import filters
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from rest_framework_json_api.views import ModelViewSet
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
|
||||
from api.filters import CustomDjangoFilterBackend
|
||||
from api.models import Role, Tenant
|
||||
from api.db_router import MainRouter
|
||||
|
||||
|
||||
class BaseViewSet(ModelViewSet):
|
||||
@@ -45,7 +50,13 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -58,7 +69,39 @@ 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):
|
||||
if (
|
||||
@@ -67,7 +110,8 @@ class BaseTenantViewset(BaseViewSet):
|
||||
):
|
||||
user_id = str(request.user.id)
|
||||
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
# TODO: DRY this when we have time
|
||||
@@ -78,7 +122,13 @@ class BaseTenantViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -99,6 +149,12 @@ class BaseUserViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import secrets
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -9,7 +8,6 @@ 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 = (
|
||||
@@ -25,8 +23,6 @@ TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
|
||||
POSTGRES_TENANT_VAR = "api.tenant_id"
|
||||
POSTGRES_USER_VAR = "api.user_id"
|
||||
|
||||
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def psycopg_connection(database_alias: str):
|
||||
@@ -48,23 +44,10 @@ def psycopg_connection(database_alias: str):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
|
||||
"""
|
||||
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
|
||||
if the value is a valid UUID.
|
||||
|
||||
Args:
|
||||
value (str): Database configuration parameter value.
|
||||
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
|
||||
"""
|
||||
def tenant_transaction(tenant_id: str):
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# just in case the value is an UUID object
|
||||
uuid.UUID(str(value))
|
||||
except ValueError:
|
||||
raise ValidationError("Must be a valid UUID")
|
||||
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
yield cursor
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from django.db import connection, transaction
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
|
||||
|
||||
def set_tenant(func):
|
||||
@@ -35,7 +31,7 @@ def set_tenant(func):
|
||||
pass
|
||||
|
||||
# When calling the task
|
||||
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
|
||||
some_task.delay(arg1, tenant_id="1234-abcd-5678")
|
||||
|
||||
# The tenant context will be set before the task logic executes.
|
||||
"""
|
||||
@@ -47,12 +43,9 @@ def set_tenant(func):
|
||||
tenant_id = kwargs.pop("tenant_id")
|
||||
except KeyError:
|
||||
raise KeyError("This task requires the tenant_id")
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -22,13 +22,10 @@ from api.db_utils import (
|
||||
StatusEnumField,
|
||||
)
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
@@ -36,6 +33,10 @@ from api.models import (
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
Role,
|
||||
ComplianceOverview,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
@@ -481,6 +482,43 @@ 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 = CharFilter(method="filter_permission_state")
|
||||
|
||||
def filter_permission_state(self, queryset, name, value):
|
||||
permission_fields = [
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"manage_scans",
|
||||
]
|
||||
|
||||
q_all_true = Q(**{field: True for field in permission_fields})
|
||||
q_all_false = Q(**{field: False for field in permission_fields})
|
||||
|
||||
if value == "unlimited":
|
||||
return queryset.filter(q_all_true)
|
||||
elif value == "none":
|
||||
return queryset.filter(q_all_false)
|
||||
elif value == "limited":
|
||||
return queryset.exclude(q_all_true | q_all_false)
|
||||
else:
|
||||
return queryset.none()
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
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)
|
||||
|
||||
@@ -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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -552,7 +552,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddConstraint(
|
||||
model_name="providergroupmembership",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("provider_id", "provider_group"),
|
||||
fields=("provider_id", "provider_group_id"),
|
||||
name="unique_provider_group_membership",
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-05 12:29
|
||||
|
||||
import api.rls
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0002_token_migrations"),
|
||||
]
|
||||
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+118
-15
@@ -294,29 +294,20 @@ class ProviderGroup(RowLevelSecurityProtectedModel):
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-groups"
|
||||
resource_name = "provider-group"
|
||||
|
||||
|
||||
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"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["provider_id", "provider_group"],
|
||||
fields=["provider_id", "provider_group_id"],
|
||||
name="unique_provider_group_membership",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
@@ -327,7 +318,7 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel):
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-group-memberships"
|
||||
resource_name = "provider_groups-provider"
|
||||
|
||||
|
||||
class Task(RowLevelSecurityProtectedModel):
|
||||
@@ -851,6 +842,118 @@ 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"
|
||||
)
|
||||
|
||||
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 = "role"
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from config.django.base import DISABLE_RBAC
|
||||
|
||||
from enum import Enum
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
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):
|
||||
# This is for testing/demo purposes only
|
||||
if DISABLE_RBAC:
|
||||
return True
|
||||
|
||||
required_permissions = getattr(view, "required_permissions", [])
|
||||
if not required_permissions:
|
||||
return True
|
||||
|
||||
user_roles = request.user.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
|
||||
@@ -2,7 +2,7 @@ from contextlib import nullcontext
|
||||
|
||||
from rest_framework_json_api.renderers import JSONRenderer
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.db_utils import tenant_transaction
|
||||
|
||||
|
||||
class APIJSONRenderer(JSONRenderer):
|
||||
@@ -13,9 +13,9 @@ class APIJSONRenderer(JSONRenderer):
|
||||
tenant_id = getattr(request, "tenant_id", None) if request else None
|
||||
include_param_present = "include" in request.query_params if request else False
|
||||
|
||||
# Use rls_transaction if needed for included resources, otherwise do nothing
|
||||
# Use tenant_transaction if needed for included resources, otherwise do nothing
|
||||
context_manager = (
|
||||
rls_transaction(tenant_id)
|
||||
tenant_transaction(tenant_id)
|
||||
if tenant_id and include_param_present
|
||||
else nullcontext()
|
||||
)
|
||||
|
||||
+1490
-43
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ from conftest import TEST_USER, TEST_PASSWORD, get_api_tokens, get_authorization
|
||||
def test_check_resources_between_different_tenants(
|
||||
schedule_mock,
|
||||
enforce_test_user_db_connection,
|
||||
patch_testing_flag,
|
||||
authenticated_api_client,
|
||||
tenants_fixture,
|
||||
):
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# TODO: Enable this tests
|
||||
|
||||
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_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
|
||||
|
||||
@patch("api.db_router.MainRouter.admin_db", new="default")
|
||||
def test_create_user_with_all_permissions(self, authenticated_client_rbac):
|
||||
valid_user_payload = {
|
||||
"name": "test",
|
||||
"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"
|
||||
|
||||
@patch("api.db_router.MainRouter.admin_db", new="default")
|
||||
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
|
||||
@@ -1,9 +1,7 @@
|
||||
import uuid
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import patch, call
|
||||
|
||||
import pytest
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
from api.decorators import set_tenant
|
||||
|
||||
|
||||
@@ -17,12 +15,12 @@ class TestSetTenantDecorator:
|
||||
def random_func(arg):
|
||||
return arg
|
||||
|
||||
tenant_id = str(uuid.uuid4())
|
||||
tenant_id = "1234-abcd-5678"
|
||||
|
||||
result = random_func("test_arg", tenant_id=tenant_id)
|
||||
|
||||
assert (
|
||||
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
call(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
in mock_cursor.execute.mock_calls
|
||||
)
|
||||
assert result == "test_arg"
|
||||
|
||||
@@ -9,11 +9,14 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from api.models import (
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
Invitation,
|
||||
UserRoleRelationship,
|
||||
ProviderSecret,
|
||||
Scan,
|
||||
StateChoices,
|
||||
@@ -24,6 +27,14 @@ from api.rls import Tenant
|
||||
TODAY = str(datetime.today().date())
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def enable_testing_flag(patch_testing_flag):
|
||||
"""
|
||||
Automatically applies the patch_testing_flag fixture to all tests in this file.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserViewSet:
|
||||
def test_users_list(self, authenticated_client, create_test_user):
|
||||
@@ -1200,7 +1211,7 @@ class TestProviderGroupViewSet:
|
||||
def test_provider_group_create(self, authenticated_client):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-groups",
|
||||
"type": "provider-group",
|
||||
"attributes": {
|
||||
"name": "Test Provider Group",
|
||||
},
|
||||
@@ -1219,7 +1230,7 @@ class TestProviderGroupViewSet:
|
||||
def test_provider_group_create_invalid(self, authenticated_client):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-groups",
|
||||
"type": "provider-group",
|
||||
"attributes": {
|
||||
# Name is missing
|
||||
},
|
||||
@@ -1241,7 +1252,7 @@ class TestProviderGroupViewSet:
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(provider_group.id),
|
||||
"type": "provider-groups",
|
||||
"type": "provider-group",
|
||||
"attributes": {
|
||||
"name": "Updated Provider Group Name",
|
||||
},
|
||||
@@ -1263,7 +1274,7 @@ class TestProviderGroupViewSet:
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(provider_group.id),
|
||||
"type": "provider-groups",
|
||||
"type": "provider-group",
|
||||
"attributes": {
|
||||
"name": "", # Invalid name
|
||||
},
|
||||
@@ -1294,100 +1305,6 @@ class TestProviderGroupViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_provider_group_providers_update(
|
||||
self, authenticated_client, provider_groups_fixture, providers_fixture
|
||||
):
|
||||
provider_group = provider_groups_fixture[0]
|
||||
provider_ids = [str(provider.id) for provider in providers_fixture]
|
||||
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-group-memberships",
|
||||
"id": str(provider_group.id),
|
||||
"attributes": {"provider_ids": provider_ids},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.put(
|
||||
reverse("providergroup-providers", kwargs={"pk": provider_group.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
memberships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group
|
||||
)
|
||||
assert memberships.count() == len(provider_ids)
|
||||
for membership in memberships:
|
||||
assert str(membership.provider_id) in provider_ids
|
||||
|
||||
def test_provider_group_providers_update_non_existent_provider(
|
||||
self, authenticated_client, provider_groups_fixture, providers_fixture
|
||||
):
|
||||
provider_group = provider_groups_fixture[0]
|
||||
provider_ids = [str(provider.id) for provider in providers_fixture]
|
||||
provider_ids[-1] = "1b59e032-3eb6-4694-93a5-df84cd9b3ce2"
|
||||
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-group-memberships",
|
||||
"id": str(provider_group.id),
|
||||
"attributes": {"provider_ids": provider_ids},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.put(
|
||||
reverse("providergroup-providers", kwargs={"pk": provider_group.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert (
|
||||
errors[0]["detail"]
|
||||
== f"The following provider IDs do not exist: {provider_ids[-1]}"
|
||||
)
|
||||
|
||||
def test_provider_group_providers_update_invalid_provider(
|
||||
self, authenticated_client, provider_groups_fixture
|
||||
):
|
||||
provider_group = provider_groups_fixture[1]
|
||||
invalid_provider_id = "non-existent-id"
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-group-memberships",
|
||||
"id": str(provider_group.id),
|
||||
"attributes": {"provider_ids": [invalid_provider_id]},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.put(
|
||||
reverse("providergroup-providers", kwargs={"pk": provider_group.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["detail"] == "Must be a valid UUID."
|
||||
|
||||
def test_provider_group_providers_update_invalid_payload(
|
||||
self, authenticated_client, provider_groups_fixture
|
||||
):
|
||||
provider_group = provider_groups_fixture[2]
|
||||
data = {
|
||||
# Missing "provider_ids"
|
||||
}
|
||||
|
||||
response = authenticated_client.put(
|
||||
reverse("providergroup-providers", kwargs={"pk": provider_group.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["detail"] == "Received document does not contain primary data"
|
||||
|
||||
def test_provider_group_retrieve_not_found(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("providergroup-detail", kwargs={"pk": "non-existent-id"})
|
||||
@@ -2652,7 +2569,9 @@ class TestInvitationViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_invitations_create_valid(self, authenticated_client, create_test_user):
|
||||
def test_invitations_create_valid(
|
||||
self, authenticated_client, create_test_user, roles_fixture
|
||||
):
|
||||
user = create_test_user
|
||||
data = {
|
||||
"data": {
|
||||
@@ -2661,6 +2580,11 @@ class TestInvitationViewSet:
|
||||
"email": "any_email@prowler.com",
|
||||
"expires_at": self.TOMORROW_ISO,
|
||||
},
|
||||
"relationships": {
|
||||
"roles": {
|
||||
"data": [{"type": "role", "id": str(roles_fixture[0].id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
@@ -2719,6 +2643,11 @@ class TestInvitationViewSet:
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/email"
|
||||
)
|
||||
assert response.json()["errors"][1]["code"] == "required"
|
||||
assert (
|
||||
response.json()["errors"][1]["source"]["pointer"]
|
||||
== "/data/relationships/roles"
|
||||
)
|
||||
|
||||
def test_invitations_create_invalid_expires_at(
|
||||
self, authenticated_client, invitations_fixture
|
||||
@@ -2745,6 +2674,11 @@ class TestInvitationViewSet:
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/expires_at"
|
||||
)
|
||||
assert response.json()["errors"][1]["code"] == "required"
|
||||
assert (
|
||||
response.json()["errors"][1]["source"]["pointer"]
|
||||
== "/data/relationships/roles"
|
||||
)
|
||||
|
||||
def test_invitations_partial_update_valid(
|
||||
self, authenticated_client, invitations_fixture
|
||||
@@ -2983,7 +2917,6 @@ class TestInvitationViewSet:
|
||||
response = authenticated_client.post(
|
||||
reverse("invitation-accept"), data=data, format="json"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
invitation.refresh_from_db()
|
||||
assert Membership.objects.filter(
|
||||
@@ -3166,6 +3099,596 @@ class TestInvitationViewSet:
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRoleViewSet:
|
||||
def test_role_list(self, authenticated_client, roles_fixture):
|
||||
response = authenticated_client.get(reverse("role-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(roles_fixture)
|
||||
|
||||
def test_role_retrieve(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[0]
|
||||
response = authenticated_client.get(
|
||||
reverse("role-detail", kwargs={"pk": role.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data["id"] == str(role.id)
|
||||
assert data["attributes"]["name"] == role.name
|
||||
|
||||
def test_role_create(self, authenticated_client):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "role",
|
||||
"attributes": {
|
||||
"name": "Test Role",
|
||||
"manage_users": "false",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "false",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "true",
|
||||
"manage_scans": "true",
|
||||
"unlimited_visibility": "true",
|
||||
},
|
||||
"relationships": {"provider_groups": {"data": []}},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("role-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
response_data = response.json()["data"]
|
||||
assert response_data["attributes"]["name"] == "Test Role"
|
||||
assert Role.objects.filter(name="Test Role").exists()
|
||||
|
||||
def test_role_provider_groups_create(
|
||||
self, authenticated_client, provider_groups_fixture
|
||||
):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "role",
|
||||
"attributes": {
|
||||
"name": "Test Role",
|
||||
"manage_users": "false",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "false",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "true",
|
||||
"manage_scans": "true",
|
||||
"unlimited_visibility": "true",
|
||||
},
|
||||
"relationships": {
|
||||
"provider_groups": {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_group.id)}
|
||||
for provider_group in provider_groups_fixture[:2]
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("role-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
response_data = response.json()["data"]
|
||||
assert response_data["attributes"]["name"] == "Test Role"
|
||||
assert Role.objects.filter(name="Test Role").exists()
|
||||
relationships = (
|
||||
Role.objects.filter(name="Test Role").first().provider_groups.all()
|
||||
)
|
||||
assert relationships.count() == 2
|
||||
for relationship in relationships:
|
||||
assert relationship.id in [pg.id for pg in provider_groups_fixture[:2]]
|
||||
|
||||
def test_role_create_invalid(self, authenticated_client):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "role",
|
||||
"attributes": {
|
||||
# Name is missing
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("role-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
|
||||
|
||||
def test_role_partial_update(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[1]
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(role.id),
|
||||
"type": "role",
|
||||
"attributes": {
|
||||
"name": "Updated Provider Group Name",
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("role-detail", kwargs={"pk": role.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
role.refresh_from_db()
|
||||
assert role.name == "Updated Provider Group Name"
|
||||
|
||||
def test_role_partial_update_invalid(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[2]
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(role.id),
|
||||
"type": "role",
|
||||
"attributes": {
|
||||
"name": "", # Invalid name
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("role-detail", kwargs={"pk": role.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
|
||||
|
||||
def test_role_destroy(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[2]
|
||||
response = authenticated_client.delete(
|
||||
reverse("role-detail", kwargs={"pk": role.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert not Role.objects.filter(id=role.id).exists()
|
||||
|
||||
def test_role_destroy_invalid(self, authenticated_client):
|
||||
response = authenticated_client.delete(
|
||||
reverse("role-detail", kwargs={"pk": "non-existent-id"})
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_role_retrieve_not_found(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("role-detail", kwargs={"pk": "non-existent-id"})
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_role_list_filters(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[0]
|
||||
response = authenticated_client.get(
|
||||
reverse("role-list"), {"filter[name]": role.name}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["attributes"]["name"] == role.name
|
||||
|
||||
def test_role_list_sorting(self, authenticated_client, roles_fixture):
|
||||
response = authenticated_client.get(reverse("role-list"), {"sort": "name"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
names = [item["attributes"]["name"] for item in data]
|
||||
assert names == sorted(names)
|
||||
|
||||
def test_role_invalid_method(self, authenticated_client):
|
||||
response = authenticated_client.put(reverse("role-list"))
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserRoleRelationshipViewSet:
|
||||
def test_create_relationship(
|
||||
self, authenticated_client, roles_fixture, create_test_user
|
||||
):
|
||||
data = {
|
||||
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
|
||||
assert relationships.count() == 2
|
||||
for relationship in relationships[1:]: # Skip admin role
|
||||
assert relationship.role.id in [r.id for r in roles_fixture[:2]]
|
||||
|
||||
def test_create_relationship_already_exists(
|
||||
self, authenticated_client, roles_fixture, create_test_user
|
||||
):
|
||||
data = {
|
||||
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
|
||||
}
|
||||
authenticated_client.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "role", "id": str(roles_fixture[0].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]["detail"]
|
||||
assert "already associated" in errors
|
||||
|
||||
def test_partial_update_relationship(
|
||||
self, authenticated_client, roles_fixture, create_test_user
|
||||
):
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "role", "id": str(roles_fixture[1].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
|
||||
assert relationships.count() == 1
|
||||
assert {rel.role.id for rel in relationships} == {roles_fixture[1].id}
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "role", "id": str(roles_fixture[1].id)},
|
||||
{"type": "role", "id": str(roles_fixture[2].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
|
||||
assert relationships.count() == 2
|
||||
assert {rel.role.id for rel in relationships} == {
|
||||
roles_fixture[1].id,
|
||||
roles_fixture[2].id,
|
||||
}
|
||||
|
||||
def test_destroy_relationship(
|
||||
self, authenticated_client, roles_fixture, create_test_user
|
||||
):
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(role=roles_fixture[0].id)
|
||||
assert relationships.count() == 0
|
||||
|
||||
def test_invalid_provider_group_id(self, authenticated_client, create_test_user):
|
||||
invalid_id = "non-existent-id"
|
||||
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
|
||||
response = authenticated_client.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"][0]["detail"]
|
||||
assert "valid UUID" in errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRoleProviderGroupRelationshipViewSet:
|
||||
def test_create_relationship(
|
||||
self, authenticated_client, roles_fixture, provider_groups_fixture
|
||||
):
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_group.id)}
|
||||
for provider_group in provider_groups_fixture[:2]
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[0].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = RoleProviderGroupRelationship.objects.filter(
|
||||
role=roles_fixture[0].id
|
||||
)
|
||||
assert relationships.count() == 2
|
||||
for relationship in relationships:
|
||||
assert relationship.provider_group.id in [
|
||||
pg.id for pg in provider_groups_fixture[:2]
|
||||
]
|
||||
|
||||
def test_create_relationship_already_exists(
|
||||
self, authenticated_client, roles_fixture, provider_groups_fixture
|
||||
):
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_group.id)}
|
||||
for provider_group in provider_groups_fixture[:2]
|
||||
]
|
||||
}
|
||||
authenticated_client.post(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[0].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_groups_fixture[0].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[0].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]["detail"]
|
||||
assert "already associated" in errors
|
||||
|
||||
def test_partial_update_relationship(
|
||||
self, authenticated_client, roles_fixture, provider_groups_fixture
|
||||
):
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[2].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = RoleProviderGroupRelationship.objects.filter(
|
||||
role=roles_fixture[2].id
|
||||
)
|
||||
assert relationships.count() == 1
|
||||
assert {rel.provider_group.id for rel in relationships} == {
|
||||
provider_groups_fixture[1].id
|
||||
}
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
|
||||
{"type": "provider-group", "id": str(provider_groups_fixture[2].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[2].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = RoleProviderGroupRelationship.objects.filter(
|
||||
role=roles_fixture[2].id
|
||||
)
|
||||
assert relationships.count() == 2
|
||||
assert {rel.provider_group.id for rel in relationships} == {
|
||||
provider_groups_fixture[1].id,
|
||||
provider_groups_fixture[2].id,
|
||||
}
|
||||
|
||||
def test_destroy_relationship(
|
||||
self, authenticated_client, roles_fixture, provider_groups_fixture
|
||||
):
|
||||
response = authenticated_client.delete(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[0].id}
|
||||
),
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = RoleProviderGroupRelationship.objects.filter(
|
||||
role=roles_fixture[0].id
|
||||
)
|
||||
assert relationships.count() == 0
|
||||
|
||||
def test_invalid_provider_group_id(self, authenticated_client, roles_fixture):
|
||||
invalid_id = "non-existent-id"
|
||||
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[1].id}
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"][0]["detail"]
|
||||
assert "valid UUID" in errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderGroupMembershipViewSet:
|
||||
def test_create_relationship(
|
||||
self, authenticated_client, providers_fixture, provider_groups_fixture
|
||||
):
|
||||
provider_group, *_ = provider_groups_fixture
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(provider.id)}
|
||||
for provider in providers_fixture[:2]
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group.id
|
||||
)
|
||||
assert relationships.count() == 2
|
||||
for relationship in relationships:
|
||||
assert relationship.provider.id in [p.id for p in providers_fixture[:2]]
|
||||
|
||||
def test_create_relationship_already_exists(
|
||||
self, authenticated_client, providers_fixture, provider_groups_fixture
|
||||
):
|
||||
provider_group, *_ = provider_groups_fixture
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(provider.id)}
|
||||
for provider in providers_fixture[:2]
|
||||
]
|
||||
}
|
||||
authenticated_client.post(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(providers_fixture[0].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]["detail"]
|
||||
assert "already associated" in errors
|
||||
|
||||
def test_partial_update_relationship(
|
||||
self, authenticated_client, providers_fixture, provider_groups_fixture
|
||||
):
|
||||
provider_group, *_ = provider_groups_fixture
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(providers_fixture[1].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group.id
|
||||
)
|
||||
assert relationships.count() == 1
|
||||
assert {rel.provider.id for rel in relationships} == {providers_fixture[1].id}
|
||||
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(providers_fixture[1].id)},
|
||||
{"type": "provider", "id": str(providers_fixture[2].id)},
|
||||
]
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group.id
|
||||
)
|
||||
assert relationships.count() == 2
|
||||
assert {rel.provider.id for rel in relationships} == {
|
||||
providers_fixture[1].id,
|
||||
providers_fixture[2].id,
|
||||
}
|
||||
|
||||
def test_destroy_relationship(
|
||||
self, authenticated_client, providers_fixture, provider_groups_fixture
|
||||
):
|
||||
provider_group, *_ = provider_groups_fixture
|
||||
data = {
|
||||
"data": [
|
||||
{"type": "provider", "id": str(provider.id)}
|
||||
for provider in providers_fixture[:2]
|
||||
]
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
response = authenticated_client.delete(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=providers_fixture[0].id
|
||||
)
|
||||
assert relationships.count() == 0
|
||||
|
||||
def test_invalid_provider_group_id(
|
||||
self, authenticated_client, provider_groups_fixture
|
||||
):
|
||||
provider_group, *_ = provider_groups_fixture
|
||||
invalid_id = "non-existent-id"
|
||||
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"provider_group-providers-relationship",
|
||||
kwargs={"pk": provider_group.id},
|
||||
),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"][0]["detail"]
|
||||
assert "valid UUID" in errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestComplianceOverviewViewSet:
|
||||
def test_compliance_overview_list_none(self, authenticated_client):
|
||||
|
||||
@@ -14,16 +14,20 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Finding,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
InvitationRoleRelationship,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
UserRoleRelationship,
|
||||
ComplianceOverview,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
@@ -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 = {
|
||||
@@ -361,31 +448,30 @@ class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
providers = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
|
||||
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},
|
||||
}
|
||||
|
||||
|
||||
@@ -406,41 +492,75 @@ class ProviderGroupUpdateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class ProviderGroupMembershipUpdateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
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
|
||||
Serializer for modifying provider_group memberships
|
||||
"""
|
||||
|
||||
provider_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of provider UUIDs to add to the group",
|
||||
providers = serializers.ListField(
|
||||
child=ProviderResourceIdentifierSerializer(),
|
||||
help_text="List of resource identifier objects representing providers.",
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant_id = self.context["tenant_id"]
|
||||
provider_ids = attrs.get("provider_ids", [])
|
||||
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")
|
||||
|
||||
existing_provider_ids = set(
|
||||
Provider.objects.filter(
|
||||
id__in=provider_ids, tenant_id=tenant_id
|
||||
).values_list("id", flat=True)
|
||||
)
|
||||
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)}"
|
||||
}
|
||||
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 super().validate(attrs)
|
||||
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
|
||||
@@ -1034,6 +1154,8 @@ class InvitationSerializer(RLSSerializer):
|
||||
Serializer for the Invitation model.
|
||||
"""
|
||||
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = [
|
||||
@@ -1043,6 +1165,7 @@ class InvitationSerializer(RLSSerializer):
|
||||
"email",
|
||||
"state",
|
||||
"token",
|
||||
"roles",
|
||||
"expires_at",
|
||||
"inviter",
|
||||
"url",
|
||||
@@ -1050,6 +1173,8 @@ class InvitationSerializer(RLSSerializer):
|
||||
|
||||
|
||||
class InvitationBaseWriteSerializer(BaseWriteSerializer):
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
def validate_email(self, value):
|
||||
user = User.objects.filter(email=value).first()
|
||||
tenant_id = self.context["tenant_id"]
|
||||
@@ -1086,31 +1211,54 @@ 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):
|
||||
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):
|
||||
roles = validated_data.pop("roles", [])
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
invitation = super().update(instance, validated_data)
|
||||
if roles:
|
||||
instance.roles.clear()
|
||||
for role in roles:
|
||||
InvitationRoleRelationship.objects.create(
|
||||
role=role, invitation=invitation, tenant_id=tenant_id
|
||||
)
|
||||
|
||||
return invitation
|
||||
|
||||
|
||||
class InvitationAcceptSerializer(RLSSerializer):
|
||||
"""Serializer for accepting an invitation."""
|
||||
@@ -1122,6 +1270,196 @@ class InvitationAcceptSerializer(RLSSerializer):
|
||||
fields = ["invitation_token"]
|
||||
|
||||
|
||||
# Roles
|
||||
|
||||
|
||||
class RoleSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
provider_groups = serializers.ResourceRelatedField(
|
||||
many=True, queryset=ProviderGroup.objects.all()
|
||||
)
|
||||
|
||||
permission_state = serializers.SerializerMethodField()
|
||||
|
||||
def get_permission_state(self, obj):
|
||||
permission_fields = [
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"manage_scans",
|
||||
]
|
||||
|
||||
values = [getattr(obj, field) for field in permission_fields]
|
||||
|
||||
if all(values):
|
||||
return "unlimited"
|
||||
elif not any(values):
|
||||
return "none"
|
||||
else:
|
||||
return "limited"
|
||||
|
||||
def validate(self, attrs):
|
||||
if Role.objects.filter(name=attrs.get("name")).exists():
|
||||
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",
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"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},
|
||||
"users": {"read_only": True},
|
||||
"url": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class RoleCreateSerializer(RoleSerializer):
|
||||
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=user,
|
||||
user=user,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
UserRoleRelationship.objects.bulk_create(through_model_instances)
|
||||
|
||||
return role
|
||||
|
||||
|
||||
class RoleUpdateSerializer(RoleSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"manage_scans",
|
||||
"unlimited_visibility",
|
||||
]
|
||||
|
||||
|
||||
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+589
-74
@@ -8,6 +8,7 @@ from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular_jsonapi.schemas.openapi import JsonApiAutoSchema
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
@@ -25,8 +26,10 @@ from rest_framework.exceptions import (
|
||||
ValidationError,
|
||||
)
|
||||
from rest_framework.generics import GenericAPIView, get_object_or_404
|
||||
from rest_framework_json_api.views import Response
|
||||
from rest_framework_json_api.views import RelationshipView, Response
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
|
||||
from tasks.beat import schedule_provider_scan
|
||||
from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
@@ -52,8 +55,12 @@ from api.filters import (
|
||||
TaskFilter,
|
||||
TenantFilter,
|
||||
UserFilter,
|
||||
RoleFilter,
|
||||
)
|
||||
from api.models import (
|
||||
StatusChoices,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
@@ -62,20 +69,27 @@ from api.models import (
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
ProviderSecret,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
Resource,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
from api.pagination import ComplianceOverviewPagination
|
||||
from api.rbac.permissions import DISABLE_RBAC, HasPermissions, Permissions
|
||||
from api.rls import Tenant
|
||||
from api.utils import validate_invitation
|
||||
from api.uuid_utils import datetime_to_uuid7
|
||||
from api.v1.serializers import (
|
||||
TokenSerializer,
|
||||
TokenRefreshSerializer,
|
||||
UserSerializer,
|
||||
UserCreateSerializer,
|
||||
UserUpdateSerializer,
|
||||
UserRoleRelationshipSerializer,
|
||||
ComplianceOverviewFullSerializer,
|
||||
ComplianceOverviewSerializer,
|
||||
FindingDynamicFilterSerializer,
|
||||
@@ -89,34 +103,39 @@ from api.v1.serializers import (
|
||||
OverviewProviderSerializer,
|
||||
OverviewSeveritySerializer,
|
||||
ProviderCreateSerializer,
|
||||
ProviderGroupMembershipUpdateSerializer,
|
||||
ProviderGroupMembershipSerializer,
|
||||
ProviderGroupSerializer,
|
||||
ProviderGroupUpdateSerializer,
|
||||
ProviderSecretCreateSerializer,
|
||||
ProviderSecretSerializer,
|
||||
ProviderSecretUpdateSerializer,
|
||||
RoleProviderGroupRelationshipSerializer,
|
||||
ProviderSerializer,
|
||||
ProviderUpdateSerializer,
|
||||
ResourceSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanSerializer,
|
||||
ScanUpdateSerializer,
|
||||
ScheduleDailyCreateSerializer,
|
||||
TaskSerializer,
|
||||
TenantSerializer,
|
||||
TokenRefreshSerializer,
|
||||
TokenSerializer,
|
||||
UserCreateSerializer,
|
||||
UserSerializer,
|
||||
UserUpdateSerializer,
|
||||
TaskSerializer,
|
||||
ScanSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanUpdateSerializer,
|
||||
ResourceSerializer,
|
||||
ProviderSecretSerializer,
|
||||
ProviderSecretUpdateSerializer,
|
||||
ProviderSecretCreateSerializer,
|
||||
RoleSerializer,
|
||||
RoleCreateSerializer,
|
||||
RoleUpdateSerializer,
|
||||
ScheduleDailyCreateSerializer,
|
||||
)
|
||||
|
||||
|
||||
CACHE_DECORATOR = cache_control(
|
||||
max_age=django_settings.CACHE_MAX_AGE,
|
||||
stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE,
|
||||
)
|
||||
|
||||
|
||||
class RelationshipViewSchema(JsonApiAutoSchema):
|
||||
def _resolve_path_parameters(self, _path_variables):
|
||||
return []
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["Token"],
|
||||
summary="Obtain a token",
|
||||
@@ -271,6 +290,26 @@ class UserViewSet(BaseUserViewset):
|
||||
filterset_class = UserFilter
|
||||
ordering = ["-date_joined"]
|
||||
ordering_fields = ["name", "email", "company_name", "date_joined", "is_active"]
|
||||
required_permissions = [Permissions.MANAGE_USERS]
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = self.get_required_permissions()
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_required_permissions(self):
|
||||
"""
|
||||
Returns the required permissions based on the request method.
|
||||
"""
|
||||
if self.action == "me":
|
||||
# No permissions required for me request
|
||||
return []
|
||||
else:
|
||||
# Require permission for the rest of the requests
|
||||
return [Permissions.MANAGE_USERS]
|
||||
|
||||
def get_queryset(self):
|
||||
# If called during schema generation, return an empty queryset
|
||||
@@ -347,11 +386,124 @@ class UserViewSet(BaseUserViewset):
|
||||
user=user, tenant=tenant, role=role
|
||||
)
|
||||
if invitation:
|
||||
# TODO: Add roles to output relationships
|
||||
user_role = []
|
||||
for role in invitation.roles.all():
|
||||
user_role.append(
|
||||
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
|
||||
user=user, role=role, tenant=invitation.tenant
|
||||
)
|
||||
)
|
||||
invitation.state = Invitation.State.ACCEPTED
|
||||
invitation.save(using=MainRouter.admin_db)
|
||||
else:
|
||||
role = Role.objects.using(MainRouter.admin_db).create(
|
||||
name="admin",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)
|
||||
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
|
||||
user=user,
|
||||
role=role,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return Response(data=UserSerializer(user).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Create a new user-roles relationship",
|
||||
description="Add a new user-roles relationship to the system by providing the required user-roles details.",
|
||||
responses={
|
||||
204: OpenApiResponse(description="Relationship created successfully"),
|
||||
400: OpenApiResponse(
|
||||
description="Bad request (e.g., relationship already exists)"
|
||||
),
|
||||
},
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Partially update a user-roles relationship",
|
||||
description="Update the user-roles relationship information without affecting other fields.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship updated successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Delete a user-roles relationship",
|
||||
description="Remove the user-roles relationship from the system by their ID.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship deleted successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
class UserRoleRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserRoleRelationshipSerializer
|
||||
resource_name = "roles"
|
||||
http_method_names = ["post", "patch", "delete"]
|
||||
schema = RelationshipViewSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
|
||||
role_ids = [item["id"] for item in request.data]
|
||||
existing_relationships = UserRoleRelationship.objects.filter(
|
||||
user=user, role_id__in=role_ids
|
||||
)
|
||||
|
||||
if existing_relationships.exists():
|
||||
return Response(
|
||||
{"detail": "One or more roles are already associated with the user."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
data={"roles": request.data},
|
||||
context={
|
||||
"user": user,
|
||||
"tenant_id": self.request.tenant_id,
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
serializer = self.get_serializer(
|
||||
instance=user,
|
||||
data={"roles": request.data},
|
||||
context={"tenant_id": self.request.tenant_id, "request": request},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
user.roles.clear()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Tenant"],
|
||||
@@ -389,6 +541,8 @@ class TenantViewSet(BaseTenantViewset):
|
||||
search_fields = ["name"]
|
||||
ordering = ["-inserted_at"]
|
||||
ordering_fields = ["name", "inserted_at", "updated_at"]
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def get_queryset(self):
|
||||
return Tenant.objects.all()
|
||||
@@ -562,66 +716,141 @@ class ProviderGroupViewSet(BaseRLSViewSet):
|
||||
queryset = ProviderGroup.objects.all()
|
||||
serializer_class = ProviderGroupSerializer
|
||||
filterset_class = ProviderGroupFilter
|
||||
http_method_names = ["get", "post", "patch", "put", "delete"]
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
ordering = ["inserted_at"]
|
||||
required_permissions = []
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = self.get_required_permissions()
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_required_permissions(self):
|
||||
"""
|
||||
Returns the required permissions based on the request method.
|
||||
"""
|
||||
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
|
||||
# No permissions required for GET requests
|
||||
return []
|
||||
else:
|
||||
# Require permission for non-GET requests
|
||||
return [Permissions.MANAGE_PROVIDERS]
|
||||
|
||||
def get_queryset(self):
|
||||
return ProviderGroup.objects.prefetch_related("providers")
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
|
||||
# Check if any of the user's roles have UNLIMITED_VISIBILITY
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all provider groups
|
||||
return ProviderGroup.objects.prefetch_related("providers")
|
||||
|
||||
# Collect provider groups associated with the user's roles
|
||||
provider_groups = (
|
||||
ProviderGroup.objects.filter(roles__in=user_roles)
|
||||
.distinct()
|
||||
.prefetch_related("providers")
|
||||
)
|
||||
|
||||
return provider_groups
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "partial_update":
|
||||
return ProviderGroupUpdateSerializer
|
||||
elif self.action == "providers":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return ProviderGroupMembershipUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
@extend_schema(
|
||||
tags=["Provider Group"],
|
||||
summary="Add providers to a provider group",
|
||||
description="Add one or more providers to an existing provider group.",
|
||||
request=ProviderGroupMembershipUpdateSerializer,
|
||||
responses={200: OpenApiResponse(response=ProviderGroupSerializer)},
|
||||
)
|
||||
@action(detail=True, methods=["put"], url_name="providers")
|
||||
def providers(self, request, pk=None):
|
||||
|
||||
@extend_schema(tags=["Provider Group"])
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
summary="Create a new provider_group-providers relationship",
|
||||
description="Add a new provider_group-providers relationship to the system by providing the required provider_group-providers details.",
|
||||
responses={
|
||||
204: OpenApiResponse(description="Relationship created successfully"),
|
||||
400: OpenApiResponse(
|
||||
description="Bad request (e.g., relationship already exists)"
|
||||
),
|
||||
},
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
summary="Partially update a provider_group-providers relationship",
|
||||
description="Update the provider_group-providers relationship information without affecting other fields.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship updated successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
summary="Delete a provider_group-providers relationship",
|
||||
description="Remove the provider_group-providers relationship from the system by their ID.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship deleted successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
class ProviderGroupProvidersRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
queryset = ProviderGroup.objects.all()
|
||||
serializer_class = ProviderGroupMembershipSerializer
|
||||
resource_name = "providers"
|
||||
http_method_names = ["post", "patch", "delete"]
|
||||
schema = RelationshipViewSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
return ProviderGroup.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
provider_group = self.get_object()
|
||||
|
||||
# Validate input data
|
||||
serializer = self.get_serializer_class()(
|
||||
data=request.data,
|
||||
context=self.get_serializer_context(),
|
||||
provider_ids = [item["id"] for item in request.data]
|
||||
existing_relationships = ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group, provider_id__in=provider_ids
|
||||
)
|
||||
|
||||
if existing_relationships.exists():
|
||||
return Response(
|
||||
{
|
||||
"detail": "One or more providers are already associated with the provider_group."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
data={"providers": request.data},
|
||||
context={
|
||||
"provider_group": provider_group,
|
||||
"tenant_id": self.request.tenant_id,
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
provider_ids = serializer.validated_data["provider_ids"]
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Update memberships
|
||||
ProviderGroupMembership.objects.filter(
|
||||
provider_group=provider_group, tenant_id=request.tenant_id
|
||||
).delete()
|
||||
|
||||
provider_group_memberships = [
|
||||
ProviderGroupMembership(
|
||||
tenant_id=self.request.tenant_id,
|
||||
provider_group=provider_group,
|
||||
provider_id=provider_id,
|
||||
)
|
||||
for provider_id in provider_ids
|
||||
]
|
||||
|
||||
ProviderGroupMembership.objects.bulk_create(
|
||||
provider_group_memberships, ignore_conflicts=True
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
provider_group = self.get_object()
|
||||
serializer = self.get_serializer(
|
||||
instance=provider_group,
|
||||
data={"providers": request.data},
|
||||
context={"tenant_id": self.request.tenant_id, "request": request},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Return the updated provider group with providers
|
||||
provider_group.refresh_from_db()
|
||||
self.response_serializer_class = ProviderGroupSerializer
|
||||
response_serializer = ProviderGroupSerializer(
|
||||
provider_group, context=self.get_serializer_context()
|
||||
)
|
||||
return Response(data=response_serializer.data, status=status.HTTP_200_OK)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
provider_group = self.get_object()
|
||||
provider_group.providers.clear()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
@@ -671,9 +900,43 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
required_permissions = []
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = self.get_required_permissions()
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_required_permissions(self):
|
||||
"""
|
||||
Returns the required permissions based on the request method.
|
||||
"""
|
||||
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
|
||||
# No permissions required for GET requests
|
||||
return []
|
||||
else:
|
||||
# Require permission for non-GET requests
|
||||
return [Permissions.MANAGE_PROVIDERS]
|
||||
|
||||
def get_queryset(self):
|
||||
return Provider.objects.all()
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all providers
|
||||
return Provider.objects.all()
|
||||
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
provider_groups = user_roles[0].provider_groups.all()
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
|
||||
return providers
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -793,9 +1056,42 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
required_permissions = [Permissions.MANAGE_SCANS]
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = self.get_required_permissions()
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_required_permissions(self):
|
||||
"""
|
||||
Returns the required permissions based on the request method.
|
||||
"""
|
||||
if DISABLE_RBAC or self.request.method in SAFE_METHODS:
|
||||
# No permissions required for GET requests
|
||||
return []
|
||||
else:
|
||||
# Require permission for non-GET requests
|
||||
return [Permissions.MANAGE_SCANS]
|
||||
|
||||
def get_queryset(self):
|
||||
return Scan.objects.all()
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all scans
|
||||
return Scan.objects.all()
|
||||
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
provider_groups = user_roles[0].provider_groups.all()
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
return Scan.objects.filter(provider__in=providers).distinct()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -885,11 +1181,28 @@ class TaskViewSet(BaseRLSViewSet):
|
||||
search_fields = ["name"]
|
||||
ordering = ["-inserted_at"]
|
||||
ordering_fields = ["inserted_at", "completed_at", "name", "state"]
|
||||
required_permissions = []
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def get_queryset(self):
|
||||
return Task.objects.annotate(
|
||||
name=F("task_runner_task__task_name"), state=F("task_runner_task__status")
|
||||
)
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all tasks
|
||||
return Task.objects.annotate(
|
||||
name=F("task_runner_task__task_name"),
|
||||
state=F("task_runner_task__status"),
|
||||
)
|
||||
|
||||
# User lacks permission, filter tasks based on provider groups associated with the role
|
||||
provider_groups = user_roles[0].provider_groups.all()
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
scans = Scan.objects.filter(provider__in=providers).distinct()
|
||||
return Task.objects.filter(scan__in=scans).distinct()
|
||||
|
||||
def destroy(self, request, *args, pk=None, **kwargs):
|
||||
task = get_object_or_404(Task, pk=pk)
|
||||
@@ -950,11 +1263,33 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
required_permissions = []
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = ResourceViewSet.required_permissions
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Resource.objects.all()
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Resource.objects.all()
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
provider_groups = user_roles[0].provider_groups.all()
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
queryset = Resource.objects.filter(provider__in=providers).distinct()
|
||||
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
if search_value:
|
||||
# Django's ORM will build a LEFT JOIN and OUTER JOIN on the "through" table, resulting in duplicates
|
||||
# The duplicates then require a `distinct` query
|
||||
@@ -1025,11 +1360,15 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
required_permissions = []
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def inserted_at_to_uuidv7(self, inserted_at):
|
||||
if inserted_at is None:
|
||||
return None
|
||||
return datetime_to_uuid7(inserted_at)
|
||||
def initial(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sets required_permissions before permissions are checked.
|
||||
"""
|
||||
self.required_permissions = ResourceViewSet.required_permissions
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "findings_services_regions":
|
||||
@@ -1038,9 +1377,23 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Finding.objects.all()
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
user = self.request.user
|
||||
user_roles = user.roles.all()
|
||||
if DISABLE_RBAC or getattr(
|
||||
user_roles[0], Permissions.UNLIMITED_VISIBILITY.value, False
|
||||
):
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Finding.objects.all()
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
provider_groups = user_roles[0].provider_groups.all()
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
scans = Scan.objects.filter(provider__in=providers).distinct()
|
||||
queryset = Finding.objects.filter(scan__in=scans).distinct()
|
||||
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
if search_value:
|
||||
# Django's ORM will build a LEFT JOIN and OUTER JOIN on any "through" tables, resulting in duplicates
|
||||
# The duplicates then require a `distinct` query
|
||||
@@ -1068,6 +1421,11 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
return queryset
|
||||
|
||||
def inserted_at_to_uuidv7(self, inserted_at):
|
||||
if inserted_at is None:
|
||||
return None
|
||||
return datetime_to_uuid7(inserted_at)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings_services_regions")
|
||||
def findings_services_regions(self, request):
|
||||
queryset = self.get_queryset()
|
||||
@@ -1188,6 +1546,8 @@ class InvitationViewSet(BaseRLSViewSet):
|
||||
"state",
|
||||
"inviter",
|
||||
]
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def get_queryset(self):
|
||||
return Invitation.objects.all()
|
||||
@@ -1275,6 +1635,14 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
|
||||
user=user,
|
||||
tenant=invitation.tenant,
|
||||
)
|
||||
# TODO: Add roles to output relationships
|
||||
user_role = []
|
||||
for role in invitation.roles.all():
|
||||
user_role.append(
|
||||
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
|
||||
user=user, role=role, tenant=invitation.tenant
|
||||
)
|
||||
)
|
||||
invitation.state = Invitation.State.ACCEPTED
|
||||
invitation.save(using=MainRouter.admin_db)
|
||||
|
||||
@@ -1283,6 +1651,153 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
|
||||
return Response(data=membership_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@extend_schema(tags=["Role"])
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="List all roles",
|
||||
description="Retrieve a list of all roles with options for filtering by various criteria.",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Retrieve data from a role",
|
||||
description="Fetch detailed information about a specific role by their ID.",
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Create a new role",
|
||||
description="Add a new role to the system by providing the required role details.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Partially update a role",
|
||||
description="Update certain fields of an existing role's information without affecting other fields.",
|
||||
responses={200: RoleSerializer},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Delete a role",
|
||||
description="Remove a role from the system by their ID.",
|
||||
),
|
||||
)
|
||||
class RoleViewSet(BaseRLSViewSet):
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filterset_class = RoleFilter
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
ordering = ["inserted_at"]
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
permission_classes = BaseRLSViewSet.permission_classes + [HasPermissions]
|
||||
|
||||
def get_queryset(self):
|
||||
return Role.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return RoleCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return RoleUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
user_role = user.roles.all().first()
|
||||
# If the user is the owner of the role, the manage_account field is not editable
|
||||
if user_role and kwargs["pk"] == str(user_role.id):
|
||||
request.data["manage_account"] = str(user_role.manage_account).lower()
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Create a new role-provider_groups relationship",
|
||||
description="Add a new role-provider_groups relationship to the system by providing the required role-provider_groups details.",
|
||||
responses={
|
||||
204: OpenApiResponse(description="Relationship created successfully"),
|
||||
400: OpenApiResponse(
|
||||
description="Bad request (e.g., relationship already exists)"
|
||||
),
|
||||
},
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Partially update a role-provider_groups relationship",
|
||||
description="Update the role-provider_groups relationship information without affecting other fields.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship updated successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Delete a role-provider_groups relationship",
|
||||
description="Remove the role-provider_groups relationship from the system by their ID.",
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship deleted successfully"
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleProviderGroupRelationshipSerializer
|
||||
resource_name = "provider_groups"
|
||||
http_method_names = ["post", "patch", "delete"]
|
||||
schema = RelationshipViewSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
return Role.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
|
||||
provider_group_ids = [item["id"] for item in request.data]
|
||||
existing_relationships = RoleProviderGroupRelationship.objects.filter(
|
||||
role=role, provider_group_id__in=provider_group_ids
|
||||
)
|
||||
|
||||
if existing_relationships.exists():
|
||||
return Response(
|
||||
{
|
||||
"detail": "One or more provider groups are already associated with the role."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
data={"provider_groups": request.data},
|
||||
context={
|
||||
"role": role,
|
||||
"tenant_id": self.request.tenant_id,
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
serializer = self.get_serializer(
|
||||
instance=role,
|
||||
data={"provider_groups": request.data},
|
||||
context={"tenant_id": self.request.tenant_id, "request": request},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
role.provider_groups.clear()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Compliance Overview"],
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
from celery import Celery, Task
|
||||
from config.env import env
|
||||
|
||||
BROKER_VISIBILITY_TIMEOUT = env.int("DJANGO_BROKER_VISIBILITY_TIMEOUT", default=86400)
|
||||
|
||||
celery_app = Celery("tasks")
|
||||
|
||||
celery_app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
celery_app.conf.update(result_extended=True, result_expires=None)
|
||||
|
||||
celery_app.conf.broker_transport_options = {
|
||||
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
|
||||
}
|
||||
celery_app.conf.result_backend_transport_options = {
|
||||
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
|
||||
}
|
||||
celery_app.conf.visibility_timeout = BROKER_VISIBILITY_TIMEOUT
|
||||
|
||||
celery_app.autodiscover_tasks(["api"])
|
||||
|
||||
|
||||
@@ -46,10 +35,10 @@ class RLSTask(Task):
|
||||
**options,
|
||||
)
|
||||
task_result_instance = TaskResult.objects.get(task_id=result.task_id)
|
||||
from api.db_utils import rls_transaction
|
||||
from api.db_utils import tenant_transaction
|
||||
|
||||
tenant_id = kwargs.get("tenant_id")
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
APITask.objects.create(
|
||||
id=task_result_instance.task_id,
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -207,3 +207,6 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
|
||||
|
||||
|
||||
TESTING = False
|
||||
|
||||
# Disable RBAC during tests/demos
|
||||
DISABLE_RBAC = False
|
||||
|
||||
+169
-14
@@ -1,34 +1,38 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.db import connection as django_connection
|
||||
from django.db import connections as django_connections
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from django.db import connections as django_connections, connection as django_connection
|
||||
from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Role,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
User,
|
||||
Membership,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
UserRoleRelationship,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.v1.serializers import TokenSerializer
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
|
||||
@@ -71,6 +75,16 @@ def disable_logging():
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def patch_testing_flag():
|
||||
"""
|
||||
Fixture to patch the TESTING flag to True during tests.
|
||||
"""
|
||||
with patch("api.rbac.permissions.DISABLE_RBAC", True):
|
||||
with patch("api.v1.views.DISABLE_RBAC", True):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def create_test_user(django_db_setup, django_db_blocker):
|
||||
with django_db_blocker.unblock():
|
||||
@@ -82,6 +96,106 @@ 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):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing",
|
||||
email="rbac@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="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_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_rbac(create_test_user_rbac, tenants_fixture, client):
|
||||
client.user = create_test_user_rbac
|
||||
serializer = TokenSerializer(
|
||||
data={"type": "tokens", "email": "rbac@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, client):
|
||||
client.user = create_test_user
|
||||
@@ -103,6 +217,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
|
||||
|
||||
|
||||
@@ -127,6 +242,7 @@ def tenants_fixture(create_test_user):
|
||||
tenant3 = Tenant.objects.create(
|
||||
name="Tenant Three",
|
||||
)
|
||||
|
||||
return tenant1, tenant2, tenant3
|
||||
|
||||
|
||||
@@ -209,6 +325,46 @@ def provider_groups_fixture(tenants_fixture):
|
||||
return pgroup1, pgroup2, pgroup3
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
return role1, role2, role3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider_secret_fixture(providers_fixture):
|
||||
return tuple(
|
||||
@@ -536,10 +692,9 @@ 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"]
|
||||
|
||||
|
||||
def get_authorization_header(access_token: str) -> dict:
|
||||
|
||||
@@ -2,7 +2,7 @@ from celery.utils.log import get_task_logger
|
||||
from django.db import transaction
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import batch_delete, rls_transaction
|
||||
from api.db_utils import batch_delete, tenant_transaction
|
||||
from api.models import Finding, Provider, Resource, Scan, ScanSummary, Tenant
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
@@ -66,7 +66,7 @@ def delete_tenant(pk: str):
|
||||
deletion_summary = {}
|
||||
|
||||
for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk):
|
||||
with rls_transaction(pk):
|
||||
with tenant_transaction(pk):
|
||||
summary = delete_provider(provider.id)
|
||||
deletion_summary.update(summary)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from api.compliance import (
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
|
||||
generate_scan_compliance,
|
||||
)
|
||||
from api.db_utils import rls_transaction
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
@@ -69,7 +69,7 @@ def _store_resources(
|
||||
- tuple[str, str]: A tuple containing the resource UID and region.
|
||||
|
||||
"""
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
resource_instance, created = Resource.objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider_instance,
|
||||
@@ -86,7 +86,7 @@ def _store_resources(
|
||||
resource_instance.service = finding.service_name
|
||||
resource_instance.type = finding.resource_type
|
||||
resource_instance.save()
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
tags = [
|
||||
ResourceTag.objects.get_or_create(
|
||||
tenant_id=tenant_id, key=key, value=value
|
||||
@@ -122,7 +122,7 @@ def perform_prowler_scan(
|
||||
unique_resources = set()
|
||||
start_time = time.time()
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
scan_instance = Scan.objects.get(pk=scan_id)
|
||||
scan_instance.state = StateChoices.EXECUTING
|
||||
@@ -130,7 +130,7 @@ def perform_prowler_scan(
|
||||
scan_instance.save()
|
||||
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
try:
|
||||
prowler_provider = initialize_prowler_provider(provider_instance)
|
||||
provider_instance.connected = True
|
||||
@@ -156,7 +156,7 @@ def perform_prowler_scan(
|
||||
for finding in findings:
|
||||
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
# Process resource
|
||||
resource_uid = finding.resource_uid
|
||||
if resource_uid not in resource_cache:
|
||||
@@ -188,7 +188,7 @@ def perform_prowler_scan(
|
||||
resource_instance.type = finding.resource_type
|
||||
updated_fields.append("type")
|
||||
if updated_fields:
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
resource_instance.save(update_fields=updated_fields)
|
||||
except (OperationalError, IntegrityError) as db_err:
|
||||
if attempt < CELERY_DEADLOCK_ATTEMPTS - 1:
|
||||
@@ -203,7 +203,7 @@ def perform_prowler_scan(
|
||||
|
||||
# Update tags
|
||||
tags = []
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
for key, value in finding.resource_tags.items():
|
||||
tag_key = (key, value)
|
||||
if tag_key not in tag_cache:
|
||||
@@ -219,7 +219,7 @@ def perform_prowler_scan(
|
||||
unique_resources.add((resource_instance.uid, resource_instance.region))
|
||||
|
||||
# Process finding
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
finding_uid = finding.uid
|
||||
if finding_uid not in last_status_cache:
|
||||
most_recent_finding = (
|
||||
@@ -267,7 +267,7 @@ def perform_prowler_scan(
|
||||
region_dict[finding.check_id] = finding.status.value
|
||||
|
||||
# Update scan progress
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
scan_instance.progress = progress
|
||||
scan_instance.save()
|
||||
|
||||
@@ -279,7 +279,7 @@ def perform_prowler_scan(
|
||||
scan_instance.state = StateChoices.FAILED
|
||||
|
||||
finally:
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
scan_instance.duration = time.time() - start_time
|
||||
scan_instance.completed_at = datetime.now(tz=timezone.utc)
|
||||
scan_instance.unique_resource_count = len(unique_resources)
|
||||
@@ -330,7 +330,7 @@ def perform_prowler_scan(
|
||||
total_requirements=compliance["total_requirements"],
|
||||
)
|
||||
)
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
|
||||
|
||||
if exception is not None:
|
||||
@@ -368,7 +368,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
- muted_new: Muted findings with a delta of 'new'.
|
||||
- muted_changed: Muted findings with a delta of 'changed'.
|
||||
"""
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
findings = Finding.objects.filter(scan_id=scan_id)
|
||||
|
||||
aggregation = findings.values(
|
||||
@@ -464,7 +464,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
),
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
scan_aggregations = {
|
||||
ScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
|
||||
@@ -7,7 +7,7 @@ from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.decorators import set_tenant
|
||||
from api.models import Provider, Scan
|
||||
|
||||
@@ -99,7 +99,7 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
"""
|
||||
task_id = self.request.id
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
with tenant_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=f"scan-perform-scheduled-{provider_id}"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -27,7 +26,7 @@ class TestPerformScan:
|
||||
providers_fixture,
|
||||
):
|
||||
with (
|
||||
patch("api.db_utils.rls_transaction"),
|
||||
patch("api.db_utils.tenant_transaction"),
|
||||
patch(
|
||||
"tasks.jobs.scan.initialize_prowler_provider"
|
||||
) as mock_initialize_prowler_provider,
|
||||
@@ -166,10 +165,10 @@ class TestPerformScan:
|
||||
"tasks.jobs.scan.initialize_prowler_provider",
|
||||
side_effect=Exception("Connection error"),
|
||||
)
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
def test_perform_prowler_scan_no_connection(
|
||||
self,
|
||||
mock_rls_transaction,
|
||||
mock_tenant_transaction,
|
||||
mock_initialize_prowler_provider,
|
||||
mock_prowler_scan_class,
|
||||
tenants_fixture,
|
||||
@@ -206,14 +205,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
def test_store_resources_new_resource(
|
||||
self,
|
||||
mock_rls_transaction,
|
||||
mock_tenant_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = uuid.uuid4()
|
||||
tenant_id = "tenant123"
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
@@ -254,14 +253,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
def test_store_resources_existing_resource(
|
||||
self,
|
||||
mock_rls_transaction,
|
||||
mock_tenant_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = uuid.uuid4()
|
||||
tenant_id = "tenant123"
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
@@ -311,14 +310,14 @@ class TestPerformScan:
|
||||
|
||||
@patch("api.models.ResourceTag.objects.get_or_create")
|
||||
@patch("api.models.Resource.objects.get_or_create")
|
||||
@patch("api.db_utils.rls_transaction")
|
||||
@patch("api.db_utils.tenant_transaction")
|
||||
def test_store_resources_with_tags(
|
||||
self,
|
||||
mock_rls_transaction,
|
||||
mock_tenant_transaction,
|
||||
mock_get_or_create_resource,
|
||||
mock_get_or_create_tag,
|
||||
):
|
||||
tenant_id = uuid.uuid4()
|
||||
tenant_id = "tenant123"
|
||||
provider_instance = MagicMock()
|
||||
provider_instance.id = "provider456"
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
component_management:
|
||||
individual_components:
|
||||
- component_id: "prowler"
|
||||
paths:
|
||||
- "prowler/**"
|
||||
- component_id: "api"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
comment:
|
||||
layout: "header, diff, flags, components"
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
+20
-7
@@ -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"
|
||||
|
||||
@@ -65,6 +65,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 +98,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 +123,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).
|
||||
|
||||
@@ -373,8 +386,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.
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+1
-2
@@ -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:
|
||||
|
||||
Generated
+102
-97
@@ -775,17 +775,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.35.71"
|
||||
version = "1.35.78"
|
||||
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.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"},
|
||||
{file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.35.71,<1.36.0"
|
||||
botocore = ">=1.35.78,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.71"
|
||||
version = "1.35.79"
|
||||
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.79-py3-none-any.whl", hash = "sha256:e6b10bb9a357e3f5ca2e60f6dd15a85d311b9a476eb21b3c0c2a3b364a2897c8"},
|
||||
{file = "botocore-1.35.79.tar.gz", hash = "sha256:245bfdda1b1508539ddd1819c67a8a2cc81780adf0715d3de418d64c4247f346"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1099,73 +1099,73 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.8"
|
||||
version = "7.6.9"
|
||||
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.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"},
|
||||
{file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"},
|
||||
{file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"},
|
||||
{file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"},
|
||||
{file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"},
|
||||
{file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"},
|
||||
{file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"},
|
||||
{file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"},
|
||||
{file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1719,13 +1719,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.154.0"
|
||||
version = "2.155.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.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.155.0-py2.py3-none-any.whl", hash = "sha256:83fe9b5aa4160899079d7c93a37be306546a17e6686e2549bcc9584f1a229747"},
|
||||
{file = "google_api_python_client-2.155.0.tar.gz", hash = "sha256:25529f89f0d13abcf3c05c089c423fb2858ac16e0b3727543393468d0d7af67c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2403,13 +2403,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "microsoft-kiota-abstractions"
|
||||
version = "1.6.2"
|
||||
version = "1.6.6"
|
||||
description = "Core abstractions for kiota generated libraries in Python"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "microsoft_kiota_abstractions-1.6.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.6-py3-none-any.whl", hash = "sha256:29071715baf0d604c381c5d17be47f35e6e63a441dcfb5e9141963406b469d50"},
|
||||
{file = "microsoft_kiota_abstractions-1.6.6.tar.gz", hash = "sha256:2554495b00c9c25b43f6964a71b65c89a277bd6b50f4d0028a7febcec6c4fd67"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.46"
|
||||
version = "9.5.48"
|
||||
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.48-py3-none-any.whl", hash = "sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8"},
|
||||
{file = "mkdocs_material-9.5.48.tar.gz", hash = "sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2769,13 +2769,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-sdk"
|
||||
version = "1.12.0"
|
||||
version = "1.14.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.14.0-py3-none-any.whl", hash = "sha256:1a2f327dc8fbe5a5e6d0d84cf71d605e7b118b3066b1e16f011ccd8fd927bb03"},
|
||||
{file = "msgraph_sdk-1.14.0.tar.gz", hash = "sha256:5bbda80941c5d1794682753b8b291bd2ebed719a43d6de949fd0cd613b6dfbbd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -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.2"
|
||||
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.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a"},
|
||||
{file = "pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=3.3.4,<=3.4.0-dev0"
|
||||
astroid = ">=3.3.5,<=3.4.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||
@@ -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.33.5"
|
||||
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.33.5-py2.py3-none-any.whl", hash = "sha256:b8cccadfa3d4005a5e6529f52000d25c583f46173fda8e9136fdd2bc58923ff6"},
|
||||
{file = "slack_sdk-3.33.5.tar.gz", hash = "sha256:a5e74c00c99dc844ad93e501ab764a20d86fa8184bbc9432af217496f632c4ee"},
|
||||
]
|
||||
|
||||
[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 = "e00da6013a01923ac8e79017e7fdb221e09a3dcf581ad8d74e39550be64cc2f3"
|
||||
|
||||
@@ -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.3"
|
||||
prowler_version = "5.1.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -388,6 +388,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
|
||||
|
||||
@@ -7315,6 +7315,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11097,6 +11098,7 @@
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_inside_vpc",
|
||||
"CheckTitle": "Ensure AWS Lambda Functions Are Deployed Inside a VPC",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "low",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled",
|
||||
"CheckTitle": "Check if Lambda functions invoke API operations are being recorded by CloudTrail.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "low",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_no_secrets_in_code",
|
||||
"CheckTitle": "Find secrets in Lambda functions code.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "critical",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_no_secrets_in_variables",
|
||||
"CheckTitle": "Find secrets in Lambda functions variables.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "critical",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_not_publicly_accessible",
|
||||
"CheckTitle": "Check if Lambda functions have resource-based policy set as Public.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "critical",
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import json
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Remove the Lambda function's resource-based policy to prevent public access and add a new permission for the account.
|
||||
Specifically, this fixer deletes all permission statements associated with the Lambda function's policy and then adds a new permission.
|
||||
Requires the lambda:RemovePermission and lambda:AddPermission permissions.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "lambda:RemovePermission",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "lambda:AddPermission",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The Lambda function name or ARN.
|
||||
region (str): AWS region where the Lambda function exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (policy removed and permission added), False otherwise.
|
||||
"""
|
||||
try:
|
||||
account_id = awslambda_client.audited_account
|
||||
|
||||
regional_client = awslambda_client.regional_clients[region]
|
||||
policy_response = regional_client.get_policy(FunctionName=resource_id)
|
||||
policy = json.loads(policy_response.get("Policy"))
|
||||
|
||||
for statement in policy.get("Statement", []):
|
||||
statement_id = statement.get("Sid")
|
||||
if statement_id:
|
||||
regional_client.remove_permission(
|
||||
FunctionName=resource_id, StatementId=statement_id
|
||||
)
|
||||
|
||||
regional_client.add_permission(
|
||||
FunctionName=resource_id,
|
||||
StatementId="ProwlerFixerStatement",
|
||||
Principal=account_id,
|
||||
Action="lambda:InvokeFunction",
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_url_cors_policy",
|
||||
"CheckTitle": "Check Lambda Function URL CORS configuration.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "medium",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_url_public",
|
||||
"CheckTitle": "Check Public Lambda Function URL.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "high",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_using_supported_runtimes",
|
||||
"CheckTitle": "Find obsolete Lambda runtimes.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "medium",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "awslambda_function_vpc_multi_az",
|
||||
"CheckTitle": "Check if AWS Lambda Function VPC is deployed Across Multiple Availability Zones",
|
||||
"CheckType": [],
|
||||
"ServiceName": "lambda",
|
||||
"ServiceName": "awslambda",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name",
|
||||
"Severity": "medium",
|
||||
|
||||
+3
-3
@@ -8,14 +8,14 @@ class backup_recovery_point_encrypted(Check):
|
||||
for recovery_point in backup_client.recovery_points:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = recovery_point.backup_vault_region
|
||||
report.resource_id = recovery_point.id
|
||||
report.resource_id = recovery_point.backup_vault_name
|
||||
report.resource_arn = recovery_point.arn
|
||||
report.resource_tags = recovery_point.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Backup Recovery Point {recovery_point.id} for Backup Vault {recovery_point.backup_vault_name} is not encrypted at rest."
|
||||
report.status_extended = f"Backup Recovery Point {recovery_point.arn} for Backup Vault {recovery_point.backup_vault_name} is not encrypted at rest."
|
||||
if recovery_point.encrypted:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Backup Recovery Point {recovery_point.id} for Backup Vault {recovery_point.backup_vault_name} is encrypted at rest."
|
||||
report.status_extended = f"Backup Recovery Point {recovery_point.arn} for Backup Vault {recovery_point.backup_vault_name} is encrypted at rest."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ class Backup(AWSService):
|
||||
self.backup_vault_arn_template = f"arn:{self.audited_partition}:backup:{self.region}:{self.audited_account}:backup-vault"
|
||||
self.backup_vaults = []
|
||||
self.__threading_call__(self._list_backup_vaults)
|
||||
if self.backup_vaults is not None:
|
||||
self.__threading_call__(self._list_tags, self.backup_vaults)
|
||||
self.__threading_call__(self._list_tags, self.backup_vaults)
|
||||
self.backup_plans = []
|
||||
self.__threading_call__(self._list_backup_plans)
|
||||
self.__threading_call__(self._list_tags, self.backup_plans)
|
||||
@@ -29,7 +28,6 @@ class Backup(AWSService):
|
||||
self.__threading_call__(self._list_backup_selections)
|
||||
self.recovery_points = []
|
||||
self.__threading_call__(self._list_recovery_points)
|
||||
self.__threading_call__(self._list_tags, self.recovery_points)
|
||||
|
||||
def _list_backup_vaults(self, regional_client):
|
||||
logger.info("Backup - Listing Backup Vaults...")
|
||||
@@ -173,11 +171,10 @@ class Backup(AWSService):
|
||||
|
||||
def _list_tags(self, resource):
|
||||
try:
|
||||
if getattr(resource, "arn", None):
|
||||
tags = self.regional_clients[resource.region].list_tags(
|
||||
ResourceArn=resource.arn
|
||||
)["Tags"]
|
||||
resource.tags = [tags] if tags else []
|
||||
tags = self.regional_clients[resource.region].list_tags(
|
||||
ResourceArn=resource.arn
|
||||
)["Tags"]
|
||||
resource.tags = [tags] if tags else []
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -198,13 +195,11 @@ class Backup(AWSService):
|
||||
self.recovery_points.append(
|
||||
RecoveryPoint(
|
||||
arn=arn,
|
||||
id=arn.split(":")[-1],
|
||||
backup_vault_name=backup_vault.name,
|
||||
encrypted=recovery_point.get(
|
||||
"IsEncrypted", False
|
||||
),
|
||||
backup_vault_region=backup_vault.region,
|
||||
region=regional_client.region,
|
||||
tags=[],
|
||||
)
|
||||
)
|
||||
@@ -251,8 +246,6 @@ class BackupReportPlan(BaseModel):
|
||||
|
||||
class RecoveryPoint(BaseModel):
|
||||
arn: str
|
||||
id: str
|
||||
region: str
|
||||
backup_vault_name: str
|
||||
encrypted: bool
|
||||
backup_vault_region: str
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ class cloudtrail_multi_region_enabled_logging_management_events(Check):
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.region = region
|
||||
report.region = trail.home_region
|
||||
report.status = "PASS"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Trail {trail.name} from home region {trail.home_region} is multi-region, is logging and have management events enabled."
|
||||
|
||||
@@ -18,24 +18,24 @@ def check_cloudwatch_log_metric_filter(
|
||||
for trail in trails.values():
|
||||
if trail.log_group_arn:
|
||||
log_groups.append(trail.log_group_arn.split(":")[6])
|
||||
# 2. Describe metric filters for previous log groups
|
||||
for metric_filter in metric_filters:
|
||||
if metric_filter.log_group.name in log_groups and re.search(
|
||||
metric_filter_pattern, metric_filter.pattern, flags=re.DOTALL
|
||||
):
|
||||
report.resource_id = metric_filter.log_group.name
|
||||
report.resource_arn = metric_filter.log_group.arn
|
||||
report.region = metric_filter.log_group.region
|
||||
report.resource_tags = getattr(metric_filter.log_group, "tags", [])
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} but no alarms associated."
|
||||
# 3. Check if there is an alarm for the metric
|
||||
for alarm in metric_alarms:
|
||||
if alarm.metric == metric_filter.metric:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} and alarms set."
|
||||
break
|
||||
if report.status == "PASS":
|
||||
# 2. Describe metric filters for previous log groups
|
||||
for metric_filter in metric_filters:
|
||||
if metric_filter.log_group.name in log_groups and re.search(
|
||||
metric_filter_pattern, metric_filter.pattern, flags=re.DOTALL
|
||||
):
|
||||
report.resource_id = metric_filter.log_group.name
|
||||
report.resource_arn = metric_filter.log_group.arn
|
||||
report.region = metric_filter.log_group.region
|
||||
report.resource_tags = getattr(metric_filter.log_group, "tags", [])
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} but no alarms associated."
|
||||
# 3. Check if there is an alarm for the metric
|
||||
for alarm in metric_alarms:
|
||||
if alarm.metric == metric_filter.metric:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} and alarms set."
|
||||
break
|
||||
if report.status == "PASS":
|
||||
break
|
||||
|
||||
return report
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
from prowler.providers.aws.services.ec2.lib.security_groups import check_security_group
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Revokes any ingress rule allowing CIFS ports (139, 445) from any address (0.0.0.0/0)
|
||||
for the EC2 instance's security groups.
|
||||
This fixer will only be triggered if the check identifies CIFS ports open to the Internet.
|
||||
Requires the ec2:RevokeSecurityGroupIngress permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "ec2:RevokeSecurityGroupIngress",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The EC2 instance ID.
|
||||
region (str): The AWS region where the EC2 instance exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (ingress rule revoked), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = ec2_client.regional_clients[region]
|
||||
check_ports = [139, 445]
|
||||
for instance in ec2_client.instances:
|
||||
if instance.id == resource_id:
|
||||
for sg in ec2_client.security_groups.values():
|
||||
if sg.id in instance.security_groups:
|
||||
for ingress_rule in sg.ingress_rules:
|
||||
if check_security_group(
|
||||
ingress_rule, "tcp", check_ports, any_address=True
|
||||
):
|
||||
regional_client.revoke_security_group_ingress(
|
||||
GroupId=sg.id,
|
||||
IpPermissions=[ingress_rule],
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+1
-4
@@ -19,10 +19,7 @@ class ec2_instance_uses_single_eni(Check):
|
||||
)
|
||||
else:
|
||||
for eni_id in instance.network_interfaces:
|
||||
if (
|
||||
eni_id in ec2_client.network_interfaces
|
||||
and ec2_client.network_interfaces[eni_id].type in eni_types
|
||||
):
|
||||
if ec2_client.network_interfaces[eni_id].type in eni_types:
|
||||
eni_types[ec2_client.network_interfaces[eni_id].type].append(
|
||||
eni_id
|
||||
)
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_launch_template_imdsv2_required",
|
||||
"CheckTitle": "Amazon EC2 launch templates should have IMDSv2 enabled and required.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:ec2:region:account-id:launch-template/resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEc2LaunchTemplate",
|
||||
"Description": "This control checks if Amazon EC2 launch templates are configured with IMDSv2 enabled and required. The control fails if IMDSv2 is not enabled or required in the launch template versions.",
|
||||
"Risk": "Without IMDSv2 required, EC2 instances may be vulnerable to metadata service attacks, allowing unauthorized access to instance metadata, potentially leading to compromise of instance credentials or other sensitive data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws ec2 modify-launch-template --launch-template-id <template-id> --version <version-number> --metadata-options HttpTokens=required",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-170",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "To ensure EC2 launch templates have IMDSv2 enabled and required, update the template to configure the Instance Metadata Service Version 2 as required.",
|
||||
"Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-launch-template.html#change-metadata-options"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
|
||||
class ec2_launch_template_imdsv2_required(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for template in ec2_client.launch_templates:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = template.region
|
||||
report.resource_id = template.id
|
||||
report.resource_arn = template.arn
|
||||
report.resource_tags = template.tags
|
||||
|
||||
versions_with_imdsv2_required = []
|
||||
versions_with_metadata_disabled = []
|
||||
versions_with_no_imdsv2 = []
|
||||
|
||||
for version in template.versions:
|
||||
if (
|
||||
version.template_data.http_endpoint == "enabled"
|
||||
and version.template_data.http_tokens == "required"
|
||||
):
|
||||
versions_with_imdsv2_required.append(str(version.version_number))
|
||||
elif version.template_data.http_endpoint == "disabled":
|
||||
versions_with_metadata_disabled.append(str(version.version_number))
|
||||
else:
|
||||
versions_with_no_imdsv2.append(str(version.version_number))
|
||||
|
||||
if versions_with_imdsv2_required:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 enabled and required in the following versions: {', '.join(versions_with_imdsv2_required)}."
|
||||
elif versions_with_metadata_disabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"EC2 Launch Template {template.name} has metadata service disabled in the following versions: {', '.join(versions_with_metadata_disabled)}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 disabled or not required in the following versions: {', '.join(versions_with_no_imdsv2)}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -10,7 +10,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
|
||||
################## EC2
|
||||
class EC2(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
@@ -569,6 +568,12 @@ class EC2(AWSService):
|
||||
),
|
||||
network_interfaces=enis,
|
||||
associate_public_ip_address=associate_public_ip,
|
||||
http_tokens=template_version["LaunchTemplateData"]
|
||||
.get("MetadataOptions", {})
|
||||
.get("HttpTokens", ""),
|
||||
http_endpoint=template_version["LaunchTemplateData"]
|
||||
.get("MetadataOptions", {})
|
||||
.get("HttpEndpoint", ""),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -763,6 +768,8 @@ class TemplateData(BaseModel):
|
||||
user_data: str
|
||||
network_interfaces: Optional[list[NetworkInterface]]
|
||||
associate_public_ip_address: Optional[bool]
|
||||
http_tokens: Optional[str]
|
||||
http_endpoint: Optional[str]
|
||||
|
||||
|
||||
class LaunchTemplateVersion(BaseModel):
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ecr.ecr_client import ecr_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the ECR repository's policy to remove public access.
|
||||
Specifically, this fixer delete the policy that had public access.
|
||||
Requires the ecr:DeleteRepositoryPolicy permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "ecr:DeleteRepositoryPolicy",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The ECR repository name.
|
||||
region (str): AWS region where the ECR repository exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (policy updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = ecr_client.regional_clients[region]
|
||||
|
||||
regional_client.delete_repository_policy(repositoryName=resource_id)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.glacier.glacier_client import glacier_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the Glacier vault's policy to remove public access.
|
||||
Specifically, this fixer delete the vault policy that has public access.
|
||||
Requires the glacier:DeleteVaultAccessPolicy permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "glacier:DeleteVaultAccessPolicy",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The Glacier vault name.
|
||||
region (str): AWS region where the Glacier vault exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (policy updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = glacier_client.regional_clients[region]
|
||||
|
||||
regional_client.delete_vault_access_policy(vaultName=resource_id)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -112,8 +112,7 @@ class IAM(AWSService):
|
||||
[policy for policy in self.policies if policy.type == "Custom"],
|
||||
)
|
||||
self.__threading_call__(self._list_tags, self.server_certificates)
|
||||
if self.saml_providers is not None:
|
||||
self.__threading_call__(self._list_tags, self.saml_providers.values())
|
||||
self.__threading_call__(self._list_tags, self.saml_providers.values())
|
||||
|
||||
def _get_client(self):
|
||||
return self.client
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.opensearch.opensearch_client import (
|
||||
opensearch_client,
|
||||
)
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the OpenSearch domain's resource-based policy to remove public access.
|
||||
Specifically, this fixer update the domain config and add an empty policy to remove the old one.
|
||||
Requires the es:UpdateDomainConfig permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "es:UpdateDomainConfig",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The OpenSearch domain name.
|
||||
region (str): AWS region where the OpenSearch domain exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (policy updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = opensearch_client.regional_clients[region]
|
||||
|
||||
regional_client.update_domain_config(
|
||||
DomainName=resource_id,
|
||||
AccessPolicies="",
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+1
-3
@@ -29,9 +29,7 @@ class route53_dangling_ip_subdomain_takeover(Check):
|
||||
# Check if record is an IP Address
|
||||
if validate_ip_address(record):
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = (
|
||||
f"{record_set.hosted_zone_id}/{record_set.name}/{record}"
|
||||
)
|
||||
report.resource_id = f"{record_set.hosted_zone_id}/{record}"
|
||||
report.resource_arn = route53_client.hosted_zones[
|
||||
record_set.hosted_zone_id
|
||||
].arn
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the S3 bucket's public access settings to block all public access.
|
||||
Specifically, this fixer configures the bucket's public access block settings to
|
||||
prevent any public access (ACLs and policies). Requires the s3:PutBucketPublicAccessBlock
|
||||
permission to modify the public access settings.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutBucketPublicAccessBlock",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The S3 bucket name.
|
||||
region (str): AWS region where the S3 bucket exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (public access is blocked),
|
||||
False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = s3_client.regional_clients[region]
|
||||
regional_client.put_public_access_block(
|
||||
Bucket=resource_id,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": True,
|
||||
"IgnorePublicAcls": True,
|
||||
"BlockPublicPolicy": True,
|
||||
"RestrictPublicBuckets": True,
|
||||
},
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the S3 bucket ACL to restrict public read access.
|
||||
Specifically, this fixer sets the ACL of the bucket to 'private' to prevent
|
||||
any public access to the S3 bucket.
|
||||
Requires the s3:PutBucketAcl permission.
|
||||
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutBucketAcl",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Args:
|
||||
resource_id (str): The S3 bucket name.
|
||||
region (str): AWS region where the S3 bucket exists.
|
||||
|
||||
Returns:
|
||||
bool: True if the operation is successful (bucket access is updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = s3_client.regional_clients[region]
|
||||
regional_client.put_bucket_acl(Bucket=resource_id, ACL="private")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the S3 bucket ACL to restrict public write access.
|
||||
Specifically, this fixer sets the ACL of the bucket to 'private' to prevent
|
||||
public write access to the S3 bucket. Requires the s3:PutBucketAcl permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutBucketAcl",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The S3 bucket id.
|
||||
region (str): AWS region where the S3 bucket exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (bucket access is updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = s3_client.regional_clients[region]
|
||||
regional_client.put_bucket_acl(Bucket=resource_id, ACL="private")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.sqs.sqs_client import sqs_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the SQS queue's resource-based policy to remove public access and replace with trusted account access.
|
||||
Specifically, this fixer checks if any statement has a public Principal (e.g., "*" or "CanonicalUser")
|
||||
and replaces it with the ARN of the trusted AWS account.
|
||||
Requires the sqs:SetQueueAttributes permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "sqs:SetQueueAttributes",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The SQS queue name or ARN.
|
||||
region (str): AWS region where the SQS queue exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (policy updated), False otherwise.
|
||||
"""
|
||||
try:
|
||||
account_id = sqs_client.audited_account
|
||||
audited_partition = sqs_client.audited_partition
|
||||
|
||||
regional_client = sqs_client.regional_clients[region]
|
||||
|
||||
queue_name = resource_id.split("/")[-1]
|
||||
|
||||
trusted_policy = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "ProwlerFixerStatement",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": account_id,
|
||||
},
|
||||
"Action": "sqs:*",
|
||||
"Resource": f"arn:{audited_partition}:sqs:{region}:{account_id}:{queue_name}",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
regional_client.set_queue_attributes(
|
||||
QueueUrl=resource_id,
|
||||
Attributes={"Policy": json.dumps(trusted_policy)},
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "sqlserver_recommended_minimal_tls_version",
|
||||
"CheckTitle": "Ensure SQL server has a recommended minimal TLS version required.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "sqlserver",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "SQLServer",
|
||||
"Description": "Ensure that SQL Server instances are configured with the recommended minimal TLS version to maintain secure connections.",
|
||||
"Risk": "Using outdated or weak TLS versions can expose SQL Server instances to vulnerabilities, increasing the risk of data breaches and unauthorized access.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-settings?view=azuresql&tabs=azure-portal#configure-minimum-tls-version",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az sql server update -n sql-server-name -g sql-server-group --set minimalTlsVersion=<version>",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "1. Go to Azure SQL Server 2. Navigate to 'Security' -> 'Networking' 3. Select 'Connectivity' 4. Update the TLS version in the field 'Minimum TLS version' to a recommended minimal version (e.g., TLS 1.2).",
|
||||
"Url": "https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-settings?view=azuresql&tabs=azure-portal#configure-minimum-tls-version"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Verify support for the TLS version from the application side before changing the minimal version."
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.sqlserver.sqlserver_client import sqlserver_client
|
||||
|
||||
|
||||
class sqlserver_recommended_minimal_tls_version(Check):
|
||||
def execute(self) -> List[Check_Report_Azure]:
|
||||
findings = []
|
||||
recommended_minimal_tls_versions = sqlserver_client.audit_config.get(
|
||||
"recommended_minimal_tls_versions", ["1.2", "1.3"]
|
||||
)
|
||||
for subscription, sql_servers in sqlserver_client.sql_servers.items():
|
||||
for sql_server in sql_servers:
|
||||
report = Check_Report_Azure(self.metadata())
|
||||
report.subscription = subscription
|
||||
report.resource_name = sql_server.name
|
||||
report.resource_id = sql_server.id
|
||||
report.status = "FAIL"
|
||||
report.location = sql_server.location
|
||||
report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using TLS version {sql_server.minimal_tls_version} as minimal accepted which is not recommended. Please use one of the recommended versions: {', '.join(recommended_minimal_tls_versions)}."
|
||||
if sql_server.minimal_tls_version in recommended_minimal_tls_versions:
|
||||
report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} is using version {sql_server.minimal_tls_version} as minimal accepted which is recommended."
|
||||
report.status = "PASS"
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+12
-12
@@ -23,7 +23,7 @@ packages = [
|
||||
{include = "dashboard"}
|
||||
]
|
||||
readme = "README.md"
|
||||
version = "5.0.3"
|
||||
version = "5.1.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
alive-progress = "3.2.0"
|
||||
@@ -48,19 +48,19 @@ azure-mgmt-storage = "21.2.1"
|
||||
azure-mgmt-subscription = "3.1.1"
|
||||
azure-mgmt-web = "7.3.1"
|
||||
azure-storage-blob = "12.24.0"
|
||||
boto3 = "1.35.71"
|
||||
botocore = "1.35.71"
|
||||
boto3 = "1.35.78"
|
||||
botocore = "1.35.79"
|
||||
colorama = "0.4.6"
|
||||
cryptography = "43.0.1"
|
||||
dash = "2.18.2"
|
||||
dash-bootstrap-components = "1.6.0"
|
||||
detect-secrets = "1.5.0"
|
||||
google-api-python-client = "2.154.0"
|
||||
google-api-python-client = "2.155.0"
|
||||
google-auth-httplib2 = ">=0.1,<0.3"
|
||||
jsonschema = "4.23.0"
|
||||
kubernetes = "31.0.0"
|
||||
microsoft-kiota-abstractions = "1.6.2"
|
||||
msgraph-sdk = "1.12.0"
|
||||
microsoft-kiota-abstractions = "1.6.6"
|
||||
msgraph-sdk = "1.14.0"
|
||||
numpy = "2.0.2"
|
||||
pandas = "2.2.3"
|
||||
py-ocsf-models = "0.2.0"
|
||||
@@ -70,14 +70,14 @@ python-dateutil = "^2.9.0.post0"
|
||||
pytz = "2024.2"
|
||||
schema = "0.7.7"
|
||||
shodan = "1.31.0"
|
||||
slack-sdk = "3.33.4"
|
||||
slack-sdk = "3.33.5"
|
||||
tabulate = "0.9.0"
|
||||
tzlocal = "5.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
bandit = "1.8.0"
|
||||
black = "24.10.0"
|
||||
coverage = "7.6.8"
|
||||
coverage = "7.6.9"
|
||||
docker = "7.1.0"
|
||||
flake8 = "7.1.1"
|
||||
freezegun = "1.5.1"
|
||||
@@ -85,14 +85,14 @@ mock = "5.1.0"
|
||||
moto = {extras = ["all"], version = "5.0.16"}
|
||||
openapi-schema-validator = "0.6.2"
|
||||
openapi-spec-validator = "0.7.1"
|
||||
pylint = "3.3.1"
|
||||
pytest = "8.3.3"
|
||||
pylint = "3.3.2"
|
||||
pytest = "8.3.4"
|
||||
pytest-cov = "6.0.0"
|
||||
pytest-env = "1.1.5"
|
||||
pytest-randomly = "3.16.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
safety = "3.2.9"
|
||||
vulture = "2.13"
|
||||
vulture = "2.14"
|
||||
|
||||
[tool.poetry.group.docs]
|
||||
optional = true
|
||||
@@ -100,7 +100,7 @@ optional = true
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "1.6.1"
|
||||
mkdocs-git-revision-date-localized-plugin = "1.3.0"
|
||||
mkdocs-material = "9.5.46"
|
||||
mkdocs-material = "9.5.48"
|
||||
mkdocs-material-extensions = "1.3.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
|
||||
@@ -318,6 +318,7 @@ config_azure = {
|
||||
"php_latest_version": "8.2",
|
||||
"python_latest_version": "3.12",
|
||||
"java_latest_version": "17",
|
||||
"recommended_minimal_tls_versions": ["1.2", "1.3"],
|
||||
}
|
||||
|
||||
config_gcp = {"shodan_api_key": None}
|
||||
|
||||
@@ -379,6 +379,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
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
from json import dumps
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_awslambda_function_not_publicly_accessible_fixer:
|
||||
@mock_aws
|
||||
def test_function_public(self):
|
||||
# Create the mock IAM role
|
||||
iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1)
|
||||
role_name = "test-role"
|
||||
assume_role_policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Service": "lambda.amazonaws.com"},
|
||||
"Action": "sts:AssumeRole",
|
||||
}
|
||||
],
|
||||
}
|
||||
role_arn = iam_client.create_role(
|
||||
RoleName=role_name,
|
||||
AssumeRolePolicyDocument=dumps(assume_role_policy_document),
|
||||
)["Role"]["Arn"]
|
||||
|
||||
function_name = "test-lambda"
|
||||
|
||||
# Create the lambda function using boto3 client
|
||||
lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1)
|
||||
lambda_client.create_function(
|
||||
FunctionName=function_name,
|
||||
Runtime="nodejs4.3",
|
||||
Role=role_arn,
|
||||
Handler="index.handler",
|
||||
Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"},
|
||||
Description="Test Lambda function",
|
||||
Timeout=3,
|
||||
MemorySize=128,
|
||||
Publish=True,
|
||||
Tags={"tag1": "value1", "tag2": "value2"},
|
||||
)["FunctionArn"]
|
||||
|
||||
# Attach the policy to the lambda function with a wildcard principal
|
||||
lambda_client.add_permission(
|
||||
FunctionName=function_name,
|
||||
StatementId="public-access",
|
||||
Action="lambda:InvokeFunction",
|
||||
Principal="*",
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.awslambda.awslambda_service import Lambda
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client",
|
||||
new=Lambda(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(function_name, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_function_public_with_source_account(self):
|
||||
# Create the mock IAM role
|
||||
iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1)
|
||||
role_name = "test-role"
|
||||
assume_role_policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Service": "lambda.amazonaws.com"},
|
||||
"Action": "sts:AssumeRole",
|
||||
}
|
||||
],
|
||||
}
|
||||
role_arn = iam_client.create_role(
|
||||
RoleName=role_name,
|
||||
AssumeRolePolicyDocument=dumps(assume_role_policy_document),
|
||||
)["Role"]["Arn"]
|
||||
|
||||
function_name = "test-lambda"
|
||||
|
||||
# Create the lambda function using boto3 client
|
||||
lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1)
|
||||
function_arn = lambda_client.create_function(
|
||||
FunctionName=function_name,
|
||||
Runtime="nodejs4.3",
|
||||
Role=role_arn,
|
||||
Handler="index.handler",
|
||||
Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"},
|
||||
Description="Test Lambda function",
|
||||
Timeout=3,
|
||||
MemorySize=128,
|
||||
Publish=True,
|
||||
Tags={"tag1": "value1", "tag2": "value2"},
|
||||
)["FunctionArn"]
|
||||
|
||||
# Attach the policy to the lambda function with a wildcard principal
|
||||
lambda_client.add_permission(
|
||||
FunctionName=function_name,
|
||||
StatementId="public-access",
|
||||
Action="lambda:InvokeFunction",
|
||||
Principal="*",
|
||||
SourceArn=function_arn,
|
||||
SourceAccount=AWS_ACCOUNT_NUMBER,
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.awslambda.awslambda_service import Lambda
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client",
|
||||
new=Lambda(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(function_name, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_function_not_public(self):
|
||||
# Create the mock IAM role
|
||||
iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1)
|
||||
role_name = "test-role"
|
||||
assume_role_policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Service": "lambda.amazonaws.com"},
|
||||
"Action": "sts:AssumeRole",
|
||||
}
|
||||
],
|
||||
}
|
||||
role_arn = iam_client.create_role(
|
||||
RoleName=role_name,
|
||||
AssumeRolePolicyDocument=dumps(assume_role_policy_document),
|
||||
)["Role"]["Arn"]
|
||||
|
||||
function_name = "test-lambda"
|
||||
|
||||
# Create the lambda function using boto3 client
|
||||
lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1)
|
||||
function_arn = lambda_client.create_function(
|
||||
FunctionName=function_name,
|
||||
Runtime="nodejs4.3",
|
||||
Role=role_arn,
|
||||
Handler="index.handler",
|
||||
Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"},
|
||||
Description="Test Lambda function",
|
||||
Timeout=3,
|
||||
MemorySize=128,
|
||||
Publish=True,
|
||||
Tags={"tag1": "value1", "tag2": "value2"},
|
||||
)["FunctionArn"]
|
||||
|
||||
# Attach the policy to the lambda function with a specific AWS account number as principal
|
||||
lambda_client.add_permission(
|
||||
FunctionName=function_name,
|
||||
StatementId="public-access",
|
||||
Action="lambda:InvokeFunction",
|
||||
Principal=AWS_ACCOUNT_NUMBER,
|
||||
SourceArn=function_arn,
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.awslambda.awslambda_service import Lambda
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client",
|
||||
new=Lambda(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(function_name, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_function_public_error(self):
|
||||
# Create the mock IAM role
|
||||
iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1)
|
||||
role_name = "test-role"
|
||||
assume_role_policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Service": "lambda.amazonaws.com"},
|
||||
"Action": "sts:AssumeRole",
|
||||
}
|
||||
],
|
||||
}
|
||||
role_arn = iam_client.create_role(
|
||||
RoleName=role_name,
|
||||
AssumeRolePolicyDocument=dumps(assume_role_policy_document),
|
||||
)["Role"]["Arn"]
|
||||
|
||||
function_name = "test-lambda"
|
||||
|
||||
# Create the lambda function using boto3 client
|
||||
lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1)
|
||||
lambda_client.create_function(
|
||||
FunctionName=function_name,
|
||||
Runtime="nodejs4.3",
|
||||
Role=role_arn,
|
||||
Handler="index.handler",
|
||||
Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"},
|
||||
Description="Test Lambda function",
|
||||
Timeout=3,
|
||||
MemorySize=128,
|
||||
Publish=True,
|
||||
Tags={"tag1": "value1", "tag2": "value2"},
|
||||
)["FunctionArn"]
|
||||
|
||||
# Attach the policy to the lambda function with a wildcard principal
|
||||
lambda_client.add_permission(
|
||||
FunctionName=function_name,
|
||||
StatementId="public-access",
|
||||
Action="lambda:InvokeFunction",
|
||||
Principal="*",
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.awslambda.awslambda_service import Lambda
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer.awslambda_client",
|
||||
new=Lambda(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer("non-exsting", AWS_REGION_EU_WEST_1)
|
||||
+22
-31
@@ -94,15 +94,12 @@ class Test_backup_recovery_point_encrypted:
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
),
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted import (
|
||||
@@ -127,15 +124,12 @@ class Test_backup_recovery_point_encrypted:
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
),
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted import (
|
||||
@@ -148,9 +142,9 @@ class Test_backup_recovery_point_encrypted:
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
"Backup Recovery Point 1 for Backup Vault Test Vault is not encrypted at rest."
|
||||
"Backup Recovery Point arn:aws:backup:eu-west-1:123456789012:recovery-point:1 for Backup Vault Test Vault is not encrypted at rest."
|
||||
)
|
||||
assert result[0].resource_id == "1"
|
||||
assert result[0].resource_id == "Test Vault"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:backup:eu-west-1:123456789012:recovery-point:1"
|
||||
@@ -171,15 +165,12 @@ class Test_backup_recovery_point_encrypted:
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
),
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted.backup_client",
|
||||
new=Backup(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.backup.backup_recovery_point_encrypted.backup_recovery_point_encrypted import (
|
||||
@@ -192,9 +183,9 @@ class Test_backup_recovery_point_encrypted:
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
"Backup Recovery Point 1 for Backup Vault Test Vault is encrypted at rest."
|
||||
"Backup Recovery Point arn:aws:backup:eu-west-1:123456789012:recovery-point:1 for Backup Vault Test Vault is encrypted at rest."
|
||||
)
|
||||
assert result[0].resource_id == "1"
|
||||
assert result[0].resource_id == "Test Vault"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:backup:eu-west-1:123456789012:recovery-point:1"
|
||||
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
import botocore.client
|
||||
from boto3 import client, resource
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider
|
||||
|
||||
mock_make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call_error(self, operation_name, kwarg):
|
||||
if operation_name == "RevokeSecurityGroupIngress":
|
||||
raise botocore.exceptions.ClientError(
|
||||
{
|
||||
"Error": {
|
||||
"Code": "InvalidPermission.NotFound",
|
||||
"Message": "The specified rule does not exist in this security group.",
|
||||
}
|
||||
},
|
||||
operation_name,
|
||||
)
|
||||
return mock_make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_ec2_instance_port_cifs_exposed_to_internet_fixer:
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_in_private_subnet_with_ip4_and_ip6(self):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 1,
|
||||
"ToPort": 1000,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}],
|
||||
"Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}],
|
||||
},
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 445,
|
||||
"ToPort": 445,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}],
|
||||
"Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}],
|
||||
},
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
instance_id = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
SubnetId=subnet_id,
|
||||
TagSpecifications=[
|
||||
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]}
|
||||
],
|
||||
)[0].id
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(instance_id, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_error(self):
|
||||
with mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_error
|
||||
):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 1,
|
||||
"ToPort": 1000,
|
||||
"IpRanges": [
|
||||
{"CidrIp": "0.0.0.0/0"},
|
||||
{"CidrIp": "10.0.0.0/24"},
|
||||
],
|
||||
"Ipv6Ranges": [
|
||||
{"CidrIpv6": "::/0"},
|
||||
{"CidrIpv6": "2001:db8::/32"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 445,
|
||||
"ToPort": 445,
|
||||
"IpRanges": [
|
||||
{"CidrIp": "0.0.0.0/0"},
|
||||
{"CidrIp": "10.0.0.0/24"},
|
||||
],
|
||||
"Ipv6Ranges": [
|
||||
{"CidrIpv6": "::/0"},
|
||||
{"CidrIpv6": "2001:db8::/32"},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
instance_id = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
SubnetId=subnet_id,
|
||||
TagSpecifications=[
|
||||
{
|
||||
"ResourceType": "instance",
|
||||
"Tags": [{"Key": "Name", "Value": "test"}],
|
||||
}
|
||||
],
|
||||
)[0].id
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer(instance_id, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_in_private_subnet_only_with_ip4(self):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 139,
|
||||
"ToPort": 139,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}],
|
||||
},
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 445,
|
||||
"ToPort": 445,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}],
|
||||
},
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
instance_id = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
SubnetId=subnet_id,
|
||||
TagSpecifications=[
|
||||
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]}
|
||||
],
|
||||
)[0].id
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(instance_id, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_in_private_subnet_only_with_ip6(self):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 139,
|
||||
"ToPort": 139,
|
||||
"Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}],
|
||||
},
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 445,
|
||||
"ToPort": 445,
|
||||
"Ipv6Ranges": [{"CidrIpv6": "::/0"}, {"CidrIpv6": "2001:db8::/32"}],
|
||||
},
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
instance_id = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
SubnetId=subnet_id,
|
||||
TagSpecifications=[
|
||||
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]}
|
||||
],
|
||||
)[0].id
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(instance_id, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_in_public_subnet_only_139_port(self):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 139,
|
||||
"ToPort": 139,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
instance = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
NetworkInterfaces=[
|
||||
{
|
||||
"DeviceIndex": 0,
|
||||
"SubnetId": subnet_id,
|
||||
"AssociatePublicIpAddress": True,
|
||||
}
|
||||
],
|
||||
TagSpecifications=[
|
||||
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]}
|
||||
],
|
||||
)[0]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(instance.id, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ec2_instance_exposed_port_with_public_ip_in_public_subnet_only_445_port(
|
||||
self,
|
||||
):
|
||||
# Create EC2 Mocked Resources
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
ec2_resource = resource("ec2", region_name=AWS_REGION_EU_WEST_1)
|
||||
vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"]
|
||||
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
|
||||
"SecurityGroups"
|
||||
][0]
|
||||
default_sg_id = default_sg["GroupId"]
|
||||
ec2_client.authorize_security_group_ingress(
|
||||
GroupId=default_sg_id,
|
||||
IpPermissions=[
|
||||
{
|
||||
"IpProtocol": "tcp",
|
||||
"FromPort": 445,
|
||||
"ToPort": 445,
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
subnet_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.0.0/16")[
|
||||
"Subnet"
|
||||
]["SubnetId"]
|
||||
# add default route of subnet to an internet gateway to make it public
|
||||
igw_id = ec2_client.create_internet_gateway()["InternetGateway"][
|
||||
"InternetGatewayId"
|
||||
]
|
||||
# attach internet gateway to subnet
|
||||
ec2_client.attach_internet_gateway(InternetGatewayId=igw_id, VpcId=vpc_id)
|
||||
# create route table
|
||||
route_table_id = ec2_client.create_route_table(VpcId=vpc_id)["RouteTable"][
|
||||
"RouteTableId"
|
||||
]
|
||||
# associate route table with subnet
|
||||
ec2_client.associate_route_table(
|
||||
RouteTableId=route_table_id, SubnetId=subnet_id
|
||||
)
|
||||
# add route to route table
|
||||
ec2_client.create_route(
|
||||
RouteTableId=route_table_id,
|
||||
DestinationCidrBlock="0.0.0.0/0",
|
||||
GatewayId=igw_id,
|
||||
)
|
||||
instance = ec2_resource.create_instances(
|
||||
ImageId="ami-12345678",
|
||||
MinCount=1,
|
||||
MaxCount=1,
|
||||
InstanceType="t2.micro",
|
||||
SecurityGroupIds=[default_sg_id],
|
||||
NetworkInterfaces=[
|
||||
{
|
||||
"DeviceIndex": 0,
|
||||
"SubnetId": subnet_id,
|
||||
"AssociatePublicIpAddress": True,
|
||||
}
|
||||
],
|
||||
TagSpecifications=[
|
||||
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "test"}]}
|
||||
],
|
||||
)[0]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ec2.ec2_instance_port_cifs_exposed_to_internet.ec2_instance_port_cifs_exposed_to_internet_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(instance.id, AWS_REGION_EU_WEST_1)
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
|
||||
make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call(self, operation_name, kwarg):
|
||||
if operation_name == "DescribeLaunchTemplateVersions":
|
||||
return {
|
||||
"LaunchTemplateVersions": [
|
||||
{
|
||||
"VersionNumber": 1,
|
||||
"LaunchTemplateData": {
|
||||
"MetadataOptions": {
|
||||
"HttpEndpoint": "enabled",
|
||||
"HttpTokens": "required",
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_not_required(self, operation_name, kwarg):
|
||||
if operation_name == "DescribeLaunchTemplateVersions":
|
||||
return {
|
||||
"LaunchTemplateVersions": [
|
||||
{
|
||||
"VersionNumber": 1,
|
||||
"LaunchTemplateData": {
|
||||
"MetadataOptions": {
|
||||
"HttpEndpoint": "enabled",
|
||||
"HttpTokens": "optional",
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_ec2_launch_template_imdsv2_required:
|
||||
@mock_aws
|
||||
def test_no_launch_templates(self):
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
ec2_client.launch_templates = []
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import (
|
||||
ec2_launch_template_imdsv2_required,
|
||||
)
|
||||
|
||||
check = ec2_launch_template_imdsv2_required()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_launch_template_imdsv2_required(self):
|
||||
with mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call
|
||||
):
|
||||
launch_template_name = "test-imdsv2-required"
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
ec2_client.create_launch_template(
|
||||
LaunchTemplateName=launch_template_name,
|
||||
VersionDescription="Launch Template with IMDSv2 required",
|
||||
LaunchTemplateData={
|
||||
"InstanceType": "t1.micro",
|
||||
"MetadataOptions": {
|
||||
"HttpEndpoint": "enabled",
|
||||
"HttpTokens": "required",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
launch_template_id = ec2_client.describe_launch_templates(
|
||||
LaunchTemplateNames=[launch_template_name]
|
||||
)["LaunchTemplates"][0]["LaunchTemplateId"]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import (
|
||||
ec2_launch_template_imdsv2_required,
|
||||
)
|
||||
|
||||
check = ec2_launch_template_imdsv2_required()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"EC2 Launch Template {launch_template_name} has IMDSv2 enabled and required in the following versions: 1."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}"
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
@mock_aws
|
||||
def test_launch_template_imdsv2_not_required(self):
|
||||
with mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_not_required,
|
||||
):
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
|
||||
launch_template_name = "test-imdsv2-not-required"
|
||||
ec2_client.create_launch_template(
|
||||
LaunchTemplateName=launch_template_name,
|
||||
VersionDescription="Launch Template without IMDSv2 required",
|
||||
LaunchTemplateData={
|
||||
"InstanceType": "t1.micro",
|
||||
"MetadataOptions": {
|
||||
"HttpEndpoint": "enabled",
|
||||
"HttpTokens": "optional",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
launch_template_id = ec2_client.describe_launch_templates(
|
||||
LaunchTemplateNames=[launch_template_name]
|
||||
)["LaunchTemplates"][0]["LaunchTemplateId"]
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import EC2
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client",
|
||||
new=EC2(aws_provider),
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import (
|
||||
ec2_launch_template_imdsv2_required,
|
||||
)
|
||||
|
||||
check = ec2_launch_template_imdsv2_required()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"EC2 Launch Template {launch_template_name} has IMDSv2 disabled or not required in the following versions: 1."
|
||||
)
|
||||
assert result[0].resource_id == launch_template_id
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}"
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
@@ -36,6 +36,10 @@ def mock_make_api_call(self, operation_name, kwarg):
|
||||
"foobar123".encode(encoding_format_utf_8)
|
||||
).decode(encoding_format_utf_8),
|
||||
"NetworkInterfaces": [{"AssociatePublicIpAddress": True}],
|
||||
"MetadataOptions": {
|
||||
"HttpEndpoint": "enabled",
|
||||
"HttpTokens": "optional",
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -741,6 +745,8 @@ class Test_EC2_Service:
|
||||
)
|
||||
|
||||
assert version.template_data.associate_public_ip_address
|
||||
assert version.template_data.http_endpoint == "enabled"
|
||||
assert version.template_data.http_tokens == "optional"
|
||||
|
||||
# Test EC2 Describe VPN Endpoints
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
from json import dumps
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider
|
||||
|
||||
|
||||
class Test_ecr_repositories_not_publicly_accessible_fixer:
|
||||
@mock_aws
|
||||
def test_ecr_repository_public(self):
|
||||
ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1)
|
||||
|
||||
repository_name = "test-repo"
|
||||
ecr_client.create_repository(repositoryName=repository_name)
|
||||
|
||||
ecr_client.set_repository_policy(
|
||||
repositoryName=repository_name,
|
||||
policyText=dumps(
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Action": "ecr:*",
|
||||
"Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.ecr.ecr_service import ECR
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client",
|
||||
new=ECR(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(repository_name, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ecr_repository_not_public(self):
|
||||
ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1)
|
||||
|
||||
repository_name = "test-repo"
|
||||
ecr_client.create_repository(repositoryName=repository_name)
|
||||
|
||||
ecr_client.set_repository_policy(
|
||||
repositoryName=repository_name,
|
||||
policyText=dumps(
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": "123456789012"},
|
||||
"Action": "ecr:*",
|
||||
"Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.ecr.ecr_service import ECR
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client",
|
||||
new=ECR(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(repository_name, AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_ecr_repository_public_error(self):
|
||||
ecr_client = client("ecr", region_name=AWS_REGION_EU_WEST_1)
|
||||
|
||||
repository_name = "test-repo"
|
||||
ecr_client.create_repository(repositoryName=repository_name)
|
||||
|
||||
ecr_client.set_repository_policy(
|
||||
repositoryName=repository_name,
|
||||
policyText=dumps(
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Action": "ecr:*",
|
||||
"Resource": f"arn:aws:ecr:{AWS_REGION_EU_WEST_1}:123456789012:repository/{repository_name}",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.ecr.ecr_service import ECR
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer.ecr_client",
|
||||
new=ECR(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer("repository_name_non_existing", AWS_REGION_EU_WEST_1)
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider
|
||||
|
||||
mock_make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call_public_vault(self, operation_name, kwarg):
|
||||
if operation_name == "DeleteVaultAccessPolicy":
|
||||
return {
|
||||
"ResponseMetadata": {
|
||||
"HTTPStatusCode": 204,
|
||||
"RequestId": "test-request-id",
|
||||
}
|
||||
}
|
||||
return mock_make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_public_vault_error(self, operation_name, kwarg):
|
||||
if operation_name == "DeleteVaultAccessPolicy":
|
||||
raise botocore.exceptions.ClientError(
|
||||
{
|
||||
"Error": {
|
||||
"Code": "VaultNotFound",
|
||||
"Message": "VaultNotFound",
|
||||
}
|
||||
},
|
||||
operation_name,
|
||||
)
|
||||
return mock_make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_glacier_vaults_policy_public_access_fixer:
|
||||
@mock_aws
|
||||
def test_glacier_vault_public(self):
|
||||
with mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_public_vault,
|
||||
):
|
||||
from prowler.providers.aws.services.glacier.glacier_service import Glacier
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer.glacier_client",
|
||||
new=Glacier(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(resource_id="test-vault", region=AWS_REGION_EU_WEST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_glacier_vault_public_error(self):
|
||||
with mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_public_vault_error,
|
||||
):
|
||||
from prowler.providers.aws.services.glacier.glacier_service import Glacier
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer.glacier_client",
|
||||
new=Glacier(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer(resource_id="test-vault", region=AWS_REGION_EU_WEST_1)
|
||||
+18
@@ -11,6 +11,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
@@ -37,6 +40,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
@@ -89,6 +95,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
@@ -141,6 +150,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
@@ -193,6 +205,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
@@ -249,6 +264,9 @@ class Test_glacier_vaults_policy_public_access:
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_service.Glacier",
|
||||
new=glacier_client,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.glacier.glacier_client.glacier_client",
|
||||
new=glacier_client,
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.glacier.glacier_vaults_policy_public_access.glacier_vaults_policy_public_access import (
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
from json import dumps
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_WEST_2,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
domain_name = "test-domain"
|
||||
|
||||
policy_data_restricted = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": [f"{AWS_ACCOUNT_NUMBER}"]},
|
||||
"Action": ["es:*"],
|
||||
"Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
policy_data_not_restricted = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": ["*"]},
|
||||
"Action": ["es:*"],
|
||||
"Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
policy_data_not_restricted_principal = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": ["es:*"],
|
||||
"Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class Test_opensearch_service_domains_not_publicly_accessible_fixer:
|
||||
@mock_aws
|
||||
def test_policy_data_restricted_error(self):
|
||||
opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2)
|
||||
opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"]
|
||||
opensearch_client.update_domain_config(
|
||||
DomainName=domain_name,
|
||||
AccessPolicies=str(policy_data_restricted),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2])
|
||||
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service import (
|
||||
OpenSearchService,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client",
|
||||
new=OpenSearchService(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer("domain_name_non_existing", AWS_REGION_US_WEST_2)
|
||||
|
||||
@mock_aws
|
||||
def test_policy_data_not_restricted_with_principal_AWS(self):
|
||||
opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2)
|
||||
opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"]
|
||||
opensearch_client.update_domain_config(
|
||||
DomainName=domain_name,
|
||||
AccessPolicies=dumps(policy_data_not_restricted),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2])
|
||||
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service import (
|
||||
OpenSearchService,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client",
|
||||
new=OpenSearchService(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(domain_name, AWS_REGION_US_WEST_2)
|
||||
|
||||
@mock_aws
|
||||
def test_policy_data_not_restricted_with_principal_no_AWS(self):
|
||||
opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2)
|
||||
opensearch_client.create_domain(DomainName=domain_name)["DomainStatus"]["ARN"]
|
||||
opensearch_client.update_domain_config(
|
||||
DomainName=domain_name,
|
||||
AccessPolicies=dumps(policy_data_not_restricted_principal),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2])
|
||||
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service import (
|
||||
OpenSearchService,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer.opensearch_client",
|
||||
new=OpenSearchService(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(domain_name, AWS_REGION_US_WEST_2)
|
||||
+5
-20
@@ -131,10 +131,7 @@ class Test_route53_dangling_ip_subdomain_takeover:
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== zone_id.replace("/hostedzone/", "")
|
||||
+ "/"
|
||||
+ record_set_name
|
||||
+ "/192.168.1.1"
|
||||
== zone_id.replace("/hostedzone/", "") + "/192.168.1.1"
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
@@ -199,10 +196,7 @@ class Test_route53_dangling_ip_subdomain_takeover:
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== zone_id.replace("/hostedzone/", "")
|
||||
+ "/"
|
||||
+ record_set_name
|
||||
+ "/17.5.7.3"
|
||||
== zone_id.replace("/hostedzone/", "") + "/17.5.7.3"
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
@@ -267,10 +261,7 @@ class Test_route53_dangling_ip_subdomain_takeover:
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== zone_id.replace("/hostedzone/", "")
|
||||
+ "/"
|
||||
+ record_set_name
|
||||
+ "/54.152.12.70"
|
||||
== zone_id.replace("/hostedzone/", "") + "/54.152.12.70"
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
@@ -339,10 +330,7 @@ class Test_route53_dangling_ip_subdomain_takeover:
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== zone_id.replace("/hostedzone/", "")
|
||||
+ "/"
|
||||
+ record_set_name
|
||||
+ "/17.5.7.3"
|
||||
== zone_id.replace("/hostedzone/", "") + "/17.5.7.3"
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
@@ -417,10 +405,7 @@ class Test_route53_dangling_ip_subdomain_takeover:
|
||||
)
|
||||
assert (
|
||||
result[0].resource_id
|
||||
== zone_id.replace("/hostedzone/", "")
|
||||
+ "/"
|
||||
+ record_set_name
|
||||
+ "/17.5.7.3"
|
||||
== zone_id.replace("/hostedzone/", "") + "/17.5.7.3"
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_s3_bucket_public_access_fixer:
|
||||
@mock_aws
|
||||
def test_no_buckets(self):
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer("bucket_test_us", AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_ACL(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_policy(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*"}]}'
|
||||
s3_client.put_bucket_policy(
|
||||
Bucket=bucket_name_us,
|
||||
Policy=public_write_policy,
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_due_to_policy_conditions_from_public_ip(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"IpAddress": {"aws:SourceIp": "1.2.3.4"}}}]}'
|
||||
s3_client.put_bucket_policy(
|
||||
Bucket=bucket_name_us,
|
||||
Policy=public_write_policy,
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_s3_bucket_public_list_acl_fixer:
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AllUsers_READ(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AllUsers_READ_ACP(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ_ACP",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AllUsers_FULL_CONTROL(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "FULL_CONTROL",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AuthenticatedUsers_READ(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AuthenticatedUsers_READ_ACP(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ_ACP",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AuthenticatedUsers_FULL_CONTROL(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "FULL_CONTROL",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert fixer(bucket_name_us, AWS_REGION_US_EAST_1)
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_public_list_ACL_AllUsers_READ_error(self):
|
||||
s3_client = client("s3", region_name=AWS_REGION_US_EAST_1)
|
||||
bucket_name_us = "bucket_test_us"
|
||||
s3_client.create_bucket(Bucket=bucket_name_us)
|
||||
bucket_owner = s3_client.get_bucket_acl(Bucket=bucket_name_us)["Owner"]
|
||||
# Generate S3Control Client
|
||||
s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1)
|
||||
s3control_client.put_public_access_block(
|
||||
AccountId=AWS_ACCOUNT_NUMBER,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_public_access_block(
|
||||
Bucket=bucket_name_us,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": False,
|
||||
"IgnorePublicAcls": False,
|
||||
"BlockPublicPolicy": False,
|
||||
"RestrictPublicBuckets": False,
|
||||
},
|
||||
)
|
||||
s3_client.put_bucket_acl(
|
||||
Bucket=bucket_name_us,
|
||||
AccessControlPolicy={
|
||||
"Grants": [
|
||||
{
|
||||
"Grantee": {
|
||||
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"Type": "Group",
|
||||
},
|
||||
"Permission": "READ",
|
||||
},
|
||||
],
|
||||
"Owner": bucket_owner,
|
||||
},
|
||||
)
|
||||
from prowler.providers.aws.services.s3.s3_service import S3
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer.s3_client",
|
||||
new=S3(aws_provider),
|
||||
):
|
||||
# Test Fixer
|
||||
from prowler.providers.aws.services.s3.s3_bucket_public_list_acl.s3_bucket_public_list_acl_fixer import (
|
||||
fixer,
|
||||
)
|
||||
|
||||
assert not fixer("bucket_name_us_non_existing", AWS_REGION_US_EAST_1)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user