Compare commits

..

38 Commits

Author SHA1 Message Date
Daniel Barranquero 6bc1eafd66 Merge branch 'PRWLR-5093-design-the-fixer-class' into update-fixers-docs 2025-07-15 12:34:36 +02:00
Daniel Barranquero e45e4ae0fe feat(docs): add snapshots 2025-07-15 12:32:58 +02:00
Daniel Barranquero ef4718d16c Merge branch 'master' into update-fixers-docs 2025-07-15 12:20:55 +02:00
Daniel Barranquero 933ba4c3be Merge branch 'master' into PRWLR-5093-design-the-fixer-class 2025-07-15 12:17:55 +02:00
Daniel Barranquero 877471783e feat(docs): add new version of fixer docs 2025-07-14 13:57:45 +02:00
Daniel Barranquero 55e9695915 Merge branch 'master' into update-fixers-docs 2025-07-14 09:41:53 +02:00
Daniel Barranquero 82ab20deec feat(compute): add tests for gcp fixer 2025-06-24 10:01:07 +02:00
Daniel Barranquero d7e3b1c760 feat(gcp): working version of gcp fixer 2025-06-23 13:14:25 +02:00
Daniel Barranquero 166e07939d feat(gcp): add first version of gcp tests 2025-06-23 12:33:51 +02:00
Daniel Barranquero c5cf1c4bfb merge branch 'master' into PRWLR-5093-design-the-fixer-class 2025-06-23 10:20:06 +02:00
Daniel Barranquero 09b33d05a3 fix vulture 2025-06-11 12:52:36 +02:00
Daniel Barranquero 6a7cfd175c chore(tests): improve tests 2025-06-11 12:47:42 +02:00
Daniel Barranquero 82543c0d63 fix(azure): change azure fixer tests 2025-06-11 09:45:32 +02:00
Daniel Barranquero 7360395263 feat(tests): add tests for new fixers 2025-06-10 19:15:16 +02:00
Daniel Barranquero 4ae790ee73 fix: tests with function apps 2025-06-10 11:17:54 +02:00
Daniel Barranquero 7a2d3db082 chore(app): fix app service tests 2025-06-09 16:35:11 +02:00
Daniel Barranquero 40934d34b2 fix: flake8 2025-06-09 13:38:16 +02:00
Daniel Barranquero 5c93372210 chore(tests): add tests for azure and m365 fixers 2025-06-09 13:32:47 +02:00
Daniel Barranquero ffcc516f00 chore(kms): modify fixer test 2025-06-09 11:30:44 +02:00
Daniel Barranquero 9d4094e19e fix: remove unnecessary changes 2025-06-04 13:21:25 +02:00
Daniel Barranquero 00e491415f chore(app): new version of the fixer 2025-06-04 12:51:39 +02:00
Daniel Barranquero e17cbed4b3 Merge branch 'PRWLR-7353-fix-app-function-ftps-deployment-disabled-check' into PRWLR-5093-design-the-fixer-class 2025-06-04 12:34:01 +02:00
Daniel Barranquero d1e41f16ef fix: solve comments 2025-06-04 12:32:32 +02:00
Daniel Barranquero a17c3f94fc chore(azure): add permissions to azure fixer info 2025-06-04 10:48:55 +02:00
Daniel Barranquero 70f8232747 Merge branch 'PRWLR-7353-fix-app-function-ftps-deployment-disabled-check' into PRWLR-5093-design-the-fixer-class 2025-06-04 09:47:06 +02:00
Daniel Barranquero 31189f0d11 chore(app): mantain none by default 2025-06-04 09:43:17 +02:00
Daniel Barranquero 5aaf6e4858 feat(app): add changelog 2025-06-04 09:29:14 +02:00
Daniel Barranquero e05cc4cfab fix(app): change api call for app function ftps check 2025-06-03 17:57:52 +02:00
Daniel Barranquero 18a6f29593 feat(gcp): add first version of gcp fixers 2025-06-03 17:40:05 +02:00
Daniel Barranquero fc826da50c chore(azure): add changes to azure fixers 2025-06-03 17:38:09 +02:00
Daniel Barranquero b30ee077da merge branch 'master' into PRWLR-5093-design-the-fixer-class 2025-06-02 10:38:00 +02:00
Daniel Barranquero efdd967763 feat(fixers): add first version of azure fixers 2025-05-23 11:07:38 +02:00
Daniel Barranquero ee146cd43e feat(m365): add first fixer for m365 2025-05-21 14:00:34 +02:00
Daniel Barranquero f40aea757e feat(fixers): add first version of M365 fixers 2025-05-21 10:24:59 +02:00
Daniel Barranquero 7db24f8cb7 Merge branch 'master' into PRWLR-5093-design-the-fixer-class 2025-05-20 13:14:59 +02:00
Daniel Barranquero f78e5c9e33 feat(fixers): change classes structure 2025-05-20 09:41:14 +02:00
Daniel Barranquero d91bbe1ef4 feat(fixer): add fixing and modify errors from the v1 2025-05-15 16:40:09 +02:00
Daniel Barranquero c0d211492e feat(fixer): add poc for Fixer class 2025-05-15 13:57:29 +02:00
228 changed files with 4105 additions and 6849 deletions
@@ -6,7 +6,6 @@ on:
- "master"
paths:
- "api/**"
- "prowler/**"
- ".github/workflows/api-build-lint-push-containers.yml"
# Uncomment the code below to test this action on PRs
@@ -1,215 +0,0 @@
name: Prowler Release Preparation
run-name: Prowler Release Preparation for ${{ inputs.prowler_version }}
on:
workflow_dispatch:
inputs:
prowler_version:
description: 'Prowler version to release (e.g., 5.9.0)'
required: true
type: string
env:
PROWLER_VERSION: ${{ github.event.inputs.prowler_version }}
jobs:
prepare-release:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Parse version and determine branch
run: |
# Validate version format (reusing pattern from sdk-bump-version.yml)
if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR_VERSION=${BASH_REMATCH[1]}
MINOR_VERSION=${BASH_REMATCH[2]}
PATCH_VERSION=${BASH_REMATCH[3]}
# Export version components to environment
echo "MAJOR_VERSION=${MAJOR_VERSION}" >> "${GITHUB_ENV}"
echo "MINOR_VERSION=${MINOR_VERSION}" >> "${GITHUB_ENV}"
echo "PATCH_VERSION=${PATCH_VERSION}" >> "${GITHUB_ENV}"
# Determine branch name (format: v5.9)
BRANCH_NAME="v${MAJOR_VERSION}.${MINOR_VERSION}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_ENV}"
# Calculate UI version (1.X.X format - matches Prowler minor version)
UI_VERSION="1.${MINOR_VERSION}.${PATCH_VERSION}"
echo "UI_VERSION=${UI_VERSION}" >> "${GITHUB_ENV}"
# Calculate API version (1.X.X format - one minor version ahead)
API_MINOR_VERSION=$((MINOR_VERSION + 1))
API_VERSION="1.${API_MINOR_VERSION}.${PATCH_VERSION}"
echo "API_VERSION=${API_VERSION}" >> "${GITHUB_ENV}"
echo "Prowler version: $PROWLER_VERSION"
echo "Branch name: $BRANCH_NAME"
echo "UI version: $UI_VERSION"
echo "API version: $API_VERSION"
echo "Is minor release: $([ $PATCH_VERSION -eq 0 ] && echo 'true' || echo 'false')"
else
echo "Invalid version syntax: '$PROWLER_VERSION' (must be N.N.N)" >&2
exit 1
fi
- name: Checkout existing branch for patch release
if: ${{ env.PATCH_VERSION != '0' }}
run: |
echo "Patch release detected, checking out existing branch $BRANCH_NAME..."
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
echo "Branch $BRANCH_NAME exists locally, checking out..."
git checkout "$BRANCH_NAME"
elif 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 patch release $PROWLER_VERSION"
exit 1
fi
- name: Verify version in pyproject.toml
run: |
CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_VERSION" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Version mismatch in pyproject.toml (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_VERSION')"
exit 1
fi
echo "✓ pyproject.toml version: $CURRENT_VERSION"
- name: Verify version in prowler/config/config.py
run: |
CURRENT_VERSION=$(grep '^prowler_version = ' prowler/config/config.py | sed -E 's/prowler_version = "([^"]+)"/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_VERSION" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Version mismatch in prowler/config/config.py (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_VERSION')"
exit 1
fi
echo "✓ prowler/config/config.py version: $CURRENT_VERSION"
- name: Verify version in api/pyproject.toml
run: |
CURRENT_API_VERSION=$(grep '^version = ' api/pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_API_VERSION" != "$API_VERSION_TRIMMED" ]; then
echo "ERROR: API version mismatch in api/pyproject.toml (expected: '$API_VERSION_TRIMMED', found: '$CURRENT_API_VERSION')"
exit 1
fi
echo "✓ api/pyproject.toml version: $CURRENT_API_VERSION"
- name: Verify prowler dependency in api/pyproject.toml
run: |
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_PROWLER_REF" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Prowler dependency mismatch in api/pyproject.toml (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_PROWLER_REF')"
exit 1
fi
echo "✓ api/pyproject.toml prowler dependency: $CURRENT_PROWLER_REF"
- name: Verify version in api/src/backend/api/v1/views.py
run: |
CURRENT_API_VERSION=$(grep 'spectacular_settings.VERSION = ' api/src/backend/api/v1/views.py | sed -E 's/.*spectacular_settings.VERSION = "([^"]+)".*/\1/' | tr -d '[:space:]')
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_API_VERSION" != "$API_VERSION_TRIMMED" ]; then
echo "ERROR: API version mismatch in views.py (expected: '$API_VERSION_TRIMMED', found: '$CURRENT_API_VERSION')"
exit 1
fi
echo "✓ api/src/backend/api/v1/views.py version: $CURRENT_API_VERSION"
- name: Create 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"
exit 1
fi
git checkout -b "$BRANCH_NAME"
- name: Extract changelog entries
run: |
set -e
# Function to extract changelog for a specific version
extract_changelog() {
local file="$1"
local version="$2"
local output_file="$3"
if [ ! -f "$file" ]; then
echo "Warning: $file not found, skipping..."
touch "$output_file"
return
fi
# Extract changelog section for this version
awk -v version="$version" '
/^## \[v?'"$version"'\]/ { found=1; next }
found && /^## \[v?[0-9]+\.[0-9]+\.[0-9]+\]/ { found=0 }
found && !/^## \[v?'"$version"'\]/ { print }
' "$file" > "$output_file"
# Remove --- separators
sed -i '/^---$/d' "$output_file"
# Remove trailing empty lines
sed -i '/^$/d' "$output_file"
}
# Extract changelogs
echo "Extracting changelog entries..."
extract_changelog "prowler/CHANGELOG.md" "$PROWLER_VERSION" "prowler_changelog.md"
extract_changelog "api/CHANGELOG.md" "$API_VERSION" "api_changelog.md"
extract_changelog "ui/CHANGELOG.md" "$UI_VERSION" "ui_changelog.md"
# Combine changelogs in order: UI, API, SDK
> combined_changelog.md
if [ -s "ui_changelog.md" ]; then
echo "## UI" >> combined_changelog.md
echo "" >> combined_changelog.md
cat ui_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
if [ -s "api_changelog.md" ]; then
echo "## API" >> combined_changelog.md
echo "" >> combined_changelog.md
cat api_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
if [ -s "prowler_changelog.md" ]; then
echo "## SDK" >> combined_changelog.md
echo "" >> combined_changelog.md
cat prowler_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
echo "Combined changelog preview:"
cat combined_changelog.md
- name: Create draft release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
tag_name: ${{ env.PROWLER_VERSION }}
name: Prowler ${{ env.PROWLER_VERSION }}
body_path: combined_changelog.md
draft: true
target_commitish: ${{ env.PATCH_VERSION == '0' && 'master' || env.BRANCH_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Clean up temporary files
run: |
rm -f prowler_changelog.md api_changelog.md ui_changelog.md combined_changelog.md
+1 -8
View File
@@ -102,15 +102,8 @@ jobs:
run: |
poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
- name: Dockerfile - Check if Dockerfile has changed
id: dockerfile-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
with:
files: |
Dockerfile
- name: Hadolint
if: steps.dockerfile-changed-files.outputs.any_changed == 'true'
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
/tmp/hadolint Dockerfile --ignore=DL3013
-8
View File
@@ -136,14 +136,6 @@ If your workstation's architecture is incompatible, you can resolve this by:
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
### Common Issues with Docker Pull Installation
> [!Note]
If you want to use AWS role assumption (e.g., with the "Connect assuming IAM Role" option), you may need to mount your local `.aws` directory into the container as a volume (e.g., `- "${HOME}/.aws:/home/prowler/.aws:ro"`). There are several ways to configure credentials for Docker containers. See the [Troubleshooting](./docs/troubleshooting.md) section for more details and examples.
You can find more information in the [Troubleshooting](./docs/troubleshooting.md) section.
### From GitHub
**Requirements**
+2 -6
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler API** are documented in this file.
## [v1.10.0] (Prowler v5.9.0)
## [v1.10.0] (Prowler UNRELEASED)
### Added
- SSO with SAML support [(#8175)](https://github.com/prowler-cloud/prowler/pull/8175)
@@ -12,16 +12,12 @@ All notable changes to the **Prowler API** are documented in this file.
- `/processors` endpoints to post-process findings. Currently, only the Mutelist processor is supported to allow to mute findings.
- Optimized the underlying queries for resources endpoints [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- Optimized include parameters for resources view [(#8229)](https://github.com/prowler-cloud/prowler/pull/8229)
- Optimized overview background tasks [(#8300)](https://github.com/prowler-cloud/prowler/pull/8300)
### Fixed
- Search filter for findings and resources [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- RBAC is now applied to `GET /overviews/providers` [(#8277)](https://github.com/prowler-cloud/prowler/pull/8277)
### Changed
- `POST /schedules/daily` returns a `409 CONFLICT` if already created [(#8258)](https://github.com/prowler-cloud/prowler/pull/8258)
### Security
- Enhanced password validation to enforce 12+ character passwords with special characters, uppercase, lowercase, and numbers [(#8225)](https://github.com/prowler-cloud/prowler/pull/8225)
---
+247 -1516
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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.9",
"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)",
-23
View File
@@ -175,29 +175,6 @@ def create_objects_in_batches(
model.objects.bulk_create(chunk, batch_size)
def update_objects_in_batches(
tenant_id: str, model, objects: list, fields: list, batch_size: int = 500
):
"""
Bulk-update model instances in repeated, per-tenant RLS transactions.
All chunks execute in their own transaction, so no single transaction
grows too large.
Args:
tenant_id (str): UUID string of the tenant under which to set RLS.
model: Django model class whose `.objects.bulk_update()` will be called.
objects (list): List of model instances (saved) to bulk-update.
fields (list): List of field names to update.
batch_size (int): Maximum number of objects per bulk_update call.
"""
total = len(objects)
for start in range(0, total, batch_size):
chunk = objects[start : start + batch_size]
with rls_transaction(value=tenant_id, parameter=POSTGRES_TENANT_VAR):
model.objects.bulk_update(chunk, fields, batch_size)
# Postgres Enums
-18
View File
@@ -78,21 +78,3 @@ def custom_exception_handler(exc, context):
message_item["message"] for message_item in exc.detail["messages"]
]
return exception_handler(exc, context)
class ConflictException(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = "A conflict occurred. The resource already exists."
default_code = "conflict"
def __init__(self, detail=None, code=None, pointer=None):
error_detail = {
"detail": detail or self.default_detail,
"status": self.status_code,
"code": self.default_code,
}
if pointer:
error_detail["source"] = {"pointer": pointer}
super().__init__(detail=[error_detail])
@@ -13,7 +13,6 @@ from api.db_utils import (
enum_to_choices,
generate_random_token,
one_week_from_now,
update_objects_in_batches,
)
from api.models import Provider
@@ -228,88 +227,3 @@ class TestCreateObjectsInBatches:
qs = Provider.objects.filter(tenant=tenant)
assert qs.count() == total
@pytest.mark.django_db
class TestUpdateObjectsInBatches:
@pytest.fixture
def tenant(self, tenants_fixture):
return tenants_fixture[0]
def make_provider_instances(self, tenant, count):
"""
Return a list of `count` unsaved Provider instances for the given tenant.
"""
base_uid = 2000
return [
Provider(
tenant=tenant,
uid=str(base_uid + i),
provider=Provider.ProviderChoices.AWS,
)
for i in range(count)
]
def test_exact_multiple_of_batch(self, tenant):
total = 6
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
# Fetch them back, mutate the `uid` field, then update in batches
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_non_multiple_of_batch(self, tenant):
total = 7
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_batch_size_default(self, tenant):
default_size = settings.DJANGO_DELETION_BATCH_SIZE
total = default_size + 2
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
# Update without specifying batch_size (uses default)
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
-85
View File
@@ -1,7 +1,6 @@
from unittest.mock import ANY, Mock, patch
import pytest
from conftest import TODAY
from django.urls import reverse
from rest_framework import status
@@ -410,87 +409,3 @@ class TestLimitedVisibility:
assert (
response.json()["data"]["relationships"]["providers"]["meta"]["count"] == 1
)
def test_overviews_providers(
self,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(reverse("overview-providers"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) > 0
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(reverse("overview-providers"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
@pytest.mark.parametrize(
"endpoint_name",
[
"findings",
"findings_severity",
],
)
def test_overviews_findings(
self,
endpoint_name,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(
reverse(f"overview-{endpoint_name}")
)
assert response.status_code == status.HTTP_200_OK
values = response.json()["data"]["attributes"].values()
assert any(value > 0 for value in values)
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(
reverse(f"overview-{endpoint_name}")
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]["attributes"].values()
assert all(value == 0 for value in data)
def test_overviews_services(
self,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(
reverse("overview-services"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) > 0
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(
reverse("overview-services"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
+9 -31
View File
@@ -14,13 +14,7 @@ import jwt
import pytest
from allauth.socialaccount.models import SocialAccount, SocialApp
from botocore.exceptions import ClientError, NoCredentialsError
from conftest import (
API_JSON_CONTENT_TYPE,
TEST_PASSWORD,
TEST_USER,
TODAY,
today_after_n_days,
)
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
from django.conf import settings
from django.http import JsonResponse
from django.test import RequestFactory
@@ -53,6 +47,14 @@ from api.models import (
from api.rls import Tenant
from api.v1.views import ComplianceOverviewViewSet, TenantFinishACSView
TODAY = str(datetime.today().date())
def today_after_n_days(n_days: int) -> str:
return datetime.strftime(
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
)
class TestViewSet:
def test_security_headers(self, client):
@@ -5494,30 +5496,6 @@ class TestScheduleViewSet:
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@patch("api.v1.views.Task.objects.get")
def test_schedule_daily_already_scheduled(
self,
mock_task_get,
authenticated_client,
providers_fixture,
tasks_fixture,
):
provider, *_ = providers_fixture
prowler_task = tasks_fixture[0]
mock_task_get.return_value = prowler_task
json_payload = {
"provider_id": str(provider.id),
}
response = authenticated_client.post(
reverse("schedule-daily"), data=json_payload, format="json"
)
assert response.status_code == status.HTTP_202_ACCEPTED
response = authenticated_client.post(
reverse("schedule-daily"), data=json_payload, format="json"
)
assert response.status_code == status.HTTP_409_CONFLICT
@pytest.mark.django_db
class TestIntegrationViewSet:
+24 -41
View File
@@ -94,6 +94,7 @@ from api.filters import (
UserFilter,
)
from api.models import (
ComplianceOverview,
ComplianceRequirementOverview,
Finding,
Integration,
@@ -3468,7 +3469,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
)
@method_decorator(CACHE_DECORATOR, name="list")
class OverviewViewSet(BaseRLSViewSet):
queryset = ScanSummary.objects.all()
queryset = ComplianceOverview.objects.all()
http_method_names = ["get"]
ordering = ["-inserted_at"]
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
@@ -3479,10 +3480,19 @@ class OverviewViewSet(BaseRLSViewSet):
role = get_role(self.request.user)
providers = get_providers(role)
if not role.unlimited_visibility:
self.allowed_providers = providers
def _get_filtered_queryset(model):
if role.unlimited_visibility:
return model.all_objects.filter(tenant_id=self.request.tenant_id)
return model.all_objects.filter(
tenant_id=self.request.tenant_id, scan__provider__in=providers
)
return ScanSummary.all_objects.filter(tenant_id=self.request.tenant_id)
if self.action == "providers":
return _get_filtered_queryset(Finding)
elif self.action in ("findings", "findings_severity", "services"):
return _get_filtered_queryset(ScanSummary)
else:
return super().get_queryset()
def get_serializer_class(self):
if self.action == "providers":
@@ -3515,24 +3525,18 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="providers")
def providers(self, request):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
provider_filter = (
{"provider__in": self.allowed_providers}
if hasattr(self, "allowed_providers")
else {}
)
latest_scan_ids = (
Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
findings_aggregated = (
queryset.filter(scan_id__in=latest_scan_ids)
ScanSummary.all_objects.filter(
tenant_id=tenant_id, scan_id__in=latest_scan_ids
)
.values(
"scan__provider_id",
provider=F("scan__provider__provider"),
@@ -3568,7 +3572,7 @@ class OverviewViewSet(BaseRLSViewSet):
)
return Response(
self.get_serializer(overview, many=True).data,
OverviewProviderSerializer(overview, many=True).data,
status=status.HTTP_200_OK,
)
@@ -3577,16 +3581,9 @@ class OverviewViewSet(BaseRLSViewSet):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
provider_filter = (
{"provider__in": self.allowed_providers}
if hasattr(self, "allowed_providers")
else {}
)
latest_scan_ids = (
Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
@@ -3623,16 +3620,9 @@ class OverviewViewSet(BaseRLSViewSet):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
provider_filter = (
{"provider__in": self.allowed_providers}
if hasattr(self, "allowed_providers")
else {}
)
latest_scan_ids = (
Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
@@ -3652,7 +3642,7 @@ class OverviewViewSet(BaseRLSViewSet):
for item in severity_counts:
severity_data[item["severity"]] = item["count"]
serializer = self.get_serializer(severity_data)
serializer = OverviewSeveritySerializer(severity_data)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"], url_name="services")
@@ -3660,16 +3650,9 @@ class OverviewViewSet(BaseRLSViewSet):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
provider_filter = (
{"provider__in": self.allowed_providers}
if hasattr(self, "allowed_providers")
else {}
)
latest_scan_ids = (
Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
@@ -3687,7 +3670,7 @@ class OverviewViewSet(BaseRLSViewSet):
.order_by("service")
)
serializer = self.get_serializer(services_data, many=True)
serializer = OverviewServiceSerializer(services_data, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
-7
View File
@@ -46,19 +46,12 @@ from api.v1.serializers import TokenSerializer
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
TODAY = str(datetime.today().date())
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
TEST_USER = "dev@prowler.com"
TEST_PASSWORD = "testing_psswd"
def today_after_n_days(n_days: int) -> str:
return datetime.strftime(
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
)
@pytest.fixture(scope="module")
def enforce_test_user_db_connection(django_db_setup, django_db_blocker):
"""Ensure tests use the test user for database connections."""
+10 -4
View File
@@ -2,10 +2,10 @@ import json
from datetime import datetime, timedelta, timezone
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from rest_framework_json_api.serializers import ValidationError
from tasks.tasks import perform_scheduled_scan_task
from api.db_utils import rls_transaction
from api.exceptions import ConflictException
from api.models import Provider, Scan, StateChoices
@@ -24,9 +24,15 @@ def schedule_provider_scan(provider_instance: Provider):
if PeriodicTask.objects.filter(
interval=schedule, name=task_name, task="scan-perform-scheduled"
).exists():
raise ConflictException(
detail="There is already a scheduled scan for this provider.",
pointer="/data/attributes/provider_id",
raise ValidationError(
[
{
"detail": "There is already a scheduled scan for this provider.",
"status": 400,
"source": {"pointer": "/data/attributes/provider_id"},
"code": "invalid",
}
]
)
with rls_transaction(tenant_id):
+37 -41
View File
@@ -5,8 +5,8 @@ from datetime import datetime, timezone
from celery.utils.log import get_task_logger
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
from django.db import IntegrityError, OperationalError, connection
from django.db.models import Case, Count, IntegerField, Prefetch, Sum, When
from django.db import IntegrityError, OperationalError
from django.db.models import Case, Count, IntegerField, OuterRef, Subquery, Sum, When
from tasks.utils import CustomEncoder
from api.compliance import (
@@ -547,31 +547,36 @@ def _update_resource_failed_findings_count(tenant_id: str, scan_id: str):
with rls_transaction(tenant_id):
scan = Scan.objects.get(pk=scan_id)
provider_id = str(scan.provider_id)
provider_id = scan.provider_id
with connection.cursor() as cursor:
cursor.execute(
"""
UPDATE resources AS r
SET failed_findings_count = COALESCE((
SELECT COUNT(*) FROM (
SELECT DISTINCT ON (f.uid) f.uid
FROM findings AS f
JOIN resource_finding_mappings AS rfm
ON rfm.finding_id = f.id
WHERE f.tenant_id = %s
AND f.status = %s
AND f.muted = FALSE
AND rfm.resource_id = r.id
ORDER BY f.uid, f.inserted_at DESC
) AS latest_uids
), 0)
WHERE r.tenant_id = %s
AND r.provider_id = %s
""",
[tenant_id, FindingStatus.FAIL, tenant_id, provider_id],
resources = list(
Resource.all_objects.filter(tenant_id=tenant_id, provider_id=provider_id)
)
# For each resource, calculate failed findings count based on latest findings
for resource in resources:
with rls_transaction(tenant_id):
# Get the latest finding for each finding.uid that affects this resource
latest_findings_subquery = (
Finding.all_objects.filter(
tenant_id=tenant_id, uid=OuterRef("uid"), resources=resource
)
.order_by("-inserted_at")
.values("id")[:1]
)
# Count failed findings from the latest findings
failed_count = Finding.all_objects.filter(
tenant_id=tenant_id,
resources=resource,
id__in=Subquery(latest_findings_subquery),
status=FindingStatus.FAIL,
muted=False,
).count()
resource.failed_findings_count = failed_count
resource.save(update_fields=["failed_findings_count"])
def create_compliance_requirements(tenant_id: str, scan_id: str):
"""
@@ -598,27 +603,18 @@ def create_compliance_requirements(tenant_id: str, scan_id: str):
prowler_provider = return_prowler_provider(provider_instance)
# Get check status data by region from findings
findings = (
Finding.all_objects.filter(scan_id=scan_id, muted=False)
.only("id", "check_id", "status")
.prefetch_related(
Prefetch(
"resources",
queryset=Resource.objects.only("id", "region"),
to_attr="small_resources",
)
)
.iterator(chunk_size=1000)
)
check_status_by_region = {}
with rls_transaction(tenant_id):
findings = Finding.objects.filter(scan_id=scan_id, muted=False)
for finding in findings:
for resource in finding.small_resources:
# Get region from resources
for resource in finding.resources.all():
region = resource.region
current_status = check_status_by_region.setdefault(region, {})
if current_status.get(finding.check_id) != "FAIL":
current_status[finding.check_id] = finding.status
region_dict = check_status_by_region.setdefault(region, {})
current_status = region_dict.get(finding.check_id)
if current_status == "FAIL":
continue
region_dict[finding.check_id] = finding.status
try:
# Try to get regions from provider
+3 -3
View File
@@ -3,9 +3,9 @@ from unittest.mock import patch
import pytest
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from rest_framework_json_api.serializers import ValidationError
from tasks.beat import schedule_provider_scan
from api.exceptions import ConflictException
from api.models import Scan
@@ -48,8 +48,8 @@ class TestScheduleProviderScan:
with patch("tasks.tasks.perform_scheduled_scan_task.apply_async"):
schedule_provider_scan(provider_instance)
# Now, try scheduling again, should raise ConflictException
with pytest.raises(ConflictException) as exc_info:
# Now, try scheduling again, should raise ValidationError
with pytest.raises(ValidationError) as exc_info:
schedule_provider_scan(provider_instance)
assert "There is already a scheduled scan for this provider." in str(
+587 -100
View File
@@ -15,10 +15,10 @@ from tasks.utils import CustomEncoder
from api.exceptions import ProviderConnectionError
from api.models import (
ComplianceRequirementOverview,
Finding,
Provider,
Resource,
Scan,
Severity,
StateChoices,
StatusChoices,
@@ -401,13 +401,34 @@ class TestCreateComplianceRequirements:
resources_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch("tasks.jobs.scan.create_objects_in_batches") as mock_create_objects,
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.return_value = [
"us-east-1",
"us-west-2",
]
mock_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {
"cis_1.4_aws": {
@@ -436,29 +457,104 @@ class TestCreateComplianceRequirements:
},
},
},
"aws_account_security_onboarding_aws": {
"framework": "AWS Account Security Onboarding",
"version": "1.0",
"requirements": {
"requirement1": {
"description": "Basic security requirement",
"checks_status": {
"pass": 1,
"fail": 0,
"manual": 0,
"total": 1,
},
"status": "PASS",
},
},
},
}
mock_findings_filter.return_value = []
result = create_compliance_requirements(tenant_id, scan_id)
assert "requirements_created" in result
assert "regions_processed" in result
assert "compliance_frameworks" in result
assert result["regions_processed"] == ["us-east-1", "us-west-2"]
assert result["requirements_created"] == 6
assert len(result["compliance_frameworks"]) == 2
mock_create_objects.assert_called_once()
call_args = mock_create_objects.call_args[0]
assert call_args[0] == tenant_id
assert call_args[1] == ComplianceRequirementOverview
assert len(call_args[2]) == 6
compliance_objects = call_args[2]
for obj in compliance_objects:
assert isinstance(obj, ComplianceRequirementOverview)
assert obj.tenant.id == tenant.id
assert obj.scan == scan
assert obj.region in ["us-east-1", "us-west-2"]
assert obj.compliance_id in [
"cis_1.4_aws",
"aws_account_security_onboarding_aws",
]
def test_create_compliance_requirements_with_findings(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches"),
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_finding1 = MagicMock()
mock_finding1.check_id = "check1"
mock_finding1.status = "PASS"
mock_resource1 = MagicMock()
mock_resource1.region = "us-east-1"
mock_finding1.resources.all.return_value = [mock_resource1]
mock_finding2 = MagicMock()
mock_finding2.check_id = "check2"
mock_finding2.status = "FAIL"
mock_resource2 = MagicMock()
mock_resource2.region = "us-west-2"
mock_finding2.resources.all.return_value = [mock_resource2]
mock_findings_filter.return_value = [mock_finding1, mock_finding2]
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.return_value = [
"us-east-1",
"us-west-2",
]
mock_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {
"test_compliance": {
@@ -467,6 +563,7 @@ class TestCreateComplianceRequirements:
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {"check_1": None},
"checks_status": {
"pass": 2,
"fail": 1,
@@ -475,26 +572,43 @@ class TestCreateComplianceRequirements:
},
"status": "FAIL",
},
"req_2": {
"description": "Test Requirement 2",
"checks": {"check_2": None},
"checks_status": {
"pass": 2,
"fail": 0,
"manual": 0,
"total": 2,
},
"status": "PASS",
},
},
}
}
result = create_compliance_requirements(tenant_id, scan_id)
assert "requirements_created" in result
mock_findings_filter.assert_called_once_with(scan_id=scan_id, muted=False)
assert mock_generate_compliance.call_count == 2
assert result["requirements_created"] == 4
assert set(result["regions_processed"]) == {"us-east-1", "us-west-2"}
def test_create_compliance_requirements_kubernetes_provider(
def test_create_compliance_requirements_no_provider_regions(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch("tasks.jobs.scan.create_objects_in_batches"),
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
@@ -508,6 +622,20 @@ class TestCreateComplianceRequirements:
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_finding = MagicMock()
mock_finding.check_id = "check1"
mock_finding.status = "PASS"
mock_resource = MagicMock()
mock_resource.region = "default"
mock_finding.resources.all.return_value = [mock_resource]
mock_findings_filter.return_value = [mock_finding]
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.side_effect = AttributeError(
"No get_regions method"
)
mock_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {
"kubernetes_cis": {
"framework": "CIS Kubernetes Benchmark",
@@ -529,40 +657,92 @@ class TestCreateComplianceRequirements:
result = create_compliance_requirements(tenant_id, scan_id)
assert "regions_processed" in result
assert result["regions_processed"] == ["default"]
def test_create_compliance_requirements_empty_template(
def test_create_compliance_requirements_empty_findings(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches"),
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
mock_compliance_template.__getitem__.return_value = {}
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_findings_filter.return_value = []
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"]
mock_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {
"cis_1.4_aws": {
"framework": "CIS AWS Foundations Benchmark",
"version": "1.4.0",
"requirements": {
"1.1": {
"description": "Test requirement",
"checks_status": {
"pass": 0,
"fail": 0,
"manual": 0,
"total": 1,
},
"status": "PASS",
},
},
},
}
mock_findings_filter.return_value = []
result = create_compliance_requirements(tenant_id, scan_id)
assert result["requirements_created"] == 0
assert result["regions_processed"] == ["us-east-1"]
assert result["requirements_created"] == 1
mock_generate_compliance.assert_not_called()
def test_create_compliance_requirements_error_handling(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider:
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_prowler_provider.side_effect = Exception(
"Provider initialization failed"
@@ -571,19 +751,99 @@ class TestCreateComplianceRequirements:
with pytest.raises(Exception, match="Provider initialization failed"):
create_compliance_requirements(tenant_id, scan_id)
def test_create_compliance_requirements_check_status_priority(
self, tenants_fixture, scans_fixture, providers_fixture, findings_fixture
def test_create_compliance_requirements_muted_findings_excluded(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch("tasks.jobs.scan.create_objects_in_batches"),
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_findings_filter.return_value = []
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"]
mock_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {}
mock_findings_filter.return_value = []
create_compliance_requirements(tenant_id, scan_id)
mock_findings_filter.assert_called_once_with(scan_id=scan_id, muted=False)
def test_create_compliance_requirements_check_status_priority(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch(
"tasks.jobs.scan.return_prowler_provider"
) as mock_return_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches"),
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
mock_finding1 = MagicMock()
mock_finding1.check_id = "check1"
mock_finding1.status = "PASS"
mock_resource1 = MagicMock()
mock_resource1.region = "us-east-1"
mock_finding1.resources.all.return_value = [mock_resource1]
mock_finding2 = MagicMock()
mock_finding2.check_id = "check1"
mock_finding2.status = "FAIL"
mock_resource2 = MagicMock()
mock_resource2.region = "us-east-1"
mock_finding2.resources.all.return_value = [mock_resource2]
mock_findings_filter.return_value = [mock_finding1, mock_finding2]
mock_prowler_provider_instance = MagicMock()
mock_prowler_provider_instance.get_regions.return_value = ["us-east-1"]
mock_return_prowler_provider.return_value = mock_prowler_provider_instance
mock_compliance_template.__getitem__.return_value = {
"cis_1.4_aws": {
@@ -608,21 +868,38 @@ class TestCreateComplianceRequirements:
assert mock_generate_compliance.call_count == 1
def test_create_compliance_requirements_multiple_regions(
def test_compliance_overview_aggregation_requirement_fail_priority(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch(
"tasks.jobs.scan.return_prowler_provider"
) as mock_return_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches") as mock_create_objects,
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
mock_findings_filter.return_value = []
mock_prowler_provider = MagicMock()
mock_prowler_provider.get_regions.return_value = [
"us-east-1",
"us-west-2",
"eu-west-1",
]
mock_return_prowler_provider.return_value = mock_prowler_provider
mock_compliance_template.__getitem__.return_value = {
"test_compliance": {
@@ -631,6 +908,95 @@ class TestCreateComplianceRequirements:
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {"check_1": None},
"checks_status": {
"pass": 2,
"fail": 1,
"manual": 0,
"total": 3,
},
"status": "FAIL",
}
},
}
}
mock_generate_compliance.return_value = {
"test_compliance": {
"framework": "Test Framework",
"version": "1.0",
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {
"check_1": {
"us-east-1": {"status": "PASS"},
"us-west-2": {"status": "FAIL"},
"eu-west-1": {"status": "PASS"},
}
},
"checks_status": {
"pass": 2,
"fail": 1,
"manual": 0,
"total": 3,
},
"status": "FAIL",
}
},
}
}
created_objects = []
mock_create_objects.side_effect = (
lambda tenant_id, model, objs, batch_size=500: created_objects.extend(
objs
)
)
create_compliance_requirements(str(tenant.id), str(scan.id))
assert len(created_objects) == 3
assert all(obj.requirement_status == "FAIL" for obj in created_objects)
def test_compliance_overview_aggregation_requirement_pass_all_regions(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch(
"tasks.jobs.scan.return_prowler_provider"
) as mock_return_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches") as mock_create_objects,
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
providers_fixture[0]
mock_findings_filter.return_value = []
mock_prowler_provider = MagicMock()
mock_prowler_provider.get_regions.return_value = ["us-east-1", "us-west-2"]
mock_return_prowler_provider.return_value = mock_prowler_provider
mock_compliance_template.__getitem__.return_value = {
"test_compliance": {
"framework": "Test Framework",
"version": "1.0",
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {"check_1": None},
"checks_status": {
"pass": 2,
"fail": 0,
@@ -643,26 +1009,71 @@ class TestCreateComplianceRequirements:
}
}
result = create_compliance_requirements(tenant_id, scan_id)
mock_generate_compliance.return_value = {
"test_compliance": {
"framework": "Test Framework",
"version": "1.0",
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {
"check_1": {
"us-east-1": {"status": "PASS"},
"us-west-2": {"status": "PASS"},
}
},
"checks_status": {
"pass": 2,
"fail": 0,
"manual": 0,
"total": 2,
},
"status": "PASS",
}
},
}
}
assert "requirements_created" in result
assert len(result["regions_processed"]) >= 0
created_objects = []
mock_create_objects.side_effect = (
lambda tenant_id, model, objs, batch_size=500: created_objects.extend(
objs
)
)
def test_create_compliance_requirements_mixed_status_requirements(
create_compliance_requirements(str(tenant.id), str(scan.id))
assert len(created_objects) == 2
assert all(obj.requirement_status == "PASS" for obj in created_objects)
def test_compliance_overview_aggregation_multiple_requirements_mixed_status(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
findings_fixture,
):
with (
patch("api.db_utils.rls_transaction"),
patch(
"tasks.jobs.scan.return_prowler_provider"
) as mock_return_prowler_provider,
patch(
"tasks.jobs.scan.PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE"
) as mock_compliance_template,
patch("tasks.jobs.scan.generate_scan_compliance"),
patch(
"tasks.jobs.scan.generate_scan_compliance"
) as mock_generate_compliance,
patch("tasks.jobs.scan.create_objects_in_batches") as mock_create_objects,
patch("api.models.Finding.objects.filter") as mock_findings_filter,
):
tenant_id = str(tenants_fixture[0].id)
scan_id = str(scans_fixture[0].id)
tenant = tenants_fixture[0]
scan = scans_fixture[0]
mock_findings_filter.return_value = []
mock_prowler_provider = MagicMock()
mock_prowler_provider.get_regions.return_value = ["us-east-1", "us-west-2"]
mock_return_prowler_provider.return_value = mock_prowler_provider
mock_compliance_template.__getitem__.return_value = {
"test_compliance": {
@@ -671,6 +1082,7 @@ class TestCreateComplianceRequirements:
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {"check_1": None},
"checks_status": {
"pass": 2,
"fail": 0,
@@ -681,6 +1093,7 @@ class TestCreateComplianceRequirements:
},
"req_2": {
"description": "Test Requirement 2",
"checks": {"check_2": None},
"checks_status": {
"pass": 1,
"fail": 1,
@@ -693,72 +1106,146 @@ class TestCreateComplianceRequirements:
}
}
result = create_compliance_requirements(tenant_id, scan_id)
mock_generate_compliance.return_value = {
"test_compliance": {
"framework": "Test Framework",
"version": "1.0",
"requirements": {
"req_1": {
"description": "Test Requirement 1",
"checks": {
"check_1": {
"us-east-1": {"status": "PASS"},
"us-west-2": {"status": "PASS"},
}
},
"checks_status": {
"pass": 2,
"fail": 0,
"manual": 0,
"total": 2,
},
"status": "PASS",
},
"req_2": {
"description": "Test Requirement 2",
"checks": {
"check_2": {
"us-east-1": {"status": "PASS"},
"us-west-2": {"status": "FAIL"},
}
},
"checks_status": {
"pass": 1,
"fail": 1,
"manual": 0,
"total": 2,
},
"status": "FAIL",
},
},
}
}
assert "requirements_created" in result
assert result["requirements_created"] >= 0
created_objects = []
mock_create_objects.side_effect = (
lambda tenant_id, model, objs, batch_size=500: created_objects.extend(
objs
)
)
create_compliance_requirements(str(tenant.id), str(scan.id))
assert len(created_objects) == 4
req_1_objects = [
obj for obj in created_objects if obj.requirement_id == "req_1"
]
req_2_objects = [
obj for obj in created_objects if obj.requirement_id == "req_2"
]
assert len(req_1_objects) == 2
assert len(req_2_objects) == 2
assert all(obj.requirement_status == "PASS" for obj in req_1_objects)
assert all(obj.requirement_status == "FAIL" for obj in req_2_objects)
@pytest.mark.django_db
class TestUpdateResourceFailedFindingsCount:
def test_execute_sql_update(
self, tenants_fixture, scans_fixture, providers_fixture, resources_fixture
):
resource = resources_fixture[0]
tenant_id = resource.tenant_id
scan_id = resource.provider.scans.first().id
# Common kwargs for all failing findings
base_kwargs = {
"tenant_id": tenant_id,
"scan_id": scan_id,
"delta": None,
"status": StatusChoices.FAIL,
"status_extended": "test status extended",
"impact": Severity.critical,
"impact_extended": "test impact extended",
"severity": Severity.critical,
"raw_result": {
"status": StatusChoices.FAIL,
"impact": Severity.critical,
"severity": Severity.critical,
},
"tags": {"test": "dev-qa"},
"check_id": "test_check_id",
"check_metadata": {
"CheckId": "test_check_id",
"Description": "test description apple sauce",
"servicename": "ec2",
},
"first_seen_at": "2024-01-02T00:00:00Z",
}
# UIDs to create (two with same UID, one unique)
uids = ["test_finding_uid_1", "test_finding_uid_1", "test_finding_uid_2"]
# Create findings and associate with the resource
for uid in uids:
finding = Finding.objects.create(uid=uid, **base_kwargs)
finding.add_resources([resource])
resource.refresh_from_db()
assert resource.failed_findings_count == 0
_update_resource_failed_findings_count(tenant_id=tenant_id, scan_id=scan_id)
resource.refresh_from_db()
# Only two since two findings share the same UID
assert resource.failed_findings_count == 2
@patch("tasks.jobs.scan.Scan.objects.get")
def test_scan_not_found(
@patch("api.models.Resource.all_objects.filter")
@patch("api.models.Finding.all_objects.filter")
def test_failed_findings_count_update(
self,
mock_scan_get,
mock_finding_filter,
mock_resource_filter,
tenants_fixture,
scans_fixture,
providers_fixture,
):
mock_scan_get.side_effect = Scan.DoesNotExist
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
with pytest.raises(Scan.DoesNotExist):
_update_resource_failed_findings_count(
"8614ca97-8370-4183-a7f7-e96a6c7d2c93",
"4705bed5-8782-4e8b-bab6-55e8043edaa6",
)
scan.provider = provider
scan.save()
tenant_id = str(tenant.id)
scan_id = str(scan.id)
resource1 = MagicMock()
resource1.uid = "res-1"
resource1.failed_findings_count = None
resource1.save = MagicMock()
resource2 = MagicMock()
resource2.uid = "res-2"
resource2.failed_findings_count = None
resource2.save = MagicMock()
mock_resource_filter.return_value = [resource1, resource2]
fake_subquery_qs = MagicMock()
fake_subquery_qs.order_by.return_value = fake_subquery_qs
fake_subquery_qs.values.return_value = fake_subquery_qs
fake_subquery_qs.__getitem__.return_value = fake_subquery_qs
def finding_filter_side_effect(*args, **kwargs):
if "status" in kwargs:
qs_count = MagicMock()
if kwargs.get("resources") == resource1:
qs_count.count.return_value = 3
else:
qs_count.count.return_value = 0
return qs_count
return fake_subquery_qs
mock_finding_filter.side_effect = finding_filter_side_effect
_update_resource_failed_findings_count(tenant_id, scan_id)
# resource1 should have been updated to 3
assert resource1.failed_findings_count == 3
resource1.save.assert_called_once_with(update_fields=["failed_findings_count"])
# resource2 should have been updated to 0
assert resource2.failed_findings_count == 0
resource2.save.assert_called_once_with(update_fields=["failed_findings_count"])
@patch("api.models.Resource.all_objects.filter", return_value=[])
@patch("api.models.Finding.all_objects.filter")
def test_no_resources_no_error(
self,
mock_finding_filter,
mock_resource_filter,
tenants_fixture,
scans_fixture,
providers_fixture,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
scan.provider = provider
scan.save()
_update_resource_failed_findings_count(str(tenant.id), str(scan.id))
mock_finding_filter.assert_not_called()
+6 -13
View File
@@ -1,4 +1,5 @@
# Standard library imports
import csv
import glob
import json
import os
@@ -19,6 +20,7 @@ from dash.dependencies import Input, Output
# Config import
from dashboard.config import (
critical_color,
encoding_format,
fail_color,
folder_path_overview,
high_color,
@@ -44,7 +46,6 @@ from dashboard.lib.dropdowns import (
create_table_row_dropdown,
)
from dashboard.lib.layouts import create_layout_overview
from prowler.lib.logger import logger
# Suppress warnings
warnings.filterwarnings("ignore")
@@ -54,13 +55,11 @@ warnings.filterwarnings("ignore")
csv_files = []
for file in glob.glob(os.path.join(folder_path_overview, "*.csv")):
try:
df = pd.read_csv(file, sep=";")
num_rows = len(df)
with open(file, "r", newline="", encoding=encoding_format) as csvfile:
reader = csv.reader(csvfile)
num_rows = sum(1 for row in reader)
if num_rows > 1:
csv_files.append(file)
except Exception:
logger.error(f"Error reading file {file}")
# Import logos providers
@@ -192,13 +191,7 @@ else:
data.rename(columns={"RESOURCE_ID": "RESOURCE_UID"}, inplace=True)
# Remove dupplicates on the finding_uid colummn but keep the last one taking into account the timestamp
data["DATE"] = data["TIMESTAMP"].dt.date
data = (
data.sort_values("TIMESTAMP")
.groupby(["DATE", "FINDING_UID"], as_index=False)
.last()
)
data["TIMESTAMP"] = pd.to_datetime(data["TIMESTAMP"])
data = data.sort_values("TIMESTAMP").drop_duplicates("FINDING_UID", keep="last")
data["ASSESSMENT_TIME"] = data["TIMESTAMP"].dt.strftime("%Y-%m-%d")
data_valid = pd.DataFrame()
+7 -7
View File
@@ -1,6 +1,6 @@
# Extending Prowler Lighthouse AI
# Extending Prowler Lighthouse
This guide helps developers customize and extend Prowler Lighthouse AI by adding or modifying AI agents.
This guide helps developers customize and extend Prowler Lighthouse by adding or modifying AI agents.
## Understanding AI Agents
@@ -13,7 +13,7 @@ AI agents fall into two main categories:
- **Autonomous Agents**: Freely chooses from available tools to complete tasks, adapting their approach based on context. They decide which tools to use and when.
- **Workflow Agents**: Follows structured paths with predefined logic. They execute specific tool sequences and can include conditional logic.
Prowler Lighthouse AI is an autonomous agent - selecting the right tool(s) based on the users query.
Prowler Lighthouse is an autonomous agent - selecting the right tool(s) based on the users query.
???+ note
To learn more about AI agents, read [Anthropic's blog post on building effective agents](https://www.anthropic.com/engineering/building-effective-agents).
@@ -24,15 +24,15 @@ The autonomous nature of agents depends on the underlying LLM. Autonomous agents
After evaluating multiple LLM providers (OpenAI, Gemini, Claude, LLama) based on tool calling features and response accuracy, we recommend using the `gpt-4o` model.
## Prowler Lighthouse AI Architecture
## Prowler Lighthouse Architecture
Prowler Lighthouse AI uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library.
Prowler Lighthouse uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library.
### Architecture Components
<img src="../../tutorials/img/lighthouse-architecture.png" alt="Prowler Lighthouse architecture">
Prowler Lighthouse AI integrates with the NextJS application:
Prowler Lighthouse integrates with the NextJS application:
- The [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library integrates directly with NextJS
- The system uses the authenticated user session to interact with the Prowler API server
@@ -74,7 +74,7 @@ Modifying the supervisor prompt allows you to:
The supervisor agent and all specialized agents are defined in the `route.ts` file. The supervisor agent uses [langgraph-supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor), while other agents use the prebuilt [create-react-agent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/).
To add new capabilities or all Lighthouse AI to interact with other APIs, create additional specialized agents:
To add new capabilities or all Lighthouse to interact with other APIs, create additional specialized agents:
1. First determine what the new agent would do. Create a detailed prompt defining the agent's purpose and capabilities. You can see an example from [here](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts#L359-L385).
???+ note
+26 -6
View File
@@ -491,15 +491,11 @@ The provided credentials must have the appropriate permissions to perform all th
## Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication.
### Authentication
- For local scans, no authentication is required.
- For remote repository scans, authentication can be provided via:
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
The IaC provider does not require any authentication or credentials since it scans local files directly. This makes it ideal for CI/CD pipelines and local development environments.
### Supported Frameworks
@@ -519,3 +515,27 @@ The IaC provider leverages Checkov to support multiple frameworks, including:
- Kustomize
- OpenAPI
- SAST, SCA (Software Composition Analysis)
### Usage
To run Prowler with the IaC provider, use the `iac` flag. You can specify the directory to scan, frameworks to include, and paths to exclude.
#### Basic Example
```console
prowler iac --scan-path ./my-iac-directory
```
#### Specify Frameworks
Scan only Terraform and Kubernetes files:
```console
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
```
#### Exclude Paths
```console
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
```
+3 -16
View File
@@ -614,23 +614,12 @@ prowler github --github-app-id app_id --github-app-key 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.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local 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
@@ -639,10 +628,8 @@ prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/tes
```
???+ 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.
- The IaC provider does not require cloud authentication
- 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.
-31
View File
@@ -12,34 +12,3 @@
See section [Logging](./tutorials/logging.md) for further information or [contact us](./contact.md).
## Common Issues with Docker Compose Installation
- **Problem adding AWS Provider using "Connect assuming IAM Role" in Docker (see [GitHub Issue #7745](https://github.com/prowler-cloud/prowler/issues/7745))**:
When running Prowler App via Docker, you may encounter errors such as `Provider not set`, `AWS assume role error - Unable to locate credentials`, or `Provider has no secret` when trying to add an AWS Provider using the "Connect assuming IAM Role" option. This typically happens because the container does not have access to the necessary AWS credentials or profiles.
**Workaround:**
- Ensure your AWS credentials and configuration are available to the Docker container. You can do this by mounting your local `.aws` directory into the container. For example, in your `docker-compose.yaml`, add the following volume to the relevant services:
```yaml
volumes:
- "${HOME}/.aws:/home/prowler/.aws:ro"
```
This should be added to the `api`, `worker`, and `worker-beat` services.
- Create or update your `~/.aws/config` and `~/.aws/credentials` files with the appropriate profiles and roles. For example:
```ini
[profile prowler-profile]
role_arn = arn:aws:iam::<account-id>:role/ProwlerScan
source_profile = default
```
And set the environment variable in your `.env` file:
```env
AWS_PROFILE=prowler-profile
```
- If you are scanning multiple AWS accounts, you may need to add multiple profiles to your AWS config. Note that this workaround is mainly for local testing; for production or multi-account setups, follow the [CloudFormation Template guide](https://github.com/prowler-cloud/prowler/issues/7745) and ensure the correct IAM roles and permissions are set up in each account.
-1
View File
@@ -78,7 +78,6 @@ 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 |
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String |
## GCP
+207 -132
View File
@@ -1,152 +1,227 @@
# Prowler Fixer (remediation)
Prowler allows you to fix some of the failed findings it identifies. You can use the `--fixer` flag to run the fixes that are available for the checks that failed.
# Prowler Fixers (remediations)
```sh
prowler <provider> -c <check_to_fix_1> <check_to_fix_2> ... --fixer
```
Prowler supports automated remediation ("fixers") for certain findings. This system is extensible and provider-agnostic, allowing you to implement fixers for AWS, Azure, GCP, and M365 using a unified interface.
<img src="../img/fixer.png">
---
## Overview
- **Fixers** are Python classes that encapsulate the logic to remediate a failed check.
- Each provider has its own base fixer class, inheriting from a common abstract base (`Fixer`).
- Fixers are automatically discovered and invoked by Prowler when the `--fixer` flag is used.
???+ note
You can see all the available fixes for each provider with the `--list-remediations` or `--list-fixers flag.
Right now, fixers are only available through the CLI.
```sh
prowler <provider> --list-fixers
```
It's important to note that using the fixers for `Access Analyzer`, `GuardDuty`, and `SecurityHub` may incur additional costs. These AWS services might trigger actions or deploy resources that can lead to charges on your AWS account.
## Writing a Fixer
To write a fixer, you need to create a file called `<check_id>_fixer.py` inside the check folder, with a function called `fixer` that receives either the region or the resource to be fixed as a parameter, and returns a boolean value indicating if the fix was successful or not.
---
For example, the regional fixer for the `ec2_ebs_default_encryption` check, which enables EBS encryption by default in a region, would look like this:
```python
from prowler.lib.logger import logger
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
## How to Use Fixers
To run fixers for failed findings:
def fixer(region):
"""
Enable EBS encryption by default in a region. NOTE: Custom KMS keys for EBS Default Encryption may be overwritten.
Requires the ec2:EnableEbsEncryptionByDefault permission:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2:EnableEbsEncryptionByDefault",
"Resource": "*"
}
]
}
Args:
region (str): AWS region
Returns:
bool: True if EBS encryption by default is enabled, False otherwise
"""
try:
regional_client = ec2_client.regional_clients[region]
return regional_client.enable_ebs_encryption_by_default()[
"EbsEncryptionByDefault"
]
except Exception as error:
logger.error(
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
```sh
prowler <provider> -c <check_id_1> <check_id_2> ... --fixer
```
On the other hand, the fixer for the `s3_account_level_public_access_blocks` check, which enables the account-level public access blocks for S3, would look like this:
<img src="../img/fixer-info.png">
<img src="../img/fixer-no-needed.png">
To list all available fixers for a provider:
```sh
prowler <provider> --list-fixers
```
> **Note:** Some fixers may incur additional costs (e.g., enabling certain cloud services like `Access Analyzer`, `GuardDuty`, and `SecurityHub` in AWS).
---
## Fixer Class Structure
### Base Class
All fixers inherit from the abstract `Fixer` class (`prowler/lib/fix/fixer.py`). This class defines the required interface and common logic.
**Key methods and properties:**
- `__init__(description, cost_impact=False, cost_description=None)`: Sets metadata for the fixer.
- `_get_fixer_info()`: Returns a dictionary with fixer metadata.
- `fix(finding=None, **kwargs)`: Abstract method. Must be implemented by each fixer to perform the remediation.
- `get_fixer_for_finding(finding)`: Factory method to dynamically load the correct fixer for a finding.
- `run_fixer(findings)`: Runs the fixer(s) for one or more findings.
### Provider-Specific Base Classes
Each provider extends the base class to add provider-specific logic and metadata:
- **AWS:** `AWSFixer` (`prowler/providers/aws/lib/fix/fixer.py`)
- **Azure:** `AzureFixer` (`prowler/providers/azure/lib/fix/fixer.py`)
- **GCP:** `GCPFixer` (`prowler/providers/gcp/lib/fix/fixer.py`)
- **M365:** `M365Fixer` (`prowler/providers/m365/lib/fix/fixer.py`)
These classes may add fields such as required permissions, IAM policies, or provider-specific client handling.
---
## Writing a Fixer
### 1. **Location and Naming**
- Place your fixer in the checks directory, named `<check_id>_fixer.py`.
- The fixer class should be named in PascalCase, matching the check ID, ending with `Fixer`.
Example: For `ec2_ebs_default_encryption`, use `Ec2EbsDefaultEncryptionFixer`.
### 2. **Class Definition**
- Inherit from the providers base fixer class.
- Implement the `fix()` method. This method receives a finding and/or keyword arguments and must return `True` if the remediation was successful, `False` otherwise.
**Example (AWS):**
```python
from prowler.lib.logger import logger
from prowler.providers.aws.services.s3.s3control_client import s3control_client
from prowler.providers.aws.lib.fix.fixer import AWSFixer
def fixer(resource_id: str) -> bool:
"""
Enable S3 Block Public Access for the account. NOTE: By blocking all S3 public access you may break public S3 buckets.
Requires the s3:PutAccountPublicAccessBlock permission:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:PutAccountPublicAccessBlock",
class Ec2EbsDefaultEncryptionFixer(AWSFixer):
def __init__(self):
super().__init__(
description="Enable EBS encryption by default in a region.",
service="ec2",
iam_policy_required={
"Action": ["ec2:EnableEbsEncryptionByDefault"],
"Resource": "*"
}
]
}
Returns:
bool: True if S3 Block Public Access is enabled, False otherwise
"""
try:
s3control_client.client.put_public_access_block(
AccountId=resource_id,
PublicAccessBlockConfiguration={
"BlockPublicAcls": True,
"IgnorePublicAcls": True,
"BlockPublicPolicy": True,
"RestrictPublicBuckets": True,
},
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
else:
def fix(self, finding=None, **kwargs):
# Remediation logic here
return True
```
## Fixer Config file
For some fixers, you can have configurable parameters depending on your use case. You can either use the default config file in `prowler/config/fixer_config.yaml` or create a custom config file and pass it to the fixer with the `--fixer-config` flag. The config file should be a YAML file with the following structure:
```yaml
# Fixer configuration file
aws:
# ec2_ebs_default_encryption
# No configuration needed for this check
**Example (Azure):**
```python
from prowler.providers.azure.lib.fix.fixer import AzureFixer
# s3_account_level_public_access_blocks
# No configuration needed for this check
class AppFunctionFtpsDeploymentDisabledFixer(AzureFixer):
def __init__(self):
super().__init__(
description="Disable FTP/FTPS deployments for Azure Functions.",
service="app",
permissions_required={
"actions": [
"Microsoft.Web/sites/write",
"Microsoft.Web/sites/config/write"
]
}
)
# iam_password_policy_* checks:
iam_password_policy:
MinimumPasswordLength: 14
RequireSymbols: True
RequireNumbers: True
RequireUppercaseCharacters: True
RequireLowercaseCharacters: True
AllowUsersToChangePassword: True
MaxPasswordAge: 90
PasswordReusePrevention: 24
HardExpiry: False
# accessanalyzer_enabled
accessanalyzer_enabled:
AnalyzerName: "DefaultAnalyzer"
AnalyzerType: "ACCOUNT_UNUSED_ACCESS"
# guardduty_is_enabled
# No configuration needed for this check
# securityhub_enabled
securityhub_enabled:
EnableDefaultStandards: True
# cloudtrail_multi_region_enabled
cloudtrail_multi_region_enabled:
TrailName: "DefaultTrail"
S3BucketName: "my-cloudtrail-bucket"
IsMultiRegionTrail: True
EnableLogFileValidation: True
# CloudWatchLogsLogGroupArn: "arn:aws:logs:us-east-1:123456789012:log-group:my-cloudtrail-log-group"
# CloudWatchLogsRoleArn: "arn:aws:iam::123456789012:role/my-cloudtrail-role"
# KmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
# kms_cmk_rotation_enabled
# No configuration needed for this check
# ec2_ebs_snapshot_account_block_public_access
ec2_ebs_snapshot_account_block_public_access:
State: "block-all-sharing"
# ec2_instance_account_imdsv2_enabled
# No configuration needed for this check
def fix(self, finding=None, **kwargs):
# Remediation logic here
return True
```
**Example (GCP):**
```python
from prowler.providers.gcp.lib.fix.fixer import GCPFixer
class ComputeInstancePublicIPFixer(GCPFixer):
def __init__(self):
super().__init__(
description="Remove public IP from Compute Engine instance.",
service="compute",
iam_policy_required={
"roles": ["roles/compute.instanceAdmin.v1"]
}
)
def fix(self, finding=None, **kwargs):
# Remediation logic here
return True
```
**Example (M365):**
```python
from prowler.providers.m365.lib.fix.fixer import M365Fixer
class AppFunctionFtpsDeploymentDisabledFixer(M365Fixer):
def __init__(self):
super().__init__(
description="Disable FTP/FTPS deployments for Azure Functions.",
service="app",
permissions_required={
"actions": [
"Microsoft.Web/sites/write",
"Microsoft.Web/sites/config/write"
]
}
)
def fix(self, finding=None, **kwargs):
# Remediation logic here
return True
```
---
## Fixer info
Each fixer should provide:
- **description:** What the fixer does.
- **cost_impact:** Whether the remediation may incur costs.
- **cost_description:** Details about potential costs (if any).
For some providers, there will be additional information that needs to be added to the fixer info, like:
- **service:** The cloud service affected.
- **permissions/IAM policy required:** The minimum permissions needed for the fixer to work.
In order to get the fixer info, you can use the flag `--fixer-info`. And it will print the fixer info in a pretty format.
---
## Fixer Config File
Some fixers support configurable parameters.
You can use the default config file at `prowler/config/fixer_config.yaml` or provide your own with `--fixer-config`.
**Example YAML:**
```yaml
aws:
ec2_ebs_default_encryption: {}
iam_password_policy:
MinimumPasswordLength: 14
RequireSymbols: True
# ...
azure:
app_function_ftps_deployment_disabled:
ftps_state: "Disabled"
```
---
## Best Practices
- Always document the permissions required for your fixer.
- Handle exceptions gracefully and log errors.
- Return `True` only if the remediation was actually successful.
- Use the providers client libraries and follow their best practices for API calls.
---
## Troubleshooting
- If a fixer is not available for a check, Prowler will print a warning.
- If a fixer fails due to missing permissions, check the required IAM roles or permissions and update your execution identity accordingly.
- Use the `--list-fixers` flag to see all available fixers for your provider.
---
## Extending to New Providers
To add support for a new provider:
1. Implement a new base fixer class inheriting from `Fixer`.
2. Place it in the appropriate provider directory.
3. Follow the same structure for check-specific fixers.
---
**For more details, see the code in `prowler/lib/fix/fixer.py` and the provider-specific fixer base classes.**
@@ -1,209 +0,0 @@
# Getting Started with GitHub Authentication
This guide explains how to set up authentication with GitHub for Prowler. The documentation covers credential retrieval processes for each supported authentication method.
## Prerequisites
- GitHub account
- Token creation permissions (organization-level access requires admin permissions)
## Authentication Methods
### 1. Personal Access Token (PAT)
Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios.
#### How to Create a Personal Access Token
1. **Navigate to GitHub Settings**
- Open [GitHub](https://github.com) and sign in
- Click the profile picture in the top right corner
- Select "Settings" from the dropdown menu
2. **Access Developer Settings**
- Scroll down the left sidebar
- Click "Developer settings"
3. **Generate New Token**
- Click "Personal access tokens"
- Select "Tokens (classic)"
- Click "Generate new token"
4. **Configure Token Permissions**
To enable Prowler functionality, configure the following scopes:
- `repo`: Full control of private repositories
- `read:org`: Read organization and team membership
- `read:user`: Read user profile data
- `read:discussion`: Read discussions
- `read:enterprise`: Read enterprise data (if applicable)
5. **Copy and Store the Token**
- Copy the generated token immediately (GitHub displays tokens only once)
- Store tokens securely using environment variables
#### How to Use Personal Access Tokens
Choose one of the following methods:
**Command-line flag:**
```console
prowler github --personal-access-token your_token_here
```
**Environment variable:**
```console
export GITHUB_PERSONAL_ACCESS_TOKEN="your_token_here"
prowler github
```
### 2. OAuth App Token
OAuth Apps enable applications to act on behalf of users with explicit consent.
#### How to Create an OAuth App
1. **Navigate to Developer Settings**
- Open GitHub Settings → Developer settings
- Click "OAuth Apps"
2. **Register New Application**
- Click "New OAuth App"
- Complete the required fields:
- **Application name**: Descriptive application name
- **Homepage URL**: Application homepage
- **Authorization callback URL**: User redirection URL after authorization
3. **Obtain Authorization Code**
- Request authorization code (replace `{app_id}` with the application ID):
```
https://github.com/login/oauth/authorize?client_id={app_id}
```
4. **Exchange Code for Token**
- Exchange authorization code for access token (replace `{app_id}`, `{secret}`, and `{code}`):
```
https://github.com/login/oauth/access_token?code={code}&client_id={app_id}&client_secret={secret}
```
#### How to Use OAuth Tokens
Choose one of the following methods:
**Command-line flag:**
```console
prowler github --oauth-app-token your_oauth_token
```
**Environment variable:**
```console
export GITHUB_OAUTH_APP_TOKEN="your_oauth_token"
prowler github
```
### 3. GitHub App Credentials
GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations.
#### How to Create a GitHub App
1. **Navigate to Developer Settings**
- Open GitHub Settings → Developer settings
- Click "GitHub Apps"
2. **Create New GitHub App**
- Click "New GitHub App"
- Complete the required fields:
- **GitHub App name**: Unique application name
- **Homepage URL**: Application homepage
- **Webhook URL**: Webhook payload URL (optional)
- **Permissions**: Application permission requirements
3. **Configure Permissions**
To enable Prowler functionality, configure these permissions:
- **Repository permissions**:
- Contents (Read)
- Metadata (Read)
- Pull requests (Read)
- **Organization permissions**:
- Members (Read)
- Administration (Read)
- **Account permissions**:
- Email addresses (Read)
4. **Generate Private Key**
- Scroll to the "Private keys" section after app creation
- Click "Generate a private key"
- Download the `.pem` file and store securely
5. **Record App ID**
- Locate the App ID at the top of the GitHub App settings page
#### How to Install the GitHub App
1. **Install Application**
- Navigate to GitHub App settings
- Click "Install App" in the left sidebar
- Select the target account/organization
- Choose specific repositories or select "All repositories"
#### How to Use GitHub App Credentials
Choose one of the following methods:
**Command-line flags:**
```console
prowler github --github-app-id your_app_id --github-app-key /path/to/private-key.pem
```
**Environment variables:**
```console
export GITHUB_APP_ID="your_app_id"
export GITHUB_APP_KEY="private-key-content"
prowler github
```
## Best Practices
### Security Considerations
Implement the following security measures:
- **Secure Credential Storage**: Store credentials using environment variables instead of hardcoding tokens
- **Secrets Management**: Use dedicated secrets management systems in production environments
- **Regular Token Rotation**: Rotate tokens and keys regularly
- **Least Privilege Principle**: Grant only minimum required permissions
- **Permission Auditing**: Review and audit permissions regularly
- **Token Expiration**: Set appropriate expiration times for tokens
- **Usage Monitoring**: Monitor token usage and revoke unused tokens
### Authentication Method Selection
Choose the appropriate method based on use case:
- **Personal Access Token**: Individual use, testing, or simple automation
- **OAuth App Token**: Applications requiring user consent and delegation
- **GitHub App**: Production integrations, especially for organizations
## Troubleshooting Common Issues
### Insufficient Permissions
- Verify token/app has necessary scopes/permissions
- Check organization restrictions on third-party applications
### Token Expiration
- Confirm token has not expired
- Verify fine-grained tokens have correct resource access
### Rate Limiting
- GitHub implements API call rate limits
- Consider GitHub Apps for higher rate limits
### Organization Settings
- Some organizations restrict third-party applications
- Contact organization administrator if access is denied
+6 -37
View File
@@ -1,6 +1,6 @@
# Getting Started with the IaC Provider
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.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local 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.
## Supported Frameworks
@@ -23,50 +23,21 @@ The IaC provider leverages Checkov to support multiple frameworks, including:
## How It Works
- The IaC provider scans your local directory (or a specified path) for supported IaC files, or scan a remote repository.
- No cloud credentials or authentication are required for local scans.
- For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables.
- The IaC provider scans your local directory (or a specified path) for supported IaC files.
- No cloud credentials or authentication are required.
- Mutelist logic is handled by Checkov, not Prowler.
- Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.).
## Usage
To run Prowler with the IaC provider, use the `iac` argument. You can specify the directory or repository to scan, frameworks to include, and paths to exclude.
To run Prowler with the IaC provider, use the `iac` argument. You can specify the directory to scan, frameworks to include, and paths to exclude.
### Scan a Local Directory (default)
### Basic Example
```sh
prowler iac --scan-path ./my-iac-directory
```
### Scan a Remote GitHub Repository
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git
```
#### Authentication for Remote Private Repositories
You can provide authentication for private repositories using one of the following methods:
- **GitHub Username and Personal Access Token (PAT):**
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git \
--github-username <username> --personal-access-token <token>
```
- **GitHub OAuth App Token:**
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git \
--oauth-app-token <oauth_token>
```
- If not provided via CLI, the following environment variables will be used (in order of precedence):
- `GITHUB_OAUTH_APP_TOKEN`
- `GITHUB_USERNAME` and `GITHUB_PERSONAL_ACCESS_TOKEN`
- If neither CLI flags nor environment variables are set, the scan will attempt to clone without authentication or using the provided in the [git URL](https://git-scm.com/docs/git-clone#_git_urls).
#### Mutually Exclusive Flags
- `--scan-path` and `--scan-repository-url` are mutually exclusive. Only one can be specified at a time.
### Specify Frameworks
Scan only Terraform and Kubernetes files:
@@ -91,8 +62,6 @@ prowler iac --scan-path ./iac --output-formats csv json html
## Notes
- The IaC provider does not require cloud authentication for local scans.
- For remote repository scans, authentication is optional but required for private repos.
- CLI flags override environment variables for authentication.
- The IaC provider does not require cloud authentication.
- 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).
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

+20 -20
View File
@@ -1,12 +1,12 @@
# Prowler Lighthouse AI
# Prowler Lighthouse
Prowler Lighthouse AI is a Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
Prowler Lighthouse is an AI Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
<img src="../img/lighthouse-intro.png" alt="Prowler Lighthouse">
## How It Works
Prowler Lighthouse AI uses OpenAI's language models and integrates with your Prowler security findings data.
Prowler Lighthouse uses OpenAI's language models and integrates with your Prowler security findings data.
Here's what's happening behind the scenes:
@@ -14,28 +14,28 @@ Here's what's happening behind the scenes:
- It uses a ["supervisor" architecture](https://langchain-ai.lang.chat/langgraphjs/tutorials/multi_agent/agent_supervisor/) that interacts with different agents for specialized tasks. For example, `findings_agent` can analyze detected security findings, while `overview_agent` provides a summary of connected cloud accounts.
- The system connects to OpenAI models to understand, fetch the right data, and respond to the user's query.
???+ note
Lighthouse AI is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models.
Lighthouse is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models.
- The supervisor agent is the main contact point. It is what users interact with directly from the chat interface. It coordinates with other agents to answer users' questions comprehensively.
<img src="../img/lighthouse-architecture.png" alt="Lighthouse AI Architecture">
<img src="../img/lighthouse-architecture.png" alt="Lighthouse Architecture">
???+ note
All agents can only read relevant security data. They cannot modify your data or access sensitive information like configured secrets or tenant details.
## Set up
Getting started with Prowler Lighthouse AI is easy:
Getting started with Prowler Lighthouse is easy:
1. Go to the configuration page in your Prowler dashboard.
2. Enter your OpenAI API key.
3. Select your preferred model. The recommended one for best results is `gpt-4o`.
4. (Optional) Add business context to improve response quality and prioritization.
<img src="../img/lighthouse-config.png" alt="Lighthouse AI Configuration">
<img src="../img/lighthouse-config.png" alt="Lighthouse Configuration">
### Adding Business Context
The optional business context field lets you provide additional information to help Lighthouse AI understand your environment and priorities, including:
The optional business context field lets you provide additional information to help Lighthouse understand your environment and priorities, including:
- Your organization's cloud security goals
- Information about account owners or responsible teams
@@ -46,7 +46,7 @@ Better context leads to more relevant responses and prioritization that aligns w
## Capabilities
Prowler Lighthouse AI is designed to be your AI security team member, with capabilities including:
Prowler Lighthouse is designed to be your AI security team member, with capabilities including:
### Natural Language Querying
@@ -70,7 +70,7 @@ Get tailored step-by-step instructions for fixing security issues:
### Enhanced Context and Analysis
Lighthouse AI can provide additional context to help you understand the findings:
Lighthouse can provide additional context to help you understand the findings:
- Explain security concepts related to findings in simple terms
- Provide risk assessments based on your environment and context
@@ -82,20 +82,20 @@ Lighthouse AI can provide additional context to help you understand the findings
## Important Notes
Prowler Lighthouse AI is powerful, but there are limitations:
Prowler Lighthouse is powerful, but there are limitations:
- **Continuous improvement**: Please report any issues, as the feature may make mistakes or encounter errors, despite extensive testing.
- **Access limitations**: Lighthouse AI can only access data the logged-in user can view. If you can't see certain information, Lighthouse AI can't see it either.
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
- **Access limitations**: Lighthouse can only access data the logged-in user can view. If you can't see certain information, Lighthouse can't see it either.
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse will error out. Refresh and log back in to continue.
- **Response quality**: The response quality depends on the selected OpenAI model. For best results, use gpt-4o.
### Getting Help
If you encounter issues with Prowler Lighthouse AI or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
If you encounter issues with Prowler Lighthouse or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
### What Data Is Shared to OpenAI?
The following API endpoints are accessible to Prowler Lighthouse AI. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query:
The following API endpoints are accessible to Prowler Lighthouse. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query:
#### Accessible API Endpoints
@@ -139,7 +139,7 @@ The following API endpoints are accessible to Prowler Lighthouse AI. Data from t
#### Excluded API Endpoints
Not all Prowler API endpoints are integrated with Lighthouse AI. They are intentionally excluded for the following reasons:
Not all Prowler API endpoints are integrated with Lighthouse. They are intentionally excluded for the following reasons:
- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config)
- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.)
@@ -173,7 +173,7 @@ Not all Prowler API endpoints are integrated with Lighthouse AI. They are intent
- List all tasks - `/api/v1/tasks`
- Retrieve data from a specific task - `/api/v1/tasks/{id}`
**Lighthouse AI Configuration:**
**Lighthouse Configuration:**
- List OpenAI configuration - `/api/v1/lighthouse-config`
- Retrieve OpenAI key and configuration - `/api/v1/lighthouse-config/{id}`
@@ -187,7 +187,7 @@ Not all Prowler API endpoints are integrated with Lighthouse AI. They are intent
During feature development, we evaluated other LLM models.
- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse AI to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users.
- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users.
- **Gemini Models** - Gemini lacks a solid tool calling feature like OpenAI. It calls functions recursively until exceeding limits. Gemini-2.5-Pro-Experimental is better than previous models regarding tool calling and responding, but it's still experimental.
- **Deepseek V3** - Doesn't support system prompt messages.
@@ -197,8 +197,8 @@ Context windows are limited. While demo data fits inside the context window, que
**3. Is my security data shared with OpenAI?**
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The OpenAI key configured with Lighthouse AI is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements.
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The Lighthouse key is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements.
**4. Can the Lighthouse AI change my cloud environment?**
**4. Can the Lighthouse change my cloud environment?**
No. The agent doesn't have the tools to make the changes, even if the configured cloud provider API keys contain permissions to modify resources.
-1
View File
@@ -109,7 +109,6 @@ nav:
- Use of PowerShell: tutorials/microsoft365/use-of-powershell.md
- GitHub:
- Authentication: tutorials/github/authentication.md
- Getting Started: tutorials/github/getting-started-github.md
- IaC:
- Getting Started: tutorials/iac/getting-started-iac.md
- Developer Guide:
Generated
+93 -142
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -41,103 +41,103 @@ files = [
[[package]]
name = "aiohttp"
version = "3.12.14"
version = "3.12.13"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"},
{file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"},
{file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"},
{file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"},
{file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"},
{file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"},
{file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"},
{file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"},
{file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"},
{file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"},
{file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"},
{file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"},
{file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"},
{file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"},
{file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"},
{file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"},
{file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"},
{file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"},
{file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"},
{file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"},
{file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"},
{file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"},
{file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"},
{file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"},
{file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"},
{file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"},
{file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"},
{file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"},
{file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"},
{file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"},
{file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"},
{file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"},
{file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"},
{file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"},
{file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"},
{file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"},
{file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29"},
{file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0"},
{file = "aiohttp-3.12.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cd71c9fb92aceb5a23c4c39d8ecc80389c178eba9feab77f19274843eb9412d"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ebf1aca12845066c963016655dac897651e1544f22a34c9b461ac3b4b1d3aa"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893a4639694c5b7edd4bdd8141be296042b6806e27cc1d794e585c43010cc294"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:663d8ee3ffb3494502ebcccb49078faddbb84c1d870f9c1dd5a29e85d1f747ce"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0f8f6a85a0006ae2709aa4ce05749ba2cdcb4b43d6c21a16c8517c16593aabe"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1582745eb63df267c92d8b61ca655a0ce62105ef62542c00a74590f306be8cb5"},
{file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59227776ee2aa64226f7e086638baa645f4b044f2947dbf85c76ab11dcba073"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06b07c418bde1c8e737d8fa67741072bd3f5b0fb66cf8c0655172188c17e5fa6"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9445c1842680efac0f81d272fd8db7163acfcc2b1436e3f420f4c9a9c5a50795"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09c4767af0b0b98c724f5d47f2bf33395c8986995b0a9dab0575ca81a554a8c0"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3854fbde7a465318ad8d3fc5bef8f059e6d0a87e71a0d3360bb56c0bf87b18a"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2332b4c361c05ecd381edb99e2a33733f3db906739a83a483974b3df70a51b40"},
{file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1561db63fa1b658cd94325d303933553ea7d89ae09ff21cc3bcd41b8521fbbb6"},
{file = "aiohttp-3.12.13-cp310-cp310-win32.whl", hash = "sha256:a0be857f0b35177ba09d7c472825d1b711d11c6d0e8a2052804e3b93166de1ad"},
{file = "aiohttp-3.12.13-cp310-cp310-win_amd64.whl", hash = "sha256:fcc30ad4fb5cb41a33953292d45f54ef4066746d625992aeac33b8c681173178"},
{file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c"},
{file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358"},
{file = "aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc"},
{file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2"},
{file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3"},
{file = "aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd"},
{file = "aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9"},
{file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73"},
{file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347"},
{file = "aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6"},
{file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a"},
{file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5"},
{file = "aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf"},
{file = "aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e"},
{file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938"},
{file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace"},
{file = "aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103"},
{file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911"},
{file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3"},
{file = "aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd"},
{file = "aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706"},
{file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:36f6c973e003dc9b0bb4e8492a643641ea8ef0e97ff7aaa5c0f53d68839357b4"},
{file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6cbfc73179bd67c229eb171e2e3745d2afd5c711ccd1e40a68b90427f282eab1"},
{file = "aiohttp-3.12.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e8b27b2d414f7e3205aa23bb4a692e935ef877e3a71f40d1884f6e04fd7fa74"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eabded0c2b2ef56243289112c48556c395d70150ce4220d9008e6b4b3dd15690"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:003038e83f1a3ff97409999995ec02fe3008a1d675478949643281141f54751d"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b6f46613031dbc92bdcaad9c4c22c7209236ec501f9c0c5f5f0b6a689bf50f3"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c332c6bb04650d59fb94ed96491f43812549a3ba6e7a16a218e612f99f04145e"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fea41a2c931fb582cb15dc86a3037329e7b941df52b487a9f8b5aa960153cbd"},
{file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:846104f45d18fb390efd9b422b27d8f3cf8853f1218c537f36e71a385758c896"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d6c85ac7dd350f8da2520bac8205ce99df4435b399fa7f4dc4a70407073e390"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5a1ecce0ed281bec7da8550da052a6b89552db14d0a0a45554156f085a912f48"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5304d74867028cca8f64f1cc1215eb365388033c5a691ea7aa6b0dc47412f495"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:64d1f24ee95a2d1e094a4cd7a9b7d34d08db1bbcb8aa9fb717046b0a884ac294"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:119c79922a7001ca6a9e253228eb39b793ea994fd2eccb79481c64b5f9d2a055"},
{file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bb18f00396d22e2f10cd8825d671d9f9a3ba968d708a559c02a627536b36d91c"},
{file = "aiohttp-3.12.13-cp39-cp39-win32.whl", hash = "sha256:0022de47ef63fd06b065d430ac79c6b0bd24cdae7feaf0e8c6bac23b805a23a8"},
{file = "aiohttp-3.12.13-cp39-cp39-win_amd64.whl", hash = "sha256:29e08111ccf81b2734ae03f1ad1cb03b9615e7d8f616764f22f71209c094f122"},
{file = "aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce"},
]
[package.dependencies]
aiohappyeyeballs = ">=2.5.0"
aiosignal = ">=1.4.0"
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
@@ -166,19 +166,18 @@ docs = ["sphinx (==7.3.7)", "sphinx-mdinclude (==0.6.0)"]
[[package]]
name = "aiosignal"
version = "1.4.0"
version = "1.3.2"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"},
{file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"},
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
[[package]]
name = "alive-progress"
@@ -1940,54 +1939,6 @@ files = [
{file = "dpath-2.1.3.tar.gz", hash = "sha256:d1a7a0e6427d0a4156c792c82caf1f0109603f68ace792e36ca4596fd2cb8d9d"},
]
[[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.10"
@@ -6672,4 +6623,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "7a3f5d9a2b06322b3c4b65d1010116f84ea5e725693e51316ffeb23d4ed09c96"
content-hash = "4b0eee5566caf8e9d1e2e6fe8ac37733b29dd4275c2d65ac5291fa3acd514d9e"
+4 -23
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [v5.9.0] (Prowler v5.9.0)
## [v5.9.0] (Prowler UNRELEASED)
### Added
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
@@ -11,34 +11,12 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
- `defender_attack_path_notifications_properly_configured` check for Azure provider [(#8245)](https://github.com/prowler-cloud/prowler/pull/8245)
- `entra_intune_enrollment_sign_in_frequency_every_time` check for M365 provider [(#8223)](https://github.com/prowler-cloud/prowler/pull/8223)
- Support for remote repository scanning in IaC provider [(#8193)](https://github.com/prowler-cloud/prowler/pull/8193)
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)
### Changed
- Refactor the Azure Defender get security contact configuration method to use the API REST endpoint instead of the SDK [(#8241)](https://github.com/prowler-cloud/prowler/pull/8241)
### Fixed
- Title & description wording for `iam_user_accesskey_unused` check for AWS provider [(#8233)](https://github.com/prowler-cloud/prowler/pull/8233)
- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246)
- Show `m365_identity_type` and `m365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
- Ensure `is_service_role` only returns `True` for service roles [(#8274)](https://github.com/prowler-cloud/prowler/pull/8274)
- Update DynamoDB check metadata to fix broken link [(#8273)](https://github.com/prowler-cloud/prowler/pull/8273)
- Show correct count of findings in Dashboard Security Posture page [(#8270)](https://github.com/prowler-cloud/prowler/pull/8270)
- Add Check's metadata service name validator [(#8289)](https://github.com/prowler-cloud/prowler/pull/8289)
- Use subscription ID in Azure mutelist [(#8290)](https://github.com/prowler-cloud/prowler/pull/8290)
- `ServiceName` field in Network Firewall checks metadata [(#8280)](https://github.com/prowler-cloud/prowler/pull/8280)
- Update `entra_users_mfa_capable` check to use the correct resource name and ID [(#8288)](https://github.com/prowler-cloud/prowler/pull/8288)
- Handle multiple services and severities while listing checks [(#8302)](https://github.com/prowler-cloud/prowler/pull/8302)
- Handle `tenant_id` for M365 Mutelist [(#8306)](https://github.com/prowler-cloud/prowler/pull/8306)
---
## [v5.8.2] (Prowler 5.8.2)
### Fixed
- Fix error in Dashboard Overview page when reading CSV files [(#8257)](https://github.com/prowler-cloud/prowler/pull/8257)
---
@@ -112,6 +90,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Removed
- OCSF version number references to point always to the latest [(#8064)](https://github.com/prowler-cloud/prowler/pull/8064)
### Fixed
- Update SDK Azure call for ftps_state in the App Service. [(#7923)](https://github.com/prowler-cloud/prowler/pull/7923)
---
## [v5.7.5] (Prowler 5.7.5)
+4 -3
View File
@@ -31,7 +31,6 @@ from prowler.lib.check.check import (
print_fixers,
print_services,
remove_custom_checks_module,
run_fixer,
)
from prowler.lib.check.checks_loader import load_checks_to_execute
from prowler.lib.check.compliance import update_checks_metadata_with_compliance
@@ -42,6 +41,7 @@ from prowler.lib.check.custom_checks_metadata import (
)
from prowler.lib.check.models import CheckMetadata
from prowler.lib.cli.parser import ProwlerArgumentParser
from prowler.lib.fix.fixer import Fixer
from prowler.lib.logger import logger, set_logging_config
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import (
@@ -300,6 +300,7 @@ def prowler():
output_options = M365OutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
global_provider.set_output_options(output_options)
elif provider == "nhn":
output_options = NHNOutputOptions(
args, bulk_checks_metadata, global_provider.identity
@@ -332,11 +333,11 @@ def prowler():
)
# Prowler Fixer
if output_options.fixer:
if args.fixer:
print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}")
# Check if there are any FAIL findings
if any("FAIL" in finding.status for finding in findings):
fixed_findings = run_fixer(findings)
fixed_findings = Fixer.run_fixer(findings)
if not fixed_findings:
print(
f"{Style.BRIGHT}{Fore.RED}\nThere were findings to fix, but the fixer failed or it is not implemented for those findings yet. {Style.RESET_ALL}\n"
-4
View File
@@ -430,10 +430,6 @@ azure:
# TODO: create common config
shodan_api_key: null
# Configurable minimal risk level for attack path notifications
# azure.defender_attack_path_notifications_properly_configured
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"
-91
View File
@@ -298,91 +298,6 @@ def import_check(check_path: str) -> ModuleType:
return lib
def run_fixer(check_findings: list) -> int:
"""
Run the fixer for the check if it exists and there are any FAIL findings
Args:
check_findings (list): list of findings
Returns:
int: number of fixed findings
"""
try:
# Map findings to each check
findings_dict = {}
fixed_findings = 0
for finding in check_findings:
if finding.check_metadata.CheckID not in findings_dict:
findings_dict[finding.check_metadata.CheckID] = []
findings_dict[finding.check_metadata.CheckID].append(finding)
for check, findings in findings_dict.items():
# Check if there are any FAIL findings for the check
if any("FAIL" in finding.status for finding in findings):
try:
check_module_path = f"prowler.providers.{findings[0].check_metadata.Provider}.services.{findings[0].check_metadata.ServiceName}.{check}.{check}_fixer"
lib = import_check(check_module_path)
fixer = getattr(lib, "fixer")
except ModuleNotFoundError:
logger.error(f"Fixer method not implemented for check {check}")
else:
print(
f"\nFixing fails for check {Fore.YELLOW}{check}{Style.RESET_ALL}..."
)
for finding in findings:
if finding.status == "FAIL":
# Check what type of fixer is:
# - If it is a fixer for a specific resource and region
# - If it is a fixer for a specific region
# - If it is a fixer for a specific resource
if (
"region" in fixer.__code__.co_varnames
and "resource_id" in fixer.__code__.co_varnames
):
print(
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.resource_id} in {finding.region}... "
)
if fixer(
resource_id=finding.resource_id,
region=finding.region,
):
fixed_findings += 1
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
else:
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
elif "region" in fixer.__code__.co_varnames:
print(
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.region}... "
)
if fixer(region=finding.region):
fixed_findings += 1
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
else:
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
elif "resource_arn" in fixer.__code__.co_varnames:
print(
f"\t{orange_color}FIXING{Style.RESET_ALL} Resource {finding.resource_arn}... "
)
if fixer(resource_arn=finding.resource_arn):
fixed_findings += 1
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
else:
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
else:
print(
f"\t{orange_color}FIXING{Style.RESET_ALL} Resource {finding.resource_id}... "
)
if fixer(resource_id=finding.resource_id):
fixed_findings += 1
print(f"\t\t{Fore.GREEN}DONE{Style.RESET_ALL}")
else:
print(f"\t\t{Fore.RED}ERROR{Style.RESET_ALL}")
return fixed_findings
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def execute_checks(
checks_to_execute: list,
global_provider: Any,
@@ -635,13 +550,7 @@ def execute(
is_finding_muted_args["account_name"] = (
global_provider.identity.account_name
)
elif global_provider.type == "m365":
is_finding_muted_args["tenant_id"] = global_provider.identity.tenant_id
for finding in check_findings:
if global_provider.type == "azure":
is_finding_muted_args["subscription_id"] = (
global_provider.identity.subscriptions.get(finding.subscription)
)
is_finding_muted_args["finding"] = finding
finding.muted = global_provider.mutelist.is_finding_muted(
**is_finding_muted_args
+8 -7
View File
@@ -66,15 +66,16 @@ def load_checks_to_execute(
checks_to_execute.update(check_severities[severity])
if service_list:
checks_from_services = set()
for service in service_list:
service_checks = CheckMetadata.list(
bulk_checks_metadata=bulk_checks_metadata,
service=service,
checks_to_execute = (
set(
CheckMetadata.list(
bulk_checks_metadata=bulk_checks_metadata,
service=service,
)
)
& checks_to_execute
)
checks_from_services.update(service_checks)
checks_to_execute = checks_from_services & checks_to_execute
# Handle if there are checks passed using -C/--checks-file
elif checks_file:
checks_to_execute = parse_checks_from_file(checks_file, provider)
-30
View File
@@ -149,36 +149,6 @@ class CheckMetadata(BaseModel):
raise ValueError("ResourceType must be a non-empty string")
return resource_type
@validator("ServiceName", pre=True, always=True)
def validate_service_name(cls, service_name, values):
if not service_name:
raise ValueError("ServiceName must be a non-empty string")
check_id = values.get("CheckID")
if check_id:
service_from_check_id = check_id.split("_")[0]
if service_name != service_from_check_id:
raise ValueError(
f"ServiceName {service_name} does not belong to CheckID {check_id}"
)
if not service_name.islower():
raise ValueError(f"ServiceName {service_name} must be in lowercase")
return service_name
@validator("CheckID", pre=True, always=True)
def valid_check_id(cls, check_id):
if not check_id:
raise ValueError("CheckID must be a non-empty string")
if check_id:
if "-" in check_id:
raise ValueError(
f"CheckID {check_id} contains a hyphen, which is not allowed"
)
return check_id
@staticmethod
def get_bulk(provider: str) -> dict[str, "CheckMetadata"]:
"""
+10
View File
@@ -72,6 +72,7 @@ Detailed documentation at https://docs.prowler.com
self.__init_config_parser__()
self.__init_custom_checks_metadata_parser__()
self.__init_third_party_integrations_parser__()
self.__init_fixer_parser__()
# Init Providers Arguments
init_providers_parser(self)
@@ -393,3 +394,12 @@ Detailed documentation at https://docs.prowler.com
action="store_true",
help="Send a summary of the execution with a Slack APP in your channel. Environment variables SLACK_API_TOKEN and SLACK_CHANNEL_NAME are required (see more in https://docs.prowler.cloud/en/latest/tutorials/integrations/#slack).",
)
def __init_fixer_parser__(self):
"""Initialize the fixer parser with its arguments"""
fixer_parser = self.common_providers_parser.add_argument_group("Fixer")
fixer_parser.add_argument(
"--fixer",
action="store_true",
help="Fix the failed findings that can be fixed by Prowler",
)
+219
View File
@@ -0,0 +1,219 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Union
from colorama import Fore, Style
from prowler.lib.check.models import Check_Report
from prowler.lib.logger import logger
class Fixer(ABC):
"""Base class for all fixers"""
def __init__(
self,
description: str,
cost_impact: bool = False,
cost_description: Optional[str] = None,
):
"""
Initialize base fixer class.
Args:
description (str): Description of the fixer
cost_impact (bool): Whether the fixer has a cost impact
cost_description (Optional[str]): Description of the cost impact
"""
self._client = None
self.logger = logger
self.description = description
self.cost_impact = cost_impact
self.cost_description = cost_description
def _get_fixer_info(self) -> Dict:
"""Get fixer metadata"""
return {
"description": self.description,
"cost_impact": self.cost_impact,
"cost_description": self.cost_description,
}
@abstractmethod
def fix(self, finding: Optional[Check_Report] = None, **kwargs) -> bool:
"""
Main method that all fixers must implement.
Args:
finding (Optional[Check_Report]): Finding to fix
**kwargs: Additional arguments specific to each fixer
Returns:
bool: True if fix was successful, False otherwise
"""
@property
def client(self):
"""Lazy load of the client"""
return self._client
@classmethod
def get_fixer_for_finding(
cls,
finding: Check_Report,
) -> Optional["Fixer"]:
"""
Factory method to get the appropriate fixer for a finding.
Args:
finding (Check_Report): The finding to fix
credentials (Optional[Dict]): Optional credentials for isolated execution
session_config (Optional[Dict]): Optional session configuration
Returns:
Optional[Fixer]: An instance of the appropriate fixer or None if no fixer is found
"""
try:
# Extract check name from finding
check_name = finding.check_metadata.CheckID
if not check_name:
logger.error("Finding does not contain a check ID")
return None
# Convert check name to fixer class name
# Example: rds_instance_no_public_access -> RdsInstanceNoPublicAccessFixer
fixer_name = (
"".join(word.capitalize() for word in check_name.split("_")) + "Fixer"
)
# Get provider from finding
provider = finding.check_metadata.Provider
if not provider:
logger.error("Finding does not contain a provider")
return None
# Get service name from finding
service_name = finding.check_metadata.ServiceName
# Import the fixer class dynamically
try:
# Build the module path using the service name and check name
module_path = f"prowler.providers.{provider.lower()}.services.{service_name}.{check_name}.{check_name}_fixer"
module = __import__(module_path, fromlist=[fixer_name])
fixer_class = getattr(module, fixer_name)
return fixer_class()
except (ImportError, AttributeError):
print(
f"\n{Fore.YELLOW}No fixer available for check {check_name}{Style.RESET_ALL}"
)
return None
except Exception as e:
logger.error(f"Error getting fixer for finding: {str(e)}")
return None
@classmethod
def run_fixer(
cls,
findings: Union[Check_Report, List[Check_Report]],
) -> int:
"""
Method to execute the fixer on one or multiple findings.
Args:
findings (Union[Check_Report, List[Check_Report]]): A single finding or list of findings to fix
Returns:
int: Number of findings successfully fixed
"""
try:
# Handle single finding case
if isinstance(findings, Check_Report):
if findings.status != "FAIL":
return 0
check_id = findings.check_metadata.CheckID
if not check_id:
return 0
return cls.run_individual_fixer(check_id, [findings])
# Handle multiple findings case
fixed_findings = 0
findings_by_check = {}
# Group findings by check
for finding in findings:
if finding.status != "FAIL":
continue
check_id = finding.check_metadata.CheckID
if not check_id:
continue
if check_id not in findings_by_check:
findings_by_check[check_id] = []
findings_by_check[check_id].append(finding)
# Process each check
for check_id, check_findings in findings_by_check.items():
fixed_findings += cls.run_individual_fixer(check_id, check_findings)
return fixed_findings
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return 0
@classmethod
def run_individual_fixer(cls, check_id: str, findings: List[Check_Report]) -> int:
"""
Run the fixer for a specific check ID.
Args:
check_id (str): The check ID to fix
findings (List[Check_Report]): List of findings to process
Returns:
int: Number of findings successfully fixed
"""
try:
# Filter findings for this check_id and status FAIL
check_findings = [
finding
for finding in findings
if finding.check_metadata.CheckID == check_id
and finding.status == "FAIL"
]
if not check_findings:
return 0
# Get the fixer for this check
fixer = cls.get_fixer_for_finding(check_findings[0])
if not fixer:
return 0
# Print fixer information
print(f"\n{Fore.CYAN}Fixer Information for {check_id}:{Style.RESET_ALL}")
print(f"{Fore.CYAN}================================={Style.RESET_ALL}")
for key, value in fixer._get_fixer_info().items():
print(f"{Fore.CYAN}{key}: {Style.RESET_ALL}{value}")
print(f"{Fore.CYAN}================================={Style.RESET_ALL}\n")
print(
f"\nFixing fails for check {Fore.YELLOW}{check_id}{Style.RESET_ALL}..."
)
fixed_findings = 0
for finding in check_findings:
if fixer.fix(finding=finding):
fixed_findings += 1
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
else:
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
return fixed_findings
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return 0
+4 -2
View File
@@ -283,14 +283,16 @@ class Finding(BaseModel):
output_data["region"] = check_output.location
elif provider.type == "iac":
output_data["auth_method"] = provider.auth_method
output_data["auth_method"] = "local" # Until we support remote repos
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_name
output_data["region"] = check_output.resource_path
output_data["resource_line_range"] = check_output.resource_line_range
output_data["framework"] = check_output.check_metadata.ServiceName
output_data["framework"] = (
check_output.check_metadata.ServiceName
) # TODO: can we get the framework from the check_output?
# check_output Unique ID
# TODO: move this to a function
+2 -2
View File
@@ -710,7 +710,7 @@ class HTML(Output):
<ul class="list-group
list-group-flush">
<li class="list-group-item">
{"<b>IAC repository URL:</b> " + provider.scan_repository_url if provider.scan_repository_url else "<b>IAC path:</b> " + provider.scan_path}
<b>IAC path:</b> {provider.scan_path}
</li>
</ul>
</div>
@@ -723,7 +723,7 @@ class HTML(Output):
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>IAC authentication method:</b> {provider.auth_method}
<b>IAC authentication method:</b> local
</li>
</ul>
</div>
+2 -6
View File
@@ -55,12 +55,8 @@ def display_summary_table(
entity_type = "Tenant Domain"
audited_entities = provider.identity.tenant_domain
elif provider.type == "iac":
if provider.scan_repository_url:
entity_type = "Repository"
audited_entities = provider.scan_repository_url
else:
entity_type = "Directory"
audited_entities = provider.scan_path
entity_type = "Directory"
audited_entities = provider.scan_path
# Check if there are findings and that they are not all MANUAL
if findings and not all(finding.status == "MANUAL" for finding in findings):
@@ -159,14 +159,6 @@ def init_parser(self):
help="Scan unused services",
)
# Prowler Fixer
prowler_fixer_subparser = aws_parser.add_argument_group("Prowler Fixer")
prowler_fixer_subparser.add_argument(
"--fixer",
action="store_true",
help="Fix the failed findings that can be fixed by Prowler",
)
def validate_session_duration(session_duration: int) -> int:
"""validate_session_duration validates that the input session_duration is valid"""
+101
View File
@@ -0,0 +1,101 @@
from typing import Dict, Optional
from colorama import Style
from prowler.config.config import orange_color
from prowler.lib.check.models import Check_Report_AWS
from prowler.lib.fix.fixer import Fixer
from prowler.lib.logger import logger
class AWSFixer(Fixer):
"""AWS specific fixer implementation"""
def __init__(
self,
description: str,
cost_impact: bool = False,
cost_description: Optional[str] = None,
service: str = "",
iam_policy_required: Optional[Dict] = None,
):
"""
Initialize AWS fixer with metadata.
Args:
description (str): Description of the fixer
cost_impact (bool): Whether the fixer has a cost impact
cost_description (Optional[str]): Description of the cost impact
service (str): AWS service name
iam_policy_required (Optional[Dict]): Required IAM policy for the fixer
"""
super().__init__(description, cost_impact, cost_description)
self.service = service
self.iam_policy_required = iam_policy_required or {}
def _get_fixer_info(self):
"""Each fixer must define its metadata"""
fixer_info = super()._get_fixer_info()
fixer_info["service"] = self.service
fixer_info["iam_policy_required"] = self.iam_policy_required
return fixer_info
def fix(self, finding: Optional[Check_Report_AWS] = None, **kwargs) -> bool:
"""
AWS specific method to execute the fixer.
This method handles the printing of fixing status messages.
Args:
finding (Optional[Check_Report_AWS]): Finding to fix
**kwargs: Additional AWS-specific arguments (region, resource_id, resource_arn)
Returns:
bool: True if fixing was successful, False otherwise
"""
try:
# Get values either from finding or kwargs
region = None
resource_id = None
resource_arn = None
if finding:
region = finding.region if hasattr(finding, "region") else None
resource_id = (
finding.resource_id if hasattr(finding, "resource_id") else None
)
resource_arn = (
finding.resource_arn if hasattr(finding, "resource_arn") else None
)
else:
region = kwargs.get("region")
resource_id = kwargs.get("resource_id")
resource_arn = kwargs.get("resource_arn")
# Print the appropriate message based on available information
if region and resource_id:
print(
f"\t{orange_color}FIXING {resource_id} in {region}...{Style.RESET_ALL}"
)
elif region:
print(f"\t{orange_color}FIXING {region}...{Style.RESET_ALL}")
elif resource_arn:
print(
f"\t{orange_color}FIXING Resource {resource_arn}...{Style.RESET_ALL}"
)
elif resource_id:
print(
f"\t{orange_color}FIXING Resource {resource_id}...{Style.RESET_ALL}"
)
else:
logger.error(
"Either finding or required kwargs (region, resource_id, resource_arn) must be provided"
)
return False
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
@@ -8,7 +8,7 @@
"CheckType": [
"IAM"
],
"ServiceName": "apigatewayv2",
"ServiceName": "apigateway",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
@@ -8,7 +8,7 @@
"CheckType": [
"Logging and Monitoring"
],
"ServiceName": "apigatewayv2",
"ServiceName": "apigateway",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "documentdb_cluster_backup_enabled",
"CheckTitle": "Check if DocumentDB Clusters have backup enabled.",
"CheckType": [],
"ServiceName": "documentdb",
"ServiceName": "DocumentDB",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
"Severity": "medium",
@@ -12,7 +12,7 @@
"ResourceType": "AwsDynamoDbTable",
"Description": "Check if DynamoDB table has encryption at rest enabled using CMK KMS.",
"Risk": "All user data stored in Amazon DynamoDB is fully encrypted at rest. This functionality helps reduce the operational burden and complexity involved in protecting sensitive data.",
"RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html",
"RelatedUrl": "https://docs.aws.amazon.com/amazondynamodbdb/latest/developerguide/EncryptionAtRest.html",
"Remediation": {
"Code": {
"CLI": "",
@@ -22,7 +22,7 @@
},
"Recommendation": {
"Text": "Specify an encryption key when you create a new table or switch the encryption keys on an existing table by using the AWS Management Console.",
"Url": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html"
"Url": "https://docs.aws.amazon.com/amazondynamodbdb/latest/developerguide/EncryptionAtRest.html"
}
},
"Categories": [
@@ -13,28 +13,38 @@ from prowler.providers.aws.lib.service.service import AWSService
def is_service_role(role):
try:
statements = role.get("AssumeRolePolicyDocument", {}).get("Statement", [])
if not isinstance(statements, list):
statements = [statements]
for statement in statements:
if statement.get("Effect") != "Allow" or not any(
action in statement.get("Action", [])
for action in ("sts:AssumeRole", "sts:*", "*")
):
return False
principal = statement.get("Principal", {})
if set(principal.keys()) != {"Service"}:
return False
return True
if "Statement" in role["AssumeRolePolicyDocument"]:
if isinstance(role["AssumeRolePolicyDocument"]["Statement"], list):
for statement in role["AssumeRolePolicyDocument"]["Statement"]:
if (
statement["Effect"] == "Allow"
and (
"sts:AssumeRole" in statement["Action"]
or "sts:*" in statement["Action"]
or "*" in statement["Action"]
)
# This is what defines a service role
and "Service" in statement["Principal"]
):
return True
else:
statement = role["AssumeRolePolicyDocument"]["Statement"]
if (
statement["Effect"] == "Allow"
and (
"sts:AssumeRole" in statement["Action"]
or "sts:*" in statement["Action"]
or "*" in statement["Action"]
)
# This is what defines a service role
and "Service" in statement["Principal"]
):
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
return False
class IAM(AWSService):
@@ -1,7 +1,7 @@
{
"Provider": "aws",
"CheckID": "iam_user_accesskey_unused",
"CheckTitle": "Ensure unused User Access Keys are disabled",
"CheckTitle": "Ensure User Access Keys unused are disabled",
"CheckType": [
"Software and Configuration Checks"
],
@@ -10,7 +10,7 @@
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
"ResourceType": "AwsIamUser",
"Description": "Ensure unused User Access Keys are disabled",
"Description": "Ensure User Access Keys unused are disabled",
"Risk": "To increase the security of your AWS account, remove IAM user credentials (that is, passwords and access keys) that are not needed. For example, when users leave your organization or no longer need AWS access.",
"RelatedUrl": "",
"Remediation": {
@@ -1,36 +1,74 @@
from typing import Optional
from prowler.lib.check.models import Check_Report_AWS
from prowler.lib.logger import logger
from prowler.providers.aws.lib.fix.fixer import AWSFixer
from prowler.providers.aws.services.kms.kms_client import kms_client
def fixer(resource_id: str, region: str) -> bool:
class KmsCmkNotDeletedUnintentionallyFixer(AWSFixer):
"""
Cancel the scheduled deletion of a KMS key.
Specifically, this fixer calls the 'cancel_key_deletion' method to restore the KMS key's availability if it is marked for deletion.
Requires the kms:CancelKeyDeletion permission.
Permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "kms:CancelKeyDeletion",
"Resource": "*"
}
]
}
Args:
resource_id (str): The ID of the KMS key to cancel the deletion for.
region (str): AWS region where the KMS key exists.
Returns:
bool: True if the operation is successful (deletion cancellation is completed), False otherwise.
Fixer for KMS keys marked for deletion.
This fixer cancels the scheduled deletion of KMS keys.
"""
try:
regional_client = kms_client.regional_clients[region]
regional_client.cancel_key_deletion(KeyId=resource_id)
except Exception as error:
logger.error(
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
def __init__(self):
"""
Initialize KMS fixer.
"""
super().__init__(
description="Cancel the scheduled deletion of a KMS key",
cost_impact=False,
cost_description=None,
service="kms",
iam_policy_required={
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "kms:CancelKeyDeletion",
"Resource": "*",
}
],
},
)
return False
else:
return True
def fix(self, finding: Optional[Check_Report_AWS] = None, **kwargs) -> bool:
"""
Cancel the scheduled deletion of a KMS key.
This fixer calls the 'cancel_key_deletion' method to restore the KMS key's availability
if it is marked for deletion.
Args:
finding (Optional[Check_Report_AWS]): Finding to fix
**kwargs: Additional arguments (region and resource_id are required if finding is not provided)
Returns:
bool: True if the operation is successful (deletion cancellation is completed), False otherwise
"""
try:
# Get region and resource_id either from finding or kwargs
if finding:
region = finding.region
resource_id = finding.resource_id
else:
region = kwargs.get("region")
resource_id = kwargs.get("resource_id")
if not region or not resource_id:
raise ValueError("Region and resource_id are required")
# Show the fixing message
super().fix(region=region, resource_id=resource_id)
# Get the client for this region
regional_client = kms_client.regional_clients[region]
# Cancel key deletion
regional_client.cancel_key_deletion(KeyId=resource_id)
return True
except Exception as error:
logger.error(
f"{region if 'region' in locals() else 'unknown'} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
@@ -3,7 +3,7 @@
"CheckID": "networkfirewall_deletion_protection",
"CheckTitle": "Ensure that Deletion Protection safety feature is enabled for your Amazon VPC network firewalls.",
"CheckType": [],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "networkfirewall_in_all_vpc",
"CheckTitle": "Ensure all VPCs have Network Firewall enabled",
"CheckType": [],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -5,7 +5,7 @@
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53"
],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -5,7 +5,7 @@
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -5,7 +5,7 @@
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -5,7 +5,7 @@
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
@@ -5,7 +5,7 @@
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53"
],
"ServiceName": "networkfirewall",
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall-policy/policy-name",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "ssmincidents_enabled_with_plans",
"CheckTitle": "Ensure SSM Incidents is enabled with response plans.",
"CheckType": [],
"ServiceName": "ssmincidents",
"ServiceName": "ssm",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:ssm:region:account-id:document/document-name",
"Severity": "low",
@@ -3,7 +3,7 @@
"CheckID": "trustedadvisor_premium_support_plan_subscribed",
"CheckTitle": "Check if a Premium support plan is subscribed",
"CheckType": [],
"ServiceName": "trustedadvisor",
"ServiceName": "support",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:iam::AWS_ACCOUNT_NUMBER:root",
"Severity": "low",
+97
View File
@@ -0,0 +1,97 @@
from typing import Dict, Optional
from colorama import Style
from prowler.config.config import orange_color
from prowler.lib.check.models import Check_Report_Azure
from prowler.lib.fix.fixer import Fixer
from prowler.lib.logger import logger
class AzureFixer(Fixer):
"""Azure specific fixer implementation"""
def __init__(
self,
description: str,
cost_impact: bool = False,
cost_description: Optional[str] = None,
service: str = "",
permissions_required: Optional[Dict] = None,
):
super().__init__(description, cost_impact, cost_description)
self.service = service
self.permissions_required = permissions_required or {}
def _get_fixer_info(self):
"""Each fixer must define its metadata"""
fixer_info = super()._get_fixer_info()
fixer_info["service"] = self.service
fixer_info["permissions_required"] = self.permissions_required
return fixer_info
def fix(self, finding: Optional[Check_Report_Azure] = None, **kwargs) -> bool:
"""
Azure specific method to execute the fixer.
This method handles the printing of fixing status messages.
Args:
finding (Optional[Check_Report_Azure]): Finding to fix
**kwargs: Additional Azure-specific arguments (subscription_id, resource_id, resource_group)
Returns:
bool: True if fixing was successful, False otherwise
"""
try:
# Get values either from finding or kwargs
subscription_id = None
resource_id = None
resource_group = None
if finding:
subscription_id = (
finding.subscription if hasattr(finding, "subscription") else None
)
resource_id = (
finding.resource_id if hasattr(finding, "resource_id") else None
)
resource_group = (
finding.resource.get("resource_group_name")
if hasattr(finding.resource, "resource_group_name")
else None
)
else:
subscription_id = kwargs.get("subscription_id")
resource_id = kwargs.get("resource_id")
resource_group = kwargs.get("resource_group")
# Print the appropriate message based on available information
if subscription_id and resource_id and resource_group:
print(
f"\t{orange_color}FIXING Resource {resource_id} in Resource Group {resource_group} (Subscription: {subscription_id})...{Style.RESET_ALL}"
)
elif subscription_id and resource_id:
print(
f"\t{orange_color}FIXING Resource {resource_id} (Subscription: {subscription_id})...{Style.RESET_ALL}"
)
elif subscription_id:
print(
f"\t{orange_color}FIXING Subscription {subscription_id}...{Style.RESET_ALL}"
)
elif resource_id:
print(
f"\t{orange_color}FIXING Resource {resource_id}...{Style.RESET_ALL}"
)
else:
logger.error(
"Either finding or required kwargs (subscription_id, resource_id, resource_group) must be provided"
)
return False
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
@@ -7,16 +7,9 @@ class AzureMutelist(Mutelist):
def is_finding_muted(
self,
finding: Check_Report_Azure,
subscription_id: str,
) -> bool:
return self.is_muted(
subscription_id, # support Azure Subscription ID in mutelist
finding.check_metadata.CheckID,
finding.location,
finding.resource_name,
unroll_dict(unroll_tags(finding.resource_tags)),
) or self.is_muted(
finding.subscription, # support Azure Subscription Name in mutelist
finding.subscription,
finding.check_metadata.CheckID,
finding.location,
finding.resource_name,
@@ -0,0 +1,75 @@
from typing import Optional
from azure.mgmt.web.models import SiteConfigResource
from prowler.lib.check.models import Check_Report_Azure
from prowler.providers.azure.lib.fix.fixer import AzureFixer
from prowler.providers.azure.services.app.app_client import app_client
class AppFunctionFtpsDeploymentDisabledFixer(AzureFixer):
"""
This class handles the remediation of the app_function_ftps_deployment_disabled check.
It disables FTP/FTPS deployments for Azure Functions to prevent unauthorized access.
"""
def __init__(self):
super().__init__(
description="Disable FTP/FTPS deployments for Azure Functions",
service="app",
cost_impact=False,
cost_description=None,
permissions_required={
"Microsoft.Web/sites/config/write": "Write access to the site configuration",
},
)
def fix(self, finding: Optional[Check_Report_Azure] = None, **kwargs) -> bool:
"""
Fix the failed check by disabling FTP/FTPS deployments for the Azure Function.
Args:
finding (Check_Report_Azure): Finding to fix
**kwargs: Additional Azure-specific arguments (subscription_id, resource_id, resource_group)
Returns:
bool: True if FTP/FTPS is disabled, False otherwise
"""
try:
if finding:
resource_group = finding.resource.get("resource_group_name")
resource_id = finding.resource_name
suscription_id = finding.subscription
else:
resource_group = kwargs.get("resource_group")
resource_id = kwargs.get("resource_id")
suscription_id = kwargs.get("subscription_id")
if not resource_group or not resource_id or not suscription_id:
raise ValueError(
"Resource group, app name and subscription name are required"
)
super().fix(
resource_group=resource_group,
resource_id=resource_id,
suscription_id=suscription_id,
)
client = app_client.clients[suscription_id]
site_config = SiteConfigResource(ftps_state="Disabled")
client.web_apps.update_configuration(
resource_group_name=resource_group,
name=resource_id,
site_config=site_config,
)
return True
except Exception as error:
self.logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
@@ -170,6 +170,7 @@ class App(AzureService):
ftps_state=getattr(
function_config, "ftps_state", None
),
resource_group_name=function.resource_group,
)
}
)
@@ -293,3 +294,4 @@ class FunctionApp:
public_access: bool
vnet_subnet_id: str
ftps_state: Optional[str]
resource_group_name: str
@@ -1,3 +1,5 @@
import re
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.defender.defender_client import defender_client
@@ -8,17 +10,21 @@ class defender_additional_email_configured_with_a_security_contact(Check):
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
security_contacts,
) in defender_client.security_contacts.items():
for contact in security_contacts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
report.status = "PASS"
report.subscription = subscription_name
report.status_extended = f"There is another correct email configured for subscription {subscription_name}."
if len(contact_configuration.emails) > 0:
report.status = "PASS"
report.status_extended = f"There is another correct email configured for subscription {subscription_name}."
emails = contact.emails.split(";")
for email in emails:
if re.fullmatch(
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email
):
break
else:
report.status = "FAIL"
report.status_extended = f"There is not another correct email configured for subscription {subscription_name}."
@@ -1,30 +0,0 @@
{
"Provider": "azure",
"CheckID": "defender_attack_path_notifications_properly_configured",
"CheckTitle": "Ensure that email notifications for attack paths are enabled with minimal risk level",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AzureEmailNotifications",
"Description": "Ensure that Microsoft Defender for Cloud is configured to send email notifications for attack paths identified in the Azure subscription with an appropriate minimal risk level.",
"Risk": "If attack path notifications are not enabled, security teams may not be promptly informed about exploitable attack sequences, increasing the risk of delayed mitigation and potential breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable attack path email notifications in Microsoft Defender for Cloud to ensure that security teams are notified when potential attack paths are identified. Configure the minimal risk level as appropriate for your organization.",
"Url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -1,51 +0,0 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.defender.defender_client import defender_client
class defender_attack_path_notifications_properly_configured(Check):
"""
Ensure that email notifications for attack paths are enabled.
This check evaluates whether Microsoft Defender for Cloud is configured to send email notifications for attack paths in each Azure subscription.
- PASS: Notifications are enabled for attack paths with a risk level set (not None) and equal or higher than the configured minimum.
- FAIL: Notifications are not enabled for attack paths in the subscription or the risk level is too low.
"""
def execute(self) -> list[Check_Report_Azure]:
findings = []
# Get the minimal risk level from config, default to 'High'
risk_levels = ["Low", "Medium", "High", "Critical"]
min_risk_level = defender_client.audit_config.get(
"defender_attack_path_minimal_risk_level", "High"
)
if min_risk_level not in risk_levels:
min_risk_level = "High"
min_risk_index = risk_levels.index(min_risk_level)
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
report.subscription = subscription_name
actual_risk_level = getattr(
contact_configuration, "attack_path_minimal_risk_level", None
)
if not actual_risk_level or actual_risk_level not in risk_levels:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
actual_risk_index = risk_levels.index(actual_risk_level)
if actual_risk_index <= min_risk_index:
report.status = "PASS"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
findings.append(report)
return findings
@@ -8,22 +8,20 @@ class defender_ensure_notify_alerts_severity_is_high(Check):
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
security_contacts,
) in defender_client.security_contacts.items():
for contact in security_contacts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
report.subscription = subscription_name
report.status = "FAIL"
report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}."
if (
contact_configuration.alert_minimal_severity
and contact_configuration.alert_minimal_severity != "Critical"
contact.alert_notifications_minimal_severity != "Critical"
and contact.alert_notifications_minimal_severity != ""
):
report.status = "PASS"
report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact_configuration.alert_minimal_severity}) in subscription {subscription_name}."
report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact.alert_notifications_minimal_severity}) in subscription {subscription_name}."
findings.append(report)
@@ -8,20 +8,19 @@ class defender_ensure_notify_emails_to_owners(Check):
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
security_contacts,
) in defender_client.security_contacts.items():
for contact in security_contacts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
report.subscription = subscription_name
report.status = "PASS"
report.status_extended = (
f"The Owner role is notified for subscription {subscription_name}."
)
if (
contact_configuration.notifications_by_role.state
and "Owner" in contact_configuration.notifications_by_role.roles
contact.notified_roles_state != "On"
or "Owner" not in contact.notified_roles
):
report.status = "PASS"
report.status_extended = f"The Owner role is notified for subscription {subscription_name}."
else:
report.status = "FAIL"
report.status_extended = f"The Owner role is not notified for subscription {subscription_name}."
@@ -1,8 +1,11 @@
from datetime import timedelta
from typing import Dict, Optional
from typing import Dict
import requests
from azure.core.exceptions import ClientAuthenticationError, ResourceNotFoundError
from azure.core.exceptions import (
ClientAuthenticationError,
HttpResponseError,
ResourceNotFoundError,
)
from azure.mgmt.security import SecurityCenter
from pydantic.v1 import BaseModel
@@ -19,11 +22,7 @@ class Defender(AzureService):
self.auto_provisioning_settings = self._get_auto_provisioning_settings()
self.assessments = self._get_assessments()
self.settings = self._get_settings()
self.security_contact_configurations = self._get_security_contacts(
token=provider.session.get_token(
"https://management.azure.com/.default"
).token
)
self.security_contacts = self._get_security_contacts()
self.iot_security_solutions = self._get_iot_security_solutions()
def _get_pricings(self):
@@ -150,70 +149,48 @@ class Defender(AzureService):
)
return settings
def _get_security_contacts(self, token: str) -> dict[str, dict]:
"""
Get all security contacts configuration for all subscriptions.
Args:
token: The authentication token to make the request.
Returns:
A dictionary of security contacts for all subscriptions.
"""
def _get_security_contacts(self):
logger.info("Defender - Getting security contacts...")
security_contacts = {}
for subscription_name, subscription_id in self.subscriptions.items():
for subscription_name, client in self.clients.items():
try:
url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
contact_configurations = response.json().get("value", [])
security_contacts[subscription_name] = {}
for contact_configuration in contact_configurations:
props = contact_configuration.get("properties", {})
# Map notificationsByRole.state from "On"/"Off" to boolean
notifications_by_role_state = props.get(
"notificationsByRole", {}
).get("state", "Off")
notifications_by_role_state_bool = (
notifications_by_role_state.lower() == "on"
security_contacts.update({subscription_name: {}})
# TODO: List all security contacts. For now, the list method is not working.
security_contact_default = client.security_contacts.get("default")
security_contacts[subscription_name].update(
{
security_contact_default.name: SecurityContacts(
resource_id=security_contact_default.id,
name=getattr(security_contact_default, "name", "default")
or "default",
emails=security_contact_default.emails,
phone=security_contact_default.phone,
alert_notifications_minimal_severity=security_contact_default.alert_notifications.minimal_severity,
alert_notifications_state=security_contact_default.alert_notifications.state,
notified_roles=security_contact_default.notifications_by_role.roles,
notified_roles_state=security_contact_default.notifications_by_role.state,
)
}
)
except HttpResponseError as error:
if error.status_code == 404:
security_contacts[subscription_name].update(
{
"default": SecurityContacts(
resource_id=f"/subscriptions/{self.subscriptions[subscription_name]}/providers/Microsoft.Security/securityContacts/default",
name="default",
emails="",
phone="",
alert_notifications_minimal_severity="",
alert_notifications_state="",
notified_roles=[""],
notified_roles_state="",
)
}
)
notifications_by_role_roles = props.get(
"notificationsByRole", {}
).get("roles", [])
# Extract minimalRiskLevel and minimalSeverity from notificationsSources
attack_path_minimal_risk_level = None
alert_minimal_severity = None
for source in props.get("notificationsSources", []):
if source.get("sourceType") == "AttackPath":
value = source.get("minimalRiskLevel")
if value is not None:
attack_path_minimal_risk_level = value
elif source.get("sourceType") == "Alert":
value = source.get("minimalSeverity")
if value is not None:
alert_minimal_severity = value
security_contacts[subscription_name][
contact_configuration.get("name", "default")
] = SecurityContactConfiguration(
id=contact_configuration.get("id", ""),
name=contact_configuration.get("name", "default"),
enabled=props.get("isEnabled", False),
emails=props.get("emails", "").split(";"),
phone=props.get("phone", ""),
notifications_by_role=NotificationsByRole(
state=notifications_by_role_state_bool,
roles=notifications_by_role_roles,
),
attack_path_minimal_risk_level=attack_path_minimal_risk_level,
alert_minimal_severity=alert_minimal_severity,
else:
logger.error(
f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
@@ -275,42 +252,15 @@ class Setting(BaseModel):
enabled: bool
class NotificationsByRole(BaseModel):
"""
Defines whether to send email notifications from Microsoft Defender for Cloud to persons with specific RBAC roles on the subscription.
Attributes:
state: Whether notifications by role are enabled.
roles: List of Azure roles (e.g., 'Owner', 'Admin') to be notified.
"""
state: bool
roles: list[str]
class SecurityContactConfiguration(BaseModel):
"""
Represents the configuration of an Azure Security Center security contact.
Attributes:
id: The unique resource ID of the security contact.
name: The name of the security contact (usually 'default').
enabled: Whether the security contact is enabled. If enabled, the security contact will receive notifications, otherwise it will not.
emails: List of email addresses to notify.
phone: Contact phone number.
notifications_by_role: Defines whether to send email notifications from Microsoft Defender for Cloud to persons with specific RBAC roles on the subscription.
attack_path_minimal_risk_level: Minimal risk level for Attack Path notifications (e.g., 'Critical').
alert_minimal_severity: Minimal severity for Alert notifications (e.g., 'Medium').
"""
id: str
class SecurityContacts(BaseModel):
resource_id: str
name: str
enabled: bool
emails: list[str]
phone: Optional[str] = None
notifications_by_role: NotificationsByRole
attack_path_minimal_risk_level: Optional[str] = None
alert_minimal_severity: Optional[str] = None
emails: str
phone: str
alert_notifications_minimal_severity: str
alert_notifications_state: str
notified_roles: list[str]
notified_roles_state: str
class IoTSecuritySolution(BaseModel):
-4
View File
@@ -246,14 +246,10 @@ class Provider(ABC):
elif "iac" in provider_class_name.lower():
provider_class(
scan_path=arguments.scan_path,
scan_repository_url=arguments.scan_repository_url,
frameworks=arguments.frameworks,
exclude_path=arguments.exclude_path,
config_path=arguments.config_file,
fixer_config=fixer_config,
github_username=arguments.github_username,
personal_access_token=arguments.personal_access_token,
oauth_app_token=arguments.oauth_app_token,
)
except TypeError as error:
+97
View File
@@ -0,0 +1,97 @@
from typing import Dict, Optional
from prowler.lib.check.models import Check_Report_GCP
from prowler.lib.fix.fixer import Fixer
from prowler.lib.logger import logger
from prowler.providers.gcp.gcp_provider import GcpProvider
class GCPFixer(Fixer):
"""GCP specific fixer implementation"""
def __init__(
self,
description: str,
cost_impact: bool = False,
cost_description: Optional[str] = None,
service: str = "",
iam_policy_required: Optional[Dict] = None,
):
"""
Initialize GCP fixer with metadata.
Args:
description (str): Description of the fixer
cost_impact (bool): Whether the fixer has a cost impact
cost_description (Optional[str]): Description of the cost impact
service (str): GCP service name
iam_policy_required (Optional[Dict]): Required IAM policy for the fixer
"""
super().__init__(description, cost_impact, cost_description)
self.service = service
self.iam_policy_required = iam_policy_required or {}
self._provider = None
@property
def provider(self) -> GcpProvider:
"""Get the GCP provider instance"""
if not self._provider:
self._provider = GcpProvider()
return self._provider
def _get_fixer_info(self) -> Dict:
"""Get fixer metadata"""
info = super()._get_fixer_info()
info["service"] = self.service
info["iam_policy_required"] = self.iam_policy_required
info["provider"] = "gcp"
return info
def fix(self, finding: Optional[Check_Report_GCP] = None, **kwargs) -> bool:
"""
GCP specific method to execute the fixer.
This method handles the printing of fixing status messages.
Args:
finding (Optional[Check_Report_GCP]): Finding to fix
**kwargs: Additional GCP-specific arguments (project_id, resource_id)
Returns:
bool: True if fixing was successful, False otherwise
"""
try:
# Get values either from finding or kwargs
project_id = None
resource_id = None
if finding:
project_id = (
finding.project_id if hasattr(finding, "project_id") else None
)
resource_id = (
finding.resource_id if hasattr(finding, "resource_id") else None
)
else:
project_id = kwargs.get("project_id")
resource_id = kwargs.get("resource_id")
# Print the appropriate message based on available information
if project_id and resource_id:
print(f"\tFIXING {resource_id} in project {project_id}...")
elif project_id:
print(f"\tFIXING project {project_id}...")
elif resource_id:
print(f"\tFIXING Resource {resource_id}...")
else:
logger.error(
"Either finding or required kwargs (project_id, resource_id) must be provided"
)
return False
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
@@ -3,7 +3,7 @@
"CheckID": "compute_firewall_rdp_access_from_the_internet_allowed",
"CheckTitle": "Ensure That RDP Access Is Restricted From the Internet",
"CheckType": [],
"ServiceName": "compute",
"ServiceName": "networking",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
@@ -3,7 +3,7 @@
"CheckID": "compute_firewall_ssh_access_from_the_internet_allowed",
"CheckTitle": "Ensure That SSH Access Is Restricted From the Internet",
"CheckType": [],
"ServiceName": "compute",
"ServiceName": "networking",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
@@ -0,0 +1,63 @@
from typing import Optional
from prowler.lib.check.models import Check_Report_GCP
from prowler.lib.logger import logger
from prowler.providers.gcp.lib.fix.fixer import GCPFixer
from prowler.providers.gcp.services.compute.compute_client import compute_client
class ComputeProjectOsLoginEnabledFixer(GCPFixer):
"""
Fixer for enabling OS Login at the project level.
This fixer enables the OS Login feature which provides centralized and automated SSH key pair management.
"""
def __init__(self):
"""
Initialize Compute Engine fixer.
"""
super().__init__(
description="Enable OS Login at the project level",
cost_impact=False,
cost_description=None,
service="compute",
iam_policy_required={
"roles": ["roles/compute.admin"],
},
)
def fix(self, finding: Optional[Check_Report_GCP] = None, **kwargs) -> bool:
"""
Enable OS Login at the project level.
Args:
finding (Optional[Check_Report_GCP]): Finding to fix
**kwargs: Additional arguments (project_id is required if finding is not provided)
Returns:
bool: True if the operation is successful (OS Login is enabled), False otherwise
"""
try:
# Get project_id either from finding or kwargs
if finding:
project_id = finding.project_id
else:
project_id = kwargs.get("project_id")
if not project_id:
raise ValueError("project_id is required")
# Enable OS Login
request = compute_client.client.projects().setCommonInstanceMetadata(
project=project_id,
body={"items": [{"key": "enable-oslogin", "value": "TRUE"}]},
)
request.execute()
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
+5 -130
View File
@@ -1,11 +1,7 @@
import json
import shutil
import sys
import tempfile
from os import environ
from typing import List
from alive_progress import alive_bar
from checkov.ansible.runner import Runner as AnsibleRunner
from checkov.argo_workflows.runner import Runner as ArgoWorkflowsRunner
from checkov.arm.runner import Runner as ArmRunner
@@ -39,7 +35,6 @@ from checkov.terraform.runner import Runner as TerraformRunner
from checkov.terraform_json.runner import TerraformJsonRunner
from checkov.yaml_doc.runner import Runner as YamlDocRunner
from colorama import Fore, Style
from dulwich import porcelain
from prowler.config.config import (
default_config_file_path,
@@ -59,56 +54,21 @@ class IacProvider(Provider):
def __init__(
self,
scan_path: str = ".",
scan_repository_url: str = None,
frameworks: list[str] = ["all"],
exclude_path: list[str] = [],
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
github_username: str = None,
personal_access_token: str = None,
oauth_app_token: str = None,
):
logger.info("Instantiating IAC Provider...")
self.scan_path = scan_path
self.scan_repository_url = scan_repository_url
self.frameworks = frameworks
self.exclude_path = exclude_path
self.region = "global"
self.audited_account = "local-iac"
self._session = None
self._identity = "prowler"
self._auth_method = "No auth"
if scan_repository_url:
oauth_app_token = oauth_app_token or environ.get("GITHUB_OAUTH_APP_TOKEN")
github_username = github_username or environ.get("GITHUB_USERNAME")
personal_access_token = personal_access_token or environ.get(
"GITHUB_PERSONAL_ACCESS_TOKEN"
)
if oauth_app_token:
self.oauth_app_token = oauth_app_token
self.github_username = None
self.personal_access_token = None
self._auth_method = "OAuth App Token"
logger.info("Using OAuth App Token for GitHub authentication")
elif github_username and personal_access_token:
self.github_username = github_username
self.personal_access_token = personal_access_token
self.oauth_app_token = None
self._auth_method = "Personal Access Token"
logger.info(
"Using GitHub username and personal access token for authentication"
)
else:
self.github_username = None
self.personal_access_token = None
self.oauth_app_token = None
logger.debug(
"No GitHub authentication method provided; proceeding without authentication."
)
# Audit Config
if config_content:
@@ -137,10 +97,6 @@ class IacProvider(Provider):
Provider.set_global_provider(self)
@property
def auth_method(self):
return self._auth_method
@property
def type(self):
return self._type
@@ -227,72 +183,8 @@ class IacProvider(Provider):
)
sys.exit(1)
def _clone_repository(
self,
repository_url: str,
github_username: str = None,
personal_access_token: str = None,
oauth_app_token: str = None,
) -> str:
"""
Clone a git repository to a temporary directory, supporting GitHub authentication.
"""
try:
if github_username and personal_access_token:
repository_url = repository_url.replace(
"https://github.com/",
f"https://{github_username}:{personal_access_token}@github.com/",
)
elif oauth_app_token:
repository_url = repository_url.replace(
"https://github.com/",
f"https://oauth2:{oauth_app_token}@github.com/",
)
temporary_directory = tempfile.mkdtemp()
logger.info(
f"Cloning repository {repository_url} into {temporary_directory}..."
)
with alive_bar(
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Cloning {repository_url}..."
porcelain.clone(repository_url, temporary_directory, depth=1)
bar.title = "-> Repository cloned successfully!"
except Exception as clone_error:
bar.title = "-> Cloning failed!"
raise clone_error
return temporary_directory
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
def run(self) -> List[CheckReportIAC]:
temp_dir = None
if self.scan_repository_url:
scan_dir = temp_dir = self._clone_repository(
self.scan_repository_url,
getattr(self, "github_username", None),
getattr(self, "personal_access_token", None),
getattr(self, "oauth_app_token", None),
)
else:
scan_dir = self.scan_path
try:
reports = self.run_scan(scan_dir, self.frameworks, self.exclude_path)
finally:
if temp_dir:
logger.info(f"Removing temporary directory {temp_dir}...")
shutil.rmtree(temp_dir)
return reports
return self.run_scan(self.scan_path, self.frameworks, self.exclude_path)
def run_scan(
self, directory: str, frameworks: list[str], exclude_path: list[str]
@@ -357,32 +249,15 @@ class IacProvider(Provider):
sys.exit(1)
def print_credentials(self):
if self.scan_repository_url:
report_title = (
f"{Style.BRIGHT}Scanning remote IaC repository:{Style.RESET_ALL}"
)
report_lines = [
f"Repository: {Fore.YELLOW}{self.scan_repository_url}{Style.RESET_ALL}",
]
else:
report_title = (
f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
)
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
if self.exclude_path:
report_lines.append(
f"Excluded paths: {Fore.YELLOW}{', '.join(self.exclude_path)}{Style.RESET_ALL}"
)
report_lines.append(
f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}"
)
report_lines.append(
f"Authentication method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}"
)
report_title = f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
print_boxes(report_lines, report_title)
@@ -44,17 +44,8 @@ def init_parser(self):
"-P",
dest="scan_path",
default=".",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory. Mutually exclusive with --scan-repository-url.",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory",
)
iac_scan_subparser.add_argument(
"--scan-repository-url",
"-R",
dest="scan_repository_url",
default=None,
help="URL to the repository containing your infrastructure-as-code files. Mutually exclusive with --scan-path.",
)
iac_scan_subparser.add_argument(
"--frameworks",
"-f",
@@ -72,38 +63,3 @@ def init_parser(self):
default=[],
help="Comma-separated list of paths to exclude from the scan. Default: none",
)
iac_scan_subparser.add_argument(
"--github-username",
dest="github_username",
nargs="?",
default=None,
help="GitHub username for authenticated repository cloning (used with --personal-access-token). If not provided, will use GITHUB_USERNAME env var.",
)
iac_scan_subparser.add_argument(
"--personal-access-token",
dest="personal_access_token",
nargs="?",
default=None,
help="GitHub personal access token for authenticated repository cloning (used with --github-username). If not provided, will use GITHUB_PERSONAL_ACCESS_TOKEN env var.",
)
iac_scan_subparser.add_argument(
"--oauth-app-token",
dest="oauth_app_token",
nargs="?",
default=None,
help="GitHub OAuth app token for authenticated repository cloning. If not provided, will use GITHUB_OAUTH_APP_TOKEN env var.",
)
def validate_arguments(arguments):
scan_path = getattr(arguments, "scan_path", None)
scan_repository_url = getattr(arguments, "scan_repository_url", None)
if scan_path and scan_repository_url:
# If scan_path is set to default ("."), allow scan_repository_url
if scan_path != ".":
return (
False,
"--scan-path (-P) and --scan-repository-url (-R) are mutually exclusive. Please specify only one.",
)
return (True, "")
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_bind_address",
"CheckTitle": "Ensure that the --bind-address argument is set to 127.0.0.1",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_disable_profiling",
"CheckTitle": "Ensure that the --profiling argument is set to false",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_garbage_collection",
"CheckTitle": "Ensure that the --terminated-pod-gc-threshold argument is set as appropriate",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_root_ca_file_set",
"CheckTitle": "Ensure that the --root-ca-file argument is set as appropriate",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_rotate_kubelet_server_cert",
"CheckTitle": "Ensure that the RotateKubeletServerCertificate argument is set to true",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_service_account_credentials",
"CheckTitle": "Ensure that the --use-service-account-credentials argument is set to true",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_service_account_private_key_file",
"CheckTitle": "Ensure that the --service-account-private-key-file argument is set as appropriate",
"CheckType": [],
"ServiceName": "controllermanager",
"ServiceName": "controller-manager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "rbac_cluster_admin_usage",
"CheckTitle": "Ensure that the cluster-admin role is only used where required",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_csr_approval_access",
"CheckTitle": "Minimize access to the approval sub-resource of certificatesigningrequests objects",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_node_proxy_subresource_access",
"CheckTitle": "Minimize access to the proxy sub-resource of nodes",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_pod_creation_access",
"CheckTitle": "Minimize access to create pods",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_pv_creation_access",
"CheckTitle": "Minimize access to create persistent volumes",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_secret_access",
"CheckTitle": "Minimize access to secrets",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_service_account_token_creation",
"CheckTitle": "Minimize access to the service account token creation",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_webhook_config_access",
"CheckTitle": "Minimize access to webhook configuration objects",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_wildcard_use_roles",
"CheckTitle": "Minimize wildcard use in Roles and ClusterRoles",
"CheckType": [],
"ServiceName": "rbac",
"ServiceName": "RBAC",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
+68
View File
@@ -0,0 +1,68 @@
from typing import Optional
from colorama import Style
from prowler.config.config import orange_color
from prowler.lib.check.models import CheckReportM365
from prowler.lib.fix.fixer import Fixer
from prowler.lib.logger import logger
class M365Fixer(Fixer):
"""M365 specific fixer implementation"""
def __init__(
self,
description: str,
cost_impact: bool = False,
cost_description: Optional[str] = None,
service: str = "",
):
super().__init__(description, cost_impact, cost_description)
self.service = service
def _get_fixer_info(self):
"""Each fixer must define its metadata"""
fixer_info = super()._get_fixer_info()
fixer_info["service"] = self.service
return fixer_info
def fix(self, finding: Optional[CheckReportM365] = None, **kwargs) -> bool:
"""
M365 specific method to execute the fixer.
This method handles the printing of fixing status messages.
Args:
finding (Optional[CheckReportM365]): Finding to fix
**kwargs: Additional M365-specific arguments (resource_id)
Returns:
bool: True if fixing was successful, False otherwise
"""
try:
# Get values either from finding or kwargs
resource_id = None
if finding:
resource_id = (
finding.resource_id if hasattr(finding, "resource_id") else None
)
elif kwargs.get("resource_id"):
resource_id = kwargs.get("resource_id")
# Print the appropriate message based on available information
if resource_id:
print(
f"\t{orange_color}FIXING Resource {resource_id}...{Style.RESET_ALL}"
)
else:
# If no resource_id is provided, we'll still try to proceed
print(f"\t{orange_color}FIXING...{Style.RESET_ALL}")
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False

Some files were not shown because too many files have changed in this diff Show More