Compare commits

...

7 Commits

12 changed files with 115 additions and 37 deletions

1
.env
View File

@@ -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

View File

@@ -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` |

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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"

View File

@@ -21,8 +21,6 @@ services:
condition: service_healthy
valkey:
condition: service_healthy
neo4j:
condition: service_healthy
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "prod"