Merge branch 'master' into PRWLR-5956-Export-Artifacts-only

This commit is contained in:
Adrián Jesús Peña Rodríguez committed 2025-02-10 16:28:20 +01:00
commit 326fddd206
67 files changed
+1538 -209

No files matched your search

+5
View File
@@ -17,6 +17,11 @@ Please include a summary of the change and which issue is fixed. List any depend
- [ ] Review if backport is needed.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
#### API
- [ ] Verify if API specs need to be regenerated.
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
### License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.88.4
uses: trufflesecurity/trufflehog@v3.88.5
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+20
View File
@@ -0,0 +1,20 @@
# Prowler API Changelog
All notable changes to the **Prowler API** are documented in this file.
---
## [Unreleased]
---
## [v1.4.0] (Prowler v5.3.0) - 2025-02-10
### Changed
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700).
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800).
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863).
- Increase the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869).
---
+55 -3
View File
@@ -1,4 +1,4 @@
from datetime import date, datetime, timezone
from datetime import date, datetime, timedelta, timezone
from django.conf import settings
from django.db.models import Q
@@ -346,8 +346,14 @@ class FindingFilter(FilterSet):
inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date")
inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date")
inserted_at__gte = DateFilter(method="filter_inserted_at_gte")
inserted_at__lte = DateFilter(method="filter_inserted_at_lte")
inserted_at__gte = DateFilter(
method="filter_inserted_at_gte",
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
)
inserted_at__lte = DateFilter(
method="filter_inserted_at_lte",
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
)
class Meta:
model = Finding
@@ -375,6 +381,52 @@ class FindingFilter(FilterSet):
},
}
def filter_queryset(self, queryset):
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
self.data.get("inserted_at")
or self.data.get("inserted_at__date")
or self.data.get("inserted_at__gte")
or self.data.get("inserted_at__lte")
):
raise ValidationError(
[
{
"detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], "
"or filter[inserted_at.lte].",
"status": 400,
"source": {"pointer": "/data/attributes/inserted_at"},
"code": "required",
}
]
)
gte_date = (
datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date()
if self.data.get("inserted_at__gte")
else datetime.now(timezone.utc).date()
)
lte_date = (
datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date()
if self.data.get("inserted_at__lte")
else datetime.now(timezone.utc).date()
)
if abs(lte_date - gte_date) > timedelta(
days=settings.FINDINGS_MAX_DAYS_IN_RANGE
):
raise ValidationError(
[
{
"detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
"status": 400,
"source": {"pointer": "/data/attributes/inserted_at"},
"code": "invalid",
}
]
)
return super().filter_queryset(queryset)
# Convert filter values to UUIDv7 values for use with partitioning
def filter_scan_id(self, queryset, name, value):
try:
@@ -0,0 +1,22 @@
# Generated by Django 5.1.5 on 2025-02-07 09:42
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0008_daily_scheduled_tasks_update"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="uid",
field=models.CharField(
max_length=250,
validators=[django.core.validators.MinLengthValidator(3)],
verbose_name="Unique identifier for the provider, set by the provider",
),
),
]
+3 -3
View File
@@ -227,13 +227,13 @@ class Provider(RowLevelSecurityProtectedModel):
@staticmethod
def validate_kubernetes_uid(value):
if not re.match(
r"(^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$)|(^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$)",
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
value,
):
raise ModelValidationError(
detail="The value must either be a valid Kubernetes UID (up to 63 characters, "
"starting and ending with a lowercase letter or number, containing only "
"lowercase alphanumeric characters and hyphens) or a valid EKS ARN.",
"lowercase alphanumeric characters and hyphens) or a valid AWS EKS Cluster ARN, GCP GKE Context Name or Azure AKS Cluster Name.",
code="kubernetes-uid",
pointer="/data/attributes/uid",
)
@@ -247,7 +247,7 @@ class Provider(RowLevelSecurityProtectedModel):
)
uid = models.CharField(
"Unique identifier for the provider, set by the provider",
max_length=63,
max_length=250,
blank=False,
validators=[MinLengthValidator(3)],
)
+15 -3
View File
@@ -346,6 +346,9 @@ paths:
schema:
type: string
format: date
description: At least one of the variations of the `filter[inserted_at]` filter
must be provided.
required: true
- in: query
name: filter[inserted_at__date]
schema:
@@ -356,11 +359,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -861,11 +866,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -1275,6 +1282,9 @@ paths:
schema:
type: string
format: date
description: At least one of the variations of the `filter[inserted_at]` filter
must be provided.
required: true
- in: query
name: filter[inserted_at__date]
schema:
@@ -1285,11 +1295,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -7665,7 +7677,7 @@ components:
uid:
type: string
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
minLength: 3
alias:
type: string
@@ -7777,7 +7789,7 @@ components:
uid:
type: string
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
minLength: 3
required:
- uid
@@ -7821,7 +7833,7 @@ components:
type: string
minLength: 3
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
required:
- uid
required:
+58 -15
View File
@@ -5,6 +5,7 @@ from unittest.mock import ANY, Mock, patch
import jwt
import pytest
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
from django.conf import settings
from django.urls import reverse
from rest_framework import status
@@ -27,6 +28,12 @@ from api.rls import Tenant
TODAY = str(datetime.today().date())
def today_after_n_days(n_days: int) -> str:
return datetime.strftime(
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
)
@pytest.mark.django_db
class TestUserViewSet:
def test_users_list(self, authenticated_client, create_test_user):
@@ -878,6 +885,16 @@ class TestProviderViewSet:
"uid": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "kubernetes",
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
"alias": "EKS",
},
{
"provider": "kubernetes",
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
"alias": "GKE",
},
{
"provider": "azure",
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
@@ -2379,12 +2396,33 @@ class TestResourceViewSet:
@pytest.mark.django_db
class TestFindingViewSet:
def test_findings_list_none(self, authenticated_client):
response = authenticated_client.get(reverse("finding-list"))
response = authenticated_client.get(
reverse("finding-list"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
def test_findings_list(self, authenticated_client, findings_fixture):
def test_findings_list_no_date_filter(self, authenticated_client):
response = authenticated_client.get(reverse("finding-list"))
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "required"
def test_findings_date_range_too_large(self, authenticated_client):
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[inserted_at.lte]": today_after_n_days(
-(settings.FINDINGS_MAX_DAYS_IN_RANGE + 1)
),
},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "invalid"
def test_findings_list(self, authenticated_client, findings_fixture):
response = authenticated_client.get(
reverse("finding-list"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(findings_fixture)
assert (
@@ -2404,7 +2442,8 @@ class TestFindingViewSet:
self, include_values, expected_resources, authenticated_client, findings_fixture
):
response = authenticated_client.get(
reverse("finding-list"), {"include": include_values}
reverse("finding-list"),
{"include": include_values, "filter[inserted_at]": TODAY},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(findings_fixture)
@@ -2439,13 +2478,13 @@ class TestFindingViewSet:
("service.icontains", "ec", 1),
("inserted_at", "2024-01-01", 0),
("inserted_at.date", "2024-01-01", 0),
("inserted_at.gte", "2024-01-01", 2),
("inserted_at.gte", today_after_n_days(-1), 2),
(
"inserted_at.lte",
"2028-12-31",
today_after_n_days(1),
2,
), # TODO: To avoid having to modify this value and to ensure that the tests always work, we should set the time before the fixtures are inserted
("updated_at.lte", "2024-01-01", 0),
),
("updated_at.lte", today_after_n_days(-1), 0),
("resource_type.icontains", "prowler", 2),
# full text search on finding
("search", "dev-qa", 1),
@@ -2475,9 +2514,13 @@ class TestFindingViewSet:
filter_value,
expected_count,
):
filters = {f"filter[{filter_name}]": filter_value}
if "inserted_at" not in filter_name:
filters["filter[inserted_at]"] = TODAY
response = authenticated_client.get(
reverse("finding-list"),
{f"filter[{filter_name}]": filter_value},
filters,
)
assert response.status_code == status.HTTP_200_OK
@@ -2486,9 +2529,7 @@ class TestFindingViewSet:
def test_finding_filter_by_scan_id(self, authenticated_client, findings_fixture):
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[scan]": findings_fixture[0].scan.id,
},
{"filter[scan]": findings_fixture[0].scan.id},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
@@ -2511,6 +2552,7 @@ class TestFindingViewSet:
reverse("finding-list"),
{
"filter[provider]": findings_fixture[0].scan.provider.id,
"filter[inserted_at]": TODAY,
},
)
assert response.status_code == status.HTTP_200_OK
@@ -2525,7 +2567,8 @@ class TestFindingViewSet:
"filter[provider.in]": [
findings_fixture[0].scan.provider.id,
findings_fixture[1].scan.provider.id,
]
],
"filter[inserted_at]": TODAY,
},
)
assert response.status_code == status.HTTP_200_OK
@@ -2559,13 +2602,13 @@ class TestFindingViewSet:
)
def test_findings_sort(self, authenticated_client, sort_field):
response = authenticated_client.get(
reverse("finding-list"), {"sort": sort_field}
reverse("finding-list"), {"sort": sort_field, "filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
def test_findings_sort_invalid(self, authenticated_client):
response = authenticated_client.get(
reverse("finding-list"), {"sort": "invalid"}
reverse("finding-list"), {"sort": "invalid", "filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "invalid"
@@ -2633,7 +2676,7 @@ class TestFindingViewSet:
reverse("finding-metadata"),
{
"filter[severity__in]": ["low", "medium"],
"filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d"),
"filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"),
},
)
data = response.json()
+37 -39
View File
@@ -1352,6 +1352,14 @@ class ResourceViewSet(BaseRLSViewSet):
tags=["Finding"],
summary="List all findings",
description="Retrieve a list of all findings with options for filtering by various criteria.",
parameters=[
OpenApiParameter(
name="filter[inserted_at]",
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
required=True,
type=OpenApiTypes.DATE,
)
],
),
retrieve=extend_schema(
tags=["Finding"],
@@ -1369,6 +1377,14 @@ class ResourceViewSet(BaseRLSViewSet):
tags=["Finding"],
summary="Retrieve metadata values from findings",
description="Fetch unique metadata values from a set of findings. This is useful for dynamic filtering.",
parameters=[
OpenApiParameter(
name="filter[inserted_at]",
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
required=True,
type=OpenApiTypes.DATE,
)
],
filters=True,
),
)
@@ -1445,6 +1461,12 @@ class FindingViewSet(BaseRLSViewSet):
return queryset
def filter_queryset(self, queryset):
# Do not apply filters when retrieving specific finding
if self.action == "retrieve":
return queryset
return super().filter_queryset(queryset)
def inserted_at_to_uuidv7(self, inserted_at):
if inserted_at is None:
return None
@@ -1477,50 +1499,26 @@ class FindingViewSet(BaseRLSViewSet):
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
relevant_resources = Resource.objects.filter(
tenant_id=tenant_id, findings__in=filtered_queryset
).distinct()
filtered_ids = filtered_queryset.order_by().values("id")
services = (
relevant_resources.values_list("service", flat=True)
.distinct()
.order_by("service")
relevant_resources = Resource.all_objects.filter(
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
).only("service", "region", "type")
aggregation = relevant_resources.aggregate(
services=ArrayAgg("service", flat=True),
regions=ArrayAgg("region", flat=True),
resource_types=ArrayAgg("type", flat=True),
)
regions = (
relevant_resources.exclude(region="")
.values_list("region", flat=True)
.distinct()
.order_by("region")
)
resource_types = (
relevant_resources.values_list("type", flat=True)
.distinct()
.order_by("type")
)
# Temporarily disabled until we implement tag filtering in the UI
# tag_data = (
# relevant_resources
# .filter(tags__key__isnull=False, tags__value__isnull=False)
# .exclude(tags__key="")
# .exclude(tags__value="")
# .values("tags__key", "tags__value")
# .distinct()
# .order_by("tags__key", "tags__value")
# )
#
# tags_dict = {}
# for row in tag_data:
# k, v = row["tags__key"], row["tags__value"]
# tags_dict.setdefault(k, []).append(v)
services = sorted(set(aggregation["services"] or []))
regions = sorted({region for region in aggregation["regions"] or [] if region})
resource_types = sorted(set(aggregation["resource_types"] or []))
result = {
"services": list(services),
"regions": list(regions),
"resource_types": list(resource_types),
# "tags": tags_dict
"services": services,
"regions": regions,
"resource_types": resource_types,
}
serializer = self.get_serializer(data=result)
+1
View File
@@ -207,3 +207,4 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
TESTING = False
FINDINGS_MAX_DAYS_IN_RANGE = env.int("DJANGO_FINDINGS_MAX_DAYS_IN_RANGE", 7)
+2 -2
View File
@@ -532,8 +532,8 @@ def get_bar_graph(df, column_name):
# Cut the text if it is too long
for i in range(len(colums)):
if len(colums[i]) > 15:
colums[i] = colums[i][:15] + "..."
if len(colums[i]) > 43:
colums[i] = colums[i][:43] + "..."
fig = px.bar(
df,
+2 -2
View File
@@ -1720,7 +1720,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
[
html.P(
html.Strong(
"Recomendation: ",
"Recommendation: ",
style={
"margin-right": "5px"
},
@@ -1744,7 +1744,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
[
html.P(
html.Strong(
"RecomendationUrl: ",
"RecommendationUrl: ",
style={
"margin-right": "5px"
},
+14
View File
@@ -377,6 +377,19 @@ Go to [http://localhost:3000](http://localhost:3000) after installing the app (s
<img src="img/sign-up-button.png" alt="Sign Up Button" width="320"/>
<img src="img/sign-up.png" alt="Sign Up" width="285"/>
???+ note "User creation and default tenant behavior"
When creating a new user, the behavior depends on whether an invitation is provided:
- **Without an invitation**:
- A new tenant is automatically created.
- The new user is assigned to this tenant.
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly created tenant.
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
#### **Log In**
Log in with your email and password to start using the Prowler App.
@@ -535,6 +548,7 @@ prowler kubernetes --kubeconfig-file path
For in-cluster execution, you can use the supplied yaml to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
+1
View File
@@ -9,6 +9,7 @@ For in-cluster execution, you can use the supplied yaml files inside `/kubernete
They can be used to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
+26
View File
@@ -99,6 +99,32 @@ By default, the `kubeconfig` file is located at `~/.kube/config`.
<img src="../../img/kubernetes-credentials.png" alt="Kubernetes Credentials" width="700"/>
???+ note
If you are adding an **Amazon EKS** cluster, follow these additional steps to ensure proper authentication:
1. Apply the necessary Kubernetes resources to your EKS cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
```
2. Generate a long-lived token for authentication:
```console
kubectl create token prowler-sa -n prowler-ns --duration=0
```
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
3. Update your `kubeconfig` to use the ServiceAccount token:
```console
kubectl config set-credentials prowler-sa --token=<SA_TOKEN>
kubectl config set-context <CLUSTER_ARN> --user=prowler-sa
```
Replace <SA_TOKEN> with the generated token and <CLUSTER_ARN> with your EKS cluster ARN.
4. Now you can add the modified `kubeconfig` as the credentials of the AWS EKS Cluster in Prowler Cloud. Then simply test the connection.
---
## **Step 5: Test Connection**
-13
View File
@@ -1,16 +1,3 @@
apiVersion: v1
kind: Namespace
metadata:
name: prowler-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prowler-sa
namespace: prowler-ns
---
apiVersion: batch/v1
kind: Job
metadata:
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Namespace
metadata:
name: prowler-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prowler-sa
namespace: prowler-ns
Generated
+16 -16
View File
@@ -2122,13 +2122,13 @@ referencing = ">=0.31.0"
[[package]]
name = "kubernetes"
version = "31.0.0"
version = "32.0.0"
description = "Kubernetes python client"
optional = false
python-versions = ">=3.6"
files = [
{file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"},
{file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"},
{file = "kubernetes-32.0.0-py2.py3-none-any.whl", hash = "sha256:60fd8c29e8e43d9c553ca4811895a687426717deba9c0a66fb2dcc3f5ef96692"},
{file = "kubernetes-32.0.0.tar.gz", hash = "sha256:319fa840345a482001ac5d6062222daeb66ec4d1bcb3087402aed685adf0aecb"},
]
[package.dependencies]
@@ -2359,13 +2359,13 @@ files = [
[[package]]
name = "microsoft-kiota-abstractions"
version = "1.9.1"
version = "1.9.2"
description = "Core abstractions for kiota generated libraries in Python"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "microsoft_kiota_abstractions-1.9.1-py3-none-any.whl", hash = "sha256:aca6147946fc56167038faddda0a07026b0ec4e76666520586de7ac5c1233295"},
{file = "microsoft_kiota_abstractions-1.9.1.tar.gz", hash = "sha256:291f4f88bec5452b2c6bafed4927640d2c7d2f41b08c941ee786f6ecad3dcaa5"},
{file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"},
{file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"},
]
[package.dependencies]
@@ -2536,13 +2536,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd
[[package]]
name = "mkdocs-material"
version = "9.6.2"
version = "9.6.3"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.6.2-py3-none-any.whl", hash = "sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753"},
{file = "mkdocs_material-9.6.2.tar.gz", hash = "sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f"},
{file = "mkdocs_material-9.6.3-py3-none-any.whl", hash = "sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf"},
{file = "mkdocs_material-9.6.3.tar.gz", hash = "sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832"},
]
[package.dependencies]
@@ -2592,13 +2592,13 @@ test = ["pytest", "pytest-cov"]
[[package]]
name = "moto"
version = "5.0.27"
version = "5.0.28"
description = "A library that allows you to easily mock out tests based on AWS infrastructure"
optional = false
python-versions = ">=3.8"
files = [
{file = "moto-5.0.27-py3-none-any.whl", hash = "sha256:27042fd94c8def0166d9f2ae8d39d9488d4b3115542b5fca88566c0424549013"},
{file = "moto-5.0.27.tar.gz", hash = "sha256:6c123de7e0e5e6508a10c399ba3ecf2d5143f263f8e804fd4a7091941c3f5207"},
{file = "moto-5.0.28-py3-none-any.whl", hash = "sha256:2dfbea1afe3b593e13192059a1a7fc4b3cf7fdf92e432070c22346efa45aa0f0"},
{file = "moto-5.0.28.tar.gz", hash = "sha256:4d3437693411ec943c13c77de5b0b520c4b0a9ac850fead4ba2a54709e086e8b"},
]
[package.dependencies]
@@ -3454,13 +3454,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "py-ocsf-models"
version = "0.2.0"
version = "0.3.0"
description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/."
optional = false
python-versions = "<3.13,>=3.9"
files = [
{file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"},
{file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"},
{file = "py_ocsf_models-0.3.0-py3-none-any.whl", hash = "sha256:3d31e379be5e4271f7faf62dee9c36798559a1f7f98dff142c0e4cfdb35e291c"},
{file = "py_ocsf_models-0.3.0.tar.gz", hash = "sha256:ad46b7d9761b74010f06a894df2d9541989252b7ff738cd5c7edbf4283df2279"},
]
[package.dependencies]
@@ -5090,4 +5090,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.13"
content-hash = "c387413f38df1dc2286d8e16077fdccd2573943bf7cbdaeca2389be0ee0c339c"
content-hash = "b479cce4f4992dab6f81b8118082daad2fd301cf01cbced02c90bc132dd27285"
-2
View File
@@ -541,7 +541,6 @@ class Check_Report_Microsoft365(Check_Report):
resource_name: str
resource_id: str
tenant_id: str
location: str
def __init__(self, metadata: Dict, resource: Any) -> None:
@@ -556,7 +555,6 @@ class Check_Report_Microsoft365(Check_Report):
resource, "name", getattr(resource, "resource_name", "")
)
self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", ""))
self.tenant_id = getattr(resource, "tenant_id", "")
self.location = getattr(resource, "location", "global")
+1 -1
View File
@@ -205,7 +205,7 @@ class HTML(Output):
<th scope="col">Resource Tags</th>
<th scope="col">Status Extended</th>
<th scope="col">Risk</th>
<th scope="col">Recomendation</th>
<th scope="col">Recommendation</th>
<th scope="col">Compliance</th>
</tr>
</thead>
@@ -145,6 +145,7 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -158,6 +159,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -6782,6 +6784,7 @@
"mcs": {
"regions": {
"aws": [
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
@@ -7947,6 +7950,7 @@
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
@@ -7955,6 +7959,7 @@
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"sa-east-1",
@@ -9358,6 +9363,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1,4 +1,3 @@
import hashlib
import json
from prowler.lib.check.models import Check, Check_Report_AWS
@@ -28,19 +27,13 @@ class awslambda_function_no_secrets_in_variables(Check):
"detect_secrets_plugins",
),
)
original_env_vars = {}
original_env_vars = []
for name, value in function.environment.items():
original_env_vars.update(
{
hashlib.sha1( # nosec B324 SHA1 is used here for non-security-critical unique identifiers
value.encode("utf-8")
).hexdigest(): name
}
)
original_env_vars.append(name)
if detect_secrets_output:
secrets_string = ", ".join(
[
f"{secret['type']} in variable {original_env_vars[secret['hashed_secret']]}"
f"{secret['type']} in variable {original_env_vars[secret['line_number'] - 2]}"
for secret in detect_secrets_output
]
)
@@ -10,14 +10,14 @@ class awslambda_function_not_publicly_accessible(Check):
report = Check_Report_AWS(metadata=self.metadata(), resource=function)
report.status = "PASS"
report.status_extended = f"Lambda function {function.name} has a policy resource-based policy not public."
report.status_extended = f"Lambda function {function.name} has a resource-based policy without public access."
if is_policy_public(
function.policy,
awslambda_client.audited_account,
is_cross_account_allowed=True,
):
report.status = "FAIL"
report.status_extended = f"Lambda function {function.name} has a policy resource-based policy with public access."
report.status_extended = f"Lambda function {function.name} has a resource-based policy with public access."
findings.append(report)
@@ -14,14 +14,20 @@ class cloudfront_distributions_origin_traffic_encrypted(Check):
unencrypted_origins = []
for origin in distribution.origins:
if (
origin.origin_protocol_policy == ""
or origin.origin_protocol_policy == "http-only"
) or (
origin.origin_protocol_policy == "match-viewer"
and distribution.viewer_protocol_policy == "allow-all"
):
unencrypted_origins.append(origin.id)
if origin.s3_origin_config:
# For S3, only check the viewer protocol policy
if distribution.viewer_protocol_policy == "allow-all":
unencrypted_origins.append(origin.id)
else:
# Regular check for custom origins (ALB, EC2, API Gateway, etc.)
if (
origin.origin_protocol_policy == ""
or origin.origin_protocol_policy == "http-only"
) or (
origin.origin_protocol_policy == "match-viewer"
and distribution.viewer_protocol_policy == "allow-all"
):
unencrypted_origins.append(origin.id)
if unencrypted_origins:
report.status = "FAIL"
@@ -107,21 +107,45 @@ class DirectoryService(AWSService):
if directory.region == regional_client.region:
# Operation is not supported for Shared MicrosoftAD directories.
if directory.type != DirectoryType.SharedMicrosoftAD:
describe_event_topics_parameters = {"DirectoryId": directory.id}
event_topics = []
describe_event_topics = regional_client.describe_event_topics(
**describe_event_topics_parameters
)
for event_topic in describe_event_topics["EventTopics"]:
event_topics.append(
EventTopics(
topic_arn=event_topic["TopicArn"],
topic_name=event_topic["TopicName"],
status=event_topic["Status"],
created_date_time=event_topic["CreatedDateTime"],
try:
describe_event_topics_parameters = {
"DirectoryId": directory.id
}
event_topics = []
describe_event_topics = (
regional_client.describe_event_topics(
**describe_event_topics_parameters
)
)
self.directories[directory.id].event_topics = event_topics
for event_topic in describe_event_topics["EventTopics"]:
event_topics.append(
EventTopics(
topic_arn=event_topic["TopicArn"],
topic_name=event_topic["TopicName"],
status=event_topic["Status"],
created_date_time=event_topic[
"CreatedDateTime"
],
)
)
self.directories[directory.id].event_topics = event_topics
except ClientError as error:
if (
"is in Deleting state"
in error.response["Error"]["Message"]
):
logger.warning(
f"{directory.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -202,6 +226,15 @@ class DirectoryService(AWSService):
"SnapshotLimits"
]["ManualSnapshotsLimitReached"],
)
except ClientError as error:
if "is in Deleting state" in error.response["Error"]["Message"]:
logger.warning(
f"{directory.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -1,4 +1,3 @@
import hashlib
from json import dumps
from prowler.lib.check.models import Check, Check_Report_AWS
@@ -25,16 +24,10 @@ class ecs_task_definitions_no_environment_secrets(Check):
if container.environment:
dump_env_vars = {}
original_env_vars = {}
original_env_vars = []
for env_var in container.environment:
dump_env_vars.update({env_var.name: env_var.value})
original_env_vars.update(
{
hashlib.sha1( # nosec B324 SHA1 is used here for non-security-critical unique identifiers
env_var.value.encode("utf-8")
).hexdigest(): env_var.name
}
)
original_env_vars.append(env_var.name)
env_data = dumps(dump_env_vars, indent=2)
detect_secrets_output = detect_secrets_scan(
@@ -47,7 +40,7 @@ class ecs_task_definitions_no_environment_secrets(Check):
if detect_secrets_output:
secrets_string = ", ".join(
[
f"{secret['type']} on the environment variable {original_env_vars[secret['hashed_secret']]}"
f"{secret['type']} on the environment variable {original_env_vars[secret['line_number'] - 2]}"
for secret in detect_secrets_output
]
)
@@ -147,6 +147,12 @@ class ElastiCache(AWSService):
logger.warning(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except (
regional_client.exceptions.InvalidReplicationGroupStateFault
) as error:
logger.warning(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -163,6 +169,12 @@ class ElastiCache(AWSService):
logger.warning(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except (
regional_client.exceptions.InvalidReplicationGroupStateFault
) as error:
logger.warning(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
Whitespace-only changes.
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "kms_cmk_not_multi_region",
"CheckTitle": "AWS KMS customer managed keys should not be multi-Region",
"CheckType": [
"Data Protection"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:key/resource-id",
"Severity": "high",
"ResourceType": "AwsKmsKey",
"Description": "Ensure that AWS KMS customer managed keys (CMKs) are not multi-region to maintain strict data control and compliance with security best practices.",
"Risk": "Multi-region KMS keys can increase the risk of unauthorized access and data exposure, as managing access controls and auditing across multiple regions becomes more complex. This expanded attack surface may lead to compliance violations and data breaches.",
"RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#multi-region-concepts",
"Remediation": {
"Code": {
"CLI": "aws kms create-key --no-multi-region",
"NativeIaC": "",
"Other": "",
"Terraform": "resource \"aws_kms_key\" \"example\" { description = \"Single-region key\" multi_region = false }"
},
"Recommendation": {
"Text": "Identify and replace multi-region keys with single-region KMS keys to enhance security and access control.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/mrk-when-to-use.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Multi-region keys should be used only when absolutely necessary, such as for cross-region disaster recovery, and should be carefully managed with strict access controls."
}
@@ -0,0 +1,25 @@
from typing import List
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.kms.kms_client import kms_client
class kms_cmk_not_multi_region(Check):
"""kms_cmk_not_multi_region verifies if a KMS key is multi-regional"""
def execute(self) -> List[Check_Report_AWS]:
findings = []
for key in kms_client.keys:
if key.manager == "CUSTOMER" and key.state == "Enabled":
report = Check_Report_AWS(metadata=self.metadata(), resource=key)
report.status = "PASS"
report.status_extended = f"KMS CMK {key.id} is a single-region key."
if key.multi_region:
report.status = "FAIL"
report.status_extended = f"KMS CMK {key.id} is a multi-region key."
findings.append(report)
return findings
@@ -45,12 +45,18 @@ class KMS(AWSService):
logger.info("KMS - Describing Key...")
try:
for key in self.keys:
regional_client = self.regional_clients[key.region]
response = regional_client.describe_key(KeyId=key.id)
key.state = response["KeyMetadata"]["KeyState"]
key.origin = response["KeyMetadata"]["Origin"]
key.manager = response["KeyMetadata"]["KeyManager"]
key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"]
try:
regional_client = self.regional_clients[key.region]
response = regional_client.describe_key(KeyId=key.id)
key.state = response["KeyMetadata"]["KeyState"]
key.origin = response["KeyMetadata"]["Origin"]
key.manager = response["KeyMetadata"]["KeyManager"]
key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"]
key.multi_region = response["KeyMetadata"]["MultiRegion"]
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
@@ -121,4 +127,5 @@ class Key(BaseModel):
policy: Optional[dict]
spec: Optional[str]
region: str
multi_region: Optional[bool]
tags: Optional[list] = []
@@ -15,7 +15,7 @@ class entra_security_defaults_enabled(Check):
)
report.subscription = f"Tenant: {tenant}"
report.status = "FAIL"
report.status_extended = "Entra security defaults is diabled."
report.status_extended = "Entra security defaults is disabled."
if getattr(security_default, "is_enabled", False):
report.status = "PASS"
@@ -95,7 +95,7 @@ class Rbac(KubernetesService):
"resources": rule.resources,
"verbs": rule.verbs,
}
for rule in role.rules
for rule in (role.rules or [])
],
}
roles[role.metadata.uid] = Role(**formatted_role)
@@ -50,9 +50,7 @@ from prowler.providers.microsoft365.exceptions.exceptions import (
Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError,
)
from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist
from prowler.providers.microsoft365.lib.regions.microsoft365_regions import (
get_regions_config,
)
from prowler.providers.microsoft365.lib.regions.regions import get_regions_config
from prowler.providers.microsoft365.models import (
Microsoft365IdentityInfo,
Microsoft365RegionConfig,
@@ -22,11 +22,13 @@ class AdminCenter(Microsoft365Service):
gather(
self._get_directory_roles(),
self._get_groups(),
self._get_domains(),
)
)
self.directory_roles = attributes[0]
self.groups = attributes[1]
self.domains = attributes[2]
async def _get_users(self):
logger.info("Microsoft365 - Getting users...")
@@ -130,6 +132,28 @@ class AdminCenter(Microsoft365Service):
)
return groups
async def _get_domains(self):
logger.info("Microsoft365 - Getting domains...")
domains = {}
try:
domains_list = await self.client.domains.get()
domains.update({})
for domain in domains_list.value:
domains.update(
{
domain.id: Domain(
id=domain.id,
password_validity_period=domain.password_validity_period_in_days,
)
}
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return domains
class User(BaseModel):
id: str
@@ -149,3 +173,8 @@ class Group(BaseModel):
id: str
name: str
visibility: str
class Domain(BaseModel):
id: str
password_validity_period: int
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "admincenter_settings_password_never_expire",
"CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'",
"CheckType": [],
"ServiceName": "admincenter",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft365Domain",
"Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.",
"Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.",
"RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide",
"Remediation": {
"Code": {
"CLI": "Set-MsolUser -UserPrincipalName <user> -PasswordNeverExpires $true",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.",
"Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,26 @@
from prowler.lib.check.models import Check, Check_Report_Microsoft365
from prowler.providers.microsoft365.services.admincenter.admincenter_client import (
admincenter_client,
)
class admincenter_settings_password_never_expire(Check):
def execute(self) -> Check_Report_Microsoft365:
findings = []
for domain in admincenter_client.domains.values():
report = Check_Report_Microsoft365(self.metadata(), resource=domain)
report.resource_name = domain.id
report.status = "FAIL"
report.status_extended = (
f"Domain {domain.id} does not have a Password never expires policy."
)
if domain.password_validity_period == 2147483647:
report.status = "PASS"
report.status_extended = (
f"Domain {domain.id} Password policy is set to never expire."
)
findings.append(report)
return findings
@@ -9,9 +9,7 @@ class admincenter_users_between_two_and_four_global_admins(Check):
findings = []
directory_roles = admincenter_client.directory_roles
report = Check_Report_Microsoft365(
metadata=self.metadata(), resource=admincenter_client.directory_roles
)
report = Check_Report_Microsoft365(metadata=self.metadata(), resource={})
report.status = "FAIL"
report.resource_name = "Global Administrator"
Whitespace-only changes.
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.microsoft365.services.entra.entra_service import Entra
entra_client = Entra(Provider.get_global_provider())
@@ -0,0 +1,101 @@
from asyncio import gather, get_event_loop
from typing import List, Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.microsoft365.lib.service.service import Microsoft365Service
from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider
class Entra(Microsoft365Service):
def __init__(self, provider: Microsoft365Provider):
super().__init__(provider)
loop = get_event_loop()
attributes = loop.run_until_complete(
gather(
self._get_authorization_policy(),
)
)
self.authorization_policy = attributes[0]
async def _get_authorization_policy(self):
logger.info("Entra - Getting authorization policy...")
authorization_policy = None
try:
auth_policy = await self.client.policies.authorization_policy.get()
default_user_role_permissions = getattr(
auth_policy, "default_user_role_permissions", None
)
authorization_policy = AuthorizationPolicy(
id=auth_policy.id,
name=auth_policy.display_name,
description=auth_policy.description,
default_user_role_permissions=DefaultUserRolePermissions(
allowed_to_create_apps=getattr(
default_user_role_permissions,
"allowed_to_create_apps",
None,
),
allowed_to_create_security_groups=getattr(
default_user_role_permissions,
"allowed_to_create_security_groups",
None,
),
allowed_to_create_tenants=getattr(
default_user_role_permissions,
"allowed_to_create_tenants",
None,
),
allowed_to_read_bitlocker_keys_for_owned_device=getattr(
default_user_role_permissions,
"allowed_to_read_bitlocker_keys_for_owned_device",
None,
),
allowed_to_read_other_users=getattr(
default_user_role_permissions,
"allowed_to_read_other_users",
None,
),
odata_type=getattr(
default_user_role_permissions, "odata_type", None
),
permission_grant_policies_assigned=[
policy_assigned
for policy_assigned in getattr(
default_user_role_permissions,
"permission_grant_policies_assigned",
[],
)
],
),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return authorization_policy
class DefaultUserRolePermissions(BaseModel):
allowed_to_create_apps: Optional[bool]
allowed_to_create_security_groups: Optional[bool]
allowed_to_create_tenants: Optional[bool]
allowed_to_read_bitlocker_keys_for_owned_device: Optional[bool]
allowed_to_read_other_users: Optional[bool]
odata_type: Optional[str]
permission_grant_policies_assigned: Optional[List[str]] = None
class AuthorizationPolicy(BaseModel):
id: str
name: str
description: str
default_user_role_permissions: Optional[DefaultUserRolePermissions]
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_thirdparty_integrated_apps_not_allowed",
"CheckTitle": "Ensure third party integrated applications are not allowed",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "",
"Description": "Require administrators or appropriately delegated users to register third-party applications.",
"Risk": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Azure Active Directory data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-microsoft-entra-instance",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "1. From Entra select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Ensure that Users can register applications is set to No",
"Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data."
}
@@ -0,0 +1,26 @@
from prowler.lib.check.models import Check, Check_Report_Microsoft365
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
class entra_thirdparty_integrated_apps_not_allowed(Check):
def execute(self) -> Check_Report_Microsoft365:
findings = []
auth_policy = entra_client.authorization_policy
report = Check_Report_Microsoft365(self.metadata(), auth_policy)
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status = "FAIL"
report.status_extended = "App creation is not disabled for non-admin users."
if getattr(auth_policy, "default_user_role_permissions", None) and not getattr(
auth_policy.default_user_role_permissions,
"allowed_to_create_apps",
True,
):
report.status = "PASS"
report.status_extended = "App creation is disabled for non-admin users."
findings.append(report)
return findings
+5 -5
View File
@@ -58,12 +58,12 @@ detect-secrets = "1.5.0"
google-api-python-client = "2.160.0"
google-auth-httplib2 = ">=0.1,<0.3"
jsonschema = "4.23.0"
kubernetes = "31.0.0"
microsoft-kiota-abstractions = "1.9.1"
kubernetes = "32.0.0"
microsoft-kiota-abstractions = "1.9.2"
msgraph-sdk = "1.18.0"
numpy = "2.0.2"
pandas = "2.2.3"
py-ocsf-models = "0.2.0"
py-ocsf-models = "0.3.0"
pydantic = "1.10.18"
python = ">=3.9,<3.13"
python-dateutil = "^2.9.0.post0"
@@ -82,7 +82,7 @@ docker = "7.1.0"
flake8 = "7.1.1"
freezegun = "1.5.1"
mock = "5.1.0"
moto = {extras = ["all"], version = "5.0.27"}
moto = {extras = ["all"], version = "5.0.28"}
openapi-schema-validator = "0.6.3"
openapi-spec-validator = "0.7.1"
pylint = "3.3.4"
@@ -100,7 +100,7 @@ optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "1.6.1"
mkdocs-git-revision-date-localized-plugin = "1.3.0"
mkdocs-material = "9.6.2"
mkdocs-material = "9.6.3"
mkdocs-material-extensions = "1.3.1"
[tool.poetry.scripts]
+1 -1
View File
@@ -339,7 +339,7 @@ def get_aws_html_header(args: list) -> str:
<th scope="col">Resource Tags</th>
<th scope="col">Status Extended</th>
<th scope="col">Risk</th>
<th scope="col">Recomendation</th>
<th scope="col">Recommendation</th>
<th scope="col">Compliance</th>
</tr>
</thead>
+1 -1
View File
@@ -175,7 +175,7 @@ class TestOCSF:
"vendor_name": "Prowler",
"version": prowler_version,
},
"version": "1.3.0",
"version": "1.4.0",
"profiles": ["cloud", "datetime"],
"tenant_uid": "test-organization-id",
},
@@ -76,7 +76,7 @@ class Test_awslambda_function_no_secrets_in_variables:
)
assert result[0].resource_tags == []
def test_function_secrets_in_variables(self):
def test_function_secrets_in_keyword(self):
lambda_client = mock.MagicMock
function_name = "test-lambda"
function_runtime = "nodejs4.3"
@@ -121,6 +121,51 @@ class Test_awslambda_function_no_secrets_in_variables:
)
assert result[0].resource_tags == []
def test_function_secrets_in_keyword_and_variable(self):
lambda_client = mock.MagicMock
function_name = "test-lambda"
function_runtime = "nodejs4.3"
function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}"
lambda_client.audit_config = {"secrets_ignore_patterns": []}
lambda_client.functions = {
"function_name": Function(
name=function_name,
security_groups=[],
arn=function_arn,
region=AWS_REGION_US_EAST_1,
runtime=function_runtime,
environment={"db_password": "srv://admin:pass@db"},
)
}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider(),
), mock.patch(
"prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.awslambda_client",
new=lambda_client,
):
# Test Check
from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables import (
awslambda_function_no_secrets_in_variables,
)
check = awslambda_function_no_secrets_in_variables()
result = check.execute()
assert len(result) == 1
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_id == function_name
assert result[0].resource_arn == function_arn
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Potential secret found in Lambda function {function_name} variables -> Secret Keyword in variable db_password, Basic Auth Credentials in variable db_password."
)
assert result[0].resource_tags == []
def test_function_secrets_in_variables_telegram_token(self):
lambda_client = mock.MagicMock
function_name = "test-lambda"
@@ -107,7 +107,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy with public access."
== f"Lambda function {function_name} has a resource-based policy with public access."
)
assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}]
@@ -184,7 +184,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy not public."
== f"Lambda function {function_name} has a resource-based policy without public access."
)
assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}]
@@ -260,7 +260,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy not public."
== f"Lambda function {function_name} has a resource-based policy without public access."
)
assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}]
@@ -319,7 +319,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy with public access."
== f"Lambda function {function_name} has a resource-based policy with public access."
)
assert result[0].resource_tags == []
@@ -492,7 +492,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Lambda function test-public-lambda has a policy resource-based policy with public access."
== "Lambda function test-public-lambda has a resource-based policy with public access."
)
assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}]
@@ -552,7 +552,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy not public."
== f"Lambda function {function_name} has a resource-based policy without public access."
)
assert result[0].resource_tags == []
@@ -612,7 +612,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy not public."
== f"Lambda function {function_name} has a resource-based policy without public access."
)
assert result[0].resource_tags == []
@@ -681,7 +681,7 @@ class Test_awslambda_function_not_publicly_accessible:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Lambda function {function_name} has a policy resource-based policy with public access."
== f"Lambda function {function_name} has a resource-based policy with public access."
)
assert result[0].resource_id == function_name
assert result[0].resource_arn == function_arn
@@ -225,3 +225,54 @@ class Test_cloudfront_distributions_origin_traffic_encrypted:
== f"CloudFront Distribution {DISTRIBUTION_ID} does encrypt traffic to custom origins."
)
assert result[0].resource_tags == []
def test_distribution_traffic_encrypted_with_s3_config(self):
cloudfront_client = mock.MagicMock
cloudfront_client.distributions = {
DISTRIBUTION_ID: Distribution(
arn=DISTRIBUTION_ARN,
id=DISTRIBUTION_ID,
region=REGION,
origins=[
Origin(
id="origin1",
domain_name="asdf.s3.us-east-1.amazonaws.com",
origin_protocol_policy="",
origin_ssl_protocols=[],
s3_origin_config={
"OriginAccessIdentity": "origin-access-identity/cloudfront/1234567890123456"
},
)
],
default_cache_config=DefaultCacheConfigBehaviour(
realtime_log_config_arn="",
viewer_protocol_policy=ViewerProtocolPolicy.redirect_to_https,
field_level_encryption_id="",
),
default_root_object="index.html",
viewer_protocol_policy="redirect-to-https",
)
}
with mock.patch(
"prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront",
new=cloudfront_client,
):
# Test Check
from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import (
cloudfront_distributions_origin_traffic_encrypted,
)
check = cloudfront_distributions_origin_traffic_encrypted()
result = check.execute()
assert len(result) == 1
assert result[0].region == REGION
assert result[0].resource_arn == DISTRIBUTION_ARN
assert result[0].resource_id == DISTRIBUTION_ID
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"CloudFront Distribution {DISTRIBUTION_ID} does encrypt traffic to custom origins."
)
assert result[0].resource_tags == []
@@ -2,7 +2,6 @@ from unittest.mock import patch
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
TASK_NAME = "test-task"
@@ -10,8 +9,10 @@ TASK_REVISION = "1"
CONTAINER_NAME = "test-container"
ENV_VAR_NAME_NO_SECRETS = "host"
ENV_VAR_VALUE_NO_SECRETS = "localhost:1234"
ENV_VAR_NAME_WITH_SECRETS = "DB_PASSWORD"
ENV_VAR_VALUE_WITH_SECRETS = "pass-12343"
ENV_VAR_NAME_WITH_KEYWORD = "DB_PASSWORD"
ENV_VAR_VALUE_WITH_SECRETS = "srv://admin:pass@db"
ENV_VAR_NAME_WITH_KEYWORD2 = "DATABASE_PASSWORD"
ENV_VAR_VALUE_WITH_SECRETS2 = "srv://admin:password@database"
class Test_ecs_task_definitions_no_environment_secrets:
@@ -88,7 +89,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
assert result[0].resource_tags == []
@mock_aws
def test_container_env_var_with_secrets(self):
def test_container_env_var_with_secret(self):
ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1)
task_arn = ecs_client.register_task_definition(
@@ -103,7 +104,7 @@ class Test_ecs_task_definitions_no_environment_secrets:
"user": "appuser",
"environment": [
{
"name": ENV_VAR_NAME_WITH_SECRETS,
"name": ENV_VAR_NAME_NO_SECRETS,
"value": ENV_VAR_VALUE_WITH_SECRETS,
}
],
@@ -115,6 +116,58 @@ class Test_ecs_task_definitions_no_environment_secrets:
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
), patch(
"prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client",
new=ECS(mocked_aws_provider),
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import (
ecs_task_definitions_no_environment_secrets,
)
check = ecs_task_definitions_no_environment_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Basic Auth Credentials on the environment variable host."
)
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
assert result[0].resource_arn == task_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_container_env_var_with_keyword(self):
ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1)
task_arn = ecs_client.register_task_definition(
family=TASK_NAME,
containerDefinitions=[
{
"name": CONTAINER_NAME,
"image": "ubuntu",
"memory": 128,
"readonlyRootFilesystem": True,
"privileged": False,
"user": "appuser",
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_NO_SECRETS,
}
],
}
],
)["taskDefinition"]["taskDefinitionArn"]
from prowler.providers.aws.services.ecs.ecs_service import ECS
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
@@ -138,3 +191,167 @@ class Test_ecs_task_definitions_no_environment_secrets:
assert result[0].resource_arn == task_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_container_env_var_with_keyword_and_secret(self):
ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1)
task_arn = ecs_client.register_task_definition(
family=TASK_NAME,
containerDefinitions=[
{
"name": CONTAINER_NAME,
"image": "ubuntu",
"memory": 128,
"readonlyRootFilesystem": True,
"privileged": False,
"user": "appuser",
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_WITH_SECRETS,
}
],
}
],
)["taskDefinition"]["taskDefinitionArn"]
from prowler.providers.aws.services.ecs.ecs_service import ECS
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
), patch(
"prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client",
new=ECS(mocked_aws_provider),
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import (
ecs_task_definitions_no_environment_secrets,
)
check = ecs_task_definitions_no_environment_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD."
)
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
assert result[0].resource_arn == task_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_container_multiple_env_vars_with_keyword_and_secret(self):
ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1)
task_arn = ecs_client.register_task_definition(
family=TASK_NAME,
containerDefinitions=[
{
"name": CONTAINER_NAME,
"image": "ubuntu",
"memory": 128,
"readonlyRootFilesystem": True,
"privileged": False,
"user": "appuser",
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_WITH_SECRETS,
},
{
"name": ENV_VAR_NAME_NO_SECRETS,
"value": ENV_VAR_VALUE_WITH_SECRETS2,
},
],
}
],
)["taskDefinition"]["taskDefinitionArn"]
from prowler.providers.aws.services.ecs.ecs_service import ECS
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
), patch(
"prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client",
new=ECS(mocked_aws_provider),
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import (
ecs_task_definitions_no_environment_secrets,
)
check = ecs_task_definitions_no_environment_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable host."
)
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
assert result[0].resource_arn == task_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_container_all_env_vars_with_keyword_and_secret(self):
ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1)
task_arn = ecs_client.register_task_definition(
family=TASK_NAME,
containerDefinitions=[
{
"name": CONTAINER_NAME,
"image": "ubuntu",
"memory": 128,
"readonlyRootFilesystem": True,
"privileged": False,
"user": "appuser",
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_WITH_SECRETS,
},
{
"name": ENV_VAR_NAME_WITH_KEYWORD2,
"value": ENV_VAR_VALUE_WITH_SECRETS2,
},
],
}
],
)["taskDefinition"]["taskDefinitionArn"]
from prowler.providers.aws.services.ecs.ecs_service import ECS
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
), patch(
"prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client",
new=ECS(mocked_aws_provider),
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import (
ecs_task_definitions_no_environment_secrets,
)
check = ecs_task_definitions_no_environment_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DATABASE_PASSWORD, Secret Keyword on the environment variable DATABASE_PASSWORD."
)
assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}"
assert result[0].resource_arn == task_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
Whitespace-only changes.
@@ -0,0 +1,142 @@
from unittest import mock
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
class Test_kms_cmk_not_multi_region:
@mock_aws
def test_kms_no_keys(self) -> None:
from prowler.providers.aws.services.kms.kms_service import KMS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client",
new=KMS(aws_provider),
),
):
from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import (
kms_cmk_not_multi_region,
)
check = kms_cmk_not_multi_region()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_kms_cmk_disabled_key(self) -> None:
from prowler.providers.aws.services.kms.kms_service import KMS
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
key = kms_client.create_key()["KeyMetadata"]
kms_client.disable_key(KeyId=key["KeyId"])
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client",
new=KMS(aws_provider),
),
):
from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import (
kms_cmk_not_multi_region,
)
check = kms_cmk_not_multi_region()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_kms_cmk_is_multi_regional(self) -> None:
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
key = kms_client.create_key(MultiRegion=True)["KeyMetadata"]
# The Prowler service import MUST be made within the decorated
# code not to make real API calls to the AWS service.
from prowler.providers.aws.services.kms.kms_service import KMS
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client",
new=KMS(aws_provider),
),
):
from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import (
kms_cmk_not_multi_region,
)
check = kms_cmk_not_multi_region()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"KMS CMK {key['KeyId']} is a multi-region key."
)
assert result[0].resource_id == key["KeyId"]
assert result[0].resource_arn == key["Arn"]
@mock_aws
def test_kms_cmk_is_single_regional(self) -> None:
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
key = kms_client.create_key(MultiRegion=False)["KeyMetadata"]
# The Prowler service import MUST be made within the decorated
# code not to make real API calls to the AWS service.
from prowler.providers.aws.services.kms.kms_service import KMS
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client",
new=KMS(aws_provider),
),
):
from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import (
kms_cmk_not_multi_region,
)
check = kms_cmk_not_multi_region()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"KMS CMK {key['KeyId']} is a single-region key."
)
assert result[0].resource_id == key["KeyId"]
assert result[0].resource_arn == key["Arn"]
@@ -11,7 +11,7 @@ from tests.providers.aws.utils import (
)
class Test_ACM_Service:
class Test_KMS_Service:
# Test KMS Service
@mock_aws
@@ -68,6 +68,7 @@ class Test_ACM_Service:
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
# Create KMS keys
key1 = kms_client.create_key(
MultiRegion=False,
Tags=[
{"TagKey": "test", "TagValue": "test"},
],
@@ -80,6 +81,7 @@ class Test_ACM_Service:
assert kms.keys[0].state == key1["KeyState"]
assert kms.keys[0].origin == key1["Origin"]
assert kms.keys[0].manager == key1["KeyManager"]
assert kms.keys[0].multi_region == key1["MultiRegion"]
assert kms.keys[0].tags == [
{"TagKey": "test", "TagValue": "test"},
]
@@ -90,8 +92,8 @@ class Test_ACM_Service:
# Generate KMS Client
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
# Create KMS keys
key1 = kms_client.create_key()["KeyMetadata"]
key2 = kms_client.create_key()["KeyMetadata"]
key1 = kms_client.create_key(MultiRegion=False)["KeyMetadata"]
key2 = kms_client.create_key(MultiRegion=False)["KeyMetadata"]
kms_client.enable_key_rotation(KeyId=key2["KeyId"])
# KMS client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
@@ -138,8 +140,12 @@ class Test_ACM_Service:
# Generate KMS Client
kms_client = client("kms", region_name=AWS_REGION_US_EAST_1)
# Create KMS keys
key1 = kms_client.create_key(Policy=default_policy)["KeyMetadata"]
key2 = kms_client.create_key(Policy=public_policy)["KeyMetadata"]
key1 = kms_client.create_key(MultiRegion=False, Policy=default_policy)[
"KeyMetadata"
]
key2 = kms_client.create_key(MultiRegion=False, Policy=public_policy)[
"KeyMetadata"
]
# KMS client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
kms = KMS(aws_provider)
@@ -45,7 +45,7 @@ class Test_entra_security_defaults_enabled:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Entra security defaults is diabled."
assert result[0].status_extended == "Entra security defaults is disabled."
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == ""
assert result[0].resource_id == ""
@@ -109,7 +109,7 @@ class Test_entra_security_defaults_enabled:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Entra security defaults is diabled."
assert result[0].status_extended == "Entra security defaults is disabled."
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Sec Default"
assert result[0].resource_id == id
@@ -1,6 +1,6 @@
from azure.identity import AzureAuthorityHosts
from prowler.providers.microsoft365.lib.regions.microsoft365_regions import (
from prowler.providers.microsoft365.lib.regions.regions import (
MICROSOFT365_CHINA_CLOUD,
MICROSOFT365_GENERIC_CLOUD,
MICROSOFT365_US_GOV_CLOUD,
@@ -29,7 +29,6 @@ def set_mocked_microsoft365_provider(
audit_config: dict = None,
azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(),
) -> Microsoft365Provider:
provider = MagicMock()
provider.type = "microsoft365"
provider.session.credentials = credentials
@@ -212,7 +212,6 @@ class TestMicrosoft365Provider:
"prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient"
) as mock_graph_client,
):
# Mock the return value of DefaultAzureCredential
mock_credentials = MagicMock()
mock_credentials.get_token.return_value = AccessToken(
@@ -271,7 +270,6 @@ class TestMicrosoft365Provider:
with patch(
"prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session"
) as mock_setup_session:
mock_setup_session.side_effect = Microsoft365HTTPResponseError(
file="test_file", original_exception="Simulated HttpResponseError"
)
@@ -0,0 +1,112 @@
from unittest import mock
from uuid import uuid4
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_admincenter_settings_password_never_expire:
def test_admincenter_no_domains(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
admincenter_client.domains = {}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 0
def test_admincenter_domain_password_expire(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_service import (
Domain,
)
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
id_domain = str(uuid4())
admincenter_client.domains = {
id_domain: Domain(id=id_domain, password_validity_period=5),
}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Domain {id_domain} does not have a Password never expires policy."
)
assert result[0].resource_name == id_domain
assert result[0].resource_id == id_domain
def test_admincenter_password_not_expire(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.microsoft365.services.admincenter.admincenter_service import (
Domain,
)
from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import (
admincenter_settings_password_never_expire,
)
id_domain = str(uuid4())
admincenter_client.domains = {
id_domain: Domain(id=id_domain, password_validity_period=2147483647),
}
check = admincenter_settings_password_never_expire()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Domain {id_domain} Password policy is set to never expire."
)
assert result[0].resource_name == id_domain
assert result[0].resource_id == id_domain
@@ -0,0 +1,127 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.microsoft365.services.entra.entra_service import (
DefaultUserRolePermissions,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_entra_thirdparty_integrated_apps_not_allowed:
def test_entra_no_authorization_policy(self):
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import (
entra_thirdparty_integrated_apps_not_allowed,
)
entra_client.authorization_policy = None
check = entra_thirdparty_integrated_apps_not_allowed()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
result[0].status_extended
== "App creation is not disabled for non-admin users."
)
def test_entra_default_user_role_permissions_not_allowed_to_create_apps(self):
id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_service import (
AuthorizationPolicy,
)
from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import (
entra_thirdparty_integrated_apps_not_allowed,
)
role_permissions = DefaultUserRolePermissions(allowed_to_create_apps=False)
entra_client.authorization_policy = AuthorizationPolicy(
id=id,
name="Test",
description="Test",
default_user_role_permissions=role_permissions,
)
check = entra_thirdparty_integrated_apps_not_allowed()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "App creation is disabled for non-admin users."
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
def test_entra_default_user_role_permissions_allowed_to_create_apps(self):
id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_service import (
AuthorizationPolicy,
)
from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import (
entra_thirdparty_integrated_apps_not_allowed,
)
role_permissions = DefaultUserRolePermissions(allowed_to_create_apps=True)
entra_client.authorization_policy = AuthorizationPolicy(
id=id,
name="Test",
description="Test",
default_user_role_permissions=role_permissions,
)
check = entra_thirdparty_integrated_apps_not_allowed()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "App creation is not disabled for non-admin users."
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
@@ -0,0 +1,45 @@
from unittest.mock import patch
from prowler.providers.microsoft365.models import Microsoft365IdentityInfo
from prowler.providers.microsoft365.services.entra.entra_service import (
AuthorizationPolicy,
Entra,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
async def mock_entra_get_authorization_policy(_):
return {
"id-1": AuthorizationPolicy(
id="id-1",
name="Name 1",
description="Description 1",
default_user_role_permissions=None,
)
}
@patch(
"prowler.providers.microsoft365.services.entra.entra_service.Entra._get_authorization_policy",
new=mock_entra_get_authorization_policy,
)
class Test_Entra_Service:
def test_get_client(self):
admincenter_client = Entra(
set_mocked_microsoft365_provider(
identity=Microsoft365IdentityInfo(tenant_domain=DOMAIN)
)
)
assert admincenter_client.client.__class__.__name__ == "GraphServiceClient"
def test_get_authorization_policy(self):
entra_client = Entra(set_mocked_microsoft365_provider())
assert entra_client.authorization_policy["id-1"].id == "id-1"
assert entra_client.authorization_policy["id-1"].name == "Name 1"
assert entra_client.authorization_policy["id-1"].description == "Description 1"
assert not entra_client.authorization_policy[
"id-1"
].default_user_role_permissions
+50 -12
View File
@@ -1,4 +1,5 @@
import { Spacer } from "@nextui-org/react";
import { format, subDays } from "date-fns";
import React, { Suspense } from "react";
import { getFindings, getMetadataInfo } from "@/actions/findings";
@@ -31,7 +32,21 @@ export default async function Findings({
// Make sure the sort is correctly encoded
const encodedSort = sort?.replace(/^\+/, "");
const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd");
// Check if the searchParams contain any date or scan filter
const hasDateOrScanFilter = Object.keys(searchParams).some(
(key) => key.includes("inserted_at") || key.includes("scan__in"),
);
// Default filters for getMetadataInfo
const defaultFilters: Record<string, string> = hasDateOrScanFilter
? {} // Do not apply default filters if there are date or scan filters
: { "filter[inserted_at__gte]": twoDaysAgo };
// Extract all filter parameters and combine with default filters
const filters: Record<string, string> = {
...defaultFilters,
...Object.fromEntries(
Object.entries(searchParams)
.filter(([key]) => key.startsWith("filter["))
@@ -44,11 +59,15 @@ export default async function Findings({
const query = filters["filter[search]"] || "";
const metadataInfoData = await getMetadataInfo({
query,
sort: encodedSort,
filters,
});
const [metadataInfoData, providersData, scansData] = await Promise.all([
getMetadataInfo({
query,
sort: encodedSort,
filters,
}),
getProviders({}),
getScans({}),
]);
// Extract unique regions and services from the new endpoint
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
@@ -56,8 +75,6 @@ export default async function Findings({
const uniqueResourceTypes =
metadataInfoData?.data?.attributes?.resource_types || [];
// Get findings data
const providersData = await getProviders({});
const scansData = await getScans({});
// Extract provider UIDs
const providerUIDs = Array.from(
@@ -141,7 +158,20 @@ const SSRDataTable = async ({
// Make sure the sort is correctly encoded
const encodedSort = sort.replace(/^\+/, "");
const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd");
// Check if the searchParams contain any date or scan filter
const hasDateOrScanFilter = Object.keys(searchParams).some(
(key) => key.includes("inserted_at") || key.includes("scan__in"),
);
// Default filters for getFindings
const defaultFilters: Record<string, string> = hasDateOrScanFilter
? {} // Do not apply default filters if there are date or scan filters
: { "filter[inserted_at__gte]": twoDaysAgo };
const filters: Record<string, string> = {
...defaultFilters,
...Object.fromEntries(
Object.entries(searchParams)
.filter(([key]) => key.startsWith("filter["))
@@ -190,10 +220,18 @@ const SSRDataTable = async ({
};
return (
<DataTable
columns={ColumnFindings}
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
/>
<>
{findingsData?.errors && (
<div className="mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-small text-red-700">
<p className="mr-2 font-semibold">Error:</p>
<p>{findingsData.errors[0].detail}</p>
</div>
)}
<DataTable
columns={ColumnFindings}
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
/>
</>
);
};
+15 -4
View File
@@ -1,4 +1,5 @@
import { Spacer } from "@nextui-org/react";
import { format, subDays } from "date-fns";
import { Suspense } from "react";
import { getFindings } from "@/actions/findings/findings";
@@ -131,9 +132,12 @@ const SSRDataNewFindingsTable = async () => {
const page = 1;
const sort = "severity,-inserted_at";
const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd");
const defaultFilters = {
"filter[status__in]": "FAIL",
"filter[delta__in]": "new",
"filter[inserted_at__gte]": twoDaysAgo,
};
const findingsData = await getFindings({
@@ -172,14 +176,21 @@ const SSRDataNewFindingsTable = async () => {
return (
<>
<div className="relative flex items-start justify-between">
<h3 className="mb-4 w-full text-sm font-bold uppercase">
Latest 10 failing findings to date by Severity
</h3>
<div className="relative flex w-full">
<div className="flex w-full items-center gap-2">
<h3 className="text-sm font-bold uppercase">
Latest new failing findings
</h3>
<p className="text-xs text-gray-500">
Showing the latest 10 new failing findings by severity from the last
2 days.
</p>
</div>
<div className="absolute -top-6 right-0">
<LinkToFindings />
</div>
</div>
<Spacer y={4} />
<DataTable
columns={ColumnNewFindingsToDate}
data={expandedResponse?.data || []}
+3 -3
View File
@@ -16,7 +16,7 @@ export const CustomDatePicker = () => {
const searchParams = useSearchParams();
const [value, setValue] = React.useState(() => {
const dateParam = searchParams.get("filter[updated_at]");
const dateParam = searchParams.get("filter[inserted_at]");
return dateParam ? today(getLocalTimeZone()) : null;
});
@@ -30,9 +30,9 @@ export const CustomDatePicker = () => {
(date: any) => {
const params = new URLSearchParams(searchParams.toString());
if (date) {
params.set("filter[updated_at]", date.toString());
params.set("filter[inserted_at]", date.toString());
} else {
params.delete("filter[updated_at]");
params.delete("filter[inserted_at]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
@@ -6,7 +6,7 @@ export const LinkToFindings = () => {
return (
<div className="mt-4 flex w-full items-center justify-end">
<CustomButton
asLink="/findings?sort=severity,-updated_at&filter[status__in]=FAIL"
asLink="/findings?sort=severity,-inserted_at&filter[status__in]=FAIL&filter[delta__in]=new"
ariaLabel="Go to Findings page"
variant="solid"
color="action"