Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 989c270ad1 | |||
| d54e3b25db | |||
| 6a8e8750bb | |||
| ad3d4536fb | |||
| 46c24055ee | |||
| 4c6a1592ac | |||
| 89e657561c | |||
| 55099abc86 | |||
| 3c599a75cc | |||
| f77897f813 | |||
| 30518f2e0e | |||
| efdeb431ba | |||
| bb07cf9147 | |||
| 9214b5c26f | |||
| d57df3cc28 | |||
| 2f5fce41dc | |||
| 6918a75449 | |||
| 3aeaa3d992 | |||
| fd833eecf0 | |||
| 39e4d20b24 | |||
| dfdd45e4d0 | |||
| 81478dfed3 | |||
| 2854f8405c | |||
| 0e1578cfbc | |||
| f5b1532647 | |||
| d9f3a6b88e | |||
| b0c386fc60 | |||
| 72b06261df | |||
| 1562b77581 | |||
| 10e38ca407 | |||
| 5842f2df37 | |||
| 8b3b9ffd99 | |||
| d238050065 | |||
| 5572d476ad | |||
| 3c94d3a56f | |||
| 85af4ff77c | |||
| dcee114ef3 | |||
| 760723874c | |||
| c0a4898074 | |||
| 03c0533b58 | |||
| c8dcb0edb0 | |||
| 82171ee916 | |||
| df4bf18b97 | |||
| 94e60f7329 | |||
| f1ba5abbec | |||
| 6cc1a9a2cb | |||
| 31f98092bf | |||
| 85197036ca | |||
| be43025f00 | |||
| c6b34f0a85 | |||
| 675698a26a | |||
| 8d9bf2384f |
@@ -13,6 +13,7 @@ on:
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- ".github/workflows/api-pull-request.yml"
|
||||
- "api/**"
|
||||
|
||||
env:
|
||||
@@ -81,7 +82,9 @@ jobs:
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
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
|
||||
@@ -105,6 +108,23 @@ jobs:
|
||||
run: |
|
||||
poetry lock
|
||||
|
||||
- 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'
|
||||
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: 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
|
||||
|
||||
@@ -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@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.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:
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
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@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.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@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.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 }}
|
||||
|
||||
@@ -144,27 +144,34 @@ 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"
|
||||
|
||||
# Push the new branch first so it exists remotely
|
||||
git push origin "$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
|
||||
TEMP_BRANCH="update-api-dependency-$BRANCH_NAME_TRIMMED-$(date +%s)"
|
||||
echo "TEMP_BRANCH=$TEMP_BRANCH" >> $GITHUB_ENV
|
||||
|
||||
# Switch back to master and create temp branch
|
||||
git checkout master
|
||||
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
|
||||
@@ -180,12 +187,39 @@ jobs:
|
||||
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: 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
|
||||
|
||||
Updates the API prowler dependency for release ${{ env.PROWLER_VERSION }}.
|
||||
|
||||
**Changes:**
|
||||
- Updates `api/pyproject.toml` prowler dependency from `@master` to `@${{ env.BRANCH_NAME }}`
|
||||
- Updates `api/poetry.lock` file with resolved dependencies
|
||||
|
||||
This PR should be merged into the `${{ env.BRANCH_NAME }}` release branch.
|
||||
|
||||
### License
|
||||
|
||||
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: Extract changelog entries
|
||||
run: |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Check Changelog
|
||||
name: Prowler - Check Changelog
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -84,7 +84,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
|
||||
|
||||
@@ -86,12 +86,12 @@ prowler dashboard
|
||||
|
||||
| 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 |
|
||||
| AWS | 571 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 10 | 3 |
|
||||
| Azure | 142 | 18 | 11 | 3 |
|
||||
| Azure | 162 | 19 | 11 | 4 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 16 | 2 | 1 | 0 |
|
||||
| M365 | 69 | 7 | 3 | 2 |
|
||||
| GitHub | 17 | 2 | 1 | 0 |
|
||||
| M365 | 70 | 7 | 3 | 2 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
|
||||
> [!Note]
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.12.0] (Prowler 5.11.0 - UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
|
||||
|
||||
## [1.11.0] (Prowler 5.10.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -150,19 +150,19 @@ typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
|
||||
|
||||
[[package]]
|
||||
name = "alive-progress"
|
||||
version = "3.3.0"
|
||||
version = "3.2.0"
|
||||
description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c"},
|
||||
{file = "alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff"},
|
||||
{file = "alive-progress-3.2.0.tar.gz", hash = "sha256:ede29d046ff454fe56b941f686f89dd9389430c4a5b7658e445cb0b80e0e4deb"},
|
||||
{file = "alive_progress-3.2.0-py3-none-any.whl", hash = "sha256:0677929f8d3202572e9d142f08170b34dbbe256cc6d2afbf75ef187c7da964a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
about-time = "4.2.1"
|
||||
graphemeu = "0.7.2"
|
||||
grapheme = "0.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "amqp"
|
||||
@@ -179,18 +179,6 @@ files = [
|
||||
[package.dependencies]
|
||||
vine = ">=5.0.0,<6.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
@@ -507,23 +495,6 @@ azure-mgmt-core = ">=1.3.2"
|
||||
isodate = ">=0.6.1"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-databricks"
|
||||
version = "2.0.0"
|
||||
description = "Microsoft Azure Data Bricks Management Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9"},
|
||||
{file = "azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
azure-common = ">=1.1,<2.0"
|
||||
azure-mgmt-core = ">=1.3.2,<2.0.0"
|
||||
isodate = ">=0.6.1,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-keyvault"
|
||||
version = "10.3.1"
|
||||
@@ -594,42 +565,6 @@ azure-common = ">=1.1,<2.0"
|
||||
azure-mgmt-core = ">=1.3.0,<2.0.0"
|
||||
msrest = ">=0.6.21"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-recoveryservices"
|
||||
version = "3.1.0"
|
||||
description = "Microsoft Azure Recovery Services Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66"},
|
||||
{file = "azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
azure-common = ">=1.1"
|
||||
azure-mgmt-core = ">=1.5.0"
|
||||
isodate = ">=0.6.1"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-recoveryservicesbackup"
|
||||
version = "9.2.0"
|
||||
description = "Microsoft Azure Recovery Services Backup Management Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1"},
|
||||
{file = "azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
azure-common = ">=1.1"
|
||||
azure-mgmt-core = ">=1.3.2"
|
||||
isodate = ">=0.6.1"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-resource"
|
||||
version = "23.3.0"
|
||||
@@ -824,34 +759,34 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.39.15"
|
||||
version = "1.35.99"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "boto3-1.39.15-py3-none-any.whl", hash = "sha256:38fc54576b925af0075636752de9974e172c8a2cf7133400e3e09b150d20fb6a"},
|
||||
{file = "boto3-1.39.15.tar.gz", hash = "sha256:b4483625f0d8c35045254dee46cd3c851bbc0450814f20b9b25bee1b5c0d8409"},
|
||||
{file = "boto3-1.35.99-py3-none-any.whl", hash = "sha256:83e560faaec38a956dfb3d62e05e1703ee50432b45b788c09e25107c5058bd71"},
|
||||
{file = "boto3-1.35.99.tar.gz", hash = "sha256:e0abd794a7a591d90558e92e29a9f8837d25ece8e3c120e530526fe27eba5fca"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.39.15,<1.40.0"
|
||||
botocore = ">=1.35.99,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.13.0,<0.14.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.39.15"
|
||||
version = "1.35.99"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "botocore-1.39.15-py3-none-any.whl", hash = "sha256:eb9cfe918ebfbfb8654e1b153b29f0c129d586d2c0d7fb4032731d49baf04cff"},
|
||||
{file = "botocore-1.39.15.tar.gz", hash = "sha256:2aa29a717f14f8c7ca058c2e297aaed0aa10ecea24b91514eee802814d1b7600"},
|
||||
{file = "botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445"},
|
||||
{file = "botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -860,7 +795,7 @@ python-dateutil = ">=2.1,<3.0.0"
|
||||
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
crt = ["awscrt (==0.23.8)"]
|
||||
crt = ["awscrt (==0.22.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
@@ -1207,18 +1142,6 @@ files = [
|
||||
]
|
||||
markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "contextlib2"
|
||||
version = "21.6.0"
|
||||
description = "Backports and enhancements for the contextlib module"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"},
|
||||
{file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.5.4"
|
||||
@@ -1355,18 +1278,21 @@ test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "dash"
|
||||
version = "3.1.1"
|
||||
version = "2.18.2"
|
||||
description = "A Python framework for building reactive web-apps. Developed by Plotly."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23"},
|
||||
{file = "dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18"},
|
||||
{file = "dash-2.18.2-py3-none-any.whl", hash = "sha256:0ce0479d1bc958e934630e2de7023b8a4558f23ce1f9f5a4b34b65eb3903a869"},
|
||||
{file = "dash-2.18.2.tar.gz", hash = "sha256:20e8404f73d0fe88ce2eae33c25bbc513cbe52f30d23a401fa5f24dbb44296c8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=1.0.4,<3.2"
|
||||
dash-core-components = "2.0.0"
|
||||
dash-html-components = "2.0.0"
|
||||
dash-table = "5.0.0"
|
||||
Flask = ">=1.0.4,<3.1"
|
||||
importlib-metadata = "*"
|
||||
nest-asyncio = "*"
|
||||
plotly = ">=5.0.0"
|
||||
@@ -1374,12 +1300,11 @@ requests = "*"
|
||||
retrying = "*"
|
||||
setuptools = "*"
|
||||
typing-extensions = ">=4.1.1"
|
||||
Werkzeug = "<3.2"
|
||||
Werkzeug = "<3.1"
|
||||
|
||||
[package.extras]
|
||||
async = ["flask[async]"]
|
||||
celery = ["celery[redis] (>=5.1.2,<5.4.0)", "kombu (<5.4.0)", "redis (>=3.5.3,<=5.0.4)"]
|
||||
ci = ["black (==22.3.0)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "ipython (<9.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "mypy (==1.15.0) ; python_version >= \"3.12\"", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pyright (==1.1.398) ; python_version >= \"3.7\"", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"]
|
||||
celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"]
|
||||
ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"]
|
||||
compress = ["flask-compress"]
|
||||
dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"]
|
||||
diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"]
|
||||
@@ -1387,21 +1312,57 @@ testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0
|
||||
|
||||
[[package]]
|
||||
name = "dash-bootstrap-components"
|
||||
version = "2.0.3"
|
||||
version = "1.6.0"
|
||||
description = "Bootstrap themed components for use in Plotly Dash"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = "<4,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5"},
|
||||
{file = "dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de"},
|
||||
{file = "dash_bootstrap_components-1.6.0-py3-none-any.whl", hash = "sha256:97f0f47b38363f18863e1b247462229266ce12e1e171cfb34d3c9898e6e5cd1e"},
|
||||
{file = "dash_bootstrap_components-1.6.0.tar.gz", hash = "sha256:960a1ec9397574792f49a8241024fa3cecde0f5930c971a3fc81f016cbeb1095"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
dash = ">=3.0.4"
|
||||
dash = ">=2.0.0"
|
||||
|
||||
[package.extras]
|
||||
pandas = ["numpy (>=2.0.2)", "pandas (>=2.2.3)"]
|
||||
pandas = ["numpy", "pandas"]
|
||||
|
||||
[[package]]
|
||||
name = "dash-core-components"
|
||||
version = "2.0.0"
|
||||
description = "Core component suite for Dash"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"},
|
||||
{file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dash-html-components"
|
||||
version = "2.0.0"
|
||||
description = "Vanilla HTML components for Dash"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"},
|
||||
{file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dash-table"
|
||||
version = "5.0.0"
|
||||
description = "Dash table"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"},
|
||||
{file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
@@ -1928,54 +1889,6 @@ djangorestframework-jsonapi = ">=6.0.0"
|
||||
drf-extensions = ">=0.7.1"
|
||||
drf-spectacular = ">=0.25.0"
|
||||
|
||||
[[package]]
|
||||
name = "dulwich"
|
||||
version = "0.23.0"
|
||||
description = "Python Git Library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"},
|
||||
{file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"},
|
||||
{file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d971566826f16ec67c70641c1fbdb337323aa5b533799bc5a4641f4750e73b36"},
|
||||
{file = "dulwich-0.23.0-cp310-cp310-win32.whl", hash = "sha256:27d970adf539806dfc4fe3e4c9e8dc6ebf0318977a56e24d22f13413535a51ba"},
|
||||
{file = "dulwich-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:025178533e884ffdb0d9d8db4b8870745d438cbfecb782fd1b56c3b6438e86cf"},
|
||||
{file = "dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc"},
|
||||
{file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527"},
|
||||
{file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261"},
|
||||
{file = "dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a"},
|
||||
{file = "dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd"},
|
||||
{file = "dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e"},
|
||||
{file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e"},
|
||||
{file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9"},
|
||||
{file = "dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf"},
|
||||
{file = "dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59"},
|
||||
{file = "dulwich-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:624e2223c8b705b3a217f9c8d3bfed3a573093be0b0ba033c46cba8411fb9630"},
|
||||
{file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b4eaf326d15bb3fc5316c777b0312f0fe02f6f82a4368cd971d0ce2167b7ec34"},
|
||||
{file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d754afaf7c133a015c75cc2be11703138b4be932e0eeeb2c70add56083f31109"},
|
||||
{file = "dulwich-0.23.0-cp313-cp313-win32.whl", hash = "sha256:ac53ec438bde3c1f479782c34240479b36cd47230d091979137b7ecc12c0242e"},
|
||||
{file = "dulwich-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:50d3b4ba45671fb8b7d2afbd02c10b4edbc3290a1f92260e64098b409e9ca35c"},
|
||||
{file = "dulwich-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8e18ea3fa49f10932077f39c0b960b5045870c550c3d7c74f3cfaac09457cd6"},
|
||||
{file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3e6df0eb8cca21f210e3ddce2ccb64482646893dbec2fee9f3411d037595bf7b"},
|
||||
{file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:90c0064d7df8e7fe83d3a03c7d60b9e07a92698b18442f926199b2c3f0bf34d4"},
|
||||
{file = "dulwich-0.23.0-cp39-cp39-win32.whl", hash = "sha256:84eef513aba501cbc1f223863f3b4b351fe732d3fb590cab9bdf5d33eb1a1248"},
|
||||
{file = "dulwich-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dce943da48217c26e15790fd6df62d27a7f1d067102780351ebf2635fc0ba482"},
|
||||
{file = "dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135"},
|
||||
{file = "dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
urllib3 = ">=1.25"
|
||||
|
||||
[package.extras]
|
||||
dev = ["dissolve (>=0.1.1)", "mypy (==1.16.0)", "ruff (==0.11.13)"]
|
||||
fastimport = ["fastimport"]
|
||||
https = ["urllib3 (>=1.24.1)"]
|
||||
merge = ["merge3"]
|
||||
paramiko = ["paramiko"]
|
||||
pgp = ["gpg"]
|
||||
|
||||
[[package]]
|
||||
name = "durationpy"
|
||||
version = "0.9"
|
||||
@@ -2306,20 +2219,18 @@ files = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphemeu"
|
||||
version = "0.7.2"
|
||||
name = "grapheme"
|
||||
version = "0.6.0"
|
||||
description = "Unicode grapheme helpers"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542"},
|
||||
{file = "graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8"},
|
||||
{file = "grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest"]
|
||||
docs = ["sphinx", "sphinx-autobuild"]
|
||||
test = ["pytest", "sphinx", "sphinx-autobuild", "twine", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
@@ -2458,18 +2369,6 @@ files = [
|
||||
{file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iamdata"
|
||||
version = "0.1.202507291"
|
||||
description = "IAM data for AWS actions, resources, and conditions based on IAM policy documents. Checked for updates daily."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "iamdata-0.1.202507291-py3-none-any.whl", hash = "sha256:11dfdacc3ce0312468aa5ccafee461cd39b1deb7be112042deea91cbcd4b292b"},
|
||||
{file = "iamdata-0.1.202507291.tar.gz", hash = "sha256:b386ce94819464554dc1258238ee1b232d86f0467edc13fffbf4de7332b3c7ad"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -3986,7 +3885,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.10.0"
|
||||
version = "5.8.0"
|
||||
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
|
||||
optional = false
|
||||
python-versions = ">3.9.1,<3.13"
|
||||
@@ -3995,7 +3894,7 @@ files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
alive-progress = "3.3.0"
|
||||
alive-progress = "3.2.0"
|
||||
awsipranges = "0.3.3"
|
||||
azure-identity = "1.21.0"
|
||||
azure-keyvault-keys = "4.10.0"
|
||||
@@ -4005,13 +3904,10 @@ azure-mgmt-compute = "34.0.0"
|
||||
azure-mgmt-containerregistry = "12.0.0"
|
||||
azure-mgmt-containerservice = "34.1.0"
|
||||
azure-mgmt-cosmosdb = "9.7.0"
|
||||
azure-mgmt-databricks = "2.0.0"
|
||||
azure-mgmt-keyvault = "10.3.1"
|
||||
azure-mgmt-monitor = "6.0.2"
|
||||
azure-mgmt-network = "28.1.0"
|
||||
azure-mgmt-rdbms = "10.1.0"
|
||||
azure-mgmt-recoveryservices = "3.1.0"
|
||||
azure-mgmt-recoveryservicesbackup = "9.2.0"
|
||||
azure-mgmt-resource = "23.3.0"
|
||||
azure-mgmt-search = "9.1.0"
|
||||
azure-mgmt-security = "7.0.0"
|
||||
@@ -4020,14 +3916,13 @@ azure-mgmt-storage = "22.1.1"
|
||||
azure-mgmt-subscription = "3.1.1"
|
||||
azure-mgmt-web = "8.0.0"
|
||||
azure-storage-blob = "12.24.1"
|
||||
boto3 = "1.39.15"
|
||||
botocore = "1.39.15"
|
||||
boto3 = "1.35.99"
|
||||
botocore = "1.35.99"
|
||||
colorama = "0.4.6"
|
||||
cryptography = "44.0.1"
|
||||
dash = "3.1.1"
|
||||
dash-bootstrap-components = "2.0.3"
|
||||
dash = "2.18.2"
|
||||
dash-bootstrap-components = "1.6.0"
|
||||
detect-secrets = "1.5.0"
|
||||
dulwich = "0.23.0"
|
||||
google-api-python-client = "2.163.0"
|
||||
google-auth-httplib2 = ">=0.1,<0.3"
|
||||
jsonschema = "4.23.0"
|
||||
@@ -4036,13 +3931,12 @@ microsoft-kiota-abstractions = "1.9.2"
|
||||
msgraph-sdk = "1.23.0"
|
||||
numpy = "2.0.2"
|
||||
pandas = "2.2.3"
|
||||
py-iam-expand = "0.1.0"
|
||||
py-ocsf-models = "0.5.0"
|
||||
pydantic = ">=2.0,<3.0"
|
||||
py-ocsf-models = "0.3.1"
|
||||
pydantic = "1.10.21"
|
||||
pygithub = "2.5.0"
|
||||
python-dateutil = ">=2.9.0.post0,<3.0.0"
|
||||
pytz = "2025.1"
|
||||
schema = "0.7.5"
|
||||
schema = "0.7.7"
|
||||
shodan = "1.31.0"
|
||||
slack-sdk = "3.34.0"
|
||||
tabulate = "0.9.0"
|
||||
@@ -4051,8 +3945,8 @@ tzlocal = "5.3.1"
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "v5.10"
|
||||
resolved_reference = "ff900a2a455def25eb7f5a7d25248e58eae24a34"
|
||||
reference = "master"
|
||||
resolved_reference = "ea97de7f43a2063476b49f7697bb6c7b51137c11"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
@@ -4166,37 +4060,22 @@ files = [
|
||||
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-iam-expand"
|
||||
version = "0.1.0"
|
||||
description = "This is a Python package to expand and deobfuscate IAM policies."
|
||||
optional = false
|
||||
python-versions = "<3.14,>3.9.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510"},
|
||||
{file = "py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
iamdata = ">=0.1.202504091"
|
||||
|
||||
[[package]]
|
||||
name = "py-ocsf-models"
|
||||
version = "0.5.0"
|
||||
version = "0.3.1"
|
||||
description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/."
|
||||
optional = false
|
||||
python-versions = "<3.14,>3.9.1"
|
||||
python-versions = "<3.13,>3.9.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "py_ocsf_models-0.5.0-py3-none-any.whl", hash = "sha256:7933253f56782c04c412d976796db429577810b951fe4195351794500b5962d8"},
|
||||
{file = "py_ocsf_models-0.5.0.tar.gz", hash = "sha256:bf05e955809d1ec3ab1007e4a4b2a8a0afa74b6e744ea8ffbf386e46b3af0a76"},
|
||||
{file = "py_ocsf_models-0.3.1-py3-none-any.whl", hash = "sha256:e722d567a7f3e5190fdd053c2e75a69cf33fab6f5c0a4b7de678768ba340ae3a"},
|
||||
{file = "py_ocsf_models-0.3.1.tar.gz", hash = "sha256:60defd2cc86e8882f42dc9c6dacca6dc16d6bc05f9477c2a3486a0d4b5882b94"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = "44.0.1"
|
||||
email-validator = "2.2.0"
|
||||
pydantic = ">=2.9.2,<3.0.0"
|
||||
pydantic = "1.10.21"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
@@ -4294,137 +4173,70 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
description = "Data validation using Python type hints"
|
||||
version = "1.10.21"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"},
|
||||
{file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"},
|
||||
{file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"},
|
||||
{file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"},
|
||||
{file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"},
|
||||
{file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"},
|
||||
{file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"},
|
||||
{file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"},
|
||||
{file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"},
|
||||
{file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"},
|
||||
{file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.6.0"
|
||||
pydantic-core = "2.33.2"
|
||||
typing-extensions = ">=4.12.2"
|
||||
typing-inspection = ">=0.4.0"
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
|
||||
{file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
|
||||
{file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
|
||||
{file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
|
||||
{file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
|
||||
{file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
|
||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
|
||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
|
||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
|
||||
{file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pygithub"
|
||||
@@ -5254,21 +5066,21 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.13.1"
|
||||
version = "0.10.4"
|
||||
description = "An Amazon S3 Transfer Manager"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724"},
|
||||
{file = "s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf"},
|
||||
{file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"},
|
||||
{file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.37.4,<2.0a.0"
|
||||
botocore = ">=1.33.2,<2.0a.0"
|
||||
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
|
||||
crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "safety"
|
||||
@@ -5327,19 +5139,16 @@ typing-extensions = ">=4.7.1"
|
||||
|
||||
[[package]]
|
||||
name = "schema"
|
||||
version = "0.7.5"
|
||||
version = "0.7.7"
|
||||
description = "Simple data validation library"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"},
|
||||
{file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"},
|
||||
{file = "schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde"},
|
||||
{file = "schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
contextlib2 = ">=0.5.5"
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.26.1"
|
||||
@@ -5649,21 +5458,6 @@ files = [
|
||||
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
description = "Runtime typing introspection tools"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
|
||||
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
@@ -6122,4 +5916,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "7aa50d0e8afd3dfa080541d0bfd7ea960720a9848d1e6f801bc082528f43c56b"
|
||||
content-hash = "6802b33984c2f8438c9dc02dac0a0c14d5a78af60251bd0c80ca59bc2182c48e"
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"drf-spectacular-jsonapi==0.5.1",
|
||||
"gunicorn==23.0.0",
|
||||
"lxml==5.3.2",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.10",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||
@@ -38,7 +38,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.11.2"
|
||||
version = "1.11.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1752,6 +1752,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)
|
||||
|
||||
@@ -293,7 +293,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.11.2"
|
||||
spectacular_settings.VERSION = "1.11.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
|
||||
@@ -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="../../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,257 @@
|
||||
## Running Prowler
|
||||
|
||||
Running Prowler requires specifying 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
|
||||
|
||||
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 [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 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 [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 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 [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.
|
||||
|
Before Width: | Height: | Size: 518 KiB |
@@ -1,3 +1,5 @@
|
||||
# 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:
|
||||
@@ -8,790 +10,15 @@ The official supported providers right now are:
|
||||
- **Kubernetes**
|
||||
- **M365**
|
||||
- **Github**
|
||||
- **IaC**
|
||||
|
||||
Unofficially, Prowler supports: NHN.
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
- **GCP Retry Configuration**
|
||||
|
||||
To configure the maximum number of retry attempts for Google Cloud SDK API calls, use 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 [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 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 [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,173 @@
|
||||
### 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).
|
||||
|
||||
|
||||
### 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
|
||||
@@ -80,6 +80,7 @@ The following list includes all the Azure checks with configurable variables tha
|
||||
| `app_ensure_python_version_is_latest` | `python_latest_version` | String |
|
||||
| `app_ensure_java_version_is_latest` | `java_latest_version` | String |
|
||||
| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings |
|
||||
| `vm_sufficient_daily_backup_retention_period` | `vm_backup_min_daily_retention_days` | Integer |
|
||||
| `vm_desired_sku_size` | `desired_vm_sku_sizes` | List of Strings |
|
||||
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String |
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ This guide explains how to set up authentication with GitHub for Prowler. The do
|
||||
|
||||
Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios.
|
||||
|
||||
#### How to Create a Personal Access Token
|
||||
???+ warning "Classic Tokens Deprecated"
|
||||
GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained Personal Access Tokens. We recommend using fine-grained tokens as they provide better security through more granular permissions and resource-specific access control.
|
||||
|
||||
#### **Option 1: Create a Fine-Grained Personal Access Token (Recommended)**
|
||||
|
||||
1. **Navigate to GitHub Settings**
|
||||
- Open [GitHub](https://github.com) and sign in
|
||||
@@ -80,11 +83,11 @@ Personal Access Tokens provide the simplest GitHub authentication method and sup
|
||||
|
||||
4. **Configure Token Permissions**
|
||||
To enable Prowler functionality, configure the following scopes:
|
||||
- `repo`: Full control of private repositories
|
||||
- `repo`: Full control of private repositories (includes `repo:status` and `repo:contents`)
|
||||
- `read:org`: Read organization and team membership
|
||||
- `read:user`: Read user profile data
|
||||
- `read:discussion`: Read discussions
|
||||
- `read:enterprise`: Read enterprise data (if applicable)
|
||||
- `security_events`: Access security events (secret scanning and Dependabot alerts)
|
||||
- `read:enterprise`: Read enterprise data (if using GitHub Enterprise)
|
||||
|
||||
5. **Copy and Store the Token**
|
||||
- Copy the generated token immediately (GitHub displays tokens only once)
|
||||
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,412 @@
|
||||
# Amazon S3 Integration
|
||||
|
||||
**Prowler App** allows automatic export of scan results to Amazon S3 buckets, providing seamless integration with existing data workflows and storage infrastructure. This comprehensive guide demonstrates configuration and management of Amazon S3 integrations to streamline security finding management and reporting.
|
||||
|
||||
When enabled and configured, scan results are automatically stored in the configured bucket. Results are provided in `csv`, `html` and `json-ocsf` formats, offering flexibility for custom integrations:
|
||||
|
||||
<!-- TODO: remove the comment once the AWS Security Hub integration is completed -->
|
||||
<!-- - json-asff -->
|
||||
<!--
|
||||
???+ note
|
||||
The `json-asff` file will be only present in your configured Amazon S3 Bucket if you have the AWS Security Hub integration enabled. You can get more information about that integration here. -->
|
||||
|
||||
???+ note
|
||||
Enabling this integration incurs costs in Amazon S3. Refer to [Amazon S3 pricing](https://aws.amazon.com/s3/pricing/) for more information.
|
||||
|
||||
|
||||
The Amazon S3 Integration provides the following capabilities:
|
||||
|
||||
- **Automate scan result exports** to designated S3 buckets after each scan
|
||||
|
||||
- **Configure separate bucket destinations** for different cloud providers or use cases
|
||||
|
||||
- **Customize export paths** within buckets for organized storage
|
||||
|
||||
- **Support multiple authentication methods** including IAM roles and static credentials
|
||||
|
||||
- **Verify connection reliability** through built-in connection testing
|
||||
|
||||
- **Manage integrations independently** with separate configuration and credential controls
|
||||
|
||||
|
||||
## Required Permissions
|
||||
|
||||
Before configuring the Amazon S3 Integration, ensure that AWS credentials and optionally the IAM Role used for S3 access have the necessary permissions to write scan results to the designated S3 bucket. This requirement applies when using static credentials, session credentials, or an IAM role (either self-created or generated using [Prowler's permissions templates](#available-templates)).
|
||||
|
||||
### IAM Policy
|
||||
|
||||
The S3 integration requires the following permissions. Add these to the IAM role policy, or ensure AWS credentials have these permissions:
|
||||
|
||||
```json title="s3:DeleteObject"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "<BUCKET AWS ACCOUNT NUMBER>"
|
||||
}
|
||||
},
|
||||
"Action": [
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<BUCKET NAME>/*test-prowler-connection.txt"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`s3:DeleteObject` permission is required for connection testing. When testing the S3 integration, Prowler creates a temporary beacon file, `test-prowler-connection.txt`, to verify write permissions, then deletes it to confirm the connection is working properly.
|
||||
|
||||
```json title="s3:PutObject"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "<BUCKET AWS ACCOUNT NUMBER>"
|
||||
}
|
||||
},
|
||||
"Action": [
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<BUCKET NAME>/*"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json title="s3:ListBucket"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ResourceAccount": "<BUCKET AWS ACCOUNT NUMBER>"
|
||||
}
|
||||
},
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<BUCKET NAME>"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
???+ note
|
||||
Replace `<BUCKET AWS ACCOUNT NUMBER>` with the AWS account ID that owns the destination S3 bucket, and `<BUCKET NAME>` with the actual bucket name.
|
||||
|
||||
### Cross-Account S3 Bucket
|
||||
|
||||
If the S3 destination bucket is in a different AWS account than the one providing the credentials for S3 access, configure a bucket policy on the destination bucket to allow cross-account access.
|
||||
|
||||
The following diagrams illustrate the three common S3 integration scenarios:
|
||||
|
||||
##### Same Account Setup (No Bucket Policy Required)
|
||||
|
||||
When both the IAM credentials and destination S3 bucket are in the same AWS account, no additional bucket policy is required.
|
||||
|
||||

|
||||
|
||||
##### Cross-Account Setup (Bucket Policy Required)
|
||||
|
||||
When the S3 bucket is in a different AWS account, you must configure a bucket policy to allow cross-account access.
|
||||
|
||||

|
||||
|
||||
##### Multi-Account Setup (Multiple Principals in Bucket Policy)
|
||||
|
||||
When multiple AWS accounts need to write to the same destination bucket, configure the bucket policy with multiple principals.
|
||||
|
||||

|
||||
|
||||
#### S3 Bucket Policy
|
||||
|
||||
Apply the following bucket policy to the destination S3 bucket:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::<SOURCE ACCOUNT ID>:role/ProwlerScan"
|
||||
},
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::<BUCKET NAME>/*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::<SOURCE ACCOUNT ID>:role/ProwlerScan"
|
||||
},
|
||||
"Action": "s3:DeleteObject",
|
||||
"Resource": "arn:aws:s3:::<BUCKET NAME>/*test-prowler-connection.txt"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::<SOURCE ACCOUNT ID>:role/ProwlerScan"
|
||||
},
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::<BUCKET NAME>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
???+ note
|
||||
Replace `<SOURCE ACCOUNT ID>` with the AWS account ID that contains the IAM role and `<BUCKET NAME>` with the destination bucket name. The role name `ProwlerScan` is the default name when using Prowler's permissions templates. If using a custom IAM role or different authentication method, replace `ProwlerScan` with the actual role name.
|
||||
|
||||
##### Multi-Account Configuration
|
||||
|
||||
For multiple AWS accounts, modify the `Principal` field to an array:
|
||||
|
||||
```json
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"arn:aws:iam::<SOURCE ACCOUNT ID 1>:role/ProwlerScan",
|
||||
"arn:aws:iam::<SOURCE ACCOUNT ID 2>:role/ProwlerScan"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
???+ note
|
||||
Replace `<SOURCE ACCOUNT ID>` with the AWS account ID that contains the IAM role and `<BUCKET NAME>` with the destination bucket name. The role name `ProwlerScan` is the default name when using Prowler's permissions templates. If using a custom IAM role or different authentication method, replace `ProwlerScan` with the actual role name.
|
||||
|
||||
### Available Templates
|
||||
|
||||
**Prowler App** provides Infrastructure as Code (IaC) templates to automate IAM role setup with S3 integration permissions.
|
||||
|
||||
???+ note
|
||||
Templates are optional. Custom IAM roles or static credentials can be used instead.
|
||||
|
||||
Choose from the following deployment options:
|
||||
|
||||
- [CloudFormation](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml)
|
||||
- [Terraform](https://github.com/prowler-cloud/prowler/tree/master/permissions/templates/terraform)
|
||||
|
||||
#### CloudFormation
|
||||
|
||||
##### AWS CLI
|
||||
|
||||
When using Prowler's CloudFormation template, execute the following command to update the existing Prowler stack:
|
||||
|
||||
```bash
|
||||
aws cloudformation update-stack \
|
||||
--capabilities CAPABILITY_IAM --capabilities CAPABILITY_NAMED_IAM \
|
||||
--stack-name "Prowler" \
|
||||
--template-url "https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml" \
|
||||
--parameters \
|
||||
ParameterKey=EnableS3Integration,ParameterValue="true" \
|
||||
ParameterKey=ExternalId,ParameterValue="your-external-id" \
|
||||
ParameterKey=S3IntegrationBucketName,ParameterValue="your-bucket-name" \
|
||||
ParameterKey=S3IntegrationBucketAccountId,ParameterValue="your-bucket-aws-account-id-owner"
|
||||
```
|
||||
|
||||
Alternatively, if you don't have the `ProwlerScan` IAM Role, execute the following command to create the CloudFormation stack:
|
||||
|
||||
```bash
|
||||
aws cloudformation create-stack \
|
||||
--capabilities CAPABILITY_IAM --capabilities CAPABILITY_NAMED_IAM \
|
||||
--stack-name "Prowler" \
|
||||
--template-url "https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml" \
|
||||
--parameters \
|
||||
ParameterKey=EnableS3Integration,ParameterValue="true" \
|
||||
ParameterKey=ExternalId,ParameterValue="your-external-id" \
|
||||
ParameterKey=S3IntegrationBucketName,ParameterValue="your-bucket-name" \
|
||||
ParameterKey=S3IntegrationBucketAccountId,ParameterValue="your-bucket-aws-account-id-owner"
|
||||
```
|
||||
|
||||
A CloudFormation Quick Link is also available [here](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler¶m_EnableS3Integration=true)
|
||||
|
||||
##### AWS Console
|
||||
|
||||
If using Prowler's CloudFormation template, execute the following command to update the existing Prowler stack:
|
||||
|
||||
|
||||
1. Navigate to CloudFormation service in the AWS region you are using
|
||||
2. Select "ProwlerScan", click "Update" and then "Make a direct update"
|
||||
3. Replace template, uploading the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml)
|
||||
4. Configure parameters:
|
||||
- `ExternalId`: Keep existing value
|
||||
- `EnableS3Integration`: Select "true"
|
||||
- `S3IntegrationBucketName`: Your bucket name
|
||||
- `S3IntegrationBucketAccountId`: Bucket owner's AWS account ID
|
||||
5. In the "Configure stack options" screen, again, leave everything as it is and click on "Next"
|
||||
6. Finally, under "Review Prowler", at the bottom click on "Submit"
|
||||
|
||||
#### Terraform
|
||||
|
||||
1. Download the Terraform code:
|
||||
```bash
|
||||
# Clone or download from GitHub
|
||||
git clone https://github.com/prowler-cloud/prowler.git
|
||||
cd prowler/permissions/templates/terraform
|
||||
```
|
||||
|
||||
2. Configure your variables:
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
```
|
||||
|
||||
3. Edit `terraform.tfvars` with your specific values:
|
||||
```hcl
|
||||
# Required: External ID from Prowler App
|
||||
external_id = "your-unique-external-id-here"
|
||||
|
||||
# S3 Integration Configuration
|
||||
enable_s3_integration = true
|
||||
s3_integration_bucket_name = "your-s3-bucket-name"
|
||||
s3_integration_bucket_account_id = "123456789012" # Bucket owner's AWS Account ID
|
||||
```
|
||||
|
||||
4. Deploy the infrastructure:
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan # Review the planned changes
|
||||
terraform apply # Type 'yes' when prompted
|
||||
```
|
||||
|
||||
5. After successful deployment, Terraform will display important values:
|
||||
```
|
||||
Outputs:
|
||||
prowler_role_arn = "arn:aws:iam::123456789012:role/ProwlerScan"
|
||||
prowler_role_name = "ProwlerScan"
|
||||
s3_integration_enabled = "true"
|
||||
```
|
||||
|
||||
6. Copy the `prowler_role_arn`, as it's required to complete the S3 integration credentials configuration.
|
||||
|
||||
For detailed information, refer to the [Terraform README](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/terraform/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Once the required permissions are set up, proceed to configure the S3 integration in **Prowler App**.
|
||||
|
||||
1. Navigate to "Integrations"
|
||||

|
||||
2. Locate the Amazon S3 Integration card and click on the "Configure" button
|
||||

|
||||
3. Click the "Add Integration" button
|
||||

|
||||
4. Complete the configuration form with the following details:
|
||||
|
||||
- **Cloud Providers:** Select the providers whose scan results should be exported to this S3 bucket
|
||||
- **Bucket Name:** Enter the name of the target S3 bucket (e.g., `my-security-findings-bucket`)
|
||||
- **Output Directory:** Specify the directory path within the bucket (e.g., `/prowler-findings/`, defaults to `output`)
|
||||
|
||||

|
||||
|
||||
6. Click "Next" to configure credentials
|
||||
7. Configure AWS authentication using one of the supported methods:
|
||||
|
||||
- **AWS SDK Default:** Use default AWS credentials from the environment. For Prowler Cloud users, this is the recommended option as the service has AWS credentials to assume IAM roles with ARNs matching `arn:aws:iam::*:role/Prowler*` or `arn:aws:iam::*:role/prowler*`
|
||||
- **Access Keys:** Provide AWS access key ID and secret access key
|
||||
- **IAM Role (optional):** Specify the IAM Role ARN, external ID, and optional session parameters
|
||||
|
||||

|
||||
|
||||
8. Optional - For IAM role authentication, complete the required fields:
|
||||
|
||||
- **Role ARN:** The Amazon Resource Name of the IAM role
|
||||
- **External ID:** Unique identifier for additional security (defaults to Tenant/Organization ID) - mandatory and automatically filled
|
||||
- **Role Session Name:** Optional - name for the assumed role session
|
||||
- **Session Duration:** Optional - duration in seconds for the session
|
||||
|
||||
9. Click "Create Integration" to verify the connection and complete the setup
|
||||
|
||||
???+ success
|
||||
Once credentials are configured and the connection test passes, the S3 integration will be active. Scan results will automatically be exported to the specified bucket after each scan completes. Run a new scan and check the S3 bucket to verify the integration is working.
|
||||
|
||||
???+ note
|
||||
Scan outputs are processed after scan completion. Depending on scan size and network conditions, exports may take a few minutes to appear in the S3 bucket.
|
||||
---
|
||||
|
||||
|
||||
### Integration Status
|
||||
|
||||
Once the integration is active, monitor its status and make adjustments as needed through the integrations management interface.
|
||||
|
||||
1. Review configured integrations in the management interface
|
||||
2. Each integration displays:
|
||||
|
||||
- **Connection Status:** Connected or Disconnected indicator
|
||||
- **Bucket Information:** Bucket name and output directory
|
||||
- **Last Checked:** Timestamp of the most recent connection test
|
||||
|
||||

|
||||
|
||||
#### Actions
|
||||
|
||||

|
||||
|
||||
Each S3 integration provides several management actions accessible through dedicated buttons:
|
||||
|
||||
| Button | Purpose | Available Actions | Notes |
|
||||
|--------|---------|------------------|-------|
|
||||
| **Test** | Verify integration connectivity | • Test AWS credential validity<br/>• Check S3 bucket accessibility<br/>• Verify write permissions<br/>• Validate connection setup | Results displayed in notification message |
|
||||
| **Config** | Modify integration settings | • Update selected cloud providers<br/>• Change bucket name<br/>• Modify output directory path | Click "Update Configuration" to save changes |
|
||||
| **Credentials** | Update authentication settings | • Modify AWS access keys<br/>• Update IAM role configuration<br/>• Change authentication method | Click "Update Credentials" to save changes |
|
||||
| **Enable/Disable** | Toggle integration status | • Enable integration to start exporting results<br/>• Disable integration to pause exports | Status change takes effect immediately |
|
||||
| **Delete** | Remove integration permanently | • Permanently delete integration<br/>• Remove all configuration data | ⚠️ **Cannot be undone** - confirm before deleting |
|
||||
|
||||
???+ tip "Management Best Practices"
|
||||
- Test the integration after any configuration changes
|
||||
- Use the Enable/Disable toggle for temporary changes instead of deleting
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Understanding S3 Export Structure
|
||||
|
||||
When the S3 integration is enabled and a scan completes, Prowler creates a folder inside the specified bucket path (using `output` as the default folder name) with subfolders for each output format:
|
||||
|
||||
- Regular: `prowler-output-{provider-uid}-{timestamp}.{extension}`
|
||||
- Compliance: `prowler-output-{provider-uid}-{timestamp}_{compliance_framework}.{extension}`
|
||||
|
||||
```
|
||||
output/
|
||||
├── compliance/
|
||||
│ └── prowler-output-111122223333-20250805120000_cis_5.0_aws.csv
|
||||
├── csv/
|
||||
│ └── prowler-output-111122223333-20250805120000.csv
|
||||
├── html/
|
||||
│ └── prowler-output-111122223333-20250805120000.html
|
||||
└── json-ocsf/
|
||||
└── prowler-output-111122223333-20250805120000.ocsf.json
|
||||
```
|
||||
|
||||

|
||||
|
||||
For detailed information about Prowler's reporting formats, refer to the [Prowler reporting documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/reporting/).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection test fails:**
|
||||
|
||||
- Check AWS credentials are valid
|
||||
- If using IAM Role, check its permissions
|
||||
- Verify bucket permissions and region
|
||||
- Confirm network access to S3
|
||||
|
||||
**No scan results in bucket:**
|
||||
|
||||
- Ensure integration shows "Connected"
|
||||
- Check that provider is associated with integration
|
||||
- Verify bucket policies allow writes
|
||||
@@ -201,6 +201,51 @@ For full setup instructions and requirements, check the [Microsoft 365 provider
|
||||
|
||||
<img src="../../img/m365-credentials.png" alt="Prowler Cloud M365 Credentials" width="700"/>
|
||||
|
||||
### **Step 4.6: GitHub Credentials**
|
||||
For GitHub, you must enter your Provider ID (username or organization name) and choose the authentication method you want to use:
|
||||
|
||||
- **Personal Access Token** (Recommended for individual users)
|
||||
- **OAuth App Token** (For applications requiring user consent)
|
||||
- **GitHub App** (Recommended for organizations and production use)
|
||||
|
||||
???+ note
|
||||
For full setup instructions and requirements, check the [GitHub provider requirements](./github/getting-started-github.md).
|
||||
|
||||
<img src="../img/github-auth-methods.png" alt="GitHub Authentication Methods" width="700"/>
|
||||
|
||||
#### **Step 4.6.1: Personal Access Token**
|
||||
|
||||
Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios.
|
||||
|
||||
- Select `Personal Access Token` and enter your `Personal Access Token`:
|
||||
|
||||
<img src="../img/github-pat-credentials.png" alt="GitHub Personal Access Token Credentials" width="700"/>
|
||||
|
||||
???+ note
|
||||
For detailed instructions on creating a Personal Access Token and the exact permissions required, check the [GitHub Personal Access Token tutorial](./github/getting-started-github.md#1-personal-access-token-pat).
|
||||
|
||||
#### **Step 4.6.2: OAuth App Token**
|
||||
|
||||
OAuth Apps enable applications to act on behalf of users with explicit consent.
|
||||
|
||||
- Select `OAuth App Token` and enter your `OAuth App Token`:
|
||||
|
||||
<img src="../img/github-oauth-credentials.png" alt="GitHub OAuth App Credentials" width="700"/>
|
||||
|
||||
???+ note
|
||||
To create an OAuth App, go to GitHub Settings → Developer settings → OAuth Apps → New OAuth App. You'll need to exchange an authorization code for an access token using the OAuth flow.
|
||||
|
||||
#### **Step 4.6.3: GitHub App**
|
||||
|
||||
GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations.
|
||||
|
||||
- Select `GitHub App` and enter your `GitHub App ID` and `GitHub App Private Key`:
|
||||
|
||||
<img src="../img/github-app-credentials.png" alt="GitHub App Credentials" width="700"/>
|
||||
|
||||
???+ note
|
||||
To create a GitHub App, go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App. Configure the necessary permissions and generate a private key. Install the app to your account or organization and provide the App ID and private key content.
|
||||
|
||||
## **Step 5: Test Connection**
|
||||
|
||||
After adding your credentials of your cloud account, click the `Launch` button to verify that Prowler App can successfully connect to your provider:
|
||||
|
||||
@@ -46,8 +46,20 @@ repo_name: prowler-cloud/prowler
|
||||
|
||||
nav:
|
||||
- Getting Started:
|
||||
- Overview: index.md
|
||||
- Requirements: getting-started/requirements.md
|
||||
- Overview:
|
||||
- What is Prowler?: index.md
|
||||
- Products:
|
||||
- Prowler App: products/prowler-app.md
|
||||
- Prowler CLI: products/prowler-cli.md
|
||||
- Prowler Cloud: https://cloud.prowler.com
|
||||
- Prowler Hub: https://hub.prowler.com
|
||||
- Installation:
|
||||
- Prowler App: installation/prowler-app.md
|
||||
- Prowler CLI: installation/prowler-cli.md
|
||||
- Basic Usage:
|
||||
- Prowler App: basic-usage/prowler-app.md
|
||||
- Prowler CLI: basic-usage/prowler-cli.md
|
||||
- Requirements: getting-started/requirements.md
|
||||
- Tutorials:
|
||||
- Prowler App:
|
||||
- Getting Started: tutorials/prowler-app.md
|
||||
@@ -55,6 +67,7 @@ nav:
|
||||
- Social Login: tutorials/prowler-app-social-login.md
|
||||
- SSO with SAML: tutorials/prowler-app-sso.md
|
||||
- Mute findings: tutorials/prowler-app-mute-findings.md
|
||||
- Amazon S3 Integration: tutorials/prowler-app-s3-integration.md
|
||||
- Lighthouse: tutorials/prowler-app-lighthouse.md
|
||||
- CLI:
|
||||
- Miscellaneous: tutorials/misc.md
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
## Deployment using Terraform
|
||||
|
||||
To deploy the Prowler Scan Role in order to allow scanning your AWS account from Prowler, please run the following commands in your terminal:
|
||||
This Terraform configuration creates the necessary IAM role and policies to allow Prowler to scan your AWS account, with optional S3 integration for storing scan reports.
|
||||
|
||||
1. `terraform init`
|
||||
2. `terraform plan`
|
||||
3. `terraform apply`
|
||||
### Quick Start
|
||||
|
||||
During the `terraform plan` and `terraform apply` steps you will be asked for an External ID to be configured in the `ProwlerScan` IAM role.
|
||||
1. **Configure variables:**
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
# Edit terraform.tfvars with your values
|
||||
```
|
||||
|
||||
2. **Deploy:**
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
### Variables
|
||||
|
||||
@@ -15,7 +24,7 @@ During the `terraform plan` and `terraform apply` steps you will be asked for an
|
||||
- `iam_principal` (optional): IAM principal pattern allowed to assume the role (defaults to Prowler Cloud: "role/prowler*")
|
||||
- `enable_s3_integration` (optional): Enable S3 integration for storing scan reports (default: false)
|
||||
- `s3_integration_bucket_name` (conditional): S3 bucket name for reports (required if `enable_s3_integration` is true)
|
||||
- `s3_integration_bucket_account` (conditional): S3 bucket owner account ID (required if `enable_s3_integration` is true)
|
||||
- `s3_integration_bucket_account_id` (conditional): S3 bucket owner account ID (required if `enable_s3_integration` is true)
|
||||
|
||||
### Usage Examples
|
||||
|
||||
@@ -30,18 +39,26 @@ terraform apply \
|
||||
-var="external_id=your-external-id-here" \
|
||||
-var="enable_s3_integration=true" \
|
||||
-var="s3_integration_bucket_name=your-s3-bucket-name" \
|
||||
-var="s3_integration_bucket_account=123456789012"
|
||||
-var="s3_integration_bucket_account_id=123456789012"
|
||||
```
|
||||
|
||||
#### Using terraform.tfvars file:
|
||||
Create a `terraform.tfvars` file:
|
||||
```hcl
|
||||
external_id = "your-external-id-here"
|
||||
enable_s3_integration = true
|
||||
s3_integration_bucket_name = "your-s3-bucket-name"
|
||||
s3_integration_bucket_account = "123456789012"
|
||||
#### Using terraform.tfvars file (Recommended):
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
# Edit the file with your values
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Then run: `terraform apply`
|
||||
#### Command line variables (Alternative):
|
||||
```bash
|
||||
terraform apply -var="external_id=your-external-id-here"
|
||||
```
|
||||
|
||||
> Note that Terraform will use the AWS credentials of your default profile.
|
||||
### Outputs
|
||||
|
||||
After successful deployment, you'll get:
|
||||
- `prowler_role_arn`: The ARN of the created IAM role (use this in Prowler App)
|
||||
- `prowler_role_name`: The name of the IAM role
|
||||
- `s3_integration_enabled`: Whether S3 integration is enabled
|
||||
|
||||
> **Note:** Terraform will use the AWS credentials of your default profile or AWS_PROFILE environment variable.
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
locals {
|
||||
s3_integration_validation = (
|
||||
!var.enable_s3_integration ||
|
||||
(var.enable_s3_integration && var.s3_integration_bucket_name != "" && var.s3_integration_bucket_account != "")
|
||||
(var.enable_s3_integration && var.s3_integration_bucket_name != "" && var.s3_integration_bucket_account_id != "")
|
||||
)
|
||||
}
|
||||
|
||||
# Validation check using check block (Terraform 1.5+)
|
||||
check "s3_integration_requirements" {
|
||||
assert {
|
||||
condition = !var.enable_s3_integration || (var.s3_integration_bucket_name != "" && var.s3_integration_bucket_account != "")
|
||||
error_message = "When enable_s3_integration is true, both s3_integration_bucket_name and s3_integration_bucket_account must be provided and non-empty."
|
||||
condition = !var.enable_s3_integration || (var.s3_integration_bucket_name != "" && var.s3_integration_bucket_account_id != "")
|
||||
error_message = "When enable_s3_integration is true, both s3_integration_bucket_name and s3_integration_bucket_account_id must be provided and non-empty."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ module "s3_integration" {
|
||||
source = "./s3-integration"
|
||||
|
||||
s3_integration_bucket_name = var.s3_integration_bucket_name
|
||||
s3_integration_bucket_account = var.s3_integration_bucket_account
|
||||
s3_integration_bucket_account_id = var.s3_integration_bucket_account_id
|
||||
|
||||
prowler_role_name = aws_iam_role.prowler_scan.name
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ resource "aws_iam_role_policy" "prowler_s3_integration" {
|
||||
]
|
||||
Condition = {
|
||||
StringEquals = {
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account_id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -31,7 +31,7 @@ resource "aws_iam_role_policy" "prowler_s3_integration" {
|
||||
]
|
||||
Condition = {
|
||||
StringEquals = {
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account_id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -45,7 +45,7 @@ resource "aws_iam_role_policy" "prowler_s3_integration" {
|
||||
]
|
||||
Condition = {
|
||||
StringEquals = {
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account
|
||||
"s3:ResourceAccount" = var.s3_integration_bucket_account_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ variable "s3_integration_bucket_name" {
|
||||
}
|
||||
}
|
||||
|
||||
variable "s3_integration_bucket_account" {
|
||||
variable "s3_integration_bucket_account_id" {
|
||||
type = string
|
||||
description = "The AWS Account ID owner of the S3 Bucket."
|
||||
|
||||
validation {
|
||||
condition = length(var.s3_integration_bucket_account) == 12 && can(tonumber(var.s3_integration_bucket_account))
|
||||
error_message = "s3_integration_bucket_account must be a valid 12-digit AWS Account ID."
|
||||
condition = length(var.s3_integration_bucket_account_id) == 12 && can(tonumber(var.s3_integration_bucket_account_id))
|
||||
error_message = "s3_integration_bucket_account_id must be a valid 12-digit AWS Account ID."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# =============================================================================
|
||||
# Prowler Terraform Configuration
|
||||
# =============================================================================
|
||||
|
||||
# REQUIRED: External ID from your Prowler App
|
||||
external_id = "your-unique-external-id-here"
|
||||
|
||||
# =============================================================================
|
||||
# Optional Variables (uncomment and modify as needed)
|
||||
# =============================================================================
|
||||
|
||||
# Prowler Cloud Service Account (leave default unless using self-hosted)
|
||||
# account_id = "232136659152"
|
||||
|
||||
# IAM Principal Pattern (leave default unless using self-hosted)
|
||||
# iam_principal = "role/prowler*"
|
||||
|
||||
# =============================================================================
|
||||
# S3 Integration Configuration
|
||||
# =============================================================================
|
||||
# Uncomment the following lines to enable S3 integration for storing scan reports
|
||||
|
||||
# Enable S3 integration
|
||||
# enable_s3_integration = true
|
||||
|
||||
# S3 bucket name where reports will be stored
|
||||
# s3_integration_bucket_name = "my-prowler-reports-bucket"
|
||||
|
||||
# AWS Account ID that owns the S3 bucket (usually your account)
|
||||
# s3_integration_bucket_account_id = "123456789012"
|
||||
@@ -1,13 +1,31 @@
|
||||
# Required variable
|
||||
# =============================================================================
|
||||
# Prowler Terraform Configuration
|
||||
# =============================================================================
|
||||
|
||||
# REQUIRED: External ID from your Prowler App setup
|
||||
# This must match exactly what you configured in Prowler App
|
||||
external_id = "your-unique-external-id-here"
|
||||
|
||||
# Optional Variables
|
||||
# Prowler Cloud Account
|
||||
# =============================================================================
|
||||
# Optional Variables (uncomment and modify as needed)
|
||||
# =============================================================================
|
||||
|
||||
# Prowler Cloud Service Account (leave default unless using self-hosted)
|
||||
# account_id = "232136659152"
|
||||
# Prowler Cloud Role
|
||||
|
||||
# IAM Principal Pattern (leave default unless using self-hosted)
|
||||
# iam_principal = "role/prowler*"
|
||||
|
||||
# S3 Integration (optional)
|
||||
# =============================================================================
|
||||
# S3 Integration Configuration
|
||||
# =============================================================================
|
||||
# Uncomment the following lines to enable S3 integration for storing scan reports
|
||||
|
||||
# Enable S3 integration
|
||||
# enable_s3_integration = true
|
||||
# s3_bucket_name = "your-prowler-reports-bucket"
|
||||
# s3_bucket_account = "123456789012"
|
||||
|
||||
# S3 bucket name where reports will be stored
|
||||
# s3_integration_bucket_name = "my-prowler-reports-bucket"
|
||||
|
||||
# AWS Account ID that owns the S3 bucket (usually your account)
|
||||
# s3_integration_bucket_account_id = "123456789012"
|
||||
|
||||
@@ -44,13 +44,13 @@ variable "s3_integration_bucket_name" {
|
||||
}
|
||||
}
|
||||
|
||||
variable "s3_integration_bucket_account" {
|
||||
variable "s3_integration_bucket_account_id" {
|
||||
type = string
|
||||
description = "The AWS Account ID owner of the S3 Bucket. Required if enable_s3_integration is true."
|
||||
default = ""
|
||||
|
||||
validation {
|
||||
condition = var.s3_integration_bucket_account == "" || (length(var.s3_integration_bucket_account) == 12 && can(tonumber(var.s3_integration_bucket_account)))
|
||||
error_message = "s3_integration_bucket_account must be a valid 12-digit AWS Account ID or empty."
|
||||
condition = var.s3_integration_bucket_account_id == "" || (length(var.s3_integration_bucket_account_id) == 12 && can(tonumber(var.s3_integration_bucket_account_id)))
|
||||
error_message = "s3_integration_bucket_account_id must be a valid 12-digit AWS Account ID or empty."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,37 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.10.2] (Prowler 5.10.2)
|
||||
## [v5.11.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Certificate authentication for M365 provider [(#8404)](https://github.com/prowler-cloud/prowler/pull/8404)
|
||||
- `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200)
|
||||
- `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202)
|
||||
- Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526)
|
||||
- Add User Email and APP name/installations information in GitHub provider [(#8501)](https://github.com/prowler-cloud/prowler/pull/8501)
|
||||
- Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530)
|
||||
- `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536)
|
||||
- ECS privilege escalation patterns (StartTask and RunTask) for AWS provider [(#8541)](https://github.com/prowler-cloud/prowler/pull/8541)
|
||||
|
||||
|
||||
### Changed
|
||||
- Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479)
|
||||
|
||||
### Fixed
|
||||
|
||||
---
|
||||
|
||||
## [v5.10.3] (Prowler UNRELEASED)
|
||||
|
||||
### Fixed
|
||||
- AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533)
|
||||
- GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529)
|
||||
- List all accessible organizations in GitHub provider [(#8535)](https://github.com/prowler-cloud/prowler/pull/8535)
|
||||
- Only evaluate enabled accounts in `entra_users_mfa_capable` check [(#8544)](https://github.com/prowler-cloud/prowler/pull/8544)
|
||||
|
||||
---
|
||||
|
||||
## [v5.10.2] (Prowler v5.10.2)
|
||||
|
||||
### Fixed
|
||||
- Order requirements by ID in Prowler ThreatScore AWS compliance framework [(#8495)](https://github.com/prowler-cloud/prowler/pull/8495)
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.10.2"
|
||||
prowler_version = "5.11.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -459,6 +459,9 @@ azure:
|
||||
"Standard_DS3_v2",
|
||||
"Standard_D4s_v3",
|
||||
]
|
||||
# Azure VM Backup Configuration
|
||||
# azure.vm_sufficient_daily_backup_retention_period
|
||||
vm_backup_min_daily_retention_days: 7
|
||||
|
||||
# GCP Configuration
|
||||
gcp:
|
||||
|
||||
@@ -2,6 +2,7 @@ from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
|
||||
|
||||
def get_prowler_threatscore_table(
|
||||
@@ -25,7 +26,10 @@ def get_prowler_threatscore_table(
|
||||
pillars = {}
|
||||
score_per_pillar = {}
|
||||
max_score_per_pillar = {}
|
||||
counted_findings = []
|
||||
counted_findings_per_pillar = {}
|
||||
generic_score = 0
|
||||
generic_max_score = 0
|
||||
generic_counted_findings = []
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
@@ -33,18 +37,24 @@ def get_prowler_threatscore_table(
|
||||
if compliance.Framework == "ProwlerThreatScore":
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
# Score per pillar logic
|
||||
pillar = attribute.Section
|
||||
|
||||
if not any(
|
||||
[
|
||||
pillar in score_per_pillar.keys(),
|
||||
pillar in max_score_per_pillar.keys(),
|
||||
pillar in counted_findings_per_pillar.keys(),
|
||||
]
|
||||
):
|
||||
score_per_pillar[pillar] = 0
|
||||
max_score_per_pillar[pillar] = 0
|
||||
counted_findings_per_pillar[pillar] = []
|
||||
|
||||
if index not in counted_findings:
|
||||
if (
|
||||
index not in counted_findings_per_pillar.get(pillar, [])
|
||||
and not finding.muted
|
||||
):
|
||||
if finding.status == "PASS":
|
||||
score_per_pillar[pillar] += (
|
||||
attribute.LevelOfRisk * attribute.Weight
|
||||
@@ -52,7 +62,7 @@ def get_prowler_threatscore_table(
|
||||
max_score_per_pillar[pillar] += (
|
||||
attribute.LevelOfRisk * attribute.Weight
|
||||
)
|
||||
counted_findings.append(index)
|
||||
counted_findings_per_pillar[pillar].append(index)
|
||||
|
||||
if pillar not in pillars:
|
||||
pillars[pillar] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
@@ -69,6 +79,17 @@ def get_prowler_threatscore_table(
|
||||
pass_count.append(index)
|
||||
pillars[pillar]["PASS"] += 1
|
||||
|
||||
# Generic score logic
|
||||
if index not in generic_counted_findings and not finding.muted:
|
||||
if finding.status == "PASS":
|
||||
generic_score += (
|
||||
attribute.LevelOfRisk * attribute.Weight
|
||||
)
|
||||
generic_max_score += (
|
||||
attribute.LevelOfRisk * attribute.Weight
|
||||
)
|
||||
generic_counted_findings.append(index)
|
||||
|
||||
pillars = dict(sorted(pillars.items()))
|
||||
for pillar in pillars:
|
||||
pillar_table["Provider"].append(compliance.Provider)
|
||||
@@ -88,6 +109,27 @@ def get_prowler_threatscore_table(
|
||||
f"{orange_color}{pillars[pillar]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
# Add pillars with no findings to the table with Status: PASS and Score: 100%
|
||||
provider_name = compliance_framework.split("_")[-1]
|
||||
bulk_compliance_frameworks = Compliance.get_bulk(provider_name)
|
||||
|
||||
unique_sections = set()
|
||||
for compliance_name, compliance in bulk_compliance_frameworks.items():
|
||||
if compliance_name.startswith(f"prowler_threatscore_{provider_name}"):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
unique_sections.add(attribute.Section)
|
||||
|
||||
for section in unique_sections:
|
||||
if section not in pillars:
|
||||
pillar_table["Provider"].append(provider_name.capitalize())
|
||||
pillar_table["Pillar"].append(section)
|
||||
pillar_table["Score"].append(
|
||||
f"{Style.BRIGHT}{Fore.GREEN}100.00%{Style.RESET_ALL}"
|
||||
)
|
||||
pillar_table["Status"].append(f"{Fore.GREEN}PASS(0){Style.RESET_ALL}")
|
||||
pillar_table["Muted"].append(f"{orange_color}0{Style.RESET_ALL}")
|
||||
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
@@ -104,9 +146,12 @@ def get_prowler_threatscore_table(
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(
|
||||
f"\n{Style.BRIGHT}Overall ThreatScore: {generic_score / generic_max_score * 100:.2f}%{Style.RESET_ALL}"
|
||||
)
|
||||
if len(fail_count) > 0 and len(pillar_table["Pillar"]) > 0:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
f"Framework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
|
||||
print(
|
||||
|
||||
@@ -19,6 +19,7 @@ from prowler.lib.outputs.compliance.compliance import get_check_compliance
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
|
||||
|
||||
|
||||
class Finding(BaseModel):
|
||||
@@ -250,15 +251,16 @@ class Finding(BaseModel):
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
|
||||
if hasattr(provider.identity, "account_name"):
|
||||
if isinstance(provider.identity, GithubIdentityInfo):
|
||||
# GithubIdentityInfo (Personal Access Token, OAuth)
|
||||
output_data["account_name"] = provider.identity.account_name
|
||||
output_data["account_uid"] = provider.identity.account_id
|
||||
elif hasattr(provider.identity, "app_id"):
|
||||
output_data["account_email"] = provider.identity.account_email
|
||||
elif isinstance(provider.identity, GithubAppIdentityInfo):
|
||||
# GithubAppIdentityInfo (GitHub App)
|
||||
# TODO: Get Github App name
|
||||
output_data["account_name"] = f"app-{provider.identity.app_id}"
|
||||
output_data["account_name"] = provider.identity.app_name
|
||||
output_data["account_uid"] = provider.identity.app_id
|
||||
output_data["installations"] = provider.identity.installations
|
||||
|
||||
output_data["region"] = check_output.owner
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class HTML(Output):
|
||||
<td>{finding_status}</td>
|
||||
<td>{finding.metadata.Severity.value}</td>
|
||||
<td>{finding.metadata.ServiceName}</td>
|
||||
<td>{":".join([finding.resource_metadata['file_path'], "-".join(map(str, finding.resource_metadata['file_line_range']))]) if finding.metadata.Provider == "iac" else finding.region.lower()}</td>
|
||||
<td>{":".join([finding.resource_metadata["file_path"], "-".join(map(str, finding.resource_metadata["file_line_range"]))]) if finding.metadata.Provider == "iac" else finding.region.lower()}</td>
|
||||
<td>{finding.metadata.CheckID.replace("_", "<wbr />_")}</td>
|
||||
<td>{finding.metadata.CheckTitle}</td>
|
||||
<td>{finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
@@ -558,10 +558,65 @@ class HTML(Output):
|
||||
try:
|
||||
if hasattr(provider.identity, "account_name"):
|
||||
# GithubIdentityInfo (Personal Access Token, OAuth)
|
||||
account_display = provider.identity.account_name
|
||||
account_info_items = f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub account:</b> {provider.identity.account_name}
|
||||
</li>
|
||||
"""
|
||||
# Add email if available
|
||||
if (
|
||||
hasattr(provider.identity, "account_email")
|
||||
and provider.identity.account_email
|
||||
):
|
||||
account_info_items += f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub account email:</b> {provider.identity.account_email}
|
||||
</li>"""
|
||||
elif hasattr(provider.identity, "app_id"):
|
||||
# GithubAppIdentityInfo (GitHub App)
|
||||
account_display = f"app-{provider.identity.app_id}"
|
||||
# Assessment items: App Name and Installations
|
||||
account_info_items = f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub App Name:</b> {provider.identity.app_name}
|
||||
</li>"""
|
||||
# Add installations if available
|
||||
if (
|
||||
hasattr(provider.identity, "installations")
|
||||
and provider.identity.installations
|
||||
):
|
||||
installations_display = ", ".join(provider.identity.installations)
|
||||
account_info_items += f"""
|
||||
<li class="list-group-item">
|
||||
<b>Installations:</b> {installations_display}
|
||||
</li>"""
|
||||
else:
|
||||
account_info_items += """
|
||||
<li class="list-group-item">
|
||||
<b>Installations:</b> No installations found
|
||||
</li>"""
|
||||
|
||||
# Credentials items: Authentication method and App ID
|
||||
credentials_items = f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> {provider.auth_method}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>GitHub App ID:</b> {provider.identity.app_id}
|
||||
</li>"""
|
||||
else:
|
||||
# Fallback for other identity types
|
||||
account_info_items = ""
|
||||
credentials_items = f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> {provider.auth_method}
|
||||
</li>"""
|
||||
|
||||
# For PAT/OAuth, use default credentials structure
|
||||
if hasattr(provider.identity, "account_name"):
|
||||
credentials_items = f"""
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> {provider.auth_method}
|
||||
</li>"""
|
||||
|
||||
return f"""
|
||||
<div class="col-md-2">
|
||||
@@ -569,11 +624,8 @@ class HTML(Output):
|
||||
<div class="card-header">
|
||||
GitHub Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>GitHub account:</b> {account_display}
|
||||
</li>
|
||||
<ul class="list-group list-group-flush">
|
||||
{account_info_items}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,11 +634,8 @@ class HTML(Output):
|
||||
<div class="card-header">
|
||||
GitHub Credentials
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> {provider.auth_method}
|
||||
</li>
|
||||
<ul class="list-group list-group-flush">
|
||||
{credentials_items}
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
@@ -8,7 +8,7 @@ def is_resource_filtered(resource: str, audit_resources: list) -> bool:
|
||||
Returns True if it is filtered and False if it does not match the input filters
|
||||
"""
|
||||
try:
|
||||
if resource in str(audit_resources):
|
||||
if resource in audit_resources:
|
||||
return True
|
||||
return False
|
||||
except Exception as error:
|
||||
|
||||
@@ -1430,7 +1430,9 @@
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bedrock-runtime": {
|
||||
@@ -4269,15 +4271,18 @@
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -5705,12 +5710,15 @@
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -5916,9 +5924,11 @@
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -6580,6 +6590,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -6673,6 +6684,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8260,6 +8272,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9914,6 +9927,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "eks_cluster_deletion_protection_enabled",
|
||||
"CheckTitle": "Ensure EKS clusters have deletion protection enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices/Resource Management"
|
||||
],
|
||||
"ServiceName": "eks",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEksCluster",
|
||||
"Description": "Ensure that your Amazon EKS clusters have deletion protection enabled to prevent accidental deletion of critical Kubernetes clusters.",
|
||||
"Risk": "Without deletion protection, EKS clusters can be accidentally deleted through Terraform automation, AWS CLI commands, or the AWS console, leading to data loss and service disruption.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/eks/latest/userguide/deletion-protection.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws eks update-cluster-config --region <region_name> --name <cluster_name> --deletion-protection",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "resource \"aws_eks_cluster\" \"example\" {\n name = \"example-cluster\"\n role_arn = aws_iam_role.example.arn\n deletion_protection = true\n # ... other configuration\n}"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable deletion protection on all EKS clusters to prevent accidental deletion. This is especially important for production clusters and those managed through Infrastructure as Code (IaC) tools.",
|
||||
"Url": "https://docs.aws.amazon.com/eks/latest/userguide/deletion-protection.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.eks.eks_client import eks_client
|
||||
|
||||
|
||||
class eks_cluster_deletion_protection_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in eks_client.clusters:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"EKS cluster {cluster.name} has deletion protection enabled."
|
||||
)
|
||||
if cluster.deletion_protection is False:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"EKS cluster {cluster.name} has deletion protection disabled."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -83,6 +83,10 @@ class EKS(AWSService):
|
||||
]["publicAccessCidrs"]
|
||||
if "encryptionConfig" in describe_cluster["cluster"]:
|
||||
cluster.encryptionConfig = True
|
||||
if "deletionProtection" in describe_cluster["cluster"]:
|
||||
cluster.deletion_protection = describe_cluster["cluster"][
|
||||
"deletionProtection"
|
||||
]
|
||||
cluster.tags = [describe_cluster["cluster"].get("tags")]
|
||||
cluster.version = describe_cluster["cluster"].get("version", "")
|
||||
|
||||
@@ -108,4 +112,5 @@ class EKSCluster(BaseModel):
|
||||
endpoint_private_access: bool = None
|
||||
public_access_cidrs: list[str] = []
|
||||
encryptionConfig: bool = None
|
||||
deletion_protection: bool = None
|
||||
tags: Optional[list] = []
|
||||
|
||||
@@ -24,7 +24,6 @@ privilege_escalation_policies_combination = {
|
||||
"IAMPut": {"iam:Put*"},
|
||||
"CreatePolicyVersion": {"iam:CreatePolicyVersion"},
|
||||
"SetDefaultPolicyVersion": {"iam:SetDefaultPolicyVersion"},
|
||||
"iam:PassRole": {"iam:PassRole"},
|
||||
"PassRole+EC2": {
|
||||
"iam:PassRole",
|
||||
"ec2:RunInstances",
|
||||
@@ -69,6 +68,21 @@ privilege_escalation_policies_combination = {
|
||||
},
|
||||
"GlueUpdateDevEndpoint": {"glue:UpdateDevEndpoint"},
|
||||
"lambda:UpdateFunctionCode": {"lambda:UpdateFunctionCode"},
|
||||
"lambda:UpdateFunctionConfiguration": {"lambda:UpdateFunctionConfiguration"},
|
||||
"PassRole+CodeStar": {
|
||||
"iam:PassRole",
|
||||
"codestar:CreateProject",
|
||||
},
|
||||
"PassRole+CreateAutoScaling": {
|
||||
"iam:PassRole",
|
||||
"autoscaling:CreateAutoScalingGroup",
|
||||
"autoscaling:CreateLaunchConfiguration",
|
||||
},
|
||||
"PassRole+UpdateAutoScaling": {
|
||||
"iam:PassRole",
|
||||
"autoscaling:UpdateAutoScalingGroup",
|
||||
"autoscaling:CreateLaunchConfiguration",
|
||||
},
|
||||
"iam:CreateAccessKey": {"iam:CreateAccessKey"},
|
||||
"iam:CreateLoginProfile": {"iam:CreateLoginProfile"},
|
||||
"iam:UpdateLoginProfile": {"iam:UpdateLoginProfile"},
|
||||
@@ -86,6 +100,24 @@ privilege_escalation_policies_combination = {
|
||||
"sts:AssumeRole",
|
||||
"iam:UpdateAssumeRolePolicy",
|
||||
},
|
||||
# AgentCore privilege escalation patterns
|
||||
"PassRole+AgentCoreCreateInterpreter+InvokeInterpreter": {
|
||||
"iam:PassRole",
|
||||
"bedrock-agentcore:CreateCodeInterpreter",
|
||||
"bedrock-agentcore:InvokeCodeInterpreter",
|
||||
},
|
||||
# ECS-based privilege escalation patterns
|
||||
# Reference: https://labs.reversec.com/posts/2025/08/another-ecs-privilege-escalation-path
|
||||
"PassRole+ECS+StartTask": {
|
||||
"iam:PassRole",
|
||||
"ecs:StartTask",
|
||||
"ecs:RegisterContainerInstance",
|
||||
"ecs:DeregisterContainerInstance",
|
||||
},
|
||||
"PassRole+ECS+RunTask": {
|
||||
"iam:PassRole",
|
||||
"ecs:RunTask",
|
||||
},
|
||||
# TO-DO: We have to handle AssumeRole just if the resource is * and without conditions
|
||||
# "sts:AssumeRole": {"sts:AssumeRole"},
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class Defender(AzureService):
|
||||
).token
|
||||
)
|
||||
self.iot_security_solutions = self._get_iot_security_solutions()
|
||||
self.jit_policies = self._get_jit_policies()
|
||||
|
||||
def _get_pricings(self):
|
||||
logger.info("Defender - Getting pricings...")
|
||||
@@ -246,6 +247,44 @@ class Defender(AzureService):
|
||||
)
|
||||
return iot_security_solutions
|
||||
|
||||
def _get_jit_policies(self) -> dict[str, dict]:
|
||||
"""
|
||||
Get all JIT policies for all subscriptions.
|
||||
|
||||
Returns:
|
||||
A dictionary of JIT policies for each subscription. The format will be:
|
||||
{
|
||||
"subscription_name": {
|
||||
"jit_policy_id": JITPolicy
|
||||
}
|
||||
}
|
||||
"""
|
||||
logger.info("Defender - Getting JIT policies...")
|
||||
jit_policies = {}
|
||||
for subscription_name, client in self.clients.items():
|
||||
try:
|
||||
jit_policies[subscription_name] = {}
|
||||
policies = client.jit_network_access_policies.list()
|
||||
for policy in policies:
|
||||
vm_ids = set()
|
||||
for vm in getattr(policy, "virtual_machines", []):
|
||||
vm_ids.add(vm.id)
|
||||
jit_policies[subscription_name].update(
|
||||
{
|
||||
policy.id: JITPolicy(
|
||||
id=policy.id,
|
||||
name=policy.name,
|
||||
location=getattr(policy, "location", "Global"),
|
||||
vm_ids=vm_ids,
|
||||
),
|
||||
}
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return jit_policies
|
||||
|
||||
|
||||
class Pricing(BaseModel):
|
||||
resource_id: str
|
||||
@@ -317,3 +356,10 @@ class IoTSecuritySolution(BaseModel):
|
||||
resource_id: str
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
class JITPolicy(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str = ""
|
||||
vm_ids: list[str] = []
|
||||
|
||||
@@ -11,20 +11,30 @@ from prowler.providers.azure.lib.service.service import AzureService
|
||||
|
||||
|
||||
class BackupItem(BaseModel):
|
||||
"""Minimal BackupItem: only essential identifying and descriptive fields."""
|
||||
"""Model that represents a backup item."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
workload_type: Optional[DataSourceType]
|
||||
backup_policy_id: Optional[str] = None
|
||||
|
||||
|
||||
class BackupPolicy(BaseModel):
|
||||
"""Model that represents a backup policy."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
retention_days: Optional[int] = None
|
||||
|
||||
|
||||
class BackupVault(BaseModel):
|
||||
"""Minimal BackupVault: only essential identifying fields and its backup items."""
|
||||
"""Model that represents a backup vault."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
backup_protected_items: dict[str, BackupItem] = Field(default_factory=dict)
|
||||
backup_policies: dict[str, BackupPolicy] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class Recovery(AzureService):
|
||||
@@ -71,6 +81,9 @@ class RecoveryBackup(AzureService):
|
||||
vault.backup_protected_items = self._get_backup_protected_items(
|
||||
subscription_name=subscription_name, vault=vault
|
||||
)
|
||||
vault.backup_policies = self._get_backup_policies(
|
||||
subscription_name=subscription_name, vault=vault
|
||||
)
|
||||
|
||||
def _get_backup_protected_items(
|
||||
self, subscription_name: str, vault: BackupVault
|
||||
@@ -95,7 +108,58 @@ class RecoveryBackup(AzureService):
|
||||
workload_type=(
|
||||
item_properties.workload_type if item_properties else None
|
||||
),
|
||||
backup_policy_id=(
|
||||
item_properties.policy_id if item_properties else None
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Recovery - Error getting backup protected items: {e}")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return backup_protected_items_dict
|
||||
|
||||
def _get_backup_policies(
|
||||
self, subscription_name: str, vault: BackupVault
|
||||
) -> dict[str, BackupPolicy]:
|
||||
"""
|
||||
Retrieve all backup policies for a given vault.
|
||||
"""
|
||||
logger.info("Recovery - Getting backup policies...")
|
||||
backup_policies_dict: dict[str, BackupPolicy] = {}
|
||||
unique_backup_policies: set[str] = set()
|
||||
try:
|
||||
for item in vault.backup_protected_items.values():
|
||||
if item.backup_policy_id:
|
||||
unique_backup_policies.add(item.backup_policy_id)
|
||||
for policy_id in unique_backup_policies:
|
||||
policy = self.clients[subscription_name].protection_policies.get(
|
||||
vault_name=vault.name,
|
||||
resource_group_name=vault.id.split("/")[4],
|
||||
policy_name=policy_id.split("/")[-1],
|
||||
)
|
||||
backup_policies_dict[policy_id] = BackupPolicy(
|
||||
id=policy.id,
|
||||
name=policy.name,
|
||||
retention_days=getattr(
|
||||
getattr(
|
||||
getattr(
|
||||
getattr(
|
||||
getattr(policy, "properties", None),
|
||||
"retention_policy",
|
||||
None,
|
||||
),
|
||||
"daily_schedule",
|
||||
None,
|
||||
),
|
||||
"retention_duration",
|
||||
None,
|
||||
),
|
||||
"count",
|
||||
None,
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return backup_policies_dict
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "vm_jit_access_enabled",
|
||||
"CheckTitle": "Enable Just-In-Time Access for Virtual Machines",
|
||||
"CheckType": [],
|
||||
"ServiceName": "vm",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Microsoft.Compute/virtualMachines",
|
||||
"Description": "Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access.",
|
||||
"Risk": "Without JIT access, management ports such as 22 (SSH) and 3389 (RDP) may be exposed, increasing the risk of brute-force and DDoS attacks.",
|
||||
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az security jit-policy list --query '[*].virtualMachines[*].id | []'",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable Just-in-Time (JIT) network access for your Microsoft Azure virtual machines using the Azure Portal under Security Center > Just-in-time VM access.",
|
||||
"Url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "JIT access can only be enabled via the Azure Portal. Ensure Security Center standard pricing tier for servers is enabled."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.defender.defender_client import defender_client
|
||||
from prowler.providers.azure.services.vm.vm_client import vm_client
|
||||
|
||||
|
||||
class vm_jit_access_enabled(Check):
|
||||
"""
|
||||
Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access.
|
||||
|
||||
This check evaluates whether JIT access is enabled for each VM to reduce the attack surface.
|
||||
- PASS: VM has JIT access enabled.
|
||||
- FAIL: VM does not have JIT access enabled.
|
||||
"""
|
||||
|
||||
def execute(self):
|
||||
findings = []
|
||||
jit_enabled_vms = set()
|
||||
for subscription_name, vms in vm_client.virtual_machines.items():
|
||||
for jit_policy in defender_client.jit_policies[subscription_name].values():
|
||||
jit_enabled_vms.update(jit_policy.vm_ids)
|
||||
for vm in vms.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=vm)
|
||||
report.subscription = subscription_name
|
||||
if vm.resource_id.lower() in {
|
||||
vm_id.lower() for vm_id in jit_enabled_vms
|
||||
}:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has JIT (Just-in-Time) access enabled."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} does not have JIT (Just-in-Time) access enabled."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "vm_sufficient_daily_backup_retention_period",
|
||||
"CheckTitle": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "vm",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Microsoft.Compute/virtualMachines",
|
||||
"Description": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.",
|
||||
"Risk": "Having an optimal daily backup retention period for your Azure virtual machines will enforce your backup strategy to follow the best practices as specified in the compliance regulations promoted by your organization. Retaining VM backups for a longer period of time will allow you to handle more efficiently your data restoration process in the event of a failure.",
|
||||
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Set the daily backup retention period for each VM's backup policy to meet or exceed your organization's minimum requirement.",
|
||||
"Url": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.recovery.recovery_client import recovery_client
|
||||
from prowler.providers.azure.services.vm.vm_client import vm_client
|
||||
|
||||
|
||||
class vm_sufficient_daily_backup_retention_period(Check):
|
||||
"""
|
||||
Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.
|
||||
- PASS: The VM has a backup policy with sufficient daily retention period.
|
||||
- FAIL: The VM does not have a backup policy or the retention period is insufficient.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
findings = []
|
||||
min_retention_days = getattr(vm_client, "audit_config", {}).get(
|
||||
"vm_backup_min_daily_retention_days", 7
|
||||
)
|
||||
|
||||
for subscription, vms in vm_client.virtual_machines.items():
|
||||
vaults = recovery_client.vaults.get(subscription, {})
|
||||
for vm in vms.values():
|
||||
backup_found = False
|
||||
retention_days = None
|
||||
for vault in vaults.values():
|
||||
for backup_item in vault.backup_protected_items.values():
|
||||
if (
|
||||
backup_item.workload_type == DataSourceType.VM
|
||||
and backup_item.name.split(";")[-1] == vm.resource_name
|
||||
):
|
||||
backup_found = True
|
||||
policy_id = backup_item.backup_policy_id
|
||||
if policy_id and policy_id in vault.backup_policies:
|
||||
retention_days = vault.backup_policies[
|
||||
policy_id
|
||||
].retention_days
|
||||
break
|
||||
if backup_found:
|
||||
break
|
||||
if backup_found and retention_days:
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=vm)
|
||||
report.subscription = subscription
|
||||
if retention_days >= min_retention_days:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has a daily backup retention period of {retention_days} days (minimum required: {min_retention_days})."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has insufficient daily backup retention period of {retention_days} days (minimum required: {min_retention_days})."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -222,6 +222,8 @@ class Provider(ABC):
|
||||
env_auth=arguments.env_auth,
|
||||
az_cli_auth=arguments.az_cli_auth,
|
||||
browser_auth=arguments.browser_auth,
|
||||
certificate_auth=arguments.certificate_auth,
|
||||
certificate_path=arguments.certificate_path,
|
||||
tenant_id=arguments.tenant_id,
|
||||
init_modules=arguments.init_modules,
|
||||
fixer_config=fixer_config,
|
||||
|
||||
@@ -350,10 +350,12 @@ class GithubProvider(Provider):
|
||||
auth = Auth.Token(session.token)
|
||||
g = Github(auth=auth, retry=retry_config)
|
||||
try:
|
||||
user = g.get_user()
|
||||
identity = GithubIdentityInfo(
|
||||
account_id=g.get_user().id,
|
||||
account_name=g.get_user().login,
|
||||
account_url=g.get_user().url,
|
||||
account_id=user.id,
|
||||
account_name=user.login,
|
||||
account_url=user.url,
|
||||
account_email=user.get_emails()[0].email,
|
||||
)
|
||||
return identity
|
||||
|
||||
@@ -365,8 +367,18 @@ class GithubProvider(Provider):
|
||||
elif session.id != 0 and session.key:
|
||||
auth = Auth.AppAuth(session.id, session.key)
|
||||
gi = GithubIntegration(auth=auth, retry=retry_config)
|
||||
installations = []
|
||||
for installation in gi.get_installations():
|
||||
installations.append(
|
||||
installation.raw_data.get("account", {}).get("login")
|
||||
)
|
||||
try:
|
||||
identity = GithubAppIdentityInfo(app_id=gi.get_app().id)
|
||||
app = gi.get_app()
|
||||
identity = GithubAppIdentityInfo(
|
||||
app_id=app.id,
|
||||
app_name=app.name,
|
||||
installations=installations,
|
||||
)
|
||||
return identity
|
||||
|
||||
except Exception as error:
|
||||
@@ -393,11 +405,13 @@ class GithubProvider(Provider):
|
||||
report_lines = [
|
||||
f"GitHub Account: {Fore.YELLOW}{self.identity.account_name}{Style.RESET_ALL}",
|
||||
f"GitHub Account ID: {Fore.YELLOW}{self.identity.account_id}{Style.RESET_ALL}",
|
||||
f"GitHub Account Email: {Fore.YELLOW}{self.identity.account_email}{Style.RESET_ALL}",
|
||||
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
|
||||
]
|
||||
elif isinstance(self.identity, GithubAppIdentityInfo):
|
||||
report_lines = [
|
||||
f"GitHub App ID: {Fore.YELLOW}{self.identity.app_id}{Style.RESET_ALL}",
|
||||
f"GitHub App Name: {Fore.YELLOW}{self.identity.app_name}{Style.RESET_ALL}",
|
||||
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
|
||||
]
|
||||
report_title = (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.config.config import output_file_timestamp
|
||||
@@ -14,10 +16,13 @@ class GithubIdentityInfo(BaseModel):
|
||||
account_id: str
|
||||
account_name: str
|
||||
account_url: str
|
||||
account_email: Optional[str] = None
|
||||
|
||||
|
||||
class GithubAppIdentityInfo(BaseModel):
|
||||
app_id: str
|
||||
app_name: str
|
||||
installations: list[str]
|
||||
|
||||
|
||||
class GithubOutputOptions(ProviderOutputOptions):
|
||||
|
||||
@@ -5,6 +5,7 @@ from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.github.lib.service.service import GithubService
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
|
||||
|
||||
|
||||
class Organization(GithubService):
|
||||
@@ -113,8 +114,15 @@ class Organization(GithubService):
|
||||
elif not self.provider.repositories:
|
||||
# Default behavior: get all organizations the user is a member of
|
||||
# Only when no repositories are specified
|
||||
for org in client.get_user().get_orgs():
|
||||
self._process_organization(org, organizations)
|
||||
if isinstance(self.provider.identity, GithubIdentityInfo):
|
||||
orgs = client.get_user().get_orgs()
|
||||
for org in orgs:
|
||||
self._process_organization(org, organizations)
|
||||
elif isinstance(self.provider.identity, GithubAppIdentityInfo):
|
||||
orgs = client.get_organizations()
|
||||
if orgs.totalCount > 0:
|
||||
for org in orgs:
|
||||
self._process_organization(org, organizations)
|
||||
|
||||
except github.RateLimitExceededException as error:
|
||||
logger.error(f"GitHub API rate limit exceeded: {error}")
|
||||
|
||||
@@ -7,6 +7,7 @@ from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.github.lib.service.service import GithubService
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo
|
||||
|
||||
|
||||
class Repository(GithubService):
|
||||
@@ -102,12 +103,23 @@ class Repository(GithubService):
|
||||
def _list_repositories(self):
|
||||
"""
|
||||
List repositories based on provider scoping configuration.
|
||||
If the provider is a GitHub App, it will list repositories in the organizations that the GitHub App is installed in.
|
||||
If the provider is a user, it will list repositories where the user is a member or owner.
|
||||
If input repositories are provided, it will list repositories that match the input repositories.
|
||||
If input organizations are provided, it will list repositories in the organizations that match the input organizations.
|
||||
"""
|
||||
logger.info("Repository - Listing Repositories...")
|
||||
repos = {}
|
||||
try:
|
||||
for client in self.clients:
|
||||
if self.provider.repositories or self.provider.organizations:
|
||||
if (
|
||||
self.provider.repositories
|
||||
or self.provider.organizations
|
||||
or (
|
||||
isinstance(self.provider.identity, GithubAppIdentityInfo)
|
||||
and self.provider.identity.installations
|
||||
)
|
||||
):
|
||||
if self.provider.repositories:
|
||||
logger.info(
|
||||
f"Filtering for specific repositories: {self.provider.repositories}"
|
||||
@@ -141,6 +153,24 @@ class Repository(GithubService):
|
||||
self._handle_github_api_error(
|
||||
error, "processing organization", org_name
|
||||
)
|
||||
if (
|
||||
isinstance(self.provider.identity, GithubAppIdentityInfo)
|
||||
and self.provider.identity.installations
|
||||
):
|
||||
logger.info(
|
||||
f"Filtering for repositories in the organizations or accounts that the GitHub App is installed in: {', '.join(self.provider.identity.installations)}"
|
||||
)
|
||||
for org_name in self.provider.identity.installations:
|
||||
try:
|
||||
repos_list, _ = self._get_repositories_from_owner(
|
||||
client, org_name
|
||||
)
|
||||
for repo in repos_list:
|
||||
self._process_repository(repo, repos)
|
||||
except Exception as error:
|
||||
self._handle_github_api_error(
|
||||
error, "processing organization", org_name
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"No repository or organization specified, discovering accessible repositories via GraphQL API..."
|
||||
|
||||
@@ -126,6 +126,18 @@ class M365BaseException(ProwlerException):
|
||||
"message": "Failed to establish connection to Exchange Online API.",
|
||||
"remediation": "Ensure the application has proper permission granted to access Exchange Online.",
|
||||
},
|
||||
(6030, "M365CertificateCreationError"): {
|
||||
"message": "Failed to create X.509 certificate object from provided certificate content.",
|
||||
"remediation": "Ensure the certificate content is valid base64 encoded X.509 certificate data and is properly formatted.",
|
||||
},
|
||||
(6031, "M365NotValidCertificateContentError"): {
|
||||
"message": "The provided certificate content is not valid base64 encoded data.",
|
||||
"remediation": "Ensure the certificate content is valid base64 encoded X.509 certificate data without line breaks or invalid characters.",
|
||||
},
|
||||
(6032, "M365NotValidCertificatePathError"): {
|
||||
"message": "The provided certificate path is not valid or the file cannot be accessed.",
|
||||
"remediation": "Ensure the certificate path exists, is accessible, and points to a valid certificate file.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
@@ -357,3 +369,24 @@ class M365ExchangeConnectionError(M365CredentialsError):
|
||||
super().__init__(
|
||||
6029, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class M365CertificateCreationError(M365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6030, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class M365NotValidCertificateContentError(M365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6031, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class M365NotValidCertificatePathError(M365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6032, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
@@ -28,6 +28,17 @@ def init_parser(self):
|
||||
action="store_true",
|
||||
help="Use Azure interactive browser authentication to log in against Microsoft 365",
|
||||
)
|
||||
m365_auth_modes_group.add_argument(
|
||||
"--certificate-auth",
|
||||
action="store_true",
|
||||
help="Use Certificate authentication to log in against Microsoft 365",
|
||||
)
|
||||
m365_parser.add_argument(
|
||||
"--certificate-path",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Path to the certificate file to be used with --certificate-auth option",
|
||||
)
|
||||
m365_parser.add_argument(
|
||||
"--tenant-id",
|
||||
nargs="?",
|
||||
|
||||
@@ -4,6 +4,7 @@ import platform
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.powershell.powershell import PowerShellSession
|
||||
from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365CertificateCreationError,
|
||||
M365GraphConnectionError,
|
||||
M365UserCredentialsError,
|
||||
M365UserNotBelongingToTenantError,
|
||||
@@ -51,23 +52,71 @@ class M365PowerShell(PowerShellSession):
|
||||
self.tenant_identity = identity
|
||||
self.init_credential(credentials)
|
||||
|
||||
def clean_certificate_content(self, cert_content: str) -> str:
|
||||
"""
|
||||
Clean certificate content for PowerShell consumption.
|
||||
|
||||
Removes newlines, carriage returns, and extra spaces from base64 content
|
||||
to ensure proper parsing in PowerShell.
|
||||
|
||||
Args:
|
||||
cert_content (str): Base64 encoded certificate content
|
||||
|
||||
Returns:
|
||||
str: Cleaned base64 certificate content
|
||||
"""
|
||||
# Clean base64 content - remove any newlines or whitespace
|
||||
clean_content = (
|
||||
cert_content.strip().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
)
|
||||
logger.info(f"Cleaned certificate content length: {len(clean_content)}")
|
||||
return clean_content
|
||||
|
||||
def init_credential(self, credentials: M365Credentials) -> None:
|
||||
"""
|
||||
Initialize PowerShell credential object for Microsoft 365 authentication.
|
||||
|
||||
Sanitizes the username and password, then creates a PSCredential object
|
||||
in the PowerShell session for use with Microsoft 365 cmdlets.
|
||||
Supports three authentication methods:
|
||||
1. User authentication (username/password) - Will be deprecated in September 2025
|
||||
2. Application authentication (client_id/client_secret)
|
||||
3. Certificate authentication (certificate_content in base64/application_id)
|
||||
|
||||
Args:
|
||||
credentials (M365Credentials): The credentials object containing
|
||||
username and password.
|
||||
authentication information.
|
||||
|
||||
Note:
|
||||
The credentials are sanitized to prevent command injection and
|
||||
stored securely in the PowerShell session.
|
||||
"""
|
||||
# Certificate Auth
|
||||
if credentials.certificate_content and credentials.client_id:
|
||||
# Clean certificate content for PowerShell consumption
|
||||
clean_cert_content = self.clean_certificate_content(
|
||||
credentials.certificate_content
|
||||
)
|
||||
|
||||
# Sanitize credentials
|
||||
sanitized_client_id = self.sanitize(credentials.client_id)
|
||||
sanitized_tenant_id = self.sanitize(credentials.tenant_id)
|
||||
|
||||
self.execute(
|
||||
f'$certBytes = [Convert]::FromBase64String("{clean_cert_content}")'
|
||||
)
|
||||
error = self.execute(
|
||||
"$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes)"
|
||||
)
|
||||
if error:
|
||||
raise M365CertificateCreationError(
|
||||
f"[{os.path.basename(__file__)}] Error creating certificate: {error}"
|
||||
)
|
||||
|
||||
self.execute(f'$clientID = "{sanitized_client_id}"')
|
||||
self.execute(f'$tenantID = "{sanitized_tenant_id}"')
|
||||
self.execute(f'$tenantDomain = "{credentials.tenant_domains[0]}"')
|
||||
|
||||
# User Auth (Will be deprecated in September 2025)
|
||||
if credentials.user and credentials.passwd:
|
||||
elif credentials.user and credentials.passwd:
|
||||
credentials.encrypted_passwd = self.encrypt_password(credentials.passwd)
|
||||
|
||||
# Sanitize user and password
|
||||
@@ -135,14 +184,28 @@ class M365PowerShell(PowerShellSession):
|
||||
"""
|
||||
Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
|
||||
|
||||
Supports testing three authentication methods:
|
||||
1. User authentication (username/password)
|
||||
2. Application authentication (client_id/client_secret)
|
||||
3. Certificate authentication (certificate_content in base64/application_id)
|
||||
|
||||
Args:
|
||||
credentials (M365Credentials): The credentials object containing
|
||||
username and password to test.
|
||||
authentication information to test.
|
||||
|
||||
Returns:
|
||||
bool: True if credentials are valid and authentication succeeds, False otherwise.
|
||||
"""
|
||||
if credentials.user and credentials.passwd:
|
||||
# Test Certificate Auth
|
||||
if credentials.certificate_content and credentials.client_id:
|
||||
try:
|
||||
self.test_teams_certificate_connection() or self.test_exchange_certificate_connection()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Exchange Online Certificate connection failed: {e}")
|
||||
|
||||
# Test User Auth
|
||||
elif credentials.user and credentials.passwd:
|
||||
self.execute(
|
||||
f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized
|
||||
)
|
||||
@@ -161,6 +224,7 @@ class M365PowerShell(PowerShellSession):
|
||||
)
|
||||
|
||||
# Validate credentials
|
||||
# Test Exchange Online connection
|
||||
result = self.execute("Connect-ExchangeOnline -Credential $credential")
|
||||
if "https://aka.ms/exov3-module" not in result:
|
||||
if "AADSTS" in result: # Entra Security Token Service Error
|
||||
@@ -168,21 +232,19 @@ class M365PowerShell(PowerShellSession):
|
||||
file=os.path.basename(__file__),
|
||||
message=result,
|
||||
)
|
||||
else: # Could not connect to Exchange Online, try Microsoft Teams
|
||||
result = self.execute(
|
||||
"Connect-MicrosoftTeams -Credential $credential"
|
||||
)
|
||||
if self.tenant_identity.tenant_id not in result:
|
||||
if "AADSTS" in result: # Entra Security Token Service Error
|
||||
raise M365UserCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message=result,
|
||||
)
|
||||
else: # Unknown error, could be a permission issue or modules not installed
|
||||
raise Exception(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"Error connecting to PowerShell modules: {result}",
|
||||
)
|
||||
# Test Microsoft Teams connection
|
||||
result = self.execute("Connect-MicrosoftTeams -Credential $credential")
|
||||
if self.tenant_identity.user not in result:
|
||||
if "AADSTS" in result: # Entra Security Token Service Error
|
||||
raise M365UserCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message=result,
|
||||
)
|
||||
else: # Unknown error, could be a permission issue or modules not installed
|
||||
raise M365UserCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"Error connecting to PowerShell modules: {result if result else 'Unknown error'}",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -235,6 +297,9 @@ class M365PowerShell(PowerShellSession):
|
||||
"Microsoft Teams connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return False
|
||||
self.execute(
|
||||
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -242,6 +307,31 @@ class M365PowerShell(PowerShellSession):
|
||||
)
|
||||
return False
|
||||
|
||||
def test_teams_certificate_connection(self) -> bool:
|
||||
"""Test Microsoft Teams API connection using certificate and raise exception if it fails."""
|
||||
result = self.execute(
|
||||
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
|
||||
)
|
||||
if self.tenant_identity.identity_id not in result:
|
||||
logger.error(f"Microsoft Teams Certificate connection failed: {result}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def test_teams_user_connection(self) -> bool:
|
||||
"""Test Microsoft Teams API connection using user authentication and raise exception if it fails."""
|
||||
result = self.execute("Connect-MicrosoftTeams -Credential $credential")
|
||||
if self.tenant_identity.user not in result:
|
||||
logger.error(f"Microsoft Teams User Auth connection failed: {result}.")
|
||||
return False
|
||||
|
||||
connection = self.execute("Get-CsTeamsClientConfiguration")
|
||||
if not connection:
|
||||
logger.error(
|
||||
"Microsoft Teams User Auth connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def test_exchange_connection(self) -> bool:
|
||||
"""Test Exchange Online API connection and raise exception if it fails."""
|
||||
try:
|
||||
@@ -258,6 +348,9 @@ class M365PowerShell(PowerShellSession):
|
||||
"Exchange Online connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return False
|
||||
self.execute(
|
||||
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -265,11 +358,40 @@ class M365PowerShell(PowerShellSession):
|
||||
)
|
||||
return False
|
||||
|
||||
def test_exchange_certificate_connection(self) -> bool:
|
||||
"""Test Exchange Online API connection using certificate and raise exception if it fails."""
|
||||
result = self.execute(
|
||||
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
|
||||
)
|
||||
if "https://aka.ms/exov3-module" not in result:
|
||||
logger.error(f"Exchange Online Certificate connection failed: {result}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def test_exchange_user_connection(self) -> bool:
|
||||
"""Test Exchange Online API connection using user authentication and raise exception if it fails."""
|
||||
result = self.execute("Connect-ExchangeOnline -Credential $credential")
|
||||
if "https://aka.ms/exov3-module" not in result:
|
||||
logger.error(f"Exchange Online User Auth connection failed: {result}.")
|
||||
return False
|
||||
|
||||
connection = self.execute("Get-OrganizationConfig")
|
||||
if not connection:
|
||||
logger.error(
|
||||
"Exchange Online User Auth connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def connect_microsoft_teams(self) -> dict:
|
||||
"""
|
||||
Connect to Microsoft Teams Module PowerShell Module.
|
||||
|
||||
Establishes a connection to Microsoft Teams using the initialized credentials.
|
||||
Supports three authentication methods:
|
||||
1. User authentication (username/password)
|
||||
2. Application authentication (client_id/client_secret)
|
||||
3. Certificate authentication (certificate_content in base64/application_id)
|
||||
|
||||
Returns:
|
||||
dict: Connection status information in JSON format.
|
||||
@@ -277,26 +399,15 @@ class M365PowerShell(PowerShellSession):
|
||||
Note:
|
||||
This method requires the Microsoft Teams PowerShell module to be installed.
|
||||
"""
|
||||
# Certificate Auth
|
||||
if self.execute("Write-Output $certificate") != "":
|
||||
return self.test_teams_certificate_connection()
|
||||
# User Auth
|
||||
if self.execute("Write-Output $credential") != "":
|
||||
self.execute("Connect-MicrosoftTeams -Credential $credential")
|
||||
# Test connection with a simple call
|
||||
connection = self.execute("Get-CsTeamsClientConfiguration")
|
||||
if connection:
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
"Microsoft Teams User Auth connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return connection
|
||||
return self.test_teams_user_connection()
|
||||
# Application Auth
|
||||
else:
|
||||
connection = self.test_teams_connection()
|
||||
if connection:
|
||||
self.execute(
|
||||
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
|
||||
)
|
||||
return connection
|
||||
return self.test_teams_connection()
|
||||
|
||||
def get_teams_settings(self) -> dict:
|
||||
"""
|
||||
@@ -383,6 +494,10 @@ class M365PowerShell(PowerShellSession):
|
||||
Connect to Exchange Online PowerShell Module.
|
||||
|
||||
Establishes a connection to Exchange Online using the initialized credentials.
|
||||
Supports three authentication methods:
|
||||
1. User authentication (username/password)
|
||||
2. Application authentication (client_id/client_secret)
|
||||
3. Certificate authentication (certificate_content in base64/application_id)
|
||||
|
||||
Returns:
|
||||
dict: Connection status information in JSON format.
|
||||
@@ -390,25 +505,15 @@ class M365PowerShell(PowerShellSession):
|
||||
Note:
|
||||
This method requires the Exchange Online PowerShell module to be installed.
|
||||
"""
|
||||
# Certificate Auth
|
||||
if self.execute("Write-Output $certificate") != "":
|
||||
return self.test_exchange_certificate_connection()
|
||||
# User Auth
|
||||
if self.execute("Write-Output $credential") != "":
|
||||
self.execute("Connect-ExchangeOnline -Credential $credential")
|
||||
connection = self.execute("Get-OrganizationConfig")
|
||||
if connection:
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
"Exchange Online User Auth connection failed: Please check your permissions and try again."
|
||||
)
|
||||
return False
|
||||
return self.test_exchange_user_connection()
|
||||
# Application Auth
|
||||
else:
|
||||
connection = self.test_exchange_connection()
|
||||
if connection:
|
||||
self.execute(
|
||||
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
|
||||
)
|
||||
return connection
|
||||
return self.test_exchange_connection()
|
||||
|
||||
def get_audit_log_config(self) -> dict:
|
||||
"""
|
||||
@@ -877,6 +982,20 @@ class M365PowerShell(PowerShellSession):
|
||||
"""
|
||||
return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True)
|
||||
|
||||
def get_user_account_status(self) -> dict:
|
||||
"""
|
||||
Get User Account Status.
|
||||
|
||||
Retrieves the current user account status settings for Exchange Online.
|
||||
|
||||
Returns:
|
||||
dict: User account status settings in JSON format.
|
||||
"""
|
||||
return self.execute(
|
||||
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json",
|
||||
json_parse=True,
|
||||
)
|
||||
|
||||
|
||||
# This function is used to install the required M365 PowerShell modules in Docker containers
|
||||
def initialize_m365_powershell_modules():
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
from argparse import ArgumentTypeError
|
||||
from os import getenv
|
||||
@@ -6,6 +7,7 @@ from uuid import UUID
|
||||
|
||||
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
|
||||
from azure.identity import (
|
||||
CertificateCredential,
|
||||
ClientSecretCredential,
|
||||
CredentialUnavailableError,
|
||||
DefaultAzureCredential,
|
||||
@@ -41,6 +43,8 @@ from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365MissingEnvironmentCredentialsError,
|
||||
M365NoAuthenticationMethodError,
|
||||
M365NotTenantIdButClientIdAndClientSecretError,
|
||||
M365NotValidCertificateContentError,
|
||||
M365NotValidCertificatePathError,
|
||||
M365NotValidClientIdError,
|
||||
M365NotValidClientSecretError,
|
||||
M365NotValidTenantIdError,
|
||||
@@ -111,11 +115,14 @@ class M365Provider(Provider):
|
||||
env_auth: bool = False,
|
||||
az_cli_auth: bool = False,
|
||||
browser_auth: bool = False,
|
||||
certificate_auth: bool = False,
|
||||
tenant_id: str = None,
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
password: str = None,
|
||||
certificate_content: str = None,
|
||||
certificate_path: str = None,
|
||||
init_modules: bool = False,
|
||||
region: str = "M365Global",
|
||||
config_content: dict = None,
|
||||
@@ -158,11 +165,14 @@ class M365Provider(Provider):
|
||||
sp_env_auth,
|
||||
env_auth,
|
||||
browser_auth,
|
||||
certificate_auth,
|
||||
tenant_id,
|
||||
client_id,
|
||||
client_secret,
|
||||
user,
|
||||
password,
|
||||
certificate_content,
|
||||
certificate_path,
|
||||
)
|
||||
|
||||
logger.info("Checking if region is different than default one")
|
||||
@@ -170,13 +180,15 @@ class M365Provider(Provider):
|
||||
|
||||
# Get the dict from the static credentials
|
||||
m365_credentials = None
|
||||
if tenant_id and client_id and client_secret:
|
||||
if tenant_id and client_id:
|
||||
m365_credentials = self.validate_static_credentials(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user=user,
|
||||
password=password,
|
||||
certificate_content=certificate_content,
|
||||
certificate_path=certificate_path,
|
||||
)
|
||||
|
||||
# Set up the M365 session
|
||||
@@ -185,6 +197,8 @@ class M365Provider(Provider):
|
||||
sp_env_auth,
|
||||
env_auth,
|
||||
browser_auth,
|
||||
certificate_auth,
|
||||
certificate_path,
|
||||
tenant_id,
|
||||
m365_credentials,
|
||||
self._region_config,
|
||||
@@ -196,6 +210,7 @@ class M365Provider(Provider):
|
||||
env_auth,
|
||||
browser_auth,
|
||||
az_cli_auth,
|
||||
certificate_auth,
|
||||
self._session,
|
||||
)
|
||||
|
||||
@@ -203,6 +218,8 @@ class M365Provider(Provider):
|
||||
self._credentials = self.setup_powershell(
|
||||
env_auth=env_auth,
|
||||
sp_env_auth=sp_env_auth,
|
||||
certificate_auth=certificate_auth,
|
||||
certificate_path=certificate_path,
|
||||
m365_credentials=m365_credentials,
|
||||
identity=self.identity,
|
||||
init_modules=init_modules,
|
||||
@@ -279,11 +296,14 @@ class M365Provider(Provider):
|
||||
sp_env_auth: bool,
|
||||
env_auth: bool,
|
||||
browser_auth: bool,
|
||||
certificate_auth: bool,
|
||||
tenant_id: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
user: str,
|
||||
password: str,
|
||||
certificate_content: str,
|
||||
certificate_path: str,
|
||||
):
|
||||
"""
|
||||
Validates the authentication arguments for the M365 provider.
|
||||
@@ -293,11 +313,14 @@ class M365Provider(Provider):
|
||||
sp_env_auth (bool): Flag indicating whether application authentication with environment variables is enabled.
|
||||
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
|
||||
browser_auth (bool): Flag indicating whether browser authentication is enabled.
|
||||
certificate_auth (bool): Flag indicating whether certificate authentication is enabled.
|
||||
tenant_id (str): The M365 Tenant ID.
|
||||
client_id (str): The M365 Client ID.
|
||||
client_secret (str): The M365 Client Secret.
|
||||
user (str): The M365 User Account.
|
||||
password (str): The M365 User Password.
|
||||
certificate_content (str): The M365 Certificate Content.
|
||||
certificate_path (str): The path to the certificate file.
|
||||
|
||||
Raises:
|
||||
M365BrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found.
|
||||
@@ -314,28 +337,33 @@ class M365Provider(Provider):
|
||||
and not sp_env_auth
|
||||
and not browser_auth
|
||||
and not env_auth
|
||||
and not certificate_auth
|
||||
):
|
||||
raise M365NoAuthenticationMethodError(
|
||||
file=os.path.basename(__file__),
|
||||
message="M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth]",
|
||||
message="M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]",
|
||||
)
|
||||
elif browser_auth and not tenant_id:
|
||||
raise M365BrowserAuthNoTenantIDError(
|
||||
file=os.path.basename(__file__),
|
||||
message="M365 Tenant ID (--tenant-id) is required for browser authentication mode",
|
||||
)
|
||||
elif env_auth:
|
||||
if not user or not password or not tenant_id:
|
||||
raise M365MissingEnvironmentCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message="M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth",
|
||||
)
|
||||
else:
|
||||
if not tenant_id:
|
||||
raise M365NotTenantIdButClientIdAndClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="Tenant Id is required for M365 static credentials. Make sure you are using the correct credentials.",
|
||||
)
|
||||
if (
|
||||
not certificate_content
|
||||
and not certificate_path
|
||||
and not (user and password)
|
||||
and not client_secret
|
||||
):
|
||||
raise M365ConfigCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message="You must provide a valid set of credentials. Please check your credentials and try again.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def setup_region_config(region):
|
||||
@@ -378,6 +406,8 @@ class M365Provider(Provider):
|
||||
def setup_powershell(
|
||||
env_auth: bool = False,
|
||||
sp_env_auth: bool = False,
|
||||
certificate_auth: bool = False,
|
||||
certificate_path: str = None,
|
||||
m365_credentials: dict = {},
|
||||
identity: M365IdentityInfo = None,
|
||||
init_modules: bool = False,
|
||||
@@ -402,6 +432,7 @@ class M365Provider(Provider):
|
||||
client_id=m365_credentials.get("client_id", ""),
|
||||
client_secret=m365_credentials.get("client_secret", ""),
|
||||
tenant_id=m365_credentials.get("tenant_id", ""),
|
||||
certificate_content=m365_credentials.get("certificate_content", ""),
|
||||
tenant_domains=identity.tenant_domains,
|
||||
)
|
||||
elif env_auth:
|
||||
@@ -440,10 +471,28 @@ class M365Provider(Provider):
|
||||
tenant_domains=identity.tenant_domains,
|
||||
)
|
||||
|
||||
elif certificate_auth:
|
||||
client_id = getenv("AZURE_CLIENT_ID")
|
||||
tenant_id = getenv("AZURE_TENANT_ID")
|
||||
if certificate_path:
|
||||
with open(certificate_path, "rb") as cert_file:
|
||||
# Encode the certificate content to base64 since PowerShell expects a base64 string
|
||||
certificate_content = base64.b64encode(cert_file.read())
|
||||
else:
|
||||
certificate_content = getenv("M365_CERTIFICATE_CONTENT")
|
||||
credentials = M365Credentials(
|
||||
client_id=client_id,
|
||||
tenant_id=tenant_id,
|
||||
certificate_content=certificate_content,
|
||||
tenant_domains=identity.tenant_domains,
|
||||
)
|
||||
|
||||
if credentials:
|
||||
if identity and credentials.user:
|
||||
identity.user = credentials.user
|
||||
identity.identity_type = "Service Principal and User Credentials"
|
||||
if identity and credentials.certificate_content:
|
||||
identity.identity_type = "Service Principal with Certificate"
|
||||
test_session = M365PowerShell(credentials, identity)
|
||||
try:
|
||||
if init_modules:
|
||||
@@ -478,6 +527,10 @@ class M365Provider(Provider):
|
||||
report_lines.append(
|
||||
f"M365 User: {Fore.YELLOW}{self.credentials.user}{Style.RESET_ALL}"
|
||||
)
|
||||
elif self.credentials and self.credentials.certificate_content:
|
||||
report_lines.append(
|
||||
f"M365 Certificate Thumbprint: {Fore.YELLOW}{self._identity.certificate_thumbprint}{Style.RESET_ALL}"
|
||||
)
|
||||
report_title = (
|
||||
f"{Style.BRIGHT}Using the M365 credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
@@ -491,6 +544,8 @@ class M365Provider(Provider):
|
||||
sp_env_auth: bool,
|
||||
env_auth: bool,
|
||||
browser_auth: bool,
|
||||
certificate_auth: bool,
|
||||
certificate_path: str,
|
||||
tenant_id: str,
|
||||
m365_credentials: dict,
|
||||
region_config: M365RegionConfig,
|
||||
@@ -510,6 +565,8 @@ class M365Provider(Provider):
|
||||
- client_secret: The M365 client secret
|
||||
- user: The M365 user email
|
||||
- password: The M365 user password
|
||||
- certificate_content: The M365 certificate content
|
||||
- certificate_path: The path to the certificate file.
|
||||
- provider_id: The M365 provider ID (in this case the Tenant ID).
|
||||
region_config (M365RegionConfig): The region configuration object.
|
||||
|
||||
@@ -530,16 +587,45 @@ class M365Provider(Provider):
|
||||
f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}"
|
||||
)
|
||||
raise environment_credentials_error
|
||||
elif certificate_auth:
|
||||
try:
|
||||
M365Provider.check_certificate_creds_env_vars(
|
||||
check_certificate_content=not certificate_path
|
||||
)
|
||||
except M365EnvironmentVariableError as environment_variable_error:
|
||||
logger.critical(
|
||||
f"{environment_variable_error.__class__.__name__}[{environment_variable_error.__traceback__.tb_lineno}] -- {environment_variable_error}"
|
||||
)
|
||||
raise environment_variable_error
|
||||
try:
|
||||
if m365_credentials:
|
||||
try:
|
||||
credentials = ClientSecretCredential(
|
||||
tenant_id=m365_credentials["tenant_id"],
|
||||
client_id=m365_credentials["client_id"],
|
||||
client_secret=m365_credentials["client_secret"],
|
||||
user=m365_credentials["user"],
|
||||
password=m365_credentials["password"],
|
||||
)
|
||||
if m365_credentials["certificate_content"]:
|
||||
credentials = CertificateCredential(
|
||||
tenant_id=m365_credentials["tenant_id"],
|
||||
client_id=m365_credentials["client_id"],
|
||||
certificate_data=base64.b64decode(
|
||||
m365_credentials["certificate_content"]
|
||||
),
|
||||
)
|
||||
elif m365_credentials["certificate_path"]:
|
||||
with open(
|
||||
m365_credentials["certificate_path"], "rb"
|
||||
) as cert_file:
|
||||
certificate_data = cert_file.read()
|
||||
credentials = CertificateCredential(
|
||||
tenant_id=m365_credentials["tenant_id"],
|
||||
client_id=m365_credentials["client_id"],
|
||||
certificate_data=certificate_data,
|
||||
)
|
||||
else:
|
||||
credentials = ClientSecretCredential(
|
||||
tenant_id=m365_credentials["tenant_id"],
|
||||
client_id=m365_credentials["client_id"],
|
||||
client_secret=m365_credentials["client_secret"],
|
||||
user=m365_credentials["user"],
|
||||
password=m365_credentials["password"],
|
||||
)
|
||||
return credentials
|
||||
except ClientAuthenticationError as error:
|
||||
logger.error(
|
||||
@@ -562,13 +648,34 @@ class M365Provider(Provider):
|
||||
raise M365ConfigCredentialsError(
|
||||
file=os.path.basename(__file__), original_exception=error
|
||||
)
|
||||
elif certificate_auth:
|
||||
try:
|
||||
if certificate_path:
|
||||
with open(certificate_path, "rb") as cert_file:
|
||||
certificate_data = cert_file.read()
|
||||
else:
|
||||
certificate_data = base64.b64decode(
|
||||
getenv("M365_CERTIFICATE_CONTENT")
|
||||
)
|
||||
credentials = CertificateCredential(
|
||||
tenant_id=getenv("AZURE_TENANT_ID"),
|
||||
client_id=getenv("AZURE_CLIENT_ID"),
|
||||
certificate_data=certificate_data,
|
||||
)
|
||||
except ClientAuthenticationError as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
raise M365ClientAuthenticationError(
|
||||
file=os.path.basename(__file__), original_exception=error
|
||||
)
|
||||
else:
|
||||
# Since the authentication method to be used will come as True, we have to negate it since
|
||||
# DefaultAzureCredential sets just one authentication method, excluding the others
|
||||
try:
|
||||
credentials = DefaultAzureCredential(
|
||||
exclude_environment_credential=not (
|
||||
sp_env_auth or env_auth
|
||||
sp_env_auth or env_auth or certificate_auth
|
||||
),
|
||||
exclude_cli_credential=not az_cli_auth,
|
||||
# M365 Auth using Managed Identity is not supported
|
||||
@@ -633,6 +740,7 @@ class M365Provider(Provider):
|
||||
sp_env_auth: bool = False,
|
||||
env_auth: bool = False,
|
||||
browser_auth: bool = False,
|
||||
certificate_auth: bool = False,
|
||||
tenant_id: str = None,
|
||||
region: str = "M365Global",
|
||||
raise_on_exception: bool = True,
|
||||
@@ -640,6 +748,8 @@ class M365Provider(Provider):
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
password: str = None,
|
||||
certificate_content: str = None,
|
||||
certificate_path: str = None,
|
||||
provider_id: str = None,
|
||||
) -> Connection:
|
||||
"""Test connection to M365 tenant and PowerShell modules.
|
||||
@@ -652,6 +762,7 @@ class M365Provider(Provider):
|
||||
sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables.
|
||||
env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables.
|
||||
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
|
||||
certificate_auth (bool): Flag indicating whether to use certificate authentication.
|
||||
tenant_id (str): The M365 Active Directory tenant ID.
|
||||
region (str): The M365 region.
|
||||
raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails.
|
||||
@@ -688,11 +799,14 @@ class M365Provider(Provider):
|
||||
sp_env_auth,
|
||||
env_auth,
|
||||
browser_auth,
|
||||
certificate_auth,
|
||||
tenant_id,
|
||||
client_id,
|
||||
client_secret,
|
||||
user,
|
||||
password,
|
||||
certificate_content,
|
||||
certificate_path,
|
||||
)
|
||||
region_config = M365Provider.setup_region_config(region)
|
||||
|
||||
@@ -704,8 +818,6 @@ class M365Provider(Provider):
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user=None,
|
||||
password=None,
|
||||
)
|
||||
else:
|
||||
m365_credentials = M365Provider.validate_static_credentials(
|
||||
@@ -715,6 +827,12 @@ class M365Provider(Provider):
|
||||
user=user,
|
||||
password=password,
|
||||
)
|
||||
elif tenant_id and client_id and certificate_content:
|
||||
m365_credentials = M365Provider.validate_static_credentials(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
certificate_content=certificate_content,
|
||||
)
|
||||
|
||||
# Set up the M365 session
|
||||
session = M365Provider.setup_session(
|
||||
@@ -722,6 +840,8 @@ class M365Provider(Provider):
|
||||
sp_env_auth,
|
||||
env_auth,
|
||||
browser_auth,
|
||||
certificate_auth,
|
||||
certificate_path,
|
||||
tenant_id,
|
||||
m365_credentials,
|
||||
region_config,
|
||||
@@ -737,6 +857,7 @@ class M365Provider(Provider):
|
||||
env_auth,
|
||||
browser_auth,
|
||||
az_cli_auth,
|
||||
certificate_auth,
|
||||
session,
|
||||
)
|
||||
|
||||
@@ -758,6 +879,8 @@ class M365Provider(Provider):
|
||||
M365Provider.setup_powershell(
|
||||
env_auth,
|
||||
sp_env_auth,
|
||||
certificate_auth,
|
||||
certificate_path,
|
||||
m365_credentials,
|
||||
identity,
|
||||
)
|
||||
@@ -889,12 +1012,44 @@ class M365Provider(Provider):
|
||||
message=f"Missing environment variable {env_var} required to authenticate.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_certificate_creds_env_vars(check_certificate_content: bool):
|
||||
"""
|
||||
Checks the presence of required environment variables for service principal authentication against Azure.
|
||||
|
||||
This method checks for the presence of the following environment variables:
|
||||
- AZURE_CLIENT_ID: Azure client ID
|
||||
- AZURE_TENANT_ID: Azure tenant ID
|
||||
- M365_CERTIFICATE_CONTENT: Azure certificate content
|
||||
|
||||
If any of the environment variables is missing, it logs a critical error and exits the program.
|
||||
"""
|
||||
logger.info(
|
||||
"M365 provider: checking service principal environment variables ..."
|
||||
)
|
||||
env_vars = [
|
||||
"AZURE_CLIENT_ID",
|
||||
"AZURE_TENANT_ID",
|
||||
]
|
||||
if check_certificate_content:
|
||||
env_vars.append("M365_CERTIFICATE_CONTENT")
|
||||
for env_var in env_vars:
|
||||
if not getenv(env_var):
|
||||
logger.critical(
|
||||
f"M365 provider: Missing environment variable {env_var} needed to authenticate against M365."
|
||||
)
|
||||
raise M365EnvironmentVariableError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"Missing environment variable {env_var} required to authenticate.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def setup_identity(
|
||||
sp_env_auth,
|
||||
env_auth,
|
||||
browser_auth,
|
||||
az_cli_auth,
|
||||
certificate_auth,
|
||||
session,
|
||||
):
|
||||
"""
|
||||
@@ -968,6 +1123,16 @@ class M365Provider(Provider):
|
||||
or session.credentials[0]._credential.client_id
|
||||
or "Unknown user id (Missing AAD permissions)"
|
||||
)
|
||||
elif certificate_auth:
|
||||
identity.identity_type = "Service Principal with Certificate"
|
||||
identity.identity_id = (
|
||||
getenv("AZURE_CLIENT_ID")
|
||||
or session.credentials[0]._credential.client_id
|
||||
or "Unknown user id (Missing AAD permissions)"
|
||||
)
|
||||
identity.certificate_thumbprint = session._client_credential.get(
|
||||
"thumbprint", "Unknown certificate thumbprint"
|
||||
)
|
||||
elif browser_auth or az_cli_auth:
|
||||
identity.identity_type = "User"
|
||||
try:
|
||||
@@ -987,8 +1152,14 @@ class M365Provider(Provider):
|
||||
)
|
||||
else:
|
||||
# Static Credentials
|
||||
identity.identity_type = "Service Principal"
|
||||
identity.identity_id = session._client_id
|
||||
if isinstance(session, CertificateCredential):
|
||||
identity.identity_type = "Service Principal with Certificate"
|
||||
identity.certificate_thumbprint = session._client_credential.get(
|
||||
"thumbprint", "Unknown certificate thumbprint"
|
||||
)
|
||||
else:
|
||||
identity.identity_type = "Service Principal"
|
||||
|
||||
# Retrieve tenant id from the client
|
||||
client = GraphServiceClient(credentials=session)
|
||||
@@ -1005,6 +1176,8 @@ class M365Provider(Provider):
|
||||
client_secret: str = None,
|
||||
user: str = None,
|
||||
password: str = None,
|
||||
certificate_content: str = None,
|
||||
certificate_path: str = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Validates the static credentials for the M365 provider.
|
||||
@@ -1015,6 +1188,8 @@ class M365Provider(Provider):
|
||||
client_secret (str): The M365 client secret.
|
||||
user (str): The M365 user email.
|
||||
password (str): The M365 user password.
|
||||
certificate_content (str): The M365 Certificate Content.
|
||||
certificate_path (str): The path to the certificate file.
|
||||
|
||||
Raises:
|
||||
M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid.
|
||||
@@ -1045,21 +1220,47 @@ class M365Provider(Provider):
|
||||
message="The provided Client ID is not valid.",
|
||||
)
|
||||
|
||||
# Validate the Client Secret
|
||||
if not client_secret:
|
||||
if not certificate_content and not certificate_path and not client_secret:
|
||||
raise M365NotValidClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Client Secret is not valid.",
|
||||
message="You must provide a client secret, certificate content or certificate path. Please check your credentials and try again.",
|
||||
)
|
||||
|
||||
if certificate_content:
|
||||
try:
|
||||
# Validate that certificate content can be properly decoded from base64
|
||||
base64.b64decode(certificate_content)
|
||||
except Exception as e:
|
||||
raise M365NotValidCertificateContentError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"The provided certificate content is not valid base64 encoded data: {str(e)}",
|
||||
)
|
||||
if certificate_path:
|
||||
try:
|
||||
with open(certificate_path, "rb") as cert_file:
|
||||
certificate_content = cert_file.read()
|
||||
except Exception as e:
|
||||
raise M365NotValidCertificatePathError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"The provided certificate path is not valid: {str(e)}",
|
||||
)
|
||||
|
||||
try:
|
||||
M365Provider.verify_client(tenant_id, client_id, client_secret)
|
||||
M365Provider.verify_client(
|
||||
tenant_id,
|
||||
client_id,
|
||||
client_secret,
|
||||
certificate_content,
|
||||
certificate_path,
|
||||
)
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"user": user,
|
||||
"password": password,
|
||||
"certificate_content": certificate_content,
|
||||
"certificate_path": certificate_path,
|
||||
}
|
||||
except M365NotValidTenantIdError as tenant_id_error:
|
||||
logger.error(
|
||||
@@ -1087,7 +1288,9 @@ class M365Provider(Provider):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def verify_client(tenant_id, client_id, client_secret) -> None:
|
||||
def verify_client(
|
||||
tenant_id, client_id, client_secret, certificate_content, certificate_path
|
||||
) -> None:
|
||||
"""
|
||||
Verifies the M365 client credentials using the specified tenant ID, client ID, and client secret.
|
||||
|
||||
@@ -1095,52 +1298,106 @@ class M365Provider(Provider):
|
||||
tenant_id (str): The M365 Active Directory tenant ID.
|
||||
client_id (str): The M365 client ID.
|
||||
client_secret (str): The M365 client secret.
|
||||
certificate_content (str): The M365 certificate content.
|
||||
certificate_path (str): The path to the certificate file.
|
||||
|
||||
Raises:
|
||||
M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid.
|
||||
M365NotValidClientIdError: If the provided M365 Client ID is not valid.
|
||||
M365NotValidClientSecretError: If the provided M365 Client Secret is not valid.
|
||||
M365NotValidCertificateContentError: If the provided M365 Certificate Content is not valid.
|
||||
M365NotValidCertificatePathError: If the provided M365 Certificate Path is not valid.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
authority = f"https://login.microsoftonline.com/{tenant_id}"
|
||||
try:
|
||||
# Create a ConfidentialClientApplication instance
|
||||
app = ConfidentialClientApplication(
|
||||
client_id=client_id,
|
||||
client_credential=client_secret,
|
||||
authority=authority,
|
||||
)
|
||||
if client_secret:
|
||||
# Create a ConfidentialClientApplication instance
|
||||
app = ConfidentialClientApplication(
|
||||
client_id=client_id,
|
||||
client_credential=client_secret,
|
||||
authority=authority,
|
||||
)
|
||||
# Attempt to acquire a token
|
||||
result = app.acquire_token_for_client(
|
||||
scopes=["https://graph.microsoft.com/.default"]
|
||||
)
|
||||
|
||||
# Attempt to acquire a token
|
||||
result = app.acquire_token_for_client(
|
||||
scopes=["https://graph.microsoft.com/.default"]
|
||||
)
|
||||
# Check if token acquisition was successful
|
||||
if "access_token" not in result:
|
||||
# Handle specific errors based on the MSAL response
|
||||
error_description = result.get("error_description", "")
|
||||
if f"Tenant '{tenant_id}'" in error_description:
|
||||
raise M365NotValidTenantIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Tenant ID is not valid for the specified Client ID and Client Secret.",
|
||||
)
|
||||
if (
|
||||
f"Application with identifier '{client_id}'"
|
||||
in error_description
|
||||
):
|
||||
raise M365NotValidClientIdError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Client ID is not valid for the specified Tenant ID and Client Secret.",
|
||||
)
|
||||
if "Invalid client secret provided" in error_description:
|
||||
raise M365NotValidClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Client Secret is not valid for the specified Tenant ID and Client ID.",
|
||||
)
|
||||
elif certificate_content:
|
||||
credential = CertificateCredential(
|
||||
client_id=client_id,
|
||||
tenant_id=tenant_id,
|
||||
certificate_data=base64.b64decode(certificate_content),
|
||||
)
|
||||
client = GraphServiceClient(credentials=credential)
|
||||
|
||||
# Check if token acquisition was successful
|
||||
if "access_token" not in result:
|
||||
# Handle specific errors based on the MSAL response
|
||||
error_description = result.get("error_description", "")
|
||||
if f"Tenant '{tenant_id}'" in error_description:
|
||||
raise M365NotValidTenantIdError(
|
||||
# Verify that the certificate is valid
|
||||
async def verify_certificate():
|
||||
result = await client.domains.get()
|
||||
return result.value
|
||||
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
verify_certificate()
|
||||
)
|
||||
if not result:
|
||||
raise M365NotValidCertificateContentError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Tenant ID is not valid for the specified Client ID and Client Secret.",
|
||||
message="The provided certificate content is not valid.",
|
||||
)
|
||||
if f"Application with identifier '{client_id}'" in error_description:
|
||||
raise M365NotValidClientIdError(
|
||||
elif certificate_path:
|
||||
with open(certificate_path, "rb") as cert_file:
|
||||
certificate_content = cert_file.read()
|
||||
credential = CertificateCredential(
|
||||
client_id=client_id,
|
||||
tenant_id=tenant_id,
|
||||
certificate_data=certificate_content,
|
||||
)
|
||||
client = GraphServiceClient(credentials=credential)
|
||||
|
||||
# Verify that the certificate is valid
|
||||
async def verify_certificate():
|
||||
result = await client.domains.get()
|
||||
return result.value
|
||||
|
||||
result = asyncio.get_event_loop().run_until_complete(
|
||||
verify_certificate()
|
||||
)
|
||||
if not result:
|
||||
raise M365NotValidCertificatePathError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Client ID is not valid for the specified Tenant ID and Client Secret.",
|
||||
)
|
||||
if "Invalid client secret provided" in error_description:
|
||||
raise M365NotValidClientSecretError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Client Secret is not valid for the specified Tenant ID and Client ID.",
|
||||
message="The provided certificate is not valid.",
|
||||
)
|
||||
|
||||
except (
|
||||
M365NotValidTenantIdError,
|
||||
M365NotValidClientIdError,
|
||||
M365NotValidClientSecretError,
|
||||
M365NotValidCertificateContentError,
|
||||
M365NotValidCertificatePathError,
|
||||
) as m365_error:
|
||||
# M365 specific errors already raised
|
||||
raise RuntimeError(f"{m365_error}")
|
||||
|
||||
@@ -11,6 +11,7 @@ class M365IdentityInfo(BaseModel):
|
||||
identity_type: str = ""
|
||||
tenant_id: str = ""
|
||||
tenant_domain: str = "Unknown tenant domain (missing Entra permissions)"
|
||||
certificate_thumbprint: str = ""
|
||||
tenant_domains: list[str] = []
|
||||
location: str = ""
|
||||
user: str = None
|
||||
@@ -28,9 +29,10 @@ class M365Credentials(BaseModel):
|
||||
passwd: Optional[str] = None
|
||||
encrypted_passwd: Optional[str] = None
|
||||
client_id: str = ""
|
||||
client_secret: str = ""
|
||||
client_secret: Optional[str] = None
|
||||
tenant_id: str = ""
|
||||
tenant_domains: list[str] = []
|
||||
certificate_content: Optional[str] = None
|
||||
|
||||
|
||||
class M365OutputOptions(ProviderOutputOptions):
|
||||
|
||||
@@ -14,7 +14,10 @@ from prowler.providers.m365.m365_provider import M365Provider
|
||||
class Entra(M365Service):
|
||||
def __init__(self, provider: M365Provider):
|
||||
super().__init__(provider)
|
||||
|
||||
if self.powershell:
|
||||
self.powershell.connect_exchange_online()
|
||||
self.user_accounts_status = self.powershell.get_user_account_status()
|
||||
self.powershell.close()
|
||||
|
||||
loop = get_event_loop()
|
||||
@@ -36,6 +39,7 @@ class Entra(M365Service):
|
||||
self.groups = attributes[3]
|
||||
self.organizations = attributes[4]
|
||||
self.users = attributes[5]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
async def _get_authorization_policy(self):
|
||||
logger.info("Entra - Getting authorization policy...")
|
||||
@@ -405,6 +409,9 @@ class Entra(M365Service):
|
||||
if registration_details.get(user.id, None) is not None
|
||||
else False
|
||||
),
|
||||
account_enabled=not self.user_accounts_status.get(user.id, {}).get(
|
||||
"AccountDisabled", False
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -585,6 +592,7 @@ class User(BaseModel):
|
||||
on_premises_sync_enabled: bool
|
||||
directory_roles_ids: List[str] = []
|
||||
is_mfa_capable: bool = False
|
||||
account_enabled: bool = True
|
||||
|
||||
|
||||
class InvitationsFrom(Enum):
|
||||
|
||||
@@ -26,20 +26,21 @@ class entra_users_mfa_capable(Check):
|
||||
findings = []
|
||||
|
||||
for user in entra_client.users.values():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=user,
|
||||
resource_name=user.name,
|
||||
resource_id=user.id,
|
||||
)
|
||||
if user.account_enabled:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=user,
|
||||
resource_name=user.name,
|
||||
resource_id=user.id,
|
||||
)
|
||||
|
||||
if not user.is_mfa_capable:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"User {user.name} is not MFA capable."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"User {user.name} is MFA capable."
|
||||
if not user.is_mfa_capable:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"User {user.name} is not MFA capable."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"User {user.name} is MFA capable."
|
||||
|
||||
findings.append(report)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@@ -70,7 +70,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">3.9.1,<3.13"
|
||||
version = "5.10.2"
|
||||
version = "5.11.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -585,7 +585,9 @@ class TestFinding:
|
||||
provider = MagicMock()
|
||||
provider.type = "github"
|
||||
# GitHub App identity only has app_id, not account_name/account_id
|
||||
provider.identity = GithubAppIdentityInfo(app_id=APP_ID)
|
||||
provider.identity = GithubAppIdentityInfo(
|
||||
app_id=APP_ID, app_name="test-app", installations=["test-org"]
|
||||
)
|
||||
provider.auth_method = "GitHub App Token"
|
||||
|
||||
# Mock check result
|
||||
@@ -632,8 +634,8 @@ class TestFinding:
|
||||
|
||||
# Assert account information for GitHub App - this is the core of the bug fix
|
||||
# Before the fix, this would fail because GithubAppIdentityInfo doesn't have account_name
|
||||
# After the fix, it should use app_id with "app-" prefix
|
||||
assert finding_output.account_name == f"app-{APP_ID}"
|
||||
# After the fix, it should use app_name
|
||||
assert finding_output.account_name == "test-app"
|
||||
assert finding_output.account_uid == APP_ID
|
||||
assert finding_output.account_email is None
|
||||
assert finding_output.account_organization_uid is None
|
||||
|
||||
@@ -4,6 +4,7 @@ from io import StringIO
|
||||
from mock import patch
|
||||
|
||||
from prowler.config.config import prowler_version, timestamp
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.html.html import HTML
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
@@ -231,11 +232,12 @@ github_personal_access_token_html_assessment_summary = """
|
||||
<div class="card-header">
|
||||
GitHub Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
<li class="list-group-item">
|
||||
<b>GitHub account:</b> account-name
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,8 +246,8 @@ github_personal_access_token_html_assessment_summary = """
|
||||
<div class="card-header">
|
||||
GitHub Credentials
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<ul class="list-group list-group-flush">
|
||||
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> Personal Access Token
|
||||
</li>
|
||||
@@ -259,10 +261,12 @@ github_app_html_assessment_summary = """
|
||||
<div class="card-header">
|
||||
GitHub Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>GitHub account:</b> app-app-id
|
||||
<b>GitHub App Name:</b> test-app
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Installations:</b> test-org
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -272,11 +276,13 @@ github_app_html_assessment_summary = """
|
||||
<div class="card-header">
|
||||
GitHub Credentials
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>GitHub authentication method:</b> GitHub App Token
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>GitHub App ID:</b> app-id
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
@@ -664,7 +670,12 @@ class TestHTML:
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert summary == github_personal_access_token_html_assessment_summary
|
||||
# Check for expected content in the summary
|
||||
assert "GitHub Assessment Summary" in summary
|
||||
assert "GitHub Credentials" in summary
|
||||
assert "<b>GitHub account:</b> account-name" in summary
|
||||
assert "<b>GitHub authentication method:</b> Personal Access Token" in summary
|
||||
# Note: account_email is None in the default fixture, so it shouldn't appear
|
||||
|
||||
def test_github_app_get_assessment_summary(self):
|
||||
"""Test GitHub HTML assessment summary generation with GitHub App authentication."""
|
||||
@@ -673,9 +684,18 @@ class TestHTML:
|
||||
|
||||
provider = set_mocked_github_provider(
|
||||
auth_method="GitHub App Token",
|
||||
identity=GithubAppIdentityInfo(app_id=APP_ID),
|
||||
identity=GithubAppIdentityInfo(
|
||||
app_id=APP_ID, app_name="test-app", installations=["test-org"]
|
||||
),
|
||||
)
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
logger.error(summary)
|
||||
|
||||
assert summary == github_app_html_assessment_summary
|
||||
# Check for expected content in the summary
|
||||
assert "GitHub Assessment Summary" in summary
|
||||
assert "GitHub Credentials" in summary
|
||||
assert "<b>GitHub App Name:</b> test-app" in summary
|
||||
assert "<b>Installations:</b> test-org" in summary
|
||||
assert "<b>GitHub authentication method:</b> GitHub App Token" in summary
|
||||
assert f"<b>GitHub App ID:</b> {APP_ID}" in summary
|
||||
|
||||
@@ -13,5 +13,4 @@ class Test_Scan_Filters:
|
||||
assert not is_resource_filtered(
|
||||
"arn:aws:iam::123456789012:user/test1", audit_resources
|
||||
)
|
||||
assert is_resource_filtered("test_bucket", audit_resources)
|
||||
assert is_resource_filtered("arn:aws:s3:::test_bucket", audit_resources)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.eks.eks_service import EKSCluster
|
||||
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1
|
||||
|
||||
cluster_name = "cluster_test"
|
||||
cluster_arn = (
|
||||
f"arn:aws:eks:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:cluster/{cluster_name}"
|
||||
)
|
||||
|
||||
|
||||
class Test_eks_cluster_deletion_protection_enabled:
|
||||
def test_no_clusters(self):
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.clusters = []
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
):
|
||||
from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import (
|
||||
eks_cluster_deletion_protection_enabled,
|
||||
)
|
||||
|
||||
check = eks_cluster_deletion_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_cluster_deletion_protection_disabled(self):
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.clusters = []
|
||||
eks_client.clusters.append(
|
||||
EKSCluster(
|
||||
name=cluster_name,
|
||||
arn=cluster_arn,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
deletion_protection=False,
|
||||
)
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
):
|
||||
from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import (
|
||||
eks_cluster_deletion_protection_enabled,
|
||||
)
|
||||
|
||||
check = eks_cluster_deletion_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"EKS cluster {cluster_name} has deletion protection disabled."
|
||||
)
|
||||
assert result[0].resource_id == cluster_name
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_cluster_deletion_protection_enabled(self):
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.clusters = []
|
||||
eks_client.clusters.append(
|
||||
EKSCluster(
|
||||
name=cluster_name,
|
||||
arn=cluster_arn,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
deletion_protection=True,
|
||||
)
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
):
|
||||
from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import (
|
||||
eks_cluster_deletion_protection_enabled,
|
||||
)
|
||||
|
||||
check = eks_cluster_deletion_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"EKS cluster {cluster_name} has deletion protection enabled."
|
||||
)
|
||||
assert result[0].resource_id == cluster_name
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_cluster_deletion_protection_none(self):
|
||||
eks_client = mock.MagicMock
|
||||
eks_client.clusters = []
|
||||
eks_client.clusters.append(
|
||||
EKSCluster(
|
||||
name=cluster_name,
|
||||
arn=cluster_arn,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
deletion_protection=None,
|
||||
)
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.eks.eks_service.EKS",
|
||||
eks_client,
|
||||
):
|
||||
from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import (
|
||||
eks_cluster_deletion_protection_enabled,
|
||||
)
|
||||
|
||||
check = eks_cluster_deletion_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"EKS cluster {cluster_name} has deletion protection enabled."
|
||||
)
|
||||
assert result[0].resource_id == cluster_name
|
||||
assert result[0].resource_arn == cluster_arn
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
@@ -362,17 +362,16 @@ class Test_iam_inline_policy_allows_privilege_escalation:
|
||||
check = iam_inline_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == f"test_role/{policy_name}"
|
||||
assert result[0].resource_arn == role_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ",
|
||||
f"Inline policy {policy_name} attached to role {role_name} does not allow privilege escalation",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_inline_policy_allows_privilege_escalation_two_combinations(
|
||||
@@ -511,17 +510,16 @@ class Test_iam_inline_policy_allows_privilege_escalation:
|
||||
check = iam_inline_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == f"test_role/{policy_name}"
|
||||
assert result[0].resource_arn == role_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ",
|
||||
f"Inline policy {policy_name} attached to role {role_name} does not allow privilege escalation",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_inline_policy_allows_privilege_escalation_policies_combination(
|
||||
@@ -1219,3 +1217,75 @@ class Test_iam_inline_policy_allows_privilege_escalation:
|
||||
f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions:",
|
||||
finding.status_extended,
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_inline_role_policy_allows_privilege_escalation_agentcore(self):
|
||||
"""Test detection of AWS Bedrock AgentCore privilege escalation in inline role policy."""
|
||||
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
|
||||
# Create IAM Role
|
||||
role_name = "agentcore_test_role"
|
||||
role_arn = iam_client.create_role(
|
||||
RoleName=role_name,
|
||||
AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY),
|
||||
)["Role"]["Arn"]
|
||||
|
||||
# Put Role Policy with AgentCore privilege escalation permissions
|
||||
policy_name = "agentcore_policy"
|
||||
policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:PassRole",
|
||||
"bedrock-agentcore:CreateCodeInterpreter",
|
||||
"bedrock-agentcore:InvokeCodeInterpreter",
|
||||
],
|
||||
"Resource": "*",
|
||||
}
|
||||
],
|
||||
}
|
||||
_ = iam_client.put_role_policy(
|
||||
RoleName=role_name,
|
||||
PolicyName=policy_name,
|
||||
PolicyDocument=dumps(policy_document),
|
||||
)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_inline_policy_allows_privilege_escalation.iam_inline_policy_allows_privilege_escalation.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.iam.iam_inline_policy_allows_privilege_escalation.iam_inline_policy_allows_privilege_escalation import (
|
||||
iam_inline_policy_allows_privilege_escalation,
|
||||
)
|
||||
|
||||
check = iam_inline_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == f"{role_name}/{policy_name}"
|
||||
assert result[0].resource_arn == role_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
assert search(
|
||||
"bedrock-agentcore:CreateCodeInterpreter", result[0].status_extended
|
||||
)
|
||||
assert search(
|
||||
"bedrock-agentcore:InvokeCodeInterpreter", result[0].status_extended
|
||||
)
|
||||
|
||||
@@ -322,17 +322,16 @@ class Test_iam_policy_allows_privilege_escalation:
|
||||
check = iam_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == policy_name
|
||||
assert result[0].resource_arn == policy_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ",
|
||||
f"Custom Policy {policy_arn} does not allow privilege escalation",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_policy_allows_privilege_escalation_iam_PassRole_using_wildcard(
|
||||
@@ -375,17 +374,16 @@ class Test_iam_policy_allows_privilege_escalation:
|
||||
check = iam_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == policy_name
|
||||
assert result[0].resource_arn == policy_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ",
|
||||
f"Custom Policy {policy_arn} does not allow privilege escalation",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_policy_allows_privilege_escalation_two_combinations(
|
||||
@@ -508,17 +506,16 @@ class Test_iam_policy_allows_privilege_escalation:
|
||||
check = iam_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == policy_name
|
||||
assert result[0].resource_arn == policy_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ",
|
||||
f"Custom Policy {policy_arn} does not allow privilege escalation",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_policy_allows_privilege_escalation_policies_combination(
|
||||
@@ -915,6 +912,71 @@ class Test_iam_policy_allows_privilege_escalation:
|
||||
]:
|
||||
assert search(permission, finding.status_extended)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_policy_allows_privilege_escalation_agentcore_passrole_create_invoke(
|
||||
self,
|
||||
):
|
||||
"""Test detection of AWS Bedrock AgentCore privilege escalation pattern."""
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
|
||||
policy_name = "agentcore_privilege_escalation_policy"
|
||||
policy_document = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:PassRole",
|
||||
"bedrock-agentcore:CreateCodeInterpreter",
|
||||
"bedrock-agentcore:InvokeCodeInterpreter",
|
||||
],
|
||||
"Resource": "*",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
policy_arn = iam_client.create_policy(
|
||||
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
|
||||
)["Policy"]["Arn"]
|
||||
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import (
|
||||
iam_policy_allows_privilege_escalation,
|
||||
)
|
||||
|
||||
check = iam_policy_allows_privilege_escalation()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == policy_name
|
||||
assert result[0].resource_arn == policy_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
assert search(
|
||||
f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert search("iam:PassRole", result[0].status_extended)
|
||||
assert search(
|
||||
"bedrock-agentcore:CreateCodeInterpreter", result[0].status_extended
|
||||
)
|
||||
assert search(
|
||||
"bedrock-agentcore:InvokeCodeInterpreter", result[0].status_extended
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_iam_policy_allows_privilege_escalation_iam_put(
|
||||
self,
|
||||
|
||||
@@ -52,7 +52,6 @@ class Test_PrivilegeEscalation:
|
||||
assert "iam:Put*" in result
|
||||
assert "iam:AddUserToGroup" in result
|
||||
assert "iam:AttachRolePolicy" in result
|
||||
assert "iam:PassRole" in result
|
||||
assert "iam:CreateLoginProfile" in result
|
||||
assert "iam:CreateAccessKey" in result
|
||||
assert "iam:AttachGroupPolicy" in result
|
||||
@@ -78,9 +77,9 @@ class Test_PrivilegeEscalation:
|
||||
],
|
||||
}
|
||||
result = check_privilege_escalation(policy)
|
||||
assert "iam:PassRole" in result
|
||||
assert result == ""
|
||||
|
||||
def test_check_privilege_escalation_priv_escalation_iam_PassRole_using_wildcard(
|
||||
def test_check_privilege_escalation_priv_escalation_iam_wildcard(
|
||||
self,
|
||||
):
|
||||
policy = {
|
||||
@@ -88,13 +87,16 @@ class Test_PrivilegeEscalation:
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["iam:*Role"], # Should expand to include PassRole
|
||||
"Action": [
|
||||
"iam:*"
|
||||
], # Should expand to include multiple IAM actions
|
||||
"Resource": ["*"],
|
||||
}
|
||||
],
|
||||
}
|
||||
result = check_privilege_escalation(policy)
|
||||
assert "iam:PassRole" in result
|
||||
# iam:* should expand to include PutUserPolicy and other privilege escalation actions
|
||||
assert "iam:PutUserPolicy" in result
|
||||
|
||||
def test_check_privilege_escalation_priv_escalation_not_action(
|
||||
self,
|
||||
@@ -117,7 +119,6 @@ class Test_PrivilegeEscalation:
|
||||
assert "'iam:PutGroupPolicy'" not in result
|
||||
assert "iam:AddUserToGroup" in result
|
||||
assert "iam:AttachRolePolicy" in result
|
||||
assert "iam:PassRole" in result
|
||||
assert "iam:CreateLoginProfile" in result
|
||||
assert "iam:CreateAccessKey" in result
|
||||
assert "iam:AttachGroupPolicy" in result
|
||||
|
||||
@@ -85,6 +85,7 @@ class TestAzureProvider:
|
||||
"python_latest_version": "3.12",
|
||||
"java_latest_version": "17",
|
||||
"recommended_minimal_tls_versions": ["1.2", "1.3"],
|
||||
"vm_backup_min_daily_retention_days": 7,
|
||||
"desired_vm_sku_sizes": [
|
||||
"Standard_A8_v2",
|
||||
"Standard_DS3_v2",
|
||||
|
||||
@@ -6,6 +6,7 @@ from prowler.providers.azure.services.defender.defender_service import (
|
||||
AutoProvisioningSetting,
|
||||
Defender,
|
||||
IoTSecuritySolution,
|
||||
JITPolicy,
|
||||
Pricing,
|
||||
SecurityContactConfiguration,
|
||||
Setting,
|
||||
@@ -103,6 +104,19 @@ def mock_defender_get_iot_security_solutions(_):
|
||||
}
|
||||
|
||||
|
||||
def mock_defender_get_jit_policies(_):
|
||||
return {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"policy-1": JITPolicy(
|
||||
id="policy-1",
|
||||
name="JITPolicy1",
|
||||
location="eastus",
|
||||
vm_ids=["vm-1", "vm-2"],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.defender.defender_service.Defender._get_pricings",
|
||||
new=mock_defender_get_pricings,
|
||||
@@ -127,6 +141,10 @@ def mock_defender_get_iot_security_solutions(_):
|
||||
"prowler.providers.azure.services.defender.defender_service.Defender._get_iot_security_solutions",
|
||||
new=mock_defender_get_iot_security_solutions,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.defender.defender_service.Defender._get_jit_policies",
|
||||
new=mock_defender_get_jit_policies,
|
||||
)
|
||||
class Test_Defender_Service:
|
||||
def test_get_client(self):
|
||||
defender = Defender(set_mocked_azure_provider())
|
||||
@@ -255,3 +273,13 @@ class Test_Defender_Service:
|
||||
].status
|
||||
== "Enabled"
|
||||
)
|
||||
|
||||
def test_get_jit_policies(self):
|
||||
defender = Defender(set_mocked_azure_provider())
|
||||
assert AZURE_SUBSCRIPTION_ID in defender.jit_policies
|
||||
assert "policy-1" in defender.jit_policies[AZURE_SUBSCRIPTION_ID]
|
||||
policy1 = defender.jit_policies[AZURE_SUBSCRIPTION_ID]["policy-1"]
|
||||
assert policy1.id == "policy-1"
|
||||
assert policy1.name == "JITPolicy1"
|
||||
assert policy1.location == "eastus"
|
||||
assert set(policy1.vm_ids) == {"vm-1", "vm-2"}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.azure.services.defender.defender_service import JITPolicy
|
||||
from prowler.providers.azure.services.vm.vm_service import VirtualMachine
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_vm_jit_access_enabled:
|
||||
def test_no_subscriptions(self):
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {}
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.jit_policies = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert result == []
|
||||
|
||||
def test_no_vms(self):
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
defender_client = mock.MagicMock()
|
||||
defender_client.jit_policies = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert result == []
|
||||
|
||||
def test_vm_with_jit_enabled(self):
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "TestVM"
|
||||
vm_location = "eastus"
|
||||
vm = VirtualMachine(
|
||||
resource_id=vm_id,
|
||||
resource_name=vm_name,
|
||||
location=vm_location,
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=None,
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
|
||||
defender_client = mock.MagicMock()
|
||||
jit_policy = JITPolicy(
|
||||
id="policy1",
|
||||
name="JITPolicy1",
|
||||
location="eastus",
|
||||
vm_ids={vm_id},
|
||||
)
|
||||
defender_client.jit_policies = {
|
||||
AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_id == vm_id
|
||||
assert result[0].resource_name == vm_name
|
||||
assert "has JIT (Just-in-Time) access enabled" in result[0].status_extended
|
||||
|
||||
def test_vm_with_jit_disabled(self):
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "TestVM"
|
||||
vm_location = "eastus"
|
||||
vm = VirtualMachine(
|
||||
resource_id=vm_id,
|
||||
resource_name=vm_name,
|
||||
location=vm_location,
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=None,
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
|
||||
defender_client = mock.MagicMock()
|
||||
# JIT policy does not include this VM
|
||||
jit_policy = JITPolicy(
|
||||
id="policy1",
|
||||
name="JITPolicy1",
|
||||
location="eastus",
|
||||
vm_ids={"some-other-id"},
|
||||
)
|
||||
defender_client.jit_policies = {
|
||||
AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_id == vm_id
|
||||
assert result[0].resource_name == vm_name
|
||||
assert (
|
||||
"does not have JIT (Just-in-Time) access enabled"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_vm_id_case_insensitivity(self):
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "TestVM"
|
||||
vm_location = "eastus"
|
||||
upper_vm_id = vm_id.upper()
|
||||
vm = VirtualMachine(
|
||||
resource_id=upper_vm_id,
|
||||
resource_name=vm_name,
|
||||
location=vm_location,
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=None,
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {upper_vm_id: vm}}
|
||||
defender_client = mock.MagicMock()
|
||||
jit_policy = JITPolicy(
|
||||
id="policy1",
|
||||
name="JITPolicy1",
|
||||
location="eastus",
|
||||
vm_ids={vm_id.lower()},
|
||||
)
|
||||
defender_client.jit_policies = {
|
||||
AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == upper_vm_id
|
||||
assert "has JIT (Just-in-Time) access enabled" in result[0].status_extended
|
||||
|
||||
def test_multiple_vms_and_policies(self):
|
||||
vm_id_1 = str(uuid4())
|
||||
vm_id_2 = str(uuid4())
|
||||
vm1 = VirtualMachine(
|
||||
resource_id=vm_id_1,
|
||||
resource_name="VM1",
|
||||
location="eastus",
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=None,
|
||||
)
|
||||
vm2 = VirtualMachine(
|
||||
resource_id=vm_id_2,
|
||||
resource_name="VM2",
|
||||
location="eastus",
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=None,
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {
|
||||
AZURE_SUBSCRIPTION_ID: {vm_id_1: vm1, vm_id_2: vm2}
|
||||
}
|
||||
defender_client = mock.MagicMock()
|
||||
jit_policy_1 = JITPolicy(
|
||||
id="policy1",
|
||||
name="JITPolicy1",
|
||||
location="eastus",
|
||||
vm_ids={vm_id_1},
|
||||
)
|
||||
jit_policy_2 = JITPolicy(
|
||||
id="policy2",
|
||||
name="JITPolicy2",
|
||||
location="eastus",
|
||||
vm_ids=set(),
|
||||
)
|
||||
defender_client.jit_policies = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
jit_policy_1.id: jit_policy_1,
|
||||
jit_policy_2.id: jit_policy_2,
|
||||
}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import (
|
||||
vm_jit_access_enabled,
|
||||
)
|
||||
|
||||
check = vm_jit_access_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
for r in result:
|
||||
if r.resource_id == vm_id_1:
|
||||
assert r.status == "PASS"
|
||||
elif r.resource_id == vm_id_2:
|
||||
assert r.status == "FAIL"
|
||||
@@ -0,0 +1,323 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_vm_sufficient_daily_backup_retention_period:
|
||||
def test_no_subscriptions(self):
|
||||
vm_client = mock.MagicMock()
|
||||
recovery_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {}
|
||||
recovery_client.vaults = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
|
||||
new=recovery_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
|
||||
vm_sufficient_daily_backup_retention_period,
|
||||
)
|
||||
|
||||
check = vm_sufficient_daily_backup_retention_period()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_no_vms(self):
|
||||
vm_client = mock.MagicMock()
|
||||
recovery_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
|
||||
new=recovery_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
|
||||
vm_sufficient_daily_backup_retention_period,
|
||||
)
|
||||
|
||||
check = vm_sufficient_daily_backup_retention_period()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_vm_with_sufficient_retention(self):
|
||||
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
|
||||
|
||||
from prowler.providers.azure.services.recovery.recovery_service import (
|
||||
BackupItem,
|
||||
BackupPolicy,
|
||||
BackupVault,
|
||||
)
|
||||
from prowler.providers.azure.services.vm.vm_service import (
|
||||
ManagedDiskParameters,
|
||||
OSDisk,
|
||||
StorageProfile,
|
||||
VirtualMachine,
|
||||
)
|
||||
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "VMTest"
|
||||
vault_id = str(uuid4())
|
||||
policy_id = str(uuid4())
|
||||
retention_days = 14
|
||||
min_retention_days = 7
|
||||
|
||||
vm = VirtualMachine(
|
||||
resource_id=vm_id,
|
||||
resource_name=vm_name,
|
||||
location="eastus",
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=StorageProfile(
|
||||
os_disk=OSDisk(
|
||||
name="os_disk_name",
|
||||
operating_system_type="Linux",
|
||||
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
|
||||
),
|
||||
data_disks=[],
|
||||
),
|
||||
)
|
||||
backup_item = BackupItem(
|
||||
id=str(uuid4()),
|
||||
name=f"someprefix;{vm_name}",
|
||||
workload_type=DataSourceType.VM,
|
||||
backup_policy_id=policy_id,
|
||||
)
|
||||
backup_policy = BackupPolicy(
|
||||
id=policy_id,
|
||||
name="policy1",
|
||||
retention_days=retention_days,
|
||||
)
|
||||
vault = BackupVault(
|
||||
id=vault_id,
|
||||
name="vault1",
|
||||
location="eastus",
|
||||
backup_protected_items={backup_item.id: backup_item},
|
||||
backup_policies={policy_id: backup_policy},
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
recovery_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
|
||||
recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
|
||||
vm_client.audit_config = {
|
||||
"vm_backup_min_daily_retention_days": min_retention_days
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(
|
||||
audit_config=vm_client.audit_config
|
||||
),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
|
||||
new=recovery_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
|
||||
vm_sufficient_daily_backup_retention_period,
|
||||
)
|
||||
|
||||
check = vm_sufficient_daily_backup_retention_period()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == vm_name
|
||||
assert result[0].resource_id == vm_id
|
||||
assert (
|
||||
f"has a daily backup retention period of {retention_days} days"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_vm_with_insufficient_retention(self):
|
||||
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
|
||||
|
||||
from prowler.providers.azure.services.recovery.recovery_service import (
|
||||
BackupItem,
|
||||
BackupPolicy,
|
||||
BackupVault,
|
||||
)
|
||||
from prowler.providers.azure.services.vm.vm_service import (
|
||||
ManagedDiskParameters,
|
||||
OSDisk,
|
||||
StorageProfile,
|
||||
VirtualMachine,
|
||||
)
|
||||
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "VMTest"
|
||||
vault_id = str(uuid4())
|
||||
policy_id = str(uuid4())
|
||||
retention_days = 3
|
||||
min_retention_days = 7
|
||||
|
||||
vm = VirtualMachine(
|
||||
resource_id=vm_id,
|
||||
resource_name=vm_name,
|
||||
location="eastus",
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=StorageProfile(
|
||||
os_disk=OSDisk(
|
||||
name="os_disk_name",
|
||||
operating_system_type="Linux",
|
||||
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
|
||||
),
|
||||
data_disks=[],
|
||||
),
|
||||
)
|
||||
backup_item = BackupItem(
|
||||
id=str(uuid4()),
|
||||
name=f"someprefix;{vm_name}",
|
||||
workload_type=DataSourceType.VM,
|
||||
backup_policy_id=policy_id,
|
||||
)
|
||||
backup_policy = BackupPolicy(
|
||||
id=policy_id,
|
||||
name="policy1",
|
||||
retention_days=retention_days,
|
||||
)
|
||||
vault = BackupVault(
|
||||
id=vault_id,
|
||||
name="vault1",
|
||||
location="eastus",
|
||||
backup_protected_items={backup_item.id: backup_item},
|
||||
backup_policies={policy_id: backup_policy},
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
recovery_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
|
||||
recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
|
||||
vm_client.audit_config = {
|
||||
"vm_backup_min_daily_retention_days": min_retention_days
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(
|
||||
audit_config=vm_client.audit_config
|
||||
),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
|
||||
new=recovery_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
|
||||
vm_sufficient_daily_backup_retention_period,
|
||||
)
|
||||
|
||||
check = vm_sufficient_daily_backup_retention_period()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == vm_name
|
||||
assert result[0].resource_id == vm_id
|
||||
assert (
|
||||
f"has insufficient daily backup retention period of {retention_days} days"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_vm_with_no_backup_policy(self):
|
||||
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
|
||||
|
||||
from prowler.providers.azure.services.recovery.recovery_service import (
|
||||
BackupItem,
|
||||
BackupVault,
|
||||
)
|
||||
from prowler.providers.azure.services.vm.vm_service import (
|
||||
ManagedDiskParameters,
|
||||
OSDisk,
|
||||
StorageProfile,
|
||||
VirtualMachine,
|
||||
)
|
||||
|
||||
vm_id = str(uuid4())
|
||||
vm_name = "VMTest"
|
||||
vault_id = str(uuid4())
|
||||
|
||||
vm = VirtualMachine(
|
||||
resource_id=vm_id,
|
||||
resource_name=vm_name,
|
||||
location="eastus",
|
||||
security_profile=None,
|
||||
extensions=[],
|
||||
storage_profile=StorageProfile(
|
||||
os_disk=OSDisk(
|
||||
name="os_disk_name",
|
||||
operating_system_type="Linux",
|
||||
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
|
||||
),
|
||||
data_disks=[],
|
||||
),
|
||||
)
|
||||
backup_item = BackupItem(
|
||||
id=str(uuid4()),
|
||||
name=f"someprefix;{vm_name}",
|
||||
workload_type=DataSourceType.VM,
|
||||
backup_policy_id=None,
|
||||
)
|
||||
vault = BackupVault(
|
||||
id=vault_id,
|
||||
name="vault1",
|
||||
location="eastus",
|
||||
backup_protected_items={backup_item.id: backup_item},
|
||||
backup_policies={},
|
||||
)
|
||||
vm_client = mock.MagicMock()
|
||||
recovery_client = mock.MagicMock()
|
||||
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
|
||||
recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
|
||||
new=vm_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
|
||||
new=recovery_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
|
||||
vm_sufficient_daily_backup_retention_period,
|
||||
)
|
||||
|
||||
check = vm_sufficient_daily_backup_retention_period()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
@@ -12,6 +12,7 @@ ACCOUNT_URL = "/user"
|
||||
PAT_TOKEN = "github-token"
|
||||
OAUTH_TOKEN = "oauth-token"
|
||||
APP_ID = "app-id"
|
||||
APP_NAME = "app-name"
|
||||
APP_KEY = "app-key"
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from tests.providers.github.github_fixtures import (
|
||||
ACCOUNT_URL,
|
||||
APP_ID,
|
||||
APP_KEY,
|
||||
APP_NAME,
|
||||
OAUTH_TOKEN,
|
||||
PAT_TOKEN,
|
||||
)
|
||||
@@ -135,6 +136,8 @@ class TestGitHubProvider:
|
||||
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
|
||||
return_value=GithubAppIdentityInfo(
|
||||
app_id=APP_ID,
|
||||
app_name=APP_NAME,
|
||||
installations=["test-org"],
|
||||
),
|
||||
),
|
||||
):
|
||||
@@ -147,7 +150,9 @@ class TestGitHubProvider:
|
||||
|
||||
assert provider._type == "github"
|
||||
assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY)
|
||||
assert provider.identity == GithubAppIdentityInfo(app_id=APP_ID)
|
||||
assert provider.identity == GithubAppIdentityInfo(
|
||||
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
|
||||
)
|
||||
assert provider._audit_config == {
|
||||
"inactive_not_archived_days_threshold": 180,
|
||||
}
|
||||
@@ -206,7 +211,9 @@ class TestGitHubProvider:
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
|
||||
return_value=GithubAppIdentityInfo(app_id=APP_ID),
|
||||
return_value=GithubAppIdentityInfo(
|
||||
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
|
||||
),
|
||||
),
|
||||
):
|
||||
connection = GithubProvider.test_connection(
|
||||
|
||||