From 6b8b1e1b0848a2384c21c85b90f65f2ef99a6867 Mon Sep 17 00:00:00 2001 From: Josema Camacho Date: Wed, 11 Feb 2026 12:23:33 +0100 Subject: [PATCH] feat(api): allow running the API and workers without Neo4j --- .env | 1 + README.md | 7 +++-- api/CHANGELOG.md | 1 + api/src/backend/api/apps.py | 22 ++++++++------- api/src/backend/api/v1/views.py | 12 +++++---- api/src/backend/config/django/base.py | 3 +++ .../tasks/jobs/attack_paths/db_utils.py | 5 ++++ api/src/backend/tasks/jobs/deletion.py | 27 ++++++++++--------- docker-compose-dev.yml | 6 ----- docker-compose.yml | 2 -- 10 files changed, 50 insertions(+), 36 deletions(-) diff --git a/.env b/.env index c5c1f34342..3ba3c66c3f 100644 --- a/.env +++ b/.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 diff --git a/README.md b/README.md index d583a33a15..b2eb90ed25 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 6f0cc8b484..c639b91dad 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -8,6 +8,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: allow disabling Attack Paths and Neo4j dependency via `ATTACK_PATHS_ENABLED` setting [(#)](https://github.com/prowler-cloud/prowler/pull/) --- diff --git a/api/src/backend/api/apps.py b/api/src/backend/api/apps.py index 543c10ab88..4a01e9aa49 100644 --- a/api/src/backend/api/apps.py +++ b/api/src/backend/api/apps.py @@ -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: diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 0c61622d06..cd59af7940 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -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) diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index c9e1b4750f..d05d03d7a5 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -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) diff --git a/api/src/backend/tasks/jobs/attack_paths/db_utils.py b/api/src/backend/tasks/jobs/attack_paths/db_utils.py index c9bbf5afdf..87b77ee63e 100644 --- a/api/src/backend/tasks/jobs/attack_paths/db_utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/db_utils.py @@ -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) diff --git a/api/src/backend/tasks/jobs/deletion.py b/api/src/backend/tasks/jobs/deletion.py index ba59eaeb5f..1ec7e45935 100644 --- a/api/src/backend/tasks/jobs/deletion.py +++ b/api/src/backend/tasks/jobs/deletion.py @@ -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,14 @@ 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)) + 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 + 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 +91,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() diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b5de1af83f..d1afe6b090 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index 992a753ac0..62d7b6872a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,6 @@ services: condition: service_healthy valkey: condition: service_healthy - neo4j: - condition: service_healthy entrypoint: - "/home/prowler/docker-entrypoint.sh" - "prod"