mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-28 14:24:26 +00:00
Merge branch 'master' into PRWLR-5956-Export-Artifacts-only
This commit is contained in:
commit
326fddd206
67 files changed
+1538
-209
No files matched your search
@@ -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.
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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)],
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+3
-10
@@ -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
|
||||
]
|
||||
)
|
||||
|
||||
+2
-2
@@ -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
-8
@@ -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}"
|
||||
|
||||
+3
-10
@@ -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.
+32
@@ -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."
|
||||
}
|
||||
+25
@@ -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] = []
|
||||
+1
-1
@@ -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)
|
||||
|
||||
File renamed without changes.
@@ -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
|
||||
Whitespace-only changes.
+30
@@ -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": ""
|
||||
}
|
||||
+26
@@ -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
|
||||
+1
-3
@@ -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]
|
||||
Whitespace-only changes.
+30
@@ -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."
|
||||
}
|
||||
+26
@@ -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
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
+46
-1
@@ -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"
|
||||
|
||||
+8
-8
@@ -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
|
||||
|
||||
+51
@@ -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 == []
|
||||
+222
-5
@@ -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.
+142
@@ -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)
|
||||
|
||||
+2
-2
@@ -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"
|
||||
)
|
||||
|
||||
+112
@@ -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
|
||||
+127
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 || []}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user