fix(attack-paths): clear Neo4j database cache after scan and queries (#9878)

Co-authored-by: Josema Camacho <josema@prowler.com>
This commit is contained in:
Prowler Bot
2026-01-23 16:26:05 +01:00
committed by GitHub
parent fcc106abe3
commit 09ee0698d4
7 changed files with 25 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Use `Findings.all_objects` to avoid the `ActiveProviderPartitionedManager` [(#9869)](https://github.com/prowler-cloud/prowler/pull/9869) - Use `Findings.all_objects` to avoid the `ActiveProviderPartitionedManager` [(#9869)](https://github.com/prowler-cloud/prowler/pull/9869)
- Lazy load Neo4j driver for workers only [(#9872)](https://github.com/prowler-cloud/prowler/pull/9872) - Lazy load Neo4j driver for workers only [(#9872)](https://github.com/prowler-cloud/prowler/pull/9872)
- Improve Cypher query for inserting Findings into Attack Paths scan graphs [(#9874)](https://github.com/prowler-cloud/prowler/pull/9874) - Improve Cypher query for inserting Findings into Attack Paths scan graphs [(#9874)](https://github.com/prowler-cloud/prowler/pull/9874)
- Clear Neo4j database cache after Attack Paths scan and each API query [(#9877)](https://github.com/prowler-cloud/prowler/pull/9877)
## [1.18.0] (Prowler v5.17.0) ## [1.18.0] (Prowler v5.17.0)

View File

@@ -125,6 +125,17 @@ def drop_subgraph(database: str, root_node_label: str, root_node_id: str) -> int
return 0 # As there are no nodes to delete, the result is empty return 0 # As there are no nodes to delete, the result is empty
def clear_cache(database: str) -> None:
query = "CALL db.clearQueryCaches()"
try:
with get_session(database) as session:
session.run(query)
except GraphDatabaseQueryException as exc:
logging.warning(f"Failed to clear query cache for database `{database}`: {exc}")
# Neo4j functions related to Prowler + Cartography # Neo4j functions related to Prowler + Cartography
DATABASE_NAME_TEMPLATE = "db-{attack_paths_scan_id}" DATABASE_NAME_TEMPLATE = "db-{attack_paths_scan_id}"

View File

@@ -64,9 +64,9 @@ class RetryableSession:
return method(*args, **kwargs) return method(*args, **kwargs)
except ( except (
neo4j.exceptions.ServiceUnavailable,
ConnectionResetError,
BrokenPipeError, BrokenPipeError,
ConnectionResetError,
neo4j.exceptions.ServiceUnavailable,
) as exc: # pragma: no cover - depends on infra ) as exc: # pragma: no cover - depends on infra
last_exc = exc last_exc = exc
attempt += 1 attempt += 1

View File

@@ -3867,6 +3867,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query", "api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value=graph_payload, return_value=graph_payload,
) as mock_execute, ) as mock_execute,
patch("api.v1.views.graph_database.clear_cache") as mock_clear_cache,
): ):
response = authenticated_client.post( response = authenticated_client.post(
reverse( reverse(
@@ -3889,6 +3890,7 @@ class TestAttackPathsScanViewSet:
query_definition, query_definition,
prepared_parameters, prepared_parameters,
) )
mock_clear_cache.assert_called_once_with(attack_paths_scan.graph_database)
result = response.json()["data"] result = response.json()["data"]
attributes = result["attributes"] attributes = result["attributes"]
assert attributes["nodes"] == graph_payload["nodes"] assert attributes["nodes"] == graph_payload["nodes"]
@@ -4000,6 +4002,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query", "api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value={"nodes": [], "relationships": []}, return_value={"nodes": [], "relationships": []},
), ),
patch("api.v1.views.graph_database.clear_cache"),
): ):
response = authenticated_client.post( response = authenticated_client.post(
reverse( reverse(

View File

@@ -77,6 +77,7 @@ from rest_framework_json_api.views import RelationshipView, Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from api.attack_paths import ( from api.attack_paths import (
database as graph_database,
get_queries_for_provider, get_queries_for_provider,
get_query_by_id, get_query_by_id,
views_helpers as attack_paths_views_helpers, views_helpers as attack_paths_views_helpers,
@@ -2435,6 +2436,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
graph = attack_paths_views_helpers.execute_attack_paths_query( graph = attack_paths_views_helpers.execute_attack_paths_query(
attack_paths_scan, query_definition, parameters attack_paths_scan, query_definition, parameters
) )
graph_database.clear_cache(attack_paths_scan.graph_database)
status_code = status.HTTP_200_OK status_code = status.HTTP_200_OK
if not graph.get("nodes"): if not graph.get("nodes"):

View File

@@ -137,6 +137,11 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
neo4j_session, prowler_api_provider, scan_id, cartography_config neo4j_session, prowler_api_provider, scan_id, cartography_config
) )
logger.info(
f"Clearing Neo4j cache for database {cartography_config.neo4j_database}"
)
graph_database.clear_cache(cartography_config.neo4j_database)
logger.info( logger.info(
f"Completed Cartography ({attack_paths_scan.id}) for " f"Completed Cartography ({attack_paths_scan.id}) for "
f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}" f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}"

View File

@@ -68,6 +68,7 @@ class TestAttackPathsRun:
"tasks.jobs.attack_paths.scan.graph_database.get_session", "tasks.jobs.attack_paths.scan.graph_database.get_session",
return_value=session_ctx, return_value=session_ctx,
) as mock_get_session, ) as mock_get_session,
patch("tasks.jobs.attack_paths.scan.graph_database.clear_cache"),
patch( patch(
"tasks.jobs.attack_paths.scan.cartography_create_indexes.run" "tasks.jobs.attack_paths.scan.cartography_create_indexes.run"
) as mock_cartography_indexes, ) as mock_cartography_indexes,