merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables
@@ -10,6 +10,8 @@ NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL}
|
||||
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
|
||||
AUTH_TRUST_HOST=true
|
||||
UI_PORT=3000
|
||||
# Temp URL for feeds need to use actual
|
||||
RSS_FEED_URL=https://prowler.com/blog/rss
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
|
||||
# Google Tag Manager ID
|
||||
@@ -83,55 +85,21 @@ DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_MANAGE_DB_PARTITIONS=True
|
||||
# openssl genrsa -out private.pem 2048
|
||||
DJANGO_TOKEN_SIGNING_KEY="-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDs4e+kt7SnUJek
|
||||
6V5r9zMGzXCoU5qnChfPiqu+BgANyawz+MyVZPs6RCRfeo6tlCknPQtOziyXYM2I
|
||||
7X+qckmuzsjqp8+u+o1mw3VvUuJew5k2SQLPYwsiTzuFNVJEOgRo3hywGiGwS2iv
|
||||
/5nh2QAl7fq2qLqZEXQa5+/xJlQggS1CYxOJgggvLyra50QZlBvPve/AxKJ/EV/Q
|
||||
irWTZU5lLNI8sH2iZR05vQeBsxZ0dCnGMT+vGl+cGkqrvzQzKsYbDmabMcfTYhYi
|
||||
78fpv6A4uharJFHayypYBjE39PwhMyyeycrNXlpm1jpq+03HgmDuDMHydk1tNwuT
|
||||
nEC7m7iNAgMBAAECggEAA2m48nJcJbn9SVi8bclMwKkWmbJErOnyEGEy2sTK3Of+
|
||||
NWx9BB0FmqAPNxn0ss8K7cANKOhDD7ZLF9E2MO4/HgfoMKtUzHRbM7MWvtEepldi
|
||||
nnvcUMEgULD8Dk4HnqiIVjt3BdmGiTv46OpBnRWrkSBV56pUL+7msZmMZTjUZvh2
|
||||
ZWv0+I3gtDIjo2Zo/FiwDV7CfwRjJarRpYUj/0YyuSA4FuOUYl41WAX1I301FKMH
|
||||
xo3jiAYi1s7IneJ16OtPpOA34Wg5F6ebm/UO0uNe+iD4kCXKaZmxYQPh5tfB0Qa3
|
||||
qj1T7GNpFNyvtG7VVdauhkb8iu8X/wl6PCwbg0RCKQKBgQD9HfpnpH0lDlHMRw9K
|
||||
X7Vby/1fSYy1BQtlXFEIPTN/btJ/asGxLmAVwJ2HAPXWlrfSjVAH7CtVmzN7v8oj
|
||||
HeIHfeSgoWEu1syvnv2AMaYSo03UjFFlfc/GUxF7DUScRIhcJUPCP8jkAROz9nFv
|
||||
DByNjUL17Q9r43DmDiRsy0IFqQKBgQDvlJ9Uhl+Sp7gRgKYwa/IG0+I4AduAM+Gz
|
||||
Dxbm52QrMGMTjaJFLmLHBUZ/ot+pge7tZZGws8YR8ufpyMJbMqPjxhIvRRa/p1Tf
|
||||
E3TQPW93FMsHUvxAgY3MV5MzXFPhlNAKb+akP/RcXUhetGAuZKLubtDCWa55ZQuL
|
||||
wj2OS+niRQKBgE7K8zUqNi6/22S8xhy/2GPgB1qPObbsABUofK0U6CAGLo6te+gc
|
||||
6Jo84IyzFtQbDNQFW2Fr+j1m18rw9AqkdcUhQndiZS9AfG07D+zFB86LeWHt4DS4
|
||||
ymIRX8Kvaak/iDcu/n3Mf0vCrhB6aetImObTj4GgrwlFvtJOmrYnO8EpAoGAIXXP
|
||||
Xt25gWD9OyyNiVu6HKwA/zN7NYeJcRmdaDhO7B1A6R0x2Zml4AfjlbXoqOLlvLAf
|
||||
zd79vcoAC82nH1eOPiSOq51plPDI0LMF8IN0CtyTkn1Lj7LIXA6rF1RAvtOqzppc
|
||||
SvpHpZK9pcRpXnFdtBE0BMDDtl6fYzCIqlP94UUCgYEAnhXbAQMF7LQifEm34Dx8
|
||||
BizRMOKcqJGPvbO2+Iyt50O5X6onU2ITzSV1QHtOvAazu+B1aG9pEuBFDQ+ASxEu
|
||||
L9ruJElkOkb/o45TSF6KCsHd55ReTZ8AqnRjf5R+lyzPqTZCXXb8KTcRvWT4zQa3
|
||||
VxyT2PnaSqEcexWUy4+UXoQ=
|
||||
-----END PRIVATE KEY-----"
|
||||
DJANGO_TOKEN_SIGNING_KEY=""
|
||||
# openssl rsa -in private.pem -pubout -out public.pem
|
||||
DJANGO_TOKEN_VERIFYING_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7OHvpLe0p1CXpOlea/cz
|
||||
Bs1wqFOapwoXz4qrvgYADcmsM/jMlWT7OkQkX3qOrZQpJz0LTs4sl2DNiO1/qnJJ
|
||||
rs7I6qfPrvqNZsN1b1LiXsOZNkkCz2MLIk87hTVSRDoEaN4csBohsEtor/+Z4dkA
|
||||
Je36tqi6mRF0Gufv8SZUIIEtQmMTiYIILy8q2udEGZQbz73vwMSifxFf0Iq1k2VO
|
||||
ZSzSPLB9omUdOb0HgbMWdHQpxjE/rxpfnBpKq780MyrGGw5mmzHH02IWIu/H6b+g
|
||||
OLoWqyRR2ssqWAYxN/T8ITMsnsnKzV5aZtY6avtNx4Jg7gzB8nZNbTcLk5xAu5u4
|
||||
jQIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
DJANGO_TOKEN_VERIFYING_KEY=""
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
DJANGO_SENTRY_DSN=
|
||||
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
|
||||
|
||||
# Sentry settings
|
||||
SENTRY_ENVIRONMENT=local
|
||||
SENTRY_RELEASE=local
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.7.5
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.12.2
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -37,6 +37,11 @@ provider/iac:
|
||||
- any-glob-to-any-file: "prowler/providers/iac/**"
|
||||
- any-glob-to-any-file: "tests/providers/iac/**"
|
||||
|
||||
provider/mongodbatlas:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/mongodbatlas/**"
|
||||
- any-glob-to-any-file: "tests/providers/mongodbatlas/**"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ".github/workflows/*"
|
||||
@@ -52,11 +57,13 @@ mutelist:
|
||||
- any-glob-to-any-file: "prowler/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/kubernetes/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/mongodbatlas/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/kubernetes/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/mongodbatlas/lib/mutelist/**"
|
||||
|
||||
integration/s3:
|
||||
- changed-files:
|
||||
@@ -103,6 +110,10 @@ component/ui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "ui/**"
|
||||
|
||||
component/mcp-server:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "mcp_server/**"
|
||||
|
||||
compliance:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/compliance/**"
|
||||
@@ -112,3 +123,7 @@ compliance:
|
||||
review-django-migrations:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "api/src/backend/api/migrations/**"
|
||||
|
||||
metadata-review:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "**/*.metadata.json"
|
||||
|
||||
@@ -8,6 +8,10 @@ If fixes an issue please add it with `Fix #XXXX`
|
||||
|
||||
Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
|
||||
|
||||
### Steps to review
|
||||
|
||||
Please add a detailed description of how to review this PR.
|
||||
|
||||
### Checklist
|
||||
|
||||
- Are there new checks included in this PR? Yes / No
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: vars
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Trigger deployment
|
||||
if: github.event_name == 'push'
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
|
||||
@@ -44,16 +44,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/api-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- ".github/workflows/api-pull-request.yml"
|
||||
- "api/**"
|
||||
|
||||
env:
|
||||
@@ -75,18 +76,20 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: api/**
|
||||
files: |
|
||||
api/**
|
||||
.github/workflows/api-pull-request.yml
|
||||
files_ignore: ${{ env.IGNORE_FILES }}
|
||||
|
||||
- name: Replace @master with current branch in pyproject.toml
|
||||
- name: Replace @master with current branch in pyproject.toml - Only for pull requests to `master`
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'pull_request' && github.base_ref == 'master'
|
||||
run: |
|
||||
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
echo "Using branch: $BRANCH_NAME"
|
||||
@@ -99,7 +102,24 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry==2.1.1
|
||||
|
||||
- name: Update poetry.lock after the branch name change
|
||||
- name: Update SDK's poetry.lock resolved_reference to latest commit - Only for push events to `master`
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
# Get the latest commit hash from the prowler-cloud/prowler repository
|
||||
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
|
||||
echo "Latest commit hash: $LATEST_COMMIT"
|
||||
|
||||
# Update the resolved_reference specifically for prowler-cloud/prowler repository
|
||||
sed -i '/url = "https:\/\/github\.com\/prowler-cloud\/prowler\.git"/,/resolved_reference = / {
|
||||
s/resolved_reference = "[a-f0-9]\{40\}"/resolved_reference = "'"$LATEST_COMMIT"'"/
|
||||
}' poetry.lock
|
||||
|
||||
# Verify the change was made
|
||||
echo "Updated resolved_reference:"
|
||||
grep -A2 -B2 "resolved_reference" poetry.lock
|
||||
|
||||
- name: Update poetry.lock
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
@@ -107,17 +127,11 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install system dependencies for xmlsec
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-dev libxmlsec1-dev libxmlsec1-openssl pkg-config
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
@@ -188,7 +202,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
@@ -196,11 +210,11 @@ jobs:
|
||||
test-container-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: api/**
|
||||
files_ignore: ${{ env.IGNORE_FILES }}
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- 'v3'
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/build-documentation-on-pr.yml'
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -16,9 +17,20 @@ jobs:
|
||||
name: Documentation Link
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Leave PR comment with the Prowler Documentation URI
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
- name: Find existing documentation comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '<!-- prowler-docs-link -->'
|
||||
|
||||
- name: Create or update PR comment with the Prowler Documentation URI
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
body: |
|
||||
<!-- prowler-docs-link -->
|
||||
You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
|
||||
edit-mode: replace
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create Backport Label
|
||||
name: Prowler - Create Backport Label
|
||||
|
||||
on:
|
||||
release:
|
||||
|
||||
@@ -7,11 +7,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@6641d4ba5b684fffe195b9820345de1bf19f3181 # v3.89.2
|
||||
uses: trufflesecurity/trufflehog@466da5b0bb161144f6afca9afe5d57975828c410 # v3.90.8
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
name: Prowler - PR Conflict Checker
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
branches:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
|
||||
jobs:
|
||||
conflict-checker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
**
|
||||
|
||||
- name: Check for conflict markers
|
||||
id: conflict-check
|
||||
run: |
|
||||
echo "Checking for conflict markers in changed files..."
|
||||
|
||||
CONFLICT_FILES=""
|
||||
HAS_CONFLICTS=false
|
||||
|
||||
# Check each changed file for conflict markers
|
||||
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Checking file: $file"
|
||||
|
||||
# Look for conflict markers
|
||||
if grep -l "^<<<<<<<\|^=======\|^>>>>>>>" "$file" 2>/dev/null; then
|
||||
echo "Conflict markers found in: $file"
|
||||
CONFLICT_FILES="$CONFLICT_FILES$file "
|
||||
HAS_CONFLICTS=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$HAS_CONFLICTS" = true ]; then
|
||||
echo "has_conflicts=true" >> $GITHUB_OUTPUT
|
||||
echo "conflict_files=$CONFLICT_FILES" >> $GITHUB_OUTPUT
|
||||
echo "Conflict markers detected in files: $CONFLICT_FILES"
|
||||
else
|
||||
echo "has_conflicts=false" >> $GITHUB_OUTPUT
|
||||
echo "No conflict markers found in changed files"
|
||||
fi
|
||||
|
||||
- name: Add conflict label
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
script: |
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const hasConflictLabel = labels.some(label => label.name === 'has-conflicts');
|
||||
|
||||
if (!hasConflictLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['has-conflicts']
|
||||
});
|
||||
console.log('Added has-conflicts label');
|
||||
} else {
|
||||
console.log('has-conflicts label already exists');
|
||||
}
|
||||
|
||||
- name: Remove conflict label
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'false'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'has-conflicts'
|
||||
});
|
||||
console.log('Removed has-conflicts label');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('has-conflicts label was not present');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
- name: Find existing conflict comment
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'true'
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
|
||||
|
||||
- name: Create or update conflict comment
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'true'
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
⚠️ **Conflict Markers Detected**
|
||||
|
||||
This pull request contains unresolved conflict markers in the following files:
|
||||
```
|
||||
${{ steps.conflict-check.outputs.conflict_files }}
|
||||
```
|
||||
|
||||
Please resolve these conflicts by:
|
||||
1. Locating the conflict markers: `<<<<<<<`, `=======`, and `>>>>>>>`
|
||||
2. Manually editing the files to resolve the conflicts
|
||||
3. Removing all conflict markers
|
||||
4. Committing and pushing the changes
|
||||
|
||||
- name: Find existing conflict comment when resolved
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'false'
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-resolved-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
|
||||
|
||||
- name: Update comment when conflicts resolved
|
||||
if: steps.conflict-check.outputs.has_conflicts == 'false' && steps.find-resolved-comment.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
comment-id: ${{ steps.find-resolved-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
✅ **Conflict Markers Resolved**
|
||||
|
||||
All conflict markers have been successfully resolved in this pull request.
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Prowler Release Preparation
|
||||
name: Prowler - Release Preparation
|
||||
|
||||
run-name: Prowler Release Preparation for ${{ inputs.prowler_version }}
|
||||
|
||||
@@ -22,12 +22,13 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
@@ -36,6 +37,11 @@ jobs:
|
||||
python3 -m pip install --user poetry
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "prowler-bot"
|
||||
git config --global user.email "179230569+prowler-bot@users.noreply.github.com"
|
||||
|
||||
- name: Parse version and determine branch
|
||||
run: |
|
||||
# Validate version format (reusing pattern from sdk-bump-version.yml)
|
||||
@@ -53,25 +59,157 @@ jobs:
|
||||
BRANCH_NAME="v${MAJOR_VERSION}.${MINOR_VERSION}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_ENV}"
|
||||
|
||||
# Calculate UI version (1.X.X format - matches Prowler minor version)
|
||||
UI_VERSION="1.${MINOR_VERSION}.${PATCH_VERSION}"
|
||||
echo "UI_VERSION=${UI_VERSION}" >> "${GITHUB_ENV}"
|
||||
# Function to extract the latest version from changelog
|
||||
extract_latest_version() {
|
||||
local changelog_file="$1"
|
||||
if [ -f "$changelog_file" ]; then
|
||||
# Extract the first version entry (most recent) from changelog
|
||||
# Format: ## [version] (1.2.3) or ## [vversion] (v1.2.3)
|
||||
local version=$(grep -m 1 '^## \[' "$changelog_file" | sed 's/^## \[\(.*\)\].*/\1/' | sed 's/^v//' | tr -d '[:space:]')
|
||||
echo "$version"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Calculate API version (1.X.X format - one minor version ahead)
|
||||
API_MINOR_VERSION=$((MINOR_VERSION + 1))
|
||||
API_VERSION="1.${API_MINOR_VERSION}.${PATCH_VERSION}"
|
||||
# Read actual versions from changelogs (source of truth)
|
||||
UI_VERSION=$(extract_latest_version "ui/CHANGELOG.md")
|
||||
API_VERSION=$(extract_latest_version "api/CHANGELOG.md")
|
||||
SDK_VERSION=$(extract_latest_version "prowler/CHANGELOG.md")
|
||||
|
||||
echo "UI_VERSION=${UI_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "API_VERSION=${API_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "SDK_VERSION=${SDK_VERSION}" >> "${GITHUB_ENV}"
|
||||
|
||||
if [ -n "$UI_VERSION" ]; then
|
||||
echo "Read UI version from changelog: $UI_VERSION"
|
||||
else
|
||||
echo "Warning: No UI version found in ui/CHANGELOG.md"
|
||||
fi
|
||||
|
||||
if [ -n "$API_VERSION" ]; then
|
||||
echo "Read API version from changelog: $API_VERSION"
|
||||
else
|
||||
echo "Warning: No API version found in api/CHANGELOG.md"
|
||||
fi
|
||||
|
||||
if [ -n "$SDK_VERSION" ]; then
|
||||
echo "Read SDK version from changelog: $SDK_VERSION"
|
||||
else
|
||||
echo "Warning: No SDK version found in prowler/CHANGELOG.md"
|
||||
fi
|
||||
|
||||
echo "Prowler version: $PROWLER_VERSION"
|
||||
echo "Branch name: $BRANCH_NAME"
|
||||
echo "UI version: $UI_VERSION"
|
||||
echo "API version: $API_VERSION"
|
||||
echo "SDK version: $SDK_VERSION"
|
||||
echo "Is minor release: $([ $PATCH_VERSION -eq 0 ] && echo 'true' || echo 'false')"
|
||||
else
|
||||
echo "Invalid version syntax: '$PROWLER_VERSION' (must be N.N.N)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract changelog entries
|
||||
run: |
|
||||
set -e
|
||||
|
||||
# Function to extract changelog for a specific version
|
||||
extract_changelog() {
|
||||
local file="$1"
|
||||
local version="$2"
|
||||
local output_file="$3"
|
||||
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "Warning: $file not found, skipping..."
|
||||
touch "$output_file"
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract changelog section for this version
|
||||
awk -v version="$version" '
|
||||
/^## \[v?'"$version"'\]/ { found=1; next }
|
||||
found && /^## \[v?[0-9]+\.[0-9]+\.[0-9]+\]/ { found=0 }
|
||||
found && !/^## \[v?'"$version"'\]/ { print }
|
||||
' "$file" > "$output_file"
|
||||
|
||||
# Remove --- separators
|
||||
sed -i '/^---$/d' "$output_file"
|
||||
|
||||
# Remove trailing empty lines
|
||||
sed -i '/^$/d' "$output_file"
|
||||
}
|
||||
|
||||
# Calculate expected versions for this release
|
||||
if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
EXPECTED_UI_VERSION="1.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
|
||||
EXPECTED_API_VERSION="1.$((${BASH_REMATCH[2]} + 1)).${BASH_REMATCH[3]}"
|
||||
|
||||
echo "Expected UI version for this release: $EXPECTED_UI_VERSION"
|
||||
echo "Expected API version for this release: $EXPECTED_API_VERSION"
|
||||
fi
|
||||
|
||||
# Determine if components have changes for this specific release
|
||||
# UI has changes if its current version matches what we expect for this release
|
||||
if [ -n "$UI_VERSION" ] && [ "$UI_VERSION" = "$EXPECTED_UI_VERSION" ]; then
|
||||
echo "HAS_UI_CHANGES=true" >> $GITHUB_ENV
|
||||
echo "✓ UI changes detected - version matches expected: $UI_VERSION"
|
||||
extract_changelog "ui/CHANGELOG.md" "$UI_VERSION" "ui_changelog.md"
|
||||
else
|
||||
echo "HAS_UI_CHANGES=false" >> $GITHUB_ENV
|
||||
echo "ℹ No UI changes for this release (current: $UI_VERSION, expected: $EXPECTED_UI_VERSION)"
|
||||
touch "ui_changelog.md"
|
||||
fi
|
||||
|
||||
# API has changes if its current version matches what we expect for this release
|
||||
if [ -n "$API_VERSION" ] && [ "$API_VERSION" = "$EXPECTED_API_VERSION" ]; then
|
||||
echo "HAS_API_CHANGES=true" >> $GITHUB_ENV
|
||||
echo "✓ API changes detected - version matches expected: $API_VERSION"
|
||||
extract_changelog "api/CHANGELOG.md" "$API_VERSION" "api_changelog.md"
|
||||
else
|
||||
echo "HAS_API_CHANGES=false" >> $GITHUB_ENV
|
||||
echo "ℹ No API changes for this release (current: $API_VERSION, expected: $EXPECTED_API_VERSION)"
|
||||
touch "api_changelog.md"
|
||||
fi
|
||||
|
||||
# SDK has changes if its current version matches the input version
|
||||
if [ -n "$SDK_VERSION" ] && [ "$SDK_VERSION" = "$PROWLER_VERSION" ]; then
|
||||
echo "HAS_SDK_CHANGES=true" >> $GITHUB_ENV
|
||||
echo "✓ SDK changes detected - version matches input: $SDK_VERSION"
|
||||
extract_changelog "prowler/CHANGELOG.md" "$PROWLER_VERSION" "prowler_changelog.md"
|
||||
else
|
||||
echo "HAS_SDK_CHANGES=false" >> $GITHUB_ENV
|
||||
echo "ℹ No SDK changes for this release (current: $SDK_VERSION, input: $PROWLER_VERSION)"
|
||||
touch "prowler_changelog.md"
|
||||
fi
|
||||
|
||||
# Combine changelogs in order: UI, API, SDK
|
||||
> combined_changelog.md
|
||||
|
||||
if [ "$HAS_UI_CHANGES" = "true" ] && [ -s "ui_changelog.md" ]; then
|
||||
echo "## UI" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat ui_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
if [ "$HAS_API_CHANGES" = "true" ] && [ -s "api_changelog.md" ]; then
|
||||
echo "## API" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat api_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
if [ "$HAS_SDK_CHANGES" = "true" ] && [ -s "prowler_changelog.md" ]; then
|
||||
echo "## SDK" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat prowler_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
echo "Combined changelog preview:"
|
||||
cat combined_changelog.md
|
||||
|
||||
- name: Checkout existing branch for patch release
|
||||
if: ${{ env.PATCH_VERSION != '0' }}
|
||||
run: |
|
||||
@@ -108,6 +246,7 @@ jobs:
|
||||
echo "✓ prowler/config/config.py version: $CURRENT_VERSION"
|
||||
|
||||
- name: Verify version in api/pyproject.toml
|
||||
if: ${{ env.HAS_API_CHANGES == 'true' }}
|
||||
run: |
|
||||
CURRENT_API_VERSION=$(grep '^version = ' api/pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')
|
||||
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
|
||||
@@ -118,7 +257,7 @@ jobs:
|
||||
echo "✓ api/pyproject.toml version: $CURRENT_API_VERSION"
|
||||
|
||||
- name: Verify prowler dependency in api/pyproject.toml
|
||||
if: ${{ env.PATCH_VERSION != '0' }}
|
||||
if: ${{ env.PATCH_VERSION != '0' && env.HAS_API_CHANGES == 'true' }}
|
||||
run: |
|
||||
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
|
||||
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
|
||||
@@ -129,6 +268,7 @@ jobs:
|
||||
echo "✓ api/pyproject.toml prowler dependency: $CURRENT_PROWLER_REF"
|
||||
|
||||
- name: Verify version in api/src/backend/api/v1/views.py
|
||||
if: ${{ env.HAS_API_CHANGES == 'true' }}
|
||||
run: |
|
||||
CURRENT_API_VERSION=$(grep 'spectacular_settings.VERSION = ' api/src/backend/api/v1/views.py | sed -E 's/.*spectacular_settings.VERSION = "([^"]+)".*/\1/' | tr -d '[:space:]')
|
||||
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
|
||||
@@ -138,24 +278,33 @@ jobs:
|
||||
fi
|
||||
echo "✓ api/src/backend/api/v1/views.py version: $CURRENT_API_VERSION"
|
||||
|
||||
- name: Create release branch for minor release
|
||||
- name: Checkout existing release branch for minor release
|
||||
if: ${{ env.PATCH_VERSION == '0' }}
|
||||
run: |
|
||||
echo "Minor release detected (patch = 0), creating new branch $BRANCH_NAME..."
|
||||
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME" || git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
|
||||
echo "ERROR: Branch $BRANCH_NAME already exists for minor release $PROWLER_VERSION"
|
||||
echo "Minor release detected (patch = 0), checking out existing branch $BRANCH_NAME..."
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
|
||||
echo "Branch $BRANCH_NAME exists remotely, checking out..."
|
||||
git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME"
|
||||
else
|
||||
echo "ERROR: Branch $BRANCH_NAME should exist for minor release $PROWLER_VERSION. Please create it manually first."
|
||||
exit 1
|
||||
fi
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
- name: Update prowler dependency in api/pyproject.toml
|
||||
- name: Prepare prowler dependency update for minor release
|
||||
if: ${{ env.PATCH_VERSION == '0' }}
|
||||
run: |
|
||||
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
|
||||
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
|
||||
|
||||
# Minor release: update the dependency to use the new branch
|
||||
echo "Minor release detected - updating prowler dependency from '$CURRENT_PROWLER_REF' to '$BRANCH_NAME_TRIMMED'"
|
||||
# Create a temporary branch for the PR from the minor version branch
|
||||
TEMP_BRANCH="update-api-dependency-$BRANCH_NAME_TRIMMED-$(date +%s)"
|
||||
echo "TEMP_BRANCH=$TEMP_BRANCH" >> $GITHUB_ENV
|
||||
|
||||
# Create temp branch from the current minor version branch
|
||||
git checkout -b "$TEMP_BRANCH"
|
||||
|
||||
# Minor release: update the dependency to use the release branch
|
||||
echo "Updating prowler dependency from '$CURRENT_PROWLER_REF' to '$BRANCH_NAME_TRIMMED'"
|
||||
sed -i "s|prowler @ git+https://github.com/prowler-cloud/prowler.git@[^\"]*\"|prowler @ git+https://github.com/prowler-cloud/prowler.git@$BRANCH_NAME_TRIMMED\"|" api/pyproject.toml
|
||||
|
||||
# Verify the change was made
|
||||
@@ -168,87 +317,51 @@ jobs:
|
||||
# Update poetry lock file
|
||||
echo "Updating poetry.lock file..."
|
||||
cd api
|
||||
poetry lock --no-update
|
||||
poetry lock
|
||||
cd ..
|
||||
|
||||
# Commit and push the changes
|
||||
# Commit and push the temporary branch
|
||||
git add api/pyproject.toml api/poetry.lock
|
||||
git commit -m "chore(api): update prowler dependency to $BRANCH_NAME_TRIMMED for release $PROWLER_VERSION"
|
||||
git push origin "$BRANCH_NAME"
|
||||
git push origin "$TEMP_BRANCH"
|
||||
|
||||
echo "✓ api/pyproject.toml prowler dependency updated to: $UPDATED_PROWLER_REF"
|
||||
echo "✓ Prepared prowler dependency update to: $UPDATED_PROWLER_REF"
|
||||
|
||||
- name: Extract changelog entries
|
||||
run: |
|
||||
set -e
|
||||
- name: Create Pull Request against release branch
|
||||
if: ${{ env.PATCH_VERSION == '0' }}
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
branch: ${{ env.TEMP_BRANCH }}
|
||||
base: ${{ env.BRANCH_NAME }}
|
||||
title: "chore(api): Update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}"
|
||||
body: |
|
||||
### Description
|
||||
|
||||
# Function to extract changelog for a specific version
|
||||
extract_changelog() {
|
||||
local file="$1"
|
||||
local version="$2"
|
||||
local output_file="$3"
|
||||
Updates the API prowler dependency for release ${{ env.PROWLER_VERSION }}.
|
||||
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "Warning: $file not found, skipping..."
|
||||
touch "$output_file"
|
||||
return
|
||||
fi
|
||||
**Changes:**
|
||||
- Updates `api/pyproject.toml` prowler dependency from `@master` to `@${{ env.BRANCH_NAME }}`
|
||||
- Updates `api/poetry.lock` file with resolved dependencies
|
||||
|
||||
# Extract changelog section for this version
|
||||
awk -v version="$version" '
|
||||
/^## \[v?'"$version"'\]/ { found=1; next }
|
||||
found && /^## \[v?[0-9]+\.[0-9]+\.[0-9]+\]/ { found=0 }
|
||||
found && !/^## \[v?'"$version"'\]/ { print }
|
||||
' "$file" > "$output_file"
|
||||
This PR should be merged into the `${{ env.BRANCH_NAME }}` release branch.
|
||||
|
||||
# Remove --- separators
|
||||
sed -i '/^---$/d' "$output_file"
|
||||
### License
|
||||
|
||||
# Remove trailing empty lines
|
||||
sed -i '/^$/d' "$output_file"
|
||||
}
|
||||
|
||||
# Extract changelogs
|
||||
echo "Extracting changelog entries..."
|
||||
extract_changelog "prowler/CHANGELOG.md" "$PROWLER_VERSION" "prowler_changelog.md"
|
||||
extract_changelog "api/CHANGELOG.md" "$API_VERSION" "api_changelog.md"
|
||||
extract_changelog "ui/CHANGELOG.md" "$UI_VERSION" "ui_changelog.md"
|
||||
|
||||
# Combine changelogs in order: UI, API, SDK
|
||||
> combined_changelog.md
|
||||
|
||||
if [ -s "ui_changelog.md" ]; then
|
||||
echo "## UI" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat ui_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
if [ -s "api_changelog.md" ]; then
|
||||
echo "## API" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat api_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
if [ -s "prowler_changelog.md" ]; then
|
||||
echo "## SDK" >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
cat prowler_changelog.md >> combined_changelog.md
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
echo "Combined changelog preview:"
|
||||
cat combined_changelog.md
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
|
||||
labels: |
|
||||
component/api
|
||||
no-changelog
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
with:
|
||||
tag_name: ${{ env.PROWLER_VERSION }}
|
||||
name: Prowler ${{ env.PROWLER_VERSION }}
|
||||
body_path: combined_changelog.md
|
||||
draft: true
|
||||
target_commitish: ${{ env.PATCH_VERSION == '0' && 'master' || env.BRANCH_NAME }}
|
||||
target_commitish: ${{ env.BRANCH_NAME }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Check Changelog
|
||||
name: Prowler - Check Changelog
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
MONITORED_FOLDERS: "api ui prowler"
|
||||
MONITORED_FOLDERS: "api ui prowler dashboard"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -49,35 +49,26 @@ jobs:
|
||||
- name: Find existing changelog comment
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
id: find_comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e #v3.1.0
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad #v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '<!-- changelog-check -->'
|
||||
|
||||
- name: Comment on PR if changelog is missing
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository && steps.check_folders.outputs.missing_changelogs != ''
|
||||
- name: Update PR comment with changelog status
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find_comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!-- changelog-check -->
|
||||
⚠️ **Changes detected in the following folders without a corresponding update to the `CHANGELOG.md`:**
|
||||
${{ steps.check_folders.outputs.missing_changelogs != '' && format('⚠️ **Changes detected in the following folders without a corresponding update to the `CHANGELOG.md`:**
|
||||
|
||||
${{ steps.check_folders.outputs.missing_changelogs }}
|
||||
{0}
|
||||
|
||||
Please add an entry to the corresponding `CHANGELOG.md` file to maintain a clear history of changes.
|
||||
|
||||
- name: Comment on PR if all changelogs are present
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository && steps.check_folders.outputs.missing_changelogs == ''
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find_comment.outputs.comment-id }}
|
||||
body: |
|
||||
<!-- changelog-check -->
|
||||
✅ All necessary `CHANGELOG.md` files have been updated. Great job! 🎉
|
||||
Please add an entry to the corresponding `CHANGELOG.md` file to maintain a clear history of changes.', steps.check_folders.outputs.missing_changelogs) || '✅ All necessary `CHANGELOG.md` files have been updated. Great job! 🎉' }}
|
||||
|
||||
- name: Fail if changelog is missing
|
||||
if: steps.check_folders.outputs.missing_changelogs != ''
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
if: github.event.pull_request.merged == true && github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Trigger pull request
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
|
||||
@@ -59,10 +59,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -108,13 +108,13 @@ jobs:
|
||||
esac
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
@@ -157,6 +157,22 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# - name: Push README to Docker Hub (toniblyx)
|
||||
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
# repository: ${{ env.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}
|
||||
# readme-filepath: ./README.md
|
||||
#
|
||||
# - name: Push README to Docker Hub (prowlercloud)
|
||||
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
# repository: ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}
|
||||
# readme-filepath: ./README.md
|
||||
|
||||
dispatch-action:
|
||||
needs: container-build-push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Get Prowler version
|
||||
shell: bash
|
||||
|
||||
@@ -52,16 +52,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Dockerfile - Check if Dockerfile has changed
|
||||
id: dockerfile-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
Dockerfile
|
||||
@@ -117,12 +117,12 @@ jobs:
|
||||
# Test AWS
|
||||
- name: AWS - Check if any file has changed
|
||||
id: aws-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/aws/**
|
||||
./tests/providers/aws/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: AWS - Test
|
||||
if: steps.aws-changed-files.outputs.any_changed == 'true'
|
||||
@@ -132,12 +132,12 @@ jobs:
|
||||
# Test Azure
|
||||
- name: Azure - Check if any file has changed
|
||||
id: azure-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/azure/**
|
||||
./tests/providers/azure/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: Azure - Test
|
||||
if: steps.azure-changed-files.outputs.any_changed == 'true'
|
||||
@@ -147,12 +147,12 @@ jobs:
|
||||
# Test GCP
|
||||
- name: GCP - Check if any file has changed
|
||||
id: gcp-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/gcp/**
|
||||
./tests/providers/gcp/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: GCP - Test
|
||||
if: steps.gcp-changed-files.outputs.any_changed == 'true'
|
||||
@@ -162,12 +162,12 @@ jobs:
|
||||
# Test Kubernetes
|
||||
- name: Kubernetes - Check if any file has changed
|
||||
id: kubernetes-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/kubernetes/**
|
||||
./tests/providers/kubernetes/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: Kubernetes - Test
|
||||
if: steps.kubernetes-changed-files.outputs.any_changed == 'true'
|
||||
@@ -177,12 +177,12 @@ jobs:
|
||||
# Test GitHub
|
||||
- name: GitHub - Check if any file has changed
|
||||
id: github-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/github/**
|
||||
./tests/providers/github/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: GitHub - Test
|
||||
if: steps.github-changed-files.outputs.any_changed == 'true'
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
# Test NHN
|
||||
- name: NHN - Check if any file has changed
|
||||
id: nhn-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/nhn/**
|
||||
./tests/providers/nhn/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: NHN - Test
|
||||
if: steps.nhn-changed-files.outputs.any_changed == 'true'
|
||||
@@ -207,12 +207,12 @@ jobs:
|
||||
# Test M365
|
||||
- name: M365 - Check if any file has changed
|
||||
id: m365-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/m365/**
|
||||
./tests/providers/m365/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: M365 - Test
|
||||
if: steps.m365-changed-files.outputs.any_changed == 'true'
|
||||
@@ -222,18 +222,33 @@ jobs:
|
||||
# Test IaC
|
||||
- name: IaC - Check if any file has changed
|
||||
id: iac-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/iac/**
|
||||
./tests/providers/iac/**
|
||||
.poetry.lock
|
||||
./poetry.lock
|
||||
|
||||
- name: IaC - Test
|
||||
if: steps.iac-changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac
|
||||
|
||||
# Test MongoDB Atlas
|
||||
- name: MongoDB Atlas - Check if any file has changed
|
||||
id: mongodb-atlas-changed-files
|
||||
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/mongodbatlas/**
|
||||
./tests/providers/mongodbatlas/**
|
||||
.poetry.lock
|
||||
|
||||
- name: MongoDB Atlas - Test
|
||||
if: steps.mongodb-atlas-changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodb_atlas_coverage.xml tests/providers/mongodbatlas
|
||||
|
||||
# Common Tests
|
||||
- name: Lib - Test
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
@@ -248,7 +263,7 @@ jobs:
|
||||
# Codecov
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -64,14 +64,14 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pipx install poetry==2.1.1
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
# cache: ${{ env.CACHE }}
|
||||
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ env.GITHUB_BRANCH }}
|
||||
|
||||
- name: setup python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: 3.9 #install the python needed
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
pip install boto3
|
||||
|
||||
- name: Configure AWS Credentials -- DEV
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_DEV }}
|
||||
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services"
|
||||
branch: "aws-services-regions-updated-${{ github.sha }}"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws, no-changelog"
|
||||
title: "chore(regions_update): Changes in regions for AWS services"
|
||||
body: |
|
||||
### Description
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: vars
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Trigger deployment
|
||||
if: github.event_name == 'push'
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
|
||||
@@ -44,16 +44,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/ui-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -20,7 +20,9 @@ jobs:
|
||||
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Fix API data directory permissions
|
||||
run: docker run --rm -v $(pwd)/_data/api:/data alpine chown -R 1000:1000 /data
|
||||
- name: Start API services
|
||||
run: |
|
||||
# Override docker-compose image tag to use latest instead of stable
|
||||
@@ -57,7 +59,7 @@ jobs:
|
||||
echo "All database fixtures loaded successfully!"
|
||||
'
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
working-directory: ./ui
|
||||
run: npm run build
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
working-directory: ./ui
|
||||
run: npm run test:e2e
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
test-container-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Build Container
|
||||
|
||||
@@ -63,6 +63,7 @@ junit-reports/
|
||||
# .env
|
||||
ui/.env*
|
||||
api/.env*
|
||||
mcp_server/.env*
|
||||
.env.local
|
||||
|
||||
# Coverage
|
||||
@@ -75,3 +76,13 @@ node_modules
|
||||
|
||||
# Persistent data
|
||||
_data/
|
||||
|
||||
# Claude
|
||||
CLAUDE.md
|
||||
|
||||
# LLM's (Until we have a standard one)
|
||||
AGENTS.md
|
||||
|
||||
# MCP Server
|
||||
mcp_server/prowler_mcp_server/prowler_app/server.py
|
||||
mcp_server/prowler_mcp_server/prowler_app/utils/schema.yaml
|
||||
|
||||
@@ -6,6 +6,7 @@ repos:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
args: ["--unsafe"]
|
||||
exclude: prowler/config/llm_config.yaml
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
@@ -45,3 +45,7 @@ pypi-upload: ## Upload package
|
||||
help: ## Show this help.
|
||||
@echo "Prowler Makefile"
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Development Environment
|
||||
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
|
||||
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat --build
|
||||
|
||||
@@ -19,19 +19,16 @@
|
||||
<a href="https://goto.prowler.com/slack"><img alt="Slack Shield" src="https://img.shields.io/badge/slack-prowler-brightgreen.svg?logo=slack"></a>
|
||||
<a href="https://pypi.org/project/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/v/prowler.svg"></a>
|
||||
<a href="https://pypi.python.org/pypi/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/pyversions/prowler.svg"></a>
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Prowler Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=prowler%20downloads"></a>
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=downloads"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/toniblyx/prowler"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/cloud/build/toniblyx/prowler"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/image-size/toniblyx/prowler"></a>
|
||||
<a href="https://gallery.ecr.aws/prowler-cloud/prowler"><img width="120" height=19" alt="AWS ECR Gallery" src="https://user-images.githubusercontent.com/3985464/151531396-b6535a68-c907-44eb-95a1-a09508178616.png"></a>
|
||||
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/release-date/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="Contributors" src="https://img.shields.io/github/contributors-anon/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="License" src="https://img.shields.io/github/license/prowler-cloud/prowler"></a>
|
||||
<a href="https://twitter.com/ToniBlyx"><img alt="Twitter" src="https://img.shields.io/twitter/follow/toniblyx?style=social"></a>
|
||||
<a href="https://twitter.com/prowlercloud"><img alt="Twitter" src="https://img.shields.io/twitter/follow/prowlercloud?style=social"></a>
|
||||
@@ -55,15 +52,11 @@ Prowler includes hundreds of built-in controls to ensure compliance with standar
|
||||
- **National Security Standards:** ENS (Spanish National Security Scheme)
|
||||
- **Custom Security Frameworks:** Tailored to your needs
|
||||
|
||||
## Prowler CLI and Prowler Cloud
|
||||
|
||||
Prowler offers a Command Line Interface (CLI), known as Prowler Open Source, and an additional service built on top of it, called <a href="https://prowler.com">Prowler Cloud</a>.
|
||||
|
||||
## Prowler App
|
||||
|
||||
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
|
||||
|
||||

|
||||

|
||||
|
||||
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
|
||||
|
||||
@@ -80,28 +73,37 @@ prowler <provider>
|
||||
```console
|
||||
prowler dashboard
|
||||
```
|
||||

|
||||

|
||||
|
||||
# Prowler at a Glance
|
||||
> [!Tip]
|
||||
> For the most accurate and up-to-date information about checks, services, frameworks, and categories, visit [**Prowler Hub**](https://hub.prowler.com).
|
||||
|
||||
| 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 | 567 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 10 | 3 |
|
||||
| Azure | 142 | 18 | 11 | 3 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 16 | 2 | 1 | 0 |
|
||||
| M365 | 69 | 7 | 3 | 2 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
|
||||
| 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) | Support | Stage | Interface |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| AWS | 576 | 82 | 36 | 10 | Official | Stable | UI, API, CLI |
|
||||
| GCP | 79 | 13 | 10 | 3 | Official | Stable | UI, API, CLI |
|
||||
| Azure | 162 | 19 | 11 | 4 | Official | Stable | UI, API, CLI |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 | Official | Stable | UI, API, CLI |
|
||||
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
|
||||
| M365 | 70 | 7 | 3 | 2 | Official | Stable | UI, API, CLI |
|
||||
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | Beta | CLI |
|
||||
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | Beta | CLI |
|
||||
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | Beta | CLI |
|
||||
| NHN | 6 | 2 | 1 | 0 | Unofficial | Beta | CLI |
|
||||
|
||||
> [!Note]
|
||||
> The numbers in the table are updated periodically.
|
||||
|
||||
> [!Tip]
|
||||
> For the most accurate and up-to-date information about checks, services, frameworks, and categories, visit [**Prowler Hub**](https://hub.prowler.com).
|
||||
|
||||
|
||||
> [!Note]
|
||||
> Use the following commands to list Prowler's available checks, services, compliance frameworks, and categories: `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
|
||||
> Use the following commands to list Prowler's available checks, services, compliance frameworks, and categories:
|
||||
> - `prowler <provider> --list-checks`
|
||||
> - `prowler <provider> --list-services`
|
||||
> - `prowler <provider> --list-compliance`
|
||||
> - `prowler <provider> --list-categories`
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
@@ -239,7 +241,7 @@ The following versions of Prowler CLI are available, depending on your requireme
|
||||
|
||||
The container images are available here:
|
||||
- Prowler CLI:
|
||||
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
|
||||
- [DockerHub](https://hub.docker.com/r/prowlercloud/prowler/tags)
|
||||
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
|
||||
- Prowler App:
|
||||
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
|
||||
@@ -274,7 +276,7 @@ python prowler-cli.py -v
|
||||
- **Prowler API**: A backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
|
||||
- **Prowler SDK**: A Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
|
||||
|
||||

|
||||

|
||||
|
||||
## Prowler CLI
|
||||
|
||||
@@ -300,40 +302,12 @@ And many more environments.
|
||||
|
||||

|
||||
|
||||
# Deprecations from v3
|
||||
|
||||
## General
|
||||
- `Allowlist` now is called `Mutelist`.
|
||||
- The `--quiet` option has been deprecated. Use the `--status` flag to filter findings based on their status: PASS, FAIL, or MANUAL.
|
||||
- All findings with an `INFO` status have been reclassified as `MANUAL`.
|
||||
- The CSV output format is standardized across all providers.
|
||||
|
||||
**Deprecated Output Formats**
|
||||
|
||||
The following formats are now deprecated:
|
||||
- Native JSON has been replaced with JSON in [OCSF] v1.1.0 format, which is standardized across all providers (https://schema.ocsf.io/).
|
||||
|
||||
## AWS
|
||||
|
||||
**AWS Flag Deprecation**
|
||||
|
||||
The flag --sts-endpoint-region has been deprecated due to the adoption of AWS STS regional tokens.
|
||||
|
||||
**Sending FAIL Results to AWS Security Hub**
|
||||
|
||||
- To send only FAILS to AWS Security Hub, use one of the following options: `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
|
||||
# 📖 Documentation
|
||||
|
||||
**Documentation Resources**
|
||||
|
||||
For installation instructions, usage details, tutorials, and the Developer Guide, visit https://docs.prowler.com/
|
||||
|
||||
# 📃 License
|
||||
|
||||
**Prowler License Information**
|
||||
|
||||
Prowler is licensed under the Apache License 2.0, as indicated in each file within the repository. Obtaining a Copy of the License
|
||||
Prowler is licensed under the Apache License 2.0.
|
||||
|
||||
A copy of the License is available at <http://www.apache.org/licenses/LICENSE-2.0>
|
||||
|
||||
@@ -1,23 +1,65 @@
|
||||
# Security Policy
|
||||
# Security
|
||||
|
||||
## Software Security
|
||||
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
- `bandit` for code security review.
|
||||
- `safety` and `dependabot` for dependencies.
|
||||
- `hadolint` and `dockle` for our containers security.
|
||||
- `snyk` in Docker Hub.
|
||||
- `clair` in Amazon ECR.
|
||||
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
|
||||
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to https://support.prowler.com.
|
||||
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
|
||||
|
||||
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
- Social engineering support or attacks requiring social engineering.
|
||||
- Clickjacking on pages with no sensitive actions.
|
||||
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
|
||||
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
|
||||
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
|
||||
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
|
||||
- Missing best practices in SSL/TLS configuration.
|
||||
- Any activity that could lead to the disruption of service (DoS).
|
||||
- Rate limiting or brute force issues on non-authentication endpoints.
|
||||
- Missing best practices in Content Security Policy (CSP).
|
||||
- Missing HttpOnly or Secure flags on cookies.
|
||||
- Configuration of or missing security headers.
|
||||
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
|
||||
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
|
||||
- Software version disclosure, banner identification issues, or descriptive error messages.
|
||||
- Tabnabbing.
|
||||
- Issues that require unlikely user interaction.
|
||||
- Improper logout functionality and improper session timeout.
|
||||
- CORS misconfiguration without an exploitation scenario.
|
||||
- Broken link hijacking.
|
||||
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
|
||||
- Content spoofing and text injection issues without a clear attack vector.
|
||||
- Email spoofing without exploiting security flaws.
|
||||
- Dead links or broken links.
|
||||
- User enumeration.
|
||||
|
||||
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
|
||||
Testing guidelines:
|
||||
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
|
||||
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
|
||||
|
||||
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
|
||||
Reporting guidelines:
|
||||
- File a report through our Support Desk at https://support.prowler.com
|
||||
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
|
||||
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
|
||||
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
|
||||
|
||||
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
|
||||
Disclosure guidelines:
|
||||
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
|
||||
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
|
||||
- Data regarding any Prowler user or customer projects.
|
||||
- Prowler customers' data.
|
||||
- Information about Prowler employees, contractors or partners.
|
||||
|
||||
What we promise:
|
||||
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
|
||||
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
|
||||
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
|
||||
- We will keep you informed of the progress towards resolving the problem.
|
||||
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
|
||||
|
||||
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
|
||||
|
||||
---
|
||||
|
||||
For more information about our security policies, please refer to our [Security](https://docs.prowler.com/projects/prowler-open-source/en/latest/security/) section in our documentation.
|
||||
@@ -19,6 +19,8 @@ DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Throttle, two options: Empty means no throttle; or if desired use one in DRF format: https://www.django-rest-framework.org/api-guide/throttling/#setting-the-throttling-policy
|
||||
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
|
||||
@@ -2,12 +2,65 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.11.0] (Prowler UNRELEASED)
|
||||
## [1.14.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Default JWT keys are generated and stored if they are missing from configuration [(#8655)](https://github.com/prowler-cloud/prowler/pull/8655)
|
||||
- `compliance_name` for each compliance [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920)
|
||||
|
||||
### Changed
|
||||
- Now the MANAGE_ACCOUNT permission is required to modify or read user permissions instead of MANAGE_USERS [(#8281)](https://github.com/prowler-cloud/prowler/pull/8281)
|
||||
- Now at least one user with MANAGE_ACCOUNT permission is required in the tenant [(#8729)](https://github.com/prowler-cloud/prowler/pull/8729)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.2] (Prowler 5.12.3)
|
||||
|
||||
### Fixed
|
||||
- 500 error when deleting user [(#8731)](https://github.com/prowler-cloud/prowler/pull/8731)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.1] (Prowler 5.12.2)
|
||||
|
||||
### Changed
|
||||
- Renamed compliance overview task queue to `compliance` [(#8755)](https://github.com/prowler-cloud/prowler/pull/8755)
|
||||
|
||||
### Security
|
||||
- Django updated to the latest 5.1 security release, 5.1.12, due to [problems](https://www.djangoproject.com/weblog/2025/sep/03/security-releases/) with potential SQL injection in FilteredRelation column aliases [(#8693)](https://github.com/prowler-cloud/prowler/pull/8693)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.0] (Prowler 5.12.0)
|
||||
|
||||
### Added
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
|
||||
- `GET /overviews/findings_severity` now supports `filter[status]` and `filter[status__in]` to aggregate by specific statuses (`FAIL`, `PASS`)[(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
|
||||
- Throttling options for `/api/v1/tokens` using the `DJANGO_THROTTLE_TOKEN_OBTAIN` environment variable [(#8647)](https://github.com/prowler-cloud/prowler/pull/8647)
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] (Prowler 5.11.0)
|
||||
|
||||
### Added
|
||||
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
|
||||
- Integration with Amazon Security Hub, enabling sending findings to Security Hub [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
|
||||
- Generate ASFF output for AWS providers with SecurityHub integration enabled [(#8569)](https://github.com/prowler-cloud/prowler/pull/8569)
|
||||
|
||||
### Fixed
|
||||
- GitHub provider always scans user instead of organization when using provider UID [(#8587)](https://github.com/prowler-cloud/prowler/pull/8587)
|
||||
|
||||
---
|
||||
|
||||
## [1.11.0] (Prowler 5.10.0)
|
||||
|
||||
### Added
|
||||
- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
|
||||
- Integration with Amazon S3, enabling storage and retrieval of scan data via S3 buckets [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056)
|
||||
|
||||
### Fixed
|
||||
- Avoid sending errors to Sentry in M365 provider when user authentication fails [(#8420)](https://github.com/prowler-cloud/prowler/pull/8420)
|
||||
|
||||
---
|
||||
|
||||
## [1.10.2] (Prowler v5.9.2)
|
||||
|
||||
@@ -20,6 +20,8 @@ Valkey exposes a Redis 7.2 compliant API. Any service that exposes the Redis API
|
||||
|
||||
Under the root path of the project, you can find a file called `.env.example`. This file shows all the environment variables that the project uses. You *must* create a new file called `.env` and set the values for the variables.
|
||||
|
||||
If you don’t set `DJANGO_TOKEN_SIGNING_KEY` or `DJANGO_TOKEN_VERIFYING_KEY`, the API will generate them at `~/.config/prowler-api/` with `0600` and `0644` permissions; back up these files to persist identity across redeploys.
|
||||
|
||||
## Local deployment
|
||||
Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the Poetry interpreter, not before. Otherwise, variables will not be loaded properly.
|
||||
|
||||
|
||||
@@ -32,7 +32,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,scan-reports,deletion,backfill,overview,integrations -E --max-tasks-per-child 1
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill,overview,integrations,compliance -E --max-tasks-per-child 1
|
||||
}
|
||||
|
||||
start_worker_beat() {
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
|
||||
dependencies = [
|
||||
"celery[pytest] (>=5.4.0,<6.0.0)",
|
||||
"dj-rest-auth[with_social,jwt] (==7.0.1)",
|
||||
"django==5.1.10",
|
||||
"django (==5.1.12)",
|
||||
"django-allauth[saml] (>=65.8.0,<66.0.0)",
|
||||
"django-celery-beat (>=2.7.0,<3.0.0)",
|
||||
"django-celery-results (>=2.5.1,<3.0.0)",
|
||||
@@ -30,7 +30,9 @@ dependencies = [
|
||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||
"uuid6==2024.7.10",
|
||||
"openai (>=1.82.0,<2.0.0)",
|
||||
"xmlsec==1.3.14"
|
||||
"xmlsec==1.3.14",
|
||||
"h2 (==4.3.0)",
|
||||
"markdown (>=3.9,<4.0)"
|
||||
]
|
||||
description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
@@ -38,7 +40,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.10.2"
|
||||
version = "1.14.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
from config.custom_logging import BackendLogger
|
||||
from config.env import env
|
||||
|
||||
logger = logging.getLogger(BackendLogger.API)
|
||||
|
||||
SIGNING_KEY_ENV = "DJANGO_TOKEN_SIGNING_KEY"
|
||||
VERIFYING_KEY_ENV = "DJANGO_TOKEN_VERIFYING_KEY"
|
||||
|
||||
PRIVATE_KEY_FILE = "jwt_private.pem"
|
||||
PUBLIC_KEY_FILE = "jwt_public.pem"
|
||||
|
||||
KEYS_DIRECTORY = (
|
||||
Path.home() / ".config" / "prowler-api"
|
||||
) # `/home/prowler/.config/prowler-api` inside the container
|
||||
|
||||
_keys_initialized = False # Flag to prevent multiple executions within the same process
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
@@ -9,4 +33,138 @@ class ApiConfig(AppConfig):
|
||||
from api import signals # noqa: F401
|
||||
from api.compliance import load_prowler_compliance
|
||||
|
||||
# Generate required cryptographic keys if not present, but only if:
|
||||
# `"manage.py" not in sys.argv`: If an external server (e.g., Gunicorn) is running the app
|
||||
# `os.environ.get("RUN_MAIN")`: If it's not a Django command or using `runserver`,
|
||||
# only the main process will do it
|
||||
if "manage.py" not in sys.argv or os.environ.get("RUN_MAIN"):
|
||||
self._ensure_crypto_keys()
|
||||
|
||||
load_prowler_compliance()
|
||||
|
||||
def _ensure_crypto_keys(self):
|
||||
"""
|
||||
Orchestrator method that ensures all required cryptographic keys are present.
|
||||
This method coordinates the generation of:
|
||||
- RSA key pairs for JWT token signing and verification
|
||||
Note: During development, Django spawns multiple processes (migrations, fixtures, etc.)
|
||||
which will each generate their own keys. This is expected behavior and each process
|
||||
will have consistent keys for its lifetime. In production, set the keys as environment
|
||||
variables to avoid regeneration.
|
||||
"""
|
||||
global _keys_initialized
|
||||
|
||||
# Skip key generation if running tests
|
||||
if hasattr(settings, "TESTING") and settings.TESTING:
|
||||
return
|
||||
|
||||
# Skip if already initialized in this process
|
||||
if _keys_initialized:
|
||||
return
|
||||
|
||||
# Check if both JWT keys are set; if not, generate them
|
||||
signing_key = env.str(SIGNING_KEY_ENV, default="").strip()
|
||||
verifying_key = env.str(VERIFYING_KEY_ENV, default="").strip()
|
||||
|
||||
if not signing_key or not verifying_key:
|
||||
logger.info(
|
||||
f"Generating JWT RSA key pair. In production, set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' "
|
||||
"environment variables."
|
||||
)
|
||||
self._ensure_jwt_keys()
|
||||
|
||||
# Mark as initialized to prevent future executions in this process
|
||||
_keys_initialized = True
|
||||
|
||||
def _read_key_file(self, file_name):
|
||||
"""
|
||||
Utility method to read the contents of a file.
|
||||
"""
|
||||
file_path = KEYS_DIRECTORY / file_name
|
||||
return file_path.read_text().strip() if file_path.is_file() else None
|
||||
|
||||
def _write_key_file(self, file_name, content, private=True):
|
||||
"""
|
||||
Utility method to write content to a file.
|
||||
"""
|
||||
try:
|
||||
file_path = KEYS_DIRECTORY / file_name
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
file_path.chmod(0o600 if private else 0o644)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error writing key file '{file_name}': {e}. "
|
||||
f"Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
|
||||
)
|
||||
raise e
|
||||
|
||||
def _ensure_jwt_keys(self):
|
||||
"""
|
||||
Generate RSA key pairs for JWT token signing and verification
|
||||
if they are not already set in environment variables.
|
||||
"""
|
||||
# Read existing keys from files if they exist
|
||||
signing_key = self._read_key_file(PRIVATE_KEY_FILE)
|
||||
verifying_key = self._read_key_file(PUBLIC_KEY_FILE)
|
||||
|
||||
if not signing_key or not verifying_key:
|
||||
# Generate and store the RSA key pair
|
||||
signing_key, verifying_key = self._generate_jwt_keys()
|
||||
self._write_key_file(PRIVATE_KEY_FILE, signing_key, private=True)
|
||||
self._write_key_file(PUBLIC_KEY_FILE, verifying_key, private=False)
|
||||
logger.info("JWT keys generated and stored successfully")
|
||||
|
||||
else:
|
||||
logger.info("JWT keys already generated")
|
||||
|
||||
# Set environment variables and Django settings
|
||||
os.environ[SIGNING_KEY_ENV] = signing_key
|
||||
settings.SIMPLE_JWT["SIGNING_KEY"] = signing_key
|
||||
|
||||
os.environ[VERIFYING_KEY_ENV] = verifying_key
|
||||
settings.SIMPLE_JWT["VERIFYING_KEY"] = verifying_key
|
||||
|
||||
def _generate_jwt_keys(self):
|
||||
"""
|
||||
Generate and set RSA key pairs for JWT token operations.
|
||||
"""
|
||||
try:
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
# Generate RSA key pair
|
||||
private_key = rsa.generate_private_key( # Future improvement: we could read the next values from env vars
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
)
|
||||
|
||||
# Serialize private key (for signing)
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
).decode("utf-8")
|
||||
|
||||
# Serialize public key (for verification)
|
||||
public_key = private_key.public_key()
|
||||
public_pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode("utf-8")
|
||||
|
||||
logger.debug("JWT RSA key pair generated successfully.")
|
||||
return private_pem, public_pem
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(
|
||||
"The 'cryptography' package is required for automatic JWT key generation."
|
||||
)
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error generating JWT keys: {e}. Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
|
||||
)
|
||||
raise e
|
||||
|
||||
@@ -225,6 +225,7 @@ def generate_compliance_overview_template(prowler_compliance: dict):
|
||||
# Build compliance dictionary
|
||||
compliance_dict = {
|
||||
"framework": compliance_data.Framework,
|
||||
"name": compliance_data.Name,
|
||||
"version": compliance_data.Version,
|
||||
"provider": provider_type,
|
||||
"description": compliance_data.Description,
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, Q
|
||||
from django_filters.rest_framework import (
|
||||
BaseInFilter,
|
||||
BooleanFilter,
|
||||
@@ -28,6 +28,7 @@ from api.models import (
|
||||
Integration,
|
||||
Invitation,
|
||||
Membership,
|
||||
OverviewStatusChoices,
|
||||
PermissionChoices,
|
||||
Processor,
|
||||
Provider,
|
||||
@@ -750,6 +751,72 @@ class ScanSummaryFilter(FilterSet):
|
||||
}
|
||||
|
||||
|
||||
class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
"""Filter for findings_severity ScanSummary endpoint - includes status filters"""
|
||||
|
||||
# Custom status filters - only for severity grouping endpoint
|
||||
status = ChoiceFilter(method="filter_status", choices=OverviewStatusChoices.choices)
|
||||
status__in = CharInFilter(method="filter_status_in", lookup_expr="in")
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
# Validate the status value
|
||||
if value not in [choice[0] for choice in OverviewStatusChoices.choices]:
|
||||
raise ValidationError(f"Invalid status value: {value}")
|
||||
|
||||
# Apply the filter by annotating the queryset with the status field
|
||||
if value == OverviewStatusChoices.FAIL:
|
||||
return queryset.annotate(status_count=F("fail"))
|
||||
elif value == OverviewStatusChoices.PASS:
|
||||
return queryset.annotate(status_count=F("_pass"))
|
||||
else:
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
def filter_status_in(self, queryset, name, value):
|
||||
# Validate the status values
|
||||
valid_statuses = [choice[0] for choice in OverviewStatusChoices.choices]
|
||||
for status_val in value:
|
||||
if status_val not in valid_statuses:
|
||||
raise ValidationError(f"Invalid status value: {status_val}")
|
||||
|
||||
# If all statuses or no valid statuses, use total
|
||||
if (
|
||||
set(value)
|
||||
>= {
|
||||
OverviewStatusChoices.FAIL,
|
||||
OverviewStatusChoices.PASS,
|
||||
}
|
||||
or not value
|
||||
):
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
# Build the sum expression based on status values
|
||||
sum_expression = None
|
||||
for status in value:
|
||||
if status == OverviewStatusChoices.FAIL:
|
||||
field_expr = F("fail")
|
||||
elif status == OverviewStatusChoices.PASS:
|
||||
field_expr = F("_pass")
|
||||
else:
|
||||
continue
|
||||
|
||||
if sum_expression is None:
|
||||
sum_expression = field_expr
|
||||
else:
|
||||
sum_expression = sum_expression + field_expr
|
||||
|
||||
if sum_expression is None:
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
return queryset.annotate(status_count=sum_expression)
|
||||
|
||||
class Meta:
|
||||
model = ScanSummary
|
||||
fields = {
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
|
||||
class ServiceOverviewFilter(ScanSummaryFilter):
|
||||
def is_valid(self):
|
||||
# Check if at least one of the inserted_at filters is present
|
||||
@@ -793,3 +860,23 @@ class ProcessorFilter(FilterSet):
|
||||
field_name="processor_type",
|
||||
lookup_expr="in",
|
||||
)
|
||||
|
||||
|
||||
class IntegrationJiraFindingsFilter(FilterSet):
|
||||
# To be expanded as needed
|
||||
finding_id = UUIDFilter(field_name="id", lookup_expr="exact")
|
||||
finding_id__in = UUIDInFilter(field_name="id", lookup_expr="in")
|
||||
|
||||
class Meta:
|
||||
model = Finding
|
||||
fields = {}
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
# Validate that there is at least one filter provided
|
||||
if not self.data:
|
||||
raise ValidationError(
|
||||
{
|
||||
"findings": "No finding filters provided. At least one filter is required."
|
||||
}
|
||||
)
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.1.10 on 2025-08-20 09:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0045_alter_scan_output_location"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="lighthouseconfiguration",
|
||||
name="model",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("gpt-4o-2024-11-20", "GPT-4o v2024-11-20"),
|
||||
("gpt-4o-2024-08-06", "GPT-4o v2024-08-06"),
|
||||
("gpt-4o-2024-05-13", "GPT-4o v2024-05-13"),
|
||||
("gpt-4o", "GPT-4o Default"),
|
||||
("gpt-4o-mini-2024-07-18", "GPT-4o Mini v2024-07-18"),
|
||||
("gpt-4o-mini", "GPT-4o Mini Default"),
|
||||
("gpt-5-2025-08-07", "GPT-5 v2025-08-07"),
|
||||
("gpt-5", "GPT-5 Default"),
|
||||
("gpt-5-mini-2025-08-07", "GPT-5 Mini v2025-08-07"),
|
||||
("gpt-5-mini", "GPT-5 Mini Default"),
|
||||
],
|
||||
default="gpt-4o-2024-08-06",
|
||||
help_text="Must be one of the supported model names",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.1.10 on 2025-08-20 08:24
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0046_lighthouse_gpt5"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="integration",
|
||||
name="unique_configuration_per_tenant",
|
||||
),
|
||||
]
|
||||
@@ -74,6 +74,15 @@ class StatusChoices(models.TextChoices):
|
||||
MANUAL = "MANUAL", _("Manual")
|
||||
|
||||
|
||||
class OverviewStatusChoices(models.TextChoices):
|
||||
"""
|
||||
Status filters allowed in overview/severity endpoints.
|
||||
"""
|
||||
|
||||
FAIL = "FAIL", _("Fail")
|
||||
PASS = "PASS", _("Pass")
|
||||
|
||||
|
||||
class StateChoices(models.TextChoices):
|
||||
AVAILABLE = "available", _("Available")
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
@@ -1372,10 +1381,6 @@ class Integration(RowLevelSecurityProtectedModel):
|
||||
db_table = "integrations"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("configuration", "tenant"),
|
||||
name="unique_configuration_per_tenant",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
@@ -1752,6 +1757,10 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel):
|
||||
GPT_4O = "gpt-4o", _("GPT-4o Default")
|
||||
GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18", _("GPT-4o Mini v2024-07-18")
|
||||
GPT_4O_MINI = "gpt-4o-mini", _("GPT-4o Mini Default")
|
||||
GPT_5_2025_08_07 = "gpt-5-2025-08-07", _("GPT-5 v2025-08-07")
|
||||
GPT_5 = "gpt-5", _("GPT-5 Default")
|
||||
GPT_5_MINI_2025_08_07 = "gpt-5-mini-2025-08-07", _("GPT-5 Mini v2025-08-07")
|
||||
GPT_5_MINI = "gpt-5-mini", _("GPT-5 Mini Default")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
def _pick_task_response_component(components):
|
||||
schemas = components.get("schemas", {}) or {}
|
||||
for candidate in ("TaskResponse",):
|
||||
if candidate in schemas:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _extract_task_example_from_components(components):
|
||||
schemas = components.get("schemas", {}) or {}
|
||||
candidate = "TaskResponse"
|
||||
doc = schemas.get(candidate)
|
||||
if isinstance(doc, dict) and "example" in doc:
|
||||
return doc["example"]
|
||||
|
||||
res = schemas.get(candidate)
|
||||
if isinstance(res, dict) and "example" in res:
|
||||
example = res["example"]
|
||||
return example if "data" in example else {"data": example}
|
||||
|
||||
# Fallback
|
||||
return {
|
||||
"data": {
|
||||
"type": "tasks",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"attributes": {
|
||||
"inserted_at": "2019-08-24T14:15:22Z",
|
||||
"completed_at": "2019-08-24T14:15:22Z",
|
||||
"name": "string",
|
||||
"state": "available",
|
||||
"result": None,
|
||||
"task_args": None,
|
||||
"metadata": None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def attach_task_202_examples(result, generator, request, public): # noqa: F841
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
components = result.get("components", {}) or {}
|
||||
task_resp_component = _pick_task_response_component(components)
|
||||
task_example = _extract_task_example_from_components(components)
|
||||
|
||||
paths = result.get("paths", {}) or {}
|
||||
for path_item in paths.values():
|
||||
if not isinstance(path_item, dict):
|
||||
continue
|
||||
|
||||
for method_obj in path_item.values():
|
||||
if not isinstance(method_obj, dict):
|
||||
continue
|
||||
|
||||
responses = method_obj.get("responses", {}) or {}
|
||||
resp_202 = responses.get("202")
|
||||
if not isinstance(resp_202, dict):
|
||||
continue
|
||||
|
||||
content = resp_202.get("content", {}) or {}
|
||||
jsonapi = content.get("application/vnd.api+json")
|
||||
if not isinstance(jsonapi, dict):
|
||||
continue
|
||||
|
||||
# Inject example if missing
|
||||
if "examples" not in jsonapi and "example" not in jsonapi:
|
||||
jsonapi["examples"] = {
|
||||
"Task queued": {
|
||||
"summary": "Task queued",
|
||||
"value": task_example,
|
||||
}
|
||||
}
|
||||
|
||||
# Rewrite schema $ref if needed
|
||||
if task_resp_component:
|
||||
schema = jsonapi.get("schema")
|
||||
must_replace = False
|
||||
if not isinstance(schema, dict):
|
||||
must_replace = True
|
||||
else:
|
||||
ref = schema.get("$ref")
|
||||
if not ref:
|
||||
must_replace = True
|
||||
else:
|
||||
current = ref.split("/")[-1]
|
||||
if current != task_resp_component:
|
||||
must_replace = True
|
||||
|
||||
if must_replace:
|
||||
jsonapi["schema"] = {
|
||||
"$ref": f"#/components/schemas/{task_resp_component}"
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
||||
import api.apps as api_apps_module
|
||||
from api.apps import (
|
||||
ApiConfig,
|
||||
PRIVATE_KEY_FILE,
|
||||
PUBLIC_KEY_FILE,
|
||||
SIGNING_KEY_ENV,
|
||||
VERIFYING_KEY_ENV,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_keys_initialized(monkeypatch):
|
||||
"""Ensure per-test clean state for the module-level guard flag."""
|
||||
monkeypatch.setattr(api_apps_module, "_keys_initialized", False, raising=False)
|
||||
|
||||
|
||||
def _stub_keys():
|
||||
return (
|
||||
"""-----BEGIN PRIVATE KEY-----\nPRIVATE\n-----END PRIVATE KEY-----\n""",
|
||||
"""-----BEGIN PUBLIC KEY-----\nPUBLIC\n-----END PUBLIC KEY-----\n""",
|
||||
)
|
||||
|
||||
|
||||
def test_generate_jwt_keys_when_missing(monkeypatch, tmp_path):
|
||||
# Arrange: isolate FS, env, and settings; force generation path
|
||||
monkeypatch.setattr(
|
||||
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
|
||||
)
|
||||
monkeypatch.delenv(SIGNING_KEY_ENV, raising=False)
|
||||
monkeypatch.delenv(VERIFYING_KEY_ENV, raising=False)
|
||||
|
||||
# Work on a copy of SIMPLE_JWT to avoid mutating the global settings dict for other tests
|
||||
monkeypatch.setattr(
|
||||
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
|
||||
)
|
||||
monkeypatch.setattr(settings, "TESTING", False, raising=False)
|
||||
|
||||
# Avoid dependency on the cryptography package
|
||||
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_stub_keys))
|
||||
|
||||
config = ApiConfig("api", api_apps_module)
|
||||
|
||||
# Act
|
||||
config._ensure_crypto_keys()
|
||||
|
||||
# Assert: files created with expected content
|
||||
priv_path = Path(tmp_path) / PRIVATE_KEY_FILE
|
||||
pub_path = Path(tmp_path) / PUBLIC_KEY_FILE
|
||||
assert priv_path.is_file()
|
||||
assert pub_path.is_file()
|
||||
assert priv_path.read_text() == _stub_keys()[0]
|
||||
assert pub_path.read_text() == _stub_keys()[1]
|
||||
|
||||
# Env vars and Django settings updated
|
||||
assert os.environ[SIGNING_KEY_ENV] == _stub_keys()[0]
|
||||
assert os.environ[VERIFYING_KEY_ENV] == _stub_keys()[1]
|
||||
assert settings.SIMPLE_JWT["SIGNING_KEY"] == _stub_keys()[0]
|
||||
assert settings.SIMPLE_JWT["VERIFYING_KEY"] == _stub_keys()[1]
|
||||
|
||||
|
||||
def test_ensure_crypto_keys_are_idempotent_within_process(monkeypatch, tmp_path):
|
||||
# Arrange
|
||||
monkeypatch.setattr(
|
||||
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
|
||||
)
|
||||
monkeypatch.delenv(SIGNING_KEY_ENV, raising=False)
|
||||
monkeypatch.delenv(VERIFYING_KEY_ENV, raising=False)
|
||||
monkeypatch.setattr(
|
||||
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
|
||||
)
|
||||
monkeypatch.setattr(settings, "TESTING", False, raising=False)
|
||||
|
||||
mock_generate = MagicMock(side_effect=_stub_keys)
|
||||
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(mock_generate))
|
||||
|
||||
config = ApiConfig("api", api_apps_module)
|
||||
|
||||
# Act: first call should generate, second should be a no-op (guard flag)
|
||||
config._ensure_crypto_keys()
|
||||
config._ensure_crypto_keys()
|
||||
|
||||
# Assert: generation occurred exactly once
|
||||
assert mock_generate.call_count == 1
|
||||
|
||||
|
||||
def test_ensure_jwt_keys_uses_existing_files(monkeypatch, tmp_path):
|
||||
# Arrange: pre-create key files
|
||||
monkeypatch.setattr(
|
||||
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
|
||||
)
|
||||
|
||||
existing_private, existing_public = _stub_keys()
|
||||
|
||||
(Path(tmp_path) / PRIVATE_KEY_FILE).write_text(existing_private)
|
||||
(Path(tmp_path) / PUBLIC_KEY_FILE).write_text(existing_public)
|
||||
|
||||
# If generation were called, fail the test
|
||||
def _fail_generate():
|
||||
raise AssertionError("_generate_jwt_keys should not be called when files exist")
|
||||
|
||||
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_fail_generate))
|
||||
|
||||
config = ApiConfig("api", api_apps_module)
|
||||
|
||||
# Act: call the lower-level method directly to set env/settings from files
|
||||
config._ensure_jwt_keys()
|
||||
|
||||
# Assert
|
||||
# _read_key_file() strips trailing newlines; environment/settings should reflect stripped content
|
||||
assert os.environ[SIGNING_KEY_ENV] == existing_private.strip()
|
||||
assert os.environ[VERIFYING_KEY_ENV] == existing_public.strip()
|
||||
assert settings.SIMPLE_JWT["SIGNING_KEY"] == existing_private.strip()
|
||||
assert settings.SIMPLE_JWT["VERIFYING_KEY"] == existing_public.strip()
|
||||
|
||||
|
||||
def test_ensure_crypto_keys_skips_when_env_vars(monkeypatch, tmp_path):
|
||||
# Arrange: put values in env so the orchestrator doesn't generate
|
||||
monkeypatch.setattr(
|
||||
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
|
||||
)
|
||||
monkeypatch.setenv(SIGNING_KEY_ENV, "ENV-PRIVATE")
|
||||
monkeypatch.setenv(VERIFYING_KEY_ENV, "ENV-PUBLIC")
|
||||
monkeypatch.setattr(
|
||||
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
|
||||
)
|
||||
monkeypatch.setattr(settings, "TESTING", False, raising=False)
|
||||
|
||||
called = {"ensure": False}
|
||||
|
||||
def _track_call():
|
||||
called["ensure"] = True
|
||||
return _stub_keys()
|
||||
|
||||
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_track_call))
|
||||
|
||||
config = ApiConfig("api", api_apps_module)
|
||||
|
||||
# Act
|
||||
config._ensure_crypto_keys()
|
||||
|
||||
# Assert: orchestrator did not trigger generation when env present
|
||||
assert called["ensure"] is False
|
||||
@@ -239,6 +239,7 @@ class TestCompliance:
|
||||
Framework="Framework 1",
|
||||
Version="1.0",
|
||||
Description="Description of compliance1",
|
||||
Name="Compliance 1",
|
||||
)
|
||||
prowler_compliance = {"aws": {"compliance1": compliance1}}
|
||||
|
||||
@@ -248,6 +249,7 @@ class TestCompliance:
|
||||
"aws": {
|
||||
"compliance1": {
|
||||
"framework": "Framework 1",
|
||||
"name": "Compliance 1",
|
||||
"version": "1.0",
|
||||
"provider": "aws",
|
||||
"description": "Description of compliance1",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from unittest.mock import ANY, Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -151,6 +152,221 @@ class TestUserViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["email"] == "rbac_limited@rbac.com"
|
||||
|
||||
def test_me_shows_own_roles_and_memberships_without_manage_account(
|
||||
self, authenticated_client_no_permissions_rbac
|
||||
):
|
||||
response = authenticated_client_no_permissions_rbac.get(reverse("user-me"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
rels = response.json()["data"]["relationships"]
|
||||
|
||||
# Self should see own roles and memberships even without manage_account
|
||||
assert isinstance(rels["roles"]["data"], list)
|
||||
assert rels["memberships"]["meta"]["count"] == 1
|
||||
|
||||
def test_me_shows_roles_and_memberships_with_manage_account(
|
||||
self, authenticated_client_rbac
|
||||
):
|
||||
response = authenticated_client_rbac.get(reverse("user-me"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
rels = response.json()["data"]["relationships"]
|
||||
|
||||
# Roles should have data when manage_account is True
|
||||
assert len(rels["roles"]["data"]) > 0
|
||||
|
||||
# Memberships should be present and count > 0
|
||||
assert rels["memberships"]["meta"]["count"] > 0
|
||||
|
||||
def test_me_include_roles_and_memberships_included_block(
|
||||
self, authenticated_client_rbac
|
||||
):
|
||||
# Request current user info including roles and memberships
|
||||
response = authenticated_client_rbac.get(
|
||||
reverse("user-me"), {"include": "roles,memberships"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
payload = response.json()
|
||||
|
||||
# Included must contain memberships corresponding to relationships data
|
||||
rel_memberships = payload["data"]["relationships"]["memberships"]
|
||||
ids_in_relationship = {item["id"] for item in rel_memberships["data"]}
|
||||
|
||||
included = payload["included"]
|
||||
included_membership_ids = {
|
||||
item["id"] for item in included if item["type"] == "memberships"
|
||||
}
|
||||
|
||||
# If there are memberships in relationships, they must be present in included
|
||||
if ids_in_relationship:
|
||||
assert ids_in_relationship.issubset(included_membership_ids)
|
||||
else:
|
||||
# At minimum, included should contain the user's membership when requested
|
||||
# (count should align with meta count)
|
||||
assert rel_memberships["meta"]["count"] == len(included_membership_ids)
|
||||
|
||||
def test_list_users_with_manage_account_only_forbidden(
|
||||
self, authenticated_client_rbac_manage_account
|
||||
):
|
||||
response = authenticated_client_rbac_manage_account.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_retrieve_other_user_with_manage_account_only_forbidden(
|
||||
self, authenticated_client_rbac_manage_account, create_test_user
|
||||
):
|
||||
response = authenticated_client_rbac_manage_account.get(
|
||||
reverse("user-detail", kwargs={"pk": create_test_user.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_list_users_with_manage_users_only_hides_relationships(
|
||||
self, authenticated_client_rbac_manage_users_only
|
||||
):
|
||||
# Ensure there is at least one other user in the same tenant
|
||||
mu_user = authenticated_client_rbac_manage_users_only.user
|
||||
mu_membership = Membership.objects.filter(user=mu_user).first()
|
||||
tenant = mu_membership.tenant
|
||||
|
||||
other_user = User.objects.create_user(
|
||||
name="other_in_tenant",
|
||||
email="other_in_tenant@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=other_user, tenant=tenant)
|
||||
|
||||
response = authenticated_client_rbac_manage_users_only.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert isinstance(data, list)
|
||||
|
||||
current_user_id = str(mu_user.id)
|
||||
assert any(item["id"] == current_user_id for item in data)
|
||||
|
||||
for item in data:
|
||||
rels = item["relationships"]
|
||||
if item["id"] == current_user_id:
|
||||
# Self should see own relationships
|
||||
assert isinstance(rels["roles"]["data"], list)
|
||||
assert rels["memberships"]["meta"].get("count", 0) >= 1
|
||||
else:
|
||||
# Others should be hidden without manage_account
|
||||
assert rels["roles"]["data"] == []
|
||||
assert rels["memberships"]["data"] == []
|
||||
assert rels["memberships"]["meta"]["count"] == 0
|
||||
|
||||
def test_include_roles_hidden_without_manage_account(
|
||||
self, authenticated_client_rbac_manage_users_only
|
||||
):
|
||||
# Arrange: ensure another user in the same tenant with its own role
|
||||
mu_user = authenticated_client_rbac_manage_users_only.user
|
||||
mu_membership = Membership.objects.filter(user=mu_user).first()
|
||||
tenant = mu_membership.tenant
|
||||
|
||||
other_user = User.objects.create_user(
|
||||
name="other_in_tenant_inc",
|
||||
email="other_in_tenant_inc@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=other_user, tenant=tenant)
|
||||
other_role = Role.objects.create(
|
||||
name="other_inc_role",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=other_user, role=other_role, tenant_id=tenant.id
|
||||
)
|
||||
|
||||
response = authenticated_client_rbac_manage_users_only.get(
|
||||
reverse("user-list"), {"include": "roles"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
payload = response.json()
|
||||
|
||||
# Assert: included must not contain the other user's role
|
||||
included = payload.get("included", [])
|
||||
included_role_ids = {
|
||||
item["id"] for item in included if item.get("type") == "roles"
|
||||
}
|
||||
assert str(other_role.id) not in included_role_ids
|
||||
|
||||
# Relationships for other user should be empty
|
||||
for item in payload["data"]:
|
||||
if item["id"] == str(other_user.id):
|
||||
rels = item["relationships"]
|
||||
assert rels["roles"]["data"] == []
|
||||
|
||||
def test_include_roles_visible_with_manage_account(
|
||||
self, authenticated_client_rbac, tenants_fixture
|
||||
):
|
||||
# Arrange: another user in tenant[0] with its role
|
||||
tenant = tenants_fixture[0]
|
||||
other_user = User.objects.create_user(
|
||||
name="other_with_role",
|
||||
email="other_with_role@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=other_user, tenant=tenant)
|
||||
other_role = Role.objects.create(
|
||||
name="other_visible_role",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=other_user, role=other_role, tenant_id=tenant.id
|
||||
)
|
||||
|
||||
response = authenticated_client_rbac.get(
|
||||
reverse("user-list"), {"include": "roles"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
payload = response.json()
|
||||
|
||||
# Assert: included must contain the other user's role
|
||||
included = payload.get("included", [])
|
||||
included_role_ids = {
|
||||
item["id"] for item in included if item.get("type") == "roles"
|
||||
}
|
||||
assert str(other_role.id) in included_role_ids
|
||||
|
||||
def test_retrieve_user_with_manage_users_only_hides_relationships(
|
||||
self, authenticated_client_rbac_manage_users_only
|
||||
):
|
||||
# Create a target user in the same tenant to ensure visibility
|
||||
mu_user = authenticated_client_rbac_manage_users_only.user
|
||||
mu_membership = Membership.objects.filter(user=mu_user).first()
|
||||
tenant = mu_membership.tenant
|
||||
|
||||
target_user = User.objects.create_user(
|
||||
name="target_same_tenant",
|
||||
email="target_same_tenant@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=target_user, tenant=tenant)
|
||||
|
||||
response = authenticated_client_rbac_manage_users_only.get(
|
||||
reverse("user-detail", kwargs={"pk": target_user.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
rels = response.json()["data"]["relationships"]
|
||||
assert rels["roles"]["data"] == []
|
||||
assert rels["memberships"]["data"] == []
|
||||
assert rels["memberships"]["meta"]["count"] == 0
|
||||
|
||||
def test_list_users_with_all_permissions_shows_relationships(
|
||||
self, authenticated_client_rbac
|
||||
):
|
||||
response = authenticated_client_rbac.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert isinstance(data, list)
|
||||
|
||||
rels = data[0]["relationships"]
|
||||
assert len(rels["roles"]["data"]) >= 0
|
||||
assert rels["memberships"]["meta"]["count"] >= 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderViewSet:
|
||||
@@ -494,3 +710,123 @@ class TestLimitedVisibility:
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRolePermissions:
|
||||
def test_role_create_with_manage_account_only_allowed(
|
||||
self, authenticated_client_rbac_manage_account
|
||||
):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "roles",
|
||||
"attributes": {
|
||||
"name": "Role Manage Account Only",
|
||||
"manage_users": "false",
|
||||
"manage_account": "true",
|
||||
"manage_providers": "false",
|
||||
"manage_scans": "false",
|
||||
"unlimited_visibility": "false",
|
||||
},
|
||||
"relationships": {"provider_groups": {"data": []}},
|
||||
}
|
||||
}
|
||||
response = authenticated_client_rbac_manage_account.post(
|
||||
reverse("role-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_role_create_with_manage_users_only_forbidden(
|
||||
self, authenticated_client_rbac_manage_users_only
|
||||
):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "roles",
|
||||
"attributes": {
|
||||
"name": "Role Manage Users Only",
|
||||
"manage_users": "true",
|
||||
"manage_account": "false",
|
||||
"manage_providers": "false",
|
||||
"manage_scans": "false",
|
||||
"unlimited_visibility": "false",
|
||||
},
|
||||
"relationships": {"provider_groups": {"data": []}},
|
||||
}
|
||||
}
|
||||
response = authenticated_client_rbac_manage_users_only.post(
|
||||
reverse("role-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserRoleLinkPermissions:
|
||||
def test_link_user_roles_with_manage_account_only_allowed(
|
||||
self, authenticated_client_rbac_manage_account
|
||||
):
|
||||
# Arrange: create a second user in the same tenant as the manage_account user
|
||||
ma_user = authenticated_client_rbac_manage_account.user
|
||||
ma_membership = Membership.objects.filter(user=ma_user).first()
|
||||
tenant = ma_membership.tenant
|
||||
|
||||
user2 = User.objects.create_user(
|
||||
name="target_user",
|
||||
email="target_user_ma@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=user2, tenant=tenant)
|
||||
|
||||
# Create a role in the same tenant
|
||||
role = Role.objects.create(
|
||||
name="linkable_role",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
)
|
||||
|
||||
data = {"data": [{"type": "roles", "id": str(role.id)}]}
|
||||
|
||||
# Act
|
||||
response = authenticated_client_rbac_manage_account.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": user2.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
def test_link_user_roles_with_manage_users_only_forbidden(
|
||||
self, authenticated_client_rbac_manage_users_only
|
||||
):
|
||||
mu_user = authenticated_client_rbac_manage_users_only.user
|
||||
mu_membership = Membership.objects.filter(user=mu_user).first()
|
||||
tenant = mu_membership.tenant
|
||||
|
||||
user2 = User.objects.create_user(
|
||||
name="target_user2",
|
||||
email="target_user_mu@rbac.com",
|
||||
password="Password123@",
|
||||
)
|
||||
Membership.objects.create(user=user2, tenant=tenant)
|
||||
|
||||
role = Role.objects.create(
|
||||
name="linkable_role_mu",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=False,
|
||||
)
|
||||
|
||||
data = {"data": [{"type": "roles", "id": str(role.id)}]}
|
||||
|
||||
response = authenticated_client_rbac_manage_users_only.post(
|
||||
reverse("user-roles-relationship", kwargs={"pk": user2.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@@ -30,9 +30,6 @@ class TestS3ConfigSerializer:
|
||||
"""Test that empty values raise validation errors."""
|
||||
serializer = S3ConfigSerializer()
|
||||
|
||||
with pytest.raises(ValidationError, match="Output directory cannot be empty"):
|
||||
serializer.validate_output_directory("")
|
||||
|
||||
with pytest.raises(
|
||||
ValidationError, match="Output directory cannot be empty or just"
|
||||
):
|
||||
|
||||
@@ -6,16 +6,18 @@ from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation, Provider
|
||||
from api.models import Integration, Invitation, Provider
|
||||
from api.utils import (
|
||||
get_prowler_provider_kwargs,
|
||||
initialize_prowler_provider,
|
||||
merge_dicts,
|
||||
prowler_integration_connection_test,
|
||||
prowler_provider_connection_test,
|
||||
return_prowler_provider,
|
||||
validate_invitation,
|
||||
)
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
@@ -197,6 +199,10 @@ class TestGetProwlerProviderKwargs:
|
||||
Provider.ProviderChoices.M365.value,
|
||||
{},
|
||||
),
|
||||
(
|
||||
Provider.ProviderChoices.GITHUB.value,
|
||||
{"organizations": ["provider_uid"]},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
|
||||
@@ -390,3 +396,258 @@ class TestValidateInvitation:
|
||||
mock_db.get.assert_called_once_with(
|
||||
token="VALID_TOKEN", email__iexact="user@example.com"
|
||||
)
|
||||
|
||||
|
||||
class TestProwlerIntegrationConnectionTest:
|
||||
"""Test prowler_integration_connection_test function for SecurityHub regions reset."""
|
||||
|
||||
@patch("api.utils.SecurityHub")
|
||||
def test_security_hub_connection_failure_resets_regions(
|
||||
self, mock_security_hub_class
|
||||
):
|
||||
"""Test that SecurityHub connection failure resets regions to empty dict."""
|
||||
# Create integration with existing regions configuration
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
integration.credentials = {
|
||||
"aws_access_key_id": "test_key",
|
||||
"aws_secret_access_key": "test_secret",
|
||||
}
|
||||
integration.configuration = {
|
||||
"send_only_fails": True,
|
||||
"regions": {
|
||||
"us-east-1": True,
|
||||
"us-west-2": True,
|
||||
"eu-west-1": False,
|
||||
"ap-south-1": False,
|
||||
},
|
||||
}
|
||||
|
||||
# Mock provider relationship
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.uid = "123456789012"
|
||||
mock_relationship = MagicMock()
|
||||
mock_relationship.provider = mock_provider
|
||||
integration.integrationproviderrelationship_set.first.return_value = (
|
||||
mock_relationship
|
||||
)
|
||||
|
||||
# Mock failed SecurityHub connection
|
||||
mock_connection = SecurityHubConnection(
|
||||
is_connected=False,
|
||||
error=Exception("SecurityHub testing"),
|
||||
enabled_regions=set(),
|
||||
disabled_regions=set(),
|
||||
)
|
||||
mock_security_hub_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Call the function
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
# Assertions
|
||||
assert result.is_connected is False
|
||||
assert str(result.error) == "SecurityHub testing"
|
||||
|
||||
# Verify regions were completely reset to empty dict
|
||||
assert integration.configuration["regions"] == {}
|
||||
|
||||
# Verify save was called to persist the change
|
||||
integration.save.assert_called_once()
|
||||
|
||||
# Verify test_connection was called with correct parameters
|
||||
mock_security_hub_class.test_connection.assert_called_once_with(
|
||||
aws_account_id="123456789012",
|
||||
raise_on_exception=False,
|
||||
aws_access_key_id="test_key",
|
||||
aws_secret_access_key="test_secret",
|
||||
)
|
||||
|
||||
@patch("api.utils.SecurityHub")
|
||||
def test_security_hub_connection_success_saves_regions(
|
||||
self, mock_security_hub_class
|
||||
):
|
||||
"""Test that successful SecurityHub connection saves regions correctly."""
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
integration.credentials = {
|
||||
"aws_access_key_id": "valid_key",
|
||||
"aws_secret_access_key": "valid_secret",
|
||||
}
|
||||
integration.configuration = {"send_only_fails": False}
|
||||
|
||||
# Mock provider relationship
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.uid = "123456789012"
|
||||
mock_relationship = MagicMock()
|
||||
mock_relationship.provider = mock_provider
|
||||
integration.integrationproviderrelationship_set.first.return_value = (
|
||||
mock_relationship
|
||||
)
|
||||
|
||||
# Mock successful SecurityHub connection with regions
|
||||
mock_connection = SecurityHubConnection(
|
||||
is_connected=True,
|
||||
error=None,
|
||||
enabled_regions={"us-east-1", "eu-west-1"},
|
||||
disabled_regions={"ap-south-1"},
|
||||
)
|
||||
mock_security_hub_class.test_connection.return_value = mock_connection
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
|
||||
# Verify regions were saved correctly
|
||||
assert integration.configuration["regions"]["us-east-1"] is True
|
||||
assert integration.configuration["regions"]["eu-west-1"] is True
|
||||
assert integration.configuration["regions"]["ap-south-1"] is False
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_success_basic_auth(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock successful JIRA connection with projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {"PROJ1": "Project 1", "PROJ2": "Project 2"}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify JIRA connection was called with correct parameters including domain from credentials
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="test@example.com",
|
||||
api_token="test_api_token",
|
||||
domain="example.atlassian.net",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called with correct tenant_id
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify projects were saved to integration configuration
|
||||
assert integration.configuration["projects"] == {
|
||||
"PROJ1": "Project 1",
|
||||
"PROJ2": "Project 2",
|
||||
}
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_failure_invalid_credentials(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "invalid@example.com",
|
||||
"api_token": "invalid_token",
|
||||
"domain": "invalid.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock failed JIRA connection
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = False
|
||||
mock_connection.error = Exception("Authentication failed: Invalid credentials")
|
||||
mock_connection.projects = {} # Empty projects when connection fails
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert "Authentication failed: Invalid credentials" in str(result.error)
|
||||
|
||||
# Verify JIRA connection was called with correct parameters
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="invalid@example.com",
|
||||
api_token="invalid_token",
|
||||
domain="invalid.atlassian.net",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called even on failure
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify empty projects dict was saved to integration configuration
|
||||
assert integration.configuration["projects"] == {}
|
||||
|
||||
# Verify integration.save() was called even on connection failure
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_projects_update_with_existing_configuration(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
"""Test that projects are properly updated when integration already has configuration data"""
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {
|
||||
"issue_types": ["Task"], # Existing configuration
|
||||
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
|
||||
}
|
||||
|
||||
# Mock successful JIRA connection with new projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify projects were updated (old projects replaced with new ones)
|
||||
assert integration.configuration["projects"] == {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
|
||||
# Verify other configuration fields were preserved
|
||||
assert integration.configuration["issue_types"] == ["Task"]
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@@ -13,6 +13,7 @@ from uuid import uuid4
|
||||
import jwt
|
||||
import pytest
|
||||
from allauth.socialaccount.models import SocialAccount, SocialApp
|
||||
from allauth.account.models import EmailAddress
|
||||
from botocore.exceptions import ClientError, NoCredentialsError
|
||||
from conftest import (
|
||||
API_JSON_CONTENT_TYPE,
|
||||
@@ -51,6 +52,7 @@ from api.models import (
|
||||
UserRoleRelationship,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.v1.serializers import TokenSerializer
|
||||
from api.v1.views import ComplianceOverviewViewSet, TenantFinishACSView
|
||||
|
||||
|
||||
@@ -324,6 +326,78 @@ class TestUserViewSet:
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert User.objects.filter(id=another_user.id).exists()
|
||||
|
||||
def test_users_destroy_cascades_allauth_and_memberships(
|
||||
self, authenticated_client, create_test_user
|
||||
):
|
||||
# Create related admin-side objects (email + SocialAccount)
|
||||
EmailAddress.objects.create(
|
||||
user=create_test_user,
|
||||
email=create_test_user.email,
|
||||
primary=True,
|
||||
verified=True,
|
||||
)
|
||||
SocialAccount.objects.create(
|
||||
user=create_test_user, provider="fake-provider", uid="uid-fake-provider"
|
||||
)
|
||||
|
||||
# Sanity check pre-conditions
|
||||
assert EmailAddress.objects.filter(user=create_test_user).exists()
|
||||
assert SocialAccount.objects.filter(user=create_test_user).exists()
|
||||
assert Membership.objects.filter(user=create_test_user).exists()
|
||||
assert UserRoleRelationship.objects.filter(user=create_test_user).exists()
|
||||
|
||||
# Delete current user
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": str(create_test_user.id)})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Assert user and related objects are gone
|
||||
assert not User.objects.filter(id=create_test_user.id).exists()
|
||||
assert not EmailAddress.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not SocialAccount.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not Membership.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not UserRoleRelationship.objects.filter(
|
||||
user_id=create_test_user.id
|
||||
).exists()
|
||||
|
||||
def test_users_destroy_with_saml_configuration_and_memberships(
|
||||
self, authenticated_client, create_test_user, saml_setup
|
||||
):
|
||||
# Ensure SAML configuration exists for tenant (from saml_setup fixture)
|
||||
domain = saml_setup["domain"]
|
||||
config = SAMLConfiguration.objects.get(email_domain=domain)
|
||||
|
||||
# Attach a SAML SocialAccount to the user
|
||||
SocialAccount.objects.create(
|
||||
user=create_test_user, provider="saml", uid="uid-saml"
|
||||
)
|
||||
|
||||
# Sanity check pre-conditions
|
||||
assert SocialAccount.objects.filter(
|
||||
user=create_test_user, provider="saml"
|
||||
).exists()
|
||||
assert Membership.objects.filter(user=create_test_user).exists()
|
||||
assert UserRoleRelationship.objects.filter(user=create_test_user).exists()
|
||||
|
||||
# Delete current user
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-detail", kwargs={"pk": str(create_test_user.id)})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Assert user-related rows are removed
|
||||
assert not User.objects.filter(id=create_test_user.id).exists()
|
||||
assert not SocialAccount.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not Membership.objects.filter(user_id=create_test_user.id).exists()
|
||||
assert not UserRoleRelationship.objects.filter(
|
||||
user_id=create_test_user.id
|
||||
).exists()
|
||||
|
||||
# Tenant-level SAML configuration should remain intact
|
||||
assert SAMLConfiguration.objects.filter(id=config.id).exists()
|
||||
assert SocialApp.objects.filter(provider="saml", client_id=domain).exists()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute_key, attribute_value, error_field",
|
||||
[
|
||||
@@ -4720,6 +4794,36 @@ class TestRoleViewSet:
|
||||
assert role.users.count() == 0
|
||||
assert role.provider_groups.count() == 0
|
||||
|
||||
def test_cannot_remove_own_assignment_via_role_update(
|
||||
self, authenticated_client, roles_fixture
|
||||
):
|
||||
role = roles_fixture[0]
|
||||
# Ensure the authenticated user is assigned to this role
|
||||
user = User.objects.get(email=TEST_USER)
|
||||
if not UserRoleRelationship.objects.filter(user=user, role=role).exists():
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user, role=role, tenant_id=role.tenant_id
|
||||
)
|
||||
|
||||
# Attempt to update role users to exclude the current user
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(role.id),
|
||||
"type": "roles",
|
||||
"relationships": {"users": {"data": []}},
|
||||
}
|
||||
}
|
||||
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
|
||||
assert (
|
||||
"cannot remove their own role"
|
||||
in response.json()["errors"][0]["detail"].lower()
|
||||
)
|
||||
|
||||
def test_role_create_with_invalid_user_relationship(
|
||||
self, authenticated_client, provider_groups_fixture
|
||||
):
|
||||
@@ -4841,15 +4945,134 @@ class TestUserRoleRelationshipViewSet:
|
||||
roles_fixture[2].id,
|
||||
}
|
||||
|
||||
def test_destroy_relationship(
|
||||
self, authenticated_client, roles_fixture, create_test_user
|
||||
def test_destroy_relationship_other_user(
|
||||
self, authenticated_client, roles_fixture, create_test_user, tenants_fixture
|
||||
):
|
||||
# Create another user in same tenant and assign a role
|
||||
tenant = tenants_fixture[0]
|
||||
other_user = User.objects.create_user(
|
||||
name="other",
|
||||
email="other_user@prowler.com",
|
||||
password="TmpPass123@",
|
||||
)
|
||||
Membership.objects.create(user=other_user, tenant=tenant)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=other_user, role=roles_fixture[0], tenant_id=tenant.id
|
||||
)
|
||||
|
||||
# Delete roles for the other user (allowed)
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-roles-relationship", kwargs={"pk": other_user.id}),
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(user=other_user.id)
|
||||
assert relationships.count() == 0
|
||||
|
||||
def test_cannot_delete_own_roles(self, authenticated_client, create_test_user):
|
||||
# Attempt to delete own roles should be forbidden
|
||||
response = authenticated_client.delete(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_prevent_removing_last_manage_account_on_patch(
|
||||
self, authenticated_client, roles_fixture, create_test_user, tenants_fixture
|
||||
):
|
||||
# roles_fixture[1] has manage_account=False
|
||||
limited_role = roles_fixture[1]
|
||||
|
||||
# Ensure there is no other user with MANAGE_ACCOUNT in the tenant
|
||||
tenant = tenants_fixture[0]
|
||||
# Create a secondary user without MANAGE_ACCOUNT
|
||||
user2 = User.objects.create_user(
|
||||
name="limited_user",
|
||||
email="limited_user@prowler.com",
|
||||
password="TmpPass123@",
|
||||
)
|
||||
Membership.objects.create(user=user2, tenant=tenant)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user2, role=limited_role, tenant_id=tenant.id
|
||||
)
|
||||
|
||||
# Attempt to switch the only MANAGE_ACCOUNT user to a role without it
|
||||
data = {"data": [{"type": "roles", "id": str(limited_role.id)}]}
|
||||
response = authenticated_client.patch(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "MANAGE_ACCOUNT" in response.json()["errors"][0]["detail"]
|
||||
|
||||
def test_allow_role_change_when_other_user_has_manage_account_on_patch(
|
||||
self, authenticated_client, roles_fixture, create_test_user, tenants_fixture
|
||||
):
|
||||
# roles_fixture[1] has manage_account=False, roles_fixture[0] has manage_account=True
|
||||
limited_role = roles_fixture[1]
|
||||
ma_role = roles_fixture[0]
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
# Create another user with MANAGE_ACCOUNT
|
||||
user2 = User.objects.create_user(
|
||||
name="ma_user",
|
||||
email="ma_user@prowler.com",
|
||||
password="TmpPass123@",
|
||||
)
|
||||
Membership.objects.create(user=user2, tenant=tenant)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user2, role=ma_role, tenant_id=tenant.id
|
||||
)
|
||||
|
||||
# Now changing the first user's roles to a non-MA role should succeed
|
||||
data = {"data": [{"type": "roles", "id": str(limited_role.id)}]}
|
||||
response = authenticated_client.patch(
|
||||
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
|
||||
data=data,
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
relationships = UserRoleRelationship.objects.filter(role=roles_fixture[0].id)
|
||||
assert relationships.count() == 0
|
||||
|
||||
def test_role_destroy_only_manage_account_blocked(
|
||||
self, authenticated_client, tenants_fixture
|
||||
):
|
||||
# Use a tenant without default admin role (tenant3)
|
||||
tenant = tenants_fixture[2]
|
||||
user = User.objects.get(email=TEST_USER)
|
||||
# Add membership for this tenant
|
||||
Membership.objects.create(user=user, tenant=tenant)
|
||||
|
||||
# Create a single MANAGE_ACCOUNT role in this tenant
|
||||
only_role = Role.objects.create(
|
||||
name="only_ma",
|
||||
tenant=tenant,
|
||||
manage_users=True,
|
||||
manage_account=True,
|
||||
manage_billing=False,
|
||||
manage_providers=False,
|
||||
manage_integrations=False,
|
||||
manage_scans=False,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
|
||||
# Switch token to this tenant
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": TEST_USER,
|
||||
"password": TEST_PASSWORD,
|
||||
"tenant_id": str(tenant.id),
|
||||
}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
access_token = serializer.validated_data["access"]
|
||||
authenticated_client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
|
||||
# Attempt to delete the only MANAGE_ACCOUNT role
|
||||
response = authenticated_client.delete(
|
||||
reverse("role-detail", kwargs={"pk": only_role.id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert Role.objects.filter(id=only_role.id).exists()
|
||||
|
||||
def test_invalid_provider_group_id(self, authenticated_client, create_test_user):
|
||||
invalid_id = "non-existent-id"
|
||||
@@ -5679,6 +5902,7 @@ class TestIntegrationViewSet:
|
||||
"integration_type": integration_type,
|
||||
"configuration": configuration,
|
||||
"credentials": credentials,
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
@@ -5696,6 +5920,7 @@ class TestIntegrationViewSet:
|
||||
assert Integration.objects.count() == 1
|
||||
integration = Integration.objects.first()
|
||||
assert integration.configuration == data["data"]["attributes"]["configuration"]
|
||||
assert integration.enabled == data["data"]["attributes"]["enabled"]
|
||||
assert (
|
||||
integration.integration_type
|
||||
== data["data"]["attributes"]["integration_type"]
|
||||
@@ -5706,6 +5931,47 @@ class TestIntegrationViewSet:
|
||||
== data["data"]["relationships"]["providers"]["data"][0]["id"]
|
||||
)
|
||||
|
||||
def test_integrations_create_valid_jira(
|
||||
self,
|
||||
authenticated_client,
|
||||
):
|
||||
"""Jira integrations are special"""
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "this-is-an-api-token-for-jira-that-works-for-sure",
|
||||
"user_mail": "testing@prowler.com",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert Integration.objects.count() == 1
|
||||
integration = Integration.objects.first()
|
||||
integration_configuration = response.json()["data"]["attributes"][
|
||||
"configuration"
|
||||
]
|
||||
assert "projects" in integration_configuration
|
||||
assert "issue_types" in integration_configuration
|
||||
assert "domain" in integration_configuration
|
||||
assert integration.enabled == data["data"]["attributes"]["enabled"]
|
||||
assert (
|
||||
integration.integration_type
|
||||
== data["data"]["attributes"]["integration_type"]
|
||||
)
|
||||
assert "credentials" not in response.json()["data"]["attributes"]
|
||||
|
||||
def test_integrations_create_valid_relationships(
|
||||
self,
|
||||
authenticated_client,
|
||||
@@ -5804,6 +6070,46 @@ class TestIntegrationViewSet:
|
||||
"invalid",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"projects": ["JIRA"],
|
||||
},
|
||||
"credentials": {"domain": "prowlerdomain"},
|
||||
},
|
||||
"invalid",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"credentialss": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "api-token",
|
||||
"user_mail": "test@prowler.com",
|
||||
},
|
||||
},
|
||||
"required",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {},
|
||||
},
|
||||
"required",
|
||||
"credentials",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {},
|
||||
"credentials": {"api_token": "api-token"},
|
||||
},
|
||||
"invalid",
|
||||
"credentials",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -5993,6 +6299,217 @@ class TestIntegrationViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_integrations_create_duplicate_amazon_s3(
|
||||
self, authenticated_client, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
# Create first S3 integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.AMAZON_S3,
|
||||
"configuration": {
|
||||
"bucket_name": "test-bucket",
|
||||
"output_directory": "test-output",
|
||||
},
|
||||
"credentials": {
|
||||
"role_arn": "arn:aws:iam::123456789012:role/test-role",
|
||||
"external_id": "test-external-id",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
"data": [{"type": "providers", "id": str(provider.id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# First creation should succeed
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Attempt to create duplicate should return 409
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert (
|
||||
"This integration already exists" in response.json()["errors"][0]["detail"]
|
||||
)
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_create_duplicate_jira(self, authenticated_client):
|
||||
# Create first JIRA integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# First creation should succeed
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Attempt to create duplicate should return 409
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert (
|
||||
"This integration already exists" in response.json()["errors"][0]["detail"]
|
||||
)
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_update_jira_configuration_readonly(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "initial-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Attempt to update configuration - should be ignored/not allowed
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"configuration": {
|
||||
"projects": {"NEW_PROJECT": "New Project"},
|
||||
"issue_types": ["Epic", "Story"],
|
||||
"domain": "malicious-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_integrations_update_jira_credentials_domain_reflects_in_configuration(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "original-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Verify initial domain in configuration
|
||||
initial_integration = response.json()["data"]
|
||||
assert (
|
||||
initial_integration["attributes"]["configuration"]["domain"]
|
||||
== "original-domain"
|
||||
)
|
||||
|
||||
# Update credentials with new domain
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"credentials": {
|
||||
"user_mail": "updated@example.com",
|
||||
"api_token": "updated-api-token",
|
||||
"domain": "updated-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify the new domain is reflected in configuration
|
||||
updated_integration = response.json()["data"]
|
||||
configuration = updated_integration["attributes"]["configuration"]
|
||||
assert configuration["domain"] == "updated-domain"
|
||||
|
||||
# Verify other configuration fields are preserved
|
||||
assert "projects" in configuration
|
||||
assert "issue_types" in configuration
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSAMLTokenValidation:
|
||||
|
||||
@@ -6,11 +6,14 @@ from django.db.models import Subquery
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Integration, Invitation, Processor, Provider, Resource
|
||||
from api.v1.serializers import FindingMetadataSerializer
|
||||
from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.common.models import Connection
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
@@ -119,6 +122,12 @@ def get_prowler_provider_kwargs(
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.KUBERNETES.value:
|
||||
prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid}
|
||||
elif provider.provider == Provider.ProviderChoices.GITHUB.value:
|
||||
if provider.uid:
|
||||
prowler_provider_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"organizations": [provider.uid],
|
||||
}
|
||||
|
||||
if mutelist_processor:
|
||||
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
|
||||
@@ -192,13 +201,53 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
|
||||
raise_on_exception=False,
|
||||
)
|
||||
# TODO: It is possible that we can unify the connection test for all integrations, but need refactoring
|
||||
# to avoid code duplication. Actually the AWS integrations are similar, so SecurityHub and S3 can be unified making some changes in the SDK.
|
||||
# to avoid code duplication. Actually the AWS integrations are similar, so SecurityHub and S3 can be unified
|
||||
# making some changes in the SDK.
|
||||
elif (
|
||||
integration.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
):
|
||||
pass
|
||||
# Get the provider associated with this integration
|
||||
provider_relationship = integration.integrationproviderrelationship_set.first()
|
||||
if not provider_relationship:
|
||||
return Connection(
|
||||
is_connected=False, error="No provider associated with this integration"
|
||||
)
|
||||
|
||||
credentials = (
|
||||
integration.credentials
|
||||
if integration.credentials
|
||||
else provider_relationship.provider.secret.secret
|
||||
)
|
||||
connection = SecurityHub.test_connection(
|
||||
aws_account_id=provider_relationship.provider.uid,
|
||||
raise_on_exception=False,
|
||||
**credentials,
|
||||
)
|
||||
|
||||
# Only save regions if connection is successful
|
||||
if connection.is_connected:
|
||||
regions_status = {r: True for r in connection.enabled_regions}
|
||||
regions_status.update({r: False for r in connection.disabled_regions})
|
||||
|
||||
# Save regions information in the integration configuration
|
||||
integration.configuration["regions"] = regions_status
|
||||
integration.save()
|
||||
else:
|
||||
# Reset regions information if connection fails
|
||||
integration.configuration["regions"] = {}
|
||||
integration.save()
|
||||
|
||||
return connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
pass
|
||||
jira_connection = Jira.test_connection(
|
||||
**integration.credentials,
|
||||
raise_on_exception=False,
|
||||
)
|
||||
project_keys = jira_connection.projects if jira_connection.is_connected else {}
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.configuration["projects"] = project_keys
|
||||
integration.save()
|
||||
return jira_connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
|
||||
pass
|
||||
else:
|
||||
@@ -298,3 +347,17 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return serializer.data
|
||||
|
||||
|
||||
def initialize_prowler_integration(integration: Integration) -> Jira:
|
||||
# TODO Refactor other integrations to use this function
|
||||
if integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
try:
|
||||
return Jira(**integration.credentials)
|
||||
except JiraBasicAuthError as jira_auth_error:
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.configuration["projects"] = {}
|
||||
integration.connected = False
|
||||
integration.connection_last_checked_at = datetime.now(tz=timezone.utc)
|
||||
integration.save()
|
||||
raise jira_auth_error
|
||||
|
||||
@@ -9,15 +9,17 @@ from api.v1.serializer_utils.base import BaseValidateSerializer
|
||||
|
||||
class S3ConfigSerializer(BaseValidateSerializer):
|
||||
bucket_name = serializers.CharField()
|
||||
output_directory = serializers.CharField()
|
||||
output_directory = serializers.CharField(allow_blank=True)
|
||||
|
||||
def validate_output_directory(self, value):
|
||||
"""
|
||||
Validate the output_directory field to ensure it's a properly formatted path.
|
||||
Prevents paths with excessive slashes like "///////test".
|
||||
If empty, sets a default value.
|
||||
"""
|
||||
# If empty or None, set default value
|
||||
if not value:
|
||||
raise serializers.ValidationError("Output directory cannot be empty.")
|
||||
return "output"
|
||||
|
||||
# Normalize the path to remove excessive slashes
|
||||
normalized_path = os.path.normpath(value)
|
||||
@@ -35,7 +37,7 @@ class S3ConfigSerializer(BaseValidateSerializer):
|
||||
# Check for empty path after normalization
|
||||
if not normalized_path or normalized_path == ".":
|
||||
raise serializers.ValidationError(
|
||||
"Output directory cannot be empty or just '.'."
|
||||
"Output directory cannot be empty or just '.' or '/'."
|
||||
)
|
||||
|
||||
# Check for paths that are too long (S3 key limit is 1024 characters, leave some room for filename)
|
||||
@@ -50,6 +52,32 @@ class S3ConfigSerializer(BaseValidateSerializer):
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class SecurityHubConfigSerializer(BaseValidateSerializer):
|
||||
send_only_fails = serializers.BooleanField(default=False)
|
||||
archive_previous_findings = serializers.BooleanField(default=False)
|
||||
regions = serializers.DictField(default=dict, read_only=True)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
validated_data = super().to_internal_value(data)
|
||||
# Always initialize regions as empty dict
|
||||
validated_data["regions"] = {}
|
||||
return validated_data
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class JiraConfigSerializer(BaseValidateSerializer):
|
||||
domain = serializers.CharField(read_only=True)
|
||||
issue_types = serializers.ListField(
|
||||
read_only=True, child=serializers.CharField(), default=["Task"]
|
||||
)
|
||||
projects = serializers.DictField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
role_arn = serializers.CharField(required=False)
|
||||
external_id = serializers.CharField(required=False)
|
||||
@@ -65,6 +93,15 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
user_mail = serializers.EmailField(required=True)
|
||||
api_token = serializers.CharField(required=True)
|
||||
domain = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
"oneOf": [
|
||||
@@ -116,6 +153,27 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "JIRA Credentials",
|
||||
"properties": {
|
||||
"user_mail": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "The email address of the JIRA user account.",
|
||||
},
|
||||
"api_token": {
|
||||
"type": "string",
|
||||
"description": "The API token for authentication with JIRA. This can be generated from your "
|
||||
"Atlassian account settings.",
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
|
||||
},
|
||||
},
|
||||
"required": ["user_mail", "api_token", "domain"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
@@ -136,12 +194,40 @@ class IntegrationCredentialField(serializers.JSONField):
|
||||
},
|
||||
"output_directory": {
|
||||
"type": "string",
|
||||
"description": 'The directory path within the bucket where files will be saved. Path will be normalized to remove excessive slashes and invalid characters are not allowed (< > : " | ? *). Maximum length is 900 characters.',
|
||||
"description": "The directory path within the bucket where files will be saved. Optional - "
|
||||
'defaults to "output" if not provided. Path will be normalized to remove '
|
||||
'excessive slashes and invalid characters are not allowed (< > : " | ? *). '
|
||||
"Maximum length is 900 characters.",
|
||||
"maxLength": 900,
|
||||
"pattern": '^[^<>:"|?*]+$',
|
||||
"default": "output",
|
||||
},
|
||||
},
|
||||
"required": ["bucket_name", "output_directory"],
|
||||
"required": ["bucket_name"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "AWS Security Hub",
|
||||
"properties": {
|
||||
"send_only_fails": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "If true, only findings with status 'FAIL' will be sent to Security Hub.",
|
||||
},
|
||||
"archive_previous_findings": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "If true, archives findings that are not present in the current execution.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "JIRA",
|
||||
"description": "JIRA integration does not accept any configuration in the payload. Leave it as an "
|
||||
"empty JSON object (`{}`).",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ from rest_framework_simplejwt.exceptions import TokenError
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import ConflictException
|
||||
from api.models import (
|
||||
Finding,
|
||||
Integration,
|
||||
@@ -45,7 +47,10 @@ from api.v1.serializer_utils.integrations import (
|
||||
AWSCredentialSerializer,
|
||||
IntegrationConfigField,
|
||||
IntegrationCredentialField,
|
||||
JiraConfigSerializer,
|
||||
JiraCredentialSerializer,
|
||||
S3ConfigSerializer,
|
||||
SecurityHubConfigSerializer,
|
||||
)
|
||||
from api.v1.serializer_utils.processors import ProcessorConfigField
|
||||
from api.v1.serializer_utils.providers import ProviderSecretField
|
||||
@@ -255,8 +260,15 @@ class UserSerializer(BaseSerializerV1):
|
||||
Serializer for the User model.
|
||||
"""
|
||||
|
||||
memberships = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
roles = serializers.ResourceRelatedField(many=True, read_only=True)
|
||||
# We use SerializerMethodResourceRelatedField so includes (e.g. ?include=roles)
|
||||
# respect RBAC and do not leak relationships of other users when the requester
|
||||
# lacks manage_account. The visibility logic lives in get_roles/get_memberships.
|
||||
memberships = SerializerMethodResourceRelatedField(
|
||||
many=True, read_only=True, source="memberships", method_name="get_memberships"
|
||||
)
|
||||
roles = SerializerMethodResourceRelatedField(
|
||||
many=True, read_only=True, source="roles", method_name="get_roles"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@@ -274,9 +286,35 @@ class UserSerializer(BaseSerializerV1):
|
||||
}
|
||||
|
||||
included_serializers = {
|
||||
"roles": "api.v1.serializers.RoleSerializer",
|
||||
"roles": "api.v1.serializers.RoleIncludeSerializer",
|
||||
"memberships": "api.v1.serializers.MembershipIncludeSerializer",
|
||||
}
|
||||
|
||||
def _can_view_relationships(self, instance) -> bool:
|
||||
"""Allow self to view own relationships. Require manage_account to view others."""
|
||||
role = self.context.get("role")
|
||||
request = self.context.get("request")
|
||||
is_self = bool(
|
||||
request
|
||||
and getattr(request, "user", None)
|
||||
and getattr(instance, "id", None) == request.user.id
|
||||
)
|
||||
return is_self or (role and role.manage_account)
|
||||
|
||||
def get_roles(self, instance):
|
||||
return (
|
||||
instance.roles.all()
|
||||
if self._can_view_relationships(instance)
|
||||
else Role.objects.none()
|
||||
)
|
||||
|
||||
def get_memberships(self, instance):
|
||||
return (
|
||||
instance.memberships.all()
|
||||
if self._can_view_relationships(instance)
|
||||
else Membership.objects.none()
|
||||
)
|
||||
|
||||
|
||||
class UserCreateSerializer(BaseWriteSerializer):
|
||||
password = serializers.CharField(write_only=True)
|
||||
@@ -384,6 +422,34 @@ class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
roles = Role.objects.filter(id__in=role_ids)
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
|
||||
# Safeguard: A tenant must always have at least one user with MANAGE_ACCOUNT.
|
||||
# If the target roles do NOT include MANAGE_ACCOUNT, and the current user is
|
||||
# the only one in the tenant with MANAGE_ACCOUNT, block the update.
|
||||
target_includes_manage_account = roles.filter(manage_account=True).exists()
|
||||
if not target_includes_manage_account:
|
||||
# Check if any other user has MANAGE_ACCOUNT
|
||||
other_users_have_manage_account = (
|
||||
UserRoleRelationship.objects.filter(
|
||||
tenant_id=tenant_id, role__manage_account=True
|
||||
)
|
||||
.exclude(user_id=instance.id)
|
||||
.exists()
|
||||
)
|
||||
|
||||
# Check if the current user has MANAGE_ACCOUNT
|
||||
instance_has_manage_account = instance.roles.filter(
|
||||
tenant_id=tenant_id, manage_account=True
|
||||
).exists()
|
||||
|
||||
# If the current user is the last holder of MANAGE_ACCOUNT, prevent removal
|
||||
if instance_has_manage_account and not other_users_have_manage_account:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"roles": "At least one user in the tenant must retain MANAGE_ACCOUNT. "
|
||||
"Assign MANAGE_ACCOUNT to another user before removing it here."
|
||||
}
|
||||
)
|
||||
|
||||
instance.roles.clear()
|
||||
new_relationships = [
|
||||
UserRoleRelationship(user=instance, role=r, tenant_id=tenant_id)
|
||||
@@ -498,6 +564,12 @@ class TenantSerializer(BaseSerializerV1):
|
||||
fields = ["id", "name", "memberships"]
|
||||
|
||||
|
||||
class TenantIncludeSerializer(BaseSerializerV1):
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
# Memberships
|
||||
|
||||
|
||||
@@ -519,6 +591,29 @@ class MembershipSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "user", "tenant", "role", "date_joined"]
|
||||
|
||||
|
||||
class MembershipIncludeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Include-oriented Membership serializer that enables including tenant objects with names
|
||||
without altering the base MembershipSerializer behavior.
|
||||
"""
|
||||
|
||||
role = MemberRoleEnumSerializerField()
|
||||
user = serializers.ResourceRelatedField(read_only=True)
|
||||
tenant = SerializerMethodResourceRelatedField(read_only=True, source="tenant")
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ["id", "user", "tenant", "role", "date_joined"]
|
||||
|
||||
included_serializers = {"tenant": "api.v1.serializers.TenantIncludeSerializer"}
|
||||
|
||||
def get_tenant(self, instance):
|
||||
try:
|
||||
return Tenant.objects.using(MainRouter.admin_db).get(id=instance.tenant_id)
|
||||
except Tenant.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
# Provider Groups
|
||||
class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
providers = serializers.ResourceRelatedField(
|
||||
@@ -1674,6 +1769,37 @@ class RoleUpdateSerializer(RoleSerializer):
|
||||
|
||||
if "users" in validated_data:
|
||||
users = validated_data.pop("users")
|
||||
# Prevent a user from removing their own role assignment via Role update
|
||||
request = self.context.get("request")
|
||||
if request and getattr(request, "user", None):
|
||||
request_user = request.user
|
||||
is_currently_assigned = instance.users.filter(
|
||||
id=request_user.id
|
||||
).exists()
|
||||
will_be_assigned = any(u.id == request_user.id for u in users)
|
||||
if is_currently_assigned and not will_be_assigned:
|
||||
raise serializers.ValidationError(
|
||||
{"users": "Users cannot remove their own role."}
|
||||
)
|
||||
|
||||
# Safeguard MANAGE_ACCOUNT coverage when updating users of this role
|
||||
if instance.manage_account:
|
||||
# Existing MANAGE_ACCOUNT assignments on other roles within the tenant
|
||||
other_ma_exists = (
|
||||
UserRoleRelationship.objects.filter(
|
||||
tenant_id=tenant_id, role__manage_account=True
|
||||
)
|
||||
.exclude(role_id=instance.id)
|
||||
.exists()
|
||||
)
|
||||
|
||||
if not other_ma_exists and len(users) == 0:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"users": "At least one user in the tenant must retain MANAGE_ACCOUNT. "
|
||||
"Assign this MANAGE_ACCOUNT role to at least one user or ensure another user has it."
|
||||
}
|
||||
)
|
||||
instance.users.clear()
|
||||
through_model_instances = [
|
||||
UserRoleRelationship(
|
||||
@@ -1688,6 +1814,37 @@ class RoleUpdateSerializer(RoleSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class RoleIncludeSerializer(RLSSerializer):
|
||||
permission_state = serializers.SerializerMethodField()
|
||||
|
||||
def get_permission_state(self, obj) -> str:
|
||||
return obj.permission_state
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"manage_users",
|
||||
"manage_account",
|
||||
# Disable for the first release
|
||||
# "manage_billing",
|
||||
# /Disable for the first release
|
||||
"manage_integrations",
|
||||
"manage_providers",
|
||||
"manage_scans",
|
||||
"permission_state",
|
||||
"unlimited_visibility",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):
|
||||
resource_type = serializers.CharField(source="type")
|
||||
id = serializers.UUIDField()
|
||||
@@ -1802,6 +1959,7 @@ class ComplianceOverviewDetailSerializer(serializers.Serializer):
|
||||
|
||||
class ComplianceOverviewAttributesSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
compliance_name = serializers.CharField()
|
||||
framework_description = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
framework = serializers.CharField()
|
||||
@@ -1951,13 +2109,59 @@ class ScheduleDailyCreateSerializer(serializers.Serializer):
|
||||
|
||||
class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
def validate(self, attrs):
|
||||
if Integration.objects.filter(
|
||||
configuration=attrs.get("configuration")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"name": "This integration already exists."}
|
||||
integration_type = attrs.get("integration_type")
|
||||
|
||||
if (
|
||||
integration_type == Integration.IntegrationChoices.AMAZON_S3
|
||||
and Integration.objects.filter(
|
||||
configuration=attrs.get("configuration")
|
||||
).exists()
|
||||
):
|
||||
raise ConflictException(
|
||||
detail="This integration already exists.",
|
||||
pointer="/data/attributes/configuration",
|
||||
)
|
||||
|
||||
if (
|
||||
integration_type == Integration.IntegrationChoices.JIRA
|
||||
and Integration.objects.filter(
|
||||
configuration__contains={
|
||||
"domain": attrs.get("configuration").get("domain")
|
||||
}
|
||||
).exists()
|
||||
):
|
||||
raise ConflictException(
|
||||
detail="This integration already exists.",
|
||||
pointer="/data/attributes/configuration",
|
||||
)
|
||||
|
||||
# Check if any provider already has a SecurityHub integration
|
||||
if hasattr(self, "instance") and self.instance and not integration_type:
|
||||
integration_type = self.instance.integration_type
|
||||
|
||||
if (
|
||||
integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
and "providers" in attrs
|
||||
):
|
||||
providers = attrs.get("providers", [])
|
||||
tenant_id = self.context.get("tenant_id")
|
||||
for provider in providers:
|
||||
# For updates, exclude the current instance from the check
|
||||
query = IntegrationProviderRelationship.objects.filter(
|
||||
provider=provider,
|
||||
integration__integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
if hasattr(self, "instance") and self.instance:
|
||||
query = query.exclude(integration=self.instance)
|
||||
|
||||
if query.exists():
|
||||
raise ConflictException(
|
||||
detail=f"Provider {provider.id} already has a Security Hub integration. Only one "
|
||||
"Security Hub integration is allowed per provider.",
|
||||
pointer="/data/relationships/providers",
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
@staticmethod
|
||||
@@ -1970,14 +2174,46 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
if integration_type == Integration.IntegrationChoices.AMAZON_S3:
|
||||
config_serializer = S3ConfigSerializer
|
||||
credentials_serializers = [AWSCredentialSerializer]
|
||||
# TODO: This will be required for AWS Security Hub
|
||||
# if providers and not all(
|
||||
# provider.provider == Provider.ProviderChoices.AWS
|
||||
# for provider in providers
|
||||
# ):
|
||||
# raise serializers.ValidationError(
|
||||
# {"providers": "All providers must be AWS for the S3 integration."}
|
||||
# )
|
||||
elif integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB:
|
||||
if providers:
|
||||
if len(providers) > 1:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "Only one provider is supported for the Security Hub integration."
|
||||
}
|
||||
)
|
||||
if providers[0].provider != Provider.ProviderChoices.AWS:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "The provider must be AWS type for the Security Hub integration."
|
||||
}
|
||||
)
|
||||
config_serializer = SecurityHubConfigSerializer
|
||||
credentials_serializers = [AWSCredentialSerializer]
|
||||
elif integration_type == Integration.IntegrationChoices.JIRA:
|
||||
if providers:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "Relationship field is not accepted. This integration applies to all providers."
|
||||
}
|
||||
)
|
||||
if configuration:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"configuration": "This integration does not support custom configuration."
|
||||
}
|
||||
)
|
||||
config_serializer = JiraConfigSerializer
|
||||
# Create non-editable configuration for JIRA integration
|
||||
default_jira_issue_types = ["Task"]
|
||||
configuration.update(
|
||||
{
|
||||
"projects": {},
|
||||
"issue_types": default_jira_issue_types,
|
||||
"domain": credentials.get("domain"),
|
||||
}
|
||||
)
|
||||
credentials_serializers = [JiraCredentialSerializer]
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
@@ -2041,6 +2277,10 @@ class IntegrationSerializer(RLSSerializer):
|
||||
for provider in representation["providers"]
|
||||
if provider["id"] in allowed_provider_ids
|
||||
]
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
@@ -2068,21 +2308,27 @@ class IntegrationCreateSerializer(BaseWriteIntegrationSerializer):
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"connected": {"read_only": True},
|
||||
"enabled": {"read_only": True},
|
||||
"connection_last_checked_at": {"read_only": True},
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
integration_type = attrs.get("integration_type")
|
||||
providers = attrs.get("providers")
|
||||
configuration = attrs.get("configuration")
|
||||
credentials = attrs.get("credentials")
|
||||
|
||||
validated_attrs = super().validate(attrs)
|
||||
if (
|
||||
not providers
|
||||
and integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"providers": "At least one provider is required for this integration."}
|
||||
)
|
||||
|
||||
self.validate_integration_data(
|
||||
integration_type, providers, configuration, credentials
|
||||
)
|
||||
validated_attrs = super().validate(attrs)
|
||||
return validated_attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -2133,16 +2379,18 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
integration_type = self.instance.integration_type
|
||||
providers = attrs.get("providers")
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
if integration_type != Integration.IntegrationChoices.JIRA:
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
else:
|
||||
configuration = attrs.get("configuration", {})
|
||||
credentials = attrs.get("credentials") or self.instance.credentials
|
||||
|
||||
validated_attrs = super().validate(attrs)
|
||||
self.validate_integration_data(
|
||||
integration_type, providers, configuration, credentials
|
||||
)
|
||||
validated_attrs = super().validate(attrs)
|
||||
return validated_attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -2157,8 +2405,62 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
]
|
||||
IntegrationProviderRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
# Preserve regions field for Security Hub integrations
|
||||
if instance.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB:
|
||||
if "configuration" in validated_data:
|
||||
# Preserve the existing regions field if it exists
|
||||
existing_regions = instance.configuration.get("regions", {})
|
||||
validated_data["configuration"]["regions"] = existing_regions
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
# Ensure JIRA integrations show updated domain in configuration from credentials
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
class IntegrationJiraDispatchSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for dispatching findings to JIRA integration.
|
||||
"""
|
||||
|
||||
project_key = serializers.CharField(required=True)
|
||||
issue_type = serializers.ChoiceField(required=True, choices=["Task"])
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "integrations-jira-dispatches"
|
||||
|
||||
def validate(self, attrs):
|
||||
validated_attrs = super().validate(attrs)
|
||||
integration_instance = Integration.objects.get(
|
||||
id=self.context.get("integration_id")
|
||||
)
|
||||
if integration_instance.integration_type != Integration.IntegrationChoices.JIRA:
|
||||
raise ValidationError(
|
||||
{"integration_type": "The given integration is not a JIRA integration"}
|
||||
)
|
||||
|
||||
if not integration_instance.enabled:
|
||||
raise ValidationError(
|
||||
{"integration": "The given integration is not enabled"}
|
||||
)
|
||||
|
||||
project_key = attrs.get("project_key")
|
||||
if project_key not in integration_instance.configuration.get("projects", {}):
|
||||
raise ValidationError(
|
||||
{
|
||||
"project_key": "The given project key is not available for this JIRA integration. Refresh the "
|
||||
"connection if this is an error."
|
||||
}
|
||||
)
|
||||
|
||||
return validated_attrs
|
||||
|
||||
|
||||
# Processors
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from api.v1.views import (
|
||||
FindingViewSet,
|
||||
GithubSocialLoginView,
|
||||
GoogleSocialLoginView,
|
||||
IntegrationJiraViewSet,
|
||||
IntegrationViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
InvitationViewSet,
|
||||
@@ -73,6 +74,13 @@ tenants_router.register(
|
||||
users_router = routers.NestedSimpleRouter(router, r"users", lookup="user")
|
||||
users_router.register(r"memberships", MembershipViewSet, basename="user-membership")
|
||||
|
||||
integrations_router = routers.NestedSimpleRouter(
|
||||
router, r"integrations", lookup="integration"
|
||||
)
|
||||
integrations_router.register(
|
||||
r"jira", IntegrationJiraViewSet, basename="integration-jira"
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"),
|
||||
path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"),
|
||||
@@ -162,6 +170,7 @@ urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
path("", include(tenants_router.urls)),
|
||||
path("", include(users_router.urls)),
|
||||
path("", include(integrations_router.urls)),
|
||||
path("schema", SchemaView.as_view(), name="schema"),
|
||||
path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"),
|
||||
]
|
||||
|
||||
@@ -62,6 +62,7 @@ from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
delete_tenant_task,
|
||||
jira_integration_task,
|
||||
perform_scan_task,
|
||||
)
|
||||
|
||||
@@ -75,8 +76,10 @@ from api.db_utils import rls_transaction
|
||||
from api.exceptions import TaskFailedException
|
||||
from api.filters import (
|
||||
ComplianceOverviewFilter,
|
||||
CustomDjangoFilterBackend,
|
||||
FindingFilter,
|
||||
IntegrationFilter,
|
||||
IntegrationJiraFindingsFilter,
|
||||
InvitationFilter,
|
||||
LatestFindingFilter,
|
||||
LatestResourceFilter,
|
||||
@@ -89,6 +92,7 @@ from api.filters import (
|
||||
RoleFilter,
|
||||
ScanFilter,
|
||||
ScanSummaryFilter,
|
||||
ScanSummarySeverityFilter,
|
||||
ServiceOverviewFilter,
|
||||
TaskFilter,
|
||||
TenantFilter,
|
||||
@@ -142,6 +146,7 @@ from api.v1.serializers import (
|
||||
FindingMetadataSerializer,
|
||||
FindingSerializer,
|
||||
IntegrationCreateSerializer,
|
||||
IntegrationJiraDispatchSerializer,
|
||||
IntegrationSerializer,
|
||||
IntegrationUpdateSerializer,
|
||||
InvitationAcceptSerializer,
|
||||
@@ -214,6 +219,8 @@ class RelationshipViewSchema(JsonApiAutoSchema):
|
||||
description="Obtain a token by providing valid credentials and an optional tenant ID.",
|
||||
)
|
||||
class CustomTokenObtainView(GenericAPIView):
|
||||
throttle_scope = "token-obtain"
|
||||
|
||||
resource_name = "tokens"
|
||||
serializer_class = TokenSerializer
|
||||
http_method_names = ["post"]
|
||||
@@ -293,7 +300,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.10.2"
|
||||
spectacular_settings.VERSION = "1.14.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -313,6 +320,11 @@ class SchemaView(SpectacularAPIView):
|
||||
"description": "Endpoints for tenant invitations management, allowing retrieval and filtering of "
|
||||
"invitations, creating new invitations, accepting and revoking them.",
|
||||
},
|
||||
{
|
||||
"name": "Role",
|
||||
"description": "Endpoints for managing RBAC roles within tenants, allowing creation, retrieval, "
|
||||
"updating, and deletion of role configurations and permissions.",
|
||||
},
|
||||
{
|
||||
"name": "Provider",
|
||||
"description": "Endpoints for managing providers (AWS, GCP, Azure, etc...).",
|
||||
@@ -321,10 +333,20 @@ class SchemaView(SpectacularAPIView):
|
||||
"name": "Provider Group",
|
||||
"description": "Endpoints for managing provider groups.",
|
||||
},
|
||||
{
|
||||
"name": "Task",
|
||||
"description": "Endpoints for task management, allowing retrieval of task status and "
|
||||
"revoking tasks that have not started.",
|
||||
},
|
||||
{
|
||||
"name": "Scan",
|
||||
"description": "Endpoints for triggering manual scans and viewing scan results.",
|
||||
},
|
||||
{
|
||||
"name": "Schedule",
|
||||
"description": "Endpoints for managing scan schedules, allowing configuration of automated "
|
||||
"scans with different scheduling options.",
|
||||
},
|
||||
{
|
||||
"name": "Resource",
|
||||
"description": "Endpoints for managing resources discovered by scans, allowing "
|
||||
@@ -336,8 +358,9 @@ class SchemaView(SpectacularAPIView):
|
||||
"findings that result from scans.",
|
||||
},
|
||||
{
|
||||
"name": "Overview",
|
||||
"description": "Endpoints for retrieving aggregated summaries of resources from the system.",
|
||||
"name": "Processor",
|
||||
"description": "Endpoints for managing post-processors used to process Prowler findings, including "
|
||||
"registration, configuration, and deletion of post-processing actions.",
|
||||
},
|
||||
{
|
||||
"name": "Compliance Overview",
|
||||
@@ -345,9 +368,8 @@ class SchemaView(SpectacularAPIView):
|
||||
" compliance framework ID.",
|
||||
},
|
||||
{
|
||||
"name": "Task",
|
||||
"description": "Endpoints for task management, allowing retrieval of task status and "
|
||||
"revoking tasks that have not started.",
|
||||
"name": "Overview",
|
||||
"description": "Endpoints for retrieving aggregated summaries of resources from the system.",
|
||||
},
|
||||
{
|
||||
"name": "Integration",
|
||||
@@ -355,14 +377,15 @@ class SchemaView(SpectacularAPIView):
|
||||
" retrieval, and deletion of integrations such as S3, JIRA, or other services.",
|
||||
},
|
||||
{
|
||||
"name": "Lighthouse",
|
||||
"description": "Endpoints for managing Lighthouse configurations, including creation, retrieval, "
|
||||
"updating, and deletion of configurations such as OpenAI keys, models, and business context.",
|
||||
"name": "Lighthouse AI",
|
||||
"description": "Endpoints for managing Lighthouse AI configurations, including creation, retrieval, "
|
||||
"updating, and deletion of configurations such as OpenAI keys, models, and business "
|
||||
"context.",
|
||||
},
|
||||
{
|
||||
"name": "Processor",
|
||||
"description": "Endpoints for managing post-processors used to process Prowler findings, including "
|
||||
"registration, configuration, and deletion of post-processing actions.",
|
||||
"name": "SAML",
|
||||
"description": "Endpoints for Single Sign-On authentication management via SAML for seamless user "
|
||||
"authentication.",
|
||||
},
|
||||
]
|
||||
return super().get(request, *args, **kwargs)
|
||||
@@ -745,11 +768,13 @@ 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")
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -767,6 +792,12 @@ class UserViewSet(BaseUserViewset):
|
||||
else:
|
||||
return UserSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
if self.request.user.is_authenticated:
|
||||
context["role"] = get_role(self.request.user)
|
||||
return context
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="me")
|
||||
def me(self, request):
|
||||
user = self.request.user
|
||||
@@ -780,7 +811,9 @@ class UserViewSet(BaseUserViewset):
|
||||
if kwargs["pk"] != str(self.request.user.id):
|
||||
raise ValidationError("Only the current user can be deleted.")
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
user = self.get_object()
|
||||
user.delete(using=MainRouter.admin_db)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
@@ -871,7 +904,11 @@ class UserViewSet(BaseUserViewset):
|
||||
partial_update=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Partially update a user-roles relationship",
|
||||
description="Update the user-roles relationship information without affecting other fields.",
|
||||
description=(
|
||||
"Update the user-roles relationship information without affecting other fields. "
|
||||
"If the update would remove MANAGE_ACCOUNT from the last remaining user in the "
|
||||
"tenant, the API rejects the request with a 400 response."
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship updated successfully"
|
||||
@@ -881,7 +918,12 @@ class UserViewSet(BaseUserViewset):
|
||||
destroy=extend_schema(
|
||||
tags=["User"],
|
||||
summary="Delete a user-roles relationship",
|
||||
description="Remove the user-roles relationship from the system by their ID.",
|
||||
description=(
|
||||
"Remove the user-roles relationship from the system by their ID. If removing "
|
||||
"MANAGE_ACCOUNT would take it away from the last remaining user in the tenant, "
|
||||
"the API rejects the request with a 400 response. Users also cannot delete their "
|
||||
"own role assignments; attempting to do so returns a 400 response."
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
response=None, description="Relationship deleted successfully"
|
||||
@@ -896,11 +938,48 @@ class UserRoleRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
http_method_names = ["post", "patch", "delete"]
|
||||
schema = RelationshipViewSchema()
|
||||
# RBAC required permissions
|
||||
required_permissions = [Permissions.MANAGE_USERS]
|
||||
required_permissions = [Permissions.MANAGE_ACCOUNT]
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""
|
||||
Prevent deleting role relationships if it would leave the tenant with no
|
||||
users having MANAGE_ACCOUNT. Supports deleting specific roles via JSON:API
|
||||
relationship payload or clearing all roles for the user when no payload.
|
||||
"""
|
||||
user = self.get_object()
|
||||
# Disallow deleting own roles
|
||||
if str(user.id) == str(request.user.id):
|
||||
return Response(
|
||||
data={
|
||||
"detail": "Users cannot delete the relationship with their role."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
tenant_id = self.request.tenant_id
|
||||
payload = request.data if isinstance(request.data, dict) else None
|
||||
|
||||
# If a user has more than one role, we will delete the relationship with the roles in the payload
|
||||
data = payload.get("data") if payload else None
|
||||
if data:
|
||||
try:
|
||||
role_ids = [item["id"] for item in data]
|
||||
except KeyError:
|
||||
role_ids = []
|
||||
roles_to_remove = Role.objects.filter(id__in=role_ids, tenant_id=tenant_id)
|
||||
else:
|
||||
roles_to_remove = user.roles.filter(tenant_id=tenant_id)
|
||||
|
||||
UserRoleRelationship.objects.filter(
|
||||
user=user,
|
||||
tenant_id=tenant_id,
|
||||
role_id__in=roles_to_remove.values_list("id", flat=True),
|
||||
).delete()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
|
||||
@@ -939,12 +1018,6 @@ class UserRoleRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
serializer.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
user.roles.clear()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
@@ -2849,13 +2922,11 @@ class InvitationAcceptViewSet(BaseRLSViewSet):
|
||||
partial_update=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Partially update a role",
|
||||
description="Update certain fields of an existing role's information without affecting other fields.",
|
||||
responses={200: RoleSerializer},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Role"],
|
||||
summary="Delete a role",
|
||||
description="Remove a role from the system by their ID.",
|
||||
),
|
||||
)
|
||||
class RoleViewSet(BaseRLSViewSet):
|
||||
@@ -2877,6 +2948,14 @@ class RoleViewSet(BaseRLSViewSet):
|
||||
return RoleUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
@extend_schema(
|
||||
description=(
|
||||
"Update selected fields on an existing role. When changing the `users` "
|
||||
"relationship of a role that grants MANAGE_ACCOUNT, the API blocks attempts "
|
||||
"that would leave the tenant without any MANAGE_ACCOUNT assignees and prevents "
|
||||
"callers from removing their own assignment to that role."
|
||||
)
|
||||
)
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
user_role = get_role(request.user)
|
||||
# If the user is the owner of the role, the manage_account field is not editable
|
||||
@@ -2884,6 +2963,12 @@ class RoleViewSet(BaseRLSViewSet):
|
||||
request.data["manage_account"] = str(user_role.manage_account).lower()
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
description=(
|
||||
"Delete the specified role. The API rejects deletion of the last role "
|
||||
"in the tenant that grants MANAGE_ACCOUNT."
|
||||
)
|
||||
)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if (
|
||||
@@ -2891,6 +2976,21 @@ class RoleViewSet(BaseRLSViewSet):
|
||||
): # TODO: Move to a constant/enum (in case other roles are created by default)
|
||||
raise ValidationError(detail="The admin role cannot be deleted.")
|
||||
|
||||
# Prevent deleting the last MANAGE_ACCOUNT role in the tenant
|
||||
if instance.manage_account:
|
||||
has_other_ma = (
|
||||
Role.objects.filter(tenant_id=instance.tenant_id, manage_account=True)
|
||||
.exclude(id=instance.id)
|
||||
.exists()
|
||||
)
|
||||
if not has_other_ma:
|
||||
return Response(
|
||||
data={
|
||||
"detail": "Cannot delete the only role with MANAGE_ACCOUNT in the tenant."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -3033,7 +3133,9 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
description="Compliance overviews metadata obtained successfully",
|
||||
response=ComplianceOverviewMetadataSerializer,
|
||||
),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
202: OpenApiResponse(
|
||||
description="The task is in progress", response=TaskSerializer
|
||||
),
|
||||
500: OpenApiResponse(
|
||||
description="Compliance overviews generation task failed"
|
||||
),
|
||||
@@ -3065,7 +3167,9 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
description="Compliance requirement details obtained successfully",
|
||||
response=ComplianceOverviewDetailSerializer(many=True),
|
||||
),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
202: OpenApiResponse(
|
||||
description="The task is in progress", response=TaskSerializer
|
||||
),
|
||||
500: OpenApiResponse(
|
||||
description="Compliance overviews generation task failed"
|
||||
),
|
||||
@@ -3443,6 +3547,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
|
||||
),
|
||||
"name": requirement.get("name", ""),
|
||||
"framework": compliance_framework.get("framework", ""),
|
||||
"compliance_name": compliance_framework.get("name", ""),
|
||||
"version": compliance_framework.get("version", ""),
|
||||
"description": requirement.get("description", ""),
|
||||
"attributes": base_attributes,
|
||||
@@ -3526,8 +3631,10 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
def get_filterset_class(self):
|
||||
if self.action == "providers":
|
||||
return None
|
||||
elif self.action in ["findings", "findings_severity"]:
|
||||
elif self.action == "findings":
|
||||
return ScanSummaryFilter
|
||||
elif self.action == "findings_severity":
|
||||
return ScanSummarySeverityFilter
|
||||
elif self.action == "services":
|
||||
return ServiceOverviewFilter
|
||||
return None
|
||||
@@ -3649,7 +3756,12 @@ 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()
|
||||
|
||||
# Load only required fields
|
||||
queryset = self.get_queryset().only(
|
||||
"tenant_id", "scan_id", "severity", "fail", "_pass", "total"
|
||||
)
|
||||
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
@@ -3669,16 +3781,22 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
# The filter will have added a status_count annotation if any status filter was used
|
||||
if "status_count" in filtered_queryset.query.annotations:
|
||||
sum_expression = Sum("status_count")
|
||||
else:
|
||||
sum_expression = Sum("total")
|
||||
|
||||
severity_counts = (
|
||||
filtered_queryset.values("severity")
|
||||
.annotate(count=Sum("total"))
|
||||
.annotate(count=sum_expression)
|
||||
.order_by("severity")
|
||||
)
|
||||
|
||||
severity_data = {sev[0]: 0 for sev in SeverityChoices}
|
||||
|
||||
for item in severity_counts:
|
||||
severity_data[item["severity"]] = item["count"]
|
||||
severity_data.update(
|
||||
{item["severity"]: item["count"] for item in severity_counts}
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(severity_data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -3866,31 +3984,111 @@ class IntegrationViewSet(BaseRLSViewSet):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
dispatches=extend_schema(
|
||||
tags=["Integration"],
|
||||
summary="Send findings to a Jira integration",
|
||||
description="Send a set of filtered findings to the given integration. At least one finding filter must be "
|
||||
"provided.",
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
filters=True,
|
||||
)
|
||||
)
|
||||
class IntegrationJiraViewSet(BaseRLSViewSet):
|
||||
queryset = Finding.all_objects.all()
|
||||
serializer_class = IntegrationJiraDispatchSerializer
|
||||
http_method_names = ["post"]
|
||||
filter_backends = [CustomDjangoFilterBackend]
|
||||
filterset_class = IntegrationJiraFindingsFilter
|
||||
# RBAC required permissions
|
||||
required_permissions = [Permissions.MANAGE_INTEGRATIONS]
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def create(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="POST")
|
||||
|
||||
def get_queryset(self):
|
||||
tenant_id = self.request.tenant_id
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all findings
|
||||
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
|
||||
else:
|
||||
# User lacks permission, filter findings based on provider groups associated with the role
|
||||
queryset = Finding.all_objects.filter(
|
||||
scan__provider__in=get_providers(user_roles)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=["post"], url_name="dispatches")
|
||||
def dispatches(self, request, integration_pk=None):
|
||||
get_object_or_404(Integration, pk=integration_pk)
|
||||
serializer = self.get_serializer(
|
||||
data=request.data, context={"integration_id": integration_pk}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if self.filter_queryset(self.get_queryset()).count() == 0:
|
||||
raise ValidationError(
|
||||
{"findings": "No findings match the provided filters"}
|
||||
)
|
||||
|
||||
finding_ids = [
|
||||
str(finding_id)
|
||||
for finding_id in self.filter_queryset(self.get_queryset()).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
]
|
||||
project_key = serializer.validated_data["project_key"]
|
||||
issue_type = serializer.validated_data["issue_type"]
|
||||
|
||||
with transaction.atomic():
|
||||
task = jira_integration_task.delay(
|
||||
tenant_id=self.request.tenant_id,
|
||||
integration_id=integration_pk,
|
||||
project_key=project_key,
|
||||
issue_type=issue_type,
|
||||
finding_ids=finding_ids,
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": prowler_task.id}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="List all Lighthouse configurations",
|
||||
description="Retrieve a list of all Lighthouse configurations.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="List all Lighthouse AI configurations",
|
||||
description="Retrieve a list of all Lighthouse AI configurations.",
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Create a new Lighthouse configuration",
|
||||
description="Create a new Lighthouse configuration with the specified details.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Create a new Lighthouse AI configuration",
|
||||
description="Create a new Lighthouse AI configuration with the specified details.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Partially update a Lighthouse configuration",
|
||||
description="Update certain fields of an existing Lighthouse configuration.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Partially update a Lighthouse AI configuration",
|
||||
description="Update certain fields of an existing Lighthouse AI configuration.",
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Delete a Lighthouse configuration",
|
||||
description="Remove a Lighthouse configuration by its ID.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Delete a Lighthouse AI configuration",
|
||||
description="Remove a Lighthouse AI configuration by its ID.",
|
||||
),
|
||||
connection=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Check the connection to the OpenAI API",
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse configuration.",
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse AI configuration.",
|
||||
request=None,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
),
|
||||
|
||||
@@ -108,6 +108,13 @@ REST_FRAMEWORK = {
|
||||
),
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json",
|
||||
"JSON_API_UNIFORM_EXCEPTIONS": True,
|
||||
"DEFAULT_THROTTLE_CLASSES": [
|
||||
"rest_framework.throttling.ScopedRateThrottle",
|
||||
],
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"token-obtain": env("DJANGO_THROTTLE_TOKEN_OBTAIN", default=None),
|
||||
"dj_rest_auth": None,
|
||||
},
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
@@ -116,6 +123,9 @@ SPECTACULAR_SETTINGS = {
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
|
||||
],
|
||||
"POSTPROCESSING_HOOKS": [
|
||||
"api.schema_hooks.attach_task_202_examples",
|
||||
],
|
||||
"TITLE": "API Reference - Prowler",
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ IGNORED_EXCEPTIONS = [
|
||||
"AzureClientIdAndClientSecretNotBelongingToTenantIdError",
|
||||
"AzureHTTPResponseError",
|
||||
"Error with credentials provided",
|
||||
# PowerShell Errors in User Authentication
|
||||
"Microsoft Teams User Auth connection failed: Please check your permissions and try again.",
|
||||
"Exchange Online User Auth connection failed: Please check your permissions and try again.",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -191,6 +191,108 @@ def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac_manage_account(django_db_setup, django_db_blocker):
|
||||
"""User with only manage_account permission (no manage_users)."""
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing_manage_account",
|
||||
email="rbac_manage_account@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = Tenant.objects.create(
|
||||
name="Tenant Test Manage Account",
|
||||
)
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
role=Membership.RoleChoices.OWNER,
|
||||
)
|
||||
role = Role.objects.create(
|
||||
name="manage_account",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=False,
|
||||
manage_account=True,
|
||||
manage_billing=False,
|
||||
manage_providers=False,
|
||||
manage_integrations=False,
|
||||
manage_scans=False,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
UserRoleRelationship.objects.create(
|
||||
user=user,
|
||||
role=role,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client_rbac_manage_account(
|
||||
create_test_user_rbac_manage_account, tenants_fixture, client
|
||||
):
|
||||
client.user = create_test_user_rbac_manage_account
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac_manage_account@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def create_test_user_rbac_manage_users_only(django_db_setup, django_db_blocker):
|
||||
"""User with only manage_users permission (no manage_account)."""
|
||||
with django_db_blocker.unblock():
|
||||
user = User.objects.create_user(
|
||||
name="testing_manage_users_only",
|
||||
email="rbac_manage_users_only@rbac.com",
|
||||
password=TEST_PASSWORD,
|
||||
)
|
||||
tenant = Tenant.objects.create(name="Tenant Test Manage Users Only")
|
||||
Membership.objects.create(
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
role=Membership.RoleChoices.OWNER,
|
||||
)
|
||||
role = Role.objects.create(
|
||||
name="manage_users_only",
|
||||
tenant_id=tenant.id,
|
||||
manage_users=True,
|
||||
manage_account=False,
|
||||
manage_billing=False,
|
||||
manage_providers=False,
|
||||
manage_integrations=False,
|
||||
manage_scans=False,
|
||||
unlimited_visibility=False,
|
||||
)
|
||||
UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant.id)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client_rbac_manage_users_only(
|
||||
create_test_user_rbac_manage_users_only, client
|
||||
):
|
||||
client.user = create_test_user_rbac_manage_users_only
|
||||
serializer = TokenSerializer(
|
||||
data={
|
||||
"type": "tokens",
|
||||
"email": "rbac_manage_users_only@rbac.com",
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
access_token = serializer.validated_data["access"]
|
||||
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
|
||||
client.user = create_test_user_rbac
|
||||
|
||||
@@ -8,12 +8,15 @@ from botocore.exceptions import ClientError, NoCredentialsError, ParamValidation
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Scan
|
||||
from prowler.config.config import (
|
||||
csv_file_suffix,
|
||||
html_file_suffix,
|
||||
json_asff_file_suffix,
|
||||
json_ocsf_file_suffix,
|
||||
output_file_timestamp,
|
||||
)
|
||||
from prowler.lib.outputs.asff.asff import ASFF
|
||||
from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import (
|
||||
AWSWellArchitected,
|
||||
)
|
||||
@@ -108,6 +111,7 @@ OUTPUT_FORMATS_MAPPING = {
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}},
|
||||
"json-asff": {"class": ASFF, "suffix": json_asff_file_suffix, "kwargs": {}},
|
||||
"html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}},
|
||||
}
|
||||
|
||||
@@ -248,15 +252,19 @@ def _generate_output_directory(
|
||||
# Sanitize the prowler provider name to ensure it is a valid directory name
|
||||
prowler_provider_sanitized = re.sub(r"[^\w\-]", "-", prowler_provider)
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
started_at = Scan.objects.get(id=scan_id).started_at
|
||||
|
||||
timestamp = started_at.strftime("%Y%m%d%H%M%S")
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{output_file_timestamp}"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
compliance_path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/compliance/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{output_file_timestamp}"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(compliance_path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
|
||||
@@ -2,15 +2,21 @@ import os
|
||||
from glob import glob
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
|
||||
from tasks.utils import batched
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Integration
|
||||
from api.models import Finding, Integration, Provider
|
||||
from api.utils import initialize_prowler_integration, initialize_prowler_provider
|
||||
from prowler.lib.outputs.asff.asff import ASFF
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
from prowler.lib.outputs.csv.csv import CSV
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
from prowler.lib.outputs.html.html import HTML
|
||||
from prowler.lib.outputs.ocsf.ocsf import OCSF
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
from prowler.providers.common.models import Connection
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
@@ -83,17 +89,12 @@ def upload_s3_integration(
|
||||
for integration in integrations:
|
||||
try:
|
||||
connected, s3 = get_s3_client_from_integration(integration)
|
||||
# Since many scans will be send to the same S3 bucket, we need to
|
||||
# add the output directory to the S3 output directory to avoid
|
||||
# overwriting the files and known the scan origin.
|
||||
folder = os.getenv("OUTPUT_DIRECTORY", "/tmp/prowler_api_output")
|
||||
s3._output_directory = (
|
||||
f"{s3._output_directory}{output_directory.split(folder)[-1]}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.info(
|
||||
f"S3 connection failed for integration {integration.id}: {e}"
|
||||
)
|
||||
integration.connected = False
|
||||
integration.save()
|
||||
continue
|
||||
|
||||
if connected:
|
||||
@@ -145,7 +146,7 @@ def upload_s3_integration(
|
||||
integration.connected = False
|
||||
integration.save()
|
||||
logger.error(
|
||||
f"S3 upload failed for integration {integration.id}: {s3.error}"
|
||||
f"S3 upload failed, connection failed for integration {integration.id}: {s3.error}"
|
||||
)
|
||||
|
||||
result = integration_executions == len(integrations)
|
||||
@@ -154,8 +155,352 @@ def upload_s3_integration(
|
||||
f"All the S3 integrations completed successfully for provider {provider_id}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Some S3 integrations failed for provider {provider_id}")
|
||||
logger.info(f"Some S3 integrations failed for provider {provider_id}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"S3 integrations failed for provider {provider_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_security_hub_client_from_integration(
|
||||
integration: Integration, tenant_id: str, findings: list
|
||||
) -> tuple[bool, SecurityHub | Connection]:
|
||||
"""
|
||||
Create and return a SecurityHub client using AWS credentials from an integration.
|
||||
|
||||
Args:
|
||||
integration (Integration): The integration to get the Security Hub client from.
|
||||
tenant_id (str): The tenant identifier.
|
||||
findings (list): List of findings in ASFF format to send to Security Hub.
|
||||
|
||||
Returns:
|
||||
tuple[bool, SecurityHub | Connection]: A tuple containing a boolean indicating
|
||||
if the connection was successful and the SecurityHub client or connection object.
|
||||
"""
|
||||
# Get the provider associated with this integration
|
||||
with rls_transaction(tenant_id):
|
||||
provider_relationship = integration.integrationproviderrelationship_set.first()
|
||||
if not provider_relationship:
|
||||
return Connection(
|
||||
is_connected=False, error="No provider associated with this integration"
|
||||
)
|
||||
provider_uid = provider_relationship.provider.uid
|
||||
provider_secret = provider_relationship.provider.secret.secret
|
||||
|
||||
credentials = (
|
||||
integration.credentials if integration.credentials else provider_secret
|
||||
)
|
||||
connection = SecurityHub.test_connection(
|
||||
aws_account_id=provider_uid,
|
||||
raise_on_exception=False,
|
||||
**credentials,
|
||||
)
|
||||
|
||||
if connection.is_connected:
|
||||
all_security_hub_regions = AwsProvider.get_available_aws_service_regions(
|
||||
"securityhub", connection.partition
|
||||
)
|
||||
|
||||
# Create regions status dictionary
|
||||
regions_status = {}
|
||||
for region in set(all_security_hub_regions):
|
||||
regions_status[region] = region in connection.enabled_regions
|
||||
|
||||
# Save regions information in the integration configuration
|
||||
with rls_transaction(tenant_id):
|
||||
integration.configuration["regions"] = regions_status
|
||||
integration.save()
|
||||
|
||||
# Create SecurityHub client with all necessary parameters
|
||||
security_hub = SecurityHub(
|
||||
aws_account_id=provider_uid,
|
||||
findings=findings,
|
||||
send_only_fails=integration.configuration.get("send_only_fails", False),
|
||||
aws_security_hub_available_regions=list(connection.enabled_regions),
|
||||
**credentials,
|
||||
)
|
||||
return True, security_hub
|
||||
else:
|
||||
# Reset regions information if connection fails
|
||||
with rls_transaction(tenant_id):
|
||||
integration.configuration["regions"] = {}
|
||||
integration.save()
|
||||
|
||||
return False, connection
|
||||
|
||||
|
||||
def upload_security_hub_integration(
|
||||
tenant_id: str, provider_id: str, scan_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
Upload findings to AWS Security Hub using configured integrations.
|
||||
|
||||
This function retrieves findings from the database, transforms them to ASFF format,
|
||||
and sends them to AWS Security Hub using the configured integration credentials.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier.
|
||||
provider_id (str): The provider identifier.
|
||||
scan_id (str): The scan identifier for which to send findings.
|
||||
|
||||
Returns:
|
||||
bool: True if all integrations executed successfully, False otherwise.
|
||||
"""
|
||||
logger.info(f"Processing Security Hub integrations for provider {provider_id}")
|
||||
|
||||
try:
|
||||
with rls_transaction(tenant_id):
|
||||
# Get Security Hub integrations for this provider
|
||||
integrations = list(
|
||||
Integration.objects.filter(
|
||||
integrationproviderrelationship__provider_id=provider_id,
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
|
||||
enabled=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not integrations:
|
||||
logger.error(
|
||||
f"No Security Hub integrations found for provider {provider_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Get the provider object
|
||||
provider = Provider.objects.get(id=provider_id)
|
||||
|
||||
# Initialize prowler provider for finding transformation
|
||||
prowler_provider = initialize_prowler_provider(provider)
|
||||
|
||||
# Process each Security Hub integration
|
||||
integration_executions = 0
|
||||
total_findings_sent = {} # Track findings sent per integration
|
||||
|
||||
for integration in integrations:
|
||||
try:
|
||||
# Initialize Security Hub client for this integration
|
||||
# We'll create the client once and reuse it for all batches
|
||||
security_hub_client = None
|
||||
send_only_fails = integration.configuration.get(
|
||||
"send_only_fails", False
|
||||
)
|
||||
total_findings_sent[integration.id] = 0
|
||||
|
||||
# Process findings in batches to avoid memory issues
|
||||
has_findings = False
|
||||
batch_number = 0
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
qs = (
|
||||
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
|
||||
.order_by("uid")
|
||||
.iterator()
|
||||
)
|
||||
|
||||
for batch, _ in batched(qs, DJANGO_FINDINGS_BATCH_SIZE):
|
||||
batch_number += 1
|
||||
has_findings = True
|
||||
|
||||
# Transform findings for this batch
|
||||
transformed_findings = [
|
||||
FindingOutput.transform_api_finding(
|
||||
finding, prowler_provider
|
||||
)
|
||||
for finding in batch
|
||||
]
|
||||
|
||||
# Convert to ASFF format
|
||||
asff_transformer = ASFF(
|
||||
findings=transformed_findings,
|
||||
file_path="",
|
||||
file_extension="json",
|
||||
)
|
||||
asff_transformer.transform(transformed_findings)
|
||||
|
||||
# Get the batch of ASFF findings
|
||||
batch_asff_findings = asff_transformer.data
|
||||
|
||||
if batch_asff_findings:
|
||||
# Create Security Hub client for first batch or reuse existing
|
||||
if not security_hub_client:
|
||||
connected, security_hub = (
|
||||
get_security_hub_client_from_integration(
|
||||
integration, tenant_id, batch_asff_findings
|
||||
)
|
||||
)
|
||||
|
||||
if not connected:
|
||||
logger.error(
|
||||
f"Security Hub connection failed for integration {integration.id}: "
|
||||
f"{security_hub.error}"
|
||||
)
|
||||
integration.connected = False
|
||||
integration.save()
|
||||
break # Skip this integration
|
||||
|
||||
security_hub_client = security_hub
|
||||
logger.info(
|
||||
f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via "
|
||||
f"integration {integration.id}"
|
||||
)
|
||||
else:
|
||||
# Update findings in existing client for this batch
|
||||
security_hub_client._findings_per_region = (
|
||||
security_hub_client.filter(
|
||||
batch_asff_findings, send_only_fails
|
||||
)
|
||||
)
|
||||
|
||||
# Send this batch to Security Hub
|
||||
try:
|
||||
findings_sent = (
|
||||
security_hub_client.batch_send_to_security_hub()
|
||||
)
|
||||
total_findings_sent[integration.id] += findings_sent
|
||||
|
||||
if findings_sent > 0:
|
||||
logger.debug(
|
||||
f"Sent batch {batch_number} with {findings_sent} findings to Security Hub"
|
||||
)
|
||||
except Exception as batch_error:
|
||||
logger.error(
|
||||
f"Failed to send batch {batch_number} to Security Hub: {str(batch_error)}"
|
||||
)
|
||||
|
||||
# Clear memory after processing each batch
|
||||
asff_transformer._data.clear()
|
||||
del batch_asff_findings
|
||||
del transformed_findings
|
||||
|
||||
if not has_findings:
|
||||
logger.info(
|
||||
f"No findings to send to Security Hub for scan {scan_id}"
|
||||
)
|
||||
integration_executions += 1
|
||||
elif security_hub_client:
|
||||
if total_findings_sent[integration.id] > 0:
|
||||
logger.info(
|
||||
f"Successfully sent {total_findings_sent[integration.id]} total findings to Security Hub via integration {integration.id}"
|
||||
)
|
||||
integration_executions += 1
|
||||
else:
|
||||
logger.warning(
|
||||
f"No findings were sent to Security Hub via integration {integration.id}"
|
||||
)
|
||||
|
||||
# Archive previous findings if configured to do so
|
||||
if integration.configuration.get(
|
||||
"archive_previous_findings", False
|
||||
):
|
||||
logger.info(
|
||||
f"Archiving previous findings in Security Hub via integration {integration.id}"
|
||||
)
|
||||
try:
|
||||
findings_archived = (
|
||||
security_hub_client.archive_previous_findings()
|
||||
)
|
||||
logger.info(
|
||||
f"Successfully archived {findings_archived} previous findings in Security Hub"
|
||||
)
|
||||
except Exception as archive_error:
|
||||
logger.warning(
|
||||
f"Failed to archive previous findings: {str(archive_error)}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Security Hub integration {integration.id} failed: {str(e)}"
|
||||
)
|
||||
continue
|
||||
|
||||
result = integration_executions == len(integrations)
|
||||
if result:
|
||||
logger.info(
|
||||
f"All Security Hub integrations completed successfully for provider {provider_id}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Some Security Hub integrations failed for provider {provider_id}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Security Hub integrations failed for provider {provider_id}: {str(e)}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def send_findings_to_jira(
|
||||
tenant_id: str,
|
||||
integration_id: str,
|
||||
project_key: str,
|
||||
issue_type: str,
|
||||
finding_ids: list[str],
|
||||
):
|
||||
with rls_transaction(tenant_id):
|
||||
integration = Integration.objects.get(id=integration_id)
|
||||
jira_integration = initialize_prowler_integration(integration)
|
||||
|
||||
num_tickets_created = 0
|
||||
for finding_id in finding_ids:
|
||||
with rls_transaction(tenant_id):
|
||||
finding_instance = (
|
||||
Finding.all_objects.select_related("scan__provider")
|
||||
.prefetch_related("resources")
|
||||
.get(id=finding_id)
|
||||
)
|
||||
|
||||
# Extract resource information
|
||||
resource = (
|
||||
finding_instance.resources.first()
|
||||
if finding_instance.resources.exists()
|
||||
else None
|
||||
)
|
||||
resource_uid = resource.uid if resource else ""
|
||||
resource_name = resource.name if resource else ""
|
||||
resource_tags = {}
|
||||
if resource and hasattr(resource, "tags"):
|
||||
resource_tags = resource.get_tags(tenant_id)
|
||||
|
||||
# Get region
|
||||
region = resource.region if resource and resource.region else ""
|
||||
|
||||
# Extract remediation information from check_metadata
|
||||
check_metadata = finding_instance.check_metadata
|
||||
remediation = check_metadata.get("remediation", {})
|
||||
recommendation = remediation.get("recommendation", {})
|
||||
remediation_code = remediation.get("code", {})
|
||||
|
||||
# Send the individual finding to Jira
|
||||
result = jira_integration.send_finding(
|
||||
check_id=finding_instance.check_id,
|
||||
check_title=check_metadata.get("checktitle", ""),
|
||||
severity=finding_instance.severity,
|
||||
status=finding_instance.status,
|
||||
status_extended=finding_instance.status_extended or "",
|
||||
provider=finding_instance.scan.provider.provider,
|
||||
region=region,
|
||||
resource_uid=resource_uid,
|
||||
resource_name=resource_name,
|
||||
risk=check_metadata.get("risk", ""),
|
||||
recommendation_text=recommendation.get("text", ""),
|
||||
recommendation_url=recommendation.get("url", ""),
|
||||
remediation_code_native_iac=remediation_code.get("nativeiac", ""),
|
||||
remediation_code_terraform=remediation_code.get("terraform", ""),
|
||||
remediation_code_cli=remediation_code.get("cli", ""),
|
||||
remediation_code_other=remediation_code.get("other", ""),
|
||||
resource_tags=resource_tags,
|
||||
compliance=finding_instance.compliance or {},
|
||||
project_key=project_key,
|
||||
issue_type=issue_type,
|
||||
)
|
||||
if result:
|
||||
num_tickets_created += 1
|
||||
else:
|
||||
logger.error(f"Failed to send finding {finding_id} to Jira")
|
||||
|
||||
return {
|
||||
"created_count": num_tickets_created,
|
||||
"failed_count": len(finding_ids) - num_tickets_created,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ from tasks.jobs.export import (
|
||||
_generate_output_directory,
|
||||
_upload_to_s3,
|
||||
)
|
||||
from tasks.jobs.integrations import upload_s3_integration
|
||||
from tasks.jobs.integrations import (
|
||||
send_findings_to_jira,
|
||||
upload_s3_integration,
|
||||
upload_security_hub_integration,
|
||||
)
|
||||
from tasks.jobs.scan import (
|
||||
aggregate_findings,
|
||||
create_compliance_requirements,
|
||||
@@ -62,6 +66,7 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str)
|
||||
check_integrations_task.si(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
scan_id=scan_id,
|
||||
),
|
||||
).apply_async()
|
||||
|
||||
@@ -323,12 +328,30 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
|
||||
ScanSummary.objects.filter(scan_id=scan_id)
|
||||
)
|
||||
|
||||
qs = Finding.all_objects.filter(scan_id=scan_id).order_by("uid").iterator()
|
||||
# Check if we need to generate ASFF output for AWS providers with SecurityHub integration
|
||||
generate_asff = False
|
||||
if provider_type == "aws":
|
||||
security_hub_integrations = Integration.objects.filter(
|
||||
integrationproviderrelationship__provider_id=provider_id,
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
|
||||
enabled=True,
|
||||
)
|
||||
generate_asff = security_hub_integrations.exists()
|
||||
|
||||
qs = (
|
||||
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
|
||||
.order_by("uid")
|
||||
.iterator()
|
||||
)
|
||||
for batch, is_last in batched(qs, DJANGO_FINDINGS_BATCH_SIZE):
|
||||
fos = [FindingOutput.transform_api_finding(f, prowler_provider) for f in batch]
|
||||
|
||||
# Outputs
|
||||
for mode, cfg in OUTPUT_FORMATS_MAPPING.items():
|
||||
# Skip ASFF generation if not needed
|
||||
if mode == "json-asff" and not generate_asff:
|
||||
continue
|
||||
|
||||
cls = cfg["class"]
|
||||
suffix = cfg["suffix"]
|
||||
extra = cfg.get("kwargs", {}).copy()
|
||||
@@ -438,7 +461,7 @@ def backfill_scan_resource_summaries_task(tenant_id: str, scan_id: str):
|
||||
return backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="overview")
|
||||
@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="compliance")
|
||||
def create_compliance_requirements_task(tenant_id: str, scan_id: str):
|
||||
"""
|
||||
Creates detailed compliance requirement records for a scan.
|
||||
@@ -474,17 +497,19 @@ def check_lighthouse_connection_task(lighthouse_config_id: str, tenant_id: str =
|
||||
|
||||
|
||||
@shared_task(name="integration-check")
|
||||
def check_integrations_task(tenant_id: str, provider_id: str):
|
||||
def check_integrations_task(tenant_id: str, provider_id: str, scan_id: str = None):
|
||||
"""
|
||||
Check and execute all configured integrations for a provider.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier
|
||||
provider_id (str): The provider identifier
|
||||
scan_id (str, optional): The scan identifier for integrations that need scan data
|
||||
"""
|
||||
logger.info(f"Checking integrations for provider {provider_id}")
|
||||
|
||||
try:
|
||||
integration_tasks = []
|
||||
with rls_transaction(tenant_id):
|
||||
integrations = Integration.objects.filter(
|
||||
integrationproviderrelationship__provider_id=provider_id,
|
||||
@@ -495,7 +520,16 @@ def check_integrations_task(tenant_id: str, provider_id: str):
|
||||
logger.info(f"No integrations configured for provider {provider_id}")
|
||||
return {"integrations_processed": 0}
|
||||
|
||||
integration_tasks = []
|
||||
# Security Hub integration
|
||||
security_hub_integrations = integrations.filter(
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
)
|
||||
if security_hub_integrations.exists():
|
||||
integration_tasks.append(
|
||||
security_hub_integration_task.s(
|
||||
tenant_id=tenant_id, provider_id=provider_id, scan_id=scan_id
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Add other integration types here
|
||||
# slack_integrations = integrations.filter(
|
||||
@@ -541,3 +575,41 @@ def s3_integration_task(
|
||||
output_directory (str): Path to the directory containing output files
|
||||
"""
|
||||
return upload_s3_integration(tenant_id, provider_id, output_directory)
|
||||
|
||||
|
||||
@shared_task(
|
||||
base=RLSTask,
|
||||
name="integration-security-hub",
|
||||
queue="integrations",
|
||||
)
|
||||
def security_hub_integration_task(
|
||||
tenant_id: str,
|
||||
provider_id: str,
|
||||
scan_id: str,
|
||||
):
|
||||
"""
|
||||
Process Security Hub integrations for a provider.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier
|
||||
provider_id (str): The provider identifier
|
||||
scan_id (str): The scan identifier
|
||||
"""
|
||||
return upload_security_hub_integration(tenant_id, provider_id, scan_id)
|
||||
|
||||
|
||||
@shared_task(
|
||||
base=RLSTask,
|
||||
name="integration-jira",
|
||||
queue="integrations",
|
||||
)
|
||||
def jira_integration_task(
|
||||
tenant_id: str,
|
||||
integration_id: str,
|
||||
project_key: str,
|
||||
issue_type: str,
|
||||
finding_ids: list[str],
|
||||
):
|
||||
return send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -127,14 +129,26 @@ class TestOutputs:
|
||||
_upload_to_s3("tenant", str(zip_path), "scan")
|
||||
mock_logger.assert_called()
|
||||
|
||||
def test_generate_output_directory_creates_paths(self, tmpdir):
|
||||
from prowler.config.config import output_file_timestamp
|
||||
@patch("tasks.jobs.export.rls_transaction")
|
||||
@patch("tasks.jobs.export.Scan")
|
||||
def test_generate_output_directory_creates_paths(
|
||||
self, mock_scan, mock_rls_transaction, tmpdir
|
||||
):
|
||||
# Mock the scan object with a started_at timestamp
|
||||
mock_scan_instance = MagicMock()
|
||||
mock_scan_instance.started_at = datetime(2023, 6, 15, 10, 30, 45)
|
||||
mock_scan.objects.get.return_value = mock_scan_instance
|
||||
|
||||
# Mock rls_transaction as a context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
base_tmp = Path(str(tmpdir.mkdir("generate_output")))
|
||||
base_dir = str(base_tmp)
|
||||
tenant_id = "t1"
|
||||
scan_id = "s1"
|
||||
tenant_id = str(uuid.uuid4())
|
||||
scan_id = str(uuid.uuid4())
|
||||
provider = "aws"
|
||||
expected_timestamp = "20230615103045"
|
||||
|
||||
path, compliance = _generate_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id
|
||||
@@ -143,17 +157,29 @@ class TestOutputs:
|
||||
assert os.path.isdir(os.path.dirname(path))
|
||||
assert os.path.isdir(os.path.dirname(compliance))
|
||||
|
||||
assert path.endswith(f"{provider}-{output_file_timestamp}")
|
||||
assert compliance.endswith(f"{provider}-{output_file_timestamp}")
|
||||
assert path.endswith(f"{provider}-{expected_timestamp}")
|
||||
assert compliance.endswith(f"{provider}-{expected_timestamp}")
|
||||
|
||||
def test_generate_output_directory_invalid_character(self, tmpdir):
|
||||
from prowler.config.config import output_file_timestamp
|
||||
@patch("tasks.jobs.export.rls_transaction")
|
||||
@patch("tasks.jobs.export.Scan")
|
||||
def test_generate_output_directory_invalid_character(
|
||||
self, mock_scan, mock_rls_transaction, tmpdir
|
||||
):
|
||||
# Mock the scan object with a started_at timestamp
|
||||
mock_scan_instance = MagicMock()
|
||||
mock_scan_instance.started_at = datetime(2023, 6, 15, 10, 30, 45)
|
||||
mock_scan.objects.get.return_value = mock_scan_instance
|
||||
|
||||
# Mock rls_transaction as a context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
base_tmp = Path(str(tmpdir.mkdir("generate_output")))
|
||||
base_dir = str(base_tmp)
|
||||
tenant_id = "t1"
|
||||
scan_id = "s1"
|
||||
tenant_id = str(uuid.uuid4())
|
||||
scan_id = str(uuid.uuid4())
|
||||
provider = "aws/test@check"
|
||||
expected_timestamp = "20230615103045"
|
||||
|
||||
path, compliance = _generate_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id
|
||||
@@ -162,5 +188,5 @@ class TestOutputs:
|
||||
assert os.path.isdir(os.path.dirname(path))
|
||||
assert os.path.isdir(os.path.dirname(compliance))
|
||||
|
||||
assert path.endswith(f"aws-test-check-{output_file_timestamp}")
|
||||
assert compliance.endswith(f"aws-test-check-{output_file_timestamp}")
|
||||
assert path.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
assert compliance.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
|
||||
@@ -7,6 +7,7 @@ from tasks.tasks import (
|
||||
check_integrations_task,
|
||||
generate_outputs_task,
|
||||
s3_integration_task,
|
||||
security_hub_integration_task,
|
||||
)
|
||||
|
||||
from api.models import Integration
|
||||
@@ -521,31 +522,68 @@ class TestCheckIntegrationsTask:
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
@patch("tasks.tasks.security_hub_integration_task")
|
||||
@patch("tasks.tasks.group")
|
||||
@patch("tasks.tasks.rls_transaction")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
def test_check_integrations_s3_success(
|
||||
self, mock_integration_filter, mock_rls, mock_group
|
||||
def test_check_integrations_security_hub_success(
|
||||
self, mock_integration_filter, mock_rls, mock_group, mock_security_hub_task
|
||||
):
|
||||
# Mock that we have some integrations
|
||||
mock_integration_filter.return_value.exists.return_value = True
|
||||
"""Test that SecurityHub integrations are processed correctly."""
|
||||
# Mock that we have SecurityHub integrations
|
||||
mock_integrations = MagicMock()
|
||||
mock_integrations.exists.return_value = True
|
||||
|
||||
# Mock SecurityHub integrations to return existing integrations
|
||||
mock_security_hub_integrations = MagicMock()
|
||||
mock_security_hub_integrations.exists.return_value = True
|
||||
|
||||
# Set up the filter chain
|
||||
mock_integration_filter.return_value = mock_integrations
|
||||
mock_integrations.filter.return_value = mock_security_hub_integrations
|
||||
|
||||
# Mock the task signature
|
||||
mock_task_signature = MagicMock()
|
||||
mock_security_hub_task.s.return_value = mock_task_signature
|
||||
|
||||
# Mock group job
|
||||
mock_job = MagicMock()
|
||||
mock_group.return_value = mock_job
|
||||
|
||||
# Ensure rls_transaction is mocked
|
||||
mock_rls.return_value.__enter__.return_value = None
|
||||
|
||||
# Since the current implementation doesn't actually create tasks yet (TODO comment),
|
||||
# we test that no tasks are created but the function returns the correct count
|
||||
# Execute the function
|
||||
result = check_integrations_task(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_id=self.provider_id,
|
||||
scan_id="test-scan-id",
|
||||
)
|
||||
|
||||
assert result == {"integrations_processed": 0}
|
||||
# Should process 1 SecurityHub integration
|
||||
assert result == {"integrations_processed": 1}
|
||||
|
||||
# Verify the integration filter was called
|
||||
mock_integration_filter.assert_called_once_with(
|
||||
integrationproviderrelationship__provider_id=self.provider_id,
|
||||
enabled=True,
|
||||
)
|
||||
# group should not be called since no integration tasks are created yet
|
||||
mock_group.assert_not_called()
|
||||
|
||||
# Verify SecurityHub integrations were filtered
|
||||
mock_integrations.filter.assert_called_once_with(
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
)
|
||||
|
||||
# Verify SecurityHub task was created with correct parameters
|
||||
mock_security_hub_task.s.assert_called_once_with(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_id=self.provider_id,
|
||||
scan_id="test-scan-id",
|
||||
)
|
||||
|
||||
# Verify group was called and job was executed
|
||||
mock_group.assert_called_once_with([mock_task_signature])
|
||||
mock_job.apply_async.assert_called_once()
|
||||
|
||||
@patch("tasks.tasks.rls_transaction")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
@@ -567,6 +605,369 @@ class TestCheckIntegrationsTask:
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
@patch("tasks.tasks.s3_integration_task")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@patch("tasks.tasks.Provider.objects.get")
|
||||
@patch("tasks.tasks.initialize_prowler_provider")
|
||||
@patch("tasks.tasks.Compliance.get_bulk")
|
||||
@patch("tasks.tasks.get_compliance_frameworks")
|
||||
@patch("tasks.tasks.Finding.all_objects.filter")
|
||||
@patch("tasks.tasks._generate_output_directory")
|
||||
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
|
||||
@patch("tasks.tasks.FindingOutput.transform_api_finding")
|
||||
@patch("tasks.tasks._compress_output_files")
|
||||
@patch("tasks.tasks._upload_to_s3")
|
||||
@patch("tasks.tasks.Scan.all_objects.filter")
|
||||
@patch("tasks.tasks.rmtree")
|
||||
def test_generate_outputs_with_asff_for_aws_with_security_hub(
|
||||
self,
|
||||
mock_rmtree,
|
||||
mock_scan_update,
|
||||
mock_upload,
|
||||
mock_compress,
|
||||
mock_transform_finding,
|
||||
mock_transform_stats,
|
||||
mock_generate_dir,
|
||||
mock_findings,
|
||||
mock_get_frameworks,
|
||||
mock_compliance_bulk,
|
||||
mock_initialize_provider,
|
||||
mock_provider_get,
|
||||
mock_scan_summary,
|
||||
mock_integration_filter,
|
||||
mock_s3_task,
|
||||
):
|
||||
"""Test that ASFF output is generated for AWS providers with SecurityHub integration."""
|
||||
# Setup
|
||||
mock_scan_summary_qs = MagicMock()
|
||||
mock_scan_summary_qs.exists.return_value = True
|
||||
mock_scan_summary.return_value = mock_scan_summary_qs
|
||||
|
||||
# Mock AWS provider
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.uid = "aws-account-123"
|
||||
mock_provider.provider = "aws"
|
||||
mock_provider_get.return_value = mock_provider
|
||||
|
||||
# Mock SecurityHub integration exists
|
||||
mock_security_hub_integrations = MagicMock()
|
||||
mock_security_hub_integrations.exists.return_value = True
|
||||
mock_integration_filter.return_value = mock_security_hub_integrations
|
||||
|
||||
# Mock s3_integration_task
|
||||
mock_s3_task.apply_async.return_value.get.return_value = True
|
||||
|
||||
# Mock other necessary components
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
mock_finding = MagicMock()
|
||||
mock_findings.return_value.order_by.return_value.iterator.return_value = [
|
||||
[mock_finding],
|
||||
True,
|
||||
]
|
||||
mock_transform_finding.return_value = MagicMock(compliance={})
|
||||
|
||||
# Track which output formats were created
|
||||
created_writers = {}
|
||||
|
||||
def track_writer_creation(cls_type):
|
||||
def factory(*args, **kwargs):
|
||||
writer = MagicMock()
|
||||
writer._data = []
|
||||
writer.transform = MagicMock()
|
||||
writer.batch_write_data_to_file = MagicMock()
|
||||
created_writers[cls_type] = writer
|
||||
return writer
|
||||
|
||||
return factory
|
||||
|
||||
# Mock OUTPUT_FORMATS_MAPPING with tracking
|
||||
with patch(
|
||||
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
|
||||
{
|
||||
"csv": {
|
||||
"class": track_writer_creation("csv"),
|
||||
"suffix": ".csv",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-asff": {
|
||||
"class": track_writer_creation("asff"),
|
||||
"suffix": ".asff.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-ocsf": {
|
||||
"class": track_writer_creation("ocsf"),
|
||||
"suffix": ".ocsf.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
},
|
||||
):
|
||||
mock_compress.return_value = "/tmp/compressed.zip"
|
||||
mock_upload.return_value = "s3://bucket/file.zip"
|
||||
|
||||
# Execute
|
||||
result = generate_outputs_task(
|
||||
scan_id=self.scan_id,
|
||||
provider_id=self.provider_id,
|
||||
tenant_id=self.tenant_id,
|
||||
)
|
||||
|
||||
# Verify ASFF was created for AWS with SecurityHub
|
||||
assert "asff" in created_writers, "ASFF writer should be created"
|
||||
assert "csv" in created_writers, "CSV writer should be created"
|
||||
assert "ocsf" in created_writers, "OCSF writer should be created"
|
||||
|
||||
# Verify SecurityHub integration was checked
|
||||
assert mock_integration_filter.call_count == 2
|
||||
mock_integration_filter.assert_any_call(
|
||||
integrationproviderrelationship__provider_id=self.provider_id,
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
assert result == {"upload": True}
|
||||
|
||||
@patch("tasks.tasks.s3_integration_task")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@patch("tasks.tasks.Provider.objects.get")
|
||||
@patch("tasks.tasks.initialize_prowler_provider")
|
||||
@patch("tasks.tasks.Compliance.get_bulk")
|
||||
@patch("tasks.tasks.get_compliance_frameworks")
|
||||
@patch("tasks.tasks.Finding.all_objects.filter")
|
||||
@patch("tasks.tasks._generate_output_directory")
|
||||
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
|
||||
@patch("tasks.tasks.FindingOutput.transform_api_finding")
|
||||
@patch("tasks.tasks._compress_output_files")
|
||||
@patch("tasks.tasks._upload_to_s3")
|
||||
@patch("tasks.tasks.Scan.all_objects.filter")
|
||||
@patch("tasks.tasks.rmtree")
|
||||
def test_generate_outputs_no_asff_for_aws_without_security_hub(
|
||||
self,
|
||||
mock_rmtree,
|
||||
mock_scan_update,
|
||||
mock_upload,
|
||||
mock_compress,
|
||||
mock_transform_finding,
|
||||
mock_transform_stats,
|
||||
mock_generate_dir,
|
||||
mock_findings,
|
||||
mock_get_frameworks,
|
||||
mock_compliance_bulk,
|
||||
mock_initialize_provider,
|
||||
mock_provider_get,
|
||||
mock_scan_summary,
|
||||
mock_integration_filter,
|
||||
mock_s3_task,
|
||||
):
|
||||
"""Test that ASFF output is NOT generated for AWS providers without SecurityHub integration."""
|
||||
# Setup
|
||||
mock_scan_summary_qs = MagicMock()
|
||||
mock_scan_summary_qs.exists.return_value = True
|
||||
mock_scan_summary.return_value = mock_scan_summary_qs
|
||||
|
||||
# Mock AWS provider
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.uid = "aws-account-123"
|
||||
mock_provider.provider = "aws"
|
||||
mock_provider_get.return_value = mock_provider
|
||||
|
||||
# Mock NO SecurityHub integration
|
||||
mock_security_hub_integrations = MagicMock()
|
||||
mock_security_hub_integrations.exists.return_value = False
|
||||
mock_integration_filter.return_value = mock_security_hub_integrations
|
||||
|
||||
# Mock other necessary components
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
mock_finding = MagicMock()
|
||||
mock_findings.return_value.order_by.return_value.iterator.return_value = [
|
||||
[mock_finding],
|
||||
True,
|
||||
]
|
||||
mock_transform_finding.return_value = MagicMock(compliance={})
|
||||
|
||||
# Track which output formats were created
|
||||
created_writers = {}
|
||||
|
||||
def track_writer_creation(cls_type):
|
||||
def factory(*args, **kwargs):
|
||||
writer = MagicMock()
|
||||
writer._data = []
|
||||
writer.transform = MagicMock()
|
||||
writer.batch_write_data_to_file = MagicMock()
|
||||
created_writers[cls_type] = writer
|
||||
return writer
|
||||
|
||||
return factory
|
||||
|
||||
# Mock OUTPUT_FORMATS_MAPPING with tracking
|
||||
with patch(
|
||||
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
|
||||
{
|
||||
"csv": {
|
||||
"class": track_writer_creation("csv"),
|
||||
"suffix": ".csv",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-asff": {
|
||||
"class": track_writer_creation("asff"),
|
||||
"suffix": ".asff.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-ocsf": {
|
||||
"class": track_writer_creation("ocsf"),
|
||||
"suffix": ".ocsf.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
},
|
||||
):
|
||||
mock_compress.return_value = "/tmp/compressed.zip"
|
||||
mock_upload.return_value = "s3://bucket/file.zip"
|
||||
|
||||
# Execute
|
||||
result = generate_outputs_task(
|
||||
scan_id=self.scan_id,
|
||||
provider_id=self.provider_id,
|
||||
tenant_id=self.tenant_id,
|
||||
)
|
||||
|
||||
# Verify ASFF was NOT created when no SecurityHub integration
|
||||
assert "asff" not in created_writers, "ASFF writer should NOT be created"
|
||||
assert "csv" in created_writers, "CSV writer should be created"
|
||||
assert "ocsf" in created_writers, "OCSF writer should be created"
|
||||
|
||||
# Verify SecurityHub integration was checked
|
||||
assert mock_integration_filter.call_count == 2
|
||||
mock_integration_filter.assert_any_call(
|
||||
integrationproviderrelationship__provider_id=self.provider_id,
|
||||
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
assert result == {"upload": True}
|
||||
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@patch("tasks.tasks.Provider.objects.get")
|
||||
@patch("tasks.tasks.initialize_prowler_provider")
|
||||
@patch("tasks.tasks.Compliance.get_bulk")
|
||||
@patch("tasks.tasks.get_compliance_frameworks")
|
||||
@patch("tasks.tasks.Finding.all_objects.filter")
|
||||
@patch("tasks.tasks._generate_output_directory")
|
||||
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
|
||||
@patch("tasks.tasks.FindingOutput.transform_api_finding")
|
||||
@patch("tasks.tasks._compress_output_files")
|
||||
@patch("tasks.tasks._upload_to_s3")
|
||||
@patch("tasks.tasks.Scan.all_objects.filter")
|
||||
@patch("tasks.tasks.rmtree")
|
||||
def test_generate_outputs_no_asff_for_non_aws_provider(
|
||||
self,
|
||||
mock_rmtree,
|
||||
mock_scan_update,
|
||||
mock_upload,
|
||||
mock_compress,
|
||||
mock_transform_finding,
|
||||
mock_transform_stats,
|
||||
mock_generate_dir,
|
||||
mock_findings,
|
||||
mock_get_frameworks,
|
||||
mock_compliance_bulk,
|
||||
mock_initialize_provider,
|
||||
mock_provider_get,
|
||||
mock_scan_summary,
|
||||
):
|
||||
"""Test that ASFF output is NOT generated for non-AWS providers (e.g., Azure, GCP)."""
|
||||
# Setup
|
||||
mock_scan_summary_qs = MagicMock()
|
||||
mock_scan_summary_qs.exists.return_value = True
|
||||
mock_scan_summary.return_value = mock_scan_summary_qs
|
||||
|
||||
# Mock Azure provider (non-AWS)
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.uid = "azure-subscription-123"
|
||||
mock_provider.provider = "azure" # Non-AWS provider
|
||||
mock_provider_get.return_value = mock_provider
|
||||
|
||||
# Mock other necessary components
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
mock_finding = MagicMock()
|
||||
mock_findings.return_value.order_by.return_value.iterator.return_value = [
|
||||
[mock_finding],
|
||||
True,
|
||||
]
|
||||
mock_transform_finding.return_value = MagicMock(compliance={})
|
||||
|
||||
# Track which output formats were created
|
||||
created_writers = {}
|
||||
|
||||
def track_writer_creation(cls_type):
|
||||
def factory(*args, **kwargs):
|
||||
writer = MagicMock()
|
||||
writer._data = []
|
||||
writer.transform = MagicMock()
|
||||
writer.batch_write_data_to_file = MagicMock()
|
||||
created_writers[cls_type] = writer
|
||||
return writer
|
||||
|
||||
return factory
|
||||
|
||||
# Mock OUTPUT_FORMATS_MAPPING with tracking
|
||||
with patch(
|
||||
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
|
||||
{
|
||||
"csv": {
|
||||
"class": track_writer_creation("csv"),
|
||||
"suffix": ".csv",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-asff": {
|
||||
"class": track_writer_creation("asff"),
|
||||
"suffix": ".asff.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
"json-ocsf": {
|
||||
"class": track_writer_creation("ocsf"),
|
||||
"suffix": ".ocsf.json",
|
||||
"kwargs": {},
|
||||
},
|
||||
},
|
||||
):
|
||||
mock_compress.return_value = "/tmp/compressed.zip"
|
||||
mock_upload.return_value = "s3://bucket/file.zip"
|
||||
|
||||
# Execute
|
||||
result = generate_outputs_task(
|
||||
scan_id=self.scan_id,
|
||||
provider_id=self.provider_id,
|
||||
tenant_id=self.tenant_id,
|
||||
)
|
||||
|
||||
# Verify ASFF was NOT created for non-AWS provider
|
||||
assert (
|
||||
"asff" not in created_writers
|
||||
), "ASFF writer should NOT be created for non-AWS providers"
|
||||
assert "csv" in created_writers, "CSV writer should be created"
|
||||
assert "ocsf" in created_writers, "OCSF writer should be created"
|
||||
|
||||
assert result == {"upload": True}
|
||||
|
||||
@patch("tasks.tasks.upload_s3_integration")
|
||||
def test_s3_integration_task_success(self, mock_upload):
|
||||
mock_upload.return_value = True
|
||||
@@ -598,3 +999,33 @@ class TestCheckIntegrationsTask:
|
||||
mock_upload.assert_called_once_with(
|
||||
self.tenant_id, self.provider_id, output_directory
|
||||
)
|
||||
|
||||
@patch("tasks.tasks.upload_security_hub_integration")
|
||||
def test_security_hub_integration_task_success(self, mock_upload):
|
||||
"""Test successful SecurityHub integration task execution."""
|
||||
mock_upload.return_value = True
|
||||
scan_id = "test-scan-123"
|
||||
|
||||
result = security_hub_integration_task(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_id=self.provider_id,
|
||||
scan_id=scan_id,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id)
|
||||
|
||||
@patch("tasks.tasks.upload_security_hub_integration")
|
||||
def test_security_hub_integration_task_failure(self, mock_upload):
|
||||
"""Test SecurityHub integration task handling failure."""
|
||||
mock_upload.return_value = False
|
||||
scan_id = "test-scan-123"
|
||||
|
||||
result = security_hub_integration_task(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_id=self.provider_id,
|
||||
scan_id=scan_id,
|
||||
)
|
||||
|
||||
assert result is False
|
||||
mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* Override Tailwind CSS reset for markdown content */
|
||||
.markdown-content ul {
|
||||
list-style: disc !important;
|
||||
margin-left: 20px !important;
|
||||
padding-left: 10px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
list-style: decimal !important;
|
||||
margin-left: 20px !important;
|
||||
padding-left: 10px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 4px !important;
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Ensure nested lists work properly */
|
||||
.markdown-content ul ul {
|
||||
margin-top: 4px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.markdown-content ol ol {
|
||||
margin-top: 4px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
@@ -1654,6 +1654,39 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
# Description as first details item
|
||||
html.Div(
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Description: ",
|
||||
style={
|
||||
"margin-bottom": "8px"
|
||||
},
|
||||
)
|
||||
),
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get(
|
||||
"DESCRIPTION",
|
||||
"",
|
||||
)
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
html.P(
|
||||
@@ -1793,19 +1826,27 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Risk: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
style={},
|
||||
)
|
||||
),
|
||||
html.P(
|
||||
str(data.get("RISK", "")),
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get("RISK", "")
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "5px"
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"display": "flex"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
@@ -1847,23 +1888,32 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
html.Strong(
|
||||
"Recommendation: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
"margin-bottom": "8px"
|
||||
},
|
||||
)
|
||||
),
|
||||
html.P(
|
||||
str(
|
||||
data.get(
|
||||
"REMEDIATION_RECOMMENDATION_TEXT",
|
||||
"",
|
||||
)
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get(
|
||||
"REMEDIATION_RECOMMENDATION_TEXT",
|
||||
"",
|
||||
)
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "5px"
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"display": "flex"},
|
||||
style={"margin-bottom": "15px"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
|
||||
@@ -14,9 +14,11 @@ services:
|
||||
ports:
|
||||
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
|
||||
volumes:
|
||||
- "./api/src/backend:/home/prowler/backend"
|
||||
- "./api/pyproject.toml:/home/prowler/pyproject.toml"
|
||||
- "outputs:/tmp/prowler_api_output"
|
||||
- ./api/src/backend:/home/prowler/backend
|
||||
- ./api/pyproject.toml:/home/prowler/pyproject.toml
|
||||
- ./api/docker-entrypoint.sh:/home/prowler/docker-entrypoint.sh
|
||||
- ./_data/api:/home/prowler/.config/prowler-api
|
||||
- outputs:/tmp/prowler_api_output
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -64,7 +66,7 @@ services:
|
||||
image: valkey/valkey:7-alpine3.19
|
||||
hostname: "valkey"
|
||||
volumes:
|
||||
- ./api/_data/valkey:/data
|
||||
- ./_data/valkey:/data
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
|
||||
@@ -8,7 +8,8 @@ services:
|
||||
ports:
|
||||
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
|
||||
volumes:
|
||||
- "output:/tmp/prowler_api_output"
|
||||
- ./_data/api:/home/prowler/.config/prowler-api
|
||||
- output:/tmp/prowler_api_output
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
# About
|
||||
|
||||
## Author
|
||||
Prowler was created by **Toni de la Fuente** in 2016.
|
||||
|
||||
| <br>[](https://twitter.com/toniblyx) [](https://twitter.com/prowlercloud)|
|
||||
|:--:|
|
||||
| <b>Toni de la Fuente </b>|
|
||||
|
||||
## Maintainers
|
||||
Prowler is maintained by the Engineers of the **Prowler Team** :
|
||||
|
||||
| [](https://twitter.com/NachoRivCor) | [](https://twitter.com/sergargar1) |[](https://twitter.com/jfagoagas) |
|
||||
|:--:|:--:|:--:
|
||||
| <b>Nacho Rivera</b>| <b>Sergio Garcia</b>| <b>Pepe Fagoaga</b>|
|
||||
|
||||
## License
|
||||
|
||||
Prowler is licensed as **Apache License 2.0** as specified in each file. You may obtain a copy of the License at
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>
|
||||
@@ -0,0 +1,61 @@
|
||||
## Access Prowler App
|
||||
|
||||
After [installation](../installation/prowler-app.md), navigate to [http://localhost:3000](http://localhost:3000) and sign up with email and password.
|
||||
|
||||
<img src="../../img/sign-up-button.png" alt="Sign Up Button" width="320"/>
|
||||
<img src="../../img/sign-up.png" alt="Sign Up" width="285"/>
|
||||
|
||||
???+ note "User creation and default tenant behavior"
|
||||
|
||||
When creating a new user, the behavior depends on whether an invitation is provided:
|
||||
|
||||
- **Without an invitation**:
|
||||
|
||||
- A new tenant is automatically created.
|
||||
- The new user is assigned to this tenant.
|
||||
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant.
|
||||
|
||||
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
|
||||
|
||||
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
|
||||
|
||||
## Log In
|
||||
|
||||
Access Prowler App by logging in with **email and password**.
|
||||
|
||||
<img src="../../img/log-in.png" alt="Log In" width="285"/>
|
||||
|
||||
## Add Cloud Provider
|
||||
|
||||
Configure a cloud provider for scanning:
|
||||
|
||||
1. Navigate to `Settings > Cloud Providers` and click `Add Account`.
|
||||
2. Select the cloud provider.
|
||||
3. Enter the provider's identifier (Optional: Add an alias):
|
||||
- **AWS**: Account ID
|
||||
- **GCP**: Project ID
|
||||
- **Azure**: Subscription ID
|
||||
- **Kubernetes**: Cluster ID
|
||||
- **M365**: Domain ID
|
||||
4. Follow the guided instructions to add and authenticate your credentials.
|
||||
|
||||
## Start a Scan
|
||||
|
||||
Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment.
|
||||
|
||||
Click `Go to Scans` to monitor progress.
|
||||
|
||||
## View Results
|
||||
|
||||
Review findings during scan execution in the following sections:
|
||||
|
||||
- **Overview** – Provides a high-level summary of your scans.
|
||||
<img src="../../products/img/overview.png" alt="Overview" width="700"/>
|
||||
|
||||
- **Compliance** – Displays compliance insights based on security frameworks.
|
||||
<img src="../../img/compliance.png" alt="Compliance" width="700"/>
|
||||
|
||||
> For detailed usage instructions, refer to the [Prowler App Guide](../tutorials/prowler-app.md).
|
||||
|
||||
???+ note
|
||||
Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored.
|
||||
@@ -0,0 +1,282 @@
|
||||
## Running Prowler
|
||||
|
||||
Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
|
||||
|
||||
???+ note
|
||||
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
???+ note
|
||||
Running the `prowler` command without options will uses environment variable credentials. Refer to the Authentication section of each provider for credential configuration details.
|
||||
|
||||
## Verbose Output
|
||||
|
||||
If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled.
|
||||
|
||||
## Report Generation
|
||||
|
||||
By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`:
|
||||
|
||||
```console
|
||||
prowler <provider> -M csv json-asff json-ocsf html
|
||||
```
|
||||
The HTML report is saved in the output directory, alongside other reports. It will look like this:
|
||||
|
||||

|
||||
|
||||
## Listing Available Checks and Services
|
||||
|
||||
List all available checks or services within a provider using `-l`/`--list-checks` or `--list-services`.
|
||||
|
||||
```console
|
||||
prowler <provider> --list-checks
|
||||
prowler <provider> --list-services
|
||||
```
|
||||
## Running Specific Checks or Services
|
||||
|
||||
Execute specific checks or services using `-c`/`checks` or `-s`/`services`:
|
||||
|
||||
```console
|
||||
prowler azure --checks storage_blob_public_access_level_is_disabled
|
||||
prowler aws --services s3 ec2
|
||||
prowler gcp --services iam compute
|
||||
prowler kubernetes --services etcd apiserver
|
||||
```
|
||||
## Excluding Checks and Services
|
||||
|
||||
Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`:
|
||||
|
||||
```console
|
||||
prowler aws --excluded-checks s3_bucket_public_access
|
||||
prowler azure --excluded-services defender iam
|
||||
prowler gcp --excluded-services kms
|
||||
prowler kubernetes --excluded-services controllermanager
|
||||
```
|
||||
## Additional Options
|
||||
|
||||
Explore more advanced time-saving execution methods in the [Miscellaneous](../tutorials/misc.md) section.
|
||||
|
||||
Access the help menu and view all available options with `-h`/`--help`:
|
||||
|
||||
```console
|
||||
prowler --help
|
||||
```
|
||||
|
||||
## AWS
|
||||
|
||||
Use a custom AWS profile with `-p`/`--profile` and/or specific AWS regions with `-f`/`--filter-region`:
|
||||
|
||||
```console
|
||||
prowler aws --profile custom-profile -f us-east-1 eu-south-2
|
||||
```
|
||||
|
||||
???+ note
|
||||
By default, `prowler` will scan all AWS regions.
|
||||
|
||||
See more details about AWS Authentication in the [Authentication Section](../tutorials/aws/authentication.md) section.
|
||||
|
||||
## Azure
|
||||
|
||||
Azure requires specifying the auth method:
|
||||
|
||||
```console
|
||||
# To use service principal authentication
|
||||
prowler azure --sp-env-auth
|
||||
|
||||
# To use az cli authentication
|
||||
prowler azure --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler azure --browser-auth --tenant-id "XXXXXXXX"
|
||||
|
||||
# To use managed identity auth
|
||||
prowler azure --managed-identity-auth
|
||||
```
|
||||
|
||||
See more details about Azure Authentication in the [Authentication Section](../tutorials/azure/authentication.md)
|
||||
|
||||
By default, Prowler scans all accessible subscriptions. Scan specific subscriptions using the following flag (using az cli auth as example):
|
||||
|
||||
```console
|
||||
prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription ID 2> ... <subscription ID N>
|
||||
```
|
||||
## Google Cloud
|
||||
|
||||
- **User Account Credentials**
|
||||
|
||||
By default, Prowler uses **User Account credentials**. Configure accounts using:
|
||||
|
||||
- `gcloud init` – Set up a new account.
|
||||
- `gcloud config set account <account>` – Switch to an existing account.
|
||||
|
||||
Once configured, obtain access credentials using: `gcloud auth application-default login`.
|
||||
|
||||
- **Service Account Authentication**
|
||||
|
||||
Alternatively, you can use Service Account credentials:
|
||||
|
||||
Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details.
|
||||
|
||||
Provide the key file location using this argument:
|
||||
|
||||
```console
|
||||
prowler gcp --credentials-file path
|
||||
```
|
||||
|
||||
- **Scanning Specific GCP Projects**
|
||||
|
||||
By default, Prowler scans all accessible GCP projects. Scan specific projects with the `--project-ids` flag:
|
||||
|
||||
```console
|
||||
prowler gcp --project-ids <Project ID 1> <Project ID 2> ... <Project ID N>
|
||||
```
|
||||
|
||||
- **GCP Retry Configuration**
|
||||
|
||||
Configure the maximum number of retry attempts for Google Cloud SDK API calls with the `--gcp-retries-max-attempts` flag:
|
||||
|
||||
```console
|
||||
prowler gcp --gcp-retries-max-attempts 5
|
||||
```
|
||||
|
||||
This is useful when experiencing quota exceeded errors (HTTP 429) to increase the number of automatic retry attempts.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution.
|
||||
|
||||
- **Non In-Cluster Execution**
|
||||
|
||||
```console
|
||||
prowler kubernetes --kubeconfig-file path
|
||||
```
|
||||
???+ note
|
||||
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
|
||||
|
||||
- **In-Cluster Execution**
|
||||
|
||||
To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace:
|
||||
|
||||
```console
|
||||
kubectl apply -f kubernetes/prowler-sa.yaml
|
||||
kubectl apply -f kubernetes/job.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
|
||||
kubectl logs prowler-XXXXX --namespace prowler-ns
|
||||
```
|
||||
|
||||
???+ note
|
||||
By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
|
||||
|
||||
## Microsoft 365
|
||||
|
||||
Microsoft 365 requires specifying the auth method:
|
||||
|
||||
```console
|
||||
|
||||
# To use service principal authentication for MSGraph and PowerShell modules
|
||||
prowler m365 --sp-env-auth
|
||||
|
||||
# To use both service principal (for MSGraph) and user credentials (for PowerShell modules)
|
||||
prowler m365 --env-auth
|
||||
|
||||
# To use az cli authentication
|
||||
prowler m365 --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler m365 --browser-auth --tenant-id "XXXXXXXX"
|
||||
|
||||
```
|
||||
|
||||
See more details about M365 Authentication in the [Authentication Section](../tutorials/microsoft365/authentication.md) section.
|
||||
|
||||
## GitHub
|
||||
|
||||
Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**.
|
||||
|
||||
- **Supported Authentication Methods**
|
||||
|
||||
Authenticate using one of the following methods:
|
||||
|
||||
```console
|
||||
# Personal Access Token (PAT):
|
||||
prowler github --personal-access-token pat
|
||||
|
||||
# OAuth App Token:
|
||||
prowler github --oauth-app-token oauth_token
|
||||
|
||||
# GitHub App Credentials:
|
||||
prowler github --github-app-id app_id --github-app-key app_key
|
||||
```
|
||||
|
||||
???+ note
|
||||
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
|
||||
|
||||
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
|
||||
2. `OAUTH_APP_TOKEN`
|
||||
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
|
||||
|
||||
## Infrastructure as Code (IaC)
|
||||
|
||||
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
|
||||
|
||||
```console
|
||||
# Scan a directory for IaC files
|
||||
prowler iac --scan-path ./my-iac-directory
|
||||
|
||||
# Scan a remote GitHub repository (public or private)
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git
|
||||
|
||||
# Authenticate to a private repo with GitHub username and PAT
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git \
|
||||
--github-username <username> --personal-access-token <token>
|
||||
|
||||
# Authenticate to a private repo with OAuth App Token
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git \
|
||||
--oauth-app-token <oauth_token>
|
||||
|
||||
# Specify frameworks to scan (default: all)
|
||||
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
|
||||
|
||||
# Exclude specific paths
|
||||
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
|
||||
```
|
||||
|
||||
???+ note
|
||||
- `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time.
|
||||
- For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence.
|
||||
- The IaC provider does not require cloud authentication for local scans.
|
||||
- It is ideal for CI/CD pipelines and local development environments.
|
||||
- For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/)
|
||||
|
||||
See more details about IaC scanning in the [IaC Tutorial](../tutorials/iac/getting-started-iac.md) section.
|
||||
|
||||
## MongoDB Atlas
|
||||
|
||||
Prowler allows you to scan your MongoDB Atlas cloud database deployments for security and compliance issues.
|
||||
|
||||
Authentication is done using MongoDB Atlas API key pairs:
|
||||
|
||||
```console
|
||||
# Using command-line arguments
|
||||
prowler mongodbatlas --atlas-public-key <public_key> --atlas-private-key <private_key>
|
||||
|
||||
# Using environment variables
|
||||
export ATLAS_PUBLIC_KEY=<public_key>
|
||||
export ATLAS_PRIVATE_KEY=<private_key>
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
You can filter scans to specific organizations or projects:
|
||||
|
||||
```console
|
||||
# Scan specific project
|
||||
prowler mongodbatlas --atlas-project-id <project_id>
|
||||
```
|
||||
|
||||
See more details about MongoDB Atlas Authentication in [MongoDB Atlas Authentication](../tutorials/mongodbatlas/authentication.md)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In this page you can find all the details about [Amazon Web Services (AWS)](https://aws.amazon.com/) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [getting started](../index.md#aws) page.
|
||||
By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [AWS getting started guide](../tutorials/aws/getting-started-aws.md).
|
||||
|
||||
## AWS Provider Classes Architecture
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In this page you can find all the details about [Microsoft Azure](https://azure.microsoft.com/) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [getting started](../index.md#azure) page.
|
||||
By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [Azure getting started guide](../tutorials/azure/getting-started-azure.md).
|
||||
|
||||
## Azure Provider Classes Architecture
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Check Metadata Guidelines
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide provides comprehensive guidelines for creating check metadata in Prowler. For basic information on check metadata structure, refer to the [check metadata](./checks.md#metadata-structure-for-prowler-checks) section.
|
||||
|
||||
## Check Title Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Determine Resource Finding Scope (Singular vs. Plural)**:
|
||||
When determining whether to use singular or plural in the check title, examine the code for certain patterns. If the code contains a loop that generates an individual report for each resource, use the singular form. If the code produces a single report that covers all resources collectively, use the plural form. For organization- or account-wide checks, select the scope that best matches the breadth of the evaluation. Additionally, review the `status_extended` field messages in the code, as they often provide clues about whether the check is scoped to individual resources or to groups of resources.
|
||||
Analyze the detection code to determine if the check reports on individual resources or aggregated resources:
|
||||
- **Singular**: Use when the check creates one report per resource (e.g., "EC2 instance has IMDSv2 enforced", "S3 bucket does not allow public write access").
|
||||
- **Plural**: Use when the check creates one report for all resources together (e.g., "All EC2 instances have IMDSv2 enforced", "S3 buckets do not allow public write access").
|
||||
2. **Describe the Compliant (*PASS*) State**:
|
||||
Always write the title to describe the **desired, compliant state** of the resources. The title should reflect what it looks like when the audited resource is following the check's requirements.
|
||||
3. **Be Specific and Factual**:
|
||||
Include the exact secure configuration being verified. Avoid vague or generic terms like "properly configured".
|
||||
4. **Avoid Redundant or Action Words**:
|
||||
Do not include verbs like "Check", "Verify", "Ensure", or "Monitor". The title is a declarative statement of the secure condition.
|
||||
5. **Length Limit**:
|
||||
Keep the title under 150 characters.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- Starting with verbs like "Check", "Verify", "Ensure", "Make sure". Always start with the affected resource instead.
|
||||
- Being too vague or generic (e.g., "Ensure security groups are properly configured", what does it mean? "properly configured" is not a clear description of the compliant state).
|
||||
- Focusing on the non-compliant state instead of the compliant state.
|
||||
- Using unclear scope and resource identification.
|
||||
|
||||
## Check Type Guidelines (AWS Only)
|
||||
|
||||
### AWS Security Hub Type Format
|
||||
|
||||
AWS Security Hub uses a three-part type taxonomy:
|
||||
|
||||
- **Namespace**: The top-level security domain.
|
||||
- **Category**: The security control family or area.
|
||||
- **Classifier**: The specific security concern (optional).
|
||||
|
||||
A partial path may be defined (e.g., `TTPs` or `TTPs/Defense Evasion` are valid).
|
||||
|
||||
### Selection Guidelines
|
||||
|
||||
1. **Be Specific**: Use the most specific classifier that accurately describes the check.
|
||||
2. **Standard Compliance**: Consider if the check relates to specific compliance standards.
|
||||
3. **Multiple Types**: You can specify multiple types if the check addresses multiple concerns.
|
||||
|
||||
## Description Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Focus on the Finding**: All fields should address how the finding affects the security posture, rather than the control itself.
|
||||
2. **Use Natural Language**: Write in simple, clear paragraphs with complete, grammatically correct sentences.
|
||||
3. **Use Markdown Formatting**: Enhance readability with:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
4. **Be Concise**: Maximum 400 characters (spaces count). Every word should add value.
|
||||
5. **Explain What the Finding Means**: Focus on what the security control evaluates and what it means when it passes or fails, but without explicitly stating the pass or fail state.
|
||||
6. **Be Technical but Clear**: Use appropriate technical terminology while remaining understandable.
|
||||
7. **Avoid Risk Descriptions**: Do not describe potential risks, threats, or consequences.
|
||||
8. **CheckTitle and Description can be the same**: If the check is very simple and the title is already clear, you can use the same text for the description.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Technical Implementation Details**: "The control loops through all instances and calls the describe_instances API...".
|
||||
- **Vague Descriptions**: "This control verifies proper configuration of resources". What does it mean? "proper configuration" is not a clear description of the compliant state.
|
||||
- **Risk Descriptions**: "This could lead to data breaches" or "This poses a security threat".
|
||||
- **Starting with Verbs**: "Check if...", "Verify...", "Ensure...". Always start with the affected resource instead.
|
||||
- **References to Pass/Fail States**: Avoid using words like "pass" or "fail".
|
||||
|
||||
## Risk Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Explain the Cybersecurity Impact**: Focus on how the finding affects confidentiality, integrity, or availability (CIA triad). If the CIA triad does not apply, explain the risk in terms of the organization's business objectives.
|
||||
2. **Be Specific About Threats**: Clearly state what could happen if this security control is not in place. What attacks or incidents become possible?
|
||||
3. **Focus on Risk Context**: Explain the specific security implications of the finding, not just generic security risks.
|
||||
4. **Use Markdown Formatting**: Enhance readability with markdown formatting:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
5. **Be Concise**: Maximum 400 characters. Make every word count.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Generic Risks**: "This could lead to security issues" or "Regulatory compliance violations".
|
||||
- **Technical Implementation Focus**: "The API call might fail and return incorrect results...".
|
||||
- **Overly Broad Statements**: "This is a serious security risk that could impact everything".
|
||||
- **Vague Threats**: "This could be exploited by threat actors" without explaining how.
|
||||
|
||||
## Recommendation Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Provide Actionable Best Practice Guidance**: Explain what should be done to maintain security posture. Focus on preventive measures and proactive security practices.
|
||||
2. **Be Principle-Based**: Reference established security principles (least privilege, defense in depth, zero trust, separation of duties) where applicable.
|
||||
3. **Focus on Prevention**: Explain best practices that prevent the security issue from occurring, not just detection or remediation.
|
||||
4. **Use Markdown Formatting**: Enhance readability with markdown formatting:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
5. **Be Concise**: Maximum 400 characters.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Specific Remediation Steps**: "1. Go to the console\n2. Click on settings..." - Focus on principles, not click-by-click instructions.
|
||||
- **Implementation Details**: "Configure the JSON policy with the following IAM actions..." - Explain what to achieve, not how.
|
||||
- **Vague Guidance**: "Follow security best practices..." without explaining what those practices are.
|
||||
- **Resource-Specific Recommendations**: "Enable MFA on user john.doe@example.com" - Keep it general.
|
||||
- **Missing Context**: Not explaining why the best practice is important for security.
|
||||
|
||||
### Good Examples
|
||||
|
||||
- *"Avoid exposing sensitive resources directly to the Internet; configure access controls to limit exposure."*
|
||||
- *"Apply the principle of least privilege when assigning permissions to users and services."*
|
||||
- *"Regularly review and update your security configurations to align with current best practices."*
|
||||
|
||||
## Remediation Code Guidelines
|
||||
|
||||
### Critical Requirement
|
||||
|
||||
The **fundamental principle** is to focus on the **specific change** that converts the finding from non-compliant to compliant.
|
||||
|
||||
Also is important to keep all code examples as short as possible, including the essential code to fix the issue. Remove any extra configuration, optional parameters, or nice-to-have settings and add comments to explain the code when possible.
|
||||
|
||||
### Common Guidelines for All Code Fields
|
||||
|
||||
1. **Be Minimal**: Keep code blocks as short as possible - only include what is absolutely necessary.
|
||||
2. **Focus on the Fix**: Remove any extra configuration, optional parameters, or nice-to-have settings.
|
||||
3. **Be Accurate**: Ensure all commands and code are syntactically correct.
|
||||
4. **Use Markdown Formatting**: Format code properly using code blocks and appropriate syntax highlighting.
|
||||
5. **Follow Best Practices**: Use the most secure and recommended approaches for each platform.
|
||||
|
||||
### CLI Guidelines
|
||||
|
||||
- Only provide a single command that directly changes the finding from fail to pass.
|
||||
- The command must be executable as-is and resolve the security issue completely.
|
||||
- Use proper command syntax for the provider (AWS CLI, Azure CLI, gcloud, kubectl, etc.).
|
||||
- Do not use markdown formatting or code blocks - just the raw command.
|
||||
- Do not include multiple commands, comments, or explanations.
|
||||
- If the issue cannot be resolved with a single command, leave this field empty.
|
||||
|
||||
### Native IaC Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the specific resource/configuration that fixes the security issue.
|
||||
- Format as markdown code blocks with proper syntax highlighting.
|
||||
- Include only the required properties to fix the issue.
|
||||
- Add comments indicating the critical line(s) that remediate the check.
|
||||
- Use `example_resource` as the generic name for all resources and IDs.
|
||||
|
||||
### Terraform Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the specific resource/configuration that fixes the security issue.
|
||||
- Provide valid HCL (HashiCorp Configuration Language) code with an example of a compliant configuration.
|
||||
- Use the latest Terraform syntax and provider versions.
|
||||
- Include only the required arguments to fix the issue - skip optional parameters.
|
||||
- Format as markdown code blocks with `hcl` syntax highlighting.
|
||||
- Add comments indicating the critical line(s) that remediate the check.
|
||||
- Use `example_resource` as the generic name for all resources and IDs.
|
||||
- Skip provider requirements unless critical for the fix.
|
||||
|
||||
### Other (Manual Steps) Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the exact steps needed to fix the security issue.
|
||||
- Provide step-by-step instructions for manual remediation through web interfaces.
|
||||
- Use numbered lists for sequential steps.
|
||||
- Be specific about menu locations, button names, and settings.
|
||||
- Skip optional configurations or nice-to-have settings.
|
||||
- Format using markdown for better readability.
|
||||
|
||||
## Categories Guidelines
|
||||
|
||||
### Selection Guidelines
|
||||
|
||||
1. **Be Specific**: Only select categories that directly relate to what the automated control evaluates.
|
||||
2. **Primary Focus**: Consider the primary security concern the automated control addresses.
|
||||
3. **Avoid Over-Categorization**: Do not select categories just because they are tangentially related.
|
||||
|
||||
### Available Categories
|
||||
|
||||
| Category | Definition |
|
||||
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| encryption | Ensures data is encrypted in transit and/or at rest, including key management practices |
|
||||
| internet-exposed | Checks that limit or flag public access to services, APIs, or assets from the Internet |
|
||||
| logging | Ensures appropriate logging of events, activities, and system interactions for traceability |
|
||||
| secrets | Manages and protects credentials, API keys, tokens, and other sensitive information |
|
||||
| resilience | Ensures systems can maintain availability and recover from disruptions, failures, or degradation. Includes redundancy, fault-tolerance, auto-scaling, backup, disaster recovery, and failover strategies |
|
||||
| threat-detection | Identifies suspicious activity or behaviors using IDS, malware scanning, or anomaly detection |
|
||||
| trust-boundaries | Enforces isolation or segmentation between different trust levels (e.g., VPCs, tenants, network zones) |
|
||||
| vulnerabilities | Detects or remediates known software, infrastructure, or config vulnerabilities (e.g., CVEs) |
|
||||
| cluster-security | Secures Kubernetes cluster components such as API server, etcd, and role-based access |
|
||||
| container-security | Ensures container images and runtimes follow security best practices |
|
||||
| node-security | Secures nodes running containers or services |
|
||||
| gen-ai | Checks related to safe and secure use of generative AI services or models |
|
||||
| ci-cd | Ensures secure configurations in CI/CD pipelines |
|
||||
| identity-access | Governs user and service identities, including least privilege, MFA, and permission boundaries |
|
||||
| email-security | Ensures detection and protection against phishing, spam, spoofing, etc. |
|
||||
| forensics-ready | Ensures systems are instrumented to support post-incident investigations. Any digital trace or evidence (logs, volume snapshots, memory dumps, network captures, etc.) preserved immutably and accompanied by integrity guarantees, which can be used in a forensic analysis |
|
||||
| software-supply-chain | Detects or prevents tampering, unauthorized packages, or third-party risks in software supply chain |
|
||||
| e3 | M365-specific controls enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) |
|
||||
| e5 | M365-specific controls enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) |
|
||||
@@ -40,7 +40,7 @@ Each check in Prowler follows a straightforward structure. Within the newly crea
|
||||
|
||||
- `__init__.py` (empty file) – Ensures Python treats the check folder as a package.
|
||||
- `<check_name>.py` (code file) – Contains the check logic, following the prescribed format. Please refer to the [prowler's check code structure](./checks.md#prowlers-check-code-structure) for more information.
|
||||
- `<check_name>.metadata.json` (metadata file) – Defines the check's metadata for contextual information. Please refer to the [check metadata](./checks.md#) for more information.
|
||||
- `<check_name>.metadata.json` (metadata file) – Defines the check's metadata for contextual information. Please refer to the [check metadata](./checks.md#metadata-structure-for-prowler-checks) for more information.
|
||||
|
||||
## Prowler's Check Code Structure
|
||||
|
||||
@@ -226,68 +226,148 @@ Below is a generic example of a check metadata file. **Do not include comments i
|
||||
```json
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "example_check_id",
|
||||
"CheckTitle": "Example Check Title",
|
||||
"CheckType": ["Infrastructure Security"],
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "ami",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "critical",
|
||||
"CheckID": "service_resource_security_setting",
|
||||
"CheckTitle": "Service resource has security setting enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "service",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"Description": "Example description of the check.",
|
||||
"Risk": "Example risk if the check fails.",
|
||||
"RelatedUrl": "https://example.com",
|
||||
"Description": "This check verifies that the service resource has the required **security setting** enabled to protect against potential vulnerabilities.\n\nIt ensures that the resource follows security best practices and maintains proper access controls. The check evaluates whether the security configuration is properly implemented and active.",
|
||||
"Risk": "Without proper security settings, the resource may be vulnerable to:\n\n- **Unauthorized access** - Malicious actors could gain entry\n- **Data breaches** - Sensitive information could be compromised\n- **Security threats** - Various attack vectors could be exploited\n\nThis could result in compliance violations and potential financial or reputational damage.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": ["https://example.com/security-documentation", "https://example.com/best-practices"],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "example CLI command",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
"CLI": "provider-cli service enable-security-setting --resource-id resource-123",
|
||||
"NativeIaC": "```yaml\nType: Provider::Service::Resource\nProperties:\n SecuritySetting: enabled\n ResourceId: resource-123\n```",
|
||||
"Other": "1. Open the provider management console\n2. Navigate to the service section\n3. Select the resource\n4. Enable the security setting\n5. Save the configuration",
|
||||
"Terraform": "```hcl\nresource \"provider_service_resource\" \"example\" {\n resource_id = \"resource-123\"\n security_setting = true\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Example recommendation text.",
|
||||
"Url": "https://example.com/remediation"
|
||||
"Text": "Enable security settings on all service resources to ensure proper protection. Regularly review and update security configurations to align with current best practices.",
|
||||
"Url": "https://hub.prowler.com/check/service_resource_security_setting"
|
||||
}
|
||||
},
|
||||
"Categories": ["example-category"],
|
||||
"Categories": ["internet-exposed", "secrets"],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"RelatedTo": ["service_resource_security_setting", "service_resource_security_setting_2"],
|
||||
"Notes": "This is a generic example check that should be customized for specific provider and service requirements."
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata Fields and Their Purpose
|
||||
|
||||
- **Provider** — The Prowler provider related to the check. The name **must** be lowercase and match the provider folder name. For supported providers refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
- **CheckID** — The unique identifier for the check inside the provider, this field **must** match the check's folder and python file and json metadata file name. For more information about the naming refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
- **CheckTitle** — A concise, descriptive title for the check.
|
||||
- **CheckType** — *For now this field is only standardized for the AWS provider*.
|
||||
- For AWS this field must follow the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format. So the common pattern to follow is `namespace/category/classifier`, refer to the attached documentation for the valid values for this fields.
|
||||
- **ServiceName** — The name of the provider service being audited. This field **must** be in lowercase and match with the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
- **SubServiceName** — The subservice or resource within the service, if applicable. For more information refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Prowler's Resource Identification](#prowlers-resource-identification) section.
|
||||
- **Severity** — The severity of the finding if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, this field **must** be in lowercase. To get more information about the severity levels refer to the [Prowler's Check Severity Levels](#prowlers-check-severity-levels) section.
|
||||
- **ResourceType** — The type of resource being audited. *For now this field is only standardized for the AWS provider*.
|
||||
- For AWS use the [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or, if not available, the PascalCase version of the [CloudFormation type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) (e.g., `AwsEc2Instance`). Use "Other" if no match exists.
|
||||
- **Description** — A short description of what the check does.
|
||||
- **Risk** — The risk or impact if the check fails, explaining why the finding matters.
|
||||
- **RelatedUrl** — A URL to official documentation or further reading about the check's purpose. If no official documentation is available, use the risk and recommendation text from trusted third-party sources.
|
||||
- **Remediation** — Guidance for fixing a failed check, including:
|
||||
- **Code** — Remediation commands or code snippets for CLI, Terraform, native IaC, or other tools like the Web Console.
|
||||
- **Recommendation** — A textual human readable recommendation. Here it is not necessary to include actual steps, but rather a general recommendation about what to do to fix the check.
|
||||
- **Categories** — One or more categories for grouping checks in execution (e.g., `internet-exposed`). For the current list of categories, refer to the [Prowler Hub](https://hub.prowler.com/check).
|
||||
- **DependsOn** — Currently not used.
|
||||
- **RelatedTo** — Currently not used.
|
||||
- **Notes** — Any additional information not covered by other fields.
|
||||
#### Provider
|
||||
|
||||
### Remediation Code Guidelines
|
||||
The Prowler provider related to the check. The name **must** be lowercase and match the provider folder name. For supported providers refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
|
||||
When providing remediation steps, reference the following sources:
|
||||
#### CheckID
|
||||
|
||||
- Official provider documentation.
|
||||
- [Prowler Checks Remediation Index](https://docs.prowler.com/checks/checks-index)
|
||||
- [TrendMicro Cloud One Conformity](https://www.trendmicro.com/cloudoneconformity)
|
||||
- [CloudMatos Remediation Repository](https://github.com/cloudmatos/matos/tree/master/remediations)
|
||||
The unique identifier for the check inside the provider. This field **must** match the check's folder, Python file, and JSON metadata file name. For more information about naming, refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
|
||||
#### CheckTitle
|
||||
|
||||
The `CheckTitle` field must be plain text, clearly and succinctly define **the best practice being evaluated and which resource(s) each finding applies to**. The title should be specific, concise (no more than 150 characters), and reference the relevant resource(s) involved.
|
||||
|
||||
**Always write the `CheckTitle` to describe the *PASS* case**, the desired secure or compliant state of the resource(s). This helps ensure that findings are easy to interpret and that the title always reflects the best practice being met.
|
||||
|
||||
For detailed guidelines on writing effective check titles, including how to determine singular vs. plural scope and common mistakes to avoid, see [Check Title Guidelines](./check-metadata-guidelines.md#check-title-guidelines).
|
||||
|
||||
#### CheckType
|
||||
|
||||
???+ warning
|
||||
This field is only applicable to the AWS provider.
|
||||
|
||||
It follows the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format using the pattern `namespace/category/classifier`.
|
||||
|
||||
For the complete AWS Security Hub selection guidelines, see [Check Type Guidelines](./check-metadata-guidelines.md#check-type-guidelines-aws-only).
|
||||
|
||||
#### ServiceName
|
||||
|
||||
The name of the provider service being audited. Must be lowercase and match the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or the [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
|
||||
#### SubServiceName
|
||||
|
||||
This field is in the process of being deprecated and should be **left empty**.
|
||||
|
||||
#### ResourceIdTemplate
|
||||
|
||||
This field is in the process of being deprecated and should be **left empty**.
|
||||
|
||||
#### Severity
|
||||
|
||||
Severity level if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, and written in lowercase. See [Prowler's Check Severity Levels](#prowlers-check-severity-levels) for details.
|
||||
|
||||
#### ResourceType
|
||||
|
||||
The type of resource being audited. This field helps categorize and organize findings by resource type for better analysis and reporting. For each provider:
|
||||
|
||||
- **AWS**: Use [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or PascalCase CloudFormation types removing the `::` separator used in CloudFormation templates (e.g., in CloudFormation template the type of an EC2 instance is `AWS::EC2::Instance` but in the check it should be `AwsEc2Instance`). Use `Other` if none apply.
|
||||
- **Azure**: Use types from [Azure Resource Graph](https://learn.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources), for example: `Microsoft.Storage/storageAccounts`.
|
||||
- **Google Cloud**: Use [Cloud Asset Inventory asset types](https://cloud.google.com/asset-inventory/docs/asset-types), for example: `compute.googleapis.com/Instance`.
|
||||
- **Kubernetes**: Use types shown under `KIND` from `kubectl api-resources`.
|
||||
- **M365 / GitHub**: Leave empty due to lack of standardized types.
|
||||
|
||||
#### Description
|
||||
|
||||
A concise, natural language explanation that **clearly describes what the finding means**, focusing on clarity and context rather than technical implementation details. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. This field is limited to maximum 400 characters.
|
||||
|
||||
For detailed writing guidelines and common mistakes to avoid, see [Description Guidelines](./check-metadata-guidelines.md#description-guidelines).
|
||||
|
||||
#### Risk
|
||||
|
||||
A clear, natural language explanation of **why this finding poses a cybersecurity risk**. Focus on how it may impact confidentiality, integrity, or availability. If those do not apply, describe any relevant operational or financial risks. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. Limit your explanation to 400 characters.
|
||||
|
||||
For detailed writing guidelines and common mistakes to avoid, see [Risk Guidelines](./check-metadata-guidelines.md#risk-guidelines).
|
||||
|
||||
#### RelatedUrl
|
||||
|
||||
*Deprecated*. Use `AdditionalURLs` for adding your URLs references.
|
||||
|
||||
#### AdditionalURLs
|
||||
|
||||
???+ warning
|
||||
URLs must be valid and not repeated.
|
||||
|
||||
A list of official documentation URLs for further reading. These should be authoritative sources that provide additional context, best practices, or detailed information about the security control being checked. Prefer official provider documentation, security standards, or well-established security resources. Avoid third-party blogs or unofficial sources unless they are highly reputable and directly relevant.
|
||||
|
||||
#### Remediation
|
||||
|
||||
Provides both code examples and best practice recommendations for addressing the security issue.
|
||||
|
||||
- **Code**: Contains remediation examples in different formats:
|
||||
- **CLI**: Command-line interface commands to make the finding compliant in runtime.
|
||||
- **NativeIaC**: Native Infrastructure as Code templates with an example of a compliant configuration. For now it applies to:
|
||||
- **AWS**: CloudFormation YAML formatted code (do not use JSON format).
|
||||
- **Azure**: Bicep formatted code (do not use ARM templates).
|
||||
- **Terraform**: HashiCorp Configuration Language (HCL) code with an example of a compliant configuration.
|
||||
- **Other**: Manual steps through web interfaces or other tools to make the finding compliant.
|
||||
|
||||
For detailed guidelines on writing remediation code, see [Remediation Code Guidelines](./check-metadata-guidelines.md#remediation-code-guidelines).
|
||||
|
||||
- **Recommendation**
|
||||
- **Text**: Generic best practice guidance in natural language using Markdown format (maximum 400 characters). For writing guidelines, see [Recommendation Guidelines](./check-metadata-guidelines.md#recommendation-guidelines).
|
||||
- **Url**: [Prowler Hub URL](https://hub.prowler.com/) of the check. This URL is always composed by `https://hub.prowler.com/check/<check_id>`.
|
||||
|
||||
#### Categories
|
||||
|
||||
One or more functional groupings used for execution filtering (e.g., `internet-exposed`). You can define new categories just by adding to this field.
|
||||
|
||||
For the complete list of available categories, see [Categories Guidelines](./check-metadata-guidelines.md#categories-guidelines).
|
||||
|
||||
#### DependsOn
|
||||
|
||||
List of check IDs of checks that if are compliant, this check will be a compliant too or it is not going to give any finding.
|
||||
|
||||
#### RelatedTo
|
||||
|
||||
List of check IDs of checks that are conceptually related, even if they do not share a technical dependency.
|
||||
|
||||
#### Notes
|
||||
|
||||
Any additional information not covered in the above fields.
|
||||
|
||||
### Python Model Reference
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This page details the [Google Cloud Platform (GCP)](https://cloud.google.com/) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [getting started](../index.md#google-cloud) page.
|
||||
By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [GCP getting started guide](../tutorials/gcp/getting-started-gcp.md).
|
||||
|
||||
## GCP Provider Classes Architecture
|
||||
|
||||
@@ -50,6 +50,49 @@ The GCP provider implementation follows the general [Provider structure](./provi
|
||||
- **Location:** [`prowler/providers/gcp/lib/`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/lib/)
|
||||
- **Purpose:** Helpers for argument parsing, mutelist management, and other cross-cutting concerns.
|
||||
|
||||
## Retry Configuration
|
||||
|
||||
GCP services implement automatic retry functionality for rate limiting errors (HTTP 429). This is configured centrally and must be included in all API calls:
|
||||
|
||||
### Required Implementation
|
||||
|
||||
```python
|
||||
from prowler.providers.gcp.config import DEFAULT_RETRY_ATTEMPTS
|
||||
|
||||
# In discovery.build()
|
||||
client = discovery.build(
|
||||
service, version, credentials=credentials,
|
||||
num_retries=DEFAULT_RETRY_ATTEMPTS
|
||||
)
|
||||
|
||||
# In request.execute()
|
||||
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
- **Default Value**: 3 attempts (configurable in `prowler/providers/gcp/config.py`)
|
||||
- **Command Line Flag**: `--gcp-retries-max-attempts` for runtime configuration
|
||||
- **Error Types**: HTTP 429 and quota exceeded errors
|
||||
- **Backoff Strategy**: Exponential backoff with randomization
|
||||
|
||||
### Example Service Implementation
|
||||
|
||||
```python
|
||||
def _get_instances(self):
|
||||
for project_id in self.project_ids:
|
||||
try:
|
||||
client = discovery.build(
|
||||
"compute", "v1", credentials=self.credentials,
|
||||
num_retries=DEFAULT_RETRY_ATTEMPTS
|
||||
)
|
||||
request = client.instances().list(project=project_id)
|
||||
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
# Process response...
|
||||
except Exception as error:
|
||||
logger.error(f"{error.__class__.__name__}: {error}")
|
||||
```
|
||||
|
||||
## Specific Patterns in GCP Services
|
||||
|
||||
The generic service pattern is described in [service page](./services.md#service-structure-and-initialisation). You can find all the currently implemented services in the following locations:
|
||||
@@ -69,6 +112,7 @@ The best reference to understand how to implement a new service is following the
|
||||
- Resource discovery and attribute collection can be parallelized using `self.__threading_call__`, typically by region/zone or resource.
|
||||
- All GCP resources are represented as Pydantic `BaseModel` classes, providing type safety and structured access to resource attributes.
|
||||
- Each GCP API calls are wrapped in try/except blocks, always logging errors.
|
||||
- **Retry Configuration**: All `request.execute()` calls must include `num_retries=DEFAULT_RETRY_ATTEMPTS` for automatic retry on rate limiting errors (HTTP 429).
|
||||
- Tags and additional attributes that cannot be retrieved from the default call should be collected and stored for each resource using dedicated methods and threading.
|
||||
|
||||
## Specific Patterns in GCP Checks
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This page details the [GitHub](https://github.com/) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [getting started](../index.md#github) page.
|
||||
By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [GitHub getting started guide](../tutorials/github/getting-started-github.md).
|
||||
|
||||
## GitHub Provider Classes Architecture
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This page details the [Kubernetes](https://kubernetes.io/) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, follow the [getting started](../index.md#kubernetes) page.
|
||||
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, see the [In-Cluster Execution](../tutorials/kubernetes/in-cluster.md) or [Non In-Cluster Execution](../tutorials/kubernetes/outside-cluster.md) guides.
|
||||
|
||||
## Kubernetes Provider Classes Architecture
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# LLM Provider
|
||||
|
||||
This page details the [Large Language Model (LLM)](https://en.wikipedia.org/wiki/Large_language_model) provider implementation in Prowler.
|
||||
|
||||
The LLM provider enables security testing of language models using red team techniques. By default, Prowler uses the built-in LLM configuration that targets OpenAI models with comprehensive security test suites. To configure it, follow the [LLM getting started guide](../tutorials/llm/getting-started-llm.md).
|
||||
|
||||
## LLM Provider Classes Architecture
|
||||
|
||||
The LLM provider implementation follows the general [Provider structure](./provider.md). This section focuses on the LLM-specific implementation, highlighting how the generic provider concepts are realized for LLM security testing in Prowler. For a full overview of the provider pattern, base classes, and extension guidelines, see [Provider documentation](./provider.md).
|
||||
|
||||
### Main Class
|
||||
|
||||
- **Location:** [`prowler/providers/llm/llm_provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/llm/llm_provider.py)
|
||||
- **Base Class:** Inherits from `Provider` (see [base class details](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py)).
|
||||
- **Purpose:** Central orchestrator for LLM-specific logic, configuration management, and integration with promptfoo for red team testing.
|
||||
- **Key LLM Responsibilities:**
|
||||
- Initializes and manages LLM configuration using promptfoo.
|
||||
- Validates configuration and sets up the LLM testing context.
|
||||
- Loads and manages red team test configuration, plugins, and target models.
|
||||
- Provides properties and methods for downstream LLM security testing.
|
||||
- Integrates with promptfoo for comprehensive LLM security evaluation.
|
||||
|
||||
### Data Models
|
||||
|
||||
- **Location:** [`prowler/providers/llm/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/llm/models.py)
|
||||
- **Purpose:** Define structured data for LLM output options and configuration.
|
||||
- **Key LLM Models:**
|
||||
- `LLMOutputOptions`: Customizes output filename logic for LLM-specific reporting.
|
||||
|
||||
### LLM Security Testing Integration
|
||||
|
||||
- **Location:** [`prowler/providers/llm/llm_provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/llm/llm_provider.py)
|
||||
- **Purpose:** Integrates with promptfoo for comprehensive LLM security testing.
|
||||
- **Key LLM Responsibilities:**
|
||||
- Executes promptfoo red team evaluations against target LLMs.
|
||||
- Processes security test results and converts them to Prowler reports.
|
||||
- Manages test concurrency and progress tracking.
|
||||
- Handles real-time streaming of test results.
|
||||
|
||||
### Configuration Management
|
||||
|
||||
The LLM provider uses promptfoo configuration files to define:
|
||||
|
||||
- **Target Models**: The LLM models to test (e.g., OpenAI GPT, Anthropic Claude)
|
||||
- **Red Team Plugins**: Security test suites (OWASP, MITRE, NIST, EU AI Act)
|
||||
- **Test Parameters**: Concurrency, test counts, and evaluation criteria
|
||||
|
||||
### Default Configuration
|
||||
|
||||
Prowler includes a comprehensive default LLM configuration that:
|
||||
|
||||
- Targets OpenAI models by default
|
||||
- Includes multiple security test frameworks (OWASP, MITRE, NIST, EU AI Act)
|
||||
- Provides extensive test coverage for LLM security vulnerabilities
|
||||
- Supports custom configuration for specific testing needs
|
||||
|
||||
## Specific Patterns in LLM Security Testing
|
||||
|
||||
The LLM provider implements security testing through integration with promptfoo, following these patterns:
|
||||
|
||||
### Red Team Testing Framework
|
||||
|
||||
- **Plugin-based Architecture**: Uses promptfoo plugins for different security test categories
|
||||
- **Comprehensive Coverage**: Includes OWASP LLM Top 10, MITRE ATLAS, NIST AI Risk Management, and EU AI Act compliance
|
||||
- **Real-Time Evaluation**: Streams test results as they are generated
|
||||
- **Progress Tracking**: Provides detailed progress information during test execution
|
||||
|
||||
### Test Execution Flow
|
||||
|
||||
1. **Configuration Loading**: Loads promptfoo configuration with target models and test plugins
|
||||
2. **Test Generation**: Generates security test cases based on configured plugins
|
||||
3. **Concurrent Execution**: Runs tests with configurable concurrency limits
|
||||
4. **Result Processing**: Converts promptfoo results to Prowler security reports
|
||||
5. **Progress Monitoring**: Tracks and displays test execution progress
|
||||
|
||||
### Security Test Categories
|
||||
|
||||
The LLM provider supports comprehensive security testing across multiple frameworks:
|
||||
|
||||
- **OWASP LLM Top 10**: Covers prompt injection, data leakage, and model security
|
||||
- **MITRE ATLAS**: Adversarial threat landscape for AI systems
|
||||
- **NIST AI Risk Management**: AI system risk assessment and mitigation
|
||||
- **EU AI Act**: European Union AI regulation compliance
|
||||
- **Custom Tests**: Support for organization-specific security requirements
|
||||
|
||||
## Error Handling and Validation
|
||||
|
||||
The LLM provider includes comprehensive error handling for:
|
||||
|
||||
- **Configuration Validation**: Ensures valid promptfoo configuration files
|
||||
- **Model Access**: Handles authentication and access issues with target LLMs
|
||||
- **Test Execution**: Manages test failures and timeout scenarios
|
||||
- **Result Processing**: Handles malformed or incomplete test results
|
||||
|
||||
## Integration with Prowler Ecosystem
|
||||
|
||||
The LLM provider seamlessly integrates with Prowler's existing infrastructure:
|
||||
|
||||
- **Output Formats**: Supports all Prowler output formats (JSON, CSV, HTML, etc.)
|
||||
- **Compliance Frameworks**: Integrates with Prowler's compliance reporting
|
||||
- **Fixer Integration**: Supports automated remediation recommendations
|
||||
- **Dashboard Integration**: Compatible with Prowler App for centralized management
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This page details the [Microsoft 365 (M365)](https://www.microsoft.com/en-us/microsoft-365) provider implementation in Prowler.
|
||||
|
||||
By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [getting started](../index.md#microsoft-365) page.
|
||||
By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [M365 getting started guide](../tutorials/microsoft365/getting-started-m365.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@ By default, Prowler will audit the Microsoft Entra ID tenant and its supported s
|
||||
- **Required modules:**
|
||||
- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (≥ 3.6.0)
|
||||
- [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (≥ 6.6.0)
|
||||
- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Requirements: Supported PowerShell Versions](../getting-started/requirements.md#supported-powershell-versions) and [Needed PowerShell Modules](../getting-started/requirements.md#needed-powershell-modules).
|
||||
- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Authentication: Supported PowerShell Versions](../tutorials/microsoft365/authentication.md#supported-powershell-versions) and [Needed PowerShell Modules](../tutorials/microsoft365/authentication.md#required-powershell-modules).
|
||||
- For more details and troubleshooting, see [Use of PowerShell in M365](../tutorials/microsoft365/use-of-powershell.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -101,6 +101,7 @@ Prowler supports multiple output formats, allowing users to tailor findings pres
|
||||
finding_dict["DESCRIPTION"] = finding.metadata.Description
|
||||
finding_dict["RISK"] = finding.metadata.Risk
|
||||
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
|
||||
finding_dict["ADDITIONAL_URLS"] = unroll_list(finding.metadata.AdditionalURLs)
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
|
||||
finding.metadata.Remediation.Recommendation.Text
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ A provider is any platform or service that offers resources, data, or functional
|
||||
- Software as a Service (SaaS) Platforms (like Microsoft 365)
|
||||
- Development Platforms (like GitHub)
|
||||
- Container Orchestration Platforms (like Kubernetes)
|
||||
- Database-as-a-Service Platforms (like MongoDB Atlas)
|
||||
|
||||
For providers supported by Prowler, refer to [Prowler Hub](https://hub.prowler.com/).
|
||||
|
||||
@@ -63,6 +64,7 @@ Given the complexity and variability of providers, use existing provider impleme
|
||||
- [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)
|
||||
- [GitHub](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/github/github_provider.py)
|
||||
- [MongoDB Atlas](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/mongodbatlas/mongodbatlas_provider.py)
|
||||
|
||||
### Basic Provider Implementation: Pseudocode Example
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Renaming Checks in Prowler
|
||||
|
||||
To rename a check in Prowler, follow these steps when aligning with Check ID structure, fixing typos, or updating check logic that requires a new name.
|
||||
|
||||
When changing a Check ID, update the following files:
|
||||
|
||||
## Update Check Folder Structure
|
||||
|
||||
First, rename the check folder with the new check name.
|
||||
|
||||
**Path:** `prowler/providers/<provider>/services/<service>/<check_name>`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Before
|
||||
prowler/providers/aws/services/inspector2/inspector2_findings_exist/
|
||||
|
||||
# After
|
||||
prowler/providers/aws/services/inspector2/inspector2_active_findings_exist/
|
||||
```
|
||||
|
||||
Next, rename the file that contains the check logic. Inside that file, also rename the class name to match the new check name.
|
||||
|
||||
**Path:** `prowler/providers/<provider>/services/<service>/<check_name>/<check_name>.py`
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# Before
|
||||
class inspector2_findings_exist(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# ... check logic ...
|
||||
|
||||
# After
|
||||
class inspector2_active_findings_exist(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# ... check logic ...
|
||||
```
|
||||
|
||||
Then, rename the file that contains the check metadata. Inside that file, add the old check name as an alias in the `CheckAliases` field and modify the `CheckID` to the new check name.
|
||||
|
||||
**Path:** `prowler/providers/<provider>/services/<service>/<check_name>/<check_name>.metadata.json`
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_exist",
|
||||
"CheckTitle": "Check if Inspector2 active findings exist",
|
||||
"CheckAliases": [
|
||||
"inspector2_findings_exist"
|
||||
],
|
||||
"CheckType": [],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:inspector2:region:account-id/detector-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"Description": "This check determines if there are any active findings in your AWS account that have been detected by AWS Inspector2.",
|
||||
"Risk": "Without using AWS Inspector, you may not be aware of all the security vulnerabilities in your AWS resources.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector/amazon-inspector-findings.html",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Review the active findings from Inspector2",
|
||||
"Url": "https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Update Test Files
|
||||
|
||||
Second, rename the tests folder with the new check name.
|
||||
|
||||
**Path:** `tests/providers/<provider>/services/<service>/<check_name>`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Before
|
||||
tests/providers/aws/services/inspector2/inspector2_findings_exist/
|
||||
|
||||
# After
|
||||
tests/providers/aws/services/inspector2/inspector2_active_findings_exist/
|
||||
```
|
||||
|
||||
Next, rename the test file that contains all the unit tests. Inside that file, rename all appearances of the old check name to the new check name.
|
||||
|
||||
**Path:** `tests/providers/<provider>/services/<service>/<check_name>/<check_name>_test.py`
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# Before
|
||||
from prowler.providers.aws.services.inspector2.inspector2_findings_exist.inspector2_findings_exist import (
|
||||
inspector2_findings_exist,
|
||||
)
|
||||
|
||||
class Test_inspector2_findings_exist:
|
||||
def test_inspector2_no_findings(self):
|
||||
# ... test logic ...
|
||||
|
||||
def test_inspector2_with_findings(self):
|
||||
# ... test logic ...
|
||||
|
||||
# After
|
||||
from prowler.providers.aws.services.inspector2.inspector2_active_findings_exist.inspector2_active_findings_exist import (
|
||||
inspector2_active_findings_exist,
|
||||
)
|
||||
|
||||
class Test_inspector2_active_findings_exist:
|
||||
def test_inspector2_no_findings(self):
|
||||
# ... test logic ...
|
||||
|
||||
def test_inspector2_with_findings(self):
|
||||
# ... test logic ...
|
||||
```
|
||||
|
||||
**Important:** Update all references to the old check name in the test file, including:
|
||||
|
||||
- Import statements at the top of the file
|
||||
- Class name in the test class
|
||||
- Any function calls to the check
|
||||
- Any string references to the check name
|
||||
- Mock patches that reference the check
|
||||
|
||||
**Complete example of all changes needed in test files:**
|
||||
```python
|
||||
# Before
|
||||
from prowler.providers.aws.services.inspector2.inspector2_findings_exist.inspector2_findings_exist import (
|
||||
inspector2_findings_exist,
|
||||
)
|
||||
|
||||
class Test_inspector2_findings_exist:
|
||||
def test_inspector2_no_findings(self):
|
||||
# Mock setup
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.inspector2.inspector2_findings_exist.inspector2_findings_exist.inspector2_client",
|
||||
inspector2_client,
|
||||
):
|
||||
check = inspector2_findings_exist()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "No active findings found" in result[0].status_extended
|
||||
|
||||
# After
|
||||
from prowler.providers.aws.services.inspector2.inspector2_active_findings_exist.inspector2_active_findings_exist import (
|
||||
inspector2_active_findings_exist,
|
||||
)
|
||||
|
||||
class Test_inspector2_active_findings_exist:
|
||||
def test_inspector2_no_findings(self):
|
||||
# Mock setup
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.inspector2.inspector2_active_findings_exist.inspector2_active_findings_exist.inspector2_client",
|
||||
inspector2_client,
|
||||
):
|
||||
check = inspector2_active_findings_exist()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "No active findings found" in result[0].status_extended
|
||||
```
|
||||
|
||||
## Update Compliance Mappings
|
||||
|
||||
Finally, rename all appearances of the old check name to the new check name inside any compliance framework where the check is mapped.
|
||||
|
||||
- `prowler/compliance/<service>/<compliance_where_the_check_is_mapped>.json`
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"Framework": "CIS",
|
||||
"Version": "2.0",
|
||||
"Provider": "AWS",
|
||||
"Description": "The CIS Amazon Web Services Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Amazon Web Services.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "4.1",
|
||||
"Description": "Ensure a log metric filter and alarm exist for unauthorized API calls",
|
||||
"Checks": [
|
||||
"inspector2_active_findings_exist"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "4 Logging and Monitoring",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The development compliance file may contain examples of the check being renamed. If so, modify this file as well:
|
||||
|
||||
- `api/src/backend/api/fixtures/dev/7_dev_compliance.json`
|
||||
@@ -231,11 +231,11 @@ Before implementing a new service, verify that Prowler's existing permissions fo
|
||||
|
||||
Provider-Specific Permissions Documentation:
|
||||
|
||||
- [AWS](../getting-started/requirements.md#authentication)
|
||||
- [Azure](../getting-started/requirements.md#needed-permissions)
|
||||
- [GCP](../getting-started/requirements.md#needed-permissions_1)
|
||||
- [M365](../getting-started/requirements.md#needed-permissions_2)
|
||||
- [GitHub](../getting-started/requirements.md#authentication_2)
|
||||
- [AWS](../tutorials/aws/authentication.md#required-permissions)
|
||||
- [Azure](../tutorials/azure/authentication.md#required-permissions)
|
||||
- [GCP](../tutorials/gcp/authentication.md#required-permissions)
|
||||
- [M365](../tutorials/microsoft365/authentication.md#required-permissions)
|
||||
- [GitHub](../tutorials/github/authentication.md)
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ To execute the Prowler test suite, install the necessary dependencies listed in
|
||||
|
||||
### Prerequisites
|
||||
|
||||
If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#get-the-code-and-install-all-dependencies).
|
||||
If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#getting-the-code-and-installing-all-dependencies).
|
||||
|
||||
### Executing Tests
|
||||
|
||||
@@ -520,7 +520,7 @@ Execute tests on the service `__init__` to ensure correct information retrieval.
|
||||
|
||||
While service tests resemble *Integration Tests*, as they assess how the service interacts with the provider, they ultimately fall under *Unit Tests*, due to the use of Moto or custom mock objects.
|
||||
|
||||
For detailed guidance on test creation and existing service tests, refer to the [AWS checks test](./unit-testing.md#checks) [documentation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
|
||||
For detailed guidance on test creation and existing service tests, check the current [AWS checks implementation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
|
||||
|
||||
## GCP
|
||||
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# Prowler Requirements
|
||||
|
||||
Prowler is built in Python and utilizes the following SDKs:
|
||||
|
||||
- [AWS SDK (Boto3)](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html#)
|
||||
- [Azure SDK](https://azure.github.io/azure-sdk-for-python/)
|
||||
- [GCP API Python Client](https://github.com/googleapis/google-api-python-client/)
|
||||
- [Kubernetes SDK](https://github.com/kubernetes-client/python)
|
||||
- [M365 Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-python)
|
||||
- [Github REST API SDK](https://github.com/PyGithub/PyGithub)
|
||||
|
||||
## AWS
|
||||
|
||||
Prowler requires AWS credentials to function properly. You can authenticate using any method outlined in the [AWS CLI configuration guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence).
|
||||
|
||||
### Authentication Steps
|
||||
|
||||
Ensure your AWS CLI is correctly configured with valid credentials and region settings. You can achieve this via:
|
||||
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||
|
||||
#### Required IAM Permissions
|
||||
|
||||
The credentials used must be associated with a user or role that has appropriate permissions for security checks. Attach the following AWS managed policies to ensure access:
|
||||
|
||||
- `arn:aws:iam::aws:policy/SecurityAudit`
|
||||
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
|
||||
|
||||
#### Additional Permissions
|
||||
|
||||
For certain checks, additional read-only permissions are required. Attach the following custom policy to your role:
|
||||
|
||||
[prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json)
|
||||
|
||||
If you intend to send findings to
|
||||
[AWS Security Hub](https://aws.amazon.com/security-hub), attach the following custom policy:
|
||||
|
||||
[prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json).
|
||||
|
||||
### Multi-Factor Authentication (MFA)
|
||||
|
||||
If your IAM entity requires Multi-Factor Authentication (MFA), you can use the `--mfa` flag. Prowler will prompt you to enter the following values to initiate a new session:
|
||||
|
||||
- **ARN of your MFA device**
|
||||
- **TOTP (Time-Based One-Time Password)**
|
||||
|
||||
## Azure
|
||||
|
||||
Prowler for Azure supports multiple authentication types. To use a specific method, pass the appropriate flag during execution:
|
||||
|
||||
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
|
||||
- Existing **AZ CLI credentials**
|
||||
- **Interactive browser authentication**
|
||||
- [**Managed Identity**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication
|
||||
|
||||
> ⚠️ **Important:** For Prowler App, only Service Principal authentication is supported.
|
||||
|
||||
### Service Principal Application Authentication
|
||||
|
||||
To allow Prowler to authenticate using a Service Principal Application, set up the following environment variables:
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXX"
|
||||
```
|
||||
|
||||
If you execute Prowler with the `--sp-env-auth` flag and these variables are not set or exported, execution will fail.
|
||||
|
||||
Refer to the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md#how-to-create-prowler-service-principal-application) guide for detailed setup instructions.
|
||||
|
||||
### Azure Authentication Methods
|
||||
|
||||
Prowler for Azure supports the following authentication methods:
|
||||
|
||||
- **AZ CLI Authentication (`--az-cli-auth`)** – Automated authentication using stored AZ CLI credentials.
|
||||
- **Managed Identity Authentication (`--managed-identity-auth`)** – Automated authentication via Azure Managed Identity.
|
||||
- **Browser Authentication (`--browser-auth`)** – Requires the user to authenticate using the default browser. The `tenant-id` parameter is mandatory for this method.
|
||||
|
||||
### Required Permissions
|
||||
|
||||
Prowler for Azure requires two types of permission scopes:
|
||||
|
||||
#### Microsoft Entra ID Permissions
|
||||
|
||||
These permissions allow Prowler to retrieve metadata from the assumed identity and perform specific Entra checks. While not mandatory for execution, they enhance functionality.
|
||||
|
||||
Required permissions:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All` (used for Entra multifactor authentication checks)
|
||||
|
||||
???+ note
|
||||
You can replace `Directory.Read.All` with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers.
|
||||
|
||||
|
||||
#### Subscription Scope Permissions
|
||||
|
||||
These permissions are required to perform security checks against Azure resources. The following **RBAC roles** must be assigned per subscription to the entity used by Prowler:
|
||||
|
||||
- `Reader` – Grants read-only access to Azure resources.
|
||||
- `ProwlerRole` – A custom role with minimal permissions, defined in the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json).
|
||||
|
||||
???+ note
|
||||
The `assignableScopes` field in the JSON custom role file must be updated to reflect the correct subscription or management group. Use one of the following formats: `/subscriptions/<subscription-id>` or `/providers/Microsoft.Management/managementGroups/<management-group-id>`.
|
||||
|
||||
### Assigning Permissions
|
||||
|
||||
To properly configure permissions, follow these guides:
|
||||
|
||||
- [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions)
|
||||
- [Azure subscription permissions](../tutorials/azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler)
|
||||
|
||||
???+ warning
|
||||
Some permissions in `ProwlerRole` involve **write access**. If a `ReadOnly` lock is attached to certain resources, you may encounter errors, and findings for those checks will not be available.
|
||||
|
||||
#### Checks Requiring `ProwlerRole`
|
||||
|
||||
The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks:
|
||||
|
||||
- `app_function_access_keys_configured`
|
||||
- `app_function_ftps_deployment_disabled`
|
||||
|
||||
## Google Cloud
|
||||
|
||||
### Authentication
|
||||
|
||||
Prowler follows the same credential discovery process as the [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order):
|
||||
|
||||
1. **Environment Variable Authentication** – Uses the [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC).
|
||||
2. **Google Cloud CLI Credentials** – Uses credentials configured via the [Google Cloud CLI](https://cloud.google.com/docs/authentication/application-default-credentials#personal).
|
||||
3. **Service Account Authentication** – Retrieves the attached service account credentials from the metadata server. More details [here](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
||||
|
||||
### Required Permissions
|
||||
|
||||
Prowler for Google Cloud requires the following permissions:
|
||||
|
||||
#### IAM Roles
|
||||
- **Reader (`roles/reader`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
|
||||
|
||||
#### Project-Level Settings
|
||||
|
||||
At least one project must have the following configurations:
|
||||
|
||||
- **Identity and Access Management (IAM) API (`iam.googleapis.com`)** – Must be enabled via:
|
||||
|
||||
- The [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics), or
|
||||
- The `gcloud` CLI:
|
||||
```sh
|
||||
gcloud services enable iam.googleapis.com --project <your-project-id>
|
||||
```
|
||||
|
||||
- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role – Required for resource scanning.
|
||||
|
||||
- **Quota Project Setting** – Define a quota project using either:
|
||||
|
||||
- The `gcloud` CLI:
|
||||
```sh
|
||||
gcloud auth application-default set-quota-project <project-id>
|
||||
```
|
||||
- Setting an environment variable:
|
||||
```sh
|
||||
export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>
|
||||
```
|
||||
|
||||
### Default Project Scanning
|
||||
|
||||
By default, Prowler scans **all accessible GCP projects**. To limit the scan to specific projects, use the `--project-ids` flag.
|
||||
|
||||
## Microsoft 365
|
||||
|
||||
Prowler for Microsoft 365 (M365) supports the following authentication methods:
|
||||
|
||||
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
|
||||
- **Service Principal Application with Microsoft User Credentials**
|
||||
- **Stored AZ CLI credentials**
|
||||
- **Interactive browser authentication**
|
||||
|
||||
???+ warning
|
||||
Prowler App supports the **Service Principal** authentication method and the **Service Principal with User Credentials** authentication method, but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
|
||||
|
||||
### Service Principal Authentication (Recommended)
|
||||
|
||||
**Authentication flag:** `--sp-env-auth`
|
||||
|
||||
To enable Prowler to authenticate as the **Service Principal Application**, configure the following environment variables:
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
```
|
||||
|
||||
If these variables are not set or exported, execution using `--sp-env-auth` will fail.
|
||||
|
||||
Refer to the [Create Prowler Service Principal](../tutorials/microsoft365/getting-started-m365.md#create-the-service-principal-app) guide for setup instructions.
|
||||
|
||||
If the external API permissions described in the mentioned section above are not added only checks that work through MS Graph will be executed. This means that the full provider will not be executed.
|
||||
|
||||
???+ note
|
||||
In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [Needed permissions](/docs/tutorials/microsoft365/getting-started-m365.md#needed-permissions) section for more information.
|
||||
|
||||
### Service Principal and User Credentials Authentication
|
||||
|
||||
Authentication flag: `--env-auth`
|
||||
|
||||
???+ warning
|
||||
This method is not recommended anymore, we recommend just use the **Service Principal Application** authentication method instead.
|
||||
|
||||
This method builds upon the Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`.
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export M365_USER="your_email@example.com"
|
||||
export M365_PASSWORD="examplepassword"
|
||||
```
|
||||
|
||||
These two new environment variables are **required** in this authentication method to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
|
||||
|
||||
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
|
||||
|
||||
???+ warning
|
||||
If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired.
|
||||
|
||||
???+ warning
|
||||
If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you don’t complete this step, user authentication will fail because Microsoft marks the initial password as expired.
|
||||
|
||||
???+ warning
|
||||
The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information.
|
||||
|
||||
???+ warning
|
||||
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
|
||||
|
||||
Ensure you are using the right domain for the user you are trying to authenticate with.
|
||||
|
||||

|
||||
|
||||
- `M365_PASSWORD` must be the user password.
|
||||
|
||||
???+ note
|
||||
Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you.
|
||||
|
||||
|
||||
|
||||
### Interactive Browser Authentication
|
||||
|
||||
**Authentication flag:** `--browser-auth`
|
||||
|
||||
This authentication method requires the user to authenticate against Azure using the default browser to start the scan. The `--tenant-id` flag is also required.
|
||||
|
||||
With these credentials, you will only be able to run checks that rely on Microsoft Graph. This means you won't be able to run the entire provider. To perform a full M365 security scan, use the **recommended authentication method**.
|
||||
|
||||
Since this is a **delegated permission** authentication method, necessary permissions should be assigned to the user rather than the application.
|
||||
|
||||
### Required Permissions
|
||||
|
||||
To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**.
|
||||
|
||||
#### For Service Principal Authentication (`--sp-env-auth`) - Recommended
|
||||
|
||||
When using service principal authentication, you need to add the following **Application Permissions** configured to:
|
||||
|
||||
**Microsoft Graph API Permissions:**
|
||||
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
- `Directory.Read.All`: Required for all services.
|
||||
- `Policy.Read.All`: Required for all services.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
|
||||
|
||||
**External API Permissions:**
|
||||
|
||||
- `Exchange.ManageAsApp` from external API `Office 365 Exchange Online`: Required for Exchange PowerShell module app authentication. You also need to assign the `Global Reader` role to the app.
|
||||
- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication.
|
||||
|
||||
???+ note
|
||||
`Directory.Read.All` can be replaced with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers.
|
||||
|
||||
> If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate.
|
||||
|
||||
???+ note
|
||||
This is the **recommended authentication method** because it allows you to run the full M365 provider including PowerShell checks, providing complete coverage of all available security checks, same as the Service Principal Authentication + User Credentials Authentication but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
|
||||
|
||||
|
||||
#### For Service Principal + User Credentials Authentication (`--env-auth`)
|
||||
|
||||
When using service principal with user credentials authentication, you need **both** sets of permissions:
|
||||
|
||||
**1. Service Principal Application Permissions**:
|
||||
- You **will need** all the Microsoft Graph API permissions listed above.
|
||||
- You **won't need** the External API permissions listed above.
|
||||
|
||||
**2. User-Level Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles:
|
||||
|
||||
- `Global Reader` (recommended): this allows you to read all roles needed.
|
||||
- `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (since only read access is needed, Global Reader is recommended).
|
||||
|
||||
|
||||
#### For Browser Authentication (`--browser-auth`)
|
||||
|
||||
When using browser authentication, permissions are delegated to the user, so the user must have the appropriate permissions rather than the application.
|
||||
|
||||
???+ warning
|
||||
With browser authentication, you will only be able to run checks that work through MS Graph API. PowerShell module checks will not be executed.
|
||||
|
||||
### Assigning Permissions and Roles
|
||||
|
||||
For guidance on assigning the necessary permissions and roles, follow these instructions:
|
||||
- [Grant API Permissions](../tutorials/microsoft365/getting-started-m365.md#grant-required-graph-api-permissions)
|
||||
- [Assign Required Roles](../tutorials/microsoft365/getting-started-m365.md#if-using-user-authentication)
|
||||
|
||||
### Supported PowerShell Versions
|
||||
|
||||
PowerShell is required to run certain M365 checks.
|
||||
|
||||
**Supported versions:**
|
||||
- **PowerShell 7.4 or higher** (7.5 is recommended)
|
||||
|
||||
#### Why Is PowerShell 7.4+ Required?
|
||||
|
||||
- **PowerShell 5.1** (default on some Windows systems) does not support required cmdlets.
|
||||
- Older [cross-platform PowerShell versions](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) are **unsupported**, leading to potential errors.
|
||||
|
||||
???+ note
|
||||
Installing PowerShell is only necessary if you install Prowler via **pip or other sources**. **SDK and API containers include PowerShell by default.**
|
||||
|
||||
### Installing PowerShell
|
||||
|
||||
Installing PowerShell is different depending on your OS.
|
||||
|
||||
- [Windows](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#install-powershell-using-winget-recommended): you will need to update PowerShell to +7.4 to be able to run prowler, if not some checks will not show findings and the provider could not work as expected. This version of PowerShell is [supported](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#supported-versions-of-windows) on Windows 10, Windows 11, Windows Server 2016 and higher versions.
|
||||
|
||||
```console
|
||||
winget install --id Microsoft.PowerShell --source winget
|
||||
```
|
||||
|
||||
|
||||
- [MacOS](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.5#install-the-latest-stable-release-of-powershell): installing PowerShell on MacOS needs to have installed [brew](https://brew.sh/), once you have it is just running the command above, Pwsh is only supported in macOS 15 (Sequoia) x64 and Arm64, macOS 14 (Sonoma) x64 and Arm64, macOS 13 (Ventura) x64 and Arm64
|
||||
|
||||
```console
|
||||
brew install powershell/tap/powershell
|
||||
```
|
||||
|
||||
Once it's installed run `pwsh` on your terminal to verify it's working.
|
||||
|
||||
- Linux: installing PowerShell on Linux depends on the distro you are using:
|
||||
|
||||
- [Ubuntu](https://learn.microsoft.com/es-es/powershell/scripting/install/install-ubuntu?view=powershell-7.5#installation-via-package-repository-the-package-repository): The required version for installing PowerShell +7.4 on Ubuntu are Ubuntu 22.04 and Ubuntu 24.04. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
|
||||
|
||||
```console
|
||||
###################################
|
||||
# Prerequisites
|
||||
|
||||
# Update the list of packages
|
||||
sudo apt-get update
|
||||
|
||||
# Install pre-requisite packages.
|
||||
sudo apt-get install -y wget apt-transport-https software-properties-common
|
||||
|
||||
# Get the version of Ubuntu
|
||||
source /etc/os-release
|
||||
|
||||
# Download the Microsoft repository keys
|
||||
wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb
|
||||
|
||||
# Register the Microsoft repository keys
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
# Delete the Microsoft repository keys file
|
||||
rm packages-microsoft-prod.deb
|
||||
|
||||
# Update the list of packages after we added packages.microsoft.com
|
||||
sudo apt-get update
|
||||
|
||||
###################################
|
||||
# Install PowerShell
|
||||
sudo apt-get install -y powershell
|
||||
|
||||
# Start PowerShell
|
||||
pwsh
|
||||
```
|
||||
|
||||
- [Alpine](https://learn.microsoft.com/es-es/powershell/scripting/install/install-alpine?view=powershell-7.5#installation-steps): The only supported version for installing PowerShell +7.4 on Alpine is Alpine 3.20. The unique way to install it is downloading the tar.gz package available on [PowerShell github](https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz). You just need to follow the following steps:
|
||||
|
||||
```console
|
||||
# Install the requirements
|
||||
sudo apk add --no-cache \
|
||||
ca-certificates \
|
||||
less \
|
||||
ncurses-terminfo-base \
|
||||
krb5-libs \
|
||||
libgcc \
|
||||
libintl \
|
||||
libssl3 \
|
||||
libstdc++ \
|
||||
tzdata \
|
||||
userspace-rcu \
|
||||
zlib \
|
||||
icu-libs \
|
||||
curl
|
||||
|
||||
apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache \
|
||||
lttng-ust \
|
||||
openssh-client \
|
||||
|
||||
# Download the powershell '.tar.gz' archive
|
||||
curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz
|
||||
|
||||
# Create the target folder where powershell will be placed
|
||||
sudo mkdir -p /opt/microsoft/powershell/7
|
||||
|
||||
# Expand powershell to the target folder
|
||||
sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7
|
||||
|
||||
# Set execute permissions
|
||||
sudo chmod +x /opt/microsoft/powershell/7/pwsh
|
||||
|
||||
# Create the symbolic link that points to pwsh
|
||||
sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh
|
||||
|
||||
# Start PowerShell
|
||||
pwsh
|
||||
```
|
||||
|
||||
- [Debian](https://learn.microsoft.com/es-es/powershell/scripting/install/install-debian?view=powershell-7.5#installation-on-debian-11-or-12-via-the-package-repository): The required version for installing PowerShell +7.4 on Debian are Debian 11 and Debian 12. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
|
||||
|
||||
```console
|
||||
###################################
|
||||
# Prerequisites
|
||||
|
||||
# Update the list of packages
|
||||
sudo apt-get update
|
||||
|
||||
# Install pre-requisite packages.
|
||||
sudo apt-get install -y wget
|
||||
|
||||
# Get the version of Debian
|
||||
source /etc/os-release
|
||||
|
||||
# Download the Microsoft repository GPG keys
|
||||
wget -q https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb
|
||||
|
||||
# Register the Microsoft repository GPG keys
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
# Delete the Microsoft repository GPG keys file
|
||||
rm packages-microsoft-prod.deb
|
||||
|
||||
# Update the list of packages after we added packages.microsoft.com
|
||||
sudo apt-get update
|
||||
|
||||
###################################
|
||||
# Install PowerShell
|
||||
sudo apt-get install -y powershell
|
||||
|
||||
# Start PowerShell
|
||||
pwsh
|
||||
```
|
||||
|
||||
- [Rhel](https://learn.microsoft.com/es-es/powershell/scripting/install/install-rhel?view=powershell-7.5#installation-via-the-package-repository): The required version for installing PowerShell +7.4 on Red Hat are RHEL 8 and RHEL 9. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
|
||||
|
||||
```console
|
||||
###################################
|
||||
# Prerequisites
|
||||
|
||||
# Get version of RHEL
|
||||
source /etc/os-release
|
||||
if [ ${VERSION_ID%.*} -lt 8 ]
|
||||
then majorver=7
|
||||
elif [ ${VERSION_ID%.*} -lt 9 ]
|
||||
then majorver=8
|
||||
else majorver=9
|
||||
fi
|
||||
|
||||
# Download the Microsoft RedHat repository package
|
||||
curl -sSL -O https://packages.microsoft.com/config/rhel/$majorver/packages-microsoft-prod.rpm
|
||||
|
||||
# Register the Microsoft RedHat repository
|
||||
sudo rpm -i packages-microsoft-prod.rpm
|
||||
|
||||
# Delete the downloaded package after installing
|
||||
rm packages-microsoft-prod.rpm
|
||||
|
||||
# Update package index files
|
||||
sudo dnf update
|
||||
# Install PowerShell
|
||||
sudo dnf install powershell -y
|
||||
```
|
||||
|
||||
- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell:
|
||||
|
||||
```console
|
||||
docker pull mcr.microsoft.com/dotnet/sdk:9.0
|
||||
```
|
||||
|
||||
To start an interactive shell of Pwsh you just need to run:
|
||||
|
||||
```console
|
||||
docker run -it mcr.microsoft.com/dotnet/sdk:9.0 pwsh
|
||||
```
|
||||
|
||||
### Required PowerShell Modules
|
||||
|
||||
Prowler relies on several PowerShell cmdlets to retrieve necessary data.
|
||||
These cmdlets come from different modules that must be installed.
|
||||
|
||||
#### Automatic Installation
|
||||
|
||||
The required modules are automatically installed when running Prowler with the `--init-modules` flag.
|
||||
|
||||
Example command:
|
||||
|
||||
```console
|
||||
python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules
|
||||
```
|
||||
If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available.
|
||||
|
||||
???+ note
|
||||
Prowler installs the modules using `-Scope CurrentUser`.
|
||||
If you encounter any issues with services not working after the automatic installation, try installing the modules manually using `-Scope AllUsers` (administrator permissions are required for this).
|
||||
The command needed to install a module manually is:
|
||||
```powershell
|
||||
Install-Module -Name "ModuleName" -Scope AllUsers -Force
|
||||
```
|
||||
|
||||
#### Modules Version
|
||||
|
||||
- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (Minimum version: 3.6.0) Required for checks across Exchange, Defender, and Purview.
|
||||
- [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (Minimum version: 6.6.0) Required for all Teams checks.
|
||||
- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
|
||||
|
||||
[MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
|
||||
|
||||
## GitHub
|
||||
|
||||
Prowler supports multiple [authentication methods for GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api).
|
||||
|
||||
### Supported Authentication Methods
|
||||
|
||||
- **Personal Access Token (PAT)**
|
||||
- **OAuth App Token**
|
||||
- **GitHub App Credentials**
|
||||
|
||||
These options provide flexibility for scanning and analyzing your GitHub account, repositories, organizations, and applications. Choose the authentication method that best suits your security needs.
|
||||
|
||||
???+ note
|
||||
GitHub App Credentials support less checks than other authentication methods.
|
||||
|
||||
## Infrastructure as Code (IaC)
|
||||
|
||||
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans.
|
||||
|
||||
### Authentication
|
||||
|
||||
- For local scans, no authentication is required.
|
||||
- For remote repository scans, authentication can be provided via:
|
||||
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
|
||||
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
|
||||
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
|
||||
|
||||
### Supported Frameworks
|
||||
|
||||
The IaC provider leverages Checkov to support multiple frameworks, including:
|
||||
|
||||
- Terraform
|
||||
- CloudFormation
|
||||
- Kubernetes
|
||||
- ARM (Azure Resource Manager)
|
||||
- Serverless
|
||||
- Dockerfile
|
||||
- YAML/JSON (generic IaC)
|
||||
- Bicep
|
||||
- Helm
|
||||
- GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines, CircleCI, Argo Workflows
|
||||
- Ansible
|
||||
- Kustomize
|
||||
- OpenAPI
|
||||
- SAST, SCA (Software Composition Analysis)
|
||||
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 261 KiB |
@@ -1,787 +1,27 @@
|
||||
# What is Prowler?
|
||||
|
||||
**Prowler** is the open source cloud security platform trusted by thousands to **automate security and compliance** in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
|
||||
|
||||
The official supported providers right now are:
|
||||
|
||||
- **AWS**
|
||||
- **Azure**
|
||||
- **Google Cloud**
|
||||
- **Kubernetes**
|
||||
- **M365**
|
||||
- **Github**
|
||||
| Provider | Support | Stage | Interface |
|
||||
|----------|--------|-------|----------|
|
||||
| **AWS** | Official | Stable | UI, API, CLI |
|
||||
| **Azure** | Official | Stable | UI, API, CLI |
|
||||
| **Google Cloud** | Official | Stable | UI, API, CLI |
|
||||
| **Kubernetes** | Official | Stable | UI, API, CLI |
|
||||
| **M365** | Official | Stable | UI, API, CLI |
|
||||
| **Github** | Official | Stable | UI, API, CLI |
|
||||
| **IaC** | Official | Beta | CLI |
|
||||
| **MongoDB Atlas** | Official | Beta | CLI |
|
||||
| **LLM** | Official | Beta | CLI |
|
||||
| **NHN** | Unofficial | Beta | CLI |
|
||||
|
||||
Prowler supports **auditing, incident response, continuous monitoring, hardening, forensic readiness, and remediation**.
|
||||
|
||||
### Prowler Components
|
||||
### Products
|
||||
|
||||
- **Prowler CLI** (Command Line Interface) – Known as **Prowler Open Source**.
|
||||
- **Prowler Cloud** – A managed service built on top of Prowler CLI.
|
||||
More information: [Prowler Cloud](https://prowler.com)
|
||||
|
||||
## Prowler App
|
||||
|
||||

|
||||
|
||||
Prowler App is a web application that simplifies running Prowler. It provides:
|
||||
|
||||
- A **user-friendly interface** for configuring and executing scans.
|
||||
- A dashboard to **view results** and manage **security findings**.
|
||||
|
||||
### Installation Guide
|
||||
Refer to the [Quick Start](#prowler-app-installation) section for installation steps.
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
## Prowler Dashboard
|
||||
|
||||
```console
|
||||
prowler dashboard
|
||||
```
|
||||

|
||||
|
||||
Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including:
|
||||
|
||||
- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub)
|
||||
- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171
|
||||
- NIST Cybersecurity Framework (CSF)
|
||||
- CISA Guidelines
|
||||
- FedRAMP Low & Moderate
|
||||
- PCI DSS v3.2.1 and v4.0
|
||||
- ISO/IEC 27001:2013 and 2022
|
||||
- SOC 2
|
||||
- GDPR (General Data Protection Regulation)
|
||||
- HIPAA (Health Insurance Portability and Accountability Act)
|
||||
- FFIEC (Federal Financial Institutions Examination Council)
|
||||
- ENS RD2022 (Spanish National Security Framework)
|
||||
- GxP 21 CFR Part 11 and EU Annex 11
|
||||
- RBI Cybersecurity Framework (Reserve Bank of India)
|
||||
- KISA ISMS-P (Korean Information Security Management System)
|
||||
- MITRE ATT&CK
|
||||
- AWS Well-Architected Framework (Security & Reliability Pillars)
|
||||
- AWS Foundational Technical Review (FTR)
|
||||
- Microsoft NIS2 Directive (EU)
|
||||
- Custom threat scoring frameworks (prowler_threatscore)
|
||||
- Custom security frameworks for enterprise needs
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prowler App Installation
|
||||
|
||||
Prowler App supports multiple installation methods based on your environment.
|
||||
|
||||
Refer to the [Prowler App Tutorial](tutorials/prowler-app.md) for detailed usage instructions.
|
||||
|
||||
=== "Docker Compose"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
|
||||
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ note
|
||||
You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments.
|
||||
|
||||
???+ note
|
||||
There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode.
|
||||
|
||||
???+ warning
|
||||
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
|
||||
|
||||
=== "GitHub"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `git` installed.
|
||||
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
???+ warning
|
||||
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
|
||||
|
||||
_Commands to run the API_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
docker compose up postgres valkey -d \
|
||||
cd src/backend \
|
||||
python manage.py migrate --database admin \
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
|
||||
???+ important
|
||||
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
|
||||
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
_Commands to run the API Worker_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
_Commands to run the API Scheduler_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
```
|
||||
|
||||
_Commands to run the UI_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/ui \
|
||||
npm install \
|
||||
npm run build \
|
||||
npm start
|
||||
```
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ warning
|
||||
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
|
||||
|
||||
### Prowler CLI Installation
|
||||
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Consequently, it can be installed as Python package with `Python >= 3.9, <= 3.12`:
|
||||
|
||||
=== "pipx"
|
||||
|
||||
[pipx](https://pipx.pypa.io/stable/) is a tool to install Python applications in isolated environments. It is recommended to use `pipx` for a global installation.
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
To upgrade Prowler to the latest version, run:
|
||||
|
||||
``` bash
|
||||
pipx upgrade prowler
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
???+ warning
|
||||
This method is not recommended because it will modify the environment which you choose to install. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for a global installation.
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* `Python pip >= 21.0.0`
|
||||
* AWS, GCP, Azure, M365 and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
pip install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
To upgrade Prowler to the latest version, run:
|
||||
|
||||
``` bash
|
||||
pip install --upgrade prowler
|
||||
```
|
||||
|
||||
=== "Docker"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Have `docker` installed: https://docs.docker.com/get-docker/.
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \
|
||||
--name prowler \
|
||||
--env AWS_ACCESS_KEY_ID \
|
||||
--env AWS_SECRET_ACCESS_KEY \
|
||||
--env AWS_SESSION_TOKEN toniblyx/prowler:latest
|
||||
```
|
||||
|
||||
=== "GitHub"
|
||||
|
||||
_Requirements for Developers_:
|
||||
|
||||
* `git`
|
||||
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler
|
||||
poetry install
|
||||
poetry run python prowler-cli.py -v
|
||||
```
|
||||
???+ note
|
||||
If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
|
||||
|
||||
=== "Amazon Linux 2"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`.
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
pipx ensurepath
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "Brew"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Brew` installed in your Mac or Linux
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
brew install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "AWS CloudShell"
|
||||
|
||||
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Open AWS CloudShell `bash`.
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
sudo bash
|
||||
adduser prowler
|
||||
su prowler
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
cd /tmp
|
||||
prowler aws
|
||||
```
|
||||
|
||||
???+ note
|
||||
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv`
|
||||
|
||||
=== "Azure CloudShell"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Open Azure CloudShell `bash`.
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
cd /tmp
|
||||
prowler azure --az-cli-auth
|
||||
```
|
||||
|
||||
### Prowler App Update
|
||||
|
||||
You have two options to upgrade your Prowler App installation:
|
||||
|
||||
#### Option 1: Change env file with the following values
|
||||
|
||||
Edit your `.env` file and change the version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.9.0"
|
||||
PROWLER_API_VERSION="5.9.0"
|
||||
```
|
||||
|
||||
#### Option 2: Run the following command
|
||||
|
||||
```bash
|
||||
docker compose pull --policy always
|
||||
```
|
||||
|
||||
The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally.
|
||||
|
||||
|
||||
???+ note "What Gets Preserved During Upgrade"
|
||||
|
||||
Everything is preserved, nothing will be deleted after the update.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
If containers don't start, check logs for errors:
|
||||
|
||||
```bash
|
||||
# Check logs for errors
|
||||
docker compose logs
|
||||
|
||||
# Verify image versions
|
||||
docker images | grep prowler
|
||||
```
|
||||
|
||||
If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Prowler container versions
|
||||
|
||||
The available versions of Prowler CLI are the following:
|
||||
|
||||
- `latest`: in sync with `master` branch (please note that it is not a stable version)
|
||||
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v4-stable`: this tag always point to the latest release for v4.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
- Prowler CLI:
|
||||
|
||||
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
|
||||
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
|
||||
|
||||
- Prowler App:
|
||||
|
||||
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
|
||||
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
|
||||
|
||||
## High level architecture
|
||||
|
||||
You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more.
|
||||
|
||||

|
||||
|
||||
### Prowler App
|
||||
|
||||
The **Prowler App** consists of three main components:
|
||||
|
||||
- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js.
|
||||
- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework.
|
||||
- **Prowler SDK**: A Python SDK that integrates with Prowler CLI for advanced functionality.
|
||||
|
||||
The app leverages the following supporting infrastructure:
|
||||
|
||||
- **PostgreSQL**: Used for persistent storage of scan results.
|
||||
- **Celery Workers**: Facilitate asynchronous execution of Prowler scans.
|
||||
- **Valkey**: An in-memory database serving as a message broker for the Celery workers.
|
||||
|
||||

|
||||
|
||||
## Deprecations from v3
|
||||
|
||||
The following are the deprecations carried out from v3.
|
||||
|
||||
### General
|
||||
|
||||
- `Allowlist` now is called `Mutelist`.
|
||||
- The `--quiet` option has been deprecated. From now on use the `--status` flag to select the finding's status you want to get: PASS, FAIL or MANUAL.
|
||||
- All `INFO` finding's status has changed to `MANUAL`.
|
||||
- The CSV output format is common for all providers.
|
||||
|
||||
Some output formats are now deprecated:
|
||||
|
||||
- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers.
|
||||
|
||||
### AWS
|
||||
- Deprecate the AWS flag `--sts-endpoint-region` since AWS STS regional tokens are used.
|
||||
- To send only FAILS to AWS Security Hub, now you must use either `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Prowler App
|
||||
|
||||
#### **Access the App**
|
||||
|
||||
Go to [http://localhost:3000](http://localhost:3000) after installing the app (see [Quick Start](#prowler-app-installation)). Sign up with your email and password.
|
||||
|
||||
<img src="img/sign-up-button.png" alt="Sign Up Button" width="320"/>
|
||||
<img src="img/sign-up.png" alt="Sign Up" width="285"/>
|
||||
|
||||
???+ note "User creation and default tenant behavior"
|
||||
|
||||
When creating a new user, the behavior depends on whether an invitation is provided:
|
||||
|
||||
- **Without an invitation**:
|
||||
|
||||
- A new tenant is automatically created.
|
||||
- The new user is assigned to this tenant.
|
||||
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant.
|
||||
|
||||
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
|
||||
|
||||
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
|
||||
|
||||
#### Log In
|
||||
|
||||
Log in using your **email and password** to access the Prowler App.
|
||||
|
||||
<img src="img/log-in.png" alt="Log In" width="285"/>
|
||||
|
||||
#### Add a Cloud Provider
|
||||
|
||||
To configure a cloud provider for scanning:
|
||||
|
||||
1. Navigate to `Settings > Cloud Providers` and click `Add Account`.
|
||||
2. Select the cloud provider you wish to scan (**AWS, GCP, Azure, Kubernetes**).
|
||||
3. Enter the provider's identifier (Optional: Add an alias):
|
||||
- **AWS**: Account ID
|
||||
- **GCP**: Project ID
|
||||
- **Azure**: Subscription ID
|
||||
- **Kubernetes**: Cluster ID
|
||||
- **M36**: Domain ID
|
||||
4. Follow the guided instructions to add and authenticate your credentials.
|
||||
|
||||
#### Start a Scan
|
||||
|
||||
Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment.
|
||||
|
||||
Click `Go to Scans` to monitor progress.
|
||||
|
||||
#### View Results
|
||||
|
||||
While the scan is running, you can review findings in the following sections:
|
||||
|
||||
- **Overview** – Provides a high-level summary of your scans.
|
||||
<img src="img/overview.png" alt="Overview" width="700"/>
|
||||
|
||||
- **Compliance** – Displays compliance insights based on security frameworks.
|
||||
<img src="img/compliance.png" alt="Compliance" width="700"/>
|
||||
|
||||
> For detailed usage instructions, refer to the [Prowler App Guide](tutorials/prowler-app.md).
|
||||
|
||||
???+ note
|
||||
Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored.
|
||||
|
||||
### Prowler CLI
|
||||
|
||||
#### Running Prowler
|
||||
|
||||
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`):
|
||||
|
||||
???+ note
|
||||
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
???+ note
|
||||
Running the `prowler` command without options will uses environment variable credentials. Refer to the [Requirements](./getting-started/requirements.md) section for credential configuration details.
|
||||
|
||||
#### Verbose Output
|
||||
|
||||
If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled.
|
||||
|
||||
#### Report Generation
|
||||
|
||||
By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`:
|
||||
|
||||
```console
|
||||
prowler <provider> -M csv json-asff json-ocsf html
|
||||
```
|
||||
The HTML report is saved in the output directory, alongside other reports. It will look like this:
|
||||
|
||||

|
||||
|
||||
#### Listing Available Checks and Services
|
||||
|
||||
To view all available checks or services within a provider:, use `-l`/`--list-checks` or `--list-services`.
|
||||
|
||||
```console
|
||||
prowler <provider> --list-checks
|
||||
prowler <provider> --list-services
|
||||
```
|
||||
#### Running Specific Checks or Services
|
||||
|
||||
Execute specific checks or services using `-c`/`checks` or `-s`/`services`:
|
||||
|
||||
```console
|
||||
prowler azure --checks storage_blob_public_access_level_is_disabled
|
||||
prowler aws --services s3 ec2
|
||||
prowler gcp --services iam compute
|
||||
prowler kubernetes --services etcd apiserver
|
||||
```
|
||||
#### Excluding Checks and Services
|
||||
|
||||
Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`:
|
||||
|
||||
```console
|
||||
prowler aws --excluded-checks s3_bucket_public_access
|
||||
prowler azure --excluded-services defender iam
|
||||
prowler gcp --excluded-services kms
|
||||
prowler kubernetes --excluded-services controllermanager
|
||||
```
|
||||
#### Additional Options
|
||||
|
||||
Explore more advanced time-saving execution methods in the [Miscellaneous](tutorials/misc.md) section.
|
||||
|
||||
To access the help menu and view all available options, use: `-h`/`--help`:
|
||||
|
||||
```console
|
||||
prowler --help
|
||||
```
|
||||
|
||||
#### AWS
|
||||
|
||||
Use a custom AWS profile with `-p`/`--profile` and/or the AWS regions you want to audit with `-f`/`--filter-region`:
|
||||
|
||||
```console
|
||||
prowler aws --profile custom-profile -f us-east-1 eu-south-2
|
||||
```
|
||||
|
||||
???+ note
|
||||
By default, `prowler` will scan all AWS regions.
|
||||
|
||||
See more details about AWS Authentication in the [Requirements](getting-started/requirements.md#aws) section.
|
||||
|
||||
#### Azure
|
||||
|
||||
Azure requires specifying the auth method:
|
||||
|
||||
```console
|
||||
# To use service principal authentication
|
||||
prowler azure --sp-env-auth
|
||||
|
||||
# To use az cli authentication
|
||||
prowler azure --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler azure --browser-auth --tenant-id "XXXXXXXX"
|
||||
|
||||
# To use managed identity auth
|
||||
prowler azure --managed-identity-auth
|
||||
```
|
||||
|
||||
See more details about Azure Authentication in [Requirements](getting-started/requirements.md#azure)
|
||||
|
||||
By default, Prowler scans all the subscriptions for which it has permissions. To scan a single or various specific subscription you can use the following flag (using az cli auth as example):
|
||||
|
||||
```console
|
||||
prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription ID 2> ... <subscription ID N>
|
||||
```
|
||||
#### Google Cloud
|
||||
|
||||
- **User Account Credentials**
|
||||
|
||||
By default, Prowler uses **User Account credentials**. You can configure your account using:
|
||||
|
||||
- `gcloud init` – Set up a new account.
|
||||
- `gcloud config set account <account>` – Switch to an existing account.
|
||||
|
||||
Once configured, obtain access credentials using: `gcloud auth application-default login`.
|
||||
|
||||
- **Service Account Authentication**
|
||||
|
||||
Alternatively, you can use Service Account credentials:
|
||||
|
||||
Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details.
|
||||
|
||||
Provide the key file location using this argument:
|
||||
|
||||
```console
|
||||
prowler gcp --credentials-file path
|
||||
```
|
||||
|
||||
- **Scanning Specific GCP Projects**
|
||||
|
||||
By default, Prowler scans all accessible GCP projects. To scan specific projects, use the `--project-ids` flag:
|
||||
|
||||
```console
|
||||
prowler gcp --project-ids <Project ID 1> <Project ID 2> ... <Project ID N>
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution.
|
||||
|
||||
- **Non In-Cluster Execution**
|
||||
|
||||
```console
|
||||
prowler kubernetes --kubeconfig-file path
|
||||
```
|
||||
???+ note
|
||||
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
|
||||
|
||||
- **In-Cluster Execution**
|
||||
|
||||
To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace:
|
||||
|
||||
```console
|
||||
kubectl apply -f kubernetes/prowler-sa.yaml
|
||||
kubectl apply -f kubernetes/job.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
|
||||
kubectl logs prowler-XXXXX --namespace prowler-ns
|
||||
```
|
||||
|
||||
???+ note
|
||||
By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
|
||||
|
||||
#### Microsoft 365
|
||||
|
||||
Microsoft 365 requires specifying the auth method:
|
||||
|
||||
```console
|
||||
|
||||
# To use service principal authentication for MSGraph and PowerShell modules
|
||||
prowler m365 --sp-env-auth
|
||||
|
||||
# To use both service principal (for MSGraph) and user credentials (for PowerShell modules)
|
||||
prowler m365 --env-auth
|
||||
|
||||
# To use az cli authentication
|
||||
prowler m365 --az-cli-auth
|
||||
|
||||
# To use browser authentication
|
||||
prowler m365 --browser-auth --tenant-id "XXXXXXXX"
|
||||
|
||||
```
|
||||
|
||||
See more details about M365 Authentication in the [Requirements](getting-started/requirements.md#microsoft-365) section.
|
||||
|
||||
#### GitHub
|
||||
|
||||
Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**.
|
||||
|
||||
- **Supported Authentication Methods**
|
||||
|
||||
Authenticate using one of the following methods:
|
||||
|
||||
```console
|
||||
# Personal Access Token (PAT):
|
||||
prowler github --personal-access-token pat
|
||||
|
||||
# OAuth App Token:
|
||||
prowler github --oauth-app-token oauth_token
|
||||
|
||||
# GitHub App Credentials:
|
||||
prowler github --github-app-id app_id --github-app-key /path/to/private-key.pem
|
||||
```
|
||||
|
||||
???+ note
|
||||
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
|
||||
|
||||
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
|
||||
2. `OAUTH_APP_TOKEN`
|
||||
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
|
||||
|
||||
#### Infrastructure as Code (IaC)
|
||||
|
||||
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
|
||||
|
||||
```console
|
||||
# Scan a directory for IaC files
|
||||
prowler iac --scan-path ./my-iac-directory
|
||||
|
||||
# Scan a remote GitHub repository (public or private)
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git
|
||||
|
||||
# Authenticate to a private repo with GitHub username and PAT
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git \
|
||||
--github-username <username> --personal-access-token <token>
|
||||
|
||||
# Authenticate to a private repo with OAuth App Token
|
||||
prowler iac --scan-repository-url https://github.com/user/repo.git \
|
||||
--oauth-app-token <oauth_token>
|
||||
|
||||
# Specify frameworks to scan (default: all)
|
||||
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
|
||||
|
||||
# Exclude specific paths
|
||||
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
|
||||
```
|
||||
|
||||
???+ note
|
||||
- `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time.
|
||||
- For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence.
|
||||
- The IaC provider does not require cloud authentication for local scans.
|
||||
- It is ideal for CI/CD pipelines and local development environments.
|
||||
- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html)
|
||||
|
||||
See more details about IaC scanning in the [IaC Tutorial](tutorials/iac/getting-started-iac.md) section.
|
||||
|
||||
## Prowler v2 Documentation
|
||||
|
||||
For **Prowler v2 Documentation**, refer to the [official repository](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md).
|
||||
- **Prowler CLI** (Command Line Interface)
|
||||
- **Prowler App** (Web Application)
|
||||
- [**Prowler Cloud**](https://cloud.prowler.com) – A managed service built on top of Prowler App.
|
||||
- [**Prowler Hub**](https://hub.prowler.com) – A public library of versioned checks, cloud service artifacts, and compliance frameworks.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
### Installation
|
||||
|
||||
Prowler App supports multiple installation methods based on your environment.
|
||||
|
||||
Refer to the [Prowler App Tutorial](../tutorials/prowler-app.md) for detailed usage instructions.
|
||||
|
||||
=== "Docker Compose"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
|
||||
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ note
|
||||
You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments.
|
||||
|
||||
???+ note
|
||||
For a secure setup, leave empty or remove `DJANGO_TOKEN_SIGNING_KEY` and `DJANGO_TOKEN_VERIFYING_KEY` in `.env` before first start. When absent, the API auto‑generates a unique key pair and stores it in `~/.config/prowler-api` (non-container) or the bound Docker volume in `_data/api` (container). Never commit or reuse static/default keys. To rotate, delete the stored key files and restart the API.
|
||||
|
||||
???+ note
|
||||
There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode.
|
||||
|
||||
???+ warning
|
||||
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
|
||||
|
||||
=== "GitHub"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `git` installed.
|
||||
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
???+ warning
|
||||
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
|
||||
|
||||
_Commands to run the API_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
docker compose up postgres valkey -d \
|
||||
cd src/backend \
|
||||
python manage.py migrate --database admin \
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
|
||||
???+ important
|
||||
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
|
||||
|
||||
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
|
||||
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
|
||||
|
||||
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
_Commands to run the API Worker_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
_Commands to run the API Scheduler_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/api \
|
||||
poetry install \
|
||||
eval $(poetry env activate) \
|
||||
set -a \
|
||||
source .env \
|
||||
cd src/backend \
|
||||
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
```
|
||||
|
||||
_Commands to run the UI_:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/ui \
|
||||
npm install \
|
||||
npm run build \
|
||||
npm start
|
||||
```
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
???+ warning
|
||||
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
|
||||
|
||||
|
||||
### Update Prowler App
|
||||
|
||||
Upgrade Prowler App installation using one of two options:
|
||||
|
||||
#### Option 1: Update Environment File
|
||||
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.9.0"
|
||||
PROWLER_API_VERSION="5.9.0"
|
||||
```
|
||||
|
||||
#### Option 2: Use Docker Compose Pull
|
||||
|
||||
```bash
|
||||
docker compose pull --policy always
|
||||
```
|
||||
|
||||
The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally.
|
||||
|
||||
|
||||
???+ note "What Gets Preserved During Upgrade"
|
||||
Everything is preserved, nothing will be deleted after the update.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If containers don't start, check logs for errors:
|
||||
|
||||
```bash
|
||||
# Check logs for errors
|
||||
docker compose logs
|
||||
|
||||
# Verify image versions
|
||||
docker images | grep prowler
|
||||
```
|
||||
|
||||
If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
|
||||
### Container versions
|
||||
|
||||
The available versions of Prowler CLI are the following:
|
||||
|
||||
- `latest`: in sync with `master` branch (please note that it is not a stable version)
|
||||
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v4-stable`: this tag always point to the latest release for v4.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
- Prowler App:
|
||||
|
||||
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
|
||||
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
|
||||
@@ -0,0 +1,208 @@
|
||||
|
||||
## Installation
|
||||
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Install it as a Python package with `Python >= 3.9, <= 3.12`:
|
||||
|
||||
=== "pipx"
|
||||
|
||||
[pipx](https://pipx.pypa.io/stable/) installs Python applications in isolated environments. Use `pipx` for global installation.
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
Upgrade Prowler to the latest version:
|
||||
|
||||
``` bash
|
||||
pipx upgrade prowler
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
???+ warning
|
||||
This method modifies the chosen installation environment. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for global installation.
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* `Python pip >= 21.0.0`
|
||||
* AWS, GCP, Azure, M365 and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
pip install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
Upgrade Prowler to the latest version:
|
||||
|
||||
``` bash
|
||||
pip install --upgrade prowler
|
||||
```
|
||||
|
||||
=== "Docker"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Have `docker` installed: https://docs.docker.com/get-docker/.
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \
|
||||
--name prowler \
|
||||
--env AWS_ACCESS_KEY_ID \
|
||||
--env AWS_SECRET_ACCESS_KEY \
|
||||
--env AWS_SESSION_TOKEN toniblyx/prowler:latest
|
||||
```
|
||||
|
||||
=== "GitHub"
|
||||
|
||||
_Requirements for Developers_:
|
||||
|
||||
* `git`
|
||||
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler
|
||||
poetry install
|
||||
poetry run python prowler-cli.py -v
|
||||
```
|
||||
???+ note
|
||||
If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
|
||||
|
||||
=== "Amazon Linux 2"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
```
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "Ubuntu"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`.
|
||||
* `Python >= 3.9, <= 3.12`
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
pipx ensurepath
|
||||
pipx install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "Brew"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* `Brew` installed in your Mac or Linux
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
``` bash
|
||||
brew install prowler
|
||||
prowler -v
|
||||
```
|
||||
|
||||
=== "AWS CloudShell"
|
||||
|
||||
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Open AWS CloudShell `bash`.
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
sudo bash
|
||||
adduser prowler
|
||||
su prowler
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
cd /tmp
|
||||
prowler aws
|
||||
```
|
||||
|
||||
???+ note
|
||||
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv`
|
||||
|
||||
=== "Azure CloudShell"
|
||||
|
||||
_Requirements_:
|
||||
|
||||
* Open Azure CloudShell `bash`.
|
||||
|
||||
_Commands_:
|
||||
|
||||
```bash
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
pipx install prowler
|
||||
cd /tmp
|
||||
prowler azure --az-cli-auth
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Container versions
|
||||
|
||||
The available versions of Prowler CLI are the following:
|
||||
|
||||
- `latest`: in sync with `master` branch (please note that it is not a stable version)
|
||||
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v4-stable`: this tag always point to the latest release for v4.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
- Prowler CLI:
|
||||
|
||||
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
|
||||
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
|
||||
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
@@ -0,0 +1,22 @@
|
||||
Prowler App is a web application that simplifies running Prowler. It provides:
|
||||
|
||||
- **User-friendly interface** for configuring and executing scans
|
||||
- Dashboard to **view results** and manage **security findings**
|
||||
|
||||

|
||||
|
||||
## Components
|
||||
|
||||
Prowler App consists of three main components:
|
||||
|
||||
- **Prowler UI**: User-friendly web interface for running Prowler and viewing results, powered by Next.js
|
||||
- **Prowler API**: Backend API that executes Prowler scans and stores results, built with Django REST Framework
|
||||
- **Prowler SDK**: Python SDK that integrates with Prowler CLI for advanced functionality
|
||||
|
||||
Supporting infrastructure includes:
|
||||
|
||||
- **PostgreSQL**: Persistent storage of scan results
|
||||
- **Celery Workers**: Asynchronous execution of Prowler scans
|
||||
- **Valkey**: In-memory database serving as message broker for Celery workers
|
||||
|
||||

|
||||
@@ -0,0 +1,37 @@
|
||||
Prowler CLI is a command-line interface for running Prowler scans from the terminal.
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
## Prowler Dashboard
|
||||
|
||||
```console
|
||||
prowler dashboard
|
||||
```
|
||||

|
||||
|
||||
Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including:
|
||||
|
||||
- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub)
|
||||
- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171
|
||||
- NIST Cybersecurity Framework (CSF)
|
||||
- CISA Guidelines
|
||||
- FedRAMP Low & Moderate
|
||||
- PCI DSS v3.2.1 and v4.0
|
||||
- ISO/IEC 27001:2013 and 2022
|
||||
- SOC 2
|
||||
- GDPR (General Data Protection Regulation)
|
||||
- HIPAA (Health Insurance Portability and Accountability Act)
|
||||
- FFIEC (Federal Financial Institutions Examination Council)
|
||||
- ENS RD2022 (Spanish National Security Framework)
|
||||
- GxP 21 CFR Part 11 and EU Annex 11
|
||||
- RBI Cybersecurity Framework (Reserve Bank of India)
|
||||
- KISA ISMS-P (Korean Information Security Management System)
|
||||
- MITRE ATT&CK
|
||||
- AWS Well-Architected Framework (Security & Reliability Pillars)
|
||||
- AWS Foundational Technical Review (FTR)
|
||||
- Microsoft NIS2 Directive (EU)
|
||||
- Custom threat scoring frameworks (prowler_threatscore)
|
||||
- Custom security frameworks for enterprise needs
|
||||
@@ -1,24 +1,87 @@
|
||||
# Security
|
||||
|
||||
## Compliance and Trust
|
||||
We publish our live SOC 2 Type 2 Compliance data at [https://trust.prowler.com](https://trust.prowler.com)
|
||||
|
||||
As an **AWS Partner**, we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/).
|
||||
|
||||
|
||||
## Encryption (Prowler Cloud)
|
||||
|
||||
We use encryption everywhere possible. The data and communications used by **Prowler Cloud** are **encrypted at-rest** and **in-transit**.
|
||||
|
||||
## Data Retention Policy (Prowler Cloud)
|
||||
|
||||
Prowler Cloud is GDPR compliant in regards to personal data and the ["right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/). When a user deletes their account their user information will be deleted from Prowler Cloud online and backup systems within 10 calendar days.
|
||||
|
||||
## Software Security
|
||||
|
||||
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
|
||||
We follow a **security-by-design approach** throughout our software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment.
|
||||
|
||||
- `bandit` for code security review.
|
||||
- `safety` and `dependabot` for dependencies.
|
||||
- `hadolint` and `dockle` for our containers security.
|
||||
- `snyk` in Docker Hub.
|
||||
- `clair` in Amazon ECR.
|
||||
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
|
||||
We enforce [pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations to catch issues early, and [our CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates to ensure code quality, secure configurations, and compliance with internal standards.
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
Our container registries are continuously scanned for vulnerabilities, with findings automatically reported to our security team for assessment and remediation. This process evolves alongside our stack as we adopt new languages, frameworks, and technologies, ensuring our security practices remain comprehensive, proactive, and adaptable.
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler Cloud service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com).
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
The information you share with the Prowler team as part of this process is kept confidential within Prowler. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
|
||||
|
||||
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
|
||||
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
|
||||
|
||||
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
|
||||
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
|
||||
|
||||
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
|
||||
- Social engineering support or attacks requiring social engineering.
|
||||
- Clickjacking on pages with no sensitive actions.
|
||||
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
|
||||
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
|
||||
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
|
||||
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
|
||||
- Missing best practices in SSL/TLS configuration.
|
||||
- Any activity that could lead to the disruption of service (DoS).
|
||||
- Rate limiting or brute force issues on non-authentication endpoints.
|
||||
- Missing best practices in Content Security Policy (CSP).
|
||||
- Missing HttpOnly or Secure flags on cookies.
|
||||
- Configuration of or missing security headers.
|
||||
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
|
||||
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
|
||||
- Software version disclosure, banner identification issues, or descriptive error messages.
|
||||
- Tabnabbing.
|
||||
- Issues that require unlikely user interaction.
|
||||
- Improper logout functionality and improper session timeout.
|
||||
- CORS misconfiguration without an exploitation scenario.
|
||||
- Broken link hijacking.
|
||||
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
|
||||
- Content spoofing and text injection issues without a clear attack vector.
|
||||
- Email spoofing without exploiting security flaws.
|
||||
- Dead links or broken links.
|
||||
- User enumeration.
|
||||
|
||||
Testing guidelines:
|
||||
|
||||
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
|
||||
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
|
||||
|
||||
Reporting guidelines:
|
||||
|
||||
- File a report through our Support Desk at https://support.prowler.com
|
||||
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
|
||||
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
|
||||
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
|
||||
|
||||
Disclosure guidelines:
|
||||
|
||||
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
|
||||
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
|
||||
- Data regarding any Prowler user or customer projects.
|
||||
- Prowler customers' data.
|
||||
- Information about Prowler employees, contractors or partners.
|
||||
|
||||
What we promise:
|
||||
|
||||
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
|
||||
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
|
||||
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
|
||||
- We will keep you informed of the progress towards resolving the problem.
|
||||
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
|
||||
|
||||
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
|
||||
|
||||
@@ -1,49 +1,130 @@
|
||||
# AWS Authentication in Prowler
|
||||
|
||||
Proper authentication is required for Prowler to perform security checks across AWS resources. Ensure that AWS-CLI is correctly configured or manually declare AWS credentials before running scans.
|
||||
Prowler requires AWS credentials to function properly. Authentication is available through the following methods:
|
||||
|
||||
## Configure AWS Credentials
|
||||
- Static Credentials
|
||||
- Assumed Role
|
||||
|
||||
Use one of the following methods to authenticate:
|
||||
## Required Permissions
|
||||
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||
|
||||
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
|
||||
|
||||
## Assign Required AWS Permissions
|
||||
To ensure full functionality, attach the following AWS managed policies to the designated user or role:
|
||||
|
||||
- `arn:aws:iam::aws:policy/SecurityAudit`
|
||||
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
|
||||
|
||||
???+ note
|
||||
Some security checks require read-only additional permissions. Attach the following custom policies to the role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json). If you want Prowler to send findings to [AWS Security Hub](https://aws.amazon.com/security-hub), make sure to also attach the custom policy: [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json).
|
||||
### Additional Permissions
|
||||
|
||||
## AWS Profiles and Service Scanning in Prowler
|
||||
For certain checks, additional read-only permissions are required. Attach the following custom policy to your role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json)
|
||||
|
||||
Prowler supports authentication and security assessments using custom AWS profiles and can optionally scan unused services.
|
||||
|
||||
**Using Custom AWS Profiles**
|
||||
## Assume Role (Recommended)
|
||||
|
||||
Prowler allows you to specify a custom AWS profile using the following command:
|
||||
This method grants permanent access and is the recommended setup for production environments.
|
||||
|
||||
```console
|
||||
prowler aws -p/--profile <profile_name>
|
||||
```
|
||||
=== "CloudFormation"
|
||||
|
||||
## Multi-Factor Authentication (MFA)
|
||||
1. Download the [Prowler Scan Role Template](https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/permissions/templates/cloudformation/prowler-scan-role.yml)
|
||||
|
||||
If MFA enforcement is required for your IAM entity, you can use `--mfa`. Prowler will prompt you to enter the following in order to get a new session:
|
||||

|
||||
|
||||
- ARN of your MFA device
|
||||
- TOTP (Time-Based One-Time Password)
|
||||

|
||||
|
||||
2. Open the [AWS Console](https://console.aws.amazon.com), search for **CloudFormation**
|
||||
|
||||

|
||||
|
||||
3. Go to **Stacks** and click "Create stack" > "With new resources (standard)"
|
||||
|
||||

|
||||
|
||||
4. In **Specify Template**, choose "Upload a template file" and select the downloaded file
|
||||
|
||||

|
||||

|
||||
|
||||
5. Click "Next", provide a stack name and the **External ID** shown in the Prowler Cloud setup screen
|
||||
|
||||

|
||||

|
||||
|
||||
!!! info
|
||||
An **External ID** is required when assuming the *ProwlerScan* role to comply with AWS [confused deputy prevention](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||
|
||||
6. Acknowledge the IAM resource creation warning and proceed
|
||||
|
||||

|
||||
|
||||
7. Click "Submit" to deploy the stack
|
||||
|
||||

|
||||
|
||||
=== "Terraform"
|
||||
|
||||
To provision the scan role using Terraform:
|
||||
|
||||
1. Run the following commands:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
2. During `plan` and `apply`, provide the **External ID** when prompted, which is available in the Prowler Cloud or Prowler App UI:
|
||||
|
||||

|
||||
|
||||
> 💡 Note: Terraform will use the AWS credentials of the default profile.
|
||||
|
||||
---
|
||||
|
||||
## Credentials
|
||||
=== "Long term credentials"
|
||||
|
||||
1. Go to the [AWS Console](https://console.aws.amazon.com), open **CloudShell**
|
||||
|
||||

|
||||
|
||||
2. Run:
|
||||
|
||||
```bash
|
||||
aws iam create-access-key
|
||||
```
|
||||
|
||||
3. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
|
||||

|
||||
|
||||
=== "Short term credentials (Recommended)"
|
||||
|
||||
Use the [AWS Access Portal](https://docs.aws.amazon.com/singlesignon/latest/userguide/howtogetcredentials.html) or the CLI:
|
||||
|
||||
1. Retrieve short-term credentials for the IAM identity using this command:
|
||||
|
||||
```bash
|
||||
aws sts get-session-token --duration-seconds 900
|
||||
```
|
||||
|
||||
???+ note
|
||||
Check the aws documentation [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/sts_example_sts_GetSessionToken_section.html)
|
||||
|
||||
2. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
- `SessionToken`
|
||||
|
||||
> Sample output:
|
||||
```json
|
||||
{
|
||||
"Credentials": {
|
||||
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
||||
"SessionToken": "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE",
|
||||
"Expiration": "2020-05-19T18:06:10+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
# Getting Started with AWS on Prowler Cloud/App
|
||||
# Getting Started With AWS on Prowler
|
||||
|
||||
## Prowler App
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/RPgIWOCERzY" title="Prowler Cloud Onboarding AWS" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
|
||||
Set up your AWS account to enable security scanning using Prowler Cloud/App.
|
||||
> Walkthrough video onboarding an AWS Account using Assumed Role.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your AWS account, you’ll need:
|
||||
|
||||
1. Access to Prowler Cloud/App
|
||||
2. Properly configured AWS credentials (either static or via an assumed IAM role)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Get Your AWS Account ID
|
||||
### Step 1: Get Your AWS Account ID
|
||||
|
||||
1. Log in to the [AWS Console](https://console.aws.amazon.com)
|
||||
2. Locate your AWS account ID in the top-right dropdown menu
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Step 2: Access Prowler Cloud/App
|
||||
### Step 2: Access Prowler Cloud or Prowler App
|
||||
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](../prowler-app.md)
|
||||
2. Go to `Configuration` > `Cloud Providers`
|
||||
2. Go to "Configuration" > "Cloud Providers"
|
||||
|
||||

|
||||
|
||||
3. Click `Add Cloud Provider`
|
||||
3. Click "Add Cloud Provider"
|
||||
|
||||

|
||||
|
||||
4. Select `Amazon Web Services`
|
||||
4. Select "Amazon Web Services"
|
||||
|
||||

|
||||
|
||||
@@ -41,93 +33,39 @@ To configure your AWS account, you’ll need:
|
||||
|
||||

|
||||
|
||||
6. Choose your preferred authentication method (next step)
|
||||
6. Choose the preferred authentication method (next step)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Step 3: Set Up AWS Authentication
|
||||
### Step 3: Set Up AWS Authentication
|
||||
|
||||
Before proceeding, choose your preferred authentication mode:
|
||||
Before proceeding, choose the preferred authentication mode:
|
||||
|
||||
Credentials
|
||||
**Credentials**
|
||||
|
||||
* Quick scan as current user ✅
|
||||
* No extra setup ✅
|
||||
* Credentials time out ❌
|
||||
* Quick scan as current user
|
||||
* No extra setup
|
||||
* Credentials time out
|
||||
|
||||
Assumed Role
|
||||
**Assumed Role**
|
||||
|
||||
* Preferred Setup
|
||||
* Permanent Credentials
|
||||
* Requires access to create role
|
||||
|
||||
* Preferred Setup ✅
|
||||
* Permanent Credentials ✅
|
||||
* Requires access to create role ❌
|
||||
|
||||
---
|
||||
|
||||
### 🔐 Assume Role (Recommended)
|
||||
|
||||

|
||||
#### Assume Role (Recommended)
|
||||
|
||||
This method grants permanent access and is the recommended setup for production environments.
|
||||
|
||||
=== "CloudFormation"
|
||||

|
||||
|
||||
1. Download the [Prowler Scan Role Template](https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/permissions/templates/cloudformation/prowler-scan-role.yml)
|
||||
For detailed instructions on how to create the role, see [Authentication > Assume Role](./authentication.md#assume-role-recommended).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. Open the [AWS Console](https://console.aws.amazon.com), search for **CloudFormation**
|
||||
|
||||

|
||||
|
||||
3. Go to **Stacks** and click `Create stack` > `With new resources (standard)`
|
||||
|
||||

|
||||
|
||||
4. In **Specify Template**, choose `Upload a template file` and select the downloaded file
|
||||
|
||||

|
||||

|
||||
|
||||
5. Click `Next`, provide a stack name and the **External ID** shown in the Prowler Cloud setup screen
|
||||
|
||||

|
||||

|
||||
|
||||
6. Acknowledge the IAM resource creation warning and proceed
|
||||
|
||||

|
||||
|
||||
7. Click `Submit` to deploy the stack
|
||||
|
||||

|
||||
|
||||
=== "Terraform"
|
||||
|
||||
To provision the scan role using Terraform:
|
||||
|
||||
1. Run the following commands:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
2. During `plan` and `apply`, you will be prompted for the **External ID**, which is available in the Prowler Cloud/App UI:
|
||||
|
||||

|
||||
|
||||
> 💡 Note: Terraform will use the AWS credentials of your default profile.
|
||||
|
||||
---
|
||||
|
||||
### Finish Setup with Assume Role
|
||||
|
||||
8. Once the role is created, go to the **IAM Console**, click on the `ProwlerScan` role to open its details:
|
||||
8. Once the role is created, go to the **IAM Console**, click on the "ProwlerScan" role to open its details:
|
||||
|
||||

|
||||
|
||||
@@ -135,80 +73,69 @@ This method grants permanent access and is the recommended setup for production
|
||||
|
||||

|
||||
|
||||
10. Paste the ARN into the corresponding field in Prowler Cloud/App
|
||||
10. Paste the ARN into the corresponding field in Prowler Cloud or Prowler App
|
||||
|
||||

|
||||
|
||||
11. Click `Next`, then `Launch Scan`
|
||||
11. Click "Next", then "Launch Scan"
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🔑 Credentials (Static Access Keys)
|
||||
#### Credentials (Static Access Keys)
|
||||
|
||||
You can also configure your AWS account using static credentials (not recommended for long-term use):
|
||||
AWS accounts can also be configured using static credentials (not recommended for long-term use):
|
||||
|
||||

|
||||
|
||||
=== "Long term credentials"
|
||||
For detailed instructions on how to create the credentials, see [Authentication > Credentials](./authentication.md#credentials).
|
||||
|
||||
1. Go to the [AWS Console](https://console.aws.amazon.com), open **CloudShell**
|
||||
1. Complete the form in Prowler Cloud or Prowler App and click "Next"
|
||||
|
||||

|
||||

|
||||
|
||||
2. Run:
|
||||
2. Click "Launch Scan"
|
||||
|
||||
```bash
|
||||
aws iam create-access-key
|
||||
```
|
||||

|
||||
|
||||
3. Copy the output containing:
|
||||
---
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
## Prowler CLI
|
||||
|
||||

|
||||
### Configure AWS Credentials
|
||||
|
||||
> ⚠️ Save these credentials securely and paste them into the Prowler Cloud/App setup screen.
|
||||
To authenticate with AWS, use one of the following methods:
|
||||
|
||||
=== "Short term credentials (Recommended)"
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
|
||||
You can use your [AWS Access Portal](https://docs.aws.amazon.com/singlesignon/latest/userguide/howtogetcredentials.html) or the CLI:
|
||||
or
|
||||
|
||||
1. Retrieve short-term credentials for the IAM identity using this command:
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||
|
||||
```bash
|
||||
aws sts get-session-token --duration-seconds 900
|
||||
```
|
||||
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
|
||||
|
||||
???+ note
|
||||
Check the aws documentation [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/sts_example_sts_GetSessionToken_section.html)
|
||||
More details on Assume Role settings from the CLI in [Assume Role](./role-assumption.md) page.
|
||||
|
||||
2. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
### AWS Profiles
|
||||
|
||||
> Sample output:
|
||||
```json
|
||||
{
|
||||
"Credentials": {
|
||||
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
||||
"SessionToken": "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE",
|
||||
"Expiration": "2020-05-19T18:06:10+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
To use a custom AWS profile, specify it with the following command:
|
||||
|
||||
> ⚠️ Save these credentials securely and paste them into the Prowler Cloud/App setup screen.
|
||||
```console
|
||||
prowler aws -p/--profile <profile_name>
|
||||
```
|
||||
|
||||
Complete the form in Prowler Cloud/App and click `Next`
|
||||
### Multi-Factor Authentication (MFA)
|
||||
|
||||

|
||||
For IAM entities requiring Multi-Factor Authentication (MFA), use the `--mfa` flag. Prowler prompts for the following values to initiate a new session:
|
||||
|
||||
Click `Launch Scan`
|
||||
|
||||

|
||||
- **ARN of your MFA device**
|
||||
- **TOTP (time-based one-time password)**
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# AWS Organizations in Prowler
|
||||
|
||||
Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally.
|
||||
|
||||
When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role.
|
||||
|
||||
> ℹ️ Trusted access can be enabled in the Management Account from the AWS Console under **AWS Organizations → Settings → Trusted access for AWS CloudFormation StackSets**.
|
||||
|
||||
When not using StackSets or Prowler and only needing to scan AWS Organization accounts using the CLI, it is possible to assume a role in each account manually or automate that logic with custom scripts.
|
||||
|
||||
## Retrieving AWS Account Details
|
||||
|
||||
If AWS Organizations is enabled, Prowler can fetch detailed account information during scans, including:
|
||||
@@ -33,7 +41,7 @@ Prowler will scan the AWS account and get the account details from AWS Organizat
|
||||
|
||||
### Handling JSON Output
|
||||
|
||||
In Prowler’s JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings.
|
||||
In Prowler's JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings.
|
||||
|
||||
```json
|
||||
"Account Email": "my-prod-account@domain.com",
|
||||
@@ -51,6 +59,67 @@ The additional fields in CSV header output are as follows:
|
||||
- ACCOUNT\_DETAILS\_ORG
|
||||
- ACCOUNT\_DETAILS\_TAGS
|
||||
|
||||
## Deploying Prowler IAM Roles Across AWS Organizations
|
||||
|
||||
When onboarding multiple AWS accounts into Prowler Cloud, it is important to deploy the Prowler Scan IAM Role in each account. The most efficient way to do this across an AWS Organization is by leveraging AWS CloudFormation StackSets, which rolls out infrastructure—like IAM roles—to all accounts centrally from the Management or Delegated Admin account.
|
||||
|
||||
When using Infrastructure as Code (IaC), Terraform is recommended to manage this deployment systematically.
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
- **Use StackSets** from the **Management Account** (or a Delegated Admin/Security Account).
|
||||
- **Use Terraform** to orchestrate the deployment.
|
||||
- **Use the official CloudFormation template** provided by Prowler.
|
||||
- Target specific Organizational Units (OUs) or the entire Organization.
|
||||
|
||||
???+ note
|
||||
A detailed community article this implementation is based on is available here:
|
||||
[Deploy IAM Roles Across an AWS Organization as Code (Unicrons)](https://unicrons.cloud/en/2024/10/14/deploy-iam-roles-across-an-aws-organization-as-code/)
|
||||
This guide has been adapted with permission and aligned with Prowler’s IAM role requirements.
|
||||
|
||||
---
|
||||
|
||||
### Step-by-Step Guide Using Terraform
|
||||
|
||||
Below is a ready Terraform snippet that deploys the [Prowler Scan IAM Role CloudFormation template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml) across the AWS Organization using StackSets:
|
||||
|
||||
```hcl title="main.tf"
|
||||
data "aws_caller_identity" "this" {}
|
||||
|
||||
data "aws_organizations_organization" "this" {}
|
||||
|
||||
module "prowler-scan-role" {
|
||||
source = "unicrons/organization-iam-role/aws"
|
||||
|
||||
stack_set_name = "prowler-scan-role"
|
||||
stack_set_description = "Deploy Prowler Scan IAM Role across all organization accounts"
|
||||
template_path = "${path.root}/prowler-scan-role.yaml"
|
||||
|
||||
template_parameters = {
|
||||
ExternalId = "<< external ID >>" # Replace with the External ID provided by Prowler Cloud
|
||||
}
|
||||
|
||||
# Specific OU IDs can be specified instead of root
|
||||
organizational_unit_ids = [data.aws_organizations_organization.this.roots[0].id]
|
||||
}
|
||||
```
|
||||
|
||||
#### `prowler-scan-role.yaml`
|
||||
|
||||
Download or reference the official CloudFormation template directly from GitHub:
|
||||
|
||||
- [prowler-scan-role.yml](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml)
|
||||
|
||||
---
|
||||
|
||||
### IAM Role: External ID Support
|
||||
|
||||
Include the `ExternalId` parameter in the StackSet if required by the organization's Prowler Cloud setup. This ensures secure cross-account access for scanning.
|
||||
|
||||
---
|
||||
|
||||
When encountering issues during deployment or needing to target specific OUs or environments (e.g., dev/staging/prod), reach out to the Prowler team via [Slack Community](https://prowler.com/slack) or [Support](mailto:support@prowler.com).
|
||||
|
||||
## Extra: Run Prowler across all accounts in AWS Organizations by assuming roles
|
||||
|
||||
### Running Prowler Across All AWS Organization Accounts
|
||||
|
||||