mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-30 03:49:48 +00:00
Compare commits
7 Commits
feat/cisa-
...
PROWLER-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc28122f8 | ||
|
|
99e1363352 | ||
|
|
ed68a30790 | ||
|
|
732d071c37 | ||
|
|
07d53dccb4 | ||
|
|
82474e890a | ||
|
|
6b8b1e1b08 |
1
.env
1
.env
@@ -66,6 +66,7 @@ NEO4J_DBMS_SECURITY_PROCEDURES_ALLOWLIST=apoc.*
|
||||
NEO4J_DBMS_SECURITY_PROCEDURES_UNRESTRICTED=apoc.*
|
||||
NEO4J_DBMS_CONNECTOR_BOLT_LISTEN_ADDRESS=0.0.0.0:7687
|
||||
# Neo4j Prowler settings
|
||||
ATTACK_PATHS_ENABLED=False
|
||||
ATTACK_PATHS_BATCH_SIZE=1000
|
||||
|
||||
# Celery-Prowler task settings
|
||||
|
||||
@@ -83,13 +83,16 @@ prowler dashboard
|
||||
|
||||
## Attack Paths
|
||||
|
||||
Attack Paths automatically extends every completed AWS scan with a Neo4j graph that combines Cartography's cloud inventory with Prowler findings. The feature runs in the API worker after each scan and therefore requires:
|
||||
Attack Paths automatically extends every completed AWS scan with a Neo4j graph that combines Cartography's cloud inventory with Prowler findings. The feature is controlled by `ATTACK_PATHS_ENABLED` (default `True`). Set it to `False` to run the API without Neo4j.
|
||||
|
||||
- An accessible Neo4j instance (the Docker Compose files already ships a `neo4j` service).
|
||||
When enabled, the feature runs in the API worker after each scan and requires:
|
||||
|
||||
- An accessible Neo4j instance (the Docker Compose files already ship a `neo4j` service).
|
||||
- The following environment variables so Django and Celery can connect:
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `ATTACK_PATHS_ENABLED` | Enable/disable Attack Paths and the Neo4j dependency. | `True` |
|
||||
| `NEO4J_HOST` | Hostname used by the API containers. | `neo4j` |
|
||||
| `NEO4J_PORT` | Bolt port exposed by Neo4j. | `7687` |
|
||||
| `NEO4J_USER` / `NEO4J_PASSWORD` | Credentials with rights to create per-tenant databases. | `neo4j` / `neo4j_password` |
|
||||
|
||||
@@ -13,6 +13,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
|
||||
- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992)
|
||||
- Attack Paths: Add full paths set from [pathfinding.cloud](https://pathfinding.cloud/) [(#10008)](https://github.com/prowler-cloud/prowler/pull/10008)
|
||||
- Attack Paths: allow disabling Attack Paths and Neo4j dependency via `ATTACK_PATHS_ENABLED` setting [(#10016)](https://github.com/prowler-cloud/prowler/pull/10016)
|
||||
- Support CSA CCM for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
- Support CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
|
||||
- Support CSA CCM for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
|
||||
|
||||
@@ -52,19 +52,23 @@ class ApiConfig(AppConfig):
|
||||
"check_and_fix_socialaccount_sites_migration",
|
||||
]
|
||||
|
||||
# Skip Neo4j initialization during tests, some Django commands, and Celery
|
||||
if getattr(settings, "TESTING", False) or (
|
||||
len(sys.argv) > 1
|
||||
and (
|
||||
(
|
||||
"manage.py" in sys.argv[0]
|
||||
and sys.argv[1] in SKIP_NEO4J_DJANGO_COMMANDS
|
||||
# Skip Neo4j initialization during tests, some Django commands, Celery, or when Attack Paths is disabled
|
||||
if (
|
||||
getattr(settings, "TESTING", False)
|
||||
or not getattr(settings, "ATTACK_PATHS_ENABLED", True)
|
||||
or (
|
||||
len(sys.argv) > 1
|
||||
and (
|
||||
(
|
||||
"manage.py" in sys.argv[0]
|
||||
and sys.argv[1] in SKIP_NEO4J_DJANGO_COMMANDS
|
||||
)
|
||||
or "celery" in sys.argv[0]
|
||||
)
|
||||
or "celery" in sys.argv[0]
|
||||
)
|
||||
):
|
||||
logger.info(
|
||||
"Skipping Neo4j initialization because tests, some Django commands or Celery"
|
||||
"Skipping Neo4j initialization because tests, Attack Paths disabled, some Django commands or Celery"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
@@ -2456,13 +2456,15 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
|
||||
attack_paths_scan.provider.uid,
|
||||
)
|
||||
|
||||
graph = attack_paths_views_helpers.execute_attack_paths_query(
|
||||
attack_paths_scan, query_definition, parameters
|
||||
)
|
||||
graph_database.clear_cache(attack_paths_scan.graph_database)
|
||||
graph = None
|
||||
if django_settings.ATTACK_PATHS_ENABLED:
|
||||
graph = attack_paths_views_helpers.execute_attack_paths_query(
|
||||
attack_paths_scan, query_definition, parameters
|
||||
)
|
||||
graph_database.clear_cache(attack_paths_scan.graph_database)
|
||||
|
||||
status_code = status.HTTP_200_OK
|
||||
if not graph.get("nodes"):
|
||||
if not graph or not graph.get("nodes"):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
|
||||
response_serializer = AttackPathsQueryResultSerializer(graph)
|
||||
|
||||
@@ -296,3 +296,6 @@ DJANGO_DELETION_BATCH_SIZE = env.int("DJANGO_DELETION_BATCH_SIZE", 5000)
|
||||
# SAML requirement
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
# Attack Paths
|
||||
ATTACK_PATHS_ENABLED = env.bool("ATTACK_PATHS_ENABLED", default=True)
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import Any
|
||||
|
||||
from cartography.config import Config as CartographyConfig
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
AttackPathsScan as ProwlerAPIAttackPathsScan,
|
||||
@@ -13,6 +15,9 @@ from tasks.jobs.attack_paths.config import is_provider_available
|
||||
|
||||
|
||||
def can_provider_run_attack_paths_scan(tenant_id: str, provider_id: int) -> bool:
|
||||
if not settings.ATTACK_PATHS_ENABLED:
|
||||
return False
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
prowler_api_provider = ProwlerAPIProvider.objects.get(id=provider_id)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
|
||||
from api.attack_paths import database as graph_database
|
||||
@@ -33,13 +34,13 @@ def delete_provider(tenant_id: str, pk: str):
|
||||
Provider.DoesNotExist: If no instance with the provided primary key exists.
|
||||
"""
|
||||
# Delete the Attack Paths' graph data related to the provider
|
||||
tenant_database_name = graph_database.get_database_name(tenant_id)
|
||||
try:
|
||||
graph_database.drop_subgraph(tenant_database_name, str(pk))
|
||||
|
||||
except graph_database.GraphDatabaseQueryException as gdb_error:
|
||||
logger.error(f"Error deleting Provider graph data: {gdb_error}")
|
||||
raise
|
||||
if settings.ATTACK_PATHS_ENABLED:
|
||||
tenant_database_name = graph_database.get_database_name(tenant_id)
|
||||
try:
|
||||
graph_database.drop_subgraph(tenant_database_name, str(pk))
|
||||
except graph_database.GraphDatabaseQueryException as gdb_error:
|
||||
logger.error(f"Error deleting Provider graph data: {gdb_error}")
|
||||
raise
|
||||
|
||||
# Get all provider related data and delete them in batches
|
||||
with rls_transaction(tenant_id):
|
||||
@@ -89,12 +90,13 @@ def delete_tenant(pk: str):
|
||||
summary = delete_provider(pk, provider.id)
|
||||
deletion_summary.update(summary)
|
||||
|
||||
try:
|
||||
tenant_database_name = graph_database.get_database_name(pk)
|
||||
graph_database.drop_database(tenant_database_name)
|
||||
except graph_database.GraphDatabaseQueryException as gdb_error:
|
||||
logger.error(f"Error dropping Tenant graph database: {gdb_error}")
|
||||
raise
|
||||
if settings.ATTACK_PATHS_ENABLED:
|
||||
try:
|
||||
tenant_database_name = graph_database.get_database_name(pk)
|
||||
graph_database.drop_database(tenant_database_name)
|
||||
except graph_database.GraphDatabaseQueryException as gdb_error:
|
||||
logger.error(f"Error dropping Tenant graph database: {gdb_error}")
|
||||
raise
|
||||
|
||||
Tenant.objects.using(MainRouter.admin_db).filter(id=pk).delete()
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from tasks.jobs.attack_paths import findings as findings_module
|
||||
from tasks.jobs.attack_paths import internet as internet_module
|
||||
from tasks.jobs.attack_paths.db_utils import can_provider_run_attack_paths_scan
|
||||
from tasks.jobs.attack_paths.scan import run as attack_paths_run
|
||||
|
||||
from api.models import (
|
||||
@@ -20,6 +22,17 @@ from api.models import (
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestCanProviderRunAttackPathsScan:
|
||||
@override_settings(ATTACK_PATHS_ENABLED=False)
|
||||
def test_returns_false_when_attack_paths_disabled(self, providers_fixture):
|
||||
provider = providers_fixture[0]
|
||||
result = can_provider_run_attack_paths_scan(
|
||||
str(provider.tenant_id), provider.id
|
||||
)
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAttackPathsRun:
|
||||
# Patching with decorators as we got a `SyntaxError: too many statically nested blocks` error if we use context managers
|
||||
|
||||
@@ -3,6 +3,7 @@ from unittest.mock import call, patch
|
||||
import pytest
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import override_settings
|
||||
|
||||
from api.models import Provider, Tenant
|
||||
from tasks.jobs.deletion import delete_provider, delete_tenant
|
||||
@@ -56,6 +57,29 @@ class TestDeleteProvider:
|
||||
non_existent_pk,
|
||||
)
|
||||
|
||||
@override_settings(ATTACK_PATHS_ENABLED=False)
|
||||
def test_delete_provider_skips_neo4j_when_attack_paths_disabled(
|
||||
self, providers_fixture
|
||||
):
|
||||
with (
|
||||
patch(
|
||||
"tasks.jobs.deletion.graph_database.get_database_name",
|
||||
) as mock_get_database_name,
|
||||
patch(
|
||||
"tasks.jobs.deletion.graph_database.drop_subgraph"
|
||||
) as mock_drop_subgraph,
|
||||
):
|
||||
instance = providers_fixture[0]
|
||||
tenant_id = str(instance.tenant_id)
|
||||
result = delete_provider(tenant_id, instance.id)
|
||||
|
||||
assert result
|
||||
with pytest.raises(ObjectDoesNotExist):
|
||||
Provider.objects.get(pk=instance.id)
|
||||
|
||||
mock_get_database_name.assert_not_called()
|
||||
mock_drop_subgraph.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestDeleteTenant:
|
||||
@@ -142,3 +166,31 @@ class TestDeleteTenant:
|
||||
mock_get_database_name.assert_called_once_with(tenant.id)
|
||||
mock_drop_subgraph.assert_not_called()
|
||||
mock_drop_database.assert_called_once_with("tenant-db")
|
||||
|
||||
@override_settings(ATTACK_PATHS_ENABLED=False)
|
||||
def test_delete_tenant_skips_neo4j_when_attack_paths_disabled(
|
||||
self, tenants_fixture, providers_fixture
|
||||
):
|
||||
with (
|
||||
patch(
|
||||
"tasks.jobs.deletion.graph_database.get_database_name",
|
||||
) as mock_get_database_name,
|
||||
patch(
|
||||
"tasks.jobs.deletion.graph_database.drop_subgraph"
|
||||
) as mock_drop_subgraph,
|
||||
patch(
|
||||
"tasks.jobs.deletion.graph_database.drop_database"
|
||||
) as mock_drop_database,
|
||||
):
|
||||
tenant = tenants_fixture[0]
|
||||
|
||||
assert Tenant.objects.filter(id=tenant.id).exists()
|
||||
|
||||
deletion_summary = delete_tenant(tenant.id)
|
||||
|
||||
assert deletion_summary is not None
|
||||
assert not Tenant.objects.filter(id=tenant.id).exists()
|
||||
|
||||
mock_get_database_name.assert_not_called()
|
||||
mock_drop_subgraph.assert_not_called()
|
||||
mock_drop_database.assert_not_called()
|
||||
|
||||
@@ -25,8 +25,6 @@ services:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "/home/prowler/docker-entrypoint.sh"
|
||||
- "dev"
|
||||
@@ -142,8 +140,6 @@ services:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "/home/prowler/docker-entrypoint.sh"
|
||||
- "worker"
|
||||
@@ -164,8 +160,6 @@ services:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "beat"
|
||||
|
||||
@@ -21,8 +21,6 @@ services:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "/home/prowler/docker-entrypoint.sh"
|
||||
- "prod"
|
||||
|
||||
Reference in New Issue
Block a user