mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
# Conflicts: # ui/CHANGELOG.md # ui/app/(prowler)/findings/page.tsx # ui/components/ui/custom/custom-dropdown-filter.tsx # ui/types/filters.ts
This commit is contained in:
@@ -86,12 +86,12 @@ prowler dashboard
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 564 | 82 | 34 | 10 |
|
||||
| AWS | 567 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 9 | 3 |
|
||||
| Azure | 140 | 18 | 8 | 3 |
|
||||
| Azure | 142 | 18 | 10 | 3 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 3 | 2 | 1 | 0 |
|
||||
| M365 | 44 | 2 | 2 | 0 |
|
||||
| GitHub | 16 | 2 | 1 | 0 |
|
||||
| M365 | 69 | 7 | 2 | 2 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
|
||||
> [!Note]
|
||||
|
||||
@@ -4,11 +4,21 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [v1.9.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Support GCP Service Account key. [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824)
|
||||
|
||||
### Changed
|
||||
- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
- Fixed task lookup to use task_kwargs instead of task_args for scan report resolution. [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.1] (Prowler v5.7.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0025_findings_uid_index_parent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"ALTER TYPE provider_secret_type ADD VALUE IF NOT EXISTS 'service_account';",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
]
|
||||
@@ -850,6 +850,7 @@ class ProviderSecret(RowLevelSecurityProtectedModel):
|
||||
class TypeChoices(models.TextChoices):
|
||||
STATIC = "static", _("Key-value pairs")
|
||||
ROLE = "role", _("Role assumption")
|
||||
SERVICE_ACCOUNT = "service_account", _("GCP Service Account Key")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
@@ -5376,7 +5376,8 @@ paths:
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: The scan has no reports
|
||||
description: The scan has no reports, or the report generation task has
|
||||
not started yet
|
||||
/api/v1/schedules/daily:
|
||||
post:
|
||||
operationId: schedules_daily_create
|
||||
@@ -9136,10 +9137,12 @@ components:
|
||||
enum:
|
||||
- static
|
||||
- role
|
||||
- service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `static` - Key-value pairs
|
||||
* `role` - Role assumption
|
||||
* `service_account` - GCP Service Account Key
|
||||
readOnly: true
|
||||
secret:
|
||||
oneOf:
|
||||
@@ -9242,7 +9245,7 @@ components:
|
||||
description: User microsoft email address.
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
description: User password.
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
@@ -9268,6 +9271,14 @@ components:
|
||||
- client_id
|
||||
- client_secret
|
||||
- refresh_token
|
||||
- type: object
|
||||
title: GCP Service Account Key
|
||||
properties:
|
||||
service_account_key:
|
||||
type: object
|
||||
description: The service account key for GCP.
|
||||
required:
|
||||
- service_account_key
|
||||
- type: object
|
||||
title: Kubernetes Static Credentials
|
||||
properties:
|
||||
@@ -10316,10 +10327,12 @@ components:
|
||||
enum:
|
||||
- static
|
||||
- role
|
||||
- service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `static` - Key-value pairs
|
||||
* `role` - Role assumption
|
||||
* `service_account` - GCP Service Account Key
|
||||
required:
|
||||
- secret_type
|
||||
relationships:
|
||||
@@ -10383,10 +10396,12 @@ components:
|
||||
enum:
|
||||
- static
|
||||
- role
|
||||
- service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `static` - Key-value pairs
|
||||
* `role` - Role assumption
|
||||
* `service_account` - GCP Service Account Key
|
||||
secret:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -10487,7 +10502,7 @@ components:
|
||||
description: User microsoft email address.
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
description: User password.
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
@@ -10513,6 +10528,14 @@ components:
|
||||
- client_id
|
||||
- client_secret
|
||||
- refresh_token
|
||||
- type: object
|
||||
title: GCP Service Account Key
|
||||
properties:
|
||||
service_account_key:
|
||||
type: object
|
||||
description: The service account key for GCP.
|
||||
required:
|
||||
- service_account_key
|
||||
- type: object
|
||||
title: Kubernetes Static Credentials
|
||||
properties:
|
||||
@@ -10591,10 +10614,12 @@ components:
|
||||
enum:
|
||||
- static
|
||||
- role
|
||||
- service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `static` - Key-value pairs
|
||||
* `role` - Role assumption
|
||||
* `service_account` - GCP Service Account Key
|
||||
secret:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -10696,7 +10721,7 @@ components:
|
||||
description: User microsoft email address.
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
description: User password.
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
@@ -10722,6 +10747,14 @@ components:
|
||||
- client_id
|
||||
- client_secret
|
||||
- refresh_token
|
||||
- type: object
|
||||
title: GCP Service Account Key
|
||||
properties:
|
||||
service_account_key:
|
||||
type: object
|
||||
description: The service account key for GCP.
|
||||
required:
|
||||
- service_account_key
|
||||
- type: object
|
||||
title: Kubernetes Static Credentials
|
||||
properties:
|
||||
@@ -10816,10 +10849,12 @@ components:
|
||||
enum:
|
||||
- static
|
||||
- role
|
||||
- service_account
|
||||
type: string
|
||||
description: |-
|
||||
* `static` - Key-value pairs
|
||||
* `role` - Role assumption
|
||||
* `service_account` - GCP Service Account Key
|
||||
readOnly: true
|
||||
secret:
|
||||
oneOf:
|
||||
@@ -10921,7 +10956,7 @@ components:
|
||||
description: User microsoft email address.
|
||||
password:
|
||||
type: string
|
||||
description: User encrypted password.
|
||||
description: User password.
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
@@ -10947,6 +10982,14 @@ components:
|
||||
- client_id
|
||||
- client_secret
|
||||
- refresh_token
|
||||
- type: object
|
||||
title: GCP Service Account Key
|
||||
properties:
|
||||
service_account_key:
|
||||
type: object
|
||||
description: The service account key for GCP.
|
||||
required:
|
||||
- service_account_key
|
||||
- type: object
|
||||
title: Kubernetes Static Credentials
|
||||
properties:
|
||||
|
||||
@@ -13,6 +13,7 @@ from botocore.exceptions import ClientError, NoCredentialsError
|
||||
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
from rest_framework import status
|
||||
|
||||
from api.compliance import get_compliance_frameworks
|
||||
@@ -1678,6 +1679,26 @@ class TestProviderSecretViewSet:
|
||||
"refresh_token": "refresh-token",
|
||||
},
|
||||
),
|
||||
# GCP with Service Account Key secret
|
||||
(
|
||||
Provider.ProviderChoices.GCP.value,
|
||||
ProviderSecret.TypeChoices.SERVICE_ACCOUNT,
|
||||
{
|
||||
"service_account_key": {
|
||||
"type": "service_account",
|
||||
"project_id": "project-id",
|
||||
"private_key_id": "private-key-id",
|
||||
"private_key": "private-key",
|
||||
"client_email": "client-email",
|
||||
"client_id": "client-id",
|
||||
"auth_uri": "auth-uri",
|
||||
"token_uri": "token-uri",
|
||||
"auth_provider_x509_cert_url": "auth-provider-x509-cert-url",
|
||||
"client_x509_cert_url": "client-x509-cert-url",
|
||||
"universe_domain": "universe-domain",
|
||||
},
|
||||
},
|
||||
),
|
||||
# Kubernetes with STATIC secret
|
||||
(
|
||||
Provider.ProviderChoices.KUBERNETES.value,
|
||||
@@ -2303,7 +2324,10 @@ class TestScanViewSet:
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json()["errors"]["detail"] == "The scan has no reports."
|
||||
assert (
|
||||
response.json()["errors"]["detail"]
|
||||
== "The scan has no reports, or the report generation task has not started yet."
|
||||
)
|
||||
|
||||
def test_report_s3_no_credentials(
|
||||
self, authenticated_client, scans_fixture, monkeypatch
|
||||
@@ -2371,7 +2395,7 @@ class TestScanViewSet:
|
||||
):
|
||||
"""
|
||||
When output_location is a local path and glob.glob returns an empty list,
|
||||
the view should return HTTP 404 with detail "The scan has no reports."
|
||||
the view should return HTTP 404 with detail "The scan has no reports, or the report generation task has not started yet."
|
||||
"""
|
||||
scan = scans_fixture[0]
|
||||
scan.output_location = "/tmp/nonexistent_report_pattern.zip"
|
||||
@@ -2383,7 +2407,10 @@ class TestScanViewSet:
|
||||
response = authenticated_client.get(url)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["errors"]["detail"] == "The scan has no reports."
|
||||
assert (
|
||||
response.json()["errors"]["detail"]
|
||||
== "The scan has no reports, or the report generation task has not started yet."
|
||||
)
|
||||
|
||||
def test_report_local_file(self, authenticated_client, scans_fixture, monkeypatch):
|
||||
scan = scans_fixture[0]
|
||||
@@ -2458,7 +2485,10 @@ class TestScanViewSet:
|
||||
url = reverse("scan-compliance", kwargs={"pk": scan.id, "name": framework})
|
||||
resp = authenticated_client.get(url)
|
||||
assert resp.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert resp.json()["errors"]["detail"] == "The scan has no reports."
|
||||
assert (
|
||||
resp.json()["errors"]["detail"]
|
||||
== "The scan has no reports, or the report generation task has not started yet."
|
||||
)
|
||||
|
||||
def test_compliance_s3_no_credentials(
|
||||
self, authenticated_client, scans_fixture, monkeypatch
|
||||
@@ -2600,6 +2630,36 @@ class TestScanViewSet:
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@patch("api.v1.views.TaskSerializer")
|
||||
def test__get_task_status_finds_task_using_kwargs(
|
||||
self, mock_task_serializer, authenticated_client, scans_fixture
|
||||
):
|
||||
scan = scans_fixture[0]
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.output_location = "dummy"
|
||||
scan.save()
|
||||
|
||||
task_result = TaskResult.objects.create(
|
||||
task_name="scan-report",
|
||||
task_kwargs={"scan_id": str(scan.id)},
|
||||
)
|
||||
|
||||
task = Task.objects.create(
|
||||
tenant_id=scan.tenant_id,
|
||||
task_runner_task=task_result,
|
||||
)
|
||||
|
||||
mock_task_serializer.return_value.data = {
|
||||
"id": str(task.id),
|
||||
"state": StateChoices.EXECUTING,
|
||||
}
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert response.data["id"] == str(task.id)
|
||||
|
||||
@patch("api.v1.views.get_s3_client")
|
||||
@patch("api.v1.views.sentry_sdk.capture_exception")
|
||||
def test_compliance_list_objects_client_error(
|
||||
@@ -2650,7 +2710,10 @@ class TestScanViewSet:
|
||||
response = authenticated_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json()["errors"]["detail"] == "The scan has no reports."
|
||||
assert (
|
||||
response.json()["errors"]["detail"]
|
||||
== "The scan has no reports, or the report generation task has not started yet."
|
||||
)
|
||||
|
||||
@patch("api.v1.views.get_s3_client")
|
||||
def test_report_s3_client_error_other(
|
||||
|
||||
@@ -121,7 +121,7 @@ from rest_framework_json_api import serializers
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "User encrypted password.",
|
||||
"description": "User password.",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
@@ -154,6 +154,17 @@ from rest_framework_json_api import serializers
|
||||
},
|
||||
"required": ["client_id", "client_secret", "refresh_token"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "GCP Service Account Key",
|
||||
"properties": {
|
||||
"service_account_key": {
|
||||
"type": "object",
|
||||
"description": "The service account key for GCP.",
|
||||
}
|
||||
},
|
||||
"required": ["service_account_key"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Kubernetes Static Credentials",
|
||||
|
||||
@@ -1159,6 +1159,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
|
||||
)
|
||||
elif secret_type == ProviderSecret.TypeChoices.ROLE:
|
||||
serializer = AWSRoleAssumptionProviderSecret(data=secret)
|
||||
elif secret_type == ProviderSecret.TypeChoices.SERVICE_ACCOUNT:
|
||||
serializer = GCPServiceAccountProviderSecret(data=secret)
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
{"secret_type": f"Secret type not supported: {secret_type}"}
|
||||
@@ -1212,6 +1214,13 @@ class GCPProviderSecret(serializers.Serializer):
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class GCPServiceAccountProviderSecret(serializers.Serializer):
|
||||
service_account_key = serializers.JSONField()
|
||||
|
||||
class Meta:
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class KubernetesProviderSecret(serializers.Serializer):
|
||||
kubeconfig_content = serializers.CharField()
|
||||
|
||||
|
||||
@@ -1160,7 +1160,9 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
200: OpenApiResponse(description="Report obtained successfully"),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(description="The scan has no reports"),
|
||||
404: OpenApiResponse(
|
||||
description="The scan has no reports, or the report generation task has not started yet"
|
||||
),
|
||||
},
|
||||
),
|
||||
compliance=extend_schema(
|
||||
@@ -1281,7 +1283,7 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
try:
|
||||
task = Task.objects.get(
|
||||
task_runner_task__task_name="scan-report",
|
||||
task_runner_task__task_args__contains=str(scan_instance.id),
|
||||
task_runner_task__task_kwargs__contains=str(scan_instance.id),
|
||||
)
|
||||
except Task.DoesNotExist:
|
||||
return None
|
||||
@@ -1363,7 +1365,9 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
code = e.response.get("Error", {}).get("Code")
|
||||
if code == "NoSuchKey":
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."},
|
||||
{
|
||||
"detail": "The scan has no reports, or the report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
return Response(
|
||||
@@ -1376,7 +1380,9 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
files = glob.glob(path_pattern)
|
||||
if not files:
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."},
|
||||
{
|
||||
"detail": "The scan has no reports, or the report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
filepath = files[0]
|
||||
@@ -1402,7 +1408,10 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
|
||||
if not scan.output_location:
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."}, status=status.HTTP_404_NOT_FOUND
|
||||
{
|
||||
"detail": "The scan has no reports, or the report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if scan.output_location.startswith("s3://"):
|
||||
@@ -1440,7 +1449,10 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
|
||||
if not scan.output_location:
|
||||
return Response(
|
||||
{"detail": "The scan has no reports."}, status=status.HTTP_404_NOT_FOUND
|
||||
{
|
||||
"detail": "The scan has no reports, or the report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if scan.output_location.startswith("s3://"):
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_3_levels
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_DESCRIPTION"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
|
||||
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_3_levels(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_3_levels
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_DESCRIPTION"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
|
||||
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_3_levels(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
)
|
||||
@@ -90,12 +90,28 @@ def create_layout_overview(
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
(
|
||||
html.Label(
|
||||
"Table Rows:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
)
|
||||
html.Label(
|
||||
"Search:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
),
|
||||
dcc.Input(
|
||||
id="search-input",
|
||||
type="text",
|
||||
placeholder="Search by check title, service, region...",
|
||||
debounce=True,
|
||||
style={
|
||||
"padding": "4px 8px",
|
||||
"border": "1px solid #ccc",
|
||||
"borderRadius": "4px",
|
||||
"marginRight": "20px",
|
||||
"width": "250px",
|
||||
},
|
||||
),
|
||||
html.Label(
|
||||
"Table Rows:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
),
|
||||
table_row_dropdown,
|
||||
download_button_csv,
|
||||
|
||||
@@ -518,6 +518,7 @@ else:
|
||||
Input("service-filter", "value"),
|
||||
Input("table-rows", "value"),
|
||||
Input("status-filter", "value"),
|
||||
Input("search-input", "value"),
|
||||
Input("aws_card", "n_clicks"),
|
||||
Input("azure_card", "n_clicks"),
|
||||
Input("gcp_card", "n_clicks"),
|
||||
@@ -540,6 +541,7 @@ def filter_data(
|
||||
service_values,
|
||||
table_row_values,
|
||||
status_values,
|
||||
search_value,
|
||||
aws_clicks,
|
||||
azure_clicks,
|
||||
gcp_clicks,
|
||||
@@ -1144,6 +1146,15 @@ def filter_data(
|
||||
}
|
||||
|
||||
index_count = 0
|
||||
if search_value:
|
||||
search_value = search_value.lower()
|
||||
filtered_data = filtered_data[
|
||||
filtered_data["CHECK_TITLE"].str.lower().str.contains(search_value)
|
||||
| filtered_data["SERVICE_NAME"].str.lower().str.contains(search_value)
|
||||
| filtered_data["REGION"].str.lower().str.contains(search_value)
|
||||
| filtered_data["STATUS"].str.lower().str.contains(search_value)
|
||||
]
|
||||
|
||||
full_filtered_data = filtered_data.copy()
|
||||
filtered_data = filtered_data.head(table_row_values)
|
||||
# Sort the filtered_data
|
||||
|
||||
@@ -81,6 +81,9 @@ Prowler for Azure needs two types of permission scopes to be set:
|
||||
|
||||
To assign the permissions, follow the instructions in the [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions) section and the [Azure subscriptions permissions](../tutorials/azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) section, respectively.
|
||||
|
||||
???+ warning
|
||||
Some permissions in `ProwlerRole` are considered **write** permissions, so if you have a `ReadOnly` lock attached to some resources you may get an error and will not get a finding for that check.
|
||||
|
||||
#### Checks that require ProwlerRole
|
||||
|
||||
The following checks require the `ProwlerRole` permissions to be executed, if you want to run them, make sure you have assigned the role to the identity that is going to be assumed by Prowler:
|
||||
@@ -160,70 +163,24 @@ export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export M365_USER="your_email@example.com"
|
||||
export M365_PASSWORD="6500780061006d0070006c006500700061007300730077006f0072006400" # replace this to yours
|
||||
export M365_PASSWORD="examplepassword"
|
||||
```
|
||||
|
||||
These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
|
||||
|
||||
- `M365_USER` should be your Microsoft account email using the default domain. This means it must look like `example@YourCompany.onmicrosoft.com`.
|
||||
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
|
||||
|
||||
To ensure that you are using the default domain you can see how to verify it [here](../tutorials/microsoft365/getting-started-m365.md#step-1-obtain-your-domain).
|
||||
???+ warning
|
||||
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
|
||||
|
||||
If you don't have a user created with that domain, Prowler will not work as it will not be able to ensure both app an user belong to the same tenant. To proceed, you can either create a new user with that domain or modify the domain of an existing user.
|
||||
Ensure you are using the right domain for the user you are trying to authenticate with.
|
||||
|
||||

|
||||
|
||||
- `M365_PASSWORD` must be an encrypted SecureString. To convert your password into a valid encrypted string, you need to use PowerShell.
|
||||
|
||||
???+ warning
|
||||
Passwords encrypted using ConvertTo-SecureString can only be decrypted on the same OS/user context. If you generate an encrypted password on macOS or Linux (both UNIX), it should fail on Windows and vice versa. As Prowler Cloud runs on UNIX if you generate your password using Windows it won't work so you'll need to generate a new password using any UNIX distro (example above)
|
||||
|
||||
If you are working from Windows and you will use your encrypted password in a different system (like for example executing Prowler in macOS or adding your password to Prowler Cloud), you will need to generate a "UNIX compatible" version of your encrypted password. This can be done using WSL which is so easy to install on Windows.
|
||||
|
||||
=== "UNIX"
|
||||
|
||||
Open a PowerShell cmd with a [supported version](requirements.md#supported-powershell-versions) and then run the following command:
|
||||
|
||||
```console
|
||||
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
|
||||
$encryptedPassword = $securePassword | ConvertFrom-SecureString
|
||||
Write-Output $encryptedPassword
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
|
||||
=== "Windows"
|
||||
|
||||
|
||||
How to install WSL and PowerShell on it to generate that password (you can use a different distro but this one will work for sure):
|
||||
|
||||
```console
|
||||
wsl --install -d Ubuntu-22.04
|
||||
```
|
||||
|
||||
Then, open the Ubuntu terminal and run the following commands:
|
||||
|
||||
```console
|
||||
sudo apt update && sudo apt install -y wget apt-transport-https software-properties-common
|
||||
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
sudo apt update
|
||||
sudo apt install -y powershell
|
||||
pwsh
|
||||
```
|
||||
|
||||
With this done you will see now that a prompt running PowerShell with the latest version is open so here you will be able to generate your encrypted password:
|
||||
|
||||
```console
|
||||
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
|
||||
$encryptedPassword = $securePassword | ConvertFrom-SecureString
|
||||
Write-Output $encryptedPassword
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
- `M365_PASSWORD` must be the user password.
|
||||
|
||||
???+ note
|
||||
Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you.
|
||||
|
||||
|
||||
### Interactive Browser authentication
|
||||
@@ -245,7 +202,6 @@ Prowler for M365 requires two types of permission scopes to be set (if you want
|
||||
- `Directory.Read.All`: Required for all services.
|
||||
- `Policy.Read.All`: Required for all services.
|
||||
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
|
||||
- `Sites.Read.All`: Required for SharePoint service.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 119 KiB |
@@ -109,6 +109,15 @@ The following list includes all the Microsoft 365 checks with configurable varia
|
||||
| `exchange_organization_mailtips_enabled` | `recommended_mailtips_large_audience_threshold` | Integer |
|
||||
|
||||
|
||||
## GitHub
|
||||
|
||||
### Configurable Checks
|
||||
The following list includes all the GitHub checks with configurable variables that can be changed in the configuration yaml file:
|
||||
|
||||
| Check Name | Value | Type |
|
||||
|--------------------------------------------|---------------------------------------------|---------|
|
||||
| `repository_inactive_not_archived` | `inactive_not_archived_days_threshold` | Integer |
|
||||
|
||||
## Config YAML File Structure
|
||||
|
||||
???+ note
|
||||
@@ -525,5 +534,10 @@ m365:
|
||||
# m365.exchange_organization_mailtips_enabled
|
||||
recommended_mailtips_large_audience_threshold: 25 # maximum number of recipients
|
||||
|
||||
# GitHub Configuration
|
||||
github:
|
||||
# github.repository_inactive_not_archived
|
||||
inactive_not_archived_days_threshold: 180
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -4,9 +4,9 @@ Set up your M365 account to enable security scanning using Prowler Cloud/App.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your M365 account, you’ll need:
|
||||
To configure your M365 account, you'll need:
|
||||
|
||||
1. Obtain your `Default Domain` from the Entra ID portal.
|
||||
1. Obtain a domain from the Entra ID portal.
|
||||
|
||||
2. Access Prowler Cloud/App and add a new cloud provider `Microsoft 365`.
|
||||
|
||||
@@ -18,8 +18,6 @@ To configure your M365 account, you’ll need:
|
||||
|
||||
3.3 Assign the required roles to your user.
|
||||
|
||||
3.4 Retrieve your encrypted password.
|
||||
|
||||
4. Add the credentials to Prowler Cloud/App.
|
||||
|
||||
## Step 1: Obtain your Domain
|
||||
@@ -32,9 +30,7 @@ Go to the Entra ID portal, then you can search for `Domain` or go to Identity >
|
||||
|
||||

|
||||
|
||||
Once you are there just look for the `Default Domain` this should be something similar to `YourCompany.onmicrosoft.com`. To ensure that you are picking the correct domain just click on it and verify that the type is `Initial` and you can't delete it.
|
||||
|
||||

|
||||
Once you are there just select the domain you want to use.
|
||||
|
||||
---
|
||||
|
||||
@@ -78,11 +74,11 @@ A Service Principal is required to grant Prowler the necessary privileges.
|
||||
|
||||

|
||||
|
||||
4. Go to `Certificates & secrets` > `+ New client secret`
|
||||
4. Go to `Certificates & secrets` > `Client secrets` > `+ New client secret`
|
||||
|
||||

|
||||
|
||||
5. Fill in the required fields and click `Add`, then copy the generated value (that value will be `AZURE_CLIENT_SECRET`)
|
||||
5. Fill in the required fields and click `Add`, then copy the generated `value` (that value will be `AZURE_CLIENT_SECRET`)
|
||||
|
||||

|
||||
|
||||
@@ -102,9 +98,9 @@ Assign the following Microsoft Graph permissions:
|
||||
|
||||
- `Directory.Read.All`: Required for all services.
|
||||
- `Policy.Read.All`: Required for all services.
|
||||
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
|
||||
- `Sites.Read.All`: Required for SharePoint service.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
|
||||
|
||||
Follow these steps to assign the permissions:
|
||||
|
||||
@@ -120,8 +116,8 @@ Follow these steps to assign the permissions:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `Sites.Read.All`
|
||||
- `SharePointTenantSettings.Read.All`
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
|
||||

|
||||
|
||||
@@ -174,25 +170,34 @@ Follow these steps to assign the role:
|
||||
|
||||
---
|
||||
|
||||
### Get your encrypted password
|
||||
|
||||
For this step you will need to use PowerShell, here you will have to create your Encrypted Password based on the password of the User that you are going to use. For more information about how to generate this Password go [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) and follow the steps needed to obtain `M365_PASSWORD`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Add credentials to Prowler Cloud/App
|
||||
|
||||
1. Go to your App Registration overview and copy the `Client ID` and `Tenant ID`
|
||||
|
||||

|
||||
|
||||
???+ warning
|
||||
For Prowler Cloud encrypted password is still needed (when we update Prowler Cloud and regular password is accepted this warning will be deleted), so the password that you paste in the next step should be generated following this steps:
|
||||
|
||||
- UNIX: Open a PowerShell cmd with a [supported version](../../getting-started/requirements.md#supported-powershell-versions) and then run the following command:
|
||||
|
||||
```console
|
||||
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
|
||||
$encryptedPassword = $securePassword | ConvertFrom-SecureString
|
||||
Write-Output $encryptedPassword
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
- Windows: Install WSL using `wsl --install -d Ubuntu-22.04`, then open the Ubuntu terminal, install powershell and run the same command above.
|
||||
|
||||
|
||||
2. Go to Prowler Cloud/App and paste:
|
||||
|
||||
- `Client ID`
|
||||
- `Tenant ID`
|
||||
- `AZURE_CLIENT_SECRET` from earlier
|
||||
- `M365_USER` your user using the default domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
|
||||
- `M365_PASSWORD` generated before
|
||||
- `M365_USER` the user using the correct assigned domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
|
||||
- `M365_PASSWORD` the password of the user
|
||||
|
||||

|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 119 KiB |
@@ -15,14 +15,18 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Add CIS 4.0 compliance framework for GCP. [(7785)](https://github.com/prowler-cloud/prowler/pull/7785)
|
||||
- Add `repository_has_codeowners_file` check for GitHub provider. [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752)
|
||||
- Add `repository_default_branch_requires_signed_commits` check for GitHub provider. [(#7777)](https://github.com/prowler-cloud/prowler/pull/7777)
|
||||
- Add `repository_inactive_not_archived` check for GitHub provider. [(#7786)](https://github.com/prowler-cloud/prowler/pull/7786)
|
||||
- Add `repository_dependency_scanning_enabled` check for GitHub provider. [(#7771)](https://github.com/prowler-cloud/prowler/pull/7771)
|
||||
- Add `repository_secret_scanning_enabled` check for GitHub provider. [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
|
||||
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
|
||||
- Add NIS 2 compliance framework for AWS. [(7839)](https://github.com/prowler-cloud/prowler/pull/7839)
|
||||
- Add NIS 2 compliance framework for Azure. [(7857)](https://github.com/prowler-cloud/prowler/pull/7857)
|
||||
|
||||
### Fixed
|
||||
- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
|
||||
- Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- Fix `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity. [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
|
||||
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -515,3 +515,8 @@ m365:
|
||||
# m365.exchange_mailbox_properties_auditing_enabled
|
||||
# Maximum number of days to keep audit logs
|
||||
audit_log_age: 90
|
||||
|
||||
# GitHub Configuration
|
||||
github:
|
||||
# github.repository_inactive_not_archived --> CIS recommends 180 days (6 months)
|
||||
inactive_not_archived_days_threshold: 180
|
||||
|
||||
@@ -548,7 +548,7 @@ class CheckReportGithub(Check_Report):
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
repository: str
|
||||
owner: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -556,7 +556,7 @@ class CheckReportGithub(Check_Report):
|
||||
resource: Any,
|
||||
resource_name: str = None,
|
||||
resource_id: str = None,
|
||||
repository: str = "global",
|
||||
owner: str = None,
|
||||
) -> None:
|
||||
"""Initialize the GitHub Check's finding information.
|
||||
|
||||
@@ -565,12 +565,16 @@ class CheckReportGithub(Check_Report):
|
||||
resource: Basic information about the resource. Defaults to None.
|
||||
resource_name: The name of the resource related with the finding.
|
||||
resource_id: The id of the resource related with the finding.
|
||||
repository: The repository of the resource related with the finding.
|
||||
owner: The owner of the resource related with the finding.
|
||||
"""
|
||||
super().__init__(metadata, resource)
|
||||
self.resource_name = resource_name or getattr(resource, "name", "")
|
||||
self.resource_id = resource_id or getattr(resource, "id", "")
|
||||
self.repository = repository or getattr(resource, "repository", "")
|
||||
self.owner = (
|
||||
owner
|
||||
or getattr(resource, "owner", "") # For Repositories
|
||||
or getattr(resource, "name", "") # For Organizations
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -251,7 +251,7 @@ class Finding(BaseModel):
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["account_name"] = provider.identity.account_name
|
||||
output_data["account_uid"] = provider.identity.account_id
|
||||
output_data["region"] = check_output.repository
|
||||
output_data["region"] = check_output.owner
|
||||
|
||||
elif provider.type == "m365":
|
||||
output_data["auth_method"] = (
|
||||
|
||||
@@ -17,7 +17,7 @@ def stdout_report(finding, color, verbose, status, fix):
|
||||
if finding.check_metadata.Provider == "kubernetes":
|
||||
details = finding.namespace.lower()
|
||||
if finding.check_metadata.Provider == "github":
|
||||
details = finding.repository
|
||||
details = finding.owner
|
||||
if finding.check_metadata.Provider == "m365":
|
||||
details = finding.location
|
||||
if finding.check_metadata.Provider == "nhn":
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"CheckID": "vpc_endpoint_for_ec2_enabled",
|
||||
"CheckTitle": "Amazon EC2 should be configured to use VPC endpoints that are created for the Amazon EC2 service.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ec2",
|
||||
"ServiceName": "vpc",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
|
||||
+13
-12
@@ -11,19 +11,20 @@ class app_function_access_keys_configured(Check):
|
||||
functions,
|
||||
) in app_client.functions.items():
|
||||
for function in functions.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=function)
|
||||
report.subscription = subscription_name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} does not have function keys configured."
|
||||
)
|
||||
|
||||
if len(function.function_keys) > 0:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} has function keys configured."
|
||||
if function.function_keys is not None:
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=function
|
||||
)
|
||||
report.subscription = subscription_name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Function {function.name} does not have function keys configured."
|
||||
|
||||
findings.append(report)
|
||||
if len(function.function_keys) > 0:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} has function keys configured."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
+21
-18
@@ -14,26 +14,29 @@ class app_function_application_insights_enabled(Check):
|
||||
functions,
|
||||
) in app_client.functions.items():
|
||||
for function in functions.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=function)
|
||||
report.subscription = subscription_name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} is not using Application Insights."
|
||||
)
|
||||
|
||||
if function.enviroment_variables.get(
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY", ""
|
||||
) in [
|
||||
component.instrumentation_key
|
||||
for component in appinsights_client.components[
|
||||
subscription_name
|
||||
].values()
|
||||
]:
|
||||
report.status = "PASS"
|
||||
if function.enviroment_variables is not None:
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=function
|
||||
)
|
||||
report.subscription = subscription_name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} is using Application Insights."
|
||||
f"Function {function.name} is not using Application Insights."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
if function.enviroment_variables.get(
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY", ""
|
||||
) in [
|
||||
component.instrumentation_key
|
||||
for component in appinsights_client.components[
|
||||
subscription_name
|
||||
].values()
|
||||
]:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} is using Application Insights."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
+18
-13
@@ -11,20 +11,25 @@ class app_function_latest_runtime_version(Check):
|
||||
functions,
|
||||
) in app_client.functions.items():
|
||||
for function in functions.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=function)
|
||||
report.subscription = subscription_name
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} is using the latest runtime."
|
||||
)
|
||||
if function.enviroment_variables is not None:
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=function
|
||||
)
|
||||
report.subscription = subscription_name
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Function {function.name} is using the latest runtime."
|
||||
)
|
||||
|
||||
if (
|
||||
function.enviroment_variables.get("FUNCTIONS_EXTENSION_VERSION", "")
|
||||
!= "~4"
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Function {function.name} is not using the latest runtime. The current runtime is '{function.enviroment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'."
|
||||
if (
|
||||
function.enviroment_variables.get(
|
||||
"FUNCTIONS_EXTENSION_VERSION", ""
|
||||
)
|
||||
!= "~4"
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Function {function.name} is not using the latest runtime. The current runtime is '{function.enviroment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'."
|
||||
|
||||
findings.append(report)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from azure.mgmt.web import WebSiteManagementClient
|
||||
|
||||
@@ -124,14 +124,16 @@ class App(AzureService):
|
||||
# Filter function apps
|
||||
if getattr(function, "kind", "").startswith("functionapp"):
|
||||
# List host keys
|
||||
host_keys = client.web_apps.list_host_keys(
|
||||
resource_group_name=function.resource_group,
|
||||
name=function.name,
|
||||
) # Need to add role 'Logic App Contributor' to the service principal to get the host keys or add to the reader role the permission 'Microsoft.Web/sites/host/listkeys'
|
||||
host_keys = self._get_function_host_keys(
|
||||
subscription_name, function.resource_group, function.name
|
||||
)
|
||||
if host_keys is not None:
|
||||
function_keys = getattr(host_keys, "function_keys", {})
|
||||
else:
|
||||
function_keys = None
|
||||
|
||||
function_config = client.web_apps.get_configuration(
|
||||
resource_group_name=function.resource_group,
|
||||
name=function.name,
|
||||
function_config = self._get_function_config(
|
||||
subscription_name, function.resource_group, function.name
|
||||
)
|
||||
|
||||
functions[subscription_name].update(
|
||||
@@ -141,16 +143,9 @@ class App(AzureService):
|
||||
name=function.name,
|
||||
location=function.location,
|
||||
kind=function.kind,
|
||||
function_keys=getattr(
|
||||
host_keys, "function_keys", {}
|
||||
),
|
||||
function_keys=function_keys,
|
||||
enviroment_variables=getattr(
|
||||
client.web_apps.list_application_settings(
|
||||
resource_group_name=function.resource_group,
|
||||
name=function.name,
|
||||
),
|
||||
"properties",
|
||||
{},
|
||||
function_config, "properties", None
|
||||
),
|
||||
identity=getattr(function, "identity", None),
|
||||
public_access=(
|
||||
@@ -167,7 +162,7 @@ class App(AzureService):
|
||||
"",
|
||||
),
|
||||
ftps_state=getattr(
|
||||
function_config, "ftps_state", ""
|
||||
function_config, "ftps_state", None
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -209,6 +204,30 @@ class App(AzureService):
|
||||
)
|
||||
return monitor_diagnostics_settings
|
||||
|
||||
def _get_function_host_keys(self, subscription, resource_group, name):
|
||||
try:
|
||||
return self.clients[subscription].web_apps.list_host_keys(
|
||||
resource_group_name=resource_group,
|
||||
name=name,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"Error getting host keys for {name} in {resource_group}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_function_config(self, subscription, resource_group, name):
|
||||
try:
|
||||
return self.clients[subscription].web_apps.list_application_settings(
|
||||
resource_group_name=resource_group,
|
||||
name=name,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"Error getting configuration for {name} in {resource_group}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManagedServiceIdentity:
|
||||
@@ -250,9 +269,9 @@ class FunctionApp:
|
||||
name: str
|
||||
location: str
|
||||
kind: str
|
||||
function_keys: Dict[str, str]
|
||||
enviroment_variables: Dict[str, str]
|
||||
function_keys: Optional[Dict[str, str]]
|
||||
enviroment_variables: Optional[Dict[str, str]]
|
||||
identity: ManagedServiceIdentity
|
||||
public_access: bool
|
||||
vnet_subnet_id: str
|
||||
ftps_state: str
|
||||
ftps_state: Optional[str]
|
||||
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "defender_ensure_notify_alerts_severity_is_high",
|
||||
"CheckTitle": "Ensure That 'Notify about alerts with the following severity' is Set to 'High'",
|
||||
"CheckTitle": "Ensure that email notifications are configured for alerts with a minimum severity of 'High' or lower",
|
||||
"CheckType": [],
|
||||
"ServiceName": "defender",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AzureEmailNotifications",
|
||||
"Description": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.",
|
||||
"Risk": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.",
|
||||
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details",
|
||||
"Description": "Microsoft Defender for Cloud sends email notifications when alerts of a certain severity level or higher are triggered. By setting the minimum severity to 'High', 'Medium', or even 'Low', you ensure that alerts with equal or greater severity (e.g., High or Critical) are still delivered. Selecting a lower threshold like 'Low' results in more comprehensive alert coverage.",
|
||||
"Risk": "If this setting is too restrictive (e.g., set to 'Critical' only), important security alerts with 'High' or 'Medium' severity might be missed. Ensuring that 'High' or a lower threshold is configured helps security teams stay informed about significant threats and respond in a timely manner.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/email-notifications-alerts#manage-notifications-on-email",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_4#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Defender for Cloud 3. Click on Environment Settings 4. Click on the appropriate Management Group, Subscription, or Workspace 5. Click on Email notifications 6. Enter a valid security contact email address (or multiple addresses separated by commas) in the Additional email addresses field 7. Click Save",
|
||||
"Text": "1. From Azure Home select the Portal Menu. 2. Select Microsoft Defender for Cloud. 3. Click on Environment Settings. 4. Click on the appropriate Management Group, Subscription, or Workspace. 5. Click on Email notifications. 6. Under 'Notify about alerts with the following severity (or higher)', select at least 'High' (or optionally 'Medium' or 'Low' for broader coverage). 7. Click Save.",
|
||||
"Url": "https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list"
|
||||
}
|
||||
},
|
||||
|
||||
+8
-5
@@ -13,12 +13,15 @@ class defender_ensure_notify_alerts_severity_is_high(Check):
|
||||
for contact in security_contacts.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
|
||||
report.subscription = subscription_name
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Notifiy alerts are enabled for severity high in subscription {subscription_name}."
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}."
|
||||
|
||||
if contact.alert_notifications_minimal_severity != "High":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Notifiy alerts are not enabled for severity high in subscription {subscription_name}."
|
||||
if (
|
||||
contact.alert_notifications_minimal_severity != "Critical"
|
||||
and contact.alert_notifications_minimal_severity != ""
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact.alert_notifications_minimal_severity}) in subscription {subscription_name}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ class sqlserver_auditing_enabled(Check):
|
||||
)
|
||||
report.subscription = subscription
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has a auditing policy configured."
|
||||
report.status_extended = f"SQL Server {sql_server.name} from subscription {subscription} has an auditing policy configured."
|
||||
for auditing_policy in sql_server.auditing_policies:
|
||||
if auditing_policy.state == "Disabled":
|
||||
report.status = "FAIL"
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Union
|
||||
|
||||
from colorama import Fore, Style
|
||||
from github import Auth, Github, GithubIntegration
|
||||
from github.GithubRetry import GithubRetry
|
||||
|
||||
from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
@@ -300,9 +301,10 @@ class GithubProvider(Provider):
|
||||
credentials = self.session
|
||||
|
||||
try:
|
||||
retry_config = GithubRetry(total=3)
|
||||
if credentials.token:
|
||||
auth = Auth.Token(credentials.token)
|
||||
g = Github(auth=auth)
|
||||
g = Github(auth=auth, retry=retry_config)
|
||||
try:
|
||||
identity = GithubIdentityInfo(
|
||||
account_id=g.get_user().id,
|
||||
@@ -318,7 +320,7 @@ class GithubProvider(Provider):
|
||||
|
||||
elif credentials.id != 0 and credentials.key:
|
||||
auth = Auth.AppAuth(credentials.id, credentials.key)
|
||||
gi = GithubIntegration(auth=auth)
|
||||
gi = GithubIntegration(auth=auth, retry=retry_config)
|
||||
try:
|
||||
identity = GithubAppIdentityInfo(app_id=gi.get_app().id)
|
||||
return identity
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from github import Auth, Github, GithubIntegration
|
||||
from github.GithubRetry import GithubRetry
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.github.github_provider import GithubProvider
|
||||
@@ -20,16 +21,17 @@ class GithubService:
|
||||
def __set_clients__(self, session):
|
||||
clients = []
|
||||
try:
|
||||
retry_config = GithubRetry(total=3)
|
||||
if session.token:
|
||||
auth = Auth.Token(session.token)
|
||||
clients = [Github(auth=auth)]
|
||||
clients = [Github(auth=auth, retry=retry_config)]
|
||||
|
||||
elif session.key and session.id:
|
||||
auth = Auth.AppAuth(
|
||||
session.id,
|
||||
session.key,
|
||||
)
|
||||
gi = GithubIntegration(auth=auth)
|
||||
gi = GithubIntegration(auth=auth, retry=retry_config)
|
||||
|
||||
for installation in gi.get_installations():
|
||||
clients.append(installation.get_github_for_installation())
|
||||
|
||||
+3
-9
@@ -22,19 +22,13 @@ class repository_branch_delete_on_merge_enabled(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does not delete branches on merge."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does not delete branches on merge in default branch ({repo.default_branch.name})."
|
||||
|
||||
if repo.delete_branch_on_merge:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does delete branches on merge."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does delete branches on merge in default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-5
@@ -22,16 +22,14 @@ class repository_default_branch_deletion_disabled(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.default_branch_deletion is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.branch_deletion is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does allow default branch deletion."
|
||||
)
|
||||
|
||||
if not repo.default_branch_deletion:
|
||||
if not repo.default_branch.branch_deletion:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does deny default branch deletion."
|
||||
|
||||
+5
-11
@@ -22,20 +22,14 @@ class repository_default_branch_disallows_force_push(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.allow_force_pushes is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.allow_force_pushes is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does allow force push."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does allow force pushes on default branch ({repo.default_branch.name})."
|
||||
|
||||
if not repo.allow_force_pushes:
|
||||
if not repo.default_branch.allow_force_pushes:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does deny force push."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does deny force pushes on default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-5
@@ -22,14 +22,12 @@ class repository_default_branch_protection_applies_to_admins(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.enforce_admins is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.enforce_admins is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce administrators to be subject to the same branch protection rules as other users."
|
||||
|
||||
if repo.enforce_admins:
|
||||
if repo.default_branch.enforce_admins:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does enforce administrators to be subject to the same branch protection rules as other users."
|
||||
|
||||
|
||||
+5
-7
@@ -22,16 +22,14 @@ class repository_default_branch_protection_enabled(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.default_branch_protection is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.protected is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce branch protection on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does not enforce branch protection on default branch ({repo.default_branch.name})."
|
||||
|
||||
if repo.default_branch_protection:
|
||||
if repo.default_branch.protected:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does enforce branch protection on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does enforce branch protection on default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-5
@@ -22,11 +22,9 @@ class repository_default_branch_requires_codeowners_review(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.require_code_owner_reviews is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.require_code_owner_reviews:
|
||||
if repo.default_branch.require_code_owner_reviews is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
if repo.default_branch.require_code_owner_reviews:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} requires code owner approval for changes to owned code."
|
||||
else:
|
||||
|
||||
+5
-11
@@ -22,20 +22,14 @@ class repository_default_branch_requires_conversation_resolution(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.conversation_resolution is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.conversation_resolution is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does not require conversation resolution."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does not require conversation resolution on default branch ({repo.default_branch.name})."
|
||||
|
||||
if repo.conversation_resolution:
|
||||
if repo.default_branch.conversation_resolution:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does require conversation resolution."
|
||||
)
|
||||
report.status_extended = f"Repository {repo.name} does require conversation resolution on default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+5
-7
@@ -22,16 +22,14 @@ class repository_default_branch_requires_linear_history(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.required_linear_history is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.required_linear_history is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require linear history on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does not require linear history on default branch ({repo.default_branch.name})."
|
||||
|
||||
if repo.required_linear_history:
|
||||
if repo.default_branch.required_linear_history:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does require linear history on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does require linear history on default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-5
@@ -22,14 +22,12 @@ class repository_default_branch_requires_multiple_approvals(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.approval_count is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.approval_count is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce at least 2 approvals for code changes."
|
||||
|
||||
if repo.approval_count >= 2:
|
||||
if repo.default_branch.approval_count >= 2:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does enforce at least 2 approvals for code changes."
|
||||
|
||||
|
||||
+5
-7
@@ -22,16 +22,14 @@ class repository_default_branch_requires_signed_commits(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.require_signed_commits is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.require_signed_commits is not None:
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require signed commits on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does not require signed commits on default branch ({repo.default_branch.name})."
|
||||
|
||||
if repo.require_signed_commits:
|
||||
if repo.default_branch.require_signed_commits:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does require signed commits on default branch ({repo.default_branch})."
|
||||
report.status_extended = f"Repository {repo.name} does require signed commits on default branch ({repo.default_branch.name})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-5
@@ -22,16 +22,14 @@ class repository_default_branch_status_checks_required(Check):
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.status_checks is not None:
|
||||
report = CheckReportGithub(
|
||||
self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
if repo.default_branch.status_checks is not None:
|
||||
report = CheckReportGithub(self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does not enforce status checks."
|
||||
)
|
||||
|
||||
if repo.status_checks:
|
||||
if repo.default_branch.status_checks:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does enforce status checks."
|
||||
|
||||
+1
-3
@@ -23,9 +23,7 @@ class repository_dependency_scanning_enabled(Check):
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.dependabot_alerts_enabled is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
if repo.dependabot_alerts_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} has package vulnerability scanning (Dependabot alerts) enabled."
|
||||
|
||||
+1
-3
@@ -23,9 +23,7 @@ class repository_has_codeowners_file(Check):
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.codeowners_exists is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
if repo.codeowners_exists:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "github",
|
||||
"CheckID": "repository_inactive_not_archived",
|
||||
"CheckTitle": "Check for inactive repositories that are not archived",
|
||||
"CheckType": [],
|
||||
"ServiceName": "repository",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "GitHubRepository",
|
||||
"Description": "Ensure that repositories with no activity are reviewed and considered for archival. Inactive repositories may have outdated dependencies or security configurations that could pose security risks.",
|
||||
"Risk": "Inactive repositories that are not archived may contain outdated dependencies, unpatched vulnerabilities, or misconfigured security settings. These repositories increase the attack surface and could be targeted by malicious actors.",
|
||||
"RelatedUrl": "https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Review inactive repositories and either: 1) Archive them if they are no longer needed, 2) Update their dependencies and security configurations if they are still required, or 3) Delete them if they contain no valuable information.",
|
||||
"Url": "https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportGithub
|
||||
from prowler.providers.github.services.repository.repository_client import (
|
||||
repository_client,
|
||||
)
|
||||
|
||||
|
||||
class repository_inactive_not_archived(Check):
|
||||
"""Check if unarchived repositories have been inactive for more than 6 months."""
|
||||
|
||||
def execute(self) -> List[CheckReportGithub]:
|
||||
findings = []
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
days_threshold = repository_client.audit_config.get(
|
||||
"inactive_not_archived_days_threshold", 180
|
||||
)
|
||||
|
||||
for repo in repository_client.repositories.values():
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
|
||||
if repo.archived:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} is properly archived."
|
||||
findings.append(report)
|
||||
continue
|
||||
|
||||
latest_activity = repo.pushed_at
|
||||
days_inactive = (now - latest_activity).days
|
||||
|
||||
if days_inactive >= days_threshold:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} has been inactive for {days_inactive} days and is not archived (threshold: {days_threshold} days)."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} has been active within the last {days_threshold} days ({days_inactive} days ago)."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+1
-3
@@ -23,9 +23,7 @@ class repository_public_has_securitymd_file(Check):
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if not repo.private and repo.securitymd is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does have a SECURITY.md file."
|
||||
|
||||
+1
-3
@@ -23,9 +23,7 @@ class repository_secret_scanning_enabled(Check):
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if repo.secret_scanning_enabled is not None:
|
||||
report = CheckReportGithub(
|
||||
metadata=self.metadata(), resource=repo, repository=repo.name
|
||||
)
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
if getattr(repo, "secret_scanning_enabled", None):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} has secret scanning enabled to detect sensitive data."
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -135,7 +136,7 @@ class Repository(GithubService):
|
||||
)
|
||||
try:
|
||||
# Use get_dependabot_alerts to check if Dependabot alerts are enabled
|
||||
repo.get_dependabot_alerts()[0]
|
||||
repo.get_dependabot_alerts().totalCount
|
||||
# If the call succeeds, Dependabot is enabled (even if no alerts)
|
||||
dependabot_alerts_enabled = True
|
||||
except Exception as error:
|
||||
@@ -160,23 +161,29 @@ class Repository(GithubService):
|
||||
repos[repo.id] = Repo(
|
||||
id=repo.id,
|
||||
name=repo.name,
|
||||
owner=repo.owner.login,
|
||||
full_name=repo.full_name,
|
||||
default_branch=repo.default_branch,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=branch_protection,
|
||||
default_branch=True,
|
||||
require_pull_request=require_pr,
|
||||
approval_count=approval_cnt,
|
||||
required_linear_history=required_linear_history,
|
||||
allow_force_pushes=allow_force_pushes,
|
||||
branch_deletion=branch_deletion,
|
||||
status_checks=status_checks,
|
||||
enforce_admins=enforce_admins,
|
||||
conversation_resolution=conversation_resolution,
|
||||
require_code_owner_reviews=require_code_owner_reviews,
|
||||
require_signed_commits=require_signed_commits,
|
||||
),
|
||||
private=repo.private,
|
||||
archived=repo.archived,
|
||||
pushed_at=repo.pushed_at,
|
||||
securitymd=securitymd_exists,
|
||||
require_pull_request=require_pr,
|
||||
approval_count=approval_cnt,
|
||||
required_linear_history=required_linear_history,
|
||||
allow_force_pushes=allow_force_pushes,
|
||||
default_branch_deletion=branch_deletion,
|
||||
status_checks=status_checks,
|
||||
enforce_admins=enforce_admins,
|
||||
conversation_resolution=conversation_resolution,
|
||||
default_branch_protection=branch_protection,
|
||||
codeowners_exists=codeowners_exists,
|
||||
require_code_owner_reviews=require_code_owner_reviews,
|
||||
secret_scanning_enabled=secret_scanning_enabled,
|
||||
require_signed_commits=require_signed_commits,
|
||||
dependabot_alerts_enabled=dependabot_alerts_enabled,
|
||||
delete_branch_on_merge=delete_branch_on_merge,
|
||||
)
|
||||
@@ -188,27 +195,37 @@ class Repository(GithubService):
|
||||
return repos
|
||||
|
||||
|
||||
class Branch(BaseModel):
|
||||
"""Model for Github Branch"""
|
||||
|
||||
name: str
|
||||
protected: bool
|
||||
default_branch: bool
|
||||
require_pull_request: Optional[bool]
|
||||
approval_count: Optional[int]
|
||||
required_linear_history: Optional[bool]
|
||||
allow_force_pushes: Optional[bool]
|
||||
branch_deletion: Optional[bool]
|
||||
status_checks: Optional[bool]
|
||||
enforce_admins: Optional[bool]
|
||||
require_code_owner_reviews: Optional[bool]
|
||||
require_signed_commits: Optional[bool]
|
||||
conversation_resolution: Optional[bool]
|
||||
|
||||
|
||||
class Repo(BaseModel):
|
||||
"""Model for Github Repository"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
owner: str
|
||||
full_name: str
|
||||
default_branch_protection: Optional[bool]
|
||||
default_branch: str
|
||||
default_branch: Branch
|
||||
private: bool
|
||||
archived: bool
|
||||
pushed_at: datetime
|
||||
securitymd: Optional[bool]
|
||||
require_pull_request: Optional[bool]
|
||||
required_linear_history: Optional[bool]
|
||||
allow_force_pushes: Optional[bool]
|
||||
default_branch_deletion: Optional[bool]
|
||||
status_checks: Optional[bool]
|
||||
enforce_admins: Optional[bool]
|
||||
approval_count: Optional[int]
|
||||
codeowners_exists: Optional[bool]
|
||||
require_code_owner_reviews: Optional[bool]
|
||||
secret_scanning_enabled: Optional[bool]
|
||||
require_signed_commits: Optional[bool]
|
||||
dependabot_alerts_enabled: Optional[bool]
|
||||
delete_branch_on_merge: Optional[bool]
|
||||
conversation_resolution: Optional[bool]
|
||||
|
||||
@@ -106,9 +106,9 @@ class M365BaseException(ProwlerException):
|
||||
"message": "The provided User is not valid.",
|
||||
"remediation": "Check the User and ensure it is a valid user.",
|
||||
},
|
||||
(6025, "M365NotValidEncryptedPasswordError"): {
|
||||
"message": "The provided Encrypted Password is not valid.",
|
||||
"remediation": "Check the Encrypted Password and ensure it is a valid password.",
|
||||
(6025, "M365NotValidPasswordError"): {
|
||||
"message": "The provided Password is not valid.",
|
||||
"remediation": "Check the Password and ensure it is a valid password.",
|
||||
},
|
||||
(6026, "M365UserNotBelongingToTenantError"): {
|
||||
"message": "The provided User does not belong to the specified tenant.",
|
||||
@@ -312,7 +312,7 @@ class M365NotValidUserError(M365CredentialsError):
|
||||
)
|
||||
|
||||
|
||||
class M365NotValidEncryptedPasswordError(M365CredentialsError):
|
||||
class M365NotValidPasswordError(M365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6025, file=file, original_exception=original_exception, message=message
|
||||
|
||||
@@ -43,7 +43,7 @@ from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365NotTenantIdButClientIdAndClientSecretError,
|
||||
M365NotValidClientIdError,
|
||||
M365NotValidClientSecretError,
|
||||
M365NotValidEncryptedPasswordError,
|
||||
M365NotValidPasswordError,
|
||||
M365NotValidTenantIdError,
|
||||
M365NotValidUserError,
|
||||
M365SetUpRegionConfigError,
|
||||
@@ -296,7 +296,7 @@ class M365Provider(Provider):
|
||||
client_id (str): The M365 Client ID.
|
||||
client_secret (str): The M365 Client Secret.
|
||||
user (str): The M365 User Account.
|
||||
encrpted_password (str): The M365 Encrypted Password.
|
||||
password (str): The M365 User Password.
|
||||
|
||||
Raises:
|
||||
M365BrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found.
|
||||
@@ -494,7 +494,7 @@ class M365Provider(Provider):
|
||||
- client_id: The M365 client ID.
|
||||
- client_secret: The M365 client secret
|
||||
- user: The M365 user email
|
||||
- password: The M365 encrypted password
|
||||
- password: The M365 user password
|
||||
- provider_id: The M365 provider ID (in this case the Tenant ID).
|
||||
region_config (M365RegionConfig): The region configuration object.
|
||||
|
||||
@@ -985,7 +985,7 @@ class M365Provider(Provider):
|
||||
client_id (str): The M365 client ID.
|
||||
client_secret (str): The M365 client secret.
|
||||
user (str): The M365 user email.
|
||||
password (str): The M365 encrypted password.
|
||||
password (str): The M365 user password.
|
||||
|
||||
Raises:
|
||||
M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid.
|
||||
@@ -1030,11 +1030,11 @@ class M365Provider(Provider):
|
||||
message="The provided User is not valid.",
|
||||
)
|
||||
|
||||
# Validate the Encrypted Password
|
||||
# Validate the Password
|
||||
if not password:
|
||||
raise M365NotValidEncryptedPasswordError(
|
||||
raise M365NotValidPasswordError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided Encrypted Password is not valid.",
|
||||
message="The provided Password is not valid.",
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
+3
-3
@@ -9,13 +9,13 @@ from prowler.providers.m365.services.entra.entra_service import (
|
||||
|
||||
|
||||
class entra_identity_protection_sign_in_risk_enabled(Check):
|
||||
"""Check if at least one Conditional Access policy is a Identity Protection sign-in risk policy.
|
||||
"""Check if at least one Conditional Access policy is an Identity Protection sign-in risk policy.
|
||||
|
||||
This check ensures that at least one Conditional Access policy is a Identity Protection sign-in risk policy.
|
||||
This check ensures that at least one Conditional Access policy is an Identity Protection sign-in risk policy.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the check to ensure that at least one Conditional Access policy is a Identity Protection sign-in risk policy.
|
||||
"""Execute the check to ensure that at least one Conditional Access policy is an Identity Protection sign-in risk policy.
|
||||
|
||||
Returns:
|
||||
list[CheckReportM365]: A list containing the results of the check.
|
||||
|
||||
+7
-7
@@ -9,13 +9,13 @@ from prowler.providers.m365.services.entra.entra_service import (
|
||||
|
||||
|
||||
class entra_identity_protection_user_risk_enabled(Check):
|
||||
"""Check if at least one Conditional Access policy is a Identity Protection user risk policy.
|
||||
"""Check if at least one Conditional Access policy is an Identity Protection user risk policy.
|
||||
|
||||
This check ensures that at least one Conditional Access policy is a Identity Protection user risk policy.
|
||||
This check ensures that at least one Conditional Access policy is an Identity Protection user risk policy.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the check to ensure that at least one Conditional Access policy is a Identity Protection user risk policy.
|
||||
"""Execute the check to ensure that at least one Conditional Access policy is an Identity Protection user risk policy.
|
||||
|
||||
Returns:
|
||||
list[CheckReportM365]: A list containing the results of the check.
|
||||
@@ -29,7 +29,7 @@ class entra_identity_protection_user_risk_enabled(Check):
|
||||
resource_id="conditionalAccessPolicies",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "No Conditional Access Policy is an user risk based Identity Protection Policy."
|
||||
report.status_extended = "No Conditional Access Policy is a user risk based Identity Protection Policy."
|
||||
|
||||
for policy in entra_client.conditional_access_policies.values():
|
||||
if policy.state == ConditionalAccessPolicyState.DISABLED:
|
||||
@@ -62,13 +62,13 @@ class entra_identity_protection_user_risk_enabled(Check):
|
||||
)
|
||||
if RiskLevel.HIGH not in policy.conditions.user_risk_levels:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is an user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
|
||||
elif policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is an user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is an user risk based Identity Protection Policy and does protect against high risk potential account compromises."
|
||||
report.status_extended = f"Conditional Access Policy '{policy.display_name}' is a user risk based Identity Protection Policy and does protect against high risk potential account compromises."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
|
||||
@@ -493,6 +493,7 @@ class ConditionalAccessGrantControl(Enum):
|
||||
BLOCK = "block"
|
||||
DOMAIN_JOINED_DEVICE = "domainJoinedDevice"
|
||||
PASSWORD_CHANGE = "passwordChange"
|
||||
COMPLIANT_DEVICE = "compliantDevice"
|
||||
|
||||
|
||||
class GrantControlOperator(Enum):
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ class compute_instance_login_user(Check):
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"VM Instance {instance.name} has a appropriate login user."
|
||||
f"VM Instance {instance.name} has an appropriate login user."
|
||||
)
|
||||
if instance.login_user:
|
||||
report.status = "FAIL"
|
||||
|
||||
@@ -857,7 +857,6 @@ KISA_ISMSP_AWS = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_AWS_NAME = "prowler_threatscore_aws"
|
||||
PROWLER_THREATSCORE_AWS = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -901,7 +900,6 @@ PROWLER_THREATSCORE_AWS = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_AZURE_NAME = "prowler_threatscore_azure"
|
||||
PROWLER_THREATSCORE_AZURE = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -945,7 +943,6 @@ PROWLER_THREATSCORE_AZURE = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_GCP_NAME = "prowler_threatscore_gcp"
|
||||
PROWLER_THREATSCORE_GCP = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -989,7 +986,6 @@ PROWLER_THREATSCORE_GCP = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_M365_NAME = "prowler_threatscore_m365"
|
||||
PROWLER_THREATSCORE_M365 = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_aws import (
|
||||
ProwlerThreatScoreAWS,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_AWS,
|
||||
PROWLER_THREATSCORE_AWS_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AWS
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1
|
||||
|
||||
@@ -24,9 +21,7 @@ class TestProwlerThreatScoreAWS:
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreAWS(
|
||||
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreAWSModel)
|
||||
assert output_data.Provider == "aws"
|
||||
@@ -134,9 +129,7 @@ class TestProwlerThreatScoreAWS:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreAWS(
|
||||
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azure import (
|
||||
ProwlerThreatScoreAzure,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_AZURE,
|
||||
PROWLER_THREATSCORE_AZURE_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AZURE
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
@@ -33,9 +30,7 @@ class TestProwlerThreatScoreAzure:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreAzure(
|
||||
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreAzureModel)
|
||||
assert output_data.Provider == "azure"
|
||||
@@ -144,9 +139,7 @@ class TestProwlerThreatScoreAzure:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreAzure(
|
||||
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
|
||||
ProwlerThreatScoreGCP,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_GCP,
|
||||
PROWLER_THREATSCORE_GCP_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_GCP
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID
|
||||
|
||||
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreGCP:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreGCP(
|
||||
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreGCPModel)
|
||||
assert output_data.Provider == "gcp"
|
||||
@@ -140,9 +135,7 @@ class TestProwlerThreatScoreGCP:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreGCP(
|
||||
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
|
||||
ProwlerThreatScoreM365,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_M365,
|
||||
PROWLER_THREATSCORE_M365_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_M365
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.m365.m365_fixtures import TENANT_ID
|
||||
|
||||
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreM365:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreM365(
|
||||
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreM365Model)
|
||||
assert output_data.Provider == "m365"
|
||||
@@ -142,9 +137,7 @@ class TestProwlerThreatScoreM365:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreM365(
|
||||
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
+49
-5
@@ -31,7 +31,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_defender_severity_alerts_low(self):
|
||||
def test_defender_severity_alerts_critical(self):
|
||||
resource_id = str(uuid4())
|
||||
defender_client = mock.MagicMock
|
||||
defender_client.security_contacts = {
|
||||
@@ -41,7 +41,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
name="default",
|
||||
emails="",
|
||||
phone="",
|
||||
alert_notifications_minimal_severity="Low",
|
||||
alert_notifications_minimal_severity="Critical",
|
||||
alert_notifications_state="On",
|
||||
notified_roles=["Contributor"],
|
||||
notified_roles_state="On",
|
||||
@@ -69,7 +69,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
@@ -113,7 +113,51 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are enabled for alerts with a minimum severity of high or lower (High) in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_defender_severity_alerts_low(self):
|
||||
resource_id = str(uuid4())
|
||||
defender_client = mock.MagicMock
|
||||
defender_client.security_contacts = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContacts(
|
||||
resource_id=resource_id,
|
||||
name="default",
|
||||
emails="",
|
||||
phone="",
|
||||
alert_notifications_minimal_severity="Low",
|
||||
alert_notifications_state="On",
|
||||
notified_roles=["Contributor"],
|
||||
notified_roles_state="On",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high import (
|
||||
defender_ensure_notify_alerts_severity_is_high,
|
||||
)
|
||||
|
||||
check = defender_ensure_notify_alerts_severity_is_high()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifications are enabled for alerts with a minimum severity of high or lower (Low) in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
@@ -156,7 +200,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
|
||||
@@ -93,7 +93,7 @@ async def mock_entra_get_conditional_access_policy(_):
|
||||
"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"],
|
||||
"exclude": [],
|
||||
},
|
||||
access_controls={"grant": ["MFA"], "block": []},
|
||||
access_controls={"grant": ["MFA", "compliantDevice"], "block": []},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ class Test_Entra_Service:
|
||||
)
|
||||
assert entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
|
||||
"grant"
|
||||
] == ["MFA"]
|
||||
] == ["MFA", "compliantDevice"]
|
||||
assert (
|
||||
entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
|
||||
"block"
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ class Test_sqlserver_auditing_enabled:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has a auditing policy configured."
|
||||
== f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_ID} has an auditing policy configured."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == sql_server_name
|
||||
|
||||
@@ -59,7 +59,9 @@ class TestGitHubProvider:
|
||||
account_id=ACCOUNT_ID,
|
||||
account_url=ACCOUNT_URL,
|
||||
)
|
||||
assert provider._audit_config == {}
|
||||
assert provider._audit_config == {
|
||||
"inactive_not_archived_days_threshold": 180,
|
||||
}
|
||||
assert provider._fixer_config == fixer_config
|
||||
|
||||
def test_github_provider_OAuth(self):
|
||||
@@ -99,7 +101,9 @@ class TestGitHubProvider:
|
||||
account_id=ACCOUNT_ID,
|
||||
account_url=ACCOUNT_URL,
|
||||
)
|
||||
assert provider._audit_config == {}
|
||||
assert provider._audit_config == {
|
||||
"inactive_not_archived_days_threshold": 180,
|
||||
}
|
||||
assert provider._fixer_config == fixer_config
|
||||
|
||||
def test_github_provider_App(self):
|
||||
@@ -133,5 +137,7 @@ class TestGitHubProvider:
|
||||
assert provider._type == "github"
|
||||
assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY)
|
||||
assert provider.identity == GithubAppIdentityInfo(app_id=APP_ID)
|
||||
assert provider._audit_config == {}
|
||||
assert provider._audit_config == {
|
||||
"inactive_not_archived_days_threshold": 180,
|
||||
}
|
||||
assert provider._fixer_config == fixer_config
|
||||
|
||||
+55
-12
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -29,15 +30,36 @@ class Test_repository_branch_delete_on_merge_enabled_test:
|
||||
|
||||
def test_branch_deletion_disabled(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
repo_name = "repo2"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
2: Repo(
|
||||
id=2,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
owner="account-name",
|
||||
full_name="account-name/repo2",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
@@ -59,25 +81,46 @@ class Test_repository_branch_delete_on_merge_enabled_test:
|
||||
check = repository_branch_delete_on_merge_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].resource_id == 2
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does not delete branches on merge."
|
||||
== f"Repository {repo_name} does not delete branches on merge in default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_branch_deletion_enabled(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
@@ -100,9 +143,9 @@ class Test_repository_branch_delete_on_merge_enabled_test:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does delete branches on merge."
|
||||
== f"Repository {repo_name} does delete branches on merge in default branch ({default_branch})."
|
||||
)
|
||||
|
||||
+55
-8
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -30,16 +31,39 @@ class Test_repository_default_branch_deletion_disabled_test:
|
||||
def test_allow_branch_deletion_enabled(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
default_branch = Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
default_branch_deletion=True,
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=now,
|
||||
default_branch_deletion=True,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -61,7 +85,7 @@ class Test_repository_default_branch_deletion_disabled_test:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -71,16 +95,39 @@ class Test_repository_default_branch_deletion_disabled_test:
|
||||
def test_allow_branch_deletion_disabled(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
default_branch = Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=False,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=now,
|
||||
default_branch_deletion=False,
|
||||
securitymd=True,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -102,7 +149,7 @@ class Test_repository_default_branch_deletion_disabled_test:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
|
||||
+50
-9
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -27,7 +28,7 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_allow_force_push_enabled(self):
|
||||
def test_force_push_allowed(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
allow_force_pushes=True,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -65,10 +86,10 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does allow force push."
|
||||
== f"Repository {repo_name} does allow force pushes on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_allow_force_push_disabled(self):
|
||||
def test_force_push_disallowed(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
@@ -76,11 +97,31 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
allow_force_pushes=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -106,5 +147,5 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does deny force push."
|
||||
== f"Repository {repo_name} does deny force pushes on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
+46
-5
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
enforce_admins=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,11 +97,31 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
enforce_admins=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+46
-5
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_protection_enabled_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
default_branch_protection=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,11 +97,31 @@ class Test_repository_default_branch_protection_enabled_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
default_branch_protection=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+46
-9
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,13 +35,31 @@ class Test_repository_default_branch_requires_codeowners_review:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
require_code_owner_reviews=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,13 +95,31 @@ class Test_repository_default_branch_requires_codeowners_review:
|
||||
2: Repo(
|
||||
id=2,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo2",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
require_code_owner_reviews=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+48
-7
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
conversation_resolution=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -65,7 +86,7 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does not require conversation resolution."
|
||||
== f"Repository {repo_name} does not require conversation resolution on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_conversation_resolution_enabled(self):
|
||||
@@ -76,11 +97,31 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
conversation_resolution=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -106,5 +147,5 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does require conversation resolution."
|
||||
== f"Repository {repo_name} does require conversation resolution on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
+46
-5
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_requires_linear_history_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
required_linear_history=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,11 +97,31 @@ class Test_repository_default_branch_requires_linear_history_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
required_linear_history=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+68
-13
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,13 +35,31 @@ class Test_repository_default_branch_requires_multiple_approvals:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch_protection=False,
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,13 +95,31 @@ class Test_repository_default_branch_requires_multiple_approvals:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch_protection=False,
|
||||
default_branch="master",
|
||||
default_branch=Branch(
|
||||
name="master",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
require_pull_request=True,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -118,13 +155,31 @@ class Test_repository_default_branch_requires_multiple_approvals:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch_protection=True,
|
||||
default_branch="master",
|
||||
default_branch=Branch(
|
||||
name="master",
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+54
-13
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,31 @@ class Test_repository_default_branch_requires_signed_commits:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
require_signed_commits=False,
|
||||
securitymd=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -61,7 +82,7 @@ class Test_repository_default_branch_requires_signed_commits:
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -70,17 +91,37 @@ class Test_repository_default_branch_requires_signed_commits:
|
||||
|
||||
def test_signed_commits_required(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo2"
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
2: Repo(
|
||||
id=2,
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo2",
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
require_signed_commits=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -101,8 +142,8 @@ class Test_repository_default_branch_requires_signed_commits:
|
||||
check = repository_default_branch_requires_signed_commits()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 2
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == "repo1"
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
|
||||
+46
-3
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -35,11 +36,32 @@ class Test_repository_default_branch_status_checks_required_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
status_checks=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
private=False,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,11 +98,32 @@ class Test_repository_default_branch_status_checks_required_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=True,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
status_checks=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+42
-7
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,14 +35,31 @@ class Test_repository_dependency_scanning_enabled:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -77,14 +95,31 @@ class Test_repository_dependency_scanning_enabled:
|
||||
2: Repo(
|
||||
id=2,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo2",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+42
-7
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,13 +35,30 @@ class Test_repository_has_codeowners_file:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,13 +94,30 @@ class Test_repository_has_codeowners_file:
|
||||
2: Repo(
|
||||
id=2,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo2",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
class Test_repository_inactive_not_archived:
|
||||
def test_no_repositories(self):
|
||||
repository_client = mock.MagicMock
|
||||
repository_client.repositories = {}
|
||||
repository_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
|
||||
repository_inactive_not_archived,
|
||||
)
|
||||
|
||||
check = repository_inactive_not_archived()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_repository_active_not_archived(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "test-repo"
|
||||
default_branch = "main"
|
||||
now = datetime.now(timezone.utc)
|
||||
recent_activity = now - timedelta(days=30) # 30 days ago
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/test-repo",
|
||||
private=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
archived=False,
|
||||
pushed_at=recent_activity,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
repository_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
|
||||
repository_inactive_not_archived,
|
||||
)
|
||||
|
||||
check = repository_inactive_not_archived()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has been active within the last 180 days (30 days ago)."
|
||||
)
|
||||
|
||||
def test_repository_inactive_not_archived(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "test-repo"
|
||||
default_branch = "main"
|
||||
now = datetime.now(timezone.utc)
|
||||
old_activity = now - timedelta(days=200) # 200 days ago
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/test-repo",
|
||||
private=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
archived=False,
|
||||
pushed_at=old_activity,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
repository_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
|
||||
repository_inactive_not_archived,
|
||||
)
|
||||
|
||||
check = repository_inactive_not_archived()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "FAIL"
|
||||
assert "has been inactive for 200 days" in result[0].status_extended
|
||||
assert "and is not archived" in result[0].status_extended
|
||||
|
||||
def test_repository_inactive_but_archived(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "test-repo"
|
||||
default_branch = "main"
|
||||
now = datetime.now(timezone.utc)
|
||||
old_activity = now - timedelta(days=200) # 200 days ago
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/test-repo",
|
||||
private=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
archived=True,
|
||||
pushed_at=old_activity,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
repository_client.audit_config = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
|
||||
repository_inactive_not_archived,
|
||||
)
|
||||
|
||||
check = repository_inactive_not_archived()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} is properly archived."
|
||||
)
|
||||
|
||||
def test_custom_days_threshold(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "test-repo"
|
||||
default_branch = "main"
|
||||
now = datetime.now(timezone.utc)
|
||||
old_activity = now - timedelta(days=50)
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/test-repo",
|
||||
private=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
archived=False,
|
||||
pushed_at=old_activity,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
repository_client.audit_config = {"inactive_not_archived_days_threshold": 40}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
|
||||
repository_inactive_not_archived,
|
||||
)
|
||||
|
||||
check = repository_inactive_not_archived()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == repo_name
|
||||
assert result[0].status == "FAIL"
|
||||
assert "has been inactive for 50 days" in result[0].status_extended
|
||||
assert "and is not archived" in result[0].status_extended
|
||||
+44
-7
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,12 +35,30 @@ class Test_repository_public_has_securitymd_file_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=False,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -75,12 +94,30 @@ class Test_repository_public_has_securitymd_file_test:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+42
-7
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import Branch, Repo
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -34,13 +35,30 @@ class Test_repository_secret_scanning_enabled:
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,13 +94,30 @@ class Test_repository_secret_scanning_enabled:
|
||||
2: Repo(
|
||||
id=2,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo2",
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Branch,
|
||||
Repo,
|
||||
Repository,
|
||||
)
|
||||
@@ -12,23 +14,30 @@ def mock_list_repositories(_):
|
||||
1: Repo(
|
||||
id=1,
|
||||
name="repo1",
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch_protection=True,
|
||||
default_branch="main",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=True,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=True,
|
||||
default_branch_deletion=True,
|
||||
status_checks=True,
|
||||
approval_count=2,
|
||||
codeowners_exists=True,
|
||||
require_code_owner_reviews=True,
|
||||
secret_scanning_enabled=True,
|
||||
enforce_admins=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
delete_branch_on_merge=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -52,20 +61,29 @@ class Test_Repository_Service:
|
||||
assert repository_service.repositories[1].name == "repo1"
|
||||
assert repository_service.repositories[1].full_name == "account-name/repo1"
|
||||
assert repository_service.repositories[1].private is False
|
||||
assert repository_service.repositories[1].default_branch == "main"
|
||||
assert repository_service.repositories[1].default_branch.name == "main"
|
||||
assert repository_service.repositories[1].securitymd
|
||||
assert repository_service.repositories[1].required_linear_history
|
||||
assert repository_service.repositories[1].require_pull_request
|
||||
assert repository_service.repositories[1].allow_force_pushes
|
||||
assert repository_service.repositories[1].default_branch_deletion
|
||||
assert repository_service.repositories[1].status_checks
|
||||
assert repository_service.repositories[1].enforce_admins
|
||||
assert repository_service.repositories[1].default_branch.required_linear_history
|
||||
assert repository_service.repositories[1].default_branch.require_pull_request
|
||||
assert repository_service.repositories[1].default_branch.allow_force_pushes
|
||||
assert repository_service.repositories[1].default_branch.branch_deletion
|
||||
assert repository_service.repositories[1].default_branch.status_checks
|
||||
assert repository_service.repositories[1].default_branch.enforce_admins
|
||||
assert repository_service.repositories[1].delete_branch_on_merge
|
||||
assert repository_service.repositories[1].conversation_resolution
|
||||
assert repository_service.repositories[1].approval_count == 2
|
||||
assert repository_service.repositories[1].default_branch.conversation_resolution
|
||||
assert repository_service.repositories[1].default_branch.approval_count == 2
|
||||
assert repository_service.repositories[1].codeowners_exists is True
|
||||
assert repository_service.repositories[1].require_code_owner_reviews is True
|
||||
assert (
|
||||
repository_service.repositories[1].default_branch.require_code_owner_reviews
|
||||
is True
|
||||
)
|
||||
assert repository_service.repositories[1].secret_scanning_enabled is True
|
||||
assert (
|
||||
repository_service.repositories[1].default_branch.require_signed_commits
|
||||
is True
|
||||
)
|
||||
assert repository_service.repositories[1].archived is False
|
||||
assert repository_service.repositories[1].pushed_at is not None
|
||||
|
||||
|
||||
class Test_Repository_FileExists:
|
||||
|
||||
@@ -24,7 +24,7 @@ from prowler.providers.m365.exceptions.exceptions import (
|
||||
M365NoAuthenticationMethodError,
|
||||
M365NotValidClientIdError,
|
||||
M365NotValidClientSecretError,
|
||||
M365NotValidEncryptedPasswordError,
|
||||
M365NotValidPasswordError,
|
||||
M365NotValidTenantIdError,
|
||||
M365NotValidUserError,
|
||||
M365UserNotBelongingToTenantError,
|
||||
@@ -396,14 +396,12 @@ class TestM365Provider:
|
||||
with patch(
|
||||
"prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials"
|
||||
) as mock_validate_static_credentials:
|
||||
mock_validate_static_credentials.side_effect = (
|
||||
M365NotValidEncryptedPasswordError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Encrypted Password is not valid.",
|
||||
)
|
||||
mock_validate_static_credentials.side_effect = M365NotValidPasswordError(
|
||||
file=os.path.basename(__file__),
|
||||
message="The provided M365 Password is not valid.",
|
||||
)
|
||||
|
||||
with pytest.raises(M365NotValidEncryptedPasswordError) as exception:
|
||||
with pytest.raises(M365NotValidPasswordError) as exception:
|
||||
M365Provider.test_connection(
|
||||
tenant_id=str(uuid4()),
|
||||
region="M365Global",
|
||||
@@ -414,10 +412,8 @@ class TestM365Provider:
|
||||
password=None,
|
||||
)
|
||||
|
||||
assert exception.type == M365NotValidEncryptedPasswordError
|
||||
assert "The provided M365 Encrypted Password is not valid." in str(
|
||||
exception.value
|
||||
)
|
||||
assert exception.type == M365NotValidPasswordError
|
||||
assert "The provided M365 Password is not valid." in str(exception.value)
|
||||
|
||||
def test_test_connection_with_httpresponseerror(self):
|
||||
with patch(
|
||||
@@ -588,7 +584,7 @@ class TestM365Provider:
|
||||
assert "The provided User is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_static_credentials_missing_password(self):
|
||||
with pytest.raises(M365NotValidEncryptedPasswordError) as exception:
|
||||
with pytest.raises(M365NotValidPasswordError) as exception:
|
||||
M365Provider.validate_static_credentials(
|
||||
tenant_id="12345678-1234-5678-1234-567812345678",
|
||||
client_id="12345678-1234-5678-1234-567812345678",
|
||||
@@ -596,7 +592,7 @@ class TestM365Provider:
|
||||
user="test@example.com",
|
||||
password="",
|
||||
)
|
||||
assert "The provided Encrypted Password is not valid." in str(exception.value)
|
||||
assert "The provided Password is not valid." in str(exception.value)
|
||||
|
||||
def test_validate_arguments_missing_env_credentials(self):
|
||||
with pytest.raises(M365MissingEnvironmentCredentialsError) as exception:
|
||||
|
||||
+5
-5
@@ -45,7 +45,7 @@ class Test_entra_identity_protection_user_risk_enabled:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy is an user risk based Identity Protection Policy."
|
||||
== "No Conditional Access Policy is a user risk based Identity Protection Policy."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
@@ -119,7 +119,7 @@ class Test_entra_identity_protection_user_risk_enabled:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No Conditional Access Policy is an user risk based Identity Protection Policy."
|
||||
== "No Conditional Access Policy is a user risk based Identity Protection Policy."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
@@ -198,7 +198,7 @@ class Test_entra_identity_protection_user_risk_enabled:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
|
||||
== f"Conditional Access Policy '{display_name}' is a user risk based Identity Protection Policy but does not protect against high risk potential account compromises."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
@@ -280,7 +280,7 @@ class Test_entra_identity_protection_user_risk_enabled:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
|
||||
== f"Conditional Access Policy '{display_name}' is a user risk based Identity Protection Policy and reports high risk potential account compromises, but does not protect against them."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
@@ -362,7 +362,7 @@ class Test_entra_identity_protection_user_risk_enabled:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Conditional Access Policy '{display_name}' is an user risk based Identity Protection Policy and does protect against high risk potential account compromises."
|
||||
== f"Conditional Access Policy '{display_name}' is a user risk based Identity Protection Policy and does protect against high risk potential account compromises."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
|
||||
@@ -68,7 +68,10 @@ async def mock_entra_get_conditional_access_policies(_):
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
|
||||
],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
@@ -211,7 +214,10 @@ class Test_Entra_Service:
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
|
||||
],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ class Test_compute_instance_login_user:
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "has a appropriate login user" in result[0].status_extended
|
||||
assert "has an appropriate login user" in result[0].status_extended
|
||||
assert result[0].resource_name == instance_name
|
||||
assert result[0].resource_id == instance_id
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
- New profile page with details about the user and their roles. [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780)
|
||||
- Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
|
||||
- Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758)
|
||||
- Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)]
|
||||
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7827)](https://github.com/prowler-cloud/prowler/pull/7827/)
|
||||
|
||||
---
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
parseStringify,
|
||||
wait,
|
||||
} from "@/lib";
|
||||
import { ProvidersApiResponse } from "@/types/providers";
|
||||
|
||||
export const getProviders = async ({
|
||||
page = 1,
|
||||
@@ -17,7 +18,7 @@ export const getProviders = async ({
|
||||
sort = "",
|
||||
filters = {},
|
||||
pageSize = 10,
|
||||
}) => {
|
||||
}): Promise<ProvidersApiResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
if (isNaN(Number(page)) || page < 1) redirect("/providers");
|
||||
|
||||
@@ -238,10 +238,23 @@ export const getExportsZip = async (scanId: string) => {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 202) {
|
||||
const json = await response.json();
|
||||
const taskId = json?.data?.id;
|
||||
const state = json?.data?.attributes?.state;
|
||||
return {
|
||||
pending: true,
|
||||
state,
|
||||
taskId,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
|
||||
throw new Error(
|
||||
errorData?.errors?.[0]?.detail || "Failed to fetch report",
|
||||
errorData?.errors?.detail ||
|
||||
"Unable to fetch scan report. Contact support if the issue continues.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,20 +286,28 @@ export const getComplianceCsv = async (
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
if (response.status === 202) {
|
||||
const json = await response.json();
|
||||
const taskId = json?.data?.id;
|
||||
const state = json?.data?.attributes?.state;
|
||||
return {
|
||||
pending: true,
|
||||
state,
|
||||
taskId,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
errorData?.errors?.[0]?.detail || "Failed to fetch compliance report",
|
||||
errorData?.errors?.detail ||
|
||||
"Unable to retrieve compliance report. Contact support if the issue continues.",
|
||||
);
|
||||
}
|
||||
|
||||
// Get the blob data as an array buffer
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
// Convert to base64
|
||||
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
|
||||
|
||||
@@ -26,3 +29,67 @@ export const getAllTenants = async () => {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const editTenantFormSchema = z
|
||||
.object({
|
||||
tenantId: z.string(),
|
||||
name: z.string().trim().min(1, { message: "Name is required" }),
|
||||
currentName: z.string(),
|
||||
})
|
||||
.refine((data) => data.name !== data.currentName, {
|
||||
message: "Name must be different from the current name",
|
||||
path: ["name"],
|
||||
});
|
||||
|
||||
export async function updateTenantName(prevState: any, formData: FormData) {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const formDataObject = Object.fromEntries(formData);
|
||||
const validatedData = editTenantFormSchema.safeParse(formDataObject);
|
||||
|
||||
if (!validatedData.success) {
|
||||
const formFieldErrors = validatedData.error.flatten().fieldErrors;
|
||||
|
||||
return {
|
||||
errors: {
|
||||
name: formFieldErrors?.name?.[0],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { tenantId, name } = validatedData.data;
|
||||
|
||||
const payload = {
|
||||
data: {
|
||||
type: "tenants",
|
||||
id: tenantId,
|
||||
attributes: {
|
||||
name: name.trim(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(`${apiBaseUrl}/tenants/${tenantId}`);
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update tenant name: ${response.statusText}`);
|
||||
}
|
||||
|
||||
await response.json();
|
||||
revalidatePath("/profile");
|
||||
return { success: "Tenant name updated successfully!" };
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error updating tenant name:", error);
|
||||
return {
|
||||
errors: {
|
||||
general: "Error updating tenant name. Please try again.",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
extractSortAndKey,
|
||||
hasDateOrScanFilter,
|
||||
} from "@/lib";
|
||||
import { ProviderProps } from "@/types";
|
||||
import {
|
||||
createProviderDetailsMapping,
|
||||
extractProviderUIDs,
|
||||
} from "@/lib/provider-helpers";
|
||||
import {
|
||||
FindingProps,
|
||||
IncludeProps,
|
||||
@@ -60,29 +63,22 @@ export default async function Findings({
|
||||
const uniqueResourceTypes =
|
||||
metadataInfoData?.data?.attributes?.resource_types || [];
|
||||
|
||||
// Extract provider UIDs
|
||||
const providerUIDs: string[] = Array.from(
|
||||
new Set(
|
||||
providersData?.data
|
||||
?.map((provider: ProviderProps) => provider.attributes?.uid)
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
// Extract provider UIDs and details using helper functions
|
||||
const providerUIDs = providersData ? extractProviderUIDs(providersData) : [];
|
||||
const providerDetails = providersData
|
||||
? createProviderDetailsMapping(providerUIDs, providersData)
|
||||
: [];
|
||||
|
||||
const providerDetails = providerUIDs.map((uid) => {
|
||||
const provider = providersData.data.find(
|
||||
(p: { attributes: { uid: string } }) => p.attributes?.uid === uid,
|
||||
);
|
||||
|
||||
return {
|
||||
[uid]: {
|
||||
providerInfo: {
|
||||
provider: provider?.attributes?.provider || "",
|
||||
uid: uid,
|
||||
alias: provider?.attributes?.alias ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
// Update the Provider UID filter
|
||||
const updatedFilters = filterFindings.map((filter) => {
|
||||
if (filter.key === "provider_uid__in") {
|
||||
return {
|
||||
...filter,
|
||||
values: providerUIDs,
|
||||
valueLabelMapping: providerDetails,
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
|
||||
// Extract scan UUIDs with "completed" state and more than one resource
|
||||
@@ -138,33 +134,31 @@ export default async function Findings({
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom
|
||||
filters={[
|
||||
...filterFindings,
|
||||
...updatedFilters,
|
||||
{
|
||||
key: "region__in",
|
||||
labelCheckboxGroup: "Regions",
|
||||
values: uniqueRegions,
|
||||
index: 5,
|
||||
},
|
||||
{
|
||||
key: "service__in",
|
||||
labelCheckboxGroup: "Services",
|
||||
values: uniqueServices,
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
key: "resource_type__in",
|
||||
labelCheckboxGroup: "Resource Type",
|
||||
values: uniqueResourceTypes,
|
||||
},
|
||||
{
|
||||
key: "provider_uid__in",
|
||||
labelCheckboxGroup: "Provider UID",
|
||||
values: providerUIDs,
|
||||
valueLabelMapping: providerDetails,
|
||||
index: 7,
|
||||
},
|
||||
{
|
||||
key: "scan__in",
|
||||
labelCheckboxGroup: "Scan ID",
|
||||
values: completedScanIds,
|
||||
valueLabelMapping: providerDetailsAssociatedWithScans,
|
||||
index: 9,
|
||||
},
|
||||
]}
|
||||
defaultOpen={true}
|
||||
|
||||
@@ -115,7 +115,7 @@ const SSRDataEditGroup = async ({
|
||||
const associatedProviders = relationships.providers?.data.map(
|
||||
(provider: ProviderProps) => {
|
||||
const matchingProvider = providersList.find(
|
||||
(p: ProviderProps) => p.id === provider.id,
|
||||
(p: { id: string; name: string }) => p.id === provider.id,
|
||||
);
|
||||
return {
|
||||
id: provider.id,
|
||||
|
||||
@@ -8,16 +8,15 @@ import { UserBasicInfoCard } from "@/components/users/profile";
|
||||
import { MembershipsCard } from "@/components/users/profile/memberships-card";
|
||||
import { RolesCard } from "@/components/users/profile/roles-card";
|
||||
import { SkeletonUserInfo } from "@/components/users/profile/skeleton-user-info";
|
||||
import { isUserOwnerAndHasManageAccount } from "@/lib/permissions";
|
||||
import { RoleDetail, TenantDetailData } from "@/types/users/users";
|
||||
|
||||
export default async function Profile() {
|
||||
return (
|
||||
<ContentLayout title="User Profile" icon="ci:users">
|
||||
<div className="w-full md:w-1/2 lg:w-1/2 xl:w-1/3 2xl:w-1/4">
|
||||
<Suspense fallback={<SkeletonUserInfo />}>
|
||||
<SSRDataUser />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonUserInfo />}>
|
||||
<SSRDataUser />
|
||||
</Suspense>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -61,14 +60,27 @@ const SSRDataUser = async () => {
|
||||
),
|
||||
);
|
||||
|
||||
const isOwner = isUserOwnerAndHasManageAccount(
|
||||
roleDetails,
|
||||
memberships?.data || [],
|
||||
userProfile.data.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<UserBasicInfoCard user={userProfile?.data} tenantId={userTenant?.id} />
|
||||
<RolesCard roles={roleDetails || []} roleDetails={roleDetailsMap} />
|
||||
<MembershipsCard
|
||||
memberships={memberships?.data || []}
|
||||
tenantsMap={tenantsMap}
|
||||
/>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
<div className="w-full md:w-1/2 lg:w-1/2 xl:w-1/2 2xl:w-1/2">
|
||||
<RolesCard roles={roleDetails || []} roleDetails={roleDetailsMap} />
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 lg:w-1/2 xl:w-1/2 2xl:w-1/2">
|
||||
<MembershipsCard
|
||||
memberships={memberships?.data || []}
|
||||
tenantsMap={tenantsMap}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,10 @@ import { SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ColumnGetScans } from "@/components/scans/table/scans";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import {
|
||||
createProviderDetailsMapping,
|
||||
extractProviderUIDs,
|
||||
} from "@/lib/provider-helpers";
|
||||
import { ProviderProps, ScanProps, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Scans({
|
||||
@@ -61,6 +65,24 @@ export default async function Scans({
|
||||
scan.attributes.state === "available",
|
||||
);
|
||||
|
||||
// Extract provider UIDs and create provider details mapping for filtering
|
||||
const providerUIDs = providersData ? extractProviderUIDs(providersData) : [];
|
||||
const providerDetails = providersData
|
||||
? createProviderDetailsMapping(providerUIDs, providersData)
|
||||
: [];
|
||||
|
||||
// Update the Provider UID filter
|
||||
const updatedFilters = filterScans.map((filter) => {
|
||||
if (filter.key === "provider_uid__in") {
|
||||
return {
|
||||
...filter,
|
||||
values: providerUIDs,
|
||||
valueLabelMapping: providerDetails,
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{thereIsNoProviders && (
|
||||
@@ -89,7 +111,7 @@ export default async function Scans({
|
||||
<div className="grid grid-cols-12 items-start gap-4 px-6 py-4 sm:px-8 xl:px-10">
|
||||
<div className="col-span-12">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<DataTableFilterCustom filters={updatedFilters || []} />
|
||||
<Spacer x={4} />
|
||||
<FilterControls />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Card, CardBody, Progress } from "@nextui-org/react";
|
||||
import Image from "next/image";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { DownloadIconButton, toast } from "@/components/ui";
|
||||
import { downloadComplianceCsv } from "@/lib/helper";
|
||||
@@ -31,6 +31,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const hasRegionFilter = searchParams.has("filter[region__in]");
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
const formatTitle = (title: string) => {
|
||||
return title.split("-").join(" ");
|
||||
@@ -67,6 +68,15 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
return "success";
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
await downloadComplianceCsv(scanId, complianceId, toast);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card fullWidth isHoverable shadow="sm">
|
||||
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
|
||||
@@ -104,11 +114,10 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
|
||||
<DownloadIconButton
|
||||
paramId={complianceId}
|
||||
onDownload={() =>
|
||||
downloadComplianceCsv(scanId, complianceId, toast)
|
||||
}
|
||||
onDownload={handleDownload}
|
||||
textTooltip="Download compliance CSV report"
|
||||
isDisabled={hasRegionFilter}
|
||||
isDownloading={isDownloading}
|
||||
/>
|
||||
{/* <small>{getScanChange()}</small> */}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,11 @@ export const filterScans = [
|
||||
labelCheckboxGroup: "Trigger",
|
||||
values: ["scheduled", "manual"],
|
||||
},
|
||||
{
|
||||
key: "provider_uid__in",
|
||||
labelCheckboxGroup: "Provider UID",
|
||||
values: [],
|
||||
},
|
||||
// Add more filter categories as needed
|
||||
];
|
||||
|
||||
@@ -38,21 +43,31 @@ export const filterFindings = [
|
||||
key: "severity__in",
|
||||
labelCheckboxGroup: "Severity",
|
||||
values: ["critical", "high", "medium", "low", "informational"],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
key: "status__in",
|
||||
labelCheckboxGroup: "Status",
|
||||
values: ["PASS", "FAIL", "MANUAL"],
|
||||
},
|
||||
{
|
||||
key: "delta__in",
|
||||
labelCheckboxGroup: "Delta",
|
||||
values: ["new", "changed"],
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
key: "provider_type__in",
|
||||
labelCheckboxGroup: "Cloud Provider",
|
||||
values: ["aws", "azure", "m365", "gcp", "kubernetes"],
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
key: "provider_uid__in",
|
||||
labelCheckboxGroup: "Provider UID",
|
||||
values: [],
|
||||
index: 8,
|
||||
},
|
||||
{
|
||||
key: "delta__in",
|
||||
labelCheckboxGroup: "Delta",
|
||||
values: ["new", "changed"],
|
||||
index: 3,
|
||||
},
|
||||
// Add more filter categories as needed
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user