mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-27 18:38:52 +00:00
Compare commits
19 Commits
v5.3
...
PRWLR-4669
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d190eb020 | ||
|
|
0459a4d6f6 | ||
|
|
19df649554 | ||
|
|
737550eb05 | ||
|
|
68d7d9f998 | ||
|
|
3c9a8b3634 | ||
|
|
81f970f2d3 | ||
|
|
3d9cd177a2 | ||
|
|
c49fdc114a | ||
|
|
95fd9d6b5e | ||
|
|
6a5bc75252 | ||
|
|
858c04b0b0 | ||
|
|
2d6f20e84b | ||
|
|
b0a98b1a87 | ||
|
|
577530ac69 | ||
|
|
c1a8d47e5b | ||
|
|
e80704d6f0 | ||
|
|
010de4b415 | ||
|
|
0a2b8e4315 |
13
.env
13
.env
@@ -6,14 +6,13 @@
|
||||
PROWLER_UI_VERSION="latest"
|
||||
SITE_URL=http://localhost:3000
|
||||
API_BASE_URL=http://prowler-api:8080/api/v1
|
||||
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
|
||||
AUTH_TRUST_HOST=true
|
||||
UI_PORT=3000
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
|
||||
|
||||
#### Prowler API Configuration ####
|
||||
PROWLER_API_VERSION="stable"
|
||||
PROWLER_API_VERSION="latest"
|
||||
# PostgreSQL settings
|
||||
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
POSTGRES_HOST=postgres-db
|
||||
@@ -41,12 +40,9 @@ DJANGO_LOGGING_FORMATTER=human_readable
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL=INFO
|
||||
# Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_WORKERS=4
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30 # Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440 # Token lifetime is in minutes
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_MANAGE_DB_PARTITIONS=True
|
||||
@@ -91,4 +87,3 @@ jQIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -16,17 +16,6 @@ updates:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/api"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "component/api"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -38,7 +27,7 @@ updates:
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/ui"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -46,7 +35,6 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "npm"
|
||||
- "component/ui"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
|
||||
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
@@ -15,13 +15,7 @@ Please include a summary of the change and which issue is fixed. List any depend
|
||||
- [ ] Review if the code is being covered by tests.
|
||||
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
|
||||
- [ ] Review if backport is needed.
|
||||
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
|
||||
|
||||
#### API
|
||||
- [ ] Verify if API specs need to be regenerated.
|
||||
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
|
||||
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
@@ -23,7 +23,6 @@ env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
|
||||
WORKING_DIRECTORY: ./api
|
||||
|
||||
@@ -32,34 +31,19 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -93,6 +77,5 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
4
.github/workflows/api-codeql.yml
vendored
4
.github/workflows/api-codeql.yml
vendored
@@ -15,12 +15,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
6
.github/workflows/api-pull-request.yml
vendored
6
.github/workflows/api-pull-request.yml
vendored
@@ -4,13 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
@@ -89,7 +87,7 @@ jobs:
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry==1.8.5
|
||||
pipx install poetry
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
@@ -114,7 +112,7 @@ jobs:
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry check --lock
|
||||
poetry lock --check
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: ./api
|
||||
|
||||
2
.github/workflows/find-secrets.yml
vendored
2
.github/workflows/find-secrets.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.88.5
|
||||
uses: trufflesecurity/trufflehog@v3.86.1
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pipx install poetry==1.8.5
|
||||
pipx install poetry
|
||||
pipx inject poetry poetry-bumpversion
|
||||
|
||||
- name: Get Prowler version
|
||||
|
||||
2
.github/workflows/sdk-codeql.yml
vendored
2
.github/workflows/sdk-codeql.yml
vendored
@@ -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/**'
|
||||
|
||||
6
.github/workflows/sdk-pull-request.yml
vendored
6
.github/workflows/sdk-pull-request.yml
vendored
@@ -37,14 +37,12 @@ jobs:
|
||||
README.md
|
||||
mkdocs.yml
|
||||
.backportrc.json
|
||||
.env
|
||||
docker-compose*
|
||||
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry==1.8.5
|
||||
pipx install poetry
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
@@ -67,7 +65,7 @@ jobs:
|
||||
- name: Poetry check
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry check --lock
|
||||
poetry lock --check
|
||||
|
||||
- name: Lint with flake8
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
|
||||
30
.github/workflows/sdk-pypi-release.yml
vendored
30
.github/workflows/sdk-pypi-release.yml
vendored
@@ -10,40 +10,12 @@ env:
|
||||
CACHE: "poetry"
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
release-prowler-job:
|
||||
runs-on: ubuntu-latest
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
name: Release Prowler to PyPI
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ "${{ github.repository }}" != "prowler-cloud/prowler" ]]; then
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get Prowler version
|
||||
run: |
|
||||
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
|
||||
@@ -68,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pipx install poetry==1.8.5
|
||||
pipx install poetry
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -23,7 +23,6 @@ env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
|
||||
WORKING_DIRECTORY: ./ui
|
||||
|
||||
@@ -32,34 +31,19 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
|
||||
|
||||
jobs:
|
||||
repository-check:
|
||||
name: Repository check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_repo: ${{ steps.repository_check.outputs.is_repo }}
|
||||
steps:
|
||||
- name: Repository check
|
||||
id: repository_check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
|
||||
then
|
||||
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "This action only runs for prowler-cloud/prowler"
|
||||
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
needs: repository-check
|
||||
if: needs.repository-check.outputs.is_repo == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
|
||||
steps:
|
||||
- name: Repository check
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -93,6 +77,5 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
2
.github/workflows/ui-codeql.yml
vendored
2
.github/workflows/ui-codeql.yml
vendored
@@ -15,12 +15,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
|
||||
7
.github/workflows/ui-pull-request.yml
vendored
7
.github/workflows/ui-pull-request.yml
vendored
@@ -1,16 +1,9 @@
|
||||
name: UI - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- "v5.*"
|
||||
paths:
|
||||
- 'ui/**'
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,7 +45,6 @@ junit-reports/
|
||||
# Terraform
|
||||
.terraform*
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# .env
|
||||
ui/.env*
|
||||
|
||||
@@ -27,7 +27,6 @@ repos:
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
exclude: contrib
|
||||
|
||||
## PYTHON
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v2.3.1
|
||||
@@ -62,25 +61,8 @@ repos:
|
||||
rev: 1.8.0
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
name: API - poetry-check
|
||||
args: ["--directory=./api"]
|
||||
pass_filenames: false
|
||||
|
||||
- id: poetry-lock
|
||||
name: API - poetry-lock
|
||||
args: ["--no-update", "--directory=./api"]
|
||||
pass_filenames: false
|
||||
|
||||
- id: poetry-check
|
||||
name: SDK - poetry-check
|
||||
args: ["--directory=./"]
|
||||
pass_filenames: false
|
||||
|
||||
- id: poetry-lock
|
||||
name: SDK - poetry-lock
|
||||
args: ["--no-update", "--directory=./"]
|
||||
pass_filenames: false
|
||||
|
||||
args: ["--no-update"]
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.13.0-beta
|
||||
@@ -108,7 +90,7 @@ repos:
|
||||
- id: bandit
|
||||
name: bandit
|
||||
description: "Bandit is a tool for finding common security issues in Python code"
|
||||
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
|
||||
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/' -r .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
@@ -121,6 +103,7 @@ repos:
|
||||
- id: vulture
|
||||
name: vulture
|
||||
description: "Vulture finds unused code in Python programs."
|
||||
entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
|
||||
entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .'
|
||||
exclude: 'api/src/backend/'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
16
README.md
16
README.md
@@ -71,13 +71,10 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 564 | 82 | 30 | 10 |
|
||||
| GCP | 77 | 13 | 4 | 3 |
|
||||
| Azure | 140 | 18 | 5 | 3 |
|
||||
| Kubernetes | 83 | 7 | 2 | 7 |
|
||||
| Microsoft365 | 5 | 2 | 1 | 0 |
|
||||
|
||||
> You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
|
||||
| AWS | 561 | 81 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 139 | 18 -> `prowler azure --list-services` | 4 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
@@ -101,7 +98,6 @@ curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/mast
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
### From GitHub
|
||||
@@ -170,7 +166,7 @@ npm start
|
||||
|
||||
## Prowler CLI
|
||||
### Pip package
|
||||
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python > 3.9.1, < 3.13:
|
||||
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9, < 3.13:
|
||||
|
||||
```console
|
||||
pip install prowler
|
||||
@@ -200,7 +196,7 @@ The container images are available here:
|
||||
|
||||
### From GitHub
|
||||
|
||||
Python > 3.9.1, < 3.13 is required with pip and poetry:
|
||||
Python >= 3.9, < 3.13 is required with pip and poetry:
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
|
||||
@@ -22,7 +22,6 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
# PostgreSQL settings
|
||||
# If running django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Prowler API Changelog
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [v1.4.0] (Prowler v5.3.0) - 2025-02-10
|
||||
|
||||
### Changed
|
||||
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700).
|
||||
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800).
|
||||
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863).
|
||||
- Increase the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869).
|
||||
|
||||
---
|
||||
@@ -28,7 +28,7 @@ start_prod_server() {
|
||||
|
||||
start_worker() {
|
||||
echo "Starting the worker..."
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E --max-tasks-per-child 1
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E
|
||||
}
|
||||
|
||||
start_worker_beat() {
|
||||
|
||||
1772
api/poetry.lock
generated
1772
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
name = "prowler-api"
|
||||
package-mode = false
|
||||
version = "1.4.0"
|
||||
version = "1.1.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
celery = {extras = ["pytest"], version = "^5.4.0"}
|
||||
django = "5.1.5"
|
||||
django = "5.1.1"
|
||||
django-celery-beat = "^2.7.0"
|
||||
django-celery-results = "^2.5.1"
|
||||
django-cors-headers = "4.4.0"
|
||||
@@ -27,7 +27,7 @@ drf-nested-routers = "^0.94.1"
|
||||
drf-spectacular = "0.27.2"
|
||||
drf-spectacular-jsonapi = "0.5.1"
|
||||
gunicorn = "23.0.0"
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "v5.3"}
|
||||
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
|
||||
@@ -37,7 +37,6 @@ uuid6 = "2024.7.10"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
bandit = "1.7.9"
|
||||
coverage = "7.5.4"
|
||||
django-silk = "5.3.2"
|
||||
docker = "7.1.0"
|
||||
freezegun = "1.5.1"
|
||||
mypy = "1.10.1"
|
||||
@@ -49,8 +48,8 @@ pytest-env = "1.1.3"
|
||||
pytest-randomly = "3.15.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
ruff = "0.5.0"
|
||||
safety = "3.2.9"
|
||||
vulture = "2.14"
|
||||
safety = "3.2.3"
|
||||
vulture = "2.11"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -4,17 +4,13 @@ class MainRouter:
|
||||
|
||||
def db_for_read(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_") or model_table_name.startswith(
|
||||
"silk_"
|
||||
):
|
||||
if model_table_name.startswith("django_"):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
def db_for_write(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_") or model_table_name.startswith(
|
||||
"silk_"
|
||||
):
|
||||
if model_table_name.startswith("django_"):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import connection, models, transaction
|
||||
from psycopg2 import connect as psycopg2_connect
|
||||
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
|
||||
@@ -119,18 +120,15 @@ def batch_delete(queryset, batch_size=5000):
|
||||
total_deleted = 0
|
||||
deletion_summary = {}
|
||||
|
||||
while True:
|
||||
# Get a batch of IDs to delete
|
||||
batch_ids = set(
|
||||
queryset.values_list("id", flat=True).order_by("id")[:batch_size]
|
||||
)
|
||||
if not batch_ids:
|
||||
# No more objects to delete
|
||||
break
|
||||
paginator = Paginator(queryset.order_by("id").only("id"), batch_size)
|
||||
|
||||
for page_num in paginator.page_range:
|
||||
batch_ids = [obj.id for obj in paginator.page(page_num).object_list]
|
||||
|
||||
deleted_count, deleted_info = queryset.filter(id__in=batch_ids).delete()
|
||||
|
||||
total_deleted += deleted_count
|
||||
|
||||
for model_label, count in deleted_info.items():
|
||||
deletion_summary[model_label] = deletion_summary.get(model_label, 0) + count
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
@@ -319,41 +319,13 @@ class FindingFilter(FilterSet):
|
||||
field_name="resources__type", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# resource_tag_key = CharFilter(field_name="resources__tags__key")
|
||||
# resource_tag_key__in = CharInFilter(
|
||||
# field_name="resources__tags__key", lookup_expr="in"
|
||||
# )
|
||||
# resource_tag_key__icontains = CharFilter(
|
||||
# field_name="resources__tags__key", lookup_expr="icontains"
|
||||
# )
|
||||
# resource_tag_value = CharFilter(field_name="resources__tags__value")
|
||||
# resource_tag_value__in = CharInFilter(
|
||||
# field_name="resources__tags__value", lookup_expr="in"
|
||||
# )
|
||||
# resource_tag_value__icontains = CharFilter(
|
||||
# field_name="resources__tags__value", lookup_expr="icontains"
|
||||
# )
|
||||
# resource_tags = CharInFilter(
|
||||
# method="filter_resource_tag",
|
||||
# lookup_expr="in",
|
||||
# help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be "
|
||||
# "separated by commas.",
|
||||
# )
|
||||
|
||||
scan = UUIDFilter(method="filter_scan_id")
|
||||
scan__in = UUIDInFilter(method="filter_scan_id_in")
|
||||
|
||||
inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__gte = DateFilter(
|
||||
method="filter_inserted_at_gte",
|
||||
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
)
|
||||
inserted_at__lte = DateFilter(
|
||||
method="filter_inserted_at_lte",
|
||||
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
)
|
||||
inserted_at__gte = DateFilter(method="filter_inserted_at_gte")
|
||||
inserted_at__lte = DateFilter(method="filter_inserted_at_lte")
|
||||
|
||||
class Meta:
|
||||
model = Finding
|
||||
@@ -381,52 +353,6 @@ class FindingFilter(FilterSet):
|
||||
},
|
||||
}
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
|
||||
self.data.get("inserted_at")
|
||||
or self.data.get("inserted_at__date")
|
||||
or self.data.get("inserted_at__gte")
|
||||
or self.data.get("inserted_at__lte")
|
||||
):
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], "
|
||||
"or filter[inserted_at.lte].",
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/attributes/inserted_at"},
|
||||
"code": "required",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
gte_date = (
|
||||
datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date()
|
||||
if self.data.get("inserted_at__gte")
|
||||
else datetime.now(timezone.utc).date()
|
||||
)
|
||||
lte_date = (
|
||||
datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date()
|
||||
if self.data.get("inserted_at__lte")
|
||||
else datetime.now(timezone.utc).date()
|
||||
)
|
||||
|
||||
if abs(lte_date - gte_date) > timedelta(
|
||||
days=settings.FINDINGS_MAX_DAYS_IN_RANGE
|
||||
):
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/attributes/inserted_at"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
# Convert filter values to UUIDv7 values for use with partitioning
|
||||
def filter_scan_id(self, queryset, name, value):
|
||||
try:
|
||||
@@ -500,16 +426,6 @@ class FindingFilter(FilterSet):
|
||||
|
||||
return queryset.filter(id__lte=end).filter(inserted_at__lte=value)
|
||||
|
||||
def filter_resource_tag(self, queryset, name, value):
|
||||
overall_query = Q()
|
||||
for key_value_pair in value:
|
||||
tag_key, tag_value = key_value_pair.split(":", 1)
|
||||
overall_query |= Q(
|
||||
resources__tags__key__icontains=tag_key,
|
||||
resources__tags__value__icontains=tag_value,
|
||||
)
|
||||
return queryset.filter(overall_query).distinct()
|
||||
|
||||
@staticmethod
|
||||
def maybe_date_to_datetime(value):
|
||||
dt = value
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.823Z",
|
||||
"updated_at": "2024-10-18T10:46:04.841Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.823Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-south-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -62,7 +61,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.855Z",
|
||||
"updated_at": "2024-10-18T10:46:04.858Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.855Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-3-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -118,7 +116,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.869Z",
|
||||
"updated_at": "2024-10-18T10:46:04.876Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.869Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -174,7 +171,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.888Z",
|
||||
"updated_at": "2024-10-18T10:46:04.892Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.888Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -230,7 +226,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.901Z",
|
||||
"updated_at": "2024-10-18T10:46:04.905Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.901Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -286,7 +281,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.915Z",
|
||||
"updated_at": "2024-10-18T10:46:04.919Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.915Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-south-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -342,7 +336,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.929Z",
|
||||
"updated_at": "2024-10-18T10:46:04.934Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.929Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -398,7 +391,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.944Z",
|
||||
"updated_at": "2024-10-18T10:46:04.947Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.944Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ca-central-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -454,7 +446,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.957Z",
|
||||
"updated_at": "2024-10-18T10:46:04.962Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.957Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-1-ConsoleAnalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c",
|
||||
"delta": "new",
|
||||
"status": "PASS",
|
||||
@@ -510,7 +501,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.971Z",
|
||||
"updated_at": "2024-10-18T10:46:04.975Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.971Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -566,7 +556,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.984Z",
|
||||
"updated_at": "2024-10-18T10:46:04.989Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.984Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-sa-east-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -622,7 +611,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.999Z",
|
||||
"updated_at": "2024-10-18T10:46:05.003Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.999Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-north-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -678,7 +666,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.013Z",
|
||||
"updated_at": "2024-10-18T10:46:05.018Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.013Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -734,7 +721,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.029Z",
|
||||
"updated_at": "2024-10-18T10:46:05.033Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.029Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -790,7 +776,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.045Z",
|
||||
"updated_at": "2024-10-18T10:46:05.050Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.045Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -846,7 +831,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.061Z",
|
||||
"updated_at": "2024-10-18T10:46:05.065Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.061Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -902,7 +886,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.080Z",
|
||||
"updated_at": "2024-10-18T10:46:05.085Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.080Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -958,7 +941,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.099Z",
|
||||
"updated_at": "2024-10-18T10:46:05.104Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.099Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-2-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -1014,7 +996,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.115Z",
|
||||
"updated_at": "2024-10-18T10:46:05.121Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.115Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-3-112233445566",
|
||||
"delta": "new",
|
||||
"status": "FAIL",
|
||||
@@ -1070,7 +1051,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.489Z",
|
||||
"updated_at": "2024-10-18T11:16:24.506Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.823Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-south-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1126,7 +1106,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.518Z",
|
||||
"updated_at": "2024-10-18T11:16:24.521Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.855Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-3-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1182,7 +1161,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.526Z",
|
||||
"updated_at": "2024-10-18T11:16:24.529Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.869Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1238,7 +1216,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.535Z",
|
||||
"updated_at": "2024-10-18T11:16:24.538Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.888Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1294,7 +1271,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.544Z",
|
||||
"updated_at": "2024-10-18T11:16:24.546Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.901Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1350,7 +1326,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.551Z",
|
||||
"updated_at": "2024-10-18T11:16:24.554Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.915Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-south-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1406,7 +1381,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.560Z",
|
||||
"updated_at": "2024-10-18T11:16:24.562Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.929Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1462,7 +1436,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.567Z",
|
||||
"updated_at": "2024-10-18T11:16:24.569Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.944Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ca-central-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1518,7 +1491,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.573Z",
|
||||
"updated_at": "2024-10-18T11:16:24.575Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.957Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-1-ConsoleAnalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c",
|
||||
"delta": null,
|
||||
"status": "PASS",
|
||||
@@ -1574,7 +1546,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.580Z",
|
||||
"updated_at": "2024-10-18T11:16:24.582Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.971Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1630,7 +1601,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.587Z",
|
||||
"updated_at": "2024-10-18T11:16:24.589Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.984Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-sa-east-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1686,7 +1656,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.595Z",
|
||||
"updated_at": "2024-10-18T11:16:24.597Z",
|
||||
"first_seen_at": "2024-10-18T10:46:04.999Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-north-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1742,7 +1711,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.602Z",
|
||||
"updated_at": "2024-10-18T11:16:24.604Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.013Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1798,7 +1766,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.610Z",
|
||||
"updated_at": "2024-10-18T11:16:24.612Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.029Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1854,7 +1821,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.617Z",
|
||||
"updated_at": "2024-10-18T11:16:24.620Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.045Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1910,7 +1876,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.625Z",
|
||||
"updated_at": "2024-10-18T11:16:24.627Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.061Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-1-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -1966,7 +1931,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.632Z",
|
||||
"updated_at": "2024-10-18T11:16:24.634Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.080Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -2022,7 +1986,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.639Z",
|
||||
"updated_at": "2024-10-18T11:16:24.642Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.099Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-2-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -2078,7 +2041,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:24.646Z",
|
||||
"updated_at": "2024-10-18T11:16:24.648Z",
|
||||
"first_seen_at": "2024-10-18T10:46:05.115Z",
|
||||
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-3-112233445566",
|
||||
"delta": null,
|
||||
"status": "FAIL",
|
||||
@@ -2134,7 +2096,6 @@
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:26.033Z",
|
||||
"updated_at": "2024-10-18T11:16:26.045Z",
|
||||
"first_seen_at": "2024-10-18T11:16:26.033Z",
|
||||
"uid": "prowler-aws-account_security_contact_information_is_registered-112233445566-us-east-1-112233445566",
|
||||
"delta": "new",
|
||||
"status": "MANUAL",
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-05 12:29
|
||||
|
||||
import uuid
|
||||
|
||||
import api.rls
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0003_update_provider_unique_constraint_with_is_deleted"),
|
||||
("api", "0002_token_migrations"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-20 13:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0002_token_migrations"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="provider",
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="provider",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "uid", "is_deleted"),
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_router import MainRouter
|
||||
|
||||
|
||||
@@ -36,7 +35,7 @@ def create_admin_role(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0004_rbac"),
|
||||
("api", "0003_rbac"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -1,15 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0005_rbac_missing_admin_roles"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="finding",
|
||||
name="first_seen_at",
|
||||
field=models.DateTimeField(editable=False, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-28 15:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0006_findings_first_seen"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="scan",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_id", "state", "inserted_at"],
|
||||
name="scans_prov_state_insert_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="scansummary",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "scan_id"], name="scan_summaries_tenant_scan_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,64 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Scan, StateChoices
|
||||
|
||||
|
||||
def migrate_daily_scheduled_scan_tasks(apps, schema_editor):
|
||||
for daily_scheduled_scan_task in PeriodicTask.objects.filter(
|
||||
task="scan-perform-scheduled"
|
||||
):
|
||||
task_kwargs = json.loads(daily_scheduled_scan_task.kwargs)
|
||||
tenant_id = task_kwargs["tenant_id"]
|
||||
provider_id = task_kwargs["provider_id"]
|
||||
|
||||
current_time = datetime.now(timezone.utc)
|
||||
scheduled_time_today = datetime.combine(
|
||||
current_time.date(),
|
||||
daily_scheduled_scan_task.start_time.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
if current_time < scheduled_time_today:
|
||||
next_scan_date = scheduled_time_today
|
||||
else:
|
||||
next_scan_date = scheduled_time_today + timedelta(days=1)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.SCHEDULED,
|
||||
scheduled_at=next_scan_date,
|
||||
scheduler_task_id=daily_scheduled_scan_task.id,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("api", "0007_scan_and_scan_summaries_indexes"),
|
||||
("django_celery_beat", "0019_alter_periodictasks_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scan",
|
||||
name="scheduler_task",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="django_celery_beat.periodictask",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_daily_scheduled_scan_tasks),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-07 09:42
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0008_daily_scheduled_tasks_update"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="provider",
|
||||
name="uid",
|
||||
field=models.CharField(
|
||||
max_length=250,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
verbose_name="Unique identifier for the provider, set by the provider",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -11,7 +11,6 @@ from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
from psqlextra.manager import PostgresManager
|
||||
from psqlextra.models import PostgresPartitionedModel
|
||||
@@ -227,13 +226,13 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
@staticmethod
|
||||
def validate_kubernetes_uid(value):
|
||||
if not re.match(
|
||||
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
|
||||
r"(^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$)|(^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$)",
|
||||
value,
|
||||
):
|
||||
raise ModelValidationError(
|
||||
detail="The value must either be a valid Kubernetes UID (up to 63 characters, "
|
||||
"starting and ending with a lowercase letter or number, containing only "
|
||||
"lowercase alphanumeric characters and hyphens) or a valid AWS EKS Cluster ARN, GCP GKE Context Name or Azure AKS Cluster Name.",
|
||||
"lowercase alphanumeric characters and hyphens) or a valid EKS ARN.",
|
||||
code="kubernetes-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
@@ -247,7 +246,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
)
|
||||
uid = models.CharField(
|
||||
"Unique identifier for the provider, set by the provider",
|
||||
max_length=250,
|
||||
max_length=63,
|
||||
blank=False,
|
||||
validators=[MinLengthValidator(3)],
|
||||
)
|
||||
@@ -272,7 +271,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "uid", "is_deleted"),
|
||||
fields=("tenant_id", "provider", "uid"),
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
@@ -411,9 +410,6 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
next_scan_at = models.DateTimeField(null=True, blank=True)
|
||||
scheduler_task = models.ForeignKey(
|
||||
PeriodicTask, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
# TODO: mutelist foreign key
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
@@ -432,10 +428,6 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
fields=["provider", "state", "trigger", "scheduled_at"],
|
||||
name="scans_prov_state_trig_sche_idx",
|
||||
),
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_id", "state", "inserted_at"],
|
||||
name="scans_prov_state_insert_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
@@ -523,8 +515,8 @@ class Resource(RowLevelSecurityProtectedModel):
|
||||
through="ResourceTagMapping",
|
||||
)
|
||||
|
||||
def get_tags(self, tenant_id: str) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.filter(tenant_id=tenant_id)}
|
||||
def get_tags(self) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.all()}
|
||||
|
||||
def clear_tags(self):
|
||||
self.tags.clear()
|
||||
@@ -623,7 +615,6 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
first_seen_at = models.DateTimeField(editable=False, null=True)
|
||||
|
||||
uid = models.CharField(max_length=300)
|
||||
delta = FindingDeltaEnumField(
|
||||
@@ -1108,12 +1099,6 @@ class ScanSummary(RowLevelSecurityProtectedModel):
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["tenant_id", "scan_id"],
|
||||
name="scan_summaries_tenant_scan_idx",
|
||||
)
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "scan-summaries"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.models import Provider, Role, User
|
||||
from api.db_router import MainRouter
|
||||
from typing import Optional
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
class Permissions(Enum):
|
||||
@@ -65,11 +63,8 @@ def get_providers(role: Role) -> QuerySet[Provider]:
|
||||
A QuerySet of Provider objects filtered by the role's provider groups.
|
||||
If the role has no provider groups, returns an empty queryset.
|
||||
"""
|
||||
tenant = role.tenant
|
||||
provider_groups = role.provider_groups.all()
|
||||
if not provider_groups.exists():
|
||||
return Provider.objects.none()
|
||||
|
||||
return Provider.objects.filter(
|
||||
tenant=tenant, provider_groups__in=provider_groups
|
||||
).distinct()
|
||||
return Provider.objects.filter(provider_groups__in=provider_groups).distinct()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.4.0
|
||||
version: 1.1.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -257,7 +257,6 @@ paths:
|
||||
- raw_result
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- first_seen_at
|
||||
- url
|
||||
- scan
|
||||
- resources
|
||||
@@ -346,9 +345,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: At least one of the variations of the `filter[inserted_at]` filter
|
||||
must be provided.
|
||||
required: true
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
@@ -359,13 +355,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -674,6 +668,8 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
@@ -719,7 +715,6 @@ paths:
|
||||
- raw_result
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- first_seen_at
|
||||
- url
|
||||
- scan
|
||||
- resources
|
||||
@@ -866,442 +861,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[provider__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[provider_alias]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[provider_alias__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[provider_alias__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[provider_uid]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[provider_uid__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[provider_uid__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_name]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_name__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_name__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_type]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_type__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resource_uid]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_uid__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[resource_uid__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[resources]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[scan]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[scan__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[service]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[service__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[service__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[severity]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- critical
|
||||
- high
|
||||
- informational
|
||||
- low
|
||||
- medium
|
||||
description: |-
|
||||
* `critical` - Critical
|
||||
* `high` - High
|
||||
* `medium` - Medium
|
||||
* `low` - Low
|
||||
* `informational` - Informational
|
||||
- in: query
|
||||
name: filter[severity__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[status]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- FAIL
|
||||
- MANUAL
|
||||
- MUTED
|
||||
- PASS
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
* `MANUAL` - Manual
|
||||
* `MUTED` - Muted
|
||||
- in: query
|
||||
name: filter[status__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[uid]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[uid__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[updated_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[updated_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[updated_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
- -severity
|
||||
- check_id
|
||||
- -check_id
|
||||
- inserted_at
|
||||
- -inserted_at
|
||||
- updated_at
|
||||
- -updated_at
|
||||
explode: false
|
||||
tags:
|
||||
- Finding
|
||||
security:
|
||||
- jwtAuth: []
|
||||
deprecated: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindingDynamicFilterResponse'
|
||||
description: ''
|
||||
/api/v1/findings/metadata:
|
||||
get:
|
||||
operationId: findings_metadata_retrieve
|
||||
description: Fetch unique metadata values from a set of findings. This is useful
|
||||
for dynamic filtering.
|
||||
summary: Retrieve metadata values from findings
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[findings-metadata]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- services
|
||||
- regions
|
||||
- resource_types
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[check_id]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[check_id__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[check_id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[delta]
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
enum:
|
||||
- changed
|
||||
- new
|
||||
description: |-
|
||||
* `new` - New
|
||||
* `changed` - Changed
|
||||
- in: query
|
||||
name: filter[delta__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[impact]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- critical
|
||||
- high
|
||||
- informational
|
||||
- low
|
||||
- medium
|
||||
description: |-
|
||||
* `critical` - Critical
|
||||
* `high` - High
|
||||
* `medium` - Medium
|
||||
* `low` - Low
|
||||
* `informational` - Informational
|
||||
- in: query
|
||||
name: filter[impact__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: At least one of the variations of the `filter[inserted_at]` filter
|
||||
must be provided.
|
||||
required: true
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -1586,6 +1150,8 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- status
|
||||
- -status
|
||||
- severity
|
||||
@@ -1602,11 +1168,11 @@ paths:
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
'201':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindingMetadataResponse'
|
||||
$ref: '#/components/schemas/OpenApiResponseResponse'
|
||||
description: ''
|
||||
/api/v1/invitations/accept:
|
||||
post:
|
||||
@@ -3382,7 +2948,9 @@ paths:
|
||||
- name
|
||||
- manage_users
|
||||
- manage_account
|
||||
- manage_billing
|
||||
- manage_providers
|
||||
- manage_integrations
|
||||
- manage_scans
|
||||
- permission_state
|
||||
- unlimited_visibility
|
||||
@@ -3500,8 +3068,12 @@ paths:
|
||||
- -manage_users
|
||||
- manage_account
|
||||
- -manage_account
|
||||
- manage_billing
|
||||
- -manage_billing
|
||||
- manage_providers
|
||||
- -manage_providers
|
||||
- manage_integrations
|
||||
- -manage_integrations
|
||||
- manage_scans
|
||||
- -manage_scans
|
||||
- permission_state
|
||||
@@ -3575,7 +3147,9 @@ paths:
|
||||
- name
|
||||
- manage_users
|
||||
- manage_account
|
||||
- manage_billing
|
||||
- manage_providers
|
||||
- manage_integrations
|
||||
- manage_scans
|
||||
- permission_state
|
||||
- unlimited_visibility
|
||||
@@ -5251,8 +4825,8 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: users_destroy
|
||||
description: Remove the current user account from the system.
|
||||
summary: Delete the user account
|
||||
description: Remove a user account from the system.
|
||||
summary: Delete a user account
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -5821,11 +5395,6 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
first_seen_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
nullable: true
|
||||
required:
|
||||
- uid
|
||||
- status
|
||||
@@ -5889,89 +5458,6 @@ components:
|
||||
readOnly: true
|
||||
required:
|
||||
- scan
|
||||
FindingDynamicFilter:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/FindingDynamicFilterTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
regions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- services
|
||||
- regions
|
||||
FindingDynamicFilterResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/FindingDynamicFilter'
|
||||
required:
|
||||
- data
|
||||
FindingDynamicFilterTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- finding-dynamic-filters
|
||||
FindingMetadata:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/FindingMetadataTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
regions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
resource_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- services
|
||||
- regions
|
||||
- resource_types
|
||||
FindingMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/FindingMetadata'
|
||||
required:
|
||||
- data
|
||||
FindingMetadataTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- findings-metadata
|
||||
FindingResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -6416,6 +5902,8 @@ components:
|
||||
- data
|
||||
description: A related resource object from type roles
|
||||
title: roles
|
||||
required:
|
||||
- roles
|
||||
InvitationUpdateResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -6427,6 +5915,7 @@ components:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
@@ -6435,6 +5924,9 @@ components:
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
@@ -6620,7 +6112,7 @@ components:
|
||||
type: integer
|
||||
fail:
|
||||
type: integer
|
||||
muted:
|
||||
manual:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
@@ -6945,6 +6437,8 @@ components:
|
||||
- data
|
||||
description: A related resource object from type roles
|
||||
title: roles
|
||||
required:
|
||||
- roles
|
||||
required:
|
||||
- data
|
||||
PatchedProviderGroupMembershipRequest:
|
||||
@@ -7150,9 +6644,6 @@ components:
|
||||
type: string
|
||||
description: The Amazon Resource Name (ARN) of the role to
|
||||
assume. Required for AWS role assumption.
|
||||
external_id:
|
||||
type: string
|
||||
description: An identifier to enhance security for role assumption.
|
||||
aws_access_key_id:
|
||||
type: string
|
||||
description: The AWS access key ID. Only required if the environment
|
||||
@@ -7171,6 +6662,10 @@ components:
|
||||
maximum: 43200
|
||||
default: 3600
|
||||
description: The duration (in seconds) for the role session.
|
||||
external_id:
|
||||
type: string
|
||||
description: An optional identifier to enhance security for
|
||||
role assumption; may be required by the role administrator.
|
||||
role_session_name:
|
||||
type: string
|
||||
description: |-
|
||||
@@ -7183,7 +6678,6 @@ components:
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
required:
|
||||
- role_arn
|
||||
- external_id
|
||||
- type: object
|
||||
title: Azure Static Credentials
|
||||
properties:
|
||||
@@ -7356,8 +6850,12 @@ components:
|
||||
type: boolean
|
||||
manage_account:
|
||||
type: boolean
|
||||
manage_billing:
|
||||
type: boolean
|
||||
manage_providers:
|
||||
type: boolean
|
||||
manage_integrations:
|
||||
type: boolean
|
||||
manage_scans:
|
||||
type: boolean
|
||||
permission_state:
|
||||
@@ -7633,6 +7131,37 @@ components:
|
||||
required:
|
||||
- name
|
||||
- email
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
roles:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
title: Resource Identifier
|
||||
description: The identifier of the related object.
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- roles
|
||||
title: Resource Type Name
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share
|
||||
common attributes and relationships.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
required:
|
||||
- data
|
||||
description: A related resource object from type roles
|
||||
title: roles
|
||||
required:
|
||||
- data
|
||||
Provider:
|
||||
@@ -7677,7 +7206,7 @@ components:
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 250
|
||||
maxLength: 63
|
||||
minLength: 3
|
||||
alias:
|
||||
type: string
|
||||
@@ -7789,7 +7318,7 @@ components:
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 250
|
||||
maxLength: 63
|
||||
minLength: 3
|
||||
required:
|
||||
- uid
|
||||
@@ -7833,7 +7362,7 @@ components:
|
||||
type: string
|
||||
minLength: 3
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
maxLength: 250
|
||||
maxLength: 63
|
||||
required:
|
||||
- uid
|
||||
required:
|
||||
@@ -8361,9 +7890,6 @@ components:
|
||||
type: string
|
||||
description: The Amazon Resource Name (ARN) of the role to assume.
|
||||
Required for AWS role assumption.
|
||||
external_id:
|
||||
type: string
|
||||
description: An identifier to enhance security for role assumption.
|
||||
aws_access_key_id:
|
||||
type: string
|
||||
description: The AWS access key ID. Only required if the environment
|
||||
@@ -8381,6 +7907,10 @@ components:
|
||||
maximum: 43200
|
||||
default: 3600
|
||||
description: The duration (in seconds) for the role session.
|
||||
external_id:
|
||||
type: string
|
||||
description: An optional identifier to enhance security for role
|
||||
assumption; may be required by the role administrator.
|
||||
role_session_name:
|
||||
type: string
|
||||
description: |-
|
||||
@@ -8393,7 +7923,6 @@ components:
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
required:
|
||||
- role_arn
|
||||
- external_id
|
||||
- type: object
|
||||
title: Azure Static Credentials
|
||||
properties:
|
||||
@@ -8542,9 +8071,6 @@ components:
|
||||
type: string
|
||||
description: The Amazon Resource Name (ARN) of the role to
|
||||
assume. Required for AWS role assumption.
|
||||
external_id:
|
||||
type: string
|
||||
description: An identifier to enhance security for role assumption.
|
||||
aws_access_key_id:
|
||||
type: string
|
||||
description: The AWS access key ID. Only required if the environment
|
||||
@@ -8563,6 +8089,10 @@ components:
|
||||
maximum: 43200
|
||||
default: 3600
|
||||
description: The duration (in seconds) for the role session.
|
||||
external_id:
|
||||
type: string
|
||||
description: An optional identifier to enhance security for
|
||||
role assumption; may be required by the role administrator.
|
||||
role_session_name:
|
||||
type: string
|
||||
description: |-
|
||||
@@ -8575,7 +8105,6 @@ components:
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
required:
|
||||
- role_arn
|
||||
- external_id
|
||||
- type: object
|
||||
title: Azure Static Credentials
|
||||
properties:
|
||||
@@ -8741,9 +8270,6 @@ components:
|
||||
type: string
|
||||
description: The Amazon Resource Name (ARN) of the role to assume.
|
||||
Required for AWS role assumption.
|
||||
external_id:
|
||||
type: string
|
||||
description: An identifier to enhance security for role assumption.
|
||||
aws_access_key_id:
|
||||
type: string
|
||||
description: The AWS access key ID. Only required if the environment
|
||||
@@ -8761,6 +8287,10 @@ components:
|
||||
maximum: 43200
|
||||
default: 3600
|
||||
description: The duration (in seconds) for the role session.
|
||||
external_id:
|
||||
type: string
|
||||
description: An optional identifier to enhance security for role
|
||||
assumption; may be required by the role administrator.
|
||||
role_session_name:
|
||||
type: string
|
||||
description: |-
|
||||
@@ -8773,7 +8303,6 @@ components:
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
required:
|
||||
- role_arn
|
||||
- external_id
|
||||
- type: object
|
||||
title: Azure Static Credentials
|
||||
properties:
|
||||
@@ -9008,8 +8537,12 @@ components:
|
||||
type: boolean
|
||||
manage_account:
|
||||
type: boolean
|
||||
manage_billing:
|
||||
type: boolean
|
||||
manage_providers:
|
||||
type: boolean
|
||||
manage_integrations:
|
||||
type: boolean
|
||||
manage_scans:
|
||||
type: boolean
|
||||
permission_state:
|
||||
@@ -9137,8 +8670,12 @@ components:
|
||||
type: boolean
|
||||
manage_account:
|
||||
type: boolean
|
||||
manage_billing:
|
||||
type: boolean
|
||||
manage_providers:
|
||||
type: boolean
|
||||
manage_integrations:
|
||||
type: boolean
|
||||
manage_scans:
|
||||
type: boolean
|
||||
permission_state:
|
||||
@@ -9271,8 +8808,12 @@ components:
|
||||
type: boolean
|
||||
manage_account:
|
||||
type: boolean
|
||||
manage_billing:
|
||||
type: boolean
|
||||
manage_providers:
|
||||
type: boolean
|
||||
manage_integrations:
|
||||
type: boolean
|
||||
manage_scans:
|
||||
type: boolean
|
||||
permission_state:
|
||||
@@ -10336,6 +9877,37 @@ components:
|
||||
required:
|
||||
- name
|
||||
- email
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
roles:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
title: Resource Identifier
|
||||
description: The identifier of the related object.
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- roles
|
||||
title: Resource Type Name
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common
|
||||
attributes and relationships.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
required:
|
||||
- data
|
||||
description: A related resource object from type roles
|
||||
title: roles
|
||||
UserUpdateResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import pytest
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_basic_authentication():
|
||||
@@ -95,85 +96,3 @@ def test_refresh_token(create_test_user, tenants_fixture):
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert new_refresh_response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fixture):
|
||||
client = APIClient()
|
||||
|
||||
role = roles_fixture[0]
|
||||
|
||||
user1_email = "user1@testing.com"
|
||||
user2_email = "user2@testing.com"
|
||||
|
||||
password = "thisisapassword123"
|
||||
|
||||
user1_response = client.post(
|
||||
reverse("user-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "user1",
|
||||
"email": user1_email,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user1_response.status_code == 201
|
||||
|
||||
user1_access_token, _ = get_api_tokens(client, user1_email, password)
|
||||
user1_headers = get_authorization_header(user1_access_token)
|
||||
|
||||
user2_invitation = client.post(
|
||||
reverse("invitation-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "invitations",
|
||||
"attributes": {"email": user2_email},
|
||||
"relationships": {
|
||||
"roles": {
|
||||
"data": [
|
||||
{
|
||||
"type": "roles",
|
||||
"id": str(role.id),
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=user1_headers,
|
||||
)
|
||||
assert user2_invitation.status_code == 201
|
||||
invitation_token = user2_invitation.json()["data"]["attributes"]["token"]
|
||||
|
||||
user2_response = client.post(
|
||||
reverse("user-list") + f"?invitation_token={invitation_token}",
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "user2",
|
||||
"email": user2_email,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user2_response.status_code == 201
|
||||
|
||||
user2_access_token, _ = get_api_tokens(client, user2_email, password)
|
||||
user2_headers = get_authorization_header(user2_access_token)
|
||||
|
||||
user1_me = client.get(reverse("user-me"), headers=user1_headers)
|
||||
assert user1_me.status_code == 200
|
||||
assert user1_me.json()["data"]["attributes"]["email"] == user1_email
|
||||
|
||||
user2_me = client.get(reverse("user-me"), headers=user2_headers)
|
||||
assert user2_me.status_code == 200
|
||||
assert user2_me.json()["data"]["attributes"]["email"] == user2_email
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from conftest import get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.models import Provider
|
||||
|
||||
|
||||
@patch("api.v1.views.Task.objects.get")
|
||||
@patch("api.v1.views.delete_provider_task.delay")
|
||||
@pytest.mark.django_db
|
||||
def test_delete_provider_without_executing_task(
|
||||
mock_delete_task, mock_task_get, create_test_user, tenants_fixture, tasks_fixture
|
||||
):
|
||||
client = APIClient()
|
||||
|
||||
test_user = "test_email@prowler.com"
|
||||
test_password = "test_password"
|
||||
|
||||
prowler_task = tasks_fixture[0]
|
||||
task_mock = Mock()
|
||||
task_mock.id = prowler_task.id
|
||||
mock_delete_task.return_value = task_mock
|
||||
mock_task_get.return_value = prowler_task
|
||||
|
||||
user_creation_response = client.post(
|
||||
reverse("user-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "test",
|
||||
"email": test_user,
|
||||
"password": test_password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user_creation_response.status_code == 201
|
||||
|
||||
access_token, _ = get_api_tokens(client, test_user, test_password)
|
||||
auth_headers = get_authorization_header(access_token)
|
||||
|
||||
create_provider_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": Provider.ProviderChoices.AWS,
|
||||
"uid": "123456789012",
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert create_provider_response.status_code == 201
|
||||
provider_id = create_provider_response.json()["data"]["id"]
|
||||
provider_uid = create_provider_response.json()["data"]["attributes"]["uid"]
|
||||
|
||||
remove_provider = client.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider_id}),
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert remove_provider.status_code == 202
|
||||
|
||||
recreate_provider_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": Provider.ProviderChoices.AWS,
|
||||
"uid": provider_uid,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert recreate_provider_response.status_code == 201
|
||||
@@ -2,15 +2,7 @@ from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from api.db_utils import (
|
||||
batch_delete,
|
||||
enum_to_choices,
|
||||
generate_random_token,
|
||||
one_week_from_now,
|
||||
)
|
||||
from api.models import Provider
|
||||
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
|
||||
|
||||
|
||||
class TestEnumToChoices:
|
||||
@@ -114,26 +106,3 @@ class TestGenerateRandomToken:
|
||||
token = generate_random_token(length=5, symbols="")
|
||||
# Default symbols
|
||||
assert len(token) == 5
|
||||
|
||||
|
||||
class TestBatchDelete:
|
||||
@pytest.fixture
|
||||
def create_test_providers(self, tenants_fixture):
|
||||
tenant = tenants_fixture[0]
|
||||
provider_id = 123456789012
|
||||
provider_count = 10
|
||||
for i in range(provider_count):
|
||||
Provider.objects.create(
|
||||
tenant=tenant,
|
||||
uid=f"{provider_id + i}",
|
||||
provider=Provider.ProviderChoices.AWS,
|
||||
)
|
||||
return provider_count
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_batch_delete(self, create_test_providers):
|
||||
_, summary = batch_delete(
|
||||
Provider.objects.all(), batch_size=create_test_providers // 2
|
||||
)
|
||||
assert Provider.objects.all().count() == 0
|
||||
assert summary == {"api.Provider": create_test_providers}
|
||||
|
||||
@@ -7,10 +7,9 @@ from api.models import Resource, ResourceTag
|
||||
class TestResourceModel:
|
||||
def test_setting_tags(self, providers_fixture):
|
||||
provider, *_ = providers_fixture
|
||||
tenant_id = provider.tenant_id
|
||||
|
||||
resource = Resource.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=provider.tenant_id,
|
||||
provider=provider,
|
||||
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
|
||||
name="My Instance 1",
|
||||
@@ -21,12 +20,12 @@ class TestResourceModel:
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=provider.tenant_id,
|
||||
key="key",
|
||||
value="value",
|
||||
),
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=provider.tenant_id,
|
||||
key="key2",
|
||||
value="value2",
|
||||
),
|
||||
@@ -34,9 +33,9 @@ class TestResourceModel:
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert len(tags) == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
assert len(tags) == len(resource.tags.all())
|
||||
|
||||
tags_dict = resource.get_tags(tenant_id=tenant_id)
|
||||
tags_dict = resource.get_tags()
|
||||
|
||||
for tag in tags:
|
||||
assert tag.key in tags_dict
|
||||
@@ -44,51 +43,47 @@ class TestResourceModel:
|
||||
|
||||
def test_adding_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=resource.tenant_id,
|
||||
key="env",
|
||||
value="test",
|
||||
),
|
||||
]
|
||||
before_count = len(resource.tags.filter(tenant_id=tenant_id))
|
||||
before_count = len(resource.tags.all())
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert before_count + 1 == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
assert before_count + 1 == len(resource.tags.all())
|
||||
|
||||
tags_dict = resource.get_tags(tenant_id=tenant_id)
|
||||
tags_dict = resource.get_tags()
|
||||
|
||||
assert "env" in tags_dict
|
||||
assert tags_dict["env"] == "test"
|
||||
|
||||
def test_adding_duplicate_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
|
||||
tags = resource.tags.filter(tenant_id=tenant_id)
|
||||
tags = resource.tags.all()
|
||||
|
||||
before_count = len(resource.tags.filter(tenant_id=tenant_id))
|
||||
before_count = len(resource.tags.all())
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
# should be the same number of tags
|
||||
assert before_count == len(resource.tags.filter(tenant_id=tenant_id))
|
||||
assert before_count == len(resource.tags.all())
|
||||
|
||||
def test_add_tags_none(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
resource.upsert_or_delete_tags(None)
|
||||
|
||||
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
|
||||
assert resource.get_tags(tenant_id=tenant_id) == {}
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
|
||||
def test_clear_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
tenant_id = str(resource.tenant_id)
|
||||
resource.clear_tags()
|
||||
|
||||
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
|
||||
assert resource.get_tags(tenant_id=tenant_id) == {}
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
|
||||
@@ -5,7 +5,6 @@ from unittest.mock import ANY, Mock, patch
|
||||
import jwt
|
||||
import pytest
|
||||
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@@ -28,12 +27,6 @@ from api.rls import Tenant
|
||||
TODAY = str(datetime.today().date())
|
||||
|
||||
|
||||
def today_after_n_days(n_days: int) -> str:
|
||||
return datetime.strftime(
|
||||
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserViewSet:
|
||||
def test_users_list(self, authenticated_client, create_test_user):
|
||||
@@ -268,16 +261,6 @@ class TestUserViewSet:
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert not User.objects.filter(id=create_test_user.id).exists()
|
||||
|
||||
def test_users_destroy_other_user(
|
||||
self, authenticated_client, create_test_user, users_fixture
|
||||
):
|
||||
user = users_fixture[2]
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": str(user.id)})
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert User.objects.filter(id=create_test_user.id).exists()
|
||||
|
||||
def test_users_destroy_invalid_user(self, authenticated_client, create_test_user):
|
||||
another_user = User.objects.create_user(
|
||||
password="otherpassword", email="other@example.com"
|
||||
@@ -285,7 +268,7 @@ class TestUserViewSet:
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": another_user.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert User.objects.filter(id=another_user.id).exists()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -357,7 +340,7 @@ class TestTenantViewSet:
|
||||
def test_tenants_list(self, authenticated_client, tenants_fixture):
|
||||
response = authenticated_client.get(reverse("tenant-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2 # Test user belongs to 2 tenants
|
||||
assert len(response.json()["data"]) == len(tenants_fixture)
|
||||
|
||||
def test_tenants_retrieve(self, authenticated_client, tenants_fixture):
|
||||
tenant1, *_ = tenants_fixture
|
||||
@@ -487,11 +470,11 @@ class TestTenantViewSet:
|
||||
(
|
||||
[
|
||||
("name", "Tenant One", 1),
|
||||
("name.icontains", "Tenant", 2),
|
||||
("inserted_at", TODAY, 2),
|
||||
("inserted_at.gte", "2024-01-01", 2),
|
||||
("name.icontains", "Tenant", 3),
|
||||
("inserted_at", TODAY, 3),
|
||||
("inserted_at.gte", "2024-01-01", 3),
|
||||
("inserted_at.lte", "2024-01-01", 0),
|
||||
("updated_at.gte", "2024-01-01", 2),
|
||||
("updated_at.gte", "2024-01-01", 3),
|
||||
("updated_at.lte", "2024-01-01", 0),
|
||||
]
|
||||
),
|
||||
@@ -527,9 +510,7 @@ class TestTenantViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == page_size
|
||||
assert response.json()["meta"]["pagination"]["page"] == 1
|
||||
assert (
|
||||
response.json()["meta"]["pagination"]["pages"] == 2
|
||||
) # Test user belongs to 2 tenants
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
|
||||
|
||||
def test_tenants_list_page_number(self, authenticated_client, tenants_fixture):
|
||||
page_size = 1
|
||||
@@ -542,13 +523,13 @@ class TestTenantViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == page_size
|
||||
assert response.json()["meta"]["pagination"]["page"] == page_number
|
||||
assert response.json()["meta"]["pagination"]["pages"] == 2
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
|
||||
|
||||
def test_tenants_list_sort_name(self, authenticated_client, tenants_fixture):
|
||||
_, tenant2, _ = tenants_fixture
|
||||
response = authenticated_client.get(reverse("tenant-list"), {"sort": "-name"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2
|
||||
assert len(response.json()["data"]) == 3
|
||||
assert response.json()["data"][0]["attributes"]["name"] == tenant2.name
|
||||
|
||||
def test_tenants_list_memberships_as_owner(
|
||||
@@ -885,16 +866,6 @@ class TestProviderViewSet:
|
||||
"uid": "kubernetes-test-123456789",
|
||||
"alias": "test",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
|
||||
"alias": "EKS",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
|
||||
"alias": "GKE",
|
||||
},
|
||||
{
|
||||
"provider": "azure",
|
||||
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
@@ -1476,8 +1447,12 @@ class TestProviderGroupViewSet:
|
||||
"id": str(group.id),
|
||||
"type": "provider-groups",
|
||||
"relationships": {
|
||||
"providers": {"data": []}, # Removing all providers
|
||||
"roles": {"data": []}, # Removing all roles
|
||||
"providers": {
|
||||
"data": [] # Removing all providers
|
||||
},
|
||||
"roles": {
|
||||
"data": [] # Removing all roles
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2368,10 +2343,7 @@ class TestResourceViewSet:
|
||||
response.json()["errors"][0]["detail"] == "invalid sort parameter: invalid"
|
||||
)
|
||||
|
||||
def test_resources_retrieve(
|
||||
self, authenticated_client, tenants_fixture, resources_fixture
|
||||
):
|
||||
tenant = tenants_fixture[0]
|
||||
def test_resources_retrieve(self, authenticated_client, resources_fixture):
|
||||
resource_1, *_ = resources_fixture
|
||||
response = authenticated_client.get(
|
||||
reverse("resource-detail", kwargs={"pk": resource_1.id}),
|
||||
@@ -2382,9 +2354,7 @@ class TestResourceViewSet:
|
||||
assert response.json()["data"]["attributes"]["region"] == resource_1.region
|
||||
assert response.json()["data"]["attributes"]["service"] == resource_1.service
|
||||
assert response.json()["data"]["attributes"]["type"] == resource_1.type
|
||||
assert response.json()["data"]["attributes"]["tags"] == resource_1.get_tags(
|
||||
tenant_id=str(tenant.id)
|
||||
)
|
||||
assert response.json()["data"]["attributes"]["tags"] == resource_1.get_tags()
|
||||
|
||||
def test_resources_invalid_retrieve(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
@@ -2396,33 +2366,12 @@ class TestResourceViewSet:
|
||||
@pytest.mark.django_db
|
||||
class TestFindingViewSet:
|
||||
def test_findings_list_none(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"filter[inserted_at]": TODAY}
|
||||
)
|
||||
response = authenticated_client.get(reverse("finding-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 0
|
||||
|
||||
def test_findings_list_no_date_filter(self, authenticated_client):
|
||||
response = authenticated_client.get(reverse("finding-list"))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "required"
|
||||
|
||||
def test_findings_date_range_too_large(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{
|
||||
"filter[inserted_at.lte]": today_after_n_days(
|
||||
-(settings.FINDINGS_MAX_DAYS_IN_RANGE + 1)
|
||||
),
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "invalid"
|
||||
|
||||
def test_findings_list(self, authenticated_client, findings_fixture):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"filter[inserted_at]": TODAY}
|
||||
)
|
||||
response = authenticated_client.get(reverse("finding-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(findings_fixture)
|
||||
assert (
|
||||
@@ -2442,8 +2391,7 @@ class TestFindingViewSet:
|
||||
self, include_values, expected_resources, authenticated_client, findings_fixture
|
||||
):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{"include": include_values, "filter[inserted_at]": TODAY},
|
||||
reverse("finding-list"), {"include": include_values}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(findings_fixture)
|
||||
@@ -2478,13 +2426,9 @@ class TestFindingViewSet:
|
||||
("service.icontains", "ec", 1),
|
||||
("inserted_at", "2024-01-01", 0),
|
||||
("inserted_at.date", "2024-01-01", 0),
|
||||
("inserted_at.gte", today_after_n_days(-1), 2),
|
||||
(
|
||||
"inserted_at.lte",
|
||||
today_after_n_days(1),
|
||||
2,
|
||||
),
|
||||
("updated_at.lte", today_after_n_days(-1), 0),
|
||||
("inserted_at.gte", "2024-01-01", 2),
|
||||
("inserted_at.lte", "2024-12-31", 2),
|
||||
("updated_at.lte", "2024-01-01", 0),
|
||||
("resource_type.icontains", "prowler", 2),
|
||||
# full text search on finding
|
||||
("search", "dev-qa", 1),
|
||||
@@ -2493,16 +2437,6 @@ class TestFindingViewSet:
|
||||
("search", "ec2", 2),
|
||||
# full text search on finding tags
|
||||
("search", "value2", 2),
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# ("resource_tag_key", "key", 2),
|
||||
# ("resource_tag_key__in", "key,key2", 2),
|
||||
# ("resource_tag_key__icontains", "key", 2),
|
||||
# ("resource_tag_value", "value", 2),
|
||||
# ("resource_tag_value__in", "value,value2", 2),
|
||||
# ("resource_tag_value__icontains", "value", 2),
|
||||
# ("resource_tags", "key:value", 2),
|
||||
# ("resource_tags", "not:exists", 0),
|
||||
# ("resource_tags", "not:exists,key:value", 2),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -2514,13 +2448,9 @@ class TestFindingViewSet:
|
||||
filter_value,
|
||||
expected_count,
|
||||
):
|
||||
filters = {f"filter[{filter_name}]": filter_value}
|
||||
if "inserted_at" not in filter_name:
|
||||
filters["filter[inserted_at]"] = TODAY
|
||||
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
filters,
|
||||
{f"filter[{filter_name}]": filter_value},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2529,7 +2459,9 @@ class TestFindingViewSet:
|
||||
def test_finding_filter_by_scan_id(self, authenticated_client, findings_fixture):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"),
|
||||
{"filter[scan]": findings_fixture[0].scan.id},
|
||||
{
|
||||
"filter[scan]": findings_fixture[0].scan.id,
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2
|
||||
@@ -2552,7 +2484,6 @@ class TestFindingViewSet:
|
||||
reverse("finding-list"),
|
||||
{
|
||||
"filter[provider]": findings_fixture[0].scan.provider.id,
|
||||
"filter[inserted_at]": TODAY,
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2567,8 +2498,7 @@ class TestFindingViewSet:
|
||||
"filter[provider.in]": [
|
||||
findings_fixture[0].scan.provider.id,
|
||||
findings_fixture[1].scan.provider.id,
|
||||
],
|
||||
"filter[inserted_at]": TODAY,
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -2602,13 +2532,13 @@ class TestFindingViewSet:
|
||||
)
|
||||
def test_findings_sort(self, authenticated_client, sort_field):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"sort": sort_field, "filter[inserted_at]": TODAY}
|
||||
reverse("finding-list"), {"sort": sort_field}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_findings_sort_invalid(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-list"), {"sort": "invalid", "filter[inserted_at]": TODAY}
|
||||
reverse("finding-list"), {"sort": "invalid"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "invalid"
|
||||
@@ -2645,74 +2575,59 @@ class TestFindingViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_findings_metadata_retrieve(self, authenticated_client, findings_fixture):
|
||||
def test_findings_services_regions_retrieve(
|
||||
self, authenticated_client, findings_fixture
|
||||
):
|
||||
finding_1, *_ = findings_fixture
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
reverse("finding-findings_services_regions"),
|
||||
{"filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d")},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
expected_services = {"ec2", "s3"}
|
||||
expected_regions = {"eu-west-1", "us-east-1"}
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
expected_resource_types = {"prowler-test"}
|
||||
|
||||
assert data["data"]["type"] == "findings-metadata"
|
||||
assert data["data"]["type"] == "finding-dynamic-filters"
|
||||
assert data["data"]["id"] is None
|
||||
assert set(data["data"]["attributes"]["services"]) == expected_services
|
||||
assert set(data["data"]["attributes"]["regions"]) == expected_regions
|
||||
assert (
|
||||
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
|
||||
)
|
||||
# assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
|
||||
def test_findings_metadata_severity_retrieve(
|
||||
def test_findings_services_regions_severity_retrieve(
|
||||
self, authenticated_client, findings_fixture
|
||||
):
|
||||
finding_1, *_ = findings_fixture
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
reverse("finding-findings_services_regions"),
|
||||
{
|
||||
"filter[severity__in]": ["low", "medium"],
|
||||
"filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"),
|
||||
"filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d"),
|
||||
},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
expected_services = {"s3"}
|
||||
expected_regions = {"eu-west-1"}
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# expected_tags = {"key": ["value"], "key2": ["value2"]}
|
||||
expected_resource_types = {"prowler-test"}
|
||||
|
||||
assert data["data"]["type"] == "findings-metadata"
|
||||
assert data["data"]["type"] == "finding-dynamic-filters"
|
||||
assert data["data"]["id"] is None
|
||||
assert set(data["data"]["attributes"]["services"]) == expected_services
|
||||
assert set(data["data"]["attributes"]["regions"]) == expected_regions
|
||||
assert (
|
||||
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
|
||||
)
|
||||
# assert data["data"]["attributes"]["tags"] == expected_tags
|
||||
|
||||
def test_findings_metadata_future_date(self, authenticated_client):
|
||||
def test_findings_services_regions_future_date(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
reverse("finding-findings_services_regions"),
|
||||
{"filter[inserted_at]": "2048-01-01"},
|
||||
)
|
||||
data = response.json()
|
||||
assert data["data"]["type"] == "findings-metadata"
|
||||
assert data["data"]["type"] == "finding-dynamic-filters"
|
||||
assert data["data"]["id"] is None
|
||||
assert data["data"]["attributes"]["services"] == []
|
||||
assert data["data"]["attributes"]["regions"] == []
|
||||
# Temporary disabled until we implement tag filtering in the UI
|
||||
# assert data["data"]["attributes"]["tags"] == {}
|
||||
assert data["data"]["attributes"]["resource_types"] == []
|
||||
|
||||
def test_findings_metadata_invalid_date(self, authenticated_client):
|
||||
def test_findings_services_regions_invalid_date(self, authenticated_client):
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-metadata"),
|
||||
reverse("finding-findings_services_regions"),
|
||||
{"filter[inserted_at]": "2048-01-011"},
|
||||
)
|
||||
assert response.json() == {
|
||||
@@ -2919,10 +2834,9 @@ class TestInvitationViewSet:
|
||||
)
|
||||
|
||||
def test_invitations_partial_update_valid(
|
||||
self, authenticated_client, invitations_fixture, roles_fixture
|
||||
self, authenticated_client, invitations_fixture
|
||||
):
|
||||
invitation, *_ = invitations_fixture
|
||||
role1, role2, *_ = roles_fixture
|
||||
new_email = "new_email@prowler.com"
|
||||
new_expires_at = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
new_expires_at_iso = new_expires_at.isoformat()
|
||||
@@ -2934,14 +2848,6 @@ class TestInvitationViewSet:
|
||||
"email": new_email,
|
||||
"expires_at": new_expires_at_iso,
|
||||
},
|
||||
"relationships": {
|
||||
"roles": {
|
||||
"data": [
|
||||
{"type": "roles", "id": str(role1.id)},
|
||||
{"type": "roles", "id": str(role2.id)},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
assert invitation.email != new_email
|
||||
@@ -2960,7 +2866,6 @@ class TestInvitationViewSet:
|
||||
|
||||
assert invitation.email == new_email
|
||||
assert invitation.expires_at == new_expires_at
|
||||
assert invitation.roles.count() == 2
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"email",
|
||||
@@ -3346,8 +3251,8 @@ class TestRoleViewSet:
|
||||
response = authenticated_client.get(reverse("role-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
len(response.json()["data"]) == len(roles_fixture) + 1
|
||||
) # 1 default admin role
|
||||
len(response.json()["data"]) == len(roles_fixture) + 2
|
||||
) # 2 default admin roles, one for each tenant
|
||||
|
||||
def test_role_retrieve(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[0]
|
||||
@@ -3385,7 +3290,9 @@ class TestRoleViewSet:
|
||||
"name": "Test Role",
|
||||
"manage_users": "false",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "false",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "true",
|
||||
"manage_scans": "true",
|
||||
"unlimited_visibility": "true",
|
||||
},
|
||||
@@ -3412,7 +3319,9 @@ class TestRoleViewSet:
|
||||
"name": "Test Role",
|
||||
"manage_users": "false",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "false",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "true",
|
||||
"manage_scans": "true",
|
||||
"unlimited_visibility": "true",
|
||||
},
|
||||
@@ -3460,26 +3369,6 @@ class TestRoleViewSet:
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
|
||||
|
||||
def test_admin_role_partial_update(self, authenticated_client, admin_role_fixture):
|
||||
role = admin_role_fixture
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(role.id),
|
||||
"type": "roles",
|
||||
"attributes": {
|
||||
"name": "Updated Role",
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("role-detail", kwargs={"pk": role.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
role.refresh_from_db()
|
||||
assert role.name != "Updated Role"
|
||||
|
||||
def test_role_partial_update(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[1]
|
||||
data = {
|
||||
@@ -3487,7 +3376,7 @@ class TestRoleViewSet:
|
||||
"id": str(role.id),
|
||||
"type": "roles",
|
||||
"attributes": {
|
||||
"name": "Updated Role",
|
||||
"name": "Updated Provider Group Name",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3498,7 +3387,7 @@ class TestRoleViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
role.refresh_from_db()
|
||||
assert role.name == "Updated Role"
|
||||
assert role.name == "Updated Provider Group Name"
|
||||
|
||||
def test_role_partial_update_invalid(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[2]
|
||||
@@ -3520,14 +3409,6 @@ class TestRoleViewSet:
|
||||
errors = response.json()["errors"]
|
||||
assert errors[0]["source"]["pointer"] == "/data/attributes/name"
|
||||
|
||||
def test_role_destroy_admin(self, authenticated_client, admin_role_fixture):
|
||||
role = admin_role_fixture
|
||||
response = authenticated_client.delete(
|
||||
reverse("role-detail", kwargs={"pk": role.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert Role.objects.filter(id=role.id).exists()
|
||||
|
||||
def test_role_destroy(self, authenticated_client, roles_fixture):
|
||||
role = roles_fixture[2]
|
||||
response = authenticated_client.delete(
|
||||
@@ -3586,7 +3467,9 @@ class TestRoleViewSet:
|
||||
"name": "Role with Users and PGs",
|
||||
"manage_users": "true",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "true",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "false",
|
||||
"manage_scans": "false",
|
||||
"unlimited_visibility": "false",
|
||||
},
|
||||
@@ -3671,8 +3554,12 @@ class TestRoleViewSet:
|
||||
"id": str(role.id),
|
||||
"type": "roles",
|
||||
"relationships": {
|
||||
"users": {"data": []}, # Clearing all users
|
||||
"provider_groups": {"data": []}, # Clearing all provider groups
|
||||
"users": {
|
||||
"data": [] # Clearing all users
|
||||
},
|
||||
"provider_groups": {
|
||||
"data": [] # Clearing all provider groups
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3700,7 +3587,9 @@ class TestRoleViewSet:
|
||||
"name": "Invalid Users Role",
|
||||
"manage_users": "false",
|
||||
"manage_account": "false",
|
||||
"manage_billing": "false",
|
||||
"manage_providers": "true",
|
||||
"manage_integrations": "true",
|
||||
"manage_scans": "true",
|
||||
"unlimited_visibility": "true",
|
||||
},
|
||||
@@ -4327,15 +4216,18 @@ class TestOverviewViewSet:
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
def test_overview_providers_list(
|
||||
self, authenticated_client, scan_summaries_fixture, resources_fixture
|
||||
self, authenticated_client, findings_fixture, resources_fixture
|
||||
):
|
||||
response = authenticated_client.get(reverse("overview-providers"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Only findings from one provider
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == 4
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == len(
|
||||
findings_fixture
|
||||
)
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 0
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["manual"] == 0
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == len(
|
||||
resources_fixture
|
||||
)
|
||||
|
||||
@@ -235,10 +235,13 @@ class UserCreateSerializer(BaseWriteSerializer):
|
||||
|
||||
class UserUpdateSerializer(BaseWriteSerializer):
|
||||
password = serializers.CharField(write_only=True, required=False)
|
||||
roles = serializers.ResourceRelatedField(
|
||||
queryset=Role.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "name", "password", "email", "company_name"]
|
||||
fields = ["id", "name", "password", "email", "company_name", "roles"]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
}
|
||||
@@ -503,6 +506,7 @@ class ProviderGroupCreateSerializer(ProviderGroupSerializer):
|
||||
"updated_at",
|
||||
"providers",
|
||||
"roles",
|
||||
"url",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -874,7 +878,7 @@ class ResourceSerializer(RLSSerializer):
|
||||
}
|
||||
)
|
||||
def get_tags(self, obj):
|
||||
return obj.get_tags(self.context.get("tenant_id"))
|
||||
return obj.get_tags()
|
||||
|
||||
def get_fields(self):
|
||||
"""`type` is a Python reserved keyword."""
|
||||
@@ -905,7 +909,6 @@ class FindingSerializer(RLSSerializer):
|
||||
"raw_result",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"first_seen_at",
|
||||
"url",
|
||||
# Relationships
|
||||
"scan",
|
||||
@@ -918,7 +921,6 @@ class FindingSerializer(RLSSerializer):
|
||||
}
|
||||
|
||||
|
||||
# To be removed when the related endpoint is removed as well
|
||||
class FindingDynamicFilterSerializer(serializers.Serializer):
|
||||
services = serializers.ListField(child=serializers.CharField(), allow_empty=True)
|
||||
regions = serializers.ListField(child=serializers.CharField(), allow_empty=True)
|
||||
@@ -927,19 +929,6 @@ class FindingDynamicFilterSerializer(serializers.Serializer):
|
||||
resource_name = "finding-dynamic-filters"
|
||||
|
||||
|
||||
class FindingMetadataSerializer(serializers.Serializer):
|
||||
services = serializers.ListField(child=serializers.CharField(), allow_empty=True)
|
||||
regions = serializers.ListField(child=serializers.CharField(), allow_empty=True)
|
||||
resource_types = serializers.ListField(
|
||||
child=serializers.CharField(), allow_empty=True
|
||||
)
|
||||
# Temporarily disabled until we implement tag filtering in the UI
|
||||
# tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
|
||||
|
||||
class Meta:
|
||||
resource_name = "findings-metadata"
|
||||
|
||||
|
||||
# Provider secrets
|
||||
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
|
||||
@staticmethod
|
||||
@@ -1012,7 +1001,7 @@ class KubernetesProviderSecret(serializers.Serializer):
|
||||
|
||||
class AWSRoleAssumptionProviderSecret(serializers.Serializer):
|
||||
role_arn = serializers.CharField()
|
||||
external_id = serializers.CharField()
|
||||
external_id = serializers.CharField(required=False)
|
||||
role_session_name = serializers.CharField(required=False)
|
||||
session_duration = serializers.IntegerField(
|
||||
required=False, min_value=900, max_value=43200
|
||||
@@ -1059,10 +1048,6 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
|
||||
"description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role "
|
||||
"assumption.",
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "An identifier to enhance security for role assumption.",
|
||||
},
|
||||
"aws_access_key_id": {
|
||||
"type": "string",
|
||||
"description": "The AWS access key ID. Only required if the environment lacks pre-configured "
|
||||
@@ -1084,6 +1069,11 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
|
||||
"default": 3600,
|
||||
"description": "The duration (in seconds) for the role session.",
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "An optional identifier to enhance security for role assumption; may be "
|
||||
"required by the role administrator.",
|
||||
},
|
||||
"role_session_name": {
|
||||
"type": "string",
|
||||
"description": "An identifier for the role session, useful for tracking sessions in AWS logs. "
|
||||
@@ -1097,7 +1087,7 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
|
||||
"pattern": "^[a-zA-Z0-9=,.@_-]+$",
|
||||
},
|
||||
},
|
||||
"required": ["role_arn", "external_id"],
|
||||
"required": ["role_arn"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
@@ -1247,12 +1237,6 @@ class InvitationSerializer(RLSSerializer):
|
||||
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = [
|
||||
@@ -1272,12 +1256,6 @@ class InvitationSerializer(RLSSerializer):
|
||||
class InvitationBaseWriteSerializer(BaseWriteSerializer):
|
||||
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
def validate_email(self, value):
|
||||
user = User.objects.filter(email=value).first()
|
||||
tenant_id = self.context["tenant_id"]
|
||||
@@ -1338,10 +1316,6 @@ class InvitationCreateSerializer(InvitationBaseWriteSerializer, RLSSerializer):
|
||||
|
||||
|
||||
class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
|
||||
roles = serializers.ResourceRelatedField(
|
||||
required=False, many=True, queryset=Role.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = ["id", "email", "expires_at", "state", "token", "roles"]
|
||||
@@ -1355,18 +1329,14 @@ class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
invitation = super().update(instance, validated_data)
|
||||
if "roles" in validated_data:
|
||||
roles = validated_data.pop("roles")
|
||||
instance.roles.clear()
|
||||
new_relationships = [
|
||||
InvitationRoleRelationship(
|
||||
role=r, invitation=instance, tenant_id=tenant_id
|
||||
for role in roles:
|
||||
InvitationRoleRelationship.objects.create(
|
||||
role=role, invitation=invitation, tenant_id=tenant_id
|
||||
)
|
||||
for r in roles
|
||||
]
|
||||
InvitationRoleRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
invitation = super().update(instance, validated_data)
|
||||
|
||||
return invitation
|
||||
|
||||
@@ -1393,17 +1363,6 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
queryset=ProviderGroup.objects.all(), many=True, required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
if tenant_id is not None:
|
||||
self.fields["users"].queryset = User.objects.filter(
|
||||
membership__tenant__id=tenant_id
|
||||
)
|
||||
self.fields["provider_groups"].queryset = ProviderGroup.objects.filter(
|
||||
tenant_id=self.context.get("tenant_id")
|
||||
)
|
||||
|
||||
def get_permission_state(self, obj) -> str:
|
||||
return obj.permission_state
|
||||
|
||||
@@ -1431,11 +1390,9 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"name",
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
# Disable for the first release
|
||||
# "manage_billing",
|
||||
# "manage_integrations",
|
||||
# /Disable for the first release
|
||||
"manage_billing",
|
||||
"manage_providers",
|
||||
"manage_integrations",
|
||||
"manage_scans",
|
||||
"permission_state",
|
||||
"unlimited_visibility",
|
||||
@@ -1736,7 +1693,7 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
"properties": {
|
||||
"pass": {"type": "integer"},
|
||||
"fail": {"type": "integer"},
|
||||
"muted": {"type": "integer"},
|
||||
"manual": {"type": "integer"},
|
||||
"total": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
@@ -1745,7 +1702,7 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
return {
|
||||
"pass": obj["findings_passed"],
|
||||
"fail": obj["findings_failed"],
|
||||
"muted": obj["findings_muted"],
|
||||
"manual": obj["findings_manual"],
|
||||
"total": obj["total_findings"],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,28 +3,28 @@ 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,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
ProviderGroupViewSet,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
ProviderSecretViewSet,
|
||||
InvitationViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
RoleViewSet,
|
||||
RoleProviderGroupRelationshipView,
|
||||
UserRoleRelationshipView,
|
||||
OverviewViewSet,
|
||||
ComplianceOverviewViewSet,
|
||||
ProviderViewSet,
|
||||
ResourceViewSet,
|
||||
RoleProviderGroupRelationshipView,
|
||||
RoleViewSet,
|
||||
ScanViewSet,
|
||||
ScheduleViewSet,
|
||||
SchemaView,
|
||||
TaskViewSet,
|
||||
TenantMembersViewSet,
|
||||
TenantViewSet,
|
||||
UserRoleRelationshipView,
|
||||
UserViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.search import SearchQuery
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
@@ -74,6 +73,7 @@ from api.models import (
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
Task,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
@@ -87,7 +87,6 @@ from api.v1.serializers import (
|
||||
ComplianceOverviewFullSerializer,
|
||||
ComplianceOverviewSerializer,
|
||||
FindingDynamicFilterSerializer,
|
||||
FindingMetadataSerializer,
|
||||
FindingSerializer,
|
||||
InvitationAcceptSerializer,
|
||||
InvitationCreateSerializer,
|
||||
@@ -99,9 +98,9 @@ from api.v1.serializers import (
|
||||
OverviewServiceSerializer,
|
||||
OverviewSeveritySerializer,
|
||||
ProviderCreateSerializer,
|
||||
ProviderGroupCreateSerializer,
|
||||
ProviderGroupMembershipSerializer,
|
||||
ProviderGroupSerializer,
|
||||
ProviderGroupCreateSerializer,
|
||||
ProviderGroupUpdateSerializer,
|
||||
ProviderSecretCreateSerializer,
|
||||
ProviderSecretSerializer,
|
||||
@@ -193,7 +192,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.4.0"
|
||||
spectacular_settings.VERSION = "1.1.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -276,8 +275,8 @@ class SchemaView(SpectacularAPIView):
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Delete the user account",
|
||||
description="Remove the current user account from the system.",
|
||||
summary="Delete a user account",
|
||||
description="Remove a user account from the system.",
|
||||
),
|
||||
me=extend_schema(
|
||||
tags=["User"],
|
||||
@@ -310,12 +309,7 @@ class UserViewSet(BaseUserViewset):
|
||||
# If called during schema generation, return an empty queryset
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return User.objects.none()
|
||||
queryset = (
|
||||
User.objects.filter(membership__tenant__id=self.request.tenant_id)
|
||||
if hasattr(self.request, "tenant_id")
|
||||
else User.objects.all()
|
||||
)
|
||||
return queryset.prefetch_related("memberships", "roles")
|
||||
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "create":
|
||||
@@ -334,19 +328,13 @@ class UserViewSet(BaseUserViewset):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="me")
|
||||
def me(self, request):
|
||||
user = self.request.user
|
||||
user = self.get_queryset().first()
|
||||
serializer = UserSerializer(user, context=self.get_serializer_context())
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if kwargs["pk"] != str(self.request.user.id):
|
||||
raise ValidationError("Only the current user can be deleted.")
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
@@ -464,7 +452,7 @@ class UserRoleRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_USERS]
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
|
||||
return User.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
@@ -552,8 +540,7 @@ class TenantViewSet(BaseTenantViewset):
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Tenant.objects.filter(membership__user=self.request.user)
|
||||
return queryset.prefetch_related("memberships")
|
||||
return Tenant.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
@@ -613,8 +600,7 @@ class MembershipViewSet(BaseTenantViewset):
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
queryset = Membership.objects.filter(user_id=user.id)
|
||||
return queryset.select_related("user", "tenant")
|
||||
return Membership.objects.filter(user_id=user.id)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
@@ -750,10 +736,10 @@ class ProviderGroupViewSet(BaseRLSViewSet):
|
||||
# Check if any of the user's roles have UNLIMITED_VISIBILITY
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all provider groups
|
||||
return ProviderGroup.objects.prefetch_related("providers", "roles")
|
||||
return ProviderGroup.objects.prefetch_related("providers")
|
||||
|
||||
# Collect provider groups associated with the user's roles
|
||||
return user_roles.provider_groups.all().prefetch_related("providers", "roles")
|
||||
return user_roles.provider_groups.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -804,7 +790,7 @@ class ProviderGroupProvidersRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_PROVIDERS]
|
||||
|
||||
def get_queryset(self):
|
||||
return ProviderGroup.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return ProviderGroup.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
provider_group = self.get_object()
|
||||
@@ -918,11 +904,10 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all providers
|
||||
queryset = Provider.objects.filter(tenant_id=self.request.tenant_id)
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
queryset = get_providers(user_roles)
|
||||
return queryset.select_related("secret").prefetch_related("provider_groups")
|
||||
return Provider.objects.all()
|
||||
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
return get_providers(user_roles)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -960,7 +945,7 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
get_object_or_404(Provider, pk=pk)
|
||||
with transaction.atomic():
|
||||
task = check_provider_connection_task.delay(
|
||||
provider_id=pk, tenant_id=self.request.tenant_id
|
||||
provider_id=pk, tenant_id=request.tenant_id
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
@@ -981,7 +966,7 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
|
||||
with transaction.atomic():
|
||||
task = delete_provider_task.delay(
|
||||
provider_id=pk, tenant_id=self.request.tenant_id
|
||||
provider_id=pk, tenant_id=request.tenant_id
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
@@ -1051,7 +1036,7 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
"""
|
||||
if self.request.method in SAFE_METHODS:
|
||||
# No permissions required for GET requests
|
||||
self.required_permissions = []
|
||||
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
|
||||
else:
|
||||
# Require permission for non-GET requests
|
||||
self.required_permissions = [Permissions.MANAGE_SCANS]
|
||||
@@ -1060,11 +1045,10 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Scan.objects.filter(tenant_id=self.request.tenant_id)
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
queryset = Scan.objects.filter(provider__in=get_providers(user_roles))
|
||||
return queryset.select_related("provider", "task")
|
||||
return Scan.objects.all()
|
||||
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
return Scan.objects.filter(provider__in=get_providers(user_roles))
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -1098,14 +1082,14 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
with transaction.atomic():
|
||||
task = perform_scan_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": self.request.tenant_id,
|
||||
"tenant_id": request.tenant_id,
|
||||
"scan_id": str(scan.id),
|
||||
"provider_id": str(scan.provider_id),
|
||||
# Disabled for now
|
||||
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
|
||||
},
|
||||
link=perform_scan_summary_task.si(
|
||||
tenant_id=self.request.tenant_id,
|
||||
tenant_id=request.tenant_id,
|
||||
scan_id=str(scan.id),
|
||||
),
|
||||
)
|
||||
@@ -1161,7 +1145,7 @@ class TaskViewSet(BaseRLSViewSet):
|
||||
return Task.objects.annotate(
|
||||
name=F("task_runner_task__task_name"),
|
||||
state=F("task_runner_task__status"),
|
||||
).select_related("task_runner_task")
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, pk=None, **kwargs):
|
||||
task = get_object_or_404(Task, pk=pk)
|
||||
@@ -1222,20 +1206,17 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
|
||||
# the provider through the provider group)
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of the provider through the provider group)
|
||||
required_permissions = []
|
||||
|
||||
def get_queryset(self):
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Resource.objects.filter(tenant_id=self.request.tenant_id)
|
||||
queryset = Resource.objects.all()
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
queryset = Resource.objects.filter(
|
||||
tenant_id=self.request.tenant_id, provider__in=get_providers(user_roles)
|
||||
)
|
||||
queryset = Resource.objects.filter(provider__in=get_providers(user_roles))
|
||||
|
||||
search_value = self.request.query_params.get("filter[search]", None)
|
||||
if search_value:
|
||||
@@ -1271,14 +1252,6 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
tags=["Finding"],
|
||||
summary="List all findings",
|
||||
description="Retrieve a list of all findings with options for filtering by various criteria.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="filter[inserted_at]",
|
||||
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
|
||||
required=True,
|
||||
type=OpenApiTypes.DATE,
|
||||
)
|
||||
],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Finding"],
|
||||
@@ -1289,21 +1262,7 @@ class ResourceViewSet(BaseRLSViewSet):
|
||||
tags=["Finding"],
|
||||
summary="Retrieve the services and regions that are impacted by findings",
|
||||
description="Fetch services and regions affected in findings.",
|
||||
filters=True,
|
||||
deprecated=True,
|
||||
),
|
||||
metadata=extend_schema(
|
||||
tags=["Finding"],
|
||||
summary="Retrieve metadata values from findings",
|
||||
description="Fetch unique metadata values from a set of findings. This is useful for dynamic filtering.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="filter[inserted_at]",
|
||||
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
|
||||
required=True,
|
||||
type=OpenApiTypes.DATE,
|
||||
)
|
||||
],
|
||||
responses={201: OpenApiResponse(response=MembershipSerializer)},
|
||||
filters=True,
|
||||
),
|
||||
)
|
||||
@@ -1321,23 +1280,21 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
}
|
||||
http_method_names = ["get"]
|
||||
filterset_class = FindingFilter
|
||||
ordering = ["-inserted_at"]
|
||||
ordering = ["-id"]
|
||||
ordering_fields = [
|
||||
"id",
|
||||
"status",
|
||||
"severity",
|
||||
"check_id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
|
||||
# the provider through the provider group)
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of the provider through the provider group)
|
||||
required_permissions = []
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "findings_services_regions":
|
||||
return FindingDynamicFilterSerializer
|
||||
elif self.action == "metadata":
|
||||
return FindingMetadataSerializer
|
||||
|
||||
return super().get_serializer_class()
|
||||
|
||||
@@ -1345,7 +1302,7 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all scans
|
||||
queryset = Finding.objects.filter(tenant_id=self.request.tenant_id)
|
||||
queryset = Finding.objects.all()
|
||||
else:
|
||||
# User lacks permission, filter providers based on provider groups associated with the role
|
||||
queryset = Finding.objects.filter(
|
||||
@@ -1380,12 +1337,6 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
# Do not apply filters when retrieving specific finding
|
||||
if self.action == "retrieve":
|
||||
return queryset
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
def inserted_at_to_uuidv7(self, inserted_at):
|
||||
if inserted_at is None:
|
||||
return None
|
||||
@@ -1412,38 +1363,6 @@ class FindingViewSet(BaseRLSViewSet):
|
||||
|
||||
return Response(data=serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="metadata")
|
||||
def metadata(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
filtered_ids = filtered_queryset.order_by().values("id")
|
||||
|
||||
relevant_resources = Resource.all_objects.filter(
|
||||
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
|
||||
).only("service", "region", "type")
|
||||
|
||||
aggregation = relevant_resources.aggregate(
|
||||
services=ArrayAgg("service", flat=True),
|
||||
regions=ArrayAgg("region", flat=True),
|
||||
resource_types=ArrayAgg("type", flat=True),
|
||||
)
|
||||
|
||||
services = sorted(set(aggregation["services"] or []))
|
||||
regions = sorted({region for region in aggregation["regions"] or [] if region})
|
||||
resource_types = sorted(set(aggregation["resource_types"] or []))
|
||||
|
||||
result = {
|
||||
"services": services,
|
||||
"regions": regions,
|
||||
"resource_types": resource_types,
|
||||
}
|
||||
|
||||
serializer = self.get_serializer(data=result)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
@@ -1490,7 +1409,7 @@ class ProviderSecretViewSet(BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_PROVIDERS]
|
||||
|
||||
def get_queryset(self):
|
||||
return ProviderSecret.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return ProviderSecret.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -1549,7 +1468,7 @@ class InvitationViewSet(BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
|
||||
def get_queryset(self):
|
||||
return Invitation.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return Invitation.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -1596,7 +1515,7 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
|
||||
http_method_names = ["post"]
|
||||
|
||||
def get_queryset(self):
|
||||
return Invitation.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return Invitation.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
@@ -1688,7 +1607,7 @@ class RoleViewSet(BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
|
||||
def get_queryset(self):
|
||||
return Role.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return Role.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
@@ -1704,22 +1623,12 @@ class RoleViewSet(BaseRLSViewSet):
|
||||
request.data["manage_account"] = str(user_role.manage_account).lower()
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if (
|
||||
instance.name == "admin"
|
||||
): # TODO: Move to a constant/enum (in case other roles are created by default)
|
||||
raise ValidationError(detail="The admin role cannot be deleted.")
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Create a new role-provider_groups relationship",
|
||||
description="Add a new role-provider_groups relationship to the system by providing the required "
|
||||
"role-provider_groups details.",
|
||||
description="Add a new role-provider_groups relationship to the system by providing the required role-provider_groups details.",
|
||||
responses={
|
||||
204: OpenApiResponse(description="Relationship created successfully"),
|
||||
400: OpenApiResponse(
|
||||
@@ -1758,7 +1667,7 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
|
||||
def get_queryset(self):
|
||||
return Role.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return Role.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
@@ -1841,8 +1750,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
|
||||
search_fields = ["compliance_id"]
|
||||
ordering = ["compliance_id"]
|
||||
ordering_fields = ["inserted_at", "compliance_id", "framework", "region"]
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
|
||||
# the provider through the provider group)
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of the provider through the provider group)
|
||||
required_permissions = []
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1853,28 +1761,20 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
if self.action == "retrieve":
|
||||
if unlimited_visibility:
|
||||
# User has unlimited visibility, return all compliance
|
||||
return ComplianceOverview.objects.filter(
|
||||
tenant_id=self.request.tenant_id
|
||||
)
|
||||
# User has unlimited visibility, return all compliance compliances
|
||||
return ComplianceOverview.objects.all()
|
||||
|
||||
providers = get_providers(role)
|
||||
return ComplianceOverview.objects.filter(
|
||||
tenant_id=self.request.tenant_id, scan__provider__in=providers
|
||||
)
|
||||
return ComplianceOverview.objects.filter(scan__provider__in=providers)
|
||||
|
||||
if unlimited_visibility:
|
||||
base_queryset = self.filter_queryset(
|
||||
ComplianceOverview.objects.filter(tenant_id=self.request.tenant_id)
|
||||
)
|
||||
base_queryset = self.filter_queryset(ComplianceOverview.objects.all())
|
||||
else:
|
||||
providers = Provider.objects.filter(
|
||||
provider_groups__in=role.provider_groups.all()
|
||||
).distinct()
|
||||
base_queryset = self.filter_queryset(
|
||||
ComplianceOverview.objects.filter(
|
||||
tenant_id=self.request.tenant_id, scan__provider__in=providers
|
||||
)
|
||||
ComplianceOverview.objects.filter(scan__provider__in=providers)
|
||||
)
|
||||
|
||||
max_failed_ids = (
|
||||
@@ -1953,8 +1853,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
queryset = ComplianceOverview.objects.all()
|
||||
http_method_names = ["get"]
|
||||
ordering = ["-id"]
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
|
||||
# the provider through the provider group)
|
||||
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of the provider through the provider group)
|
||||
required_permissions = []
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1963,10 +1862,8 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
def _get_filtered_queryset(model):
|
||||
if role.unlimited_visibility:
|
||||
return model.objects.filter(tenant_id=self.request.tenant_id)
|
||||
return model.objects.filter(
|
||||
tenant_id=self.request.tenant_id, scan__provider__in=providers
|
||||
)
|
||||
return model.objects.all()
|
||||
return model.objects.filter(scan__provider__in=providers)
|
||||
|
||||
if self.action == "providers":
|
||||
return _get_filtered_queryset(Finding)
|
||||
@@ -2005,69 +1902,74 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="providers")
|
||||
def providers(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
# Subquery to get the most recent finding for each uid
|
||||
latest_finding_ids = (
|
||||
Finding.objects.filter(
|
||||
uid=OuterRef("uid"), scan__provider=OuterRef("scan__provider")
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
.order_by("-id") # Most recent by id
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
# Filter findings to only include the most recent for each uid
|
||||
recent_findings = Finding.objects.filter(id__in=Subquery(latest_finding_ids))
|
||||
|
||||
# Aggregate findings by provider
|
||||
findings_aggregated = (
|
||||
ScanSummary.objects.filter(tenant_id=tenant_id, scan_id__in=latest_scan_ids)
|
||||
.values("scan__provider__provider")
|
||||
recent_findings.values("scan__provider__provider")
|
||||
.annotate(
|
||||
findings_passed=Coalesce(Sum("_pass"), 0),
|
||||
findings_failed=Coalesce(Sum("fail"), 0),
|
||||
findings_muted=Coalesce(Sum("muted"), 0),
|
||||
total_findings=Coalesce(Sum("total"), 0),
|
||||
findings_passed=Count("id", filter=Q(status=StatusChoices.PASS.value)),
|
||||
findings_failed=Count("id", filter=Q(status=StatusChoices.FAIL.value)),
|
||||
findings_manual=Count(
|
||||
"id", filter=Q(status=StatusChoices.MANUAL.value)
|
||||
),
|
||||
total_findings=Count("id"),
|
||||
)
|
||||
.order_by("-findings_failed")
|
||||
)
|
||||
|
||||
resources_aggregated = (
|
||||
Resource.objects.filter(tenant_id=tenant_id)
|
||||
.values("provider__provider")
|
||||
.annotate(total_resources=Count("id"))
|
||||
# Aggregate total resources by provider
|
||||
resources_aggregated = Resource.objects.values("provider__provider").annotate(
|
||||
total_resources=Count("id")
|
||||
)
|
||||
resources_dict = {
|
||||
row["provider__provider"]: row["total_resources"]
|
||||
for row in resources_aggregated
|
||||
}
|
||||
|
||||
# Combine findings and resources data
|
||||
overview = []
|
||||
for row in findings_aggregated:
|
||||
provider_type = row["scan__provider__provider"]
|
||||
for findings in findings_aggregated:
|
||||
provider = findings["scan__provider__provider"]
|
||||
total_resources = next(
|
||||
(
|
||||
res["total_resources"]
|
||||
for res in resources_aggregated
|
||||
if res["provider__provider"] == provider
|
||||
),
|
||||
0,
|
||||
)
|
||||
overview.append(
|
||||
{
|
||||
"provider": provider_type,
|
||||
"total_resources": resources_dict.get(provider_type, 0),
|
||||
"total_findings": row["total_findings"],
|
||||
"findings_passed": row["findings_passed"],
|
||||
"findings_failed": row["findings_failed"],
|
||||
"findings_muted": row["findings_muted"],
|
||||
"provider": provider,
|
||||
"total_resources": total_resources,
|
||||
"total_findings": findings["total_findings"],
|
||||
"findings_passed": findings["findings_passed"],
|
||||
"findings_failed": findings["findings_failed"],
|
||||
"findings_manual": findings["findings_manual"],
|
||||
}
|
||||
)
|
||||
|
||||
serializer = OverviewProviderSerializer(overview, many=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings")
|
||||
def findings(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id")
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.order_by("-id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
@@ -2102,17 +2004,14 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings_severity")
|
||||
def findings_severity(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id")
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.order_by("-id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
@@ -2138,17 +2037,14 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="services")
|
||||
def services(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
provider_id=OuterRef("scan__provider_id"),
|
||||
state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id")
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.order_by("-id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
@@ -207,4 +207,3 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
|
||||
|
||||
|
||||
TESTING = False
|
||||
FINDINGS_MAX_DAYS_IN_RANGE = env.int("DJANGO_FINDINGS_MAX_DAYS_IN_RANGE", 7)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=True)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
@@ -88,14 +88,16 @@ def create_test_user(django_db_setup, django_db_blocker):
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
|
||||
def create_test_user_rbac(django_db_setup, django_db_blocker):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing",
|
||||
email="rbac@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = tenants_fixture[0]
|
||||
tenant = Tenant.objects.create(
|
||||
name="Tenant Test",
|
||||
)
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
@@ -121,14 +123,16 @@ def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker, tenants_fixture):
|
||||
def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker):
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing",
|
||||
email="rbac_noroles@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = tenants_fixture[0]
|
||||
tenant = Tenant.objects.create(
|
||||
name="Tenant Test",
|
||||
)
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
@@ -176,16 +180,10 @@ def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
|
||||
@pytest.fixture
|
||||
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
|
||||
client.user = create_test_user_rbac
|
||||
tenant_id = tenants_fixture[0].id
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
data={"type": "tokens", "email": "rbac@rbac.com", "password": TEST_PASSWORD}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
@@ -305,7 +303,7 @@ def set_user_admin_roles_fixture(create_test_user, tenants_fixture):
|
||||
@pytest.fixture
|
||||
def invitations_fixture(create_test_user, tenants_fixture):
|
||||
user = create_test_user
|
||||
tenant = tenants_fixture[0]
|
||||
*_, tenant = tenants_fixture
|
||||
valid_invitation = Invitation.objects.create(
|
||||
email="testing@prowler.com",
|
||||
state=Invitation.State.PENDING,
|
||||
@@ -395,23 +393,6 @@ def provider_groups_fixture(tenants_fixture):
|
||||
return pgroup1, pgroup2, pgroup3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_role_fixture(tenants_fixture):
|
||||
tenant, *_ = tenants_fixture
|
||||
|
||||
return Role.objects.get_or_create(
|
||||
name="admin",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=True,
|
||||
manage_providers=True,
|
||||
manage_integrations=True,
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def roles_fixture(tenants_fixture):
|
||||
tenant, *_ = tenants_fixture
|
||||
@@ -626,7 +607,6 @@ def findings_fixture(scans_fixture, resources_fixture):
|
||||
"CheckId": "test_check_id",
|
||||
"Description": "test description apple sauce",
|
||||
},
|
||||
first_seen_at="2024-01-02T00:00:00Z",
|
||||
)
|
||||
|
||||
finding1.add_resources([resource1])
|
||||
@@ -652,7 +632,6 @@ def findings_fixture(scans_fixture, resources_fixture):
|
||||
"CheckId": "test_check_id",
|
||||
"Description": "test description orange juice",
|
||||
},
|
||||
first_seen_at="2024-01-02T00:00:00Z",
|
||||
)
|
||||
|
||||
finding2.add_resources([resource2])
|
||||
|
||||
@@ -5,14 +5,10 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from tasks.tasks import perform_scheduled_scan_task
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Provider, Scan, StateChoices
|
||||
from api.models import Provider
|
||||
|
||||
|
||||
def schedule_provider_scan(provider_instance: Provider):
|
||||
tenant_id = str(provider_instance.tenant_id)
|
||||
provider_id = str(provider_instance.id)
|
||||
|
||||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
||||
every=24,
|
||||
period=IntervalSchedule.HOURS,
|
||||
@@ -21,9 +17,23 @@ def schedule_provider_scan(provider_instance: Provider):
|
||||
# Create a unique name for the periodic task
|
||||
task_name = f"scan-perform-scheduled-{provider_instance.id}"
|
||||
|
||||
if PeriodicTask.objects.filter(
|
||||
interval=schedule, name=task_name, task="scan-perform-scheduled"
|
||||
).exists():
|
||||
# Schedule the task
|
||||
_, created = PeriodicTask.objects.get_or_create(
|
||||
interval=schedule,
|
||||
name=task_name,
|
||||
task="scan-perform-scheduled",
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"tenant_id": str(provider_instance.tenant_id),
|
||||
"provider_id": str(provider_instance.id),
|
||||
}
|
||||
),
|
||||
one_off=False,
|
||||
defaults={
|
||||
"start_time": datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
@@ -35,36 +45,9 @@ def schedule_provider_scan(provider_instance: Provider):
|
||||
]
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
scheduled_scan = Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.AVAILABLE,
|
||||
scheduled_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Schedule the task
|
||||
periodic_task_instance = PeriodicTask.objects.create(
|
||||
interval=schedule,
|
||||
name=task_name,
|
||||
task="scan-perform-scheduled",
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"provider_id": provider_id,
|
||||
}
|
||||
),
|
||||
one_off=False,
|
||||
start_time=datetime.now(timezone.utc) + timedelta(hours=24),
|
||||
)
|
||||
scheduled_scan.scheduler_task_id = periodic_task_instance.id
|
||||
scheduled_scan.save()
|
||||
|
||||
return perform_scheduled_scan_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": str(provider_instance.tenant_id),
|
||||
"provider_id": provider_id,
|
||||
"provider_id": str(provider_instance.id),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -116,6 +116,7 @@ def perform_prowler_scan(
|
||||
ValueError: If the provider cannot be connected.
|
||||
|
||||
"""
|
||||
generate_compliance = False
|
||||
check_status_by_region = {}
|
||||
exception = None
|
||||
unique_resources = set()
|
||||
@@ -144,6 +145,7 @@ def perform_prowler_scan(
|
||||
)
|
||||
provider_instance.save()
|
||||
|
||||
generate_compliance = provider_instance.provider != Provider.ProviderChoices.GCP
|
||||
prowler_scan = ProwlerScan(provider=prowler_provider, checks=checks_to_execute)
|
||||
|
||||
resource_cache = {}
|
||||
@@ -152,9 +154,6 @@ def perform_prowler_scan(
|
||||
|
||||
for progress, findings in prowler_scan.scan():
|
||||
for finding in findings:
|
||||
if finding is None:
|
||||
logger.error(f"None finding detected on scan {scan_id}.")
|
||||
continue
|
||||
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
@@ -179,10 +178,7 @@ def perform_prowler_scan(
|
||||
|
||||
# Update resource fields if necessary
|
||||
updated_fields = []
|
||||
if (
|
||||
finding.region
|
||||
and resource_instance.region != finding.region
|
||||
):
|
||||
if resource_instance.region != finding.region:
|
||||
resource_instance.region = finding.region
|
||||
updated_fields.append("region")
|
||||
if resource_instance.service != finding.service_name:
|
||||
@@ -225,33 +221,24 @@ def perform_prowler_scan(
|
||||
# Process finding
|
||||
with rls_transaction(tenant_id):
|
||||
finding_uid = finding.uid
|
||||
last_first_seen_at = None
|
||||
if finding_uid not in last_status_cache:
|
||||
most_recent_finding = (
|
||||
Finding.all_objects.filter(
|
||||
tenant_id=tenant_id, uid=finding_uid
|
||||
)
|
||||
.order_by("-inserted_at")
|
||||
.values("status", "first_seen_at")
|
||||
Finding.objects.filter(uid=finding_uid)
|
||||
.order_by("-id")
|
||||
.values("status")
|
||||
.first()
|
||||
)
|
||||
last_status = None
|
||||
if most_recent_finding:
|
||||
last_status = most_recent_finding["status"]
|
||||
last_first_seen_at = most_recent_finding["first_seen_at"]
|
||||
last_status_cache[finding_uid] = last_status, last_first_seen_at
|
||||
last_status = (
|
||||
most_recent_finding["status"]
|
||||
if most_recent_finding
|
||||
else None
|
||||
)
|
||||
last_status_cache[finding_uid] = last_status
|
||||
else:
|
||||
last_status, last_first_seen_at = last_status_cache[finding_uid]
|
||||
last_status = last_status_cache[finding_uid]
|
||||
|
||||
status = FindingStatus[finding.status]
|
||||
delta = _create_finding_delta(last_status, status)
|
||||
# For the findings prior to the change, when a first finding is found with delta!="new" it will be
|
||||
# assigned a current date as first_seen_at and the successive findings with the same UID will
|
||||
# always get the date of the previous finding.
|
||||
# For new findings, when a finding (delta="new") is found for the first time, the first_seen_at
|
||||
# attribute will be assigned the current date, the following findings will get that date.
|
||||
if not last_first_seen_at:
|
||||
last_first_seen_at = datetime.now(tz=timezone.utc)
|
||||
|
||||
# Create the finding
|
||||
finding_instance = Finding.objects.create(
|
||||
@@ -266,12 +253,11 @@ def perform_prowler_scan(
|
||||
raw_result=finding.raw,
|
||||
check_id=finding.check_id,
|
||||
scan=scan_instance,
|
||||
first_seen_at=last_first_seen_at,
|
||||
)
|
||||
finding_instance.add_resources([resource_instance])
|
||||
|
||||
# Update compliance data if applicable
|
||||
if finding.status.value == "MUTED":
|
||||
if not generate_compliance or finding.status.value == "MUTED":
|
||||
continue
|
||||
|
||||
region_dict = check_status_by_region.setdefault(finding.region, {})
|
||||
@@ -299,7 +285,7 @@ def perform_prowler_scan(
|
||||
scan_instance.unique_resource_count = len(unique_resources)
|
||||
scan_instance.save()
|
||||
|
||||
if exception is None:
|
||||
if exception is None and generate_compliance:
|
||||
try:
|
||||
regions = prowler_provider.get_regions()
|
||||
except AttributeError:
|
||||
@@ -383,7 +369,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
- muted_changed: Muted findings with a delta of 'changed'.
|
||||
"""
|
||||
with rls_transaction(tenant_id):
|
||||
findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
|
||||
findings = Finding.objects.filter(scan_id=scan_id)
|
||||
|
||||
aggregation = findings.values(
|
||||
"check_id",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from celery import shared_task
|
||||
from config.celery import RLSTask
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
|
||||
from tasks.utils import get_next_execution_datetime
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.decorators import set_tenant
|
||||
from api.models import Scan, StateChoices
|
||||
from api.models import Provider, Scan
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="provider-connection-check")
|
||||
@@ -99,42 +100,28 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
task_id = self.request.id
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=f"scan-perform-scheduled-{provider_id}"
|
||||
)
|
||||
next_scan_datetime = get_next_execution_datetime(task_id, provider_id)
|
||||
scan_instance, _ = Scan.objects.get_or_create(
|
||||
next_scan_date = datetime.combine(
|
||||
datetime.now(timezone.utc), periodic_task_instance.start_time.time()
|
||||
) + timedelta(hours=24)
|
||||
|
||||
scan_instance = Scan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
name="Daily scheduled scan",
|
||||
provider=provider_instance,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
|
||||
scheduler_task_id=periodic_task_instance.id,
|
||||
defaults={"state": StateChoices.SCHEDULED},
|
||||
next_scan_at=next_scan_date,
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
scan_instance.task_id = task_id
|
||||
scan_instance.save()
|
||||
|
||||
try:
|
||||
result = perform_prowler_scan(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=str(scan_instance.id),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
with rls_transaction(tenant_id):
|
||||
Scan.objects.get_or_create(
|
||||
tenant_id=tenant_id,
|
||||
name="Daily scheduled scan",
|
||||
provider_id=provider_id,
|
||||
trigger=Scan.TriggerChoices.SCHEDULED,
|
||||
state=StateChoices.SCHEDULED,
|
||||
scheduled_at=next_scan_datetime,
|
||||
scheduler_task_id=periodic_task_instance.id,
|
||||
)
|
||||
|
||||
result = perform_prowler_scan(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=str(scan_instance.id),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
perform_scan_summary_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": tenant_id,
|
||||
|
||||
@@ -6,8 +6,6 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from tasks.beat import schedule_provider_scan
|
||||
|
||||
from api.models import Scan
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestScheduleProviderScan:
|
||||
@@ -17,11 +15,9 @@ class TestScheduleProviderScan:
|
||||
with patch(
|
||||
"tasks.tasks.perform_scheduled_scan_task.apply_async"
|
||||
) as mock_apply_async:
|
||||
assert Scan.all_objects.count() == 0
|
||||
result = schedule_provider_scan(provider_instance)
|
||||
|
||||
assert result is not None
|
||||
assert Scan.all_objects.count() == 1
|
||||
|
||||
mock_apply_async.assert_called_once_with(
|
||||
kwargs={
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
from tasks.utils import get_next_execution_datetime
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestGetNextExecutionDatetime:
|
||||
@pytest.fixture
|
||||
def setup_periodic_task(self, db):
|
||||
# Create a periodic task with an hourly interval
|
||||
interval = IntervalSchedule.objects.create(
|
||||
every=1, period=IntervalSchedule.HOURS
|
||||
)
|
||||
periodic_task = PeriodicTask.objects.create(
|
||||
name="scan-perform-scheduled-123",
|
||||
task="scan-perform-scheduled",
|
||||
interval=interval,
|
||||
)
|
||||
return periodic_task
|
||||
|
||||
@pytest.fixture
|
||||
def setup_task_result(self, db):
|
||||
# Create a task result record
|
||||
task_result = TaskResult.objects.create(
|
||||
task_id="abc123",
|
||||
task_name="scan-perform-scheduled",
|
||||
status="SUCCESS",
|
||||
date_created=datetime.now(timezone.utc) - timedelta(hours=1),
|
||||
result="Success",
|
||||
)
|
||||
return task_result
|
||||
|
||||
def test_get_next_execution_datetime_success(
|
||||
self, setup_task_result, setup_periodic_task
|
||||
):
|
||||
task_result = setup_task_result
|
||||
periodic_task = setup_periodic_task
|
||||
|
||||
# Mock periodic_task_name on TaskResult
|
||||
with patch.object(
|
||||
TaskResult, "periodic_task_name", return_value=periodic_task.name
|
||||
):
|
||||
next_execution = get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="123"
|
||||
)
|
||||
|
||||
expected_time = task_result.date_created + timedelta(hours=1)
|
||||
assert next_execution == expected_time
|
||||
|
||||
def test_get_next_execution_datetime_fallback_to_provider_id(
|
||||
self, setup_task_result, setup_periodic_task
|
||||
):
|
||||
task_result = setup_task_result
|
||||
|
||||
# Simulate the case where `periodic_task_name` is missing
|
||||
with patch.object(TaskResult, "periodic_task_name", return_value=None):
|
||||
next_execution = get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="123"
|
||||
)
|
||||
|
||||
expected_time = task_result.date_created + timedelta(hours=1)
|
||||
assert next_execution == expected_time
|
||||
|
||||
def test_get_next_execution_datetime_periodic_task_does_not_exist(
|
||||
self, setup_task_result
|
||||
):
|
||||
task_result = setup_task_result
|
||||
|
||||
with pytest.raises(PeriodicTask.DoesNotExist):
|
||||
get_next_execution_datetime(
|
||||
task_id=task_result.task_id, provider_id="nonexistent"
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.models import TaskResult
|
||||
|
||||
|
||||
def get_next_execution_datetime(task_id: int, provider_id: str) -> datetime:
|
||||
task_instance = TaskResult.objects.get(task_id=task_id)
|
||||
try:
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=task_instance.periodic_task_name
|
||||
)
|
||||
except PeriodicTask.DoesNotExist:
|
||||
periodic_task_instance = PeriodicTask.objects.get(
|
||||
name=f"scan-perform-scheduled-{provider_id}"
|
||||
)
|
||||
|
||||
interval = periodic_task_instance.interval
|
||||
|
||||
current_scheduled_time = datetime.combine(
|
||||
datetime.now(timezone.utc).date(),
|
||||
task_instance.date_created.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
return current_scheduled_time + timedelta(**{interval.period: interval.every})
|
||||
@@ -1,301 +0,0 @@
|
||||
# AWS SSO to Prowler Automation Script
|
||||
|
||||
## Table of Contents
|
||||
- [Introduction](#introduction)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Script Overview](#script-overview)
|
||||
- [Usage](#usage)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Customization](#customization)
|
||||
- [Security Considerations](#security-considerations)
|
||||
- [License](#license)
|
||||
|
||||
## Introduction
|
||||
|
||||
This repository provides a Bash script that automates the process of logging into AWS Single Sign-On (SSO), extracting temporary AWS credentials, and running **Prowler**—a security tool that performs AWS security best practices assessments—inside a Docker container using those credentials.
|
||||
|
||||
By following this guide, you can streamline your AWS security assessments, ensuring that you consistently apply best practices across your AWS accounts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure that you have the following tools installed and properly configured on your system:
|
||||
|
||||
1. **AWS CLI v2**
|
||||
- AWS SSO support is available from AWS CLI version 2 onwards.
|
||||
- [Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
|
||||
|
||||
2. **jq**
|
||||
- A lightweight and flexible command-line JSON processor.
|
||||
- **macOS (Homebrew):**
|
||||
```bash
|
||||
brew install jq
|
||||
```
|
||||
- **Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
```
|
||||
- **Windows:**
|
||||
- [Download jq](https://stedolan.github.io/jq/download/)
|
||||
|
||||
3. **Docker**
|
||||
- Ensure Docker is installed and running on your system.
|
||||
- [Docker Installation Guide](https://docs.docker.com/get-docker/)
|
||||
|
||||
4. **AWS SSO Profile Configuration**
|
||||
- Ensure that you have configured an AWS CLI profile with SSO.
|
||||
- [Configuring AWS CLI with SSO](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html)
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/your-username/aws-sso-prowler-automation.git
|
||||
cd aws-sso-prowler-automation
|
||||
```
|
||||
|
||||
2. **Create the Automation Script**
|
||||
Create a new Bash script named `run_prowler_sso.sh` and make it executable.
|
||||
|
||||
```bash
|
||||
nano run_prowler_sso.sh
|
||||
chmod +x run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Add the Script Content**
|
||||
Paste the following content into `run_prowler_sso.sh`:
|
||||
|
||||
4. **Configure AWS SSO Profile**
|
||||
Ensure that your AWS CLI profile (`twodragon` in this case) is correctly configured for SSO.
|
||||
|
||||
```bash
|
||||
aws configure sso --profile twodragon
|
||||
```
|
||||
|
||||
**Example Configuration Prompts:**
|
||||
```
|
||||
SSO session name (Recommended): [twodragon]
|
||||
SSO start URL [None]: https://twodragon.awsapps.com/start
|
||||
SSO region [None]: ap-northeast-2
|
||||
SSO account ID [None]: 123456789012
|
||||
SSO role name [None]: ReadOnlyAccess
|
||||
CLI default client region [None]: ap-northeast-2
|
||||
CLI default output format [None]: json
|
||||
CLI profile name [twodragon]: twodragon
|
||||
```
|
||||
|
||||
## Script Overview
|
||||
|
||||
The `run_prowler_sso.sh` script performs the following actions:
|
||||
|
||||
1. **AWS SSO Login:**
|
||||
- Initiates AWS SSO login for the specified profile.
|
||||
- Opens the SSO authorization page in the default browser for user authentication.
|
||||
|
||||
2. **Extract Temporary Credentials:**
|
||||
- Locates the most recent SSO cache file containing the `accessToken`.
|
||||
- Uses `jq` to parse and extract the `accessToken` from the cache file.
|
||||
- Retrieves the `sso_role_name` and `sso_account_id` from the AWS CLI configuration.
|
||||
- Obtains temporary AWS credentials (`AccessKeyId`, `SecretAccessKey`, `SessionToken`) using the extracted `accessToken`.
|
||||
|
||||
3. **Set Environment Variables:**
|
||||
- Exports the extracted AWS credentials as environment variables to be used by the Docker container.
|
||||
|
||||
4. **Run Prowler:**
|
||||
- Executes the **Prowler** Docker container, passing the AWS credentials as environment variables for security assessments.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Make the Script Executable**
|
||||
Ensure the script has execute permissions.
|
||||
|
||||
```bash
|
||||
chmod +x run_prowler_sso.sh
|
||||
```
|
||||
|
||||
2. **Run the Script**
|
||||
Execute the script to start the AWS SSO login process and run Prowler.
|
||||
|
||||
```bash
|
||||
./run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Follow the Prompts**
|
||||
- A browser window will open prompting you to authenticate via AWS SSO.
|
||||
- Complete the authentication process in the browser.
|
||||
- Upon successful login, the script will extract temporary credentials and run Prowler.
|
||||
|
||||
4. **Review Prowler Output**
|
||||
- Prowler will analyze your AWS environment based on the specified checks and output the results directly in the terminal.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues during the script execution, follow these steps to diagnose and resolve them.
|
||||
|
||||
### 1. Verify AWS CLI Version
|
||||
|
||||
Ensure you are using AWS CLI version 2 or later.
|
||||
|
||||
```bash
|
||||
aws --version
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
aws-cli/2.11.10 Python/3.9.12 Darwin/20.3.0 exe/x86_64 prompt/off
|
||||
```
|
||||
|
||||
If you are not using version 2, [install or update AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).
|
||||
|
||||
### 2. Confirm AWS SSO Profile Configuration
|
||||
|
||||
Check that the `twodragon` profile is correctly configured.
|
||||
|
||||
```bash
|
||||
aws configure list-profiles
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
default
|
||||
twodragon
|
||||
```
|
||||
|
||||
Review the profile details:
|
||||
|
||||
```bash
|
||||
aws configure get sso_start_url --profile twodragon
|
||||
aws configure get sso_region --profile twodragon
|
||||
aws configure get sso_account_id --profile twodragon
|
||||
aws configure get sso_role_name --profile twodragon
|
||||
```
|
||||
|
||||
Ensure all fields return the correct values.
|
||||
|
||||
### 3. Check SSO Cache File
|
||||
|
||||
Ensure that the SSO cache file contains a valid `accessToken`.
|
||||
|
||||
```bash
|
||||
cat ~/.aws/sso/cache/*.json
|
||||
```
|
||||
|
||||
**Example Content:**
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJz93a...k4laUWw",
|
||||
"expiresAt": "2024-12-22T14:07:55Z",
|
||||
"clientId": "example-client-id",
|
||||
"clientSecret": "example-client-secret",
|
||||
"startUrl": "https://twodragon.awsapps.com/start#"
|
||||
}
|
||||
```
|
||||
|
||||
If `accessToken` is `null` or missing, retry the AWS SSO login:
|
||||
|
||||
```bash
|
||||
aws sso login --profile twodragon
|
||||
```
|
||||
|
||||
### 4. Validate `jq` Installation
|
||||
|
||||
Ensure that `jq` is installed and functioning correctly.
|
||||
|
||||
```bash
|
||||
jq --version
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
jq-1.6
|
||||
```
|
||||
|
||||
If `jq` is not installed, install it using the instructions in the [Prerequisites](#prerequisites) section.
|
||||
|
||||
### 5. Test Docker Environment Variables
|
||||
|
||||
Verify that the Docker container receives the AWS credentials correctly.
|
||||
|
||||
```bash
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
|
||||
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
|
||||
toniblyx/prowler /bin/bash -c 'echo $AWS_ACCESS_KEY_ID; echo $AWS_SECRET_ACCESS_KEY; echo $AWS_SESSION_TOKEN'
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
ASIA...
|
||||
wJalrFEMI/K7MDENG/bPxRfiCY...
|
||||
IQoJb3JpZ2luX2VjEHwaCXVz...
|
||||
```
|
||||
|
||||
Ensure that none of the environment variables are empty.
|
||||
|
||||
### 6. Review Script Output
|
||||
|
||||
Run the script with debugging enabled to get detailed output.
|
||||
|
||||
1. **Enable Debugging in Script**
|
||||
Add `set -x` for verbose output.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
# ... rest of the script ...
|
||||
```
|
||||
|
||||
2. **Run the Script**
|
||||
|
||||
```bash
|
||||
./run_prowler_sso.sh
|
||||
```
|
||||
|
||||
3. **Analyze Output**
|
||||
Look for any errors or unexpected values in the output to identify where the script is failing.
|
||||
|
||||
## Customization
|
||||
|
||||
You can modify the script to suit your specific needs, such as:
|
||||
|
||||
- **Changing the AWS Profile Name:**
|
||||
Update the `PROFILE` variable at the top of the script.
|
||||
|
||||
```bash
|
||||
PROFILE="your-profile-name"
|
||||
```
|
||||
|
||||
- **Adding Prowler Options:**
|
||||
Pass additional options to Prowler for customized checks or output formats.
|
||||
|
||||
```bash
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
|
||||
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
|
||||
toniblyx/prowler -c check123 -M json
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Handle Credentials Securely:**
|
||||
- Avoid sharing or exposing your AWS credentials.
|
||||
- Do not include sensitive information in logs or version control.
|
||||
|
||||
- **Script Permissions:**
|
||||
- Ensure the script file has appropriate permissions to prevent unauthorized access.
|
||||
|
||||
```bash
|
||||
chmod 700 run_prowler_sso.sh
|
||||
```
|
||||
|
||||
- **Environment Variables:**
|
||||
- Be cautious when exporting credentials as environment variables.
|
||||
- Consider using more secure methods for credential management if necessary.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Set the profile name
|
||||
PROFILE="twodragon"
|
||||
|
||||
# Set the Prowler output directory
|
||||
OUTPUT_DIR=~/prowler-output
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Set the port for the local web server
|
||||
WEB_SERVER_PORT=8000
|
||||
|
||||
# ----------------------------------------------
|
||||
# Functions
|
||||
# ----------------------------------------------
|
||||
|
||||
# Function to open the HTML report in the default browser
|
||||
open_report() {
|
||||
local report_path="$1"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
open "$report_path"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
xdg-open "$report_path"
|
||||
elif [[ "$OSTYPE" == "msys" ]]; then
|
||||
start "" "$report_path"
|
||||
else
|
||||
echo "Automatic method to open Prowler HTML report is not supported on this OS."
|
||||
echo "Please open the report manually at: $report_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start a simple HTTP server to host the Prowler reports
|
||||
start_web_server() {
|
||||
local directory="$1"
|
||||
local port="$2"
|
||||
|
||||
echo "Starting local web server to host Prowler reports at http://localhost:$port"
|
||||
echo "Press Ctrl+C to stop the web server."
|
||||
|
||||
# Change to the output directory
|
||||
cd "$directory"
|
||||
|
||||
# Start the HTTP server in the foreground
|
||||
# Python 3 is required
|
||||
python3 -m http.server "$port"
|
||||
}
|
||||
|
||||
# ----------------------------------------------
|
||||
# Main Script
|
||||
# ----------------------------------------------
|
||||
|
||||
# AWS SSO Login
|
||||
echo "Logging into AWS SSO..."
|
||||
aws sso login --profile "$PROFILE"
|
||||
|
||||
# Extract temporary credentials
|
||||
echo "Extracting temporary credentials..."
|
||||
|
||||
# Find the most recently modified SSO cache file
|
||||
CACHE_FILE=$(ls -t ~/.aws/sso/cache/*.json 2>/dev/null | head -n 1)
|
||||
echo "Cache File: $CACHE_FILE"
|
||||
|
||||
if [ -z "$CACHE_FILE" ]; then
|
||||
echo "SSO cache file not found. Please ensure AWS SSO login was successful."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract accessToken using jq
|
||||
ACCESS_TOKEN=$(jq -r '.accessToken' "$CACHE_FILE")
|
||||
echo "Access Token: $ACCESS_TOKEN"
|
||||
|
||||
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then
|
||||
echo "Unable to extract accessToken. Please check your SSO login and cache file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract role name and account ID from AWS CLI configuration
|
||||
ROLE_NAME=$(aws configure get sso_role_name --profile "$PROFILE")
|
||||
ACCOUNT_ID=$(aws configure get sso_account_id --profile "$PROFILE")
|
||||
echo "Role Name: $ROLE_NAME"
|
||||
echo "Account ID: $ACCOUNT_ID"
|
||||
|
||||
if [ -z "$ROLE_NAME" ] || [ -z "$ACCOUNT_ID" ]; then
|
||||
echo "Unable to extract sso_role_name or sso_account_id. Please check your profile configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Obtain temporary credentials using AWS SSO
|
||||
TEMP_CREDS=$(aws sso get-role-credentials \
|
||||
--role-name "$ROLE_NAME" \
|
||||
--account-id "$ACCOUNT_ID" \
|
||||
--access-token "$ACCESS_TOKEN" \
|
||||
--profile "$PROFILE")
|
||||
|
||||
echo "TEMP_CREDS: $TEMP_CREDS"
|
||||
|
||||
# Extract credentials from the JSON response
|
||||
AWS_ACCESS_KEY_ID=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.accessKeyId')
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.secretAccessKey')
|
||||
AWS_SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.sessionToken')
|
||||
|
||||
# Verify that all credentials were extracted successfully
|
||||
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$AWS_SESSION_TOKEN" ]; then
|
||||
echo "Unable to extract temporary credentials."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Export AWS credentials as environment variables
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
export AWS_SESSION_TOKEN
|
||||
|
||||
echo "AWS credentials have been set."
|
||||
|
||||
# Run Prowler in Docker container
|
||||
echo "Running Prowler Docker container..."
|
||||
|
||||
docker run --platform linux/amd64 \
|
||||
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
|
||||
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
|
||||
-e AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \
|
||||
-v "$OUTPUT_DIR":/home/prowler/output \
|
||||
toniblyx/prowler -M html -M csv -M json-ocsf --output-directory /home/prowler/output --output-filename prowler-output
|
||||
|
||||
echo "Prowler has finished running. Reports are saved in $OUTPUT_DIR."
|
||||
|
||||
# Open the HTML report in the default browser
|
||||
REPORT_PATH="$OUTPUT_DIR/prowler-output.html"
|
||||
echo "Opening Prowler HTML report..."
|
||||
open_report "$REPORT_PATH" &
|
||||
|
||||
# Start the local web server to host the Prowler dashboard
|
||||
# This will run in the foreground. To run it in the background, append an ampersand (&) at the end of the command.
|
||||
start_web_server "$OUTPUT_DIR" "$WEB_SERVER_PORT"
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: prowler-api
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "5.1.1"
|
||||
@@ -1,22 +0,0 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "prowler-api.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "prowler-api.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "prowler-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "prowler-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -1,62 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "prowler-api.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "prowler-api.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "prowler-api.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "prowler-api.labels" -}}
|
||||
helm.sh/chart: {{ include "prowler-api.chart" . }}
|
||||
{{ include "prowler-api.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "prowler-api.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "prowler-api.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "prowler-api.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "prowler-api.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "prowler-api.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
data:
|
||||
config.yaml: |-
|
||||
{{- toYaml .Values.mainConfig | nindent 4 }}
|
||||
@@ -1,85 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "prowler-api.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "prowler-api.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "prowler-api.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
{{- range $name,$config := .Values.containers }}
|
||||
{{- if $config.enabled }}
|
||||
- name: {{ $name }}
|
||||
securityContext:
|
||||
{{- toYaml $config.securityContext | nindent 12 }}
|
||||
image: "{{ $config.image.repository }}:{{ $config.image.tag | default $.Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ $config.image.pullPolicy }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ include "prowler-api.fullname" $ }}
|
||||
command:
|
||||
{{- toYaml $config.command | nindent 12 }}
|
||||
{{- if $config.ports }}
|
||||
ports:
|
||||
{{- toYaml $config.ports | nindent 12 }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml $config.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml $config.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml $config.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: {{ include "prowler-api.fullname" $ }}-config
|
||||
mountPath: {{ $.Values.releaseConfigRoot }}{{ $.Values.releaseConfigPath }}
|
||||
subPath: config.yaml
|
||||
{{- with .volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: {{ include "prowler-api.fullname" . }}-config
|
||||
configMap:
|
||||
name: {{ include "prowler-api.fullname" . }}-config
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,43 +0,0 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "prowler-api.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "prowler-api.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "prowler-api.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $k, $v := .Values.secrets }}
|
||||
{{ $k }}: {{ $v | toString | b64enc | quote }}
|
||||
{{- end }}
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "prowler-api.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
{{- range $name,$config := .Values.containers }}
|
||||
{{- if $config.ports }}
|
||||
{{- range $p := $config.ports }}
|
||||
- port: {{ $p.containerPort }}
|
||||
targetPort: {{ $p.containerPort }}
|
||||
protocol: TCP
|
||||
name: {{ $config.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "prowler-api.selectorLabels" . | nindent 4 }}
|
||||
@@ -1,13 +0,0 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "prowler-api.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "prowler-api.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
@@ -1,625 +0,0 @@
|
||||
# Default values for prowler-api.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
containers:
|
||||
prowler-api:
|
||||
enabled: true
|
||||
image:
|
||||
repository: prowlercloud/prowler-api
|
||||
pullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
command: ["/home/prowler/docker-entrypoint.sh", "prod"]
|
||||
worker:
|
||||
enabled: true
|
||||
image:
|
||||
repository: prowlercloud/prowler-api
|
||||
pullPolicy: IfNotPresent
|
||||
command: ["/home/prowler/docker-entrypoint.sh", "worker"]
|
||||
worker-beat:
|
||||
enabled: true
|
||||
image:
|
||||
repository: prowlercloud/prowler-api
|
||||
pullPolicy: IfNotPresent
|
||||
command: ["../docker-entrypoint.sh", "beat"]
|
||||
|
||||
secrets:
|
||||
POSTGRES_HOST:
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_ADMIN_USER:
|
||||
POSTGRES_ADMIN_PASSWORD:
|
||||
POSTGRES_USER:
|
||||
POSTGRES_PASSWORD:
|
||||
POSTGRES_DB:
|
||||
# Valkey settings
|
||||
VALKEY_HOST: valkey-headless
|
||||
VALKEY_PORT: "6379"
|
||||
VALKEY_DB: "0"
|
||||
# Django settings
|
||||
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1,prowler-api
|
||||
DJANGO_BIND_ADDRESS: 0.0.0.0
|
||||
DJANGO_PORT: "8080"
|
||||
DJANGO_DEBUG: False
|
||||
DJANGO_SETTINGS_MODULE: config.django.production
|
||||
# Select one of [ndjson|human_readable]
|
||||
DJANGO_LOGGING_FORMATTER: human_readable
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL: INFO
|
||||
# Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_WORKERS: 2
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME: "30"
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME: "1440"
|
||||
DJANGO_CACHE_MAX_AGE: "3600"
|
||||
DJANGO_STALE_WHILE_REVALIDATE: "60"
|
||||
DJANGO_MANAGE_DB_PARTITIONS: "False"
|
||||
# openssl genrsa -out private.pem 2048
|
||||
DJANGO_TOKEN_SIGNING_KEY:
|
||||
# openssl rsa -in private.pem -pubout -out public.pem
|
||||
DJANGO_TOKEN_VERIFYING_KEY:
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY:
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT: 86400
|
||||
|
||||
releaseConfigRoot: /home/prowler/.cache/pypoetry/virtualenvs/prowler-api-NnJNioq7-py3.12/lib/python3.12/site-packages/
|
||||
releaseConfigPath: prowler/config/config.yaml
|
||||
|
||||
mainConfig:
|
||||
# AWS Configuration
|
||||
aws:
|
||||
# AWS Global Configuration
|
||||
# aws.mute_non_default_regions --> Set to True to muted failed findings in non-default regions for AccessAnalyzer, GuardDuty, SecurityHub, DRS and Config
|
||||
mute_non_default_regions: False
|
||||
# If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
|
||||
# Mutelist:
|
||||
# Accounts:
|
||||
# "*":
|
||||
# Checks:
|
||||
# "*":
|
||||
# Regions:
|
||||
# - "ap-southeast-1"
|
||||
# - "ap-southeast-2"
|
||||
# Resources:
|
||||
# - "*"
|
||||
|
||||
# AWS IAM Configuration
|
||||
# aws.iam_user_accesskey_unused --> CIS recommends 45 days
|
||||
max_unused_access_keys_days: 45
|
||||
# aws.iam_user_console_access_unused --> CIS recommends 45 days
|
||||
max_console_access_days: 45
|
||||
|
||||
# AWS EC2 Configuration
|
||||
# aws.ec2_elastic_ip_shodan
|
||||
# TODO: create common config
|
||||
shodan_api_key: null
|
||||
# aws.ec2_securitygroup_with_many_ingress_egress_rules --> by default is 50 rules
|
||||
max_security_group_rules: 50
|
||||
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
|
||||
max_ec2_instance_age_in_days: 180
|
||||
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
|
||||
# allowed network interface types for security groups open to the Internet
|
||||
ec2_allowed_interface_types:
|
||||
[
|
||||
"api_gateway_managed",
|
||||
"vpc_endpoint",
|
||||
]
|
||||
# allowed network interface owners for security groups open to the Internet
|
||||
ec2_allowed_instance_owners:
|
||||
[
|
||||
"amazon-elb"
|
||||
]
|
||||
# aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports
|
||||
ec2_high_risk_ports:
|
||||
[
|
||||
25,
|
||||
110,
|
||||
135,
|
||||
143,
|
||||
445,
|
||||
3000,
|
||||
4333,
|
||||
5000,
|
||||
5500,
|
||||
8080,
|
||||
8088,
|
||||
]
|
||||
|
||||
# AWS ECS Configuration
|
||||
# aws.ecs_service_fargate_latest_platform_version
|
||||
fargate_linux_latest_version: "1.4.0"
|
||||
fargate_windows_latest_version: "1.0.0"
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
trusted_account_ids: []
|
||||
|
||||
# AWS Cloudwatch Configuration
|
||||
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
|
||||
log_group_retention_days: 365
|
||||
|
||||
# AWS CloudFormation Configuration
|
||||
# cloudformation_stack_cdktoolkit_bootstrap_version --> by default is 21
|
||||
recommended_cdk_bootstrap_version: 21
|
||||
|
||||
# AWS AppStream Session Configuration
|
||||
# aws.appstream_fleet_session_idle_disconnect_timeout
|
||||
max_idle_disconnect_timeout_in_seconds: 600 # 10 Minutes
|
||||
# aws.appstream_fleet_session_disconnect_timeout
|
||||
max_disconnect_timeout_in_seconds: 300 # 5 Minutes
|
||||
# aws.appstream_fleet_maximum_session_duration
|
||||
max_session_duration_seconds: 36000 # 10 Hours
|
||||
|
||||
# AWS Lambda Configuration
|
||||
# aws.awslambda_function_using_supported_runtimes
|
||||
obsolete_lambda_runtimes:
|
||||
[
|
||||
"java8",
|
||||
"go1.x",
|
||||
"provided",
|
||||
"python3.6",
|
||||
"python2.7",
|
||||
"python3.7",
|
||||
"nodejs4.3",
|
||||
"nodejs4.3-edge",
|
||||
"nodejs6.10",
|
||||
"nodejs",
|
||||
"nodejs8.10",
|
||||
"nodejs10.x",
|
||||
"nodejs12.x",
|
||||
"nodejs14.x",
|
||||
"nodejs16.x",
|
||||
"dotnet5.0",
|
||||
"dotnet7",
|
||||
"dotnetcore1.0",
|
||||
"dotnetcore2.0",
|
||||
"dotnetcore2.1",
|
||||
"dotnetcore3.1",
|
||||
"ruby2.5",
|
||||
"ruby2.7",
|
||||
]
|
||||
# aws.awslambda_function_vpc_is_in_multi_azs
|
||||
lambda_min_azs: 2
|
||||
|
||||
# AWS Organizations
|
||||
# aws.organizations_scp_check_deny_regions
|
||||
# aws.organizations_enabled_regions: [
|
||||
# "eu-central-1",
|
||||
# "eu-west-1",
|
||||
# "us-east-1"
|
||||
# ]
|
||||
organizations_enabled_regions: []
|
||||
organizations_trusted_delegated_administrators: []
|
||||
|
||||
# AWS ECR
|
||||
# aws.ecr_repositories_scan_vulnerabilities_in_latest_image
|
||||
# CRITICAL
|
||||
# HIGH
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
# AWS CloudTrail Configuration
|
||||
# aws.cloudtrail_threat_detection_privilege_escalation
|
||||
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
|
||||
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_privilege_escalation_actions:
|
||||
[
|
||||
"AddPermission",
|
||||
"AddRoleToInstanceProfile",
|
||||
"AddUserToGroup",
|
||||
"AssociateAccessPolicy",
|
||||
"AssumeRole",
|
||||
"AttachGroupPolicy",
|
||||
"AttachRolePolicy",
|
||||
"AttachUserPolicy",
|
||||
"ChangePassword",
|
||||
"CreateAccessEntry",
|
||||
"CreateAccessKey",
|
||||
"CreateDevEndpoint",
|
||||
"CreateEventSourceMapping",
|
||||
"CreateFunction",
|
||||
"CreateGroup",
|
||||
"CreateJob",
|
||||
"CreateKeyPair",
|
||||
"CreateLoginProfile",
|
||||
"CreatePipeline",
|
||||
"CreatePolicyVersion",
|
||||
"CreateRole",
|
||||
"CreateStack",
|
||||
"DeleteRolePermissionsBoundary",
|
||||
"DeleteRolePolicy",
|
||||
"DeleteUserPermissionsBoundary",
|
||||
"DeleteUserPolicy",
|
||||
"DetachRolePolicy",
|
||||
"DetachUserPolicy",
|
||||
"GetCredentialsForIdentity",
|
||||
"GetId",
|
||||
"GetPolicyVersion",
|
||||
"GetUserPolicy",
|
||||
"Invoke",
|
||||
"ModifyInstanceAttribute",
|
||||
"PassRole",
|
||||
"PutGroupPolicy",
|
||||
"PutPipelineDefinition",
|
||||
"PutRolePermissionsBoundary",
|
||||
"PutRolePolicy",
|
||||
"PutUserPermissionsBoundary",
|
||||
"PutUserPolicy",
|
||||
"ReplaceIamInstanceProfileAssociation",
|
||||
"RunInstances",
|
||||
"SetDefaultPolicyVersion",
|
||||
"UpdateAccessKey",
|
||||
"UpdateAssumeRolePolicy",
|
||||
"UpdateDevEndpoint",
|
||||
"UpdateEventSourceMapping",
|
||||
"UpdateFunctionCode",
|
||||
"UpdateJob",
|
||||
"UpdateLoginProfile",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_enumeration
|
||||
threat_detection_enumeration_threshold: 0.3 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.3 (30%)
|
||||
threat_detection_enumeration_minutes: 1440 # Past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_enumeration_actions:
|
||||
[
|
||||
"DescribeAccessEntry",
|
||||
"DescribeAccountAttributes",
|
||||
"DescribeAvailabilityZones",
|
||||
"DescribeBundleTasks",
|
||||
"DescribeCarrierGateways",
|
||||
"DescribeClientVpnRoutes",
|
||||
"DescribeCluster",
|
||||
"DescribeDhcpOptions",
|
||||
"DescribeFlowLogs",
|
||||
"DescribeImages",
|
||||
"DescribeInstanceAttribute",
|
||||
"DescribeInstanceInformation",
|
||||
"DescribeInstanceTypes",
|
||||
"DescribeInstances",
|
||||
"DescribeInstances",
|
||||
"DescribeKeyPairs",
|
||||
"DescribeLogGroups",
|
||||
"DescribeLogStreams",
|
||||
"DescribeOrganization",
|
||||
"DescribeRegions",
|
||||
"DescribeSecurityGroups",
|
||||
"DescribeSnapshotAttribute",
|
||||
"DescribeSnapshotTierStatus",
|
||||
"DescribeSubscriptionFilters",
|
||||
"DescribeTransitGatewayMulticastDomains",
|
||||
"DescribeVolumes",
|
||||
"DescribeVolumesModifications",
|
||||
"DescribeVpcEndpointConnectionNotifications",
|
||||
"DescribeVpcs",
|
||||
"GetAccount",
|
||||
"GetAccountAuthorizationDetails",
|
||||
"GetAccountSendingEnabled",
|
||||
"GetBucketAcl",
|
||||
"GetBucketLogging",
|
||||
"GetBucketPolicy",
|
||||
"GetBucketReplication",
|
||||
"GetBucketVersioning",
|
||||
"GetCallerIdentity",
|
||||
"GetCertificate",
|
||||
"GetConsoleScreenshot",
|
||||
"GetCostAndUsage",
|
||||
"GetDetector",
|
||||
"GetEbsDefaultKmsKeyId",
|
||||
"GetEbsEncryptionByDefault",
|
||||
"GetFindings",
|
||||
"GetFlowLogsIntegrationTemplate",
|
||||
"GetIdentityVerificationAttributes",
|
||||
"GetInstances",
|
||||
"GetIntrospectionSchema",
|
||||
"GetLaunchTemplateData",
|
||||
"GetLaunchTemplateData",
|
||||
"GetLogRecord",
|
||||
"GetParameters",
|
||||
"GetPolicyVersion",
|
||||
"GetPublicAccessBlock",
|
||||
"GetQueryResults",
|
||||
"GetRegions",
|
||||
"GetSMSAttributes",
|
||||
"GetSMSSandboxAccountStatus",
|
||||
"GetSendQuota",
|
||||
"GetTransitGatewayRouteTableAssociations",
|
||||
"GetUserPolicy",
|
||||
"HeadObject",
|
||||
"ListAccessKeys",
|
||||
"ListAccounts",
|
||||
"ListAllMyBuckets",
|
||||
"ListAssociatedAccessPolicies",
|
||||
"ListAttachedUserPolicies",
|
||||
"ListClusters",
|
||||
"ListDetectors",
|
||||
"ListDomains",
|
||||
"ListFindings",
|
||||
"ListHostedZones",
|
||||
"ListIPSets",
|
||||
"ListIdentities",
|
||||
"ListInstanceProfiles",
|
||||
"ListObjects",
|
||||
"ListOrganizationalUnitsForParent",
|
||||
"ListOriginationNumbers",
|
||||
"ListPolicyVersions",
|
||||
"ListRoles",
|
||||
"ListRoles",
|
||||
"ListRules",
|
||||
"ListServiceQuotas",
|
||||
"ListSubscriptions",
|
||||
"ListTargetsByRule",
|
||||
"ListTopics",
|
||||
"ListUsers",
|
||||
"LookupEvents",
|
||||
"Search",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_llm_jacking
|
||||
threat_detection_llm_jacking_threshold: 0.4 # Percentage of actions found to decide if it is an LLM Jacking attack event, by default is 0.4 (40%)
|
||||
threat_detection_llm_jacking_minutes: 1440 # Past minutes to search from now for LLM Jacking attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_llm_jacking_actions:
|
||||
[
|
||||
"PutUseCaseForModelAccess", # Submits a use case for model access, providing justification (Write).
|
||||
"PutFoundationModelEntitlement", # Grants entitlement for accessing a foundation model (Write).
|
||||
"PutModelInvocationLoggingConfiguration", # Configures logging for model invocations (Write).
|
||||
"CreateFoundationModelAgreement", # Creates a new agreement to use a foundation model (Write).
|
||||
"InvokeModel", # Invokes a specified Bedrock model for inference using provided prompt and parameters (Read).
|
||||
"InvokeModelWithResponseStream", # Invokes a Bedrock model for inference with real-time token streaming (Read).
|
||||
"GetUseCaseForModelAccess", # Retrieves an existing use case for model access (Read).
|
||||
"GetModelInvocationLoggingConfiguration", # Fetches the logging configuration for model invocations (Read).
|
||||
"GetFoundationModelAvailability", # Checks the availability of a foundation model for use (Read).
|
||||
"ListFoundationModelAgreementOffers", # Lists available agreement offers for accessing foundation models (List).
|
||||
"ListFoundationModels", # Lists the available foundation models in Bedrock (List).
|
||||
"ListProvisionedModelThroughputs", # Lists the provisioned throughput for previously created models (List).
|
||||
]
|
||||
|
||||
# AWS RDS Configuration
|
||||
# aws.rds_instance_backup_enabled
|
||||
# Whether to check RDS instance replicas or not
|
||||
check_rds_instance_replicas: False
|
||||
|
||||
# AWS ACM Configuration
|
||||
# aws.acm_certificates_expiration_check
|
||||
days_to_expire_threshold: 7
|
||||
# aws.acm_certificates_with_secure_key_algorithms
|
||||
insecure_key_algorithms:
|
||||
[
|
||||
"RSA-1024",
|
||||
"P-192",
|
||||
"SHA-1",
|
||||
]
|
||||
|
||||
# AWS EKS Configuration
|
||||
# aws.eks_control_plane_logging_all_types_enabled
|
||||
# EKS control plane logging types that must be enabled
|
||||
eks_required_log_types:
|
||||
[
|
||||
"api",
|
||||
"audit",
|
||||
"authenticator",
|
||||
"controllerManager",
|
||||
"scheduler",
|
||||
]
|
||||
|
||||
# aws.eks_cluster_uses_a_supported_version
|
||||
# EKS clusters must be version 1.28 or higher
|
||||
eks_cluster_oldest_version_supported: "1.28"
|
||||
|
||||
# AWS CodeBuild Configuration
|
||||
# aws.codebuild_project_no_secrets_in_variables
|
||||
# CodeBuild sensitive variables that are excluded from the check
|
||||
excluded_sensitive_environment_variables:
|
||||
[
|
||||
|
||||
]
|
||||
|
||||
# AWS ELB Configuration
|
||||
# aws.elb_is_in_multiple_az
|
||||
# Minimum number of Availability Zones that an CLB must be in
|
||||
elb_min_azs: 2
|
||||
|
||||
# AWS ELBv2 Configuration
|
||||
# aws.elbv2_is_in_multiple_az
|
||||
# Minimum number of Availability Zones that an ELBv2 must be in
|
||||
elbv2_min_azs: 2
|
||||
|
||||
|
||||
# AWS Secrets Configuration
|
||||
# Patterns to ignore in the secrets checks
|
||||
secrets_ignore_patterns: []
|
||||
|
||||
# AWS Secrets Manager Configuration
|
||||
# aws.secretsmanager_secret_unused
|
||||
# Maximum number of days a secret can be unused
|
||||
max_days_secret_unused: 90
|
||||
|
||||
# aws.secretsmanager_secret_rotated_periodically
|
||||
# Maximum number of days a secret should be rotated
|
||||
max_days_secret_unrotated: 90
|
||||
|
||||
# AWS Kinesis Configuration
|
||||
# Minimum retention period in hours for Kinesis streams
|
||||
min_kinesis_stream_retention_hours: 168 # 7 days
|
||||
|
||||
|
||||
# Azure Configuration
|
||||
azure:
|
||||
# Azure Network Configuration
|
||||
# azure.network_public_ip_shodan
|
||||
# TODO: create common config
|
||||
shodan_api_key: null
|
||||
|
||||
# Azure App Service
|
||||
# azure.app_ensure_php_version_is_latest
|
||||
php_latest_version: "8.2"
|
||||
# azure.app_ensure_python_version_is_latest
|
||||
python_latest_version: "3.12"
|
||||
# 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
|
||||
# gcp.compute_public_address_shodan
|
||||
shodan_api_key: null
|
||||
|
||||
# Kubernetes Configuration
|
||||
kubernetes:
|
||||
# Kubernetes API Server
|
||||
# kubernetes.apiserver_audit_log_maxbackup_set
|
||||
audit_log_maxbackup: 10
|
||||
# kubernetes.apiserver_audit_log_maxsize_set
|
||||
audit_log_maxsize: 100
|
||||
# kubernetes.apiserver_audit_log_maxage_set
|
||||
audit_log_maxage: 30
|
||||
# kubernetes.apiserver_strong_ciphers_only
|
||||
apiserver_strong_ciphers:
|
||||
[
|
||||
"TLS_AES_128_GCM_SHA256",
|
||||
"TLS_AES_256_GCM_SHA384",
|
||||
"TLS_CHACHA20_POLY1305_SHA256",
|
||||
]
|
||||
# Kubelet
|
||||
# kubernetes.kubelet_strong_ciphers_only
|
||||
kubelet_strong_ciphers:
|
||||
[
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
]
|
||||
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 80
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
|
||||
#This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,23 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: prowler-ui
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "5.1.1"
|
||||
@@ -1,22 +0,0 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "prowler-ui.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "prowler-ui.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "prowler-ui.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "prowler-ui.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -1,62 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "prowler-ui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "prowler-ui.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "prowler-ui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "prowler-ui.labels" -}}
|
||||
helm.sh/chart: {{ include "prowler-ui.chart" . }}
|
||||
{{ include "prowler-ui.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "prowler-ui.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "prowler-ui.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "prowler-ui.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "prowler-ui.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,72 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "prowler-ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "prowler-ui.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "prowler-ui.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ include "prowler-ui.fullname" $ }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.volumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,43 +0,0 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "prowler-ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "prowler-ui.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "prowler-ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $k, $v := .Values.secrets }}
|
||||
{{ $k }}: {{ $v | toString | b64enc | quote }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "prowler-ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "prowler-ui.selectorLabels" . | nindent 4 }}
|
||||
@@ -1,13 +0,0 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "prowler-ui.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "prowler-ui.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
@@ -1,132 +0,0 @@
|
||||
# Default values for prowler-ui.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: prowlercloud/prowler-ui
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
secrets:
|
||||
SITE_URL: http://localhost:3000
|
||||
API_BASE_URL: http://prowler-api:8080/api/v1
|
||||
NEXT_PUBLIC_API_DOCS_URL: http://prowler-api:8080/api/v1/docs
|
||||
AUTH_TRUST_HOST: True
|
||||
UI_PORT: 3000
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET:
|
||||
|
||||
#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 3000
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
|
||||
#This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -39,3 +39,4 @@ spec:
|
||||
path: {{ $value }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -532,8 +532,8 @@ def get_bar_graph(df, column_name):
|
||||
|
||||
# Cut the text if it is too long
|
||||
for i in range(len(colums)):
|
||||
if len(colums[i]) > 43:
|
||||
colums[i] = colums[i][:43] + "..."
|
||||
if len(colums[i]) > 15:
|
||||
colums[i] = colums[i][:15] + "..."
|
||||
|
||||
fig = px.bar(
|
||||
df,
|
||||
|
||||
@@ -1720,7 +1720,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Recommendation: ",
|
||||
"Recomendation: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
@@ -1744,7 +1744,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"RecommendationUrl: ",
|
||||
"RecomendationUrl: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
|
||||
@@ -35,9 +35,6 @@ services:
|
||||
required: false
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- "./ui:/app"
|
||||
- "/app/node_modules"
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine3.20
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
api:
|
||||
hostname: "prowler-api"
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
- "prod"
|
||||
|
||||
ui:
|
||||
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-stable}
|
||||
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-latest}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
- "worker"
|
||||
|
||||
worker-beat:
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-latest}
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
|
||||
@@ -279,9 +279,6 @@ Each Prowler check has metadata associated which is stored at the same level of
|
||||
"Severity": "critical",
|
||||
# ResourceType only for AWS, holds the type from here
|
||||
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html
|
||||
# In case of not existing, use CloudFormation type but removing the "::" and using capital letters only at the beginning of each word. Example: "AWS::EC2::Instance" -> "AwsEc2Instance"
|
||||
# CloudFormation type reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
|
||||
# If the resource type does not exist in the CloudFormation types, use "Other".
|
||||
"ResourceType": "Other",
|
||||
# Description holds the title of the check, for now is the same as CheckTitle
|
||||
"Description": "Ensure there are no EC2 AMIs set as Public.",
|
||||
|
||||
@@ -1,336 +1,3 @@
|
||||
# Creating a New Integration
|
||||
# Create a new integration
|
||||
|
||||
## Introduction
|
||||
|
||||
Integrating Prowler with external tools enhances its functionality and seamlessly embeds it into your workflows. Prowler supports a wide range of integrations to streamline security assessments and reporting. Common integration targets include messaging platforms like Slack, project management tools like Jira, and cloud services such as AWS Security Hub.
|
||||
|
||||
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can integrate it with the desired product!
|
||||
* Identify the best approach for the specific platform you’re targeting.
|
||||
|
||||
## Steps to Create an Integration
|
||||
|
||||
### Identify the Integration Purpose
|
||||
|
||||
* Clearly define the objective of the integration. For example:
|
||||
* Sending Prowler findings to a platform for alerts, tracking, or further analysis.
|
||||
* Review existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder for inspiration and implementation examples.
|
||||
|
||||
### Develop the Integration
|
||||
|
||||
* Script Development:
|
||||
* Write a script to process Prowler’s output and interact with the target platform’s API.
|
||||
* For example, to send findings, parse Prowler’s results and use the platform’s API to create entries or notifications.
|
||||
* Configuration:
|
||||
* Ensure your script includes configurable options for environment-specific settings, such as API endpoints and authentication tokens.
|
||||
|
||||
### Fundamental Structure
|
||||
|
||||
* Integration Class:
|
||||
* Create a class that encapsulates attributes and methods for the integration.
|
||||
Here is an example with Jira integration:
|
||||
```python title="Jira Class"
|
||||
class Jira:
|
||||
"""
|
||||
Jira class to interact with the Jira API
|
||||
|
||||
[Note]
|
||||
This integration is limited to a single Jira Cloud, therefore all the issues will be created for same Jira Cloud ID. We will need to work on the ability of providing a Jira Cloud ID if the user is present in more than one.
|
||||
|
||||
Attributes:
|
||||
- _redirect_uri: The redirect URI
|
||||
- _client_id: The client ID
|
||||
- _client_secret: The client secret
|
||||
- _access_token: The access token
|
||||
- _refresh_token: The refresh token
|
||||
- _expiration_date: The authentication expiration
|
||||
- _cloud_id: The cloud ID
|
||||
- _scopes: The scopes needed to authenticate, read:jira-user read:jira-work write:jira-work
|
||||
- AUTH_URL: The URL to authenticate with Jira
|
||||
- PARAMS_TEMPLATE: The template for the parameters to authenticate with Jira
|
||||
- TOKEN_URL: The URL to get the access token from Jira
|
||||
- API_TOKEN_URL: The URL to get the accessible resources from Jira
|
||||
|
||||
Methods:
|
||||
- __init__: Initialize the Jira object
|
||||
- input_authorization_code: Input the authorization code
|
||||
- auth_code_url: Generate the URL to authorize the application
|
||||
- get_auth: Get the access token and refresh token
|
||||
- get_cloud_id: Get the cloud ID from Jira
|
||||
- get_access_token: Get the access token
|
||||
- refresh_access_token: Refresh the access token from Jira
|
||||
- test_connection: Test the connection to Jira and return a Connection object
|
||||
- get_projects: Get the projects from Jira
|
||||
- get_available_issue_types: Get the available issue types for a project
|
||||
- send_findings: Send the findings to Jira and create an issue
|
||||
|
||||
Raises:
|
||||
- JiraGetAuthResponseError: Failed to get the access token and refresh token
|
||||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||||
- JiraAuthenticationError: Failed to authenticate
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||||
- JiraGetAccessTokenError: Failed to get the access token
|
||||
- JiraNoProjectsError: No projects found in Jira
|
||||
- JiraGetProjectsError: Failed to get projects from Jira
|
||||
- JiraGetProjectsResponseError: Failed to get projects from Jira, response code did not match 200
|
||||
- JiraInvalidIssueTypeError: The issue type is invalid
|
||||
- JiraGetAvailableIssueTypesError: Failed to get available issue types from Jira
|
||||
- JiraGetAvailableIssueTypesResponseError: Failed to get available issue types from Jira, response code did not match 200
|
||||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||||
- JiraTestConnectionError: Failed to test the connection
|
||||
|
||||
Usage:
|
||||
jira = Jira(
|
||||
redirect_uri="http://localhost:8080",
|
||||
client_id="client_id",
|
||||
client_secret="client_secret
|
||||
)
|
||||
jira.send_findings(findings=findings, project_key="KEY")
|
||||
"""
|
||||
|
||||
_redirect_uri: str = None
|
||||
_client_id: str = None
|
||||
_client_secret: str = None
|
||||
_access_token: str = None
|
||||
_refresh_token: str = None
|
||||
_expiration_date: int = None
|
||||
_cloud_id: str = None
|
||||
_scopes: list[str] = None
|
||||
AUTH_URL = "https://auth.atlassian.com/authorize"
|
||||
PARAMS_TEMPLATE = {
|
||||
"audience": "api.atlassian.com",
|
||||
"client_id": None,
|
||||
"scope": None,
|
||||
"redirect_uri": None,
|
||||
"state": None,
|
||||
"response_type": "code",
|
||||
"prompt": "consent",
|
||||
}
|
||||
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||||
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redirect_uri: str = None,
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
):
|
||||
self._redirect_uri = redirect_uri
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scopes = ["read:jira-user", "read:jira-work", "write:jira-work"]
|
||||
auth_url = self.auth_code_url()
|
||||
authorization_code = self.input_authorization_code(auth_url)
|
||||
self.get_auth(authorization_code)
|
||||
|
||||
# More properties and methods
|
||||
```
|
||||
* Test Connection Method:
|
||||
* Implement a method to validate credentials or tokens, ensuring the connection to the target platform is successful.
|
||||
The following is the code for the `test_connection` method for the `Jira` class:
|
||||
```python title="Test connection"
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
redirect_uri: str = None,
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
raise_on_exception: bool = True,
|
||||
) -> Connection:
|
||||
"""Test the connection to Jira
|
||||
|
||||
Args:
|
||||
- redirect_uri: The redirect URI
|
||||
- client_id: The client ID
|
||||
- client_secret: The client secret
|
||||
- raise_on_exception: Whether to raise an exception or not
|
||||
|
||||
Returns:
|
||||
- Connection: The connection object
|
||||
|
||||
Raises:
|
||||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||||
- JiraAuthenticationError: Failed to authenticate
|
||||
- JiraTestConnectionError: Failed to test the connection
|
||||
"""
|
||||
try:
|
||||
jira = Jira(
|
||||
redirect_uri=redirect_uri,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
access_token = jira.get_access_token()
|
||||
|
||||
if not access_token:
|
||||
return ValueError("Failed to get access token")
|
||||
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
response = requests.get(
|
||||
f"https://api.atlassian.com/ex/jira/{jira.cloud_id}/rest/api/3/myself",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return Connection(is_connected=True)
|
||||
else:
|
||||
return Connection(is_connected=False, error=response.json())
|
||||
except JiraGetCloudIDNoResourcesError as no_resources_error:
|
||||
logger.error(
|
||||
f"{no_resources_error.__class__.__name__}[{no_resources_error.__traceback__.tb_lineno}]: {no_resources_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise no_resources_error
|
||||
return Connection(error=no_resources_error)
|
||||
except JiraGetCloudIDResponseError as response_error:
|
||||
logger.error(
|
||||
f"{response_error.__class__.__name__}[{response_error.__traceback__.tb_lineno}]: {response_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise response_error
|
||||
return Connection(error=response_error)
|
||||
except JiraGetCloudIDError as cloud_id_error:
|
||||
logger.error(
|
||||
f"{cloud_id_error.__class__.__name__}[{cloud_id_error.__traceback__.tb_lineno}]: {cloud_id_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise cloud_id_error
|
||||
return Connection(error=cloud_id_error)
|
||||
except JiraAuthenticationError as auth_error:
|
||||
logger.error(
|
||||
f"{auth_error.__class__.__name__}[{auth_error.__traceback__.tb_lineno}]: {auth_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise auth_error
|
||||
return Connection(error=auth_error)
|
||||
except Exception as error:
|
||||
logger.error(f"Failed to test connection: {error}")
|
||||
if raise_on_exception:
|
||||
raise JiraTestConnectionError(
|
||||
message="Failed to test connection on the Jira integration",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
return Connection(is_connected=False, error=error)
|
||||
```
|
||||
* Send Findings Method:
|
||||
* Add a method to send Prowler findings to the target platform, adhering to its API specifications.
|
||||
The following is the code for the `send_findings` method for the `Jira` class:
|
||||
```python title="Send findings method"
|
||||
def send_findings(
|
||||
self,
|
||||
findings: list[Finding] = None,
|
||||
project_key: str = None,
|
||||
issue_type: str = None,
|
||||
):
|
||||
"""
|
||||
Send the findings to Jira
|
||||
|
||||
Args:
|
||||
- findings: The findings to send
|
||||
- project_key: The project key
|
||||
- issue_type: The issue type
|
||||
|
||||
Raises:
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||||
"""
|
||||
try:
|
||||
access_token = self.get_access_token()
|
||||
|
||||
if not access_token:
|
||||
raise JiraNoTokenError(
|
||||
message="No token was found",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
projects = self.get_projects()
|
||||
|
||||
if project_key not in projects:
|
||||
logger.error("The project key is invalid")
|
||||
raise JiraInvalidProjectKeyError(
|
||||
message="The project key is invalid",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
available_issue_types = self.get_available_issue_types(project_key)
|
||||
|
||||
if issue_type not in available_issue_types:
|
||||
logger.error("The issue type is invalid")
|
||||
raise JiraInvalidIssueTypeError(
|
||||
message="The issue type is invalid", file=os.path.basename(__file__)
|
||||
)
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
for finding in findings:
|
||||
status_color = self.get_color_from_status(finding.status.value)
|
||||
adf_description = self.get_adf_description(
|
||||
check_id=finding.metadata.CheckID,
|
||||
check_title=finding.metadata.CheckTitle,
|
||||
severity=finding.metadata.Severity.value.upper(),
|
||||
status=finding.status.value,
|
||||
status_color=status_color,
|
||||
status_extended=finding.status_extended,
|
||||
provider=finding.metadata.Provider,
|
||||
region=finding.region,
|
||||
resource_uid=finding.resource_uid,
|
||||
resource_name=finding.resource_name,
|
||||
risk=finding.metadata.Risk,
|
||||
recommendation_text=finding.metadata.Remediation.Recommendation.Text,
|
||||
recommendation_url=finding.metadata.Remediation.Recommendation.Url,
|
||||
)
|
||||
payload = {
|
||||
"fields": {
|
||||
"project": {"key": project_key},
|
||||
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
|
||||
"description": adf_description,
|
||||
"issuetype": {"name": issue_type},
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
response_error = f"Failed to send finding: {response.status_code} - {response.json()}"
|
||||
logger.warning(response_error)
|
||||
raise JiraSendFindingsResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
else:
|
||||
logger.info(f"Finding sent successfully: {response.json()}")
|
||||
except JiraRefreshTokenError as refresh_error:
|
||||
raise refresh_error
|
||||
except JiraRefreshTokenResponseError as response_error:
|
||||
raise response_error
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send findings: {e}")
|
||||
raise JiraCreateIssueError(
|
||||
message="Failed to create an issue in Jira",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
* Test the integration in a controlled environment to confirm it behaves as expected.
|
||||
* Verify that Prowler’s findings are accurately transmitted and correctly processed by the target platform.
|
||||
* Simulate edge cases to ensure robust error handling.
|
||||
|
||||
### Documentation
|
||||
|
||||
* Provide clear, detailed documentation for your integration:
|
||||
* Setup instructions, including any required dependencies.
|
||||
* Configuration details, such as environment variables or authentication steps.
|
||||
* Example use cases and troubleshooting tips.
|
||||
* Good documentation ensures maintainability and simplifies onboarding for team members.
|
||||
Coming soon ...
|
||||
|
||||
@@ -1,166 +1,3 @@
|
||||
# Create a Custom Output Format
|
||||
# Create a custom output format
|
||||
|
||||
## Introduction
|
||||
|
||||
Prowler can generate outputs in multiple formats, allowing users to customize the way findings are presented. This is particularly useful when integrating Prowler with third-party tools, creating specialized reports, or simply tailoring the data to meet specific requirements. A custom output format gives you the flexibility to extract and display only the most relevant information in the way you need it.
|
||||
|
||||
* Prowler organizes its outputs in the `/lib/outputs` directory. Each format (e.g., JSON, CSV, HTML) is implemented as a Python class.
|
||||
* Outputs are generated based on findings collected during a scan. Each finding is represented as a structured dictionary containing details like resource IDs, severities, descriptions, and more.
|
||||
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can create it with the desired output!
|
||||
* Identify the best approach for the specific output you’re targeting.
|
||||
|
||||
## Steps to Create a Custom Output Format
|
||||
|
||||
### Schema
|
||||
|
||||
* Output Class:
|
||||
* The class must inherit from `Output`. Review the [Output Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/output.py).
|
||||
* Create a class that encapsulates attributes and methods for the output.
|
||||
The following is the code for the `CSV` class:
|
||||
```python title="CSV Class"
|
||||
class CSV(Output):
|
||||
def transform(self, findings: List[Finding]) -> None:
|
||||
"""Transforms the findings into the CSV format.
|
||||
|
||||
Args:
|
||||
findings (list[Finding]): a list of Finding objects
|
||||
|
||||
"""
|
||||
...
|
||||
```
|
||||
* Transform Method:
|
||||
* This method will transform the findings provided by Prowler to a specific format.
|
||||
The following is the code for the `transform` method for the `CSV` class:
|
||||
```python title="Transform"
|
||||
def transform(self, findings: List[Finding]) -> None:
|
||||
"""Transforms the findings into the CSV format.
|
||||
|
||||
Args:
|
||||
findings (list[Finding]): a list of Finding objects
|
||||
|
||||
"""
|
||||
try:
|
||||
for finding in findings:
|
||||
finding_dict = {}
|
||||
finding_dict["AUTH_METHOD"] = finding.auth_method
|
||||
finding_dict["TIMESTAMP"] = finding.timestamp
|
||||
finding_dict["ACCOUNT_UID"] = finding.account_uid
|
||||
finding_dict["ACCOUNT_NAME"] = finding.account_name
|
||||
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
|
||||
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
|
||||
finding.account_organization_uid
|
||||
)
|
||||
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
|
||||
finding.account_organization_name
|
||||
)
|
||||
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
|
||||
finding.account_tags, separator=":"
|
||||
)
|
||||
finding_dict["FINDING_UID"] = finding.uid
|
||||
finding_dict["PROVIDER"] = finding.metadata.Provider
|
||||
finding_dict["CHECK_ID"] = finding.metadata.CheckID
|
||||
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
|
||||
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
|
||||
finding_dict["STATUS"] = finding.status.value
|
||||
finding_dict["STATUS_EXTENDED"] = finding.status_extended
|
||||
finding_dict["MUTED"] = finding.muted
|
||||
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
|
||||
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
|
||||
finding_dict["SEVERITY"] = finding.metadata.Severity.value
|
||||
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
|
||||
finding_dict["RESOURCE_UID"] = finding.resource_uid
|
||||
finding_dict["RESOURCE_NAME"] = finding.resource_name
|
||||
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
|
||||
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
|
||||
finding_dict["PARTITION"] = finding.partition
|
||||
finding_dict["REGION"] = finding.region
|
||||
finding_dict["DESCRIPTION"] = finding.metadata.Description
|
||||
finding_dict["RISK"] = finding.metadata.Risk
|
||||
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
|
||||
finding.metadata.Remediation.Recommendation.Text
|
||||
)
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
|
||||
finding.metadata.Remediation.Recommendation.Url
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
|
||||
finding.metadata.Remediation.Code.NativeIaC
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
|
||||
finding.metadata.Remediation.Code.Terraform
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_CLI"] = (
|
||||
finding.metadata.Remediation.Code.CLI
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_OTHER"] = (
|
||||
finding.metadata.Remediation.Code.Other
|
||||
)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(
|
||||
finding.compliance, separator=": "
|
||||
)
|
||||
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
|
||||
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
|
||||
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
|
||||
finding_dict["NOTES"] = finding.metadata.Notes
|
||||
finding_dict["PROWLER_VERSION"] = finding.prowler_version
|
||||
self._data.append(finding_dict)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
```
|
||||
* Batch Write Data To File Method:
|
||||
* This method will write the modeled object to a file.
|
||||
The following is the code for the `batch_write_data_to_file` method for the `CSV` class:
|
||||
```python title="Batch Write Data To File"
|
||||
def batch_write_data_to_file(self) -> None:
|
||||
"""Writes the findings to a file using the CSV format using the `Output._file_descriptor`."""
|
||||
try:
|
||||
if (
|
||||
getattr(self, "_file_descriptor", None)
|
||||
and not self._file_descriptor.closed
|
||||
and self._data
|
||||
):
|
||||
csv_writer = DictWriter(
|
||||
self._file_descriptor,
|
||||
fieldnames=self._data[0].keys(),
|
||||
delimiter=";",
|
||||
)
|
||||
csv_writer.writeheader()
|
||||
for finding in self._data:
|
||||
csv_writer.writerow(finding)
|
||||
self._file_descriptor.close()
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
```
|
||||
|
||||
### Integration With The Current Code
|
||||
|
||||
Once that the desired output format is created it has to be integrated with Prowler. Take a look at the the usage from the current supported output in order to add the new one.
|
||||
Here is an example of the CSV output creation inside [prowler code](https://github.com/prowler-cloud/prowler/blob/master/prowler/__main__.py):
|
||||
```python title="CSV creation"
|
||||
if mode == "csv":
|
||||
csv_output = CSV(
|
||||
findings=finding_outputs,
|
||||
create_file_descriptor=True,
|
||||
file_path=f"{filename}{csv_file_suffix}",
|
||||
)
|
||||
generated_outputs["regular"].append(csv_output)
|
||||
# Write CSV Finding Object to file
|
||||
csv_output.batch_write_data_to_file()
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
* Verify that Prowler’s findings are accurately writed in the desired output format.
|
||||
* Simulate edge cases to ensure robust error handling.
|
||||
|
||||
### Documentation
|
||||
|
||||
* Provide clear, detailed documentation for your output:
|
||||
* Setup instructions, including any required dependencies.
|
||||
* Configuration details.
|
||||
* Example use cases and troubleshooting tips.
|
||||
* Good documentation ensures maintainability and simplifies onboarding for new users.
|
||||
Coming soon ...
|
||||
|
||||
@@ -175,7 +175,6 @@ Due to the complexity and differences of each provider use the rest of the provi
|
||||
- [GCP](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/gcp_provider.py)
|
||||
- [Azure](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/azure_provider.py)
|
||||
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
|
||||
- [Microsoft365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/microsoft365/microsoft365_provider.py)
|
||||
|
||||
To facilitate understanding here is a pseudocode of how the most basic provider could be with examples.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user